Merge branch 'matrix-org:develop' into Bubble-bericht
This commit is contained in:
commit
1259247380
324 changed files with 14446 additions and 5540 deletions
|
@ -36,6 +36,7 @@ import {Container, WidgetLayoutStore} from "../../../stores/widgets/WidgetLayout
|
|||
import {clamp, percentageOf, percentageWithin} from "../../../utils/numbers";
|
||||
import {useStateCallback} from "../../../hooks/useStateCallback";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
|
||||
@replaceableComponent("views.rooms.AppsDrawer")
|
||||
export default class AppsDrawer extends React.Component {
|
||||
|
@ -81,13 +82,6 @@ export default class AppsDrawer extends React.Component {
|
|||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
// Room has changed probably, update apps
|
||||
this._updateApps();
|
||||
}
|
||||
|
||||
onIsResizing = (resizing) => {
|
||||
// This one is the vertical, ie. change height of apps drawer
|
||||
this.setState({ resizingVertical: resizing });
|
||||
|
@ -140,7 +134,10 @@ export default class AppsDrawer extends React.Component {
|
|||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
// Room has changed, update apps
|
||||
this._updateApps();
|
||||
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
this._loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
|
@ -290,7 +287,7 @@ const PersistentVResizer = ({
|
|||
|
||||
// Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window.
|
||||
if (!minHeight) minHeight = 100;
|
||||
if (!maxHeight) maxHeight = (window.innerHeight / 4) * 3;
|
||||
if (!maxHeight) maxHeight = (UIStore.instance.windowHeight / 4) * 3;
|
||||
|
||||
// Convert from percentage to height. Note that the default height is 280px.
|
||||
if (defaultHeight) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import Autocompleter from '../../../autocomplete/Autocompleter';
|
|||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
const MAX_PROVIDER_MATCHES = 20;
|
||||
|
||||
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
||||
|
||||
|
@ -136,7 +137,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
|
||||
processQuery(query: string, selection: ISelectionRange) {
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete,
|
||||
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
|
||||
).then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
|
|
|
@ -15,19 +15,18 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import classNames from 'classnames';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||
import {objectHasDiff} from "../../../utils/objects";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object
|
||||
|
@ -69,19 +68,21 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
counters: this._computeCounters(),
|
||||
counters: this.computeCounters(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._rateLimitedUpdate);
|
||||
if (SettingsStore.getValue("feature_state_counters")) {
|
||||
cli.on("RoomState.events", this.rateLimitedUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._rateLimitedUpdate);
|
||||
if (cli && SettingsStore.getValue("feature_state_counters")) {
|
||||
cli.removeListener("RoomState.events", this.rateLimitedUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,23 +97,11 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
onConferenceNotificationClick = (ev, type) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: type,
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
_rateLimitedUpdate = new RateLimitedFunc(() => {
|
||||
if (SettingsStore.getValue("feature_state_counters")) {
|
||||
this.setState({counters: this._computeCounters()});
|
||||
}
|
||||
private rateLimitedUpdate = new RateLimitedFunc(() => {
|
||||
this.setState({ counters: this.computeCounters() });
|
||||
}, 500);
|
||||
|
||||
_computeCounters() {
|
||||
private computeCounters() {
|
||||
const counters = [];
|
||||
|
||||
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
|
||||
|
@ -225,7 +214,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<AutoHideScrollbar className={classes} style={style} >
|
||||
<AutoHideScrollbar className={classes} style={style}>
|
||||
{ stateViews }
|
||||
{ appsDrawer }
|
||||
{ callView }
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
|
@ -24,16 +24,19 @@ import {getCaretOffsetAndText} from '../../../editor/dom';
|
|||
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
|
@ -120,6 +123,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
saveDisabled: true,
|
||||
};
|
||||
this._createEditorModel();
|
||||
window.addEventListener("beforeunload", this._saveStoredEditorState);
|
||||
}
|
||||
|
||||
_setEditorRef = ref => {
|
||||
|
@ -164,6 +168,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
@ -173,11 +178,71 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
get _editorRoomKey() {
|
||||
return `mx_edit_room_${this._getRoom().roomId}`;
|
||||
}
|
||||
|
||||
get _editorStateKey() {
|
||||
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
|
||||
}
|
||||
|
||||
_cancelEdit = () => {
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
||||
get _shouldSaveStoredEditorState() {
|
||||
return localStorage.getItem(this._editorRoomKey) !== null;
|
||||
}
|
||||
|
||||
_restoreStoredEditorState(partCreator) {
|
||||
const json = localStorage.getItem(this._editorStateKey);
|
||||
if (json) {
|
||||
try {
|
||||
const {parts: serializedParts} = JSON.parse(json);
|
||||
const parts = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
return parts;
|
||||
} catch (e) {
|
||||
console.error("Error parsing editing state: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_clearStoredEditorState() {
|
||||
localStorage.removeItem(this._editorRoomKey);
|
||||
localStorage.removeItem(this._editorStateKey);
|
||||
}
|
||||
|
||||
_clearPreviousEdit() {
|
||||
if (localStorage.getItem(this._editorRoomKey)) {
|
||||
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`);
|
||||
}
|
||||
}
|
||||
|
||||
_saveStoredEditorState() {
|
||||
const item = SendHistoryManager.createItem(this.model);
|
||||
this._clearPreviousEdit();
|
||||
localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId());
|
||||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
}
|
||||
|
||||
_isSlashCommand() {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_isContentModified(newContent) {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = this.props.editState.getEvent().getContent();
|
||||
|
@ -190,19 +255,114 @@ export default class EditMessageComposer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
_getSlashCommand() {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
return text + part.resourceId;
|
||||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const {cmd, args} = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(cmd, args, roomId) {
|
||||
const result = cmd.run(roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
_sendEdit = async () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
||||
let shouldSend = true;
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId);
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args, roomId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
<p>
|
||||
{ _t("Unrecognised command: %(commandText)s", {commandText}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||
"Did you mean to send this as a message?", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
</div>,
|
||||
button: _t('Send as message'),
|
||||
});
|
||||
const [sendAnyway] = await finished;
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
if (!sendAnyway) return;
|
||||
}
|
||||
}
|
||||
if (shouldSend) {
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
@ -235,22 +395,27 @@ export default class EditMessageComposer extends React.Component {
|
|||
// then when mounting the editor again with the same editor state,
|
||||
// it will set the cursor at the end.
|
||||
this.props.editState.setEditorState(caret, parts);
|
||||
window.removeEventListener("beforeunload", this._saveStoredEditorState);
|
||||
if (this._shouldSaveStoredEditorState) {
|
||||
this._saveStoredEditorState();
|
||||
}
|
||||
}
|
||||
|
||||
_createEditorModel() {
|
||||
const {editState} = this.props;
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
let parts;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore serialized parts from the state
|
||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, parse the body of the event
|
||||
parts = parseEvent(editState.getEvent(), partCreator);
|
||||
//otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
|
||||
}
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this._saveStoredEditorState();
|
||||
}
|
||||
|
||||
_getInitialCaretPosition() {
|
||||
|
|
|
@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import { hasText } from "../../../TextForEvent";
|
||||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -277,6 +277,12 @@ interface IProps {
|
|||
|
||||
// Helper to build permalinks for the room
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
// Symbol of the root node
|
||||
as?: string
|
||||
|
||||
// whether or not to always show timestamps
|
||||
alwaysShowTimestamps?: boolean
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -291,12 +297,15 @@ interface IState {
|
|||
previouslyRequestedKeys: boolean;
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: Relations;
|
||||
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.EventTile")
|
||||
export default class EventTile extends React.Component<IProps, IState> {
|
||||
private suppressReadReceiptAnimation: boolean;
|
||||
private isListeningForReceipts: boolean;
|
||||
private ref: React.RefObject<unknown>;
|
||||
private tile = React.createRef();
|
||||
private replyThread = React.createRef();
|
||||
|
||||
|
@ -322,6 +331,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
previouslyRequestedKeys: false,
|
||||
// The Relations model from the JS SDK for reactions to `mxEvent`
|
||||
reactions: this.getReactions(),
|
||||
|
||||
hover: false,
|
||||
};
|
||||
|
||||
// don't do RR animations until we are mounted
|
||||
|
@ -333,6 +344,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
// to determine if we've already subscribed and use a combination of other flags to find
|
||||
// out if we should even be subscribed at all.
|
||||
this.isListeningForReceipts = false;
|
||||
|
||||
this.ref = React.createRef();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -631,7 +644,18 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// return early if there are no read receipts
|
||||
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
||||
return (<span className="mx_EventTile_readAvatars" />);
|
||||
// We currently must include `mx_EventTile_readAvatars` in the DOM
|
||||
// of all events, as it is the positioned parent of the animated
|
||||
// read receipts. We can't let it unmount when a receipt moves
|
||||
// events, so for now we mount it for all events. Without it, the
|
||||
// animation will start from the top of the timeline (because it
|
||||
// lost its container).
|
||||
// See also https://github.com/vector-im/element-web/issues/17561
|
||||
return (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
<span className="mx_EventTile_readAvatars" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
||||
|
@ -639,7 +663,8 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
const receiptOffset = 15;
|
||||
let left = 0;
|
||||
|
||||
const receipts = this.props.readReceipts || [];
|
||||
const receipts = this.props.readReceipts;
|
||||
|
||||
for (let i = 0; i < receipts.length; ++i) {
|
||||
const receipt = receipts[i];
|
||||
|
||||
|
@ -690,10 +715,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
return <span className="mx_EventTile_readAvatars">
|
||||
{ remText }
|
||||
{ avatars }
|
||||
</span>;
|
||||
return (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
<span className="mx_EventTile_readAvatars">
|
||||
{ remText }
|
||||
{ avatars }
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onSenderProfileClick = event => {
|
||||
|
@ -790,13 +819,6 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
return null;
|
||||
}
|
||||
const eventId = this.props.mxEvent.getId();
|
||||
if (!eventId) {
|
||||
// XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
|
||||
console.error("EventTile attempted to get relations for an event without an ID");
|
||||
// Use event's special `toJSON` method to log key data.
|
||||
console.log(JSON.stringify(this.props.mxEvent, null, 4));
|
||||
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
|
||||
}
|
||||
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
||||
};
|
||||
|
||||
|
@ -892,6 +914,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
||||
}
|
||||
|
||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||
// Local echos have a send "status".
|
||||
const scrollToken = this.props.mxEvent.status
|
||||
? undefined
|
||||
: this.props.mxEvent.getId();
|
||||
|
||||
let avatar;
|
||||
let sender;
|
||||
let avatarSize;
|
||||
|
@ -961,7 +989,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
onFocusChange={this.onActionBarFocusChange}
|
||||
/> : undefined;
|
||||
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
const showTimestamp = this.props.mxEvent.getTs() &&
|
||||
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
|
||||
const timestamp = showTimestamp ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
||||
const keyRequestHelpText =
|
||||
|
@ -1024,68 +1054,71 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
let msgOption;
|
||||
if (this.props.showReadReceipts) {
|
||||
const readAvatars = this.getReadAvatars();
|
||||
msgOption = (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
);
|
||||
msgOption = readAvatars;
|
||||
}
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<div className="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
<div className="mx_EventTile_roomName" key="mx_EventTile_roomName">
|
||||
<RoomAvatar room={room} width={28} height={28} />
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
case 'file_grid': {
|
||||
return (
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
<div className="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
/>
|
||||
</div>,
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
key="mx_EventTile_senderDetailsLink"
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</div>
|
||||
<a
|
||||
className="mx_EventTile_senderDetailsLink"
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ sender }
|
||||
{ timestamp }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
</a>,
|
||||
]);
|
||||
}
|
||||
|
||||
case 'reply':
|
||||
|
@ -1097,29 +1130,34 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
null,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
||||
{ ircTimestamp }
|
||||
{ avatar }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_reply">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
showUrlPreview={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return React.createElement(this.props.as || "li", {
|
||||
"className": classes,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": true,
|
||||
"data-scroll-tokens": scrollToken,
|
||||
}, [
|
||||
ircTimestamp,
|
||||
avatar,
|
||||
sender,
|
||||
ircPadlock,
|
||||
<div className="mx_EventTile_reply" key="mx_EventTile_reply">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this.tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
showUrlPreview={false}
|
||||
/>
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
default: {
|
||||
const thread = ReplyThread.makeThread(
|
||||
|
@ -1128,15 +1166,25 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
this.props.permalinkCreator,
|
||||
this.replyThread,
|
||||
this.props.layout,
|
||||
this.props.alwaysShowTimestamps || this.state.hover,
|
||||
);
|
||||
|
||||
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
||||
return (
|
||||
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
||||
{ ircTimestamp }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_line">
|
||||
React.createElement(this.props.as || "li", {
|
||||
"ref": this.ref,
|
||||
"className": classes,
|
||||
"tabIndex": -1,
|
||||
"aria-live": ariaLive,
|
||||
"aria-atomic": "true",
|
||||
"data-scroll-tokens": scrollToken,
|
||||
"onMouseEnter": () => this.setState({ hover: true }),
|
||||
"onMouseLeave": () => this.setState({ hover: false }),
|
||||
}, [
|
||||
ircTimestamp,
|
||||
sender,
|
||||
ircPadlock,
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
|
@ -1153,16 +1201,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ keyRequestInfo }
|
||||
{ reactionsRow }
|
||||
{ actionBar }
|
||||
</div>
|
||||
{msgOption}
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
{ avatar }
|
||||
</div>
|
||||
);
|
||||
</div>,
|
||||
msgOption,
|
||||
avatar,
|
||||
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1184,7 +1228,7 @@ export function haveTileForEvent(e) {
|
|||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
if (handler === 'messages.TextualEvent') {
|
||||
return TextForEvent.textForEvent(e) !== '';
|
||||
return hasText(e);
|
||||
} else if (handler === 'messages.RoomCreate') {
|
||||
return Boolean(e.getContent()['predecessor']);
|
||||
} else {
|
||||
|
@ -1324,11 +1368,15 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
|
|||
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
|
||||
}
|
||||
|
||||
return <span className="mx_EventTile_readAvatars">
|
||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||
{nonCssBadge}
|
||||
{tooltip}
|
||||
</span>
|
||||
</span>;
|
||||
return (
|
||||
<div className="mx_EventTile_msgOption">
|
||||
<span className="mx_EventTile_readAvatars">
|
||||
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
||||
{nonCssBadge}
|
||||
{tooltip}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 Michael Telatynski
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from '../../../Keyboard';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.ForwardMessage")
|
||||
export default class ForwardMessage extends React.Component {
|
||||
static propTypes = {
|
||||
onCancelClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
_onKeyDown = ev => {
|
||||
switch (ev.key) {
|
||||
case Key.ESCAPE:
|
||||
this.props.onCancelClick();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_ForwardMessage">
|
||||
<h1>{ _t('Please select the destination room for this message') }</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -133,6 +133,12 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
get canInvite() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
return room && room.canInvite(cli.getUserId());
|
||||
}
|
||||
|
||||
_getMembersState(members) {
|
||||
// set the state after determining _showPresence to make sure it's
|
||||
// taken into account while rerendering
|
||||
|
@ -141,6 +147,7 @@ export default class MemberList extends React.Component {
|
|||
members: members,
|
||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||
canInvite: this.canInvite,
|
||||
|
||||
// ideally we'd size this to the page height, but
|
||||
// in practice I find that a little constraining
|
||||
|
@ -196,6 +203,8 @@ export default class MemberList extends React.Component {
|
|||
event.getType() === "m.room.third_party_invite") {
|
||||
this._updateList();
|
||||
}
|
||||
|
||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||
};
|
||||
|
||||
_updateList = rate_limited_func(() => {
|
||||
|
@ -229,6 +238,8 @@ export default class MemberList extends React.Component {
|
|||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
});
|
||||
|
@ -243,6 +254,8 @@ export default class MemberList extends React.Component {
|
|||
m.membership === 'join' || m.membership === 'invite'
|
||||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
@ -342,13 +355,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, "");
|
||||
const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, "");
|
||||
// console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`);
|
||||
return nameA.localeCompare(nameB, {
|
||||
ignorePunctuation: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
|
@ -413,7 +420,7 @@ export default class MemberList extends React.Component {
|
|||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -455,8 +462,6 @@ export default class MemberList extends React.Component {
|
|||
let inviteButton;
|
||||
|
||||
if (room && room.getMyMembership() === 'join') {
|
||||
const canInvite = room.canInvite(cli.getUserId());
|
||||
|
||||
let inviteButtonText = _t("Invite to this room");
|
||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
|
@ -467,7 +472,7 @@ export default class MemberList extends React.Component {
|
|||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}>
|
||||
<span>{ inviteButtonText }</span>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
@ -477,10 +482,10 @@ export default class MemberList extends React.Component {
|
|||
if (this._getChildCountInvited() > 0) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
}
|
||||
|
||||
const footer = (
|
||||
|
@ -513,9 +518,9 @@ export default class MemberList extends React.Component {
|
|||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
|
|
|
@ -31,6 +31,17 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import {showSpaceInvite} from "../../../utils/space";
|
||||
|
||||
import { privateShouldBeEncrypted } from "../../../createRoom";
|
||||
|
||||
import EventTileBubble from "../messages/EventTileBubble";
|
||||
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
|
||||
|
||||
function hasExpectedEncryptionSettings(room): boolean {
|
||||
const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId);
|
||||
const isPublic: boolean = room.getJoinRule() === "public";
|
||||
return isPublic || !privateShouldBeEncrypted() || isEncrypted;
|
||||
}
|
||||
|
||||
const NewRoomIntro = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const {room, roomId} = useContext(RoomContext);
|
||||
|
@ -166,7 +177,31 @@ const NewRoomIntro = () => {
|
|||
</React.Fragment>;
|
||||
}
|
||||
|
||||
function openRoomSettings(event) {
|
||||
event.preventDefault();
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
});
|
||||
}
|
||||
|
||||
const sub2 = _t(
|
||||
"Your private messages are normally encrypted, but this room isn't. "+
|
||||
"Usually this is due to an unsupported device or method being used, " +
|
||||
"like email invites. <a>Enable encryption in settings.</a>", {},
|
||||
{ a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> },
|
||||
);
|
||||
|
||||
return <div className="mx_NewRoomIntro">
|
||||
|
||||
{ !hasExpectedEncryptionSettings(room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
title={_t("End-to-end encryption isn't enabled")}
|
||||
subtitle={sub2}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ body }
|
||||
</div>;
|
||||
};
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
|
||||
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 PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MessageEvent from "../messages/MessageEvent";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {formatFullDate} from '../../../DateUtils';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.PinnedEventTile")
|
||||
export default class PinnedEventTile extends React.Component {
|
||||
static propTypes = {
|
||||
mxRoom: PropTypes.object.isRequired,
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
onUnpinned: PropTypes.func,
|
||||
};
|
||||
|
||||
onTileClicked = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
onUnpinClicked = () => {
|
||||
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
|
||||
// Nothing to do: already unpinned
|
||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
||||
} else {
|
||||
const pinned = pinnedEvents.getContent().pinned;
|
||||
const index = pinned.indexOf(this.props.mxEvent.getId());
|
||||
if (index !== -1) {
|
||||
pinned.splice(index, 1);
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
|
||||
.then(() => {
|
||||
if (this.props.onUnpinned) this.props.onUnpinned();
|
||||
});
|
||||
} else if (this.props.onUnpinned) this.props.onUnpinned();
|
||||
}
|
||||
};
|
||||
|
||||
_canUnpin() {
|
||||
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
|
||||
}
|
||||
|
||||
render() {
|
||||
const sender = this.props.mxEvent.getSender();
|
||||
// Get the latest sender profile rather than historical
|
||||
const senderProfile = this.props.mxRoom.getMember(sender);
|
||||
const avatarSize = 40;
|
||||
|
||||
let unpinButton = null;
|
||||
if (this._canUnpin()) {
|
||||
unpinButton = (
|
||||
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
|
||||
<img src={require("../../../../res/img/cancel-red.svg")} width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_PinnedEventTile">
|
||||
<div className="mx_PinnedEventTile_actions">
|
||||
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
|
||||
{ _t("Jump to message") }
|
||||
</AccessibleButton>
|
||||
{ unpinButton }
|
||||
</div>
|
||||
|
||||
<span className="mx_PinnedEventTile_senderAvatar">
|
||||
<MemberAvatar member={senderProfile} width={avatarSize} height={avatarSize} fallbackUserId={sender} />
|
||||
</span>
|
||||
<span className="mx_PinnedEventTile_sender">
|
||||
{ senderProfile ? senderProfile.name : sender }
|
||||
</span>
|
||||
<span className="mx_PinnedEventTile_timestamp">
|
||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
||||
</span>
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent
|
||||
mxEvent={this.props.mxEvent}
|
||||
className="mx_PinnedEventTile_body"
|
||||
maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
104
src/components/views/rooms/PinnedEventTile.tsx
Normal file
104
src/components/views/rooms/PinnedEventTile.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2021 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 { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MessageEvent from "../messages/MessageEvent";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatDate } from '../../../DateUtils';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
event: MatrixEvent;
|
||||
onUnpinClicked?(): void;
|
||||
}
|
||||
|
||||
const AVATAR_SIZE = 24;
|
||||
|
||||
@replaceableComponent("views.rooms.PinnedEventTile")
|
||||
export default class PinnedEventTile extends React.Component<IProps> {
|
||||
public static contextType = MatrixClientContext;
|
||||
|
||||
private onTileClicked = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.event.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.event.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const sender = this.props.event.getSender();
|
||||
const senderProfile = this.props.room.getMember(sender);
|
||||
|
||||
let unpinButton = null;
|
||||
if (this.props.onUnpinClicked) {
|
||||
unpinButton = (
|
||||
<AccessibleTooltipButton
|
||||
onClick={this.props.onUnpinClicked}
|
||||
className="mx_PinnedEventTile_unpinButton"
|
||||
title={_t("Unpin")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_PinnedEventTile">
|
||||
<MemberAvatar
|
||||
className="mx_PinnedEventTile_senderAvatar"
|
||||
member={senderProfile}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
fallbackUserId={sender}
|
||||
/>
|
||||
|
||||
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
|
||||
{ senderProfile?.name || sender }
|
||||
</span>
|
||||
|
||||
{ unpinButton }
|
||||
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent
|
||||
mxEvent={this.props.event}
|
||||
className="mx_PinnedEventTile_body"
|
||||
maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx_PinnedEventTile_footer">
|
||||
<span className="mx_PinnedEventTile_timestamp">
|
||||
{ formatDate(new Date(this.props.event.getTs())) }
|
||||
</span>
|
||||
|
||||
<AccessibleButton onClick={this.onTileClicked} kind="link">
|
||||
{ _t("View message") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 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 PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import PinnedEventTile from "./PinnedEventTile";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.PinnedEventsPanel")
|
||||
export default class PinnedEventsPanel extends React.Component {
|
||||
static propTypes = {
|
||||
// The Room from the js-sdk we're going to show pinned events for
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
onCancelClick: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._updatePinnedMessages();
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
||||
}
|
||||
}
|
||||
|
||||
_onStateEvent = ev => {
|
||||
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
|
||||
this._updatePinnedMessages();
|
||||
}
|
||||
};
|
||||
|
||||
_updatePinnedMessages = () => {
|
||||
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
|
||||
this.setState({ loading: false, pinned: [] });
|
||||
} else {
|
||||
const promises = [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
pinnedEvents.getContent().pinned.map((eventId) => {
|
||||
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
|
||||
(timeline) => {
|
||||
const event = timeline.getEvents().find((e) => e.getId() === eventId);
|
||||
return {eventId, timeline, event};
|
||||
}).catch((err) => {
|
||||
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
|
||||
console.error(err);
|
||||
return null; // return lack of context to avoid unhandled errors
|
||||
}));
|
||||
});
|
||||
|
||||
Promise.all(promises).then((contexts) => {
|
||||
// Filter out the messages before we try to render them
|
||||
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
|
||||
|
||||
this.setState({ loading: false, pinned });
|
||||
});
|
||||
}
|
||||
|
||||
this._updateReadState();
|
||||
};
|
||||
|
||||
_updateReadState() {
|
||||
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
if (!pinnedEvents) return; // nothing to read
|
||||
|
||||
let readStateEvents = [];
|
||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
||||
readStateEvents = readPinsEvent.getContent().event_ids || [];
|
||||
}
|
||||
|
||||
if (!readStateEvents.includes(pinnedEvents.getId())) {
|
||||
readStateEvents.push(pinnedEvents.getId());
|
||||
|
||||
// Only keep the last 10 event IDs to avoid infinite growth
|
||||
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
|
||||
|
||||
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
|
||||
event_ids: readStateEvents,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getPinnedTiles() {
|
||||
if (this.state.pinned.length === 0) {
|
||||
return (<div>{ _t("No pinned messages.") }</div>);
|
||||
}
|
||||
|
||||
return this.state.pinned.map((context) => {
|
||||
return (
|
||||
<PinnedEventTile
|
||||
key={context.event.getId()}
|
||||
mxRoom={this.props.room}
|
||||
mxEvent={context.event}
|
||||
onUnpinned={this._updatePinnedMessages}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let tiles = <div>{ _t("Loading...") }</div>;
|
||||
if (this.state && !this.state.loading) {
|
||||
tiles = this._getPinnedTiles();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_PinnedEventsPanel">
|
||||
<div className="mx_PinnedEventsPanel_body">
|
||||
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
|
||||
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18" />
|
||||
</AccessibleButton>
|
||||
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
|
||||
{ tiles }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -89,12 +89,13 @@ export default class ReplyPreview extends React.Component {
|
|||
</div>
|
||||
<div className="mx_ReplyPreview_clear" />
|
||||
<EventTile
|
||||
last={true}
|
||||
alwaysShowTimestamps={true}
|
||||
tileShape="reply_preview"
|
||||
mxEvent={this.state.event}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
as="div"
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
|
|
@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|||
import Analytics from "../../../Analytics";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { CSSTransition } from "react-transition-group";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Toolbar from "../../../accessibility/Toolbar";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
|
||||
public render(): React.ReactElement {
|
||||
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
||||
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
||||
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
||||
return (
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_RoomBreadcrumbs_crumb"
|
||||
|
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
<DecoratedRoomAvatar
|
||||
room={r}
|
||||
avatarSize={32}
|
||||
tag={roomTag}
|
||||
displayBadge={true}
|
||||
forceCount={true}
|
||||
/>
|
||||
|
|
|
@ -19,20 +19,18 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {PlaceCallType} from "../../../CallHandler";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.RoomHeader")
|
||||
export default class RoomHeader extends React.Component {
|
||||
|
@ -41,10 +39,8 @@ export default class RoomHeader extends React.Component {
|
|||
oobData: PropTypes.object,
|
||||
inRoom: PropTypes.bool,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onPinnedClick: PropTypes.func,
|
||||
onSearchClick: PropTypes.func,
|
||||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
|
@ -54,20 +50,17 @@ export default class RoomHeader extends React.Component {
|
|||
static defaultProps = {
|
||||
editing: false,
|
||||
inRoom: false,
|
||||
onCancelClick: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.events", this._onRoomStateEvents);
|
||||
cli.on("Room.accountData", this._onRoomAccountData);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
cli.removeListener("Room.accountData", this._onRoomAccountData);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,52 +73,13 @@ export default class RoomHeader extends React.Component {
|
|||
this._rateLimitedUpdate();
|
||||
};
|
||||
|
||||
_onRoomAccountData = (event, room) => {
|
||||
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
|
||||
if (event.getType() !== "im.vector.room.read_pins") return;
|
||||
|
||||
this._rateLimitedUpdate();
|
||||
};
|
||||
|
||||
_rateLimitedUpdate = new RateLimitedFunc(function() {
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
this.forceUpdate();
|
||||
}, 500);
|
||||
|
||||
_hasUnreadPins() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
|
||||
return false; // no pins == nothing to read
|
||||
}
|
||||
|
||||
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
|
||||
if (readPinsEvent && readPinsEvent.getContent()) {
|
||||
const readStateEvents = readPinsEvent.getContent().event_ids || [];
|
||||
if (readStateEvents) {
|
||||
return !readStateEvents.includes(currentPinEvent.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// There's pins, and we haven't read any of them
|
||||
return true;
|
||||
}
|
||||
|
||||
_hasPins() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
||||
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
|
||||
}
|
||||
|
||||
render() {
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
||||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
|
@ -177,30 +131,11 @@ export default class RoomHeader extends React.Component {
|
|||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={32}
|
||||
tag={DefaultTagID.Untagged} // to apply room publicity badging
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
|
||||
let pinsIndicator = null;
|
||||
if (this._hasUnreadPins()) {
|
||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
|
||||
} else if (this._hasPins()) {
|
||||
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
|
||||
}
|
||||
|
||||
pinnedEventsButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
|
||||
onClick={this.props.onPinnedClick}
|
||||
title={_t("Pinned Messages")}
|
||||
>
|
||||
{ pinsIndicator }
|
||||
</AccessibleTooltipButton>;
|
||||
}
|
||||
|
||||
let forgetButton;
|
||||
if (this.props.onForgetClick) {
|
||||
forgetButton =
|
||||
|
@ -250,7 +185,6 @@ export default class RoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_buttons">
|
||||
{ videoCallButton }
|
||||
{ voiceCallButton }
|
||||
{ pinnedEventsButton }
|
||||
{ forgetButton }
|
||||
{ appsButton }
|
||||
{ searchButton }
|
||||
|
@ -265,9 +199,8 @@ export default class RoomHeader extends React.Component {
|
|||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ cancelButton }
|
||||
{ rightRow }
|
||||
<RoomHeaderButtons />
|
||||
<RoomHeaderButtons room={this.props.room} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -46,17 +46,17 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
|
||||
import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
|
||||
import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onResize: () => void;
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
isMinimized: boolean;
|
||||
activeSpace: Room;
|
||||
|
@ -66,7 +66,7 @@ interface IState {
|
|||
sublists: ITagMap;
|
||||
isNameFiltering: boolean;
|
||||
currentRoomId?: string;
|
||||
suggestedRooms: ISpaceSummaryRoom[];
|
||||
suggestedRooms: ISuggestedRoom[];
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
|
@ -363,7 +363,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
return room;
|
||||
};
|
||||
|
||||
private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => {
|
||||
private updateSuggestedRooms = (suggestedRooms: ISuggestedRoom[]) => {
|
||||
this.setState({ suggestedRooms });
|
||||
};
|
||||
|
||||
|
@ -428,7 +428,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
return this.state.suggestedRooms.map(room => {
|
||||
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
|
||||
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room");
|
||||
const avatar = (
|
||||
<RoomAvatar
|
||||
oobData={{
|
||||
|
@ -443,7 +443,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const viewRoom = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_alias: room.canonical_alias || room.aliases?.[0],
|
||||
room_id: room.room_id,
|
||||
via_servers: room.viaServers,
|
||||
oobData: {
|
||||
avatarUrl: room.avatar_url,
|
||||
name,
|
||||
|
@ -536,11 +538,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
showSkeleton={showSkeleton}
|
||||
extraTiles={extraTiles}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||
onListCollapse={this.props.onListCollapse}
|
||||
/>
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
|
||||
const RoomListNumResults: React.FC = () => {
|
||||
interface IProps {
|
||||
onVisibilityChange?: () => void
|
||||
}
|
||||
|
||||
const RoomListNumResults: React.FC<IProps> = ({ onVisibilityChange }) => {
|
||||
const [count, setCount] = useState<number>(null);
|
||||
useEventEmitter(RoomListStore.instance, LISTS_UPDATE_EVENT, () => {
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
|
@ -32,6 +36,12 @@ const RoomListNumResults: React.FC = () => {
|
|||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (onVisibilityChange) {
|
||||
onVisibilityChange();
|
||||
}
|
||||
}, [count, onVisibilityChange]);
|
||||
|
||||
if (typeof count !== "number") return null;
|
||||
|
||||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
import * as React from "react";
|
||||
import { createRef, ReactComponentElement } from "react";
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
|
@ -73,11 +74,11 @@ interface IProps {
|
|||
addRoomLabel: string;
|
||||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
showSkeleton?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||
onListCollapse?: (isExpanded: boolean) => void;
|
||||
|
||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||
}
|
||||
|
@ -104,6 +105,7 @@ interface IState {
|
|||
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
private headerButton = createRef<HTMLDivElement>();
|
||||
private sublistRef = createRef<HTMLDivElement>();
|
||||
private tilesRef = createRef<HTMLDivElement>();
|
||||
private dispatcherRef: string;
|
||||
private layout: ListLayout;
|
||||
private heightAtStart: number;
|
||||
|
@ -245,11 +247,15 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
// Using the passive option to not block the main thread
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
||||
}
|
||||
|
||||
private onListsUpdated = () => {
|
||||
|
@ -259,7 +265,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const nameCondition = RoomListStore.instance.getFirstNameFilterCondition();
|
||||
if (nameCondition) {
|
||||
stateUpdates.filteredExtraTiles = this.props.extraTiles
|
||||
.filter(t => nameCondition.matches(t.props.displayName || ""));
|
||||
.filter(t => nameCondition.matches(normalize(t.props.displayName || "")));
|
||||
} else if (this.state.filteredExtraTiles) {
|
||||
stateUpdates.filteredExtraTiles = null;
|
||||
}
|
||||
|
@ -448,8 +454,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const sublist = possibleSticky.parentElement.parentElement;
|
||||
const list = sublist.parentElement.parentElement;
|
||||
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
|
||||
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
|
||||
const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
|
||||
const listScrollTop = Math.round(list.scrollTop);
|
||||
const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
|
||||
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
|
||||
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
|
||||
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
|
||||
|
||||
|
@ -472,7 +479,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private toggleCollapsed = () => {
|
||||
this.layout.isCollapsed = this.state.isExpanded;
|
||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
||||
if (this.props.onListCollapse) {
|
||||
this.props.onListCollapse(!this.layout.isCollapsed)
|
||||
}
|
||||
};
|
||||
|
||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||
|
@ -529,7 +538,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
tiles.push(<RoomTile
|
||||
room={room}
|
||||
key={`room-${room.roomId}`}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
showMessagePreview={this.layout.showPreviews}
|
||||
isMinimized={this.props.isMinimized}
|
||||
tag={this.props.tagId}
|
||||
|
@ -753,7 +761,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
|
||||
private onScrollPrevent(e: Event) {
|
||||
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
||||
// this fixes https://github.com/vector-im/element-web/issues/14413
|
||||
(e.target as HTMLDivElement).scrollTop = 0;
|
||||
|
@ -882,7 +890,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
className="mx_RoomSublist_resizeBox"
|
||||
enable={handles}
|
||||
>
|
||||
<div className="mx_RoomSublist_tiles" onScroll={this.onScrollPrevent}>
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{visibleTiles}
|
||||
</div>
|
||||
{showNButton}
|
||||
|
|
|
@ -53,14 +53,12 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getUnsentMessages } from "../../structures/RoomStatusBar";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
showMessagePreview: boolean;
|
||||
isMinimized: boolean;
|
||||
tag: TagID;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
}
|
||||
|
||||
type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||
|
@ -100,13 +98,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
hasUnsentEvents: this.countUnsentEvents() > 0,
|
||||
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: this.generatePreview(),
|
||||
messagePreview: "",
|
||||
};
|
||||
this.generatePreview();
|
||||
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.on("middlePanelResized", this.onResize);
|
||||
}
|
||||
}
|
||||
|
||||
private countUnsentEvents(): number {
|
||||
|
@ -121,12 +118,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.forceUpdate(); // notification state changed - update
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
if (this.showMessagePreview && !this.state.messagePreview) {
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
}
|
||||
};
|
||||
|
||||
private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => {
|
||||
if (!room?.roomId === this.props.room.roomId) return;
|
||||
this.setState({hasUnsentEvents: this.countUnsentEvents() > 0});
|
||||
|
@ -146,8 +137,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
|
||||
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
const showMessageChanged = prevProps.showMessagePreview !== this.props.showMessagePreview;
|
||||
const minimizedChanged = prevProps.isMinimized !== this.props.isMinimized;
|
||||
if (showMessageChanged || minimizedChanged) {
|
||||
this.generatePreview();
|
||||
}
|
||||
if (prevProps.room?.roomId !== this.props.room?.roomId) {
|
||||
MessagePreviewStore.instance.off(
|
||||
|
@ -206,9 +199,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
this.props.room.off("Room.name", this.onRoomNameUpdate);
|
||||
}
|
||||
if (this.props.resizeNotifier) {
|
||||
this.props.resizeNotifier.off("middlePanelResized", this.onResize);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
|
||||
|
@ -236,17 +226,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private onRoomPreviewChanged = (room: Room) => {
|
||||
if (this.props.room && room.roomId === this.props.room.roomId) {
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
this.setState({messagePreview: this.generatePreview()});
|
||||
this.generatePreview();
|
||||
}
|
||||
};
|
||||
|
||||
private generatePreview(): string | null {
|
||||
private async generatePreview() {
|
||||
if (!this.showMessagePreview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag);
|
||||
this.setState({ messagePreview });
|
||||
}
|
||||
|
||||
private scrollIntoView = () => {
|
||||
|
@ -576,7 +566,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
const roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={32}
|
||||
tag={this.props.tag}
|
||||
displayBadge={this.props.isMinimized}
|
||||
oobData={({avatarUrl: roomProfile.avatarMxc})}
|
||||
/>;
|
||||
|
|
|
@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
|
||||
export default class RoomUpgradeWarningBar extends React.Component {
|
||||
export default class RoomUpgradeWarningBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
room: PropTypes.object.isRequired,
|
||||
recommendation: PropTypes.object.isRequired,
|
||||
|
|
|
@ -47,6 +47,7 @@ export default class SearchResultTile extends React.Component {
|
|||
|
||||
const ts1 = mxEv.getTs();
|
||||
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
|
||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||
|
||||
const timeline = result.context.getTimeline();
|
||||
for (let j = 0; j < timeline.length; j++) {
|
||||
|
@ -67,6 +68,7 @@ export default class SearchResultTile extends React.Component {
|
|||
highlightLink={this.props.resultLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -16,23 +16,9 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
// cancel button which is shared between room header and simple room header
|
||||
export function CancelButton(props) {
|
||||
const {onClick} = props;
|
||||
|
||||
return (
|
||||
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
|
||||
<img src={require("../../../../res/img/cancel.svg")} className='mx_filterFlipColor'
|
||||
width="18" height="18" alt={_t("Cancel")} />
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* A stripped-down room header used for things like the user settings
|
||||
* and room directory.
|
||||
|
@ -41,18 +27,13 @@ export function CancelButton(props) {
|
|||
export default class SimpleRoomHeader extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
onCancelClick: PropTypes.func,
|
||||
|
||||
// `src` to a TintableSvg. Optional.
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
let cancelButton;
|
||||
let icon;
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
if (this.props.icon) {
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
icon = <TintableSvg
|
||||
|
@ -62,13 +43,10 @@ export default class SimpleRoomHeader extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader" >
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -40,7 +40,7 @@ const STICKERPICKER_Z_INDEX = 3500;
|
|||
const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
||||
|
||||
@replaceableComponent("views.rooms.Stickerpicker")
|
||||
export default class Stickerpicker extends React.Component {
|
||||
export default class Stickerpicker extends React.PureComponent {
|
||||
static currentWidget;
|
||||
|
||||
constructor(props) {
|
||||
|
@ -341,27 +341,33 @@ export default class Stickerpicker extends React.Component {
|
|||
* @param {Event} ev Event that triggered the function call
|
||||
*/
|
||||
_onHideStickersClick(ev) {
|
||||
this.setState({showStickers: false});
|
||||
if (this.state.showStickers) {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the window is resized
|
||||
*/
|
||||
_onResize() {
|
||||
this.setState({showStickers: false});
|
||||
if (this.state.showStickers) {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The stickers picker was hidden
|
||||
*/
|
||||
_onFinished() {
|
||||
this.setState({showStickers: false});
|
||||
if (this.state.showStickers) {
|
||||
this.setState({showStickers: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the integration manager on the stickers integration page
|
||||
*/
|
||||
_launchManageIntegrations() {
|
||||
_launchManageIntegrations = () => {
|
||||
// TODO: Open the right integration manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
|
@ -376,7 +382,7 @@ export default class Stickerpicker extends React.Component {
|
|||
this.state.widgetId,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let stickerPicker;
|
||||
|
@ -395,7 +401,7 @@ export default class Stickerpicker extends React.Component {
|
|||
key="controls_hide_stickers"
|
||||
className={className}
|
||||
onClick={this._onHideStickersClick}
|
||||
active={this.state.showStickers}
|
||||
active={this.state.showStickers.toString()}
|
||||
title={_t("Hide Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
|
|
|
@ -16,36 +16,45 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import * as WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { compare } from "../../../utils/strings";
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.WhoIsTypingTile")
|
||||
export default class WhoIsTypingTile extends React.Component {
|
||||
static propTypes = {
|
||||
// the room this statusbar is representing.
|
||||
room: PropTypes.object.isRequired,
|
||||
onShown: PropTypes.func,
|
||||
onHidden: PropTypes.func,
|
||||
// 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: PropTypes.number,
|
||||
};
|
||||
|
||||
export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
whoIsTypingLimit: 3,
|
||||
};
|
||||
|
||||
state = {
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
// 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: {},
|
||||
};
|
||||
|
||||
|
@ -71,37 +80,39 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||
client.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
|
||||
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
|
||||
}
|
||||
|
||||
_isVisible(state) {
|
||||
private _isVisible(state: IState): boolean {
|
||||
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
|
||||
}
|
||||
|
||||
isVisible = () => {
|
||||
public isVisible = (): boolean => {
|
||||
return this._isVisible(this.state);
|
||||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
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);
|
||||
this.setState({usersTyping});
|
||||
if (usersTyping.length !== this.state.usersTyping.length) {
|
||||
this.setState({usersTyping});
|
||||
}
|
||||
// abort timer if any
|
||||
this._abortUserTimer(userId);
|
||||
this.abortUserTimer(userId);
|
||||
}
|
||||
};
|
||||
|
||||
onRoomMemberTyping = (ev, member) => {
|
||||
private onRoomMemberTyping = (): void => {
|
||||
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
|
||||
this.setState({
|
||||
delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping),
|
||||
delayedStopTypingTimers: this.updateDelayedStopTypingTimers(usersTyping),
|
||||
usersTyping,
|
||||
});
|
||||
};
|
||||
|
||||
_updateDelayedStopTypingTimers(usersTyping) {
|
||||
private updateDelayedStopTypingTimers(usersTyping: RoomMember[]): Record<string, Timer> {
|
||||
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
|
||||
return !usersTyping.some((b) => a.userId === b.userId);
|
||||
});
|
||||
|
@ -129,7 +140,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
delayedStopTypingTimers[m.userId] = timer;
|
||||
timer.start();
|
||||
timer.finished().then(
|
||||
() => this._removeUserTimer(m.userId), // on elapsed
|
||||
() => this.removeUserTimer(m.userId), // on elapsed
|
||||
() => {/* aborted */},
|
||||
);
|
||||
}
|
||||
|
@ -139,15 +150,15 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
return delayedStopTypingTimers;
|
||||
}
|
||||
|
||||
_abortUserTimer(userId) {
|
||||
private abortUserTimer(userId: string): void {
|
||||
const timer = this.state.delayedStopTypingTimers[userId];
|
||||
if (timer) {
|
||||
timer.abort();
|
||||
this._removeUserTimer(userId);
|
||||
this.removeUserTimer(userId);
|
||||
}
|
||||
}
|
||||
|
||||
_removeUserTimer(userId) {
|
||||
private removeUserTimer(userId: string): void {
|
||||
const timer = this.state.delayedStopTypingTimers[userId];
|
||||
if (timer) {
|
||||
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
|
||||
|
@ -156,7 +167,7 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_renderTypingIndicatorAvatars(users, limit) {
|
||||
private renderTypingIndicatorAvatars(users: RoomMember[], limit: number): JSX.Element[] {
|
||||
let othersCount = 0;
|
||||
if (users.length > limit) {
|
||||
othersCount = users.length - limit + 1;
|
||||
|
@ -197,20 +208,20 @@ export default class WhoIsTypingTile extends React.Component {
|
|||
usersTyping = usersTyping.concat(stoppedUsersOnTimer);
|
||||
// sort them so the typing members don't change order when
|
||||
// moved to delayedStopTypingTimers
|
||||
usersTyping.sort((a, b) => a.name.localeCompare(b.name));
|
||||
usersTyping.sort((a, b) => compare(a.name, b.name));
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
usersTyping,
|
||||
this.props.whoIsTypingLimit,
|
||||
);
|
||||
if (!typingString) {
|
||||
return (<div className="mx_WhoIsTypingTile_empty" />);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
{ this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">
|
||||
{ typingString }
|
Loading…
Add table
Add a link
Reference in a new issue