Files
ThreadNet-Web/src/components/views/rooms/WhoIsTypingTile.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

219 lines
8.1 KiB
TypeScript
Raw Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2017-2024 New Vector Ltd.
Copyright 2015, 2016 OpenMarket Ltd
2024-09-09 14:57:16 +01:00
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";
2021-05-25 14:34:19 +01:00
2019-12-19 18:19:56 -07:00
import * as WhoIsTyping from "../../../WhoIsTyping";
import Timer from "../../../utils/Timer";
2021-05-25 14:10:16 +01:00
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import MemberAvatar from "../avatars/MemberAvatar";
2021-05-25 14:10:16 +01:00
interface IProps {
// the room this statusbar is representing.
2021-05-25 14:34:19 +01:00
room: Room;
onShown?: () => void;
onHidden?: () => void;
2021-05-25 14:10:16 +01:00
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
2021-05-25 14:34:19 +01:00
whoIsTypingLimit: number;
2021-05-25 14:10:16 +01:00
}
interface IState {
2021-05-25 14:34:19 +01:00
usersTyping: RoomMember[];
2021-05-25 14:10:16 +01:00
// 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
2021-05-25 14:34:19 +01:00
delayedStopTypingTimers: Record<string, Timer>;
2021-05-25 14:10:16 +01:00
}
2021-05-25 14:10:16 +01:00
export default class WhoIsTypingTile extends React.Component<IProps, IState> {
public static defaultProps = {
2020-08-29 12:14:16 +01:00
whoIsTypingLimit: 3,
};
2023-02-13 11:39:16 +00:00
public state: IState = {
2020-08-29 12:14:16 +01:00
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
delayedStopTypingTimers: {},
};
public componentDidMount(): void {
MatrixClientPeg.safeGet().on(RoomMemberEvent.Typing, this.onRoomMemberTyping);
MatrixClientPeg.safeGet().on(RoomEvent.Timeline, this.onRoomTimeline);
2020-08-29 12:14:16 +01:00
}
2023-02-13 11:39:16 +00:00
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();
2019-03-28 18:29:48 +01:00
} else if (this.props.onHidden && wasVisible && !isVisible) {
this.props.onHidden();
}
2020-08-29 12:14:16 +01:00
}
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);
}
2021-05-25 14:34:19 +01:00
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
2020-08-29 12:14:16 +01:00
}
private static isVisible(state: IState): boolean {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
2020-08-29 12:14:16 +01:00
}
2021-05-25 14:34:19 +01:00
public isVisible = (): boolean => {
return WhoIsTypingTile.isVisible(this.state);
2020-08-29 12:14:16 +01:00
};
private onRoomTimeline = (event: MatrixEvent, room?: Room): void => {
if (room?.roomId === this.props.room.roomId) {
const userId = event.getSender()!;
2019-01-17 18:32:46 +01:00
// remove user from usersTyping
const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId);
if (usersTyping.length !== this.state.usersTyping.length) {
2021-06-29 13:11:58 +01:00
this.setState({ usersTyping });
}
2019-01-17 18:32:53 +01:00
// abort timer if any
2021-05-25 14:34:19 +01:00
this.abortUserTimer(userId);
}
2020-08-29 12:14:16 +01:00
};
2021-05-25 14:34:19 +01:00
private onRoomMemberTyping = (): void => {
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
this.setState({
2021-05-25 14:34:19 +01:00
delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping),
usersTyping,
});
2020-08-29 12:14:16 +01:00
};
2021-05-25 14:34:19 +01:00
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) => {
2019-01-17 18:52:22 +01:00
const timer = this.state.delayedStopTypingTimers[m.userId];
if (timer) {
timer.abort();
}
});
2019-01-17 18:52:22 +01:00
// prepare new delayedStopTypingTimers object to update state with
let delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
// remove members that started typing again
2019-01-17 18:52:22 +01:00
delayedStopTypingTimers = usersThatStartedTyping.reduce((delayedStopTypingTimers, m) => {
delete delayedStopTypingTimers[m.userId];
return delayedStopTypingTimers;
}, delayedStopTypingTimers);
// start timer for members that stopped typing
2019-01-17 18:52:22 +01:00
delayedStopTypingTimers = usersThatStoppedTyping.reduce((delayedStopTypingTimers, m) => {
if (!delayedStopTypingTimers[m.userId]) {
const timer = new Timer(5000);
2019-01-17 18:52:22 +01:00
delayedStopTypingTimers[m.userId] = timer;
timer.start();
timer.finished().then(
2021-05-25 14:34:19 +01:00
() => this.removeUserTimer(m.userId), // on elapsed
() => {
/* aborted */
},
);
}
2019-01-17 18:52:22 +01:00
return delayedStopTypingTimers;
}, delayedStopTypingTimers);
2019-01-17 18:52:22 +01:00
return delayedStopTypingTimers;
2020-08-29 12:14:16 +01:00
}
2021-05-25 14:34:19 +01:00
private abortUserTimer(userId: string): void {
2019-01-17 18:52:22 +01:00
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
timer.abort();
2021-05-25 14:34:19 +01:00
this.removeUserTimer(userId);
}
2020-08-29 12:14:16 +01:00
}
2021-05-25 14:34:19 +01:00
private removeUserTimer(userId: string): void {
2019-01-17 18:52:22 +01:00
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
2019-01-17 18:52:22 +01:00
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
delete delayedStopTypingTimers[userId];
2021-06-29 13:11:58 +01:00
this.setState({ delayedStopTypingTimers });
}
2020-08-29 12:14:16 +01:00
}
2021-05-25 14:34:19 +01:00
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"
2019-02-24 02:30:45 +00:00
viewUserOnClick={true}
aria-live="off"
/>
);
});
if (othersCount > 0) {
avatars.push(
2018-11-12 18:19:18 +01:00
<span className="mx_WhoIsTypingTile_remainingAvatarPlaceholder" key="others">
+{othersCount}
</span>,
);
}
return avatars;
2020-08-29 12:14:16 +01:00
}
public render(): React.ReactNode {
2023-04-20 15:44:54 +01:00
const usersTyping = [...this.state.usersTyping];
2019-01-17 18:32:53 +01:00
// 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);
2018-11-12 18:19:18 +01:00
if (!typingString) {
2021-05-20 16:11:33 +01:00
return null;
}
return (
<li className="mx_WhoIsTypingTile" aria-atomic="true">
2018-11-12 18:19:18 +01:00
<div className="mx_WhoIsTypingTile_avatars">
2021-05-25 14:34:19 +01:00
{this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit)}
</div>
2018-11-12 18:19:18 +01:00
<div className="mx_WhoIsTypingTile_label">{typingString}</div>
2018-11-13 09:15:43 +01:00
</li>
);
2020-08-29 12:14:16 +01:00
}
}