Merge remote-tracking branch 'origin/develop' into feat/add-message-edition-wysiwyg-composer
commit
de86221c72
|
@ -1 +1 @@
|
|||
14
|
||||
16
|
||||
|
|
|
@ -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", () => {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -60,6 +60,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
private recorderProcessor: ScriptProcessorNode;
|
||||
private recording = false;
|
||||
private observable: SimpleObservable<IRecordingUpdate>;
|
||||
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
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
};
|
||||
|
||||
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<IProps, IState> {
|
|||
};
|
||||
|
||||
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
|
||||
|
|
|
@ -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<IProps, IState> {
|
|||
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 });
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ComponentProps<typeof BaseAvatar>, "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<IProps, IState> {
|
|||
}
|
||||
|
||||
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<IProps, IState> {
|
|||
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<IProps, IState> {
|
|||
<BaseAvatar
|
||||
{...otherProps}
|
||||
className={classNames(className, {
|
||||
mx_RoomAvatar_isSpaceRoom: room?.isSpaceRoom(),
|
||||
mx_RoomAvatar_isSpaceRoom: (room?.getType() ?? this.props.oobData?.roomType) === RoomType.Space,
|
||||
})}
|
||||
name={roomName}
|
||||
idName={idName}
|
||||
|
|
|
@ -47,7 +47,7 @@ const ShareLatestLocation: React.FC<Props> = ({ latestLocationState }) => {
|
|||
return <>
|
||||
<TooltipTarget label={_t('Open in OpenStreetMap')}>
|
||||
<a
|
||||
data-test-id='open-location-in-osm'
|
||||
data-testid='open-location-in-osm'
|
||||
href={mapLink}
|
||||
target='_blank'
|
||||
rel='noreferrer noopener'
|
||||
|
|
|
@ -93,6 +93,7 @@ import { TooltipOption } from "./TooltipOption";
|
|||
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||
import { useSlidingSyncRoomSearch } from "../../../../hooks/useSlidingSyncRoomSearch";
|
||||
import { shouldShowFeedback } from "../../../../utils/Feedback";
|
||||
import RoomAvatar from "../../avatars/RoomAvatar";
|
||||
|
||||
const MAX_RECENT_SEARCHES = 10;
|
||||
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(),
|
||||
}, true, ev.type !== "click");
|
||||
};
|
||||
|
||||
return (
|
||||
<Option
|
||||
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-details={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_details`}
|
||||
>
|
||||
<BaseAvatar
|
||||
<RoomAvatar
|
||||
className="mx_SearchResultAvatar"
|
||||
url={result?.publicRoom?.avatar_url
|
||||
? mediaFromMxc(result?.publicRoom?.avatar_url).getSquareThumbnailHttp(AVATAR_SIZE)
|
||||
: null}
|
||||
name={result.publicRoom.name}
|
||||
idName={result.publicRoom.room_id}
|
||||
oobData={{
|
||||
roomId: result.publicRoom.room_id,
|
||||
name: result.publicRoom.name,
|
||||
avatarUrl: result.publicRoom.avatar_url,
|
||||
roomType: result.publicRoom.room_type,
|
||||
}}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,8 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
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 HeaderButton from './HeaderButton';
|
||||
|
@ -43,6 +44,7 @@ import { SummarizedNotificationState } from "../../../stores/notifications/Summa
|
|||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
|
||||
const ROOM_INFO_PHASES = [
|
||||
RightPanelPhases.RoomSummary,
|
||||
|
@ -136,32 +138,67 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
private threadNotificationState: ThreadsRoomNotificationState;
|
||||
private globalNotificationState: SummarizedNotificationState;
|
||||
|
||||
private get supportsThreadNotifications(): boolean {
|
||||
const client = MatrixClientPeg.get();
|
||||
return client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported;
|
||||
}
|
||||
|
||||
constructor(props: IProps) {
|
||||
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;
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
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);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
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);
|
||||
}
|
||||
|
||||
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?
|
||||
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 => {
|
||||
// XXX: why don't we read from this.state.globalNotificationCount in the render methods?
|
||||
this.globalNotificationState = notificationState;
|
||||
|
@ -255,12 +292,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
? <HeaderButton
|
||||
key={RightPanelPhases.ThreadPanel}
|
||||
name="threadsButton"
|
||||
data-testid="threadsButton"
|
||||
title={_t("Threads")}
|
||||
onClick={this.onThreadsPanelClicked}
|
||||
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>
|
||||
: 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 { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
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 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 { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||
import { ElementCall } from "../../../models/Call";
|
||||
import { UnreadNotificationBadge } from './NotificationBadge/UnreadNotificationBadge';
|
||||
|
||||
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
||||
|
||||
|
@ -113,7 +115,7 @@ export interface IEventTileType extends React.Component {
|
|||
getEventTileOps?(): IEventTileOps;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
export interface EventTileProps {
|
||||
// the MatrixEvent to show
|
||||
mxEvent: MatrixEvent;
|
||||
|
||||
|
@ -248,7 +250,7 @@ interface IState {
|
|||
}
|
||||
|
||||
// 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 isListeningForReceipts: boolean;
|
||||
private tile = React.createRef<IEventTileType>();
|
||||
|
@ -267,7 +269,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
static contextType = 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);
|
||||
|
||||
const thread = this.thread;
|
||||
|
@ -394,7 +396,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
if (SettingsStore.getValue("feature_thread")) {
|
||||
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
|
||||
|
||||
if (this.thread) {
|
||||
if (this.thread && !this.supportsThreadNotifications) {
|
||||
this.setupNotificationListener(this.thread);
|
||||
}
|
||||
}
|
||||
|
@ -405,33 +407,40 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
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 {
|
||||
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
|
||||
|
||||
this.threadState = notifications.getThreadRoomState(thread);
|
||||
|
||||
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
||||
this.onThreadStateUpdate();
|
||||
if (!this.supportsThreadNotifications) {
|
||||
const notifications = RoomNotificationStateStore.instance.getThreadsRoomState(thread.room);
|
||||
this.threadState = notifications.getThreadRoomState(thread);
|
||||
this.threadState.on(NotificationStateEvents.Update, this.onThreadStateUpdate);
|
||||
this.onThreadStateUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private onThreadStateUpdate = (): void => {
|
||||
let threadNotification = null;
|
||||
switch (this.threadState?.color) {
|
||||
case NotificationColor.Grey:
|
||||
threadNotification = NotificationCountType.Total;
|
||||
break;
|
||||
case NotificationColor.Red:
|
||||
threadNotification = NotificationCountType.Highlight;
|
||||
break;
|
||||
}
|
||||
if (!this.supportsThreadNotifications) {
|
||||
let threadNotification = null;
|
||||
switch (this.threadState?.color) {
|
||||
case NotificationColor.Grey:
|
||||
threadNotification = NotificationCountType.Total;
|
||||
break;
|
||||
case NotificationColor.Red:
|
||||
threadNotification = NotificationCountType.Highlight;
|
||||
break;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
threadNotification,
|
||||
});
|
||||
this.setState({
|
||||
threadNotification,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private updateThread = (thread: Thread) => {
|
||||
if (thread !== this.state.thread) {
|
||||
if (thread !== this.state.thread && !this.supportsThreadNotifications) {
|
||||
if (this.threadState) {
|
||||
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
|
||||
// eslint-disable-next-line
|
||||
UNSAFE_componentWillReceiveProps(nextProps: IProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps: EventTileProps) {
|
||||
// re-check the sender verification as outgoing events progress through
|
||||
// the send process.
|
||||
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)) {
|
||||
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 (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
||||
MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt);
|
||||
|
@ -667,7 +676,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
}, 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 keysB = Object.keys(objB);
|
||||
|
||||
|
@ -1348,6 +1357,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
]);
|
||||
}
|
||||
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
|
||||
return (
|
||||
React.createElement(this.props.as || "li", {
|
||||
|
@ -1361,7 +1371,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
"data-shape": this.context.timelineRenderingType,
|
||||
"data-self": isOwnEvent,
|
||||
"data-has-reply": !!replyChain,
|
||||
"data-notification": this.state.threadNotification,
|
||||
"data-notification": !this.supportsThreadNotifications
|
||||
? this.state.threadNotification
|
||||
: undefined,
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
"onClick": (ev: MouseEvent) => {
|
||||
|
@ -1409,6 +1421,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
</RovingAccessibleTooltipButton>
|
||||
</Toolbar>
|
||||
{ msgOption }
|
||||
<UnreadNotificationBadge
|
||||
room={room}
|
||||
threadId={this.props.mxEvent.getId()} />
|
||||
</>)
|
||||
);
|
||||
}
|
||||
|
@ -1512,7 +1527,7 @@ 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
|
||||
const SafeEventTile = forwardRef((props: IProps, ref: RefObject<UnwrappedEventTile>) => {
|
||||
const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<UnwrappedEventTile>) => {
|
||||
return <TileErrorBoundary mxEvent={props.mxEvent} layout={props.layout}>
|
||||
<UnwrappedEventTile ref={ref} {...props} />
|
||||
</TileErrorBoundary>;
|
||||
|
|
|
@ -175,14 +175,22 @@ const NewRoomIntro = () => {
|
|||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
body = <React.Fragment>
|
||||
<MiniAvatarUploader
|
||||
hasAvatar={!!avatarUrl}
|
||||
let avatar = (
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} viewAvatarOnClick={!!avatarUrl} />
|
||||
);
|
||||
|
||||
if (!avatarUrl) {
|
||||
avatar = <MiniAvatarUploader
|
||||
hasAvatar={false}
|
||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
||||
>
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} viewAvatarOnClick={true} />
|
||||
</MiniAvatarUploader>
|
||||
{ avatar }
|
||||
</MiniAvatarUploader>;
|
||||
}
|
||||
|
||||
body = <React.Fragment>
|
||||
{ avatar }
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
|
|
|
@ -15,16 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { MouseEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { formatCount } from "../../../utils/FormattingUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { XOR } from "../../../@types/common";
|
||||
import { NotificationState, NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { StatelessNotificationBadge } from "./NotificationBadge/StatelessNotificationBadge";
|
||||
|
||||
interface IProps {
|
||||
notification: NotificationState;
|
||||
|
@ -113,61 +111,25 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
|
||||
public render(): React.ReactElement {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { notification, showUnsentTooltip, forceCount, roomId, onClick, ...props } = this.props;
|
||||
const { notification, showUnsentTooltip, onClick } = this.props;
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
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) {
|
||||
isEmptyBadge = false;
|
||||
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||
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} />;
|
||||
}
|
||||
|
||||
let symbol = notification.symbol || formatCount(notification.count);
|
||||
if (isEmptyBadge) symbol = "";
|
||||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'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 (
|
||||
<div className={classes}>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
</div>
|
||||
);
|
||||
return <StatelessNotificationBadge
|
||||
label={label}
|
||||
symbol={notification.symbol}
|
||||
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: {
|
||||
email: this.props.invitedEmail,
|
||||
signurl: this.props.signUrl,
|
||||
room_name: this.props.oobData ? this.props.oobData.room_name : null,
|
||||
room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null,
|
||||
inviter_name: this.props.oobData ? this.props.oobData.inviterName : null,
|
||||
room_name: this.props.oobData?.name ?? null,
|
||||
room_avatar_url: this.props.oobData?.avatarUrl ?? null,
|
||||
inviter_name: this.props.oobData?.inviterName ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -56,7 +56,7 @@ export interface IOOBData {
|
|||
inviterName?: string; // The display name of the person who invited us to the room
|
||||
// eslint-disable-next-line camelcase
|
||||
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_";
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
|
||||
import { NotificationColor } from "./NotificationColor";
|
||||
import { IDestroyable } from "../../utils/IDestroyable";
|
||||
|
@ -32,15 +33,16 @@ import { ThreadsRoomNotificationState } from "./ThreadsRoomNotificationState";
|
|||
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||
constructor(public readonly room: Room, private readonly threadsState?: ThreadsRoomNotificationState) {
|
||||
super();
|
||||
this.room.on(RoomEvent.Receipt, this.handleReadReceipt); // for unread indicators
|
||||
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate); // for redness on invites
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated); // for redness on unsent messages
|
||||
const cli = this.room.client;
|
||||
this.room.on(RoomEvent.Receipt, this.handleReadReceipt);
|
||||
this.room.on(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
||||
this.room.on(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||
this.room.on(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate); // for server-sent counts
|
||||
if (threadsState) {
|
||||
threadsState.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
||||
if (cli.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
||||
this.threadsState?.on(NotificationStateEvents.Update, this.handleThreadsUpdate);
|
||||
}
|
||||
MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); // for local count calculation
|
||||
MatrixClientPeg.get().on(ClientEvent.AccountData, this.handleAccountDataUpdate); // for push rules
|
||||
cli.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
cli.on(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||
this.updateNotificationState();
|
||||
}
|
||||
|
||||
|
@ -50,17 +52,17 @@ export class RoomNotificationState extends NotificationState implements IDestroy
|
|||
|
||||
public destroy(): void {
|
||||
super.destroy();
|
||||
const cli = this.room.client;
|
||||
this.room.removeListener(RoomEvent.Receipt, this.handleReadReceipt);
|
||||
this.room.removeListener(RoomEvent.MyMembership, this.handleMembershipUpdate);
|
||||
this.room.removeListener(RoomEvent.LocalEchoUpdated, this.handleLocalEchoUpdated);
|
||||
this.room.removeListener(RoomEvent.UnreadNotifications, this.handleNotificationCountUpdate);
|
||||
if (this.threadsState) {
|
||||
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);
|
||||
}
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||
}
|
||||
cli.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
cli.removeListener(ClientEvent.AccountData, this.handleAccountDataUpdate);
|
||||
}
|
||||
|
||||
private handleThreadsUpdate = () => {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
|
@ -39,9 +40,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
instance.start();
|
||||
return instance;
|
||||
})();
|
||||
|
||||
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 _globalState = new SummarizedNotificationState();
|
||||
|
||||
|
@ -86,18 +87,25 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
*/
|
||||
public getRoomState(room: Room): RoomNotificationState {
|
||||
if (!this.roomMap.has(room)) {
|
||||
// Not very elegant, but that way we ensure that we start tracking
|
||||
// threads notification at the same time at rooms.
|
||||
// There are multiple entry points, and it's unclear which one gets
|
||||
// called first
|
||||
const threadState = new ThreadsRoomNotificationState(room);
|
||||
this.roomThreadsMap.set(room, threadState);
|
||||
let threadState;
|
||||
if (room.client.canSupport.get(Feature.ThreadUnreadNotifications) === ServerSupport.Unsupported) {
|
||||
// Not very elegant, but that way we ensure that we start tracking
|
||||
// threads notification at the same time at rooms.
|
||||
// There are multiple entry points, and it's unclear which one gets
|
||||
// called first
|
||||
const threadState = new ThreadsRoomNotificationState(room);
|
||||
this.roomThreadsMap.set(room, threadState);
|
||||
}
|
||||
this.roomMap.set(room, new RoomNotificationState(room, threadState));
|
||||
}
|
||||
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)) {
|
||||
this.roomThreadsMap.set(room, new ThreadsRoomNotificationState(room));
|
||||
}
|
||||
|
|
|
@ -57,8 +57,8 @@ export function messageForResourceLimitError(
|
|||
}
|
||||
}
|
||||
|
||||
export function messageForSyncError(err: MatrixError): ReactNode {
|
||||
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
export function messageForSyncError(err: Error): ReactNode {
|
||||
if (err instanceof MatrixError && err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const limitError = messageForResourceLimitError(
|
||||
err.data.limit_type,
|
||||
err.data.admin_contact,
|
||||
|
|
|
@ -139,5 +139,7 @@ export class VoiceBroadcastRecorder
|
|||
}
|
||||
|
||||
export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => {
|
||||
return new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength());
|
||||
const voiceRecording = new VoiceRecording();
|
||||
voiceRecording.disableMaxLength();
|
||||
return new VoiceBroadcastRecorder(voiceRecording, getChunkLength());
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { Room, RoomMember, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { avatarUrlForRoom } from "../src/Avatar";
|
||||
import { Media, mediaFromMxc } from "../src/customisations/Media";
|
||||
|
@ -46,6 +46,7 @@ describe("avatarUrlForRoom", () => {
|
|||
roomId,
|
||||
getMxcAvatarUrl: jest.fn(),
|
||||
isSpaceRoom: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
getAvatarFallbackMember: jest.fn(),
|
||||
} as unknown as Room;
|
||||
dmRoomMap = {
|
||||
|
@ -70,6 +71,7 @@ describe("avatarUrlForRoom", () => {
|
|||
|
||||
it("should return null for a space room", () => {
|
||||
mocked(room.isSpaceRoom).mockReturnValue(true);
|
||||
mocked(room.getType).mockReturnValue(RoomType.Space);
|
||||
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||
});
|
||||
|
||||
|
|
|
@ -16,10 +16,15 @@ limitations under the License.
|
|||
|
||||
import { mocked } from 'jest-mock';
|
||||
import { ConditionKind, PushRuleActionName, TweakName } from "matrix-js-sdk/src/@types/PushRules";
|
||||
import { NotificationCountType, Room } from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
import { stubClient } from "./test-utils";
|
||||
import { mkEvent, stubClient } from "./test-utils";
|
||||
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||
import { getRoomNotifsState, RoomNotifState } from "../src/RoomNotifs";
|
||||
import {
|
||||
getRoomNotifsState,
|
||||
RoomNotifState,
|
||||
getUnreadNotificationCount,
|
||||
} from "../src/RoomNotifs";
|
||||
|
||||
describe("RoomNotifs test", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -83,4 +88,74 @@ describe("RoomNotifs test", () => {
|
|||
});
|
||||
expect(getRoomNotifsState("!roomId:server")).toBe(RoomNotifState.AllMessagesLoud);
|
||||
});
|
||||
|
||||
describe("getUnreadNotificationCount", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
const THREAD_ID = "$threadId";
|
||||
|
||||
let cli;
|
||||
let room: Room;
|
||||
beforeEach(() => {
|
||||
cli = MatrixClientPeg.get();
|
||||
room = new Room(ROOM_ID, cli, cli.getUserId());
|
||||
});
|
||||
|
||||
it("counts room notification type", () => {
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(0);
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(0);
|
||||
});
|
||||
|
||||
it("counts notifications type", () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 2);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(2);
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(1);
|
||||
});
|
||||
|
||||
it("counts predecessor highlight", () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 2);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||
|
||||
const OLD_ROOM_ID = "!oldRoomId:example.org";
|
||||
const oldRoom = new Room(OLD_ROOM_ID, cli, cli.getUserId());
|
||||
oldRoom.setUnreadNotificationCount(NotificationCountType.Total, 10);
|
||||
oldRoom.setUnreadNotificationCount(NotificationCountType.Highlight, 6);
|
||||
|
||||
cli.getRoom.mockReset().mockReturnValue(oldRoom);
|
||||
|
||||
const predecessorEvent = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.create",
|
||||
room: ROOM_ID,
|
||||
user: cli.getUserId(),
|
||||
content: {
|
||||
creator: cli.getUserId(),
|
||||
room_version: "5",
|
||||
predecessor: {
|
||||
room_id: OLD_ROOM_ID,
|
||||
event_id: "$someevent",
|
||||
},
|
||||
},
|
||||
ts: Date.now(),
|
||||
});
|
||||
room.addLiveEvents([predecessorEvent]);
|
||||
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Total)).toBe(8);
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight)).toBe(7);
|
||||
});
|
||||
|
||||
it("counts thread notification type", () => {
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Total, THREAD_ID)).toBe(0);
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(0);
|
||||
});
|
||||
|
||||
it("counts notifications type", () => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 2);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1);
|
||||
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Total, THREAD_ID)).toBe(2);
|
||||
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, THREAD_ID)).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
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 { VoiceRecording } from "../../src/audio/VoiceRecording";
|
||||
|
||||
/**
|
||||
* The tests here are heavily using access to private props.
|
||||
* While this is not so great, we can at lest test some behaviour easily this way.
|
||||
*/
|
||||
describe("VoiceRecording", () => {
|
||||
let recording: VoiceRecording;
|
||||
let recorderSecondsSpy: jest.SpyInstance;
|
||||
|
||||
const itShouldNotCallStop = () => {
|
||||
it("should not call stop", () => {
|
||||
expect(recording.stop).not.toHaveBeenCalled();
|
||||
});
|
||||
};
|
||||
|
||||
const simulateUpdate = (recorderSeconds: number) => {
|
||||
beforeEach(() => {
|
||||
recorderSecondsSpy.mockReturnValue(recorderSeconds);
|
||||
// @ts-ignore
|
||||
recording.processAudioUpdate(recorderSeconds);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
recording = new VoiceRecording();
|
||||
// @ts-ignore
|
||||
recording.observable = {
|
||||
update: jest.fn(),
|
||||
};
|
||||
jest.spyOn(recording, "stop").mockImplementation();
|
||||
recorderSecondsSpy = jest.spyOn(recording, "recorderSeconds", "get");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("when recording", () => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
recording.recording = true;
|
||||
});
|
||||
|
||||
describe("and there is an audio update and time left", () => {
|
||||
simulateUpdate(42);
|
||||
itShouldNotCallStop();
|
||||
});
|
||||
|
||||
describe("and there is an audio update and time is up", () => {
|
||||
// one second above the limit
|
||||
simulateUpdate(901);
|
||||
|
||||
it("should call stop", () => {
|
||||
expect(recording.stop).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the max length limit has been disabled", () => {
|
||||
beforeEach(() => {
|
||||
recording.disableMaxLength();
|
||||
});
|
||||
|
||||
describe("and there is an audio update and time left", () => {
|
||||
simulateUpdate(42);
|
||||
itShouldNotCallStop();
|
||||
});
|
||||
|
||||
describe("and there is an audio update and time is up", () => {
|
||||
// one second above the limit
|
||||
simulateUpdate(901);
|
||||
itShouldNotCallStop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when not recording", () => {
|
||||
describe("and there is an audio update and time left", () => {
|
||||
simulateUpdate(42);
|
||||
itShouldNotCallStop();
|
||||
});
|
||||
|
||||
describe("and there is an audio update and time is up", () => {
|
||||
// one second above the limit
|
||||
simulateUpdate(901);
|
||||
itShouldNotCallStop();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
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, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { getUnsentMessages } from "../../../src/components/structures/RoomStatusBar";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { mkEvent, stubClient } from "../../test-utils/test-utils";
|
||||
import { mkThread } from "../../test-utils/threads";
|
||||
|
||||
describe("RoomStatusBar", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
let event: MatrixEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
event = mkEvent({
|
||||
event: true,
|
||||
type: "m.room.message",
|
||||
user: "@user1:server",
|
||||
room: "!room1:server",
|
||||
content: {},
|
||||
});
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
});
|
||||
|
||||
describe("getUnsentMessages", () => {
|
||||
it("returns no unsent messages", () => {
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("checks the event status", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(1);
|
||||
event.status = EventStatus.SENT;
|
||||
|
||||
expect(getUnsentMessages(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("only returns events related to a thread", () => {
|
||||
room.addPendingEvent(event, "123");
|
||||
|
||||
const { rootEvent, events } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
length: 2,
|
||||
});
|
||||
rootEvent.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(rootEvent, rootEvent.getId());
|
||||
for (const event of events) {
|
||||
event.status = EventStatus.NOT_SENT;
|
||||
room.addPendingEvent(event, Date.now() + Math.random() + "");
|
||||
}
|
||||
|
||||
const pendingEvents = getUnsentMessages(room, rootEvent.getId());
|
||||
|
||||
expect(pendingEvents[0].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[1].threadRootId).toBe(rootEvent.getId());
|
||||
expect(pendingEvents[2].threadRootId).toBe(rootEvent.getId());
|
||||
|
||||
// Filters out the non thread events
|
||||
expect(pendingEvents.every(ev => ev.getId() !== event.getId())).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,8 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import {
|
||||
Beacon,
|
||||
RoomMember,
|
||||
|
@ -28,7 +27,6 @@ import { act } from 'react-dom/test-utils';
|
|||
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||
import {
|
||||
findByTestId,
|
||||
getMockClientWithEventEmitter,
|
||||
makeBeaconEvent,
|
||||
makeBeaconInfoEvent,
|
||||
|
@ -76,11 +74,9 @@ describe('<BeaconListItem />', () => {
|
|||
beacon: new Beacon(aliceBeaconEvent),
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) =>
|
||||
mount(<BeaconListItem {...defaultProps} {...props} />, {
|
||||
wrappingComponent: MatrixClientContext.Provider,
|
||||
wrappingComponentProps: { value: mockClient },
|
||||
});
|
||||
const getComponent = (props = {}) => render(<MatrixClientContext.Provider value={mockClient}>
|
||||
<BeaconListItem {...defaultProps} {...props} />
|
||||
</MatrixClientContext.Provider>);
|
||||
|
||||
const setupRoomWithBeacons = (beaconInfoEvents: MatrixEvent[], locationEvents?: MatrixEvent[]): Beacon[] => {
|
||||
const beacons = makeRoomWithBeacons(roomId, mockClient, beaconInfoEvents, locationEvents);
|
||||
|
@ -104,71 +100,72 @@ describe('<BeaconListItem />', () => {
|
|||
{ isLive: false },
|
||||
);
|
||||
const [beacon] = setupRoomWithBeacons([notLiveBeacon]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.html()).toBeNull();
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders null when beacon has no location', () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.html()).toBeNull();
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('when a beacon is live and has locations', () => {
|
||||
it('renders beacon info', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.html()).toMatchSnapshot();
|
||||
const { asFragment } = getComponent({ beacon });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('non-self beacons', () => {
|
||||
it('uses beacon description as beacon name', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.find('BeaconStatus').props().label).toEqual("Alice's car");
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector('.mx_BeaconStatus_label')).toHaveTextContent("Alice's car");
|
||||
});
|
||||
|
||||
it('uses beacon owner mxid as beacon name for a beacon without description', () => {
|
||||
const [beacon] = setupRoomWithBeacons([pinBeaconWithoutDescription], [aliceLocation1]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.find('BeaconStatus').props().label).toEqual(aliceId);
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector('.mx_BeaconStatus_label')).toHaveTextContent(aliceId);
|
||||
});
|
||||
|
||||
it('renders location icon', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.find('StyledLiveBeaconIcon').length).toBeTruthy();
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector('.mx_StyledLiveBeaconIcon')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('self locations', () => {
|
||||
it('renders beacon owner avatar', () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.find('MemberAvatar').length).toBeTruthy();
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector('.mx_BaseAvatar')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('uses beacon owner name as beacon name', () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation1]);
|
||||
const component = getComponent({ beacon });
|
||||
expect(component.find('BeaconStatus').props().label).toEqual('Alice');
|
||||
const { container } = getComponent({ beacon });
|
||||
expect(container.querySelector('.mx_BeaconStatus_label')).toHaveTextContent("Alice");
|
||||
});
|
||||
});
|
||||
|
||||
describe('on location updates', () => {
|
||||
it('updates last updated time on location updated', () => {
|
||||
const [beacon] = setupRoomWithBeacons([aliceBeaconEvent], [aliceLocation2]);
|
||||
const component = getComponent({ beacon });
|
||||
const { container } = getComponent({ beacon });
|
||||
|
||||
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated 9 minutes ago');
|
||||
expect(container.querySelector('.mx_BeaconListItem_lastUpdated'))
|
||||
.toHaveTextContent('Updated 9 minutes ago');
|
||||
|
||||
// update to a newer location
|
||||
act(() => {
|
||||
beacon.addLocations([aliceLocation1]);
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago');
|
||||
expect(container.querySelector('.mx_BeaconListItem_lastUpdated'))
|
||||
.toHaveTextContent('Updated a few seconds ago');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -176,23 +173,19 @@ describe('<BeaconListItem />', () => {
|
|||
it('does not call onClick handler when clicking share button', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const onClick = jest.fn();
|
||||
const component = getComponent({ beacon, onClick });
|
||||
const { getByTestId } = getComponent({ beacon, onClick });
|
||||
|
||||
act(() => {
|
||||
findByTestId(component, 'open-location-in-osm').at(0).simulate('click');
|
||||
});
|
||||
fireEvent.click(getByTestId('open-location-in-osm'));
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClick handler when clicking outside of share buttons', () => {
|
||||
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||
const onClick = jest.fn();
|
||||
const component = getComponent({ beacon, onClick });
|
||||
const { container } = getComponent({ beacon, onClick });
|
||||
|
||||
act(() => {
|
||||
// click the beacon name
|
||||
component.find('.mx_BeaconStatus_description').simulate('click');
|
||||
});
|
||||
// click the beacon name
|
||||
fireEvent.click(container.querySelector(".mx_BeaconStatus_description"));
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,8 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import { mocked } from 'jest-mock';
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from 'enzyme';
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Beacon, BeaconIdentifier } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
|
@ -48,9 +47,7 @@ jest.mock('../../../../src/stores/OwnBeaconStore', () => {
|
|||
);
|
||||
|
||||
describe('<LeftPanelLiveShareWarning />', () => {
|
||||
const defaultProps = {};
|
||||
const getComponent = (props = {}) =>
|
||||
mount(<LeftPanelLiveShareWarning {...defaultProps} {...props} />);
|
||||
const getComponent = (props = {}) => render(<LeftPanelLiveShareWarning {...props} />);
|
||||
|
||||
const roomId1 = '!room1:server';
|
||||
const roomId2 = '!room2:server';
|
||||
|
@ -85,8 +82,8 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
));
|
||||
|
||||
it('renders nothing when user has no live beacons', () => {
|
||||
const component = getComponent();
|
||||
expect(component.html()).toBe(null);
|
||||
const { container } = getComponent();
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('when user has live location monitor', () => {
|
||||
|
@ -110,17 +107,15 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
});
|
||||
|
||||
it('renders correctly when not minimized', () => {
|
||||
const component = getComponent();
|
||||
expect(component).toMatchSnapshot();
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('goes to room of latest beacon when clicked', () => {
|
||||
const component = getComponent();
|
||||
const { container } = getComponent();
|
||||
const dispatchSpy = jest.spyOn(dispatcher, 'dispatch');
|
||||
|
||||
act(() => {
|
||||
component.simulate('click');
|
||||
});
|
||||
fireEvent.click(container.querySelector("[role=button]"));
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -134,28 +129,26 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
});
|
||||
|
||||
it('renders correctly when minimized', () => {
|
||||
const component = getComponent({ isMinimized: true });
|
||||
expect(component).toMatchSnapshot();
|
||||
const { asFragment } = getComponent({ isMinimized: true });
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders location publish error', () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue(
|
||||
[beacon1.identifier],
|
||||
);
|
||||
const component = getComponent();
|
||||
expect(component).toMatchSnapshot();
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('goes to room of latest beacon with location publish error when clicked', () => {
|
||||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue(
|
||||
[beacon1.identifier],
|
||||
);
|
||||
const component = getComponent();
|
||||
const { container } = getComponent();
|
||||
const dispatchSpy = jest.spyOn(dispatcher, 'dispatch');
|
||||
|
||||
act(() => {
|
||||
component.simulate('click');
|
||||
});
|
||||
fireEvent.click(container.querySelector("[role=button]"));
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -172,9 +165,9 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
mocked(OwnBeaconStore.instance).getLiveBeaconIdsWithLocationPublishError.mockReturnValue(
|
||||
[beacon1.identifier],
|
||||
);
|
||||
const component = getComponent();
|
||||
const { container, rerender } = getComponent();
|
||||
// error mode
|
||||
expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual(
|
||||
expect(container.querySelector('.mx_LeftPanelLiveShareWarning').textContent).toEqual(
|
||||
'An error occurred whilst sharing your live location',
|
||||
);
|
||||
|
||||
|
@ -183,18 +176,18 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.LocationPublishError, 'abc');
|
||||
});
|
||||
|
||||
component.setProps({});
|
||||
rerender(<LeftPanelLiveShareWarning />);
|
||||
|
||||
// default mode
|
||||
expect(component.find('.mx_LeftPanelLiveShareWarning').at(0).text()).toEqual(
|
||||
expect(container.querySelector('.mx_LeftPanelLiveShareWarning').textContent).toEqual(
|
||||
'You are sharing your live location',
|
||||
);
|
||||
});
|
||||
|
||||
it('removes itself when user stops having live beacons', async () => {
|
||||
const component = getComponent({ isMinimized: true });
|
||||
const { container, rerender } = getComponent({ isMinimized: true });
|
||||
// started out rendered
|
||||
expect(component.html()).toBeTruthy();
|
||||
expect(container.innerHTML).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
|
||||
|
@ -202,9 +195,9 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
});
|
||||
|
||||
await flushPromises();
|
||||
component.setProps({});
|
||||
rerender(<LeftPanelLiveShareWarning />);
|
||||
|
||||
expect(component.html()).toBe(null);
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
it('refreshes beacon liveness monitors when pagevisibilty changes to visible', () => {
|
||||
|
@ -228,21 +221,21 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
describe('stopping errors', () => {
|
||||
it('renders stopping error', () => {
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error('error'));
|
||||
const component = getComponent();
|
||||
expect(component.text()).toEqual('An error occurred while stopping your live location');
|
||||
const { container } = getComponent();
|
||||
expect(container.textContent).toEqual('An error occurred while stopping your live location');
|
||||
});
|
||||
|
||||
it('starts rendering stopping error on beaconUpdateError emit', () => {
|
||||
const component = getComponent();
|
||||
const { container } = getComponent();
|
||||
// no error
|
||||
expect(component.text()).toEqual('You are sharing your live location');
|
||||
expect(container.textContent).toEqual('You are sharing your live location');
|
||||
|
||||
act(() => {
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error('error'));
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.BeaconUpdateError, beacon2.identifier, true);
|
||||
});
|
||||
|
||||
expect(component.text()).toEqual('An error occurred while stopping your live location');
|
||||
expect(container.textContent).toEqual('An error occurred while stopping your live location');
|
||||
});
|
||||
|
||||
it('renders stopping error when beacons have stopping and location errors', () => {
|
||||
|
@ -250,8 +243,8 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
[beacon1.identifier],
|
||||
);
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error('error'));
|
||||
const component = getComponent();
|
||||
expect(component.text()).toEqual('An error occurred while stopping your live location');
|
||||
const { container } = getComponent();
|
||||
expect(container.textContent).toEqual('An error occurred while stopping your live location');
|
||||
});
|
||||
|
||||
it('goes to room of latest beacon with stopping error when clicked', () => {
|
||||
|
@ -259,12 +252,10 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
[beacon1.identifier],
|
||||
);
|
||||
OwnBeaconStore.instance.beaconUpdateErrors.set(beacon2.identifier, new Error('error'));
|
||||
const component = getComponent();
|
||||
const { container } = getComponent();
|
||||
const dispatchSpy = jest.spyOn(dispatcher, 'dispatch');
|
||||
|
||||
act(() => {
|
||||
component.simulate('click');
|
||||
});
|
||||
fireEvent.click(container.querySelector("[role=button]"));
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
|
|
|
@ -15,9 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
|
||||
import ShareLatestLocation from '../../../../src/components/views/beacon/ShareLatestLocation';
|
||||
import { copyPlaintext } from '../../../../src/utils/strings';
|
||||
|
@ -34,26 +32,23 @@ describe('<ShareLatestLocation />', () => {
|
|||
timestamp: 123,
|
||||
},
|
||||
};
|
||||
const getComponent = (props = {}) =>
|
||||
mount(<ShareLatestLocation {...defaultProps} {...props} />);
|
||||
const getComponent = (props = {}) => render(<ShareLatestLocation {...defaultProps} {...props} />);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders null when no location', () => {
|
||||
const component = getComponent({ latestLocationState: undefined });
|
||||
expect(component.html()).toBeNull();
|
||||
const { container } = getComponent({ latestLocationState: undefined });
|
||||
expect(container.innerHTML).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders share buttons when there is a location', async () => {
|
||||
const component = getComponent();
|
||||
expect(component).toMatchSnapshot();
|
||||
const { container, asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
|
||||
await act(async () => {
|
||||
component.find('.mx_CopyableText_copyButton').at(0).simulate('click');
|
||||
await flushPromises();
|
||||
});
|
||||
fireEvent.click(container.querySelector('.mx_CopyableText_copyButton'));
|
||||
await flushPromises();
|
||||
|
||||
expect(copyPlaintext).toHaveBeenCalledWith('51,42');
|
||||
});
|
||||
|
|
|
@ -15,18 +15,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from 'enzyme';
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
import StyledLiveBeaconIcon from '../../../../src/components/views/beacon/StyledLiveBeaconIcon';
|
||||
|
||||
describe('<StyledLiveBeaconIcon />', () => {
|
||||
const defaultProps = {};
|
||||
const getComponent = (props = {}) =>
|
||||
mount(<StyledLiveBeaconIcon {...defaultProps} {...props} />);
|
||||
const getComponent = (props = {}) => render(<StyledLiveBeaconIcon {...defaultProps} {...props} />);
|
||||
|
||||
it('renders', () => {
|
||||
const component = getComponent();
|
||||
expect(component).toBeTruthy();
|
||||
const { asFragment } = getComponent();
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,68 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div class=\\"mx_BeaconListItem_interactions\\"><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `
|
||||
<DocumentFragment>
|
||||
<li
|
||||
class="mx_BeaconListItem"
|
||||
>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon"
|
||||
/>
|
||||
<div
|
||||
class="mx_BeaconListItem_info"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
|
||||
>
|
||||
<div
|
||||
class="mx_BeaconStatus_description"
|
||||
>
|
||||
<span
|
||||
class="mx_BeaconStatus_label"
|
||||
>
|
||||
Alice's car
|
||||
</span>
|
||||
<span
|
||||
class="mx_BeaconStatus_expiryTime"
|
||||
>
|
||||
Live until 16:04
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_BeaconListItem_interactions"
|
||||
>
|
||||
<div
|
||||
tabindex="0"
|
||||
>
|
||||
<a
|
||||
data-testid="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="mx_BeaconListItem_lastUpdated"
|
||||
>
|
||||
Updated a few seconds ago
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -75,7 +75,7 @@ exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
|||
tabindex="0"
|
||||
>
|
||||
<a
|
||||
data-test-id="open-location-in-osm"
|
||||
data-testid="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
|
|
|
@ -1,76 +1,40 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when minimized 1`] = `
|
||||
<LeftPanelLiveShareWarning
|
||||
isMinimized={true}
|
||||
>
|
||||
<AccessibleButton
|
||||
className="mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tabindex="0"
|
||||
title="You are sharing your live location"
|
||||
>
|
||||
<div
|
||||
className="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__minimized"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="You are sharing your live location"
|
||||
>
|
||||
<div
|
||||
height={10}
|
||||
/>
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</LeftPanelLiveShareWarning>
|
||||
height="10"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
|
||||
<LeftPanelLiveShareWarning>
|
||||
<AccessibleButton
|
||||
className="mx_LeftPanelLiveShareWarning"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
className="mx_AccessibleButton mx_LeftPanelLiveShareWarning"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
You are sharing your live location
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</LeftPanelLiveShareWarning>
|
||||
You are sharing your live location
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders location publish error 1`] = `
|
||||
<LeftPanelLiveShareWarning>
|
||||
<AccessibleButton
|
||||
className="mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
|
||||
element="div"
|
||||
onClick={[Function]}
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
className="mx_AccessibleButton mx_LeftPanelLiveShareWarning mx_LeftPanelLiveShareWarning__error"
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
An error occurred whilst sharing your live location
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
</LeftPanelLiveShareWarning>
|
||||
An error occurred whilst sharing your live location
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -1,79 +1,30 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ShareLatestLocation /> renders share buttons when there is a location 1`] = `
|
||||
<ShareLatestLocation
|
||||
latestLocationState={
|
||||
Object {
|
||||
"timestamp": 123,
|
||||
"uri": "geo:51,42;u=35",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TooltipTarget
|
||||
label="Open in OpenStreetMap"
|
||||
<DocumentFragment>
|
||||
<div
|
||||
tabindex="0"
|
||||
>
|
||||
<a
|
||||
data-testid="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=42#map=16/51/42"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<div
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseMove={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<a
|
||||
data-test-id="open-location-in-osm"
|
||||
href="https://www.openstreetmap.org/?mlat=51&mlon=42#map=16/51/42"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
className="mx_ShareLatestLocation_icon"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</TooltipTarget>
|
||||
<CopyableText
|
||||
border={false}
|
||||
className="mx_ShareLatestLocation_copy"
|
||||
getTextToCopy={[Function]}
|
||||
>
|
||||
<div
|
||||
className="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||
>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CopyableText_copyButton"
|
||||
onClick={[Function]}
|
||||
onHideTooltip={[Function]}
|
||||
title="Copy"
|
||||
>
|
||||
<AccessibleButton
|
||||
aria-label="Copy"
|
||||
className="mx_CopyableText_copyButton"
|
||||
element="div"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
aria-label="Copy"
|
||||
className="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
</CopyableText>
|
||||
</ShareLatestLocation>
|
||||
aria-label="Copy"
|
||||
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<StyledLiveBeaconIcon /> renders 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_StyledLiveBeaconIcon"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -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((
|
||||
<InviteDialog
|
||||
|
|
|
@ -15,9 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
|
||||
import StyledRadioGroup from "../../../../src/components/views/elements/StyledRadioGroup";
|
||||
|
||||
|
@ -44,16 +42,16 @@ describe('<StyledRadioGroup />', () => {
|
|||
definitions: defaultDefinitions,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => mount(<StyledRadioGroup {...defaultProps} {...props} />);
|
||||
const getComponent = (props = {}) => render(<StyledRadioGroup {...defaultProps} {...props} />);
|
||||
|
||||
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('<StyledRadioGroup />', () => {
|
|||
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('<StyledRadioGroup />', () => {
|
|||
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('<StyledRadioGroup />', () => {
|
|||
{ ...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('<StyledRadioGroup />', () => {
|
|||
onChange,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
getInputByValue(component, optionB.value).simulate('change');
|
||||
});
|
||||
fireEvent.click(getInputByValue(component, optionB.value));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(optionB.value);
|
||||
});
|
||||
|
|
|
@ -1,152 +1,83 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<StyledRadioGroup /> renders radios correctly when no value is provided 1`] = `
|
||||
<StyledRadioGroup
|
||||
className="test-class"
|
||||
definitions={
|
||||
Array [
|
||||
Object {
|
||||
"className": "a-class",
|
||||
"description": "anteater description",
|
||||
"label": <span>
|
||||
Anteater label
|
||||
</span>,
|
||||
"value": "Anteater",
|
||||
},
|
||||
Object {
|
||||
"label": <span>
|
||||
Badger label
|
||||
</span>,
|
||||
"value": "Badger",
|
||||
},
|
||||
Object {
|
||||
"description": <span>
|
||||
Canary description
|
||||
</span>,
|
||||
"label": <span>
|
||||
Canary label
|
||||
</span>,
|
||||
"value": "Canary",
|
||||
},
|
||||
]
|
||||
}
|
||||
name="test"
|
||||
onChange={[MockFunction]}
|
||||
>
|
||||
<StyledRadioButton
|
||||
aria-describedby="test-Anteater-description"
|
||||
checked={false}
|
||||
childrenInLabel={true}
|
||||
className="test-class a-class"
|
||||
id="test-Anteater"
|
||||
name="test"
|
||||
onChange={[Function]}
|
||||
value="Anteater"
|
||||
<DocumentFragment>
|
||||
<label
|
||||
class="mx_StyledRadioButton test-class a-class mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<label
|
||||
className="mx_StyledRadioButton test-class a-class mx_StyledRadioButton_enabled"
|
||||
<input
|
||||
aria-describedby="test-Anteater-description"
|
||||
id="test-Anteater"
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Anteater"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<input
|
||||
aria-describedby="test-Anteater-description"
|
||||
checked={false}
|
||||
id="test-Anteater"
|
||||
name="test"
|
||||
onChange={[Function]}
|
||||
type="radio"
|
||||
value="Anteater"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span>
|
||||
Anteater label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
</StyledRadioButton>
|
||||
<span>
|
||||
Anteater label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
id="test-Anteater-description"
|
||||
>
|
||||
anteater description
|
||||
</span>
|
||||
<StyledRadioButton
|
||||
checked={false}
|
||||
childrenInLabel={true}
|
||||
className="test-class"
|
||||
id="test-Badger"
|
||||
name="test"
|
||||
onChange={[Function]}
|
||||
value="Badger"
|
||||
<label
|
||||
class="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<label
|
||||
className="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
|
||||
<input
|
||||
id="test-Badger"
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Badger"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
id="test-Badger"
|
||||
name="test"
|
||||
onChange={[Function]}
|
||||
type="radio"
|
||||
value="Badger"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span>
|
||||
Badger label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
</StyledRadioButton>
|
||||
<StyledRadioButton
|
||||
aria-describedby="test-Canary-description"
|
||||
checked={false}
|
||||
childrenInLabel={true}
|
||||
className="test-class"
|
||||
id="test-Canary"
|
||||
name="test"
|
||||
onChange={[Function]}
|
||||
value="Canary"
|
||||
<span>
|
||||
Badger label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
class="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
|
||||
>
|
||||
<label
|
||||
className="mx_StyledRadioButton test-class mx_StyledRadioButton_enabled"
|
||||
<input
|
||||
aria-describedby="test-Canary-description"
|
||||
id="test-Canary"
|
||||
name="test"
|
||||
type="radio"
|
||||
value="Canary"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_content"
|
||||
>
|
||||
<input
|
||||
aria-describedby="test-Canary-description"
|
||||
checked={false}
|
||||
id="test-Canary"
|
||||
name="test"
|
||||
onChange={[Function]}
|
||||
type="radio"
|
||||
value="Canary"
|
||||
/>
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
className="mx_StyledRadioButton_content"
|
||||
>
|
||||
<span>
|
||||
Canary label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
</StyledRadioButton>
|
||||
<span>
|
||||
Canary label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mx_StyledRadioButton_spacer"
|
||||
/>
|
||||
</label>
|
||||
<span
|
||||
id="test-Canary-description"
|
||||
>
|
||||
|
@ -154,5 +85,5 @@ exports[`<StyledRadioGroup /> renders radios correctly when no value is provided
|
|||
Canary description
|
||||
</span>
|
||||
</span>
|
||||
</StyledRadioGroup>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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 { render } from "@testing-library/react";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import React from "react";
|
||||
|
||||
import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
|
||||
describe("RoomHeaderButtons-test.tsx", function() {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
||||
if (name === "feature_thread") return true;
|
||||
});
|
||||
});
|
||||
|
||||
function getComponent(room: Room) {
|
||||
return render(<RoomHeaderButtons
|
||||
room={room}
|
||||
excludedRightPanelPhaseButtons={[]}
|
||||
/>);
|
||||
}
|
||||
|
||||
function getThreadButton(container) {
|
||||
return container.querySelector(".mx_RightPanel_threadsButton");
|
||||
}
|
||||
|
||||
function isIndicatorOfType(container, type: "red" | "gray") {
|
||||
return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")
|
||||
.className
|
||||
.includes(type);
|
||||
}
|
||||
|
||||
it("shows the thread button", () => {
|
||||
const { container } = getComponent(room);
|
||||
expect(getThreadButton(container)).not.toBeNull();
|
||||
});
|
||||
|
||||
it("hides the thread button", () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);
|
||||
const { container } = getComponent(room);
|
||||
expect(getThreadButton(container)).toBeNull();
|
||||
});
|
||||
|
||||
it("room wide notification does not change the thread button", () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
const { container } = getComponent(room);
|
||||
|
||||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||
});
|
||||
|
||||
it("room wide notification does not change the thread button", () => {
|
||||
const { container } = getComponent(room);
|
||||
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
||||
expect(isIndicatorOfType(container, "gray")).toBe(true);
|
||||
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
|
||||
expect(isIndicatorOfType(container, "red")).toBe(true);
|
||||
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0);
|
||||
|
||||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||
});
|
||||
});
|
|
@ -140,6 +140,7 @@ describe('<UserInfo />', () => {
|
|||
describe('with a room', () => {
|
||||
const room = {
|
||||
roomId: '!fkfk',
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getMember: jest.fn().mockReturnValue(undefined),
|
||||
getMxcAvatarUrl: jest.fn().mockReturnValue('mock-avatar-url'),
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
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 { act, render } from "@testing-library/react";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import React from "react";
|
||||
|
||||
import EventTile, { EventTileProps } from "../../../../src/components/views/rooms/EventTile";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../../src/contexts/RoomContext";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { getRoomContext, mkMessage, stubClient } from "../../../test-utils";
|
||||
import { mkThread } from "../../../test-utils/threads";
|
||||
|
||||
describe("EventTile", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let mxEvent: MatrixEvent;
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
// let changeEvent: (event: MatrixEvent) => void;
|
||||
|
||||
function TestEventTile(props: Partial<EventTileProps>) {
|
||||
// const [event] = useState(mxEvent);
|
||||
// Give a way for a test to update the event prop.
|
||||
// changeEvent = setEvent;
|
||||
|
||||
return <EventTile
|
||||
mxEvent={mxEvent}
|
||||
{...props}
|
||||
/>;
|
||||
}
|
||||
|
||||
function getComponent(
|
||||
overrides: Partial<EventTileProps> = {},
|
||||
renderingType: TimelineRenderingType = TimelineRenderingType.Room,
|
||||
) {
|
||||
const context = getRoomContext(room, {
|
||||
timelineRenderingType: renderingType,
|
||||
});
|
||||
return render(
|
||||
<RoomContext.Provider value={context}>
|
||||
<TestEventTile {...overrides} />
|
||||
</RoomContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
|
||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
|
||||
mxEvent = mkMessage({
|
||||
room: room.roomId,
|
||||
user: "@alice:example.org",
|
||||
msg: "Hello world!",
|
||||
event: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventTile renderingType: ThreadsList", () => {
|
||||
beforeEach(() => {
|
||||
const { rootEvent } = mkThread({
|
||||
room,
|
||||
client,
|
||||
authorId: "@alice:example.org",
|
||||
participantUserIds: ["@alice:example.org"],
|
||||
});
|
||||
mxEvent = rootEvent;
|
||||
});
|
||||
|
||||
it("shows an unread notification bage", () => {
|
||||
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Total, 3);
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(0);
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(mxEvent.getId(), NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(1);
|
||||
expect(container.getElementsByClassName("mx_NotificationBadge_highlighted")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 { fireEvent, render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
StatelessNotificationBadge,
|
||||
} from "../../../../../src/components/views/rooms/NotificationBadge/StatelessNotificationBadge";
|
||||
import { NotificationColor } from "../../../../../src/stores/notifications/NotificationColor";
|
||||
|
||||
describe("NotificationBadge", () => {
|
||||
describe("StatelessNotificationBadge", () => {
|
||||
it("lets you click it", () => {
|
||||
const cb = jest.fn();
|
||||
|
||||
const { container } = render(<StatelessNotificationBadge
|
||||
symbol=""
|
||||
color={NotificationColor.Red}
|
||||
count={5}
|
||||
onClick={cb}
|
||||
onMouseOver={cb}
|
||||
onMouseLeave={cb}
|
||||
/>);
|
||||
|
||||
fireEvent.click(container.firstChild);
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.mouseEnter(container.firstChild);
|
||||
expect(cb).toHaveBeenCalledTimes(2);
|
||||
|
||||
fireEvent.mouseLeave(container.firstChild);
|
||||
expect(cb).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
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 "jest-mock";
|
||||
import { screen, act, render } from "@testing-library/react";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventStatus } from "matrix-js-sdk/src/models/event-status";
|
||||
|
||||
import {
|
||||
UnreadNotificationBadge,
|
||||
} from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
|
||||
import { mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import * as RoomNotifs from "../../../../../src/RoomNotifs";
|
||||
|
||||
jest.mock("../../../../../src/RoomNotifs");
|
||||
jest.mock('../../../../../src/RoomNotifs', () => ({
|
||||
...(jest.requireActual('../../../../../src/RoomNotifs') as Object),
|
||||
getRoomNotifsState: jest.fn(),
|
||||
}));
|
||||
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let THREAD_ID;
|
||||
|
||||
describe("UnreadNotificationBadge", () => {
|
||||
let mockClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
function getComponent(threadId?: string) {
|
||||
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
mockClient = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
|
||||
jest.spyOn(RoomNotifs, "getRoomNotifsState").mockReturnValue(RoomNotifs.RoomNotifState.AllMessages);
|
||||
});
|
||||
|
||||
it("renders unread notification badge", () => {
|
||||
const { container } = render(getComponent());
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy();
|
||||
|
||||
act(() => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders unread thread notification badge", () => {
|
||||
const { container } = render(getComponent(THREAD_ID));
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy();
|
||||
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 1);
|
||||
});
|
||||
|
||||
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("hides unread notification badge", () => {
|
||||
act(() => {
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||
const { container } = render(getComponent(THREAD_ID));
|
||||
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("adds a warning for unsent messages", () => {
|
||||
const evt = mkMessage({
|
||||
room: room.roomId,
|
||||
user: "@alice:example.org",
|
||||
msg: "Hello world!",
|
||||
event: true,
|
||||
});
|
||||
evt.status = EventStatus.NOT_SENT;
|
||||
|
||||
room.addPendingEvent(evt, "123");
|
||||
|
||||
render(getComponent());
|
||||
|
||||
expect(screen.queryByText("!")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("adds a warning for invites", () => {
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue("invite");
|
||||
render(getComponent());
|
||||
expect(screen.queryByText("!")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("hides counter for muted rooms", () => {
|
||||
jest.spyOn(RoomNotifs, "getRoomNotifsState")
|
||||
.mockReset()
|
||||
.mockReturnValue(RoomNotifs.RoomNotifState.Mute);
|
||||
|
||||
const { container } = render(getComponent());
|
||||
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
||||
});
|
||||
});
|
|
@ -15,18 +15,14 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
renderIntoDocument,
|
||||
Simulate,
|
||||
findRenderedDOMComponentWithClass,
|
||||
act,
|
||||
} from 'react-dom/test-utils';
|
||||
import { render, fireEvent, RenderResult, waitFor } from "@testing-library/react";
|
||||
import { Room, RoomMember, MatrixError, IContent } from 'matrix-js-sdk/src/matrix';
|
||||
|
||||
import { stubClient } from '../../../test-utils';
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
||||
import RoomPreviewBar from '../../../../src/components/views/rooms/RoomPreviewBar';
|
||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||
|
||||
jest.mock('../../../../src/IdentityAuthClient', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
|
@ -79,19 +75,18 @@ describe('<RoomPreviewBar />', () => {
|
|||
const defaultProps = {
|
||||
room: createRoom(roomId, userId),
|
||||
};
|
||||
const wrapper = renderIntoDocument<React.Component>(
|
||||
<RoomPreviewBar {...defaultProps} {...props} />,
|
||||
) as React.Component;
|
||||
return findRenderedDOMComponentWithClass(wrapper, 'mx_RoomPreviewBar') as HTMLDivElement;
|
||||
return render(<RoomPreviewBar {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
const isSpinnerRendered = (element: Element) => !!element.querySelector('.mx_Spinner');
|
||||
const getMessage = (element: Element) => element.querySelector<HTMLDivElement>('.mx_RoomPreviewBar_message');
|
||||
const getActions = (element: Element) => element.querySelector<HTMLDivElement>('.mx_RoomPreviewBar_actions');
|
||||
const getPrimaryActionButton = (element: Element) =>
|
||||
getActions(element).querySelector('.mx_AccessibleButton_kind_primary');
|
||||
const getSecondaryActionButton = (element: Element) =>
|
||||
getActions(element).querySelector('.mx_AccessibleButton_kind_secondary');
|
||||
const isSpinnerRendered = (wrapper: RenderResult) => !!wrapper.container.querySelector('.mx_Spinner');
|
||||
const getMessage = (wrapper: RenderResult) =>
|
||||
wrapper.container.querySelector<HTMLDivElement>('.mx_RoomPreviewBar_message');
|
||||
const getActions = (wrapper: RenderResult) =>
|
||||
wrapper.container.querySelector<HTMLDivElement>('.mx_RoomPreviewBar_actions');
|
||||
const getPrimaryActionButton = (wrapper: RenderResult) =>
|
||||
getActions(wrapper).querySelector('.mx_AccessibleButton_kind_primary');
|
||||
const getSecondaryActionButton = (wrapper: RenderResult) =>
|
||||
getActions(wrapper).querySelector('.mx_AccessibleButton_kind_secondary');
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
|
@ -128,6 +123,36 @@ describe('<RoomPreviewBar />', () => {
|
|||
expect(getMessage(component).textContent).toEqual('Join the conversation with an account');
|
||||
});
|
||||
|
||||
it("should send room oob data to start login", async () => {
|
||||
MatrixClientPeg.get().isGuest = jest.fn().mockReturnValue(true);
|
||||
const component = getComponent({
|
||||
oobData: {
|
||||
name: "Room Name",
|
||||
avatarUrl: "mxc://foo/bar",
|
||||
inviterName: "Charlie",
|
||||
},
|
||||
});
|
||||
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
|
||||
expect(getMessage(component).textContent).toEqual('Join the conversation with an account');
|
||||
fireEvent.click(getPrimaryActionButton(component));
|
||||
|
||||
await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
screenAfterLogin: {
|
||||
screen: 'room',
|
||||
params: expect.objectContaining({
|
||||
room_name: "Room Name",
|
||||
room_avatar_url: "mxc://foo/bar",
|
||||
inviter_name: "Charlie",
|
||||
}),
|
||||
},
|
||||
})));
|
||||
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
});
|
||||
|
||||
it('renders kicked message', () => {
|
||||
const room = createRoom(roomId, otherUserId);
|
||||
jest.spyOn(room, 'getMember').mockReturnValue(makeMockRoomMember({ isKicked: true }));
|
||||
|
@ -233,18 +258,14 @@ describe('<RoomPreviewBar />', () => {
|
|||
|
||||
it('joins room on primary button click', () => {
|
||||
const component = getComponent({ inviterName, room, onJoinClick, onRejectClick });
|
||||
act(() => {
|
||||
Simulate.click(getPrimaryActionButton(component));
|
||||
});
|
||||
fireEvent.click(getPrimaryActionButton(component));
|
||||
|
||||
expect(onJoinClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects invite on secondary button click', () => {
|
||||
const component = getComponent({ inviterName, room, onJoinClick, onRejectClick });
|
||||
act(() => {
|
||||
Simulate.click(getSecondaryActionButton(component));
|
||||
});
|
||||
fireEvent.click(getSecondaryActionButton(component));
|
||||
|
||||
expect(onRejectClick).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -296,9 +317,7 @@ describe('<RoomPreviewBar />', () => {
|
|||
await new Promise(setImmediate);
|
||||
expect(getPrimaryActionButton(component)).toBeTruthy();
|
||||
expect(getSecondaryActionButton(component)).toBeFalsy();
|
||||
act(() => {
|
||||
Simulate.click(getPrimaryActionButton(component));
|
||||
});
|
||||
fireEvent.click(getPrimaryActionButton(component));
|
||||
expect(onJoinClick).toHaveBeenCalled();
|
||||
};
|
||||
|
||||
|
|
|
@ -15,8 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { render } from "@testing-library/react";
|
||||
import { TextInputField } from "@matrix-org/react-sdk-module-api/lib/components/TextInputField";
|
||||
import { Spinner as ModuleSpinner } from "@matrix-org/react-sdk-module-api/lib/components/Spinner";
|
||||
|
||||
|
@ -31,12 +30,12 @@ describe("Module Components", () => {
|
|||
// ModuleRunner import to do its job (as per documentation in ModuleComponents).
|
||||
|
||||
it("should override the factory for a TextInputField", () => {
|
||||
const component = mount(<TextInputField label="My Label" value="My Value" onChange={() => {}} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
const { asFragment } = render(<TextInputField label="My Label" value="My Value" onChange={() => {}} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should override the factory for a ModuleSpinner", () => {
|
||||
const component = mount(<ModuleSpinner />);
|
||||
expect(component).toMatchSnapshot();
|
||||
const { asFragment } = render(<ModuleSpinner />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,68 +1,39 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Module Components should override the factory for a ModuleSpinner 1`] = `
|
||||
<Spinner>
|
||||
<Spinner
|
||||
h={32}
|
||||
w={32}
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
className="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading..."
|
||||
className="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style={
|
||||
Object {
|
||||
"height": 32,
|
||||
"width": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Spinner>
|
||||
</Spinner>
|
||||
aria-label="Loading..."
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`Module Components should override the factory for a TextInputField 1`] = `
|
||||
<TextInputField
|
||||
label="My Label"
|
||||
onChange={[Function]}
|
||||
value="My Value"
|
||||
>
|
||||
<Field
|
||||
autoComplete="off"
|
||||
element="input"
|
||||
label="My Label"
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
validateOnBlur={true}
|
||||
validateOnChange={true}
|
||||
validateOnFocus={true}
|
||||
value="My Value"
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="mx_Field mx_Field_input"
|
||||
>
|
||||
<div
|
||||
className="mx_Field mx_Field_input"
|
||||
<input
|
||||
autocomplete="off"
|
||||
id="mx_Field_1"
|
||||
label="My Label"
|
||||
placeholder="My Label"
|
||||
type="text"
|
||||
value="My Value"
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_1"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
id="mx_Field_1"
|
||||
label="My Label"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
placeholder="My Label"
|
||||
type="text"
|
||||
value="My Value"
|
||||
/>
|
||||
<label
|
||||
htmlFor="mx_Field_1"
|
||||
>
|
||||
My Label
|
||||
</label>
|
||||
</div>
|
||||
</Field>
|
||||
</TextInputField>
|
||||
My Label
|
||||
</label>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEventEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixEventEvent, MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
|
@ -24,12 +24,16 @@ import * as testUtils from "../../test-utils";
|
|||
import { NotificationStateEvents } from "../../../src/stores/notifications/NotificationState";
|
||||
|
||||
describe("RoomNotificationState", () => {
|
||||
stubClient();
|
||||
const client = MatrixClientPeg.get();
|
||||
let testRoom: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
testRoom = testUtils.mkStubRoom("$aroomid", "Test room", client);
|
||||
});
|
||||
|
||||
it("Updates on event decryption", () => {
|
||||
const testRoom = testUtils.mkStubRoom("$aroomid", "Test room", client);
|
||||
|
||||
const roomNotifState = new RoomNotificationState(testRoom as any as Room);
|
||||
const listener = jest.fn();
|
||||
roomNotifState.addListener(NotificationStateEvents.Update, listener);
|
||||
|
@ -40,4 +44,9 @@ describe("RoomNotificationState", () => {
|
|||
client.emit(MatrixEventEvent.Decrypted, testEvent);
|
||||
expect(listener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes listeners", () => {
|
||||
const roomNotifState = new RoomNotificationState(testRoom as any as Room);
|
||||
expect(() => roomNotifState.destroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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 { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { RoomNotificationStateStore } from "../../../src/stores/notifications/RoomNotificationStateStore";
|
||||
import { stubClient } from "../../test-utils";
|
||||
|
||||
describe("RoomNotificationStateStore", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
|
||||
let room;
|
||||
let client;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
room = new Room(ROOM_ID, client, client.getUserId(), {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not use legacy thread notification store", () => {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable);
|
||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull();
|
||||
});
|
||||
|
||||
it("use legacy thread notification store", () => {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not use legacy thread notification store", () => {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Stable);
|
||||
RoomNotificationStateStore.instance.getRoomState(room);
|
||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).toBeNull();
|
||||
});
|
||||
|
||||
it("use legacy thread notification store", () => {
|
||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
||||
RoomNotificationStateStore.instance.getRoomState(room);
|
||||
expect(RoomNotificationStateStore.instance.getThreadsRoomState(room)).not.toBeNull();
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Room, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { VisibilityProvider } from "../../../../src/stores/room-list/filters/VisibilityProvider";
|
||||
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
|
||||
|
@ -43,6 +43,7 @@ jest.mock("../../../../src/customisations/RoomList", () => ({
|
|||
const createRoom = (isSpaceRoom = false): Room => {
|
||||
return {
|
||||
isSpaceRoom: () => isSpaceRoom,
|
||||
getType: () => isSpaceRoom ? RoomType.Space : undefined,
|
||||
} as unknown as Room;
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
IEventRelation,
|
||||
IUnsigned,
|
||||
IPusher,
|
||||
RoomType,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
||||
|
@ -448,6 +449,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
|
|||
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||
getType: jest.fn().mockReturnValue(undefined),
|
||||
isElementVideoRoom: jest.fn().mockReturnValue(false),
|
||||
getUnreadNotificationCount: jest.fn(() => 0),
|
||||
getEventReadUpTo: jest.fn(() => null),
|
||||
|
@ -545,6 +547,7 @@ export const mkSpace = (
|
|||
): MockedObject<Room> => {
|
||||
const space = mocked(mkRoom(client, spaceId, rooms));
|
||||
space.isSpaceRoom.mockReturnValue(true);
|
||||
space.getType.mockReturnValue(RoomType.Space);
|
||||
mocked(space.currentState).getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId =>
|
||||
mkEvent({
|
||||
event: true,
|
||||
|
|
|
@ -106,7 +106,7 @@ export const mkThread = ({
|
|||
participantUserIds,
|
||||
length = 2,
|
||||
ts = 1,
|
||||
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent } => {
|
||||
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent, events: MatrixEvent[] } => {
|
||||
const { rootEvent, events } = makeThreadEvents({
|
||||
roomId: room.roomId,
|
||||
authorId,
|
||||
|
@ -120,5 +120,5 @@ export const mkThread = ({
|
|||
// So that we do not have to mock the thread loading
|
||||
thread.initialEventsFetched = true;
|
||||
|
||||
return { thread, rootEvent };
|
||||
return { thread, rootEvent, events };
|
||||
};
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
VoiceBroadcastRecorderEvent,
|
||||
} from "../../../src/voice-broadcast";
|
||||
|
||||
jest.mock("../../../src/audio/VoiceRecording");
|
||||
|
||||
describe("VoiceBroadcastRecorder", () => {
|
||||
describe("createVoiceBroadcastRecorder", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -44,6 +46,7 @@ describe("VoiceBroadcastRecorder", () => {
|
|||
|
||||
it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => {
|
||||
const voiceBroadcastRecorder = createVoiceBroadcastRecorder();
|
||||
expect(mocked(VoiceRecording).mock.instances[0].disableMaxLength).toHaveBeenCalled();
|
||||
expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder);
|
||||
expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337);
|
||||
});
|
||||
|
@ -72,16 +75,12 @@ describe("VoiceBroadcastRecorder", () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
voiceRecording = {
|
||||
contentType,
|
||||
start: jest.fn().mockResolvedValue(undefined),
|
||||
stop: jest.fn().mockResolvedValue(undefined),
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
destroy: jest.fn(),
|
||||
recorderSeconds: 23,
|
||||
} as unknown as VoiceRecording;
|
||||
voiceRecording = new VoiceRecording();
|
||||
// @ts-ignore
|
||||
voiceRecording.recorderSeconds = 23;
|
||||
// @ts-ignore
|
||||
voiceRecording.contentType = contentType;
|
||||
|
||||
voiceBroadcastRecorder = new VoiceBroadcastRecorder(voiceRecording, chunkLength);
|
||||
jest.spyOn(voiceBroadcastRecorder, "removeAllListeners");
|
||||
onChunkRecorded = jest.fn();
|
||||
|
|
Loading…
Reference in New Issue