Merge remote-tracking branch 'origin/develop' into feat/add-message-edition-wysiwyg-composer
This commit is contained in:
commit
de86221c72
55 changed files with 1551 additions and 668 deletions
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
93
src/hooks/useUnreadNotifications.ts
Normal file
93
src/hooks/useUnreadNotifications.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { getUnsentMessages } from "../components/structures/RoomStatusBar";
|
||||
import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs";
|
||||
import { NotificationColor } from "../stores/notifications/NotificationColor";
|
||||
import { doesRoomHaveUnreadMessages } from "../Unread";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
|
||||
export const useUnreadNotifications = (room: Room, threadId?: string): {
|
||||
symbol: string | null;
|
||||
count: number;
|
||||
color: NotificationColor;
|
||||
} => {
|
||||
const [symbol, setSymbol] = useState<string | null>(null);
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const [color, setColor] = useState<NotificationColor>(0);
|
||||
|
||||
useEventEmitter(room, RoomEvent.UnreadNotifications,
|
||||
(unreadNotifications: NotificationCount, evtThreadId?: string) => {
|
||||
// Discarding all events not related to the thread if one has been setup
|
||||
if (threadId && threadId !== evtThreadId) return;
|
||||
updateNotificationState();
|
||||
},
|
||||
);
|
||||
useEventEmitter(room, RoomEvent.Receipt, () => updateNotificationState());
|
||||
useEventEmitter(room, RoomEvent.Timeline, () => updateNotificationState());
|
||||
useEventEmitter(room, RoomEvent.Redaction, () => updateNotificationState());
|
||||
useEventEmitter(room, RoomEvent.LocalEchoUpdated, () => updateNotificationState());
|
||||
useEventEmitter(room, RoomEvent.MyMembership, () => updateNotificationState());
|
||||
|
||||
const updateNotificationState = useCallback(() => {
|
||||
if (getUnsentMessages(room, threadId).length > 0) {
|
||||
setSymbol("!");
|
||||
setCount(1);
|
||||
setColor(NotificationColor.Unsent);
|
||||
} else if (getEffectiveMembership(room.getMyMembership()) === EffectiveMembership.Invite) {
|
||||
setSymbol("!");
|
||||
setCount(1);
|
||||
setColor(NotificationColor.Red);
|
||||
} else if (getRoomNotifsState(room.roomId) === RoomNotifState.Mute) {
|
||||
setSymbol(null);
|
||||
setCount(0);
|
||||
setColor(NotificationColor.None);
|
||||
} else {
|
||||
const redNotifs = getUnreadNotificationCount(room, NotificationCountType.Highlight, threadId);
|
||||
const greyNotifs = getUnreadNotificationCount(room, NotificationCountType.Total, threadId);
|
||||
|
||||
const trueCount = greyNotifs || redNotifs;
|
||||
setCount(trueCount);
|
||||
setSymbol(null);
|
||||
if (redNotifs > 0) {
|
||||
setColor(NotificationColor.Red);
|
||||
} else if (greyNotifs > 0) {
|
||||
setColor(NotificationColor.Grey);
|
||||
} else if (!threadId) {
|
||||
// TODO: No support for `Bold` on threads at the moment
|
||||
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
const hasUnread = doesRoomHaveUnreadMessages(room);
|
||||
setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None);
|
||||
}
|
||||
}
|
||||
}, [room, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNotificationState();
|
||||
}, [updateNotificationState]);
|
||||
|
||||
return {
|
||||
symbol,
|
||||
count,
|
||||
color,
|
||||
};
|
||||
};
|
|
@ -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());
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue