element-portable/src/components/views/rooms/WhoIsTypingTile.tsx
David Langley 491f0cd08a
Change license (#13)
* Copyright headers 1

* Licence headers 2

* Copyright Headers 3

* Copyright Headers 4

* Copyright Headers 5

* Copyright Headers 6

* Copyright headers 7

* Add copyright headers for html and config file

* Replace license files and update package.json

* Update with CLA

* lint
2024-09-09 13:57:16 +00:00

218 lines
8.1 KiB
TypeScript

/*
Copyright 2017-2024 New Vector Ltd.
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { Room, RoomEvent, RoomMember, RoomMemberEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import * as WhoIsTyping from "../../../WhoIsTyping";
import Timer from "../../../utils/Timer";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MemberAvatar from "../avatars/MemberAvatar";
interface IProps {
// the room this statusbar is representing.
room: Room;
onShown?: () => void;
onHidden?: () => void;
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: number;
}
interface IState {
usersTyping: RoomMember[];
// a map with userid => Timer to delay
// hiding the "x is typing" message for a
// user so hiding it can coincide
// with the sent message by the other side
// resulting in less timeline jumpiness
delayedStopTypingTimers: Record<string, Timer>;
}
export default class WhoIsTypingTile extends React.Component<IProps, IState> {
public static defaultProps = {
whoIsTypingLimit: 3,
};
public state: IState = {
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
delayedStopTypingTimers: {},
};
public componentDidMount(): void {
MatrixClientPeg.safeGet().on(RoomMemberEvent.Typing, this.onRoomMemberTyping);
MatrixClientPeg.safeGet().on(RoomEvent.Timeline, this.onRoomTimeline);
}
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
const wasVisible = WhoIsTypingTile.isVisible(prevState);
const isVisible = WhoIsTypingTile.isVisible(this.state);
if (this.props.onShown && !wasVisible && isVisible) {
this.props.onShown();
} else if (this.props.onHidden && wasVisible && !isVisible) {
this.props.onHidden();
}
}
public componentWillUnmount(): void {
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
const client = MatrixClientPeg.get();
if (client) {
client.removeListener(RoomMemberEvent.Typing, this.onRoomMemberTyping);
client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
}
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
}
private static isVisible(state: IState): boolean {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
}
public isVisible = (): boolean => {
return WhoIsTypingTile.isVisible(this.state);
};
private onRoomTimeline = (event: MatrixEvent, room?: Room): void => {
if (room?.roomId === this.props.room.roomId) {
const userId = event.getSender()!;
// remove user from usersTyping
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
if (usersTyping.length !== this.state.usersTyping.length) {
this.setState({ usersTyping });
}
// abort timer if any
this.abortUserTimer(userId);
}
};
private onRoomMemberTyping = (): void => {
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
this.setState({
delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping),
usersTyping,
});
};
private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record<string, Timer> {
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
return !usersTyping.some((b) => a.userId === b.userId);
});
const usersThatStartedTyping = usersTyping.filter((a) => {
return !this.state.usersTyping.some((b) => a.userId === b.userId);
});
// abort all the timers for the users that started typing again
usersThatStartedTyping.forEach((m) => {
const timer = this.state.delayedStopTypingTimers[m.userId];
if (timer) {
timer.abort();
}
});
// prepare new delayedStopTypingTimers object to update state with
let delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
// remove members that started typing again
delayedStopTypingTimers = usersThatStartedTyping.reduce((delayedStopTypingTimers, m) => {
delete delayedStopTypingTimers[m.userId];
return delayedStopTypingTimers;
}, delayedStopTypingTimers);
// start timer for members that stopped typing
delayedStopTypingTimers = usersThatStoppedTyping.reduce((delayedStopTypingTimers, m) => {
if (!delayedStopTypingTimers[m.userId]) {
const timer = new Timer(5000);
delayedStopTypingTimers[m.userId] = timer;
timer.start();
timer.finished().then(
() => this.removeUserTimer(m.userId), // on elapsed
() => {
/* aborted */
},
);
}
return delayedStopTypingTimers;
}, delayedStopTypingTimers);
return delayedStopTypingTimers;
}
private abortUserTimer(userId: string): void {
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
timer.abort();
this.removeUserTimer(userId);
}
}
private removeUserTimer(userId: string): void {
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
delete delayedStopTypingTimers[userId];
this.setState({ delayedStopTypingTimers });
}
}
private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] {
let othersCount = 0;
if (users.length > limit) {
othersCount = users.length - limit + 1;
users = users.slice(0, limit - 1);
}
const avatars = users.map((u) => {
return (
<MemberAvatar
key={u.userId}
member={u}
size="24px"
resizeMethod="crop"
viewUserOnClick={true}
aria-live="off"
/>
);
});
if (othersCount > 0) {
avatars.push(
<span className="mx_WhoIsTypingTile_remainingAvatarPlaceholder" key="others">
+{othersCount}
</span>,
);
}
return avatars;
}
public render(): React.ReactNode {
const usersTyping = [...this.state.usersTyping];
// append the users that have been reported not typing anymore
// but have a timeout timer running so they can disappear
// when a message comes in
for (const userId in this.state.delayedStopTypingTimers) {
const member = this.props.room.getMember(userId);
if (member) usersTyping.push(member);
}
// sort them so the typing members don't change order when
// moved to delayedStopTypingTimers
const collator = new Intl.Collator();
usersTyping.sort((a, b) => collator.compare(a.name, b.name));
const typingString = WhoIsTyping.whoIsTypingString(usersTyping, this.props.whoIsTypingLimit);
if (!typingString) {
return null;
}
return (
<li className="mx_WhoIsTypingTile" aria-atomic="true">
<div className="mx_WhoIsTypingTile_avatars">
{this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit)}
</div>
<div className="mx_WhoIsTypingTile_label">{typingString}</div>
</li>
);
}
}