Merge branches 'develop' and 't3chguy/emoji_picker_composer' of github.com:matrix-org/matrix-react-sdk into t3chguy/emoji_picker_composer
Conflicts: src/components/views/rooms/MessageComposer.js
This commit is contained in:
commit
ccd0c952e3
1131 changed files with 52941 additions and 24634 deletions
|
@ -18,12 +18,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
|
@ -34,7 +34,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'AppsDrawer',
|
||||
|
||||
propTypes: {
|
||||
|
@ -55,13 +55,10 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
WidgetEchoStore.on('update', this._updateApps);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
},
|
||||
|
||||
|
@ -71,10 +68,11 @@ module.exports = createReactClass({
|
|||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
WidgetEchoStore.removeListener('update', this._updateApps);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
// Room has changed probably, update apps
|
||||
this._updateApps();
|
||||
},
|
||||
|
@ -83,12 +81,14 @@ module.exports = createReactClass({
|
|||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// Note: these booleans are awkward because localstorage is fundamentally
|
||||
// string-based. We also do exact equality on the strings later on.
|
||||
if (action.show) {
|
||||
localStorage.removeItem(hideWidgetKey);
|
||||
localStorage.setItem(hideWidgetKey, "false");
|
||||
} else {
|
||||
// Store hidden state of widget
|
||||
// Don't show if previously hidden
|
||||
localStorage.setItem(hideWidgetKey, true);
|
||||
localStorage.setItem(hideWidgetKey, "true");
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -160,11 +160,7 @@ module.exports = createReactClass({
|
|||
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
eventId={app.eventId}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
app={app}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
|
|
|
@ -17,28 +17,49 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
|
||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Autocompleter from '../../../autocomplete/Autocompleter';
|
||||
import {sleep} from "../../../utils/promise";
|
||||
|
||||
const COMPOSER_SELECTED = 0;
|
||||
|
||||
export const generateCompletionDomId = (number) => `mx_Autocomplete_Completion_${number}`;
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
interface IProps {
|
||||
// the query string for which to show autocomplete suggestions
|
||||
query: string;
|
||||
// method invoked with range and text content when completion is confirmed
|
||||
onConfirm: (ICompletion) => void;
|
||||
// method invoked when selected (if any) completion changes
|
||||
onSelectionChange?: (ICompletion, number) => void;
|
||||
selection: ISelectionRange;
|
||||
// The room in which we're autocompleting
|
||||
room: Room;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
completions: IProviderCompletions[];
|
||||
completionList: ICompletion[];
|
||||
selectionOffset: number;
|
||||
shouldShowCompletions: boolean;
|
||||
hide: boolean;
|
||||
forceComplete: boolean;
|
||||
}
|
||||
|
||||
export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
||||
autocompleter: Autocompleter;
|
||||
queryRequested: string;
|
||||
debounceCompletionsRequest: NodeJS.Timeout;
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.autocompleter = new Autocompleter(props.room);
|
||||
this.completionPromise = null;
|
||||
this.hide = this.hide.bind(this);
|
||||
this.onCompletionClicked = this.onCompletionClicked.bind(this);
|
||||
|
||||
this.state = {
|
||||
// list of completionResults, each containing completions
|
||||
|
@ -57,13 +78,15 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
forceComplete: false,
|
||||
};
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._applyNewProps();
|
||||
this.applyNewProps();
|
||||
}
|
||||
|
||||
_applyNewProps(oldQuery, oldRoom) {
|
||||
private applyNewProps(oldQuery?: string, oldRoom?: Room) {
|
||||
if (oldRoom && this.props.room.roomId !== oldRoom.roomId) {
|
||||
this.autocompleter.destroy();
|
||||
this.autocompleter = new Autocompleter(this.props.room);
|
||||
|
@ -81,7 +104,7 @@ export default class Autocomplete extends React.Component {
|
|||
this.autocompleter.destroy();
|
||||
}
|
||||
|
||||
complete(query, selection) {
|
||||
complete(query: string, selection: ISelectionRange) {
|
||||
this.queryRequested = query;
|
||||
if (this.debounceCompletionsRequest) {
|
||||
clearTimeout(this.debounceCompletionsRequest);
|
||||
|
@ -112,7 +135,7 @@ export default class Autocomplete extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
processQuery(query, selection) {
|
||||
processQuery(query: string, selection: ISelectionRange) {
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete,
|
||||
).then((completions) => {
|
||||
|
@ -124,7 +147,7 @@ export default class Autocomplete extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
processCompletions(completions) {
|
||||
processCompletions(completions: IProviderCompletions[]) {
|
||||
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||
|
||||
// Reset selection when completion list becomes empty.
|
||||
|
@ -159,7 +182,7 @@ export default class Autocomplete extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
hasSelection(): bool {
|
||||
hasSelection(): boolean {
|
||||
return this.countCompletions() > 0 && this.state.selectionOffset !== 0;
|
||||
}
|
||||
|
||||
|
@ -168,7 +191,7 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
moveSelection(delta): ?Completion {
|
||||
moveSelection(delta: number) {
|
||||
const completionCount = this.countCompletions();
|
||||
if (completionCount === 0) return; // there are no items to move the selection through
|
||||
|
||||
|
@ -177,7 +200,7 @@ export default class Autocomplete extends React.Component {
|
|||
this.setSelection(index);
|
||||
}
|
||||
|
||||
onEscape(e): boolean {
|
||||
onEscape(e: KeyboardEvent): boolean {
|
||||
const completionCount = this.countCompletions();
|
||||
if (completionCount === 0) {
|
||||
// autocomplete is already empty, so don't preventDefault
|
||||
|
@ -190,9 +213,14 @@ export default class Autocomplete extends React.Component {
|
|||
this.hide();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
|
||||
}
|
||||
hide = () => {
|
||||
this.setState({
|
||||
hide: true,
|
||||
selectionOffset: 0,
|
||||
completions: [],
|
||||
completionList: [],
|
||||
});
|
||||
};
|
||||
|
||||
forceComplete() {
|
||||
return new Promise((resolve) => {
|
||||
|
@ -207,7 +235,7 @@ export default class Autocomplete extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onCompletionClicked(selectionOffset: number): boolean {
|
||||
onCompletionClicked = (selectionOffset: number): boolean => {
|
||||
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
|
||||
return false;
|
||||
}
|
||||
|
@ -216,7 +244,7 @@ export default class Autocomplete extends React.Component {
|
|||
this.hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset, hide: false});
|
||||
|
@ -225,28 +253,24 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this._applyNewProps(prevProps.query, prevProps.room);
|
||||
componentDidUpdate(prevProps: IProps) {
|
||||
this.applyNewProps(prevProps.query, prevProps.room);
|
||||
// this is the selected completion, so scroll it into view if needed
|
||||
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
|
||||
if (selectedCompletion && this.container) {
|
||||
if (selectedCompletion && this.containerRef.current) {
|
||||
const domNode = ReactDOM.findDOMNode(selectedCompletion);
|
||||
const offsetTop = domNode && domNode.offsetTop;
|
||||
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
|
||||
offsetTop < this.container.scrollTop) {
|
||||
this.container.scrollTop = offsetTop - this.container.offsetTop;
|
||||
const offsetTop = domNode && (domNode as HTMLElement).offsetTop;
|
||||
if (offsetTop > this.containerRef.current.scrollTop + this.containerRef.current.offsetHeight ||
|
||||
offsetTop < this.containerRef.current.scrollTop) {
|
||||
this.containerRef.current.scrollTop = offsetTop - this.containerRef.current.offsetTop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(state, func) {
|
||||
super.setState(state, func);
|
||||
}
|
||||
|
||||
render() {
|
||||
let position = 1;
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, i) => {
|
||||
const completions = completionResult.completions.map((completion, j) => {
|
||||
const selected = position === this.state.selectionOffset;
|
||||
const className = classNames('mx_Autocomplete_Completion', {selected});
|
||||
const componentPosition = position;
|
||||
|
@ -257,7 +281,7 @@ export default class Autocomplete extends React.Component {
|
|||
};
|
||||
|
||||
return React.cloneElement(completion.component, {
|
||||
"key": i,
|
||||
"key": j,
|
||||
"ref": `completion${componentPosition}`,
|
||||
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
|
||||
className,
|
||||
|
@ -276,23 +300,9 @@ export default class Autocomplete extends React.Component {
|
|||
}).filter((completion) => !!completion);
|
||||
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
<div className="mx_Autocomplete" ref={this.containerRef}>
|
||||
{ renderedCompletions }
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
Autocomplete.propTypes = {
|
||||
// the query string for which to show autocomplete suggestions
|
||||
query: PropTypes.string.isRequired,
|
||||
|
||||
// method invoked with range and text content when completion is confirmed
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
|
||||
// method invoked when selected (if any) completion changes
|
||||
onSelectionChange: PropTypes.func,
|
||||
|
||||
// The room in which we're autocompleting
|
||||
room: PropTypes.instanceOf(Room),
|
||||
};
|
|
@ -18,18 +18,19 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from '../../../index';
|
||||
import dis from "../../../dispatcher";
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as ObjectUtils from '../../../ObjectUtils';
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'AuxPanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -140,6 +141,15 @@ module.exports = createReactClass({
|
|||
return counters;
|
||||
},
|
||||
|
||||
_onScroll: function(rect) {
|
||||
if (this.props.onResize) {
|
||||
this.props.onResize();
|
||||
}
|
||||
|
||||
/* Force refresh of PersistedElements which may be partially hidden */
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const CallView = sdk.getComponent("voip.CallView");
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
@ -219,7 +229,7 @@ module.exports = createReactClass({
|
|||
|
||||
if (link) {
|
||||
span = (
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||
{ span }
|
||||
</a>
|
||||
);
|
||||
|
@ -264,14 +274,14 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classes} style={style} >
|
||||
<AutoHideScrollbar className={classes} style={style} onScroll={this._onScroll}>
|
||||
{ stateViews }
|
||||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ conferenceCallNotification }
|
||||
{ this.props.children }
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -34,11 +34,12 @@ import {parsePlainTextMessage} from '../../../editor/deserialize';
|
|||
import {renderModel} from '../../../editor/render';
|
||||
import {Room} from 'matrix-js-sdk';
|
||||
import TypingStore from "../../../stores/TypingStore";
|
||||
import EMOJIBASE from 'emojibase-data/en/compact.json';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {EMOTICON_TO_EMOJI} from "../../../emoji";
|
||||
import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands";
|
||||
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||
|
||||
|
@ -80,10 +81,11 @@ export default class BasicMessageEditor extends React.Component {
|
|||
initialCaret: PropTypes.object, // See DocumentPosition in editor/model.js
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
autoComplete: null,
|
||||
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
|
||||
};
|
||||
this._editorRef = null;
|
||||
this._autocompleteRef = null;
|
||||
|
@ -92,6 +94,18 @@ export default class BasicMessageEditor extends React.Component {
|
|||
this._isIMEComposing = false;
|
||||
this._hasTextSelected = false;
|
||||
this._emoticonSettingHandle = null;
|
||||
this._shouldShowPillAvatarSettingHandle = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) {
|
||||
const {isEmpty} = this.props.model;
|
||||
if (isEmpty) {
|
||||
this._showPlaceholder();
|
||||
} else {
|
||||
this._hidePlaceholder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_replaceEmoticon = (caretPosition, inputType, diff) => {
|
||||
|
@ -107,8 +121,10 @@ export default class BasicMessageEditor extends React.Component {
|
|||
});
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
|
||||
if (emoticonMatch) {
|
||||
const query = emoticonMatch[1].toLowerCase().replace("-", "");
|
||||
const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false);
|
||||
const query = emoticonMatch[1].replace("-", "");
|
||||
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
|
||||
const data = EMOTICON_TO_EMOJI.get(query) || EMOTICON_TO_EMOJI.get(query.toLowerCase());
|
||||
|
||||
if (data) {
|
||||
const {partCreator} = model;
|
||||
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
|
||||
|
@ -136,17 +152,29 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const position = selection.end || selection;
|
||||
this._setLastCaretFromPosition(position);
|
||||
}
|
||||
const {isEmpty} = this.props.model;
|
||||
if (this.props.placeholder) {
|
||||
const {isEmpty} = this.props.model;
|
||||
if (isEmpty) {
|
||||
this._showPlaceholder();
|
||||
} else {
|
||||
this._hidePlaceholder();
|
||||
}
|
||||
}
|
||||
if (isEmpty) {
|
||||
this._formatBarRef.hide();
|
||||
}
|
||||
this.setState({autoComplete: this.props.model.autoComplete});
|
||||
this.historyManager.tryPush(this.props.model, selection, inputType, diff);
|
||||
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, !this.props.model.isEmpty);
|
||||
|
||||
let isTyping = !this.props.model.isEmpty;
|
||||
// If the user is entering a command, only consider them typing if it is one which sends a message into the room
|
||||
if (isTyping && this.props.model.parts[0].type === "command") {
|
||||
const {cmd} = parseCommandString(this.props.model.parts[0].text);
|
||||
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
|
||||
isTyping = false;
|
||||
}
|
||||
}
|
||||
TypingStore.sharedInstance().setSelfTyping(this.props.room.roomId, isTyping);
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange();
|
||||
|
@ -186,9 +214,9 @@ export default class BasicMessageEditor extends React.Component {
|
|||
if (isSafari) {
|
||||
this._onInput({inputType: "insertCompositionText"});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
Promise.resolve().then(() => {
|
||||
this._onInput({inputType: "insertCompositionText"});
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,17 +227,49 @@ export default class BasicMessageEditor extends React.Component {
|
|||
return !!(this._isIMEComposing || (event.nativeEvent && event.nativeEvent.isComposing));
|
||||
}
|
||||
|
||||
_onCutCopy = (event, type) => {
|
||||
const selection = document.getSelection();
|
||||
const text = selection.toString();
|
||||
if (text) {
|
||||
const {model} = this.props;
|
||||
const range = getRangeForSelection(this._editorRef, model, selection);
|
||||
const selectedParts = range.parts.map(p => p.serialize());
|
||||
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
|
||||
event.clipboardData.setData("text/plain", text); // so plain copy/paste works
|
||||
if (type === "cut") {
|
||||
// Remove the text, updating the model as appropriate
|
||||
this._modifiedFlag = true;
|
||||
replaceRangeAndMoveCaret(range, []);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_onCopy = (event) => {
|
||||
this._onCutCopy(event, "copy");
|
||||
}
|
||||
|
||||
_onCut = (event) => {
|
||||
this._onCutCopy(event, "cut");
|
||||
}
|
||||
|
||||
_onPaste = (event) => {
|
||||
const {model} = this.props;
|
||||
const {partCreator} = model;
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
if (text) {
|
||||
this._modifiedFlag = true;
|
||||
const range = getRangeForSelection(this._editorRef, model, document.getSelection());
|
||||
const parts = parsePlainTextMessage(text, partCreator);
|
||||
replaceRangeAndMoveCaret(range, parts);
|
||||
event.preventDefault();
|
||||
const partsText = event.clipboardData.getData("application/x-riot-composer");
|
||||
let parts;
|
||||
if (partsText) {
|
||||
const serializedTextParts = JSON.parse(partsText);
|
||||
const deserializedParts = serializedTextParts.map(p => partCreator.deserializePart(p));
|
||||
parts = deserializedParts;
|
||||
} else {
|
||||
const text = event.clipboardData.getData("text/plain");
|
||||
parts = parsePlainTextMessage(text, partCreator);
|
||||
}
|
||||
this._modifiedFlag = true;
|
||||
const range = getRangeForSelection(this._editorRef, model, document.getSelection());
|
||||
replaceRangeAndMoveCaret(range, parts);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
_onInput = (event) => {
|
||||
|
@ -228,8 +288,8 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const {caret, text} = getCaretOffsetAndText(this._editorRef, sel);
|
||||
const newText = text.substr(0, caret.offset) + textToInsert + text.substr(caret.offset);
|
||||
caret.offset += textToInsert.length;
|
||||
this.props.model.update(newText, inputType, caret);
|
||||
this._modifiedFlag = true;
|
||||
this.props.model.update(newText, inputType, caret);
|
||||
}
|
||||
|
||||
// this is used later to see if we need to recalculate the caret
|
||||
|
@ -325,6 +385,16 @@ export default class BasicMessageEditor extends React.Component {
|
|||
} else if (modKey && event.key === Key.GREATER_THAN) {
|
||||
this._onFormatAction("quote");
|
||||
handled = true;
|
||||
// redo
|
||||
} else if ((!IS_MAC && modKey && event.key === Key.Y) ||
|
||||
(IS_MAC && modKey && event.shiftKey && event.key === Key.Z)) {
|
||||
if (this.historyManager.canRedo()) {
|
||||
const {parts, caret} = this.historyManager.redo();
|
||||
// pass matching inputType so historyManager doesn't push echo
|
||||
// when invoked from rerender callback.
|
||||
model.reset(parts, caret, "historyRedo");
|
||||
}
|
||||
handled = true;
|
||||
// undo
|
||||
} else if (modKey && event.key === Key.Z) {
|
||||
if (this.historyManager.canUndo()) {
|
||||
|
@ -334,19 +404,24 @@ export default class BasicMessageEditor extends React.Component {
|
|||
model.reset(parts, caret, "historyUndo");
|
||||
}
|
||||
handled = true;
|
||||
// redo
|
||||
} else if (modKey && event.key === Key.Y) {
|
||||
if (this.historyManager.canRedo()) {
|
||||
const {parts, caret} = this.historyManager.redo();
|
||||
// pass matching inputType so historyManager doesn't push echo
|
||||
// when invoked from rerender callback.
|
||||
model.reset(parts, caret, "historyRedo");
|
||||
}
|
||||
handled = true;
|
||||
// insert newline on Shift+Enter
|
||||
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
|
||||
this._insertText("\n");
|
||||
handled = true;
|
||||
// move selection to start of composer
|
||||
} else if (modKey && event.key === Key.HOME && !event.shiftKey) {
|
||||
setSelection(this._editorRef, model, {
|
||||
index: 0,
|
||||
offset: 0,
|
||||
});
|
||||
handled = true;
|
||||
// move selection to end of composer
|
||||
} else if (modKey && event.key === Key.END && !event.shiftKey) {
|
||||
setSelection(this._editorRef, model, {
|
||||
index: model.parts.length - 1,
|
||||
offset: model.parts[model.parts.length - 1].text.length,
|
||||
});
|
||||
handled = true;
|
||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||
} else {
|
||||
const metaOrAltPressed = event.metaKey || event.altKey;
|
||||
|
@ -384,6 +459,8 @@ export default class BasicMessageEditor extends React.Component {
|
|||
} else if (event.key === Key.TAB) {
|
||||
this._tabCompleteName();
|
||||
handled = true;
|
||||
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
|
||||
this._formatBarRef.hide();
|
||||
}
|
||||
}
|
||||
if (handled) {
|
||||
|
@ -412,10 +489,14 @@ export default class BasicMessageEditor extends React.Component {
|
|||
const addedLen = range.replace([partCreator.pillCandidate(range.text)]);
|
||||
return model.positionForOffset(caret.offset + addedLen, true);
|
||||
});
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
|
||||
// Don't try to do things with the autocomplete if there is none shown
|
||||
if (model.autoComplete) {
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
@ -439,25 +520,33 @@ export default class BasicMessageEditor extends React.Component {
|
|||
this.setState({completionIndex});
|
||||
}
|
||||
|
||||
_configureEmoticonAutoReplace() {
|
||||
_configureEmoticonAutoReplace = () => {
|
||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||
this.props.model.setTransformCallback(shouldReplace ? this._replaceEmoticon : null);
|
||||
}
|
||||
};
|
||||
|
||||
_configureShouldShowPillAvatar = () => {
|
||||
const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
|
||||
this.setState({ showPillAvatar });
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("selectionchange", this._onSelectionChange);
|
||||
this._editorRef.removeEventListener("input", this._onInput, true);
|
||||
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
|
||||
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
|
||||
SettingsStore.unwatchSetting(this._emoticonSettingHandle);
|
||||
SettingsStore.unwatchSetting(this._shouldShowPillAvatarSettingHandle);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const model = this.props.model;
|
||||
model.setUpdateCallback(this._updateEditorState);
|
||||
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null, () => {
|
||||
this._configureEmoticonAutoReplace();
|
||||
});
|
||||
this._emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||
this._configureEmoticonAutoReplace);
|
||||
this._configureEmoticonAutoReplace();
|
||||
this._shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||
this._configureShouldShowPillAvatar);
|
||||
const partCreator = model.partCreator;
|
||||
// TODO: does this allow us to get rid of EditorStateTransfer?
|
||||
// not really, but we could not serialize the parts, and just change the autoCompleter
|
||||
|
@ -499,6 +588,7 @@ export default class BasicMessageEditor extends React.Component {
|
|||
return;
|
||||
}
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this._modifiedFlag = true;
|
||||
switch (action) {
|
||||
case "bold":
|
||||
toggleInlineFormat(range, "**");
|
||||
|
@ -534,9 +624,12 @@ export default class BasicMessageEditor extends React.Component {
|
|||
/>
|
||||
</div>);
|
||||
}
|
||||
const classes = classNames("mx_BasicMessageComposer", {
|
||||
const wrapperClasses = classNames("mx_BasicMessageComposer", {
|
||||
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
|
||||
});
|
||||
const classes = classNames("mx_BasicMessageComposer_input", {
|
||||
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
|
||||
});
|
||||
|
||||
const MessageComposerFormatBar = sdk.getComponent('rooms.MessageComposerFormatBar');
|
||||
const shortcuts = {
|
||||
|
@ -547,15 +640,17 @@ export default class BasicMessageEditor extends React.Component {
|
|||
|
||||
const {completionIndex} = this.state;
|
||||
|
||||
return (<div className={classes}>
|
||||
return (<div className={wrapperClasses}>
|
||||
{ autoComplete }
|
||||
<MessageComposerFormatBar ref={ref => this._formatBarRef = ref} onAction={this._onFormatAction} shortcuts={shortcuts} />
|
||||
<div
|
||||
className="mx_BasicMessageComposer_input"
|
||||
className={classes}
|
||||
contentEditable="true"
|
||||
tabIndex="0"
|
||||
onBlur={this._onBlur}
|
||||
onFocus={this._onFocus}
|
||||
onCopy={this._onCopy}
|
||||
onCut={this._onCut}
|
||||
onPaste={this._onPaste}
|
||||
onKeyDown={this._onKeyDown}
|
||||
ref={ref => this._editorRef = ref}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -14,76 +15,102 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
export default function(props) {
|
||||
const { isUser } = props;
|
||||
const isNormal = props.status === "normal";
|
||||
const isWarning = props.status === "warning";
|
||||
const isVerified = props.status === "verified";
|
||||
const e2eIconClasses = classNames({
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import {useSettingValue} from "../../../hooks/useSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
|
||||
export const E2E_STATE = {
|
||||
VERIFIED: "verified",
|
||||
WARNING: "warning",
|
||||
UNKNOWN: "unknown",
|
||||
NORMAL: "normal",
|
||||
};
|
||||
|
||||
const crossSigningUserTitles = {
|
||||
[E2E_STATE.WARNING]: _td("This user has not verified all of their sessions."),
|
||||
[E2E_STATE.NORMAL]: _td("You have not verified this user."),
|
||||
[E2E_STATE.VERIFIED]: _td("You have verified this user. This user has verified all of their sessions."),
|
||||
};
|
||||
const crossSigningRoomTitles = {
|
||||
[E2E_STATE.WARNING]: _td("Someone is using an unknown session"),
|
||||
[E2E_STATE.NORMAL]: _td("This room is end-to-end encrypted"),
|
||||
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
|
||||
};
|
||||
|
||||
const legacyUserTitles = {
|
||||
[E2E_STATE.WARNING]: _td("Some sessions for this user are not trusted"),
|
||||
[E2E_STATE.VERIFIED]: _td("All sessions for this user are trusted"),
|
||||
};
|
||||
const legacyRoomTitles = {
|
||||
[E2E_STATE.WARNING]: _td("Some sessions in this encrypted room are not trusted"),
|
||||
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
|
||||
};
|
||||
|
||||
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_warning: isWarning,
|
||||
mx_E2EIcon_normal: isNormal,
|
||||
mx_E2EIcon_verified: isVerified,
|
||||
}, props.className);
|
||||
mx_E2EIcon_warning: status === E2E_STATE.WARNING,
|
||||
mx_E2EIcon_normal: status === E2E_STATE.NORMAL,
|
||||
mx_E2EIcon_verified: status === E2E_STATE.VERIFIED,
|
||||
}, className);
|
||||
|
||||
let e2eTitle;
|
||||
|
||||
const crossSigning = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
const crossSigning = useSettingValue("feature_cross_signing");
|
||||
if (crossSigning && isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t(
|
||||
"This user has not verified all of their devices.",
|
||||
);
|
||||
} else if (isNormal) {
|
||||
e2eTitle = _t(
|
||||
"You have not verified this user. " +
|
||||
"This user has verified all of their devices.",
|
||||
);
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t(
|
||||
"You have verified this user. " +
|
||||
"This user has verified all of their devices.",
|
||||
);
|
||||
}
|
||||
e2eTitle = crossSigningUserTitles[status];
|
||||
} else if (crossSigning && !isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t(
|
||||
"Some users in this encrypted room are not verified by you or " +
|
||||
"they have not verified their own devices.",
|
||||
);
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t(
|
||||
"All users in this encrypted room are verified by you and " +
|
||||
"they have verified their own devices.",
|
||||
);
|
||||
}
|
||||
e2eTitle = crossSigningRoomTitles[status];
|
||||
} else if (!crossSigning && isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t("Some devices for this user are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t("All devices for this user are trusted");
|
||||
}
|
||||
e2eTitle = legacyUserTitles[status];
|
||||
} else if (!crossSigning && !isUser) {
|
||||
if (isWarning) {
|
||||
e2eTitle = _t("Some devices in this encrypted room are not trusted");
|
||||
} else if (isVerified) {
|
||||
e2eTitle = _t("All devices in this encrypted room are trusted");
|
||||
}
|
||||
e2eTitle = legacyRoomTitles[status];
|
||||
}
|
||||
|
||||
let style = null;
|
||||
if (props.size) {
|
||||
style = {width: `${props.size}px`, height: `${props.size}px`};
|
||||
let style;
|
||||
if (size) {
|
||||
style = {width: `${size}px`, height: `${size}px`};
|
||||
}
|
||||
|
||||
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />);
|
||||
if (props.onClick) {
|
||||
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
||||
} else {
|
||||
return icon;
|
||||
const onMouseOver = () => setHover(true);
|
||||
const onMouseOut = () => setHover(false);
|
||||
|
||||
let tip;
|
||||
if (hover && !hideTooltip) {
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
||||
}
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
onClick={onClick}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
className={classes}
|
||||
style={style}
|
||||
>
|
||||
{ tip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div onMouseOver={onMouseOver} onMouseOut={onMouseOut} className={classes} style={style}>
|
||||
{ tip }
|
||||
</div>;
|
||||
};
|
||||
|
||||
E2EIcon.propTypes = {
|
||||
isUser: PropTypes.bool,
|
||||
status: PropTypes.oneOf(Object.values(E2E_STATE)),
|
||||
className: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default E2EIcon;
|
||||
|
|
|
@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
||||
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
||||
|
@ -26,11 +26,11 @@ import {findEditableEvent} from '../../../utils/EventUtils';
|
|||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
|
@ -105,9 +105,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -117,6 +115,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
this.state = {
|
||||
saveDisabled: true,
|
||||
};
|
||||
this._createEditorModel();
|
||||
}
|
||||
|
||||
_setEditorRef = ref => {
|
||||
|
@ -124,7 +123,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
};
|
||||
|
||||
_getRoom() {
|
||||
return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
_onKeyDown = (event) => {
|
||||
|
@ -190,7 +189,8 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
this.context.matrixClient.sendMessage(roomId, editContent);
|
||||
this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
@ -205,7 +205,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
this.context.matrixClient.cancelPendingEvent(previousEdit);
|
||||
this.context.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,14 +225,10 @@ export default class EditMessageComposer extends React.Component {
|
|||
this.props.editState.setEditorState(caret, parts);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._createEditorModel();
|
||||
}
|
||||
|
||||
_createEditorModel() {
|
||||
const {editState} = this.props;
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(room, this.context.matrixClient);
|
||||
const partCreator = new PartCreator(room, this.context);
|
||||
let parts;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -18,11 +19,11 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from "classnames";
|
||||
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
||||
const PRESENCE_CLASS = {
|
||||
"offline": "mx_EntityTile_offline",
|
||||
|
@ -30,7 +31,6 @@ const PRESENCE_CLASS = {
|
|||
"unavailable": "mx_EntityTile_unavailable",
|
||||
};
|
||||
|
||||
|
||||
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
|
||||
if (showPresence === false) {
|
||||
return 'mx_EntityTile_online_beenactive';
|
||||
|
@ -69,6 +69,7 @@ const EntityTile = createReactClass({
|
|||
suppressOnHover: PropTypes.bool,
|
||||
showPresence: PropTypes.bool,
|
||||
subtextLabel: PropTypes.string,
|
||||
e2eStatus: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -156,23 +157,26 @@ const EntityTile = createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let power;
|
||||
let powerLabel;
|
||||
const powerStatus = this.props.powerStatus;
|
||||
if (powerStatus) {
|
||||
const src = {
|
||||
[EntityTile.POWER_STATUS_MODERATOR]: require("../../../../res/img/mod.svg"),
|
||||
[EntityTile.POWER_STATUS_ADMIN]: require("../../../../res/img/admin.svg"),
|
||||
}[powerStatus];
|
||||
const alt = {
|
||||
[EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
|
||||
const powerText = {
|
||||
[EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"),
|
||||
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
|
||||
}[powerStatus];
|
||||
power = <img src={src} className="mx_EntityTile_power" width="16" height="17" alt={alt} />;
|
||||
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
|
||||
}
|
||||
|
||||
let e2eIcon;
|
||||
const { e2eStatus } = this.props;
|
||||
if (e2eStatus) {
|
||||
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||
const av = this.props.avatarJsx ||
|
||||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
|
@ -181,9 +185,10 @@ const EntityTile = createReactClass({
|
|||
onClick={this.props.onClick}>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
{ av }
|
||||
{ power }
|
||||
{ e2eIcon }
|
||||
</div>
|
||||
{ nameEl }
|
||||
{ powerLabel }
|
||||
{ inviteButton }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
|
@ -194,5 +199,4 @@ const EntityTile = createReactClass({
|
|||
EntityTile.POWER_STATUS_MODERATOR = "moderator";
|
||||
EntityTile.POWER_STATUS_ADMIN = "admin";
|
||||
|
||||
|
||||
export default EntityTile;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019, 2020 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.
|
||||
|
@ -18,39 +18,37 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
const classNames = require("classnames");
|
||||
import classNames from "classnames";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
const Modal = require('../../../Modal');
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const TextForEvent = require('../../../TextForEvent');
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import * as sdk from "../../../index";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus, MatrixClient} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
import * as ObjectUtils from "../../../ObjectUtils";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {E2E_STATE} from "./E2EIcon";
|
||||
import {toRem} from "../../../utils/units";
|
||||
|
||||
const eventTileTypes = {
|
||||
'm.room.message': 'messages.MessageEvent',
|
||||
'm.sticker': 'messages.MessageEvent',
|
||||
'm.key.verification.cancel': 'messages.MKeyVerificationConclusion',
|
||||
'm.key.verification.done': 'messages.MKeyVerificationConclusion',
|
||||
'm.room.encryption': 'messages.EncryptionEvent',
|
||||
'm.call.invite': 'messages.TextualEvent',
|
||||
'm.call.answer': 'messages.TextualEvent',
|
||||
'm.call.hangup': 'messages.TextualEvent',
|
||||
};
|
||||
|
||||
const stateEventTileTypes = {
|
||||
'm.room.aliases': 'messages.TextualEvent',
|
||||
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
|
||||
'm.room.encryption': 'messages.EncryptionEvent',
|
||||
'm.room.canonical_alias': 'messages.TextualEvent',
|
||||
'm.room.create': 'messages.RoomCreate',
|
||||
'm.room.member': 'messages.TextualEvent',
|
||||
|
@ -58,11 +56,11 @@ const stateEventTileTypes = {
|
|||
'm.room.avatar': 'messages.RoomAvatarEvent',
|
||||
'm.room.third_party_invite': 'messages.TextualEvent',
|
||||
'm.room.history_visibility': 'messages.TextualEvent',
|
||||
'm.room.encryption': 'messages.TextualEvent',
|
||||
'm.room.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
'm.room.tombstone': 'messages.TextualEvent',
|
||||
'm.room.join_rules': 'messages.TextualEvent',
|
||||
|
@ -75,7 +73,7 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
||||
}
|
||||
|
||||
function getHandlerTile(ev) {
|
||||
export function getHandlerTile(ev) {
|
||||
const type = ev.getType();
|
||||
|
||||
// don't show verification requests we're not involved in,
|
||||
|
@ -102,6 +100,17 @@ function getHandlerTile(ev) {
|
|||
}
|
||||
}
|
||||
|
||||
// sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and
|
||||
// fall back to showing hidden events, if we're viewing hidden events
|
||||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
|
||||
}
|
||||
|
||||
|
@ -118,7 +127,7 @@ const MAX_READ_AVATARS = 5;
|
|||
// | '--------------------------------------' |
|
||||
// '----------------------------------------------------------'
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'EventTile',
|
||||
|
||||
propTypes: {
|
||||
|
@ -197,6 +206,9 @@ module.exports = createReactClass({
|
|||
|
||||
// whether to show reactions for this event
|
||||
showReactions: PropTypes.bool,
|
||||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -222,11 +234,12 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
// don't do RR animations until we are mounted
|
||||
this._suppressReadReceiptAnimation = true;
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
|
@ -237,15 +250,17 @@ module.exports = createReactClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._suppressReadReceiptAnimation = false;
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.on("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
// re-check the sender verification as outgoing events progress through
|
||||
// the send process.
|
||||
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
|
||||
|
@ -262,8 +277,9 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
|
||||
if (this.props.showReactions) {
|
||||
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
|
||||
|
@ -286,18 +302,56 @@ module.exports = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||
if (userId === this.props.mxEvent.getSender()) {
|
||||
this._verifyEvent(this.props.mxEvent);
|
||||
}
|
||||
},
|
||||
|
||||
_verifyEvent: async function(mxEvent) {
|
||||
if (!mxEvent.isEncrypted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const verified = await this.context.matrixClient.isEventSenderVerified(mxEvent);
|
||||
// If we directly trust the device, short-circuit here
|
||||
const verified = await this.context.isEventSenderVerified(mxEvent);
|
||||
if (verified) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.VERIFIED,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If cross-signing is off, the old behaviour is to scream at the user
|
||||
// as if they've done something wrong, which they haven't
|
||||
if (!SettingsStore.getValue("feature_cross_signing")) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.NORMAL,
|
||||
}, this.props.onHeightChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventSenderTrust = await this.context.checkEventSenderTrust(mxEvent);
|
||||
if (!eventSenderTrust) {
|
||||
this.setState({
|
||||
verified: E2E_STATE.UNKNOWN,
|
||||
}, this.props.onHeightChanged); // Decryption may have cause a change in size
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
verified: verified,
|
||||
}, () => {
|
||||
// Decryption may have caused a change in size
|
||||
this.props.onHeightChanged();
|
||||
});
|
||||
verified: eventSenderTrust.isVerified() ? E2E_STATE.VERIFIED : E2E_STATE.WARNING,
|
||||
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
||||
},
|
||||
|
||||
_propsEqual: function(objA, objB) {
|
||||
|
@ -349,11 +403,11 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
shouldHighlight: function() {
|
||||
const actions = this.context.matrixClient.getPushActionsForEvent(this.props.mxEvent);
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
||||
// don't show self-highlights from another of our clients
|
||||
if (this.props.mxEvent.getSender() === this.context.matrixClient.credentials.userId) {
|
||||
if (this.props.mxEvent.getSender() === this.context.credentials.userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -423,7 +477,7 @@ module.exports = createReactClass({
|
|||
if (remainder > 0) {
|
||||
remText = <span className="mx_EventTile_readAvatarRemainder"
|
||||
onClick={this.toggleAllReadAvatars}
|
||||
style={{ right: -(left - receiptOffset) }}>{ remainder }+
|
||||
style={{ right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }}>{ remainder }+
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
@ -442,15 +496,6 @@ module.exports = createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
onCryptoClick: function(e) {
|
||||
const event = this.props.mxEvent;
|
||||
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event},
|
||||
);
|
||||
},
|
||||
|
||||
onRequestKeysClick: function() {
|
||||
this.setState({
|
||||
// Indicate in the UI that the keys have been requested (this is expected to
|
||||
|
@ -461,7 +506,7 @@ module.exports = createReactClass({
|
|||
// Cancel any outgoing key request for this event and resend it. If a response
|
||||
// is received for the request with the required keys, the event could be
|
||||
// decrypted successfully.
|
||||
this.context.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
||||
this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
||||
},
|
||||
|
||||
onPermalinkClicked: function(e) {
|
||||
|
@ -478,23 +523,26 @@ module.exports = createReactClass({
|
|||
|
||||
_renderE2EPadlock: function() {
|
||||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClick};
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
return <E2ePadlockUndecryptable {...props} />;
|
||||
return <E2ePadlockUndecryptable />;
|
||||
}
|
||||
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
if (this.state.verified) {
|
||||
if (this.state.verified === E2E_STATE.NORMAL) {
|
||||
return; // no icon if we've not even cross-signed the user
|
||||
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
||||
return; // no icon for verified
|
||||
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
||||
return (<E2ePadlockUnknown />);
|
||||
} else {
|
||||
return (<E2ePadlockUnverified {...props} />);
|
||||
return (<E2ePadlockUnverified />);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.context.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||
if (this.context.isRoomEncrypted(ev.getRoomId())) {
|
||||
// else if room is encrypted
|
||||
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||
if (ev.status === EventStatus.ENCRYPTING) {
|
||||
|
@ -507,7 +555,7 @@ module.exports = createReactClass({
|
|||
return; // we expect this to be unencrypted
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
return <E2ePadlockUnencrypted />;
|
||||
}
|
||||
|
||||
// no padlock needed
|
||||
|
@ -541,6 +589,7 @@ module.exports = createReactClass({
|
|||
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/riot-web/issues/11120");
|
||||
}
|
||||
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
||||
},
|
||||
|
@ -568,7 +617,8 @@ module.exports = createReactClass({
|
|||
|
||||
// Info messages are basically information about commands processed on a room
|
||||
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
||||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification"));
|
||||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === "m.room.encryption");
|
||||
let isInfoMessage = (
|
||||
!isBubbleMessage && eventType !== 'm.room.message' &&
|
||||
eventType !== 'm.sticker' && eventType !== 'm.room.create'
|
||||
|
@ -618,11 +668,11 @@ module.exports = createReactClass({
|
|||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
||||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_redacted: isRedacted,
|
||||
});
|
||||
|
||||
let permalink = "#";
|
||||
|
@ -648,6 +698,9 @@ module.exports = createReactClass({
|
|||
// joins/parts/etc
|
||||
avatarSize = 14;
|
||||
needsSenderProfile = false;
|
||||
} else if (this.props.useIRCLayout) {
|
||||
avatarSize = 14;
|
||||
needsSenderProfile = true;
|
||||
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
|
||||
// no avatar or sender profile for continuation messages
|
||||
avatarSize = 0;
|
||||
|
@ -700,15 +753,15 @@ module.exports = createReactClass({
|
|||
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
|
||||
<p>
|
||||
{ this.state.previouslyRequestedKeys ?
|
||||
_t( 'Your key share request has been sent - please check your other devices ' +
|
||||
_t( 'Your key share request has been sent - please check your other sessions ' +
|
||||
'for key share requests.') :
|
||||
_t( 'Key share requests are sent to your other devices automatically. If you ' +
|
||||
'rejected or dismissed the key share request on your other devices, click ' +
|
||||
_t( 'Key share requests are sent to your other sessions automatically. If you ' +
|
||||
'rejected or dismissed the key share request on your other sessions, click ' +
|
||||
'here to request the keys for this session again.')
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{ _t( 'If your other devices do not have the key for this message you will not ' +
|
||||
{ _t( 'If your other sessions do not have the key for this message you will not ' +
|
||||
'be able to decrypt them.')
|
||||
}
|
||||
</p>
|
||||
|
@ -716,7 +769,7 @@ module.exports = createReactClass({
|
|||
const keyRequestInfoContent = this.state.previouslyRequestedKeys ?
|
||||
_t('Key request sent.') :
|
||||
_t(
|
||||
'<requestLink>Re-request encryption keys</requestLink> from your other devices.',
|
||||
'<requestLink>Re-request encryption keys</requestLink> from your other sessions.',
|
||||
{},
|
||||
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
||||
);
|
||||
|
@ -739,9 +792,22 @@ module.exports = createReactClass({
|
|||
/>;
|
||||
}
|
||||
|
||||
const linkedTimestamp = <a
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
||||
>
|
||||
{ timestamp }
|
||||
</a>;
|
||||
|
||||
const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null;
|
||||
const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
|
@ -806,13 +872,13 @@ module.exports = createReactClass({
|
|||
}
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ ircTimestamp }
|
||||
{ avatar }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_reply">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
@ -830,23 +896,21 @@ module.exports = createReactClass({
|
|||
this.props.onHeightChanged,
|
||||
this.props.permalinkCreator,
|
||||
this._replyThread,
|
||||
this.props.useIRCLayout,
|
||||
);
|
||||
|
||||
// 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}>
|
||||
{ ircTimestamp }
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_line">
|
||||
<a
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
||||
>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
{ thread }
|
||||
<EventTileType ref={this._tile}
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
@ -879,7 +943,7 @@ function isMessageEvent(ev) {
|
|||
return (messageTypes.includes(ev.getType()));
|
||||
}
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
export function haveTileForEvent(e) {
|
||||
// Only messages have a tile (black-rectangle) if redacted
|
||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
|
@ -895,7 +959,7 @@ module.exports.haveTileForEvent = function(e) {
|
|||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function E2ePadlockUndecryptable(props) {
|
||||
return (
|
||||
|
@ -905,7 +969,7 @@ function E2ePadlockUndecryptable(props) {
|
|||
|
||||
function E2ePadlockUnverified(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Encrypted by an unverified device")} icon="unverified" {...props} />
|
||||
<E2ePadlock title={_t("Encrypted by an unverified session")} icon="unverified" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -915,11 +979,16 @@ function E2ePadlockUnencrypted(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockUnknown(props) {
|
||||
return (
|
||||
<E2ePadlock title={_t("Encrypted by a deleted session")} icon="unknown" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
class E2ePadlock extends React.Component {
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
|
@ -930,10 +999,6 @@ class E2ePadlock extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onClick = (e) => {
|
||||
if (this.props.onClick) this.props.onClick(e);
|
||||
};
|
||||
|
||||
onHoverStart = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
@ -964,5 +1029,3 @@ class E2ePadlock extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getHandlerTile = getHandlerTile;
|
||||
|
|
|
@ -19,33 +19,29 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {Key} from '../../../Keyboard';
|
||||
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'ForwardMessage',
|
||||
|
||||
propTypes: {
|
||||
onCancelClick: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
rightDisabled: true,
|
||||
middleDisabled: true,
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
sideDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
|
|
53
src/components/views/rooms/InviteOnlyIcon.js
Normal file
53
src/components/views/rooms/InviteOnlyIcon.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
Copyright 2020 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 { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default class InviteOnlyIcon extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
onHoverStart = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
onHoverEnd = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
|
||||
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
let tooltip;
|
||||
if (this.state.hover) {
|
||||
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
|
||||
}
|
||||
return (<div className={classes}
|
||||
onMouseEnter={this.onHoverStart}
|
||||
onMouseLeave={this.onHoverEnd}
|
||||
>
|
||||
{ tooltip }
|
||||
</div>);
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ export default (props) => {
|
|||
}
|
||||
return (<div className="mx_JumpToBottomButton">
|
||||
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
|
||||
title={_t("Scroll to bottom of page")}
|
||||
title={_t("Scroll to most recent messages")}
|
||||
onClick={props.onScrollToBottomClick}>
|
||||
</AccessibleButton>
|
||||
{ badge }
|
||||
|
|
|
@ -18,16 +18,16 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import {linkifyElement} from '../../../HtmlUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
const ImageUtils = require('../../../ImageUtils');
|
||||
const Modal = require('../../../Modal');
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'LinkPreviewWidget',
|
||||
|
||||
propTypes: {
|
||||
|
@ -43,7 +43,8 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
|
||||
if (this.unmounted) {
|
||||
|
@ -127,15 +128,19 @@ module.exports = createReactClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
|
||||
// opaque string. This does not allow any HTML to be injected into the DOM.
|
||||
const description = AllHtmlEntities.decode(p["og:description"] || "");
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_LinkPreviewWidget" >
|
||||
{ img }
|
||||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
|
||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
||||
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
|
||||
{ p["og:description"] }
|
||||
{ description }
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton className="mx_LinkPreviewWidget_cancel" onClick={this.props.onCancelClick} aria-label={_t("Close preview")}>
|
||||
|
|
|
@ -16,13 +16,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class MemberDeviceInfo extends React.Component {
|
||||
render() {
|
||||
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
|
||||
// XXX: These checks are not cross-signing aware but this component is only used
|
||||
// from the old, pre-cross-signing memberinfopanel
|
||||
const iconClasses = classNames({
|
||||
mx_MemberDeviceInfo_icon: true,
|
||||
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),
|
||||
|
|
|
@ -31,14 +31,13 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Unread from '../../../Unread';
|
||||
import * as Unread from '../../../Unread';
|
||||
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -47,10 +46,11 @@ import MultiInviter from "../../../utils/MultiInviter";
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {EventTimeline} from "matrix-js-sdk";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'MemberInfo',
|
||||
|
||||
propTypes: {
|
||||
|
@ -76,13 +76,14 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._cancelDeviceList = null;
|
||||
const cli = this.context.matrixClient;
|
||||
const cli = this.context;
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
this._enableDevices = cli.isCryptoEnabled();
|
||||
|
@ -99,20 +100,19 @@ module.exports = createReactClass({
|
|||
cli.on("accountData", this.onAccountData);
|
||||
|
||||
this._checkIgnoreState();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._updateStateForNewMember(this.props.member);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
||||
if (this.props.member.userId !== newProps.member.userId) {
|
||||
this._updateStateForNewMember(newProps.member);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
if (client) {
|
||||
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
client.removeListener("Room", this.onRoom);
|
||||
|
@ -131,7 +131,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_checkIgnoreState: function() {
|
||||
const isIgnoring = this.context.matrixClient.isUserIgnored(this.props.member.userId);
|
||||
const isIgnoring = this.context.isUserIgnored(this.props.member.userId);
|
||||
this.setState({isIgnoring: isIgnoring});
|
||||
},
|
||||
|
||||
|
@ -161,13 +161,10 @@ module.exports = createReactClass({
|
|||
// no need to re-download the whole thing; just update our copy of
|
||||
// the list.
|
||||
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(this.context.matrixClient.getStoredDevicesForUser(userId)).then((devices) => {
|
||||
this.setState({
|
||||
devices: devices,
|
||||
e2eStatus: this._getE2EStatus(devices),
|
||||
});
|
||||
const devices = this.context.getStoredDevicesForUser(userId);
|
||||
this.setState({
|
||||
devices: devices,
|
||||
e2eStatus: this._getE2EStatus(devices),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -197,7 +194,7 @@ module.exports = createReactClass({
|
|||
onRoomReceipt: function(receiptEvent, room) {
|
||||
// because if we read a notification, it will affect notification count
|
||||
// only bother updating if there's a receipt from us
|
||||
if (findReadReceiptFromUserId(receiptEvent, this.context.matrixClient.credentials.userId)) {
|
||||
if (findReadReceiptFromUserId(receiptEvent, this.context.credentials.userId)) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
@ -242,7 +239,7 @@ module.exports = createReactClass({
|
|||
let cancelled = false;
|
||||
this._cancelDeviceList = function() { cancelled = true; };
|
||||
|
||||
const client = this.context.matrixClient;
|
||||
const client = this.context;
|
||||
const self = this;
|
||||
client.downloadKeys([member.userId], true).then(() => {
|
||||
return client.getStoredDevicesForUser(member.userId);
|
||||
|
@ -261,13 +258,13 @@ module.exports = createReactClass({
|
|||
e2eStatus: self._getE2EStatus(devices),
|
||||
});
|
||||
}, function(err) {
|
||||
console.log("Error downloading devices", err);
|
||||
console.log("Error downloading sessions", err);
|
||||
self.setState({devicesLoading: false});
|
||||
});
|
||||
},
|
||||
|
||||
onIgnoreToggle: function() {
|
||||
const ignoredUsers = this.context.matrixClient.getIgnoredUsers();
|
||||
const ignoredUsers = this.context.getIgnoredUsers();
|
||||
if (this.state.isIgnoring) {
|
||||
const index = ignoredUsers.indexOf(this.props.member.userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
|
@ -275,7 +272,7 @@ module.exports = createReactClass({
|
|||
ignoredUsers.push(this.props.member.userId);
|
||||
}
|
||||
|
||||
this.context.matrixClient.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
this.context.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
return this.setState({isIgnoring: !this.state.isIgnoring});
|
||||
});
|
||||
},
|
||||
|
@ -293,7 +290,7 @@ module.exports = createReactClass({
|
|||
if (!proceed) return;
|
||||
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.kick(
|
||||
this.context.kick(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
reason || undefined,
|
||||
).then(function() {
|
||||
|
@ -329,11 +326,11 @@ module.exports = createReactClass({
|
|||
this.setState({ updating: this.state.updating + 1 });
|
||||
let promise;
|
||||
if (this.props.member.membership === 'ban') {
|
||||
promise = this.context.matrixClient.unban(
|
||||
promise = this.context.unban(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
);
|
||||
} else {
|
||||
promise = this.context.matrixClient.ban(
|
||||
promise = this.context.ban(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
reason || undefined,
|
||||
);
|
||||
|
@ -360,7 +357,7 @@ module.exports = createReactClass({
|
|||
|
||||
onRedactAllMessages: async function() {
|
||||
const {roomId, userId} = this.props.member;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
@ -368,7 +365,7 @@ module.exports = createReactClass({
|
|||
let eventsToRedact = [];
|
||||
for (const timeline of timelineSet.getTimelines()) {
|
||||
eventsToRedact = timeline.getEvents().reduce((events, event) => {
|
||||
if (event.getSender() === userId && !event.isRedacted()) {
|
||||
if (event.getSender() === userId && !event.isRedacted() && !event.isRedaction()) {
|
||||
return events.concat(event);
|
||||
} else {
|
||||
return events;
|
||||
|
@ -414,7 +411,7 @@ module.exports = createReactClass({
|
|||
console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`);
|
||||
await Promise.all(eventsToRedact.map(async event => {
|
||||
try {
|
||||
await this.context.matrixClient.redactEvent(roomId, event.getId());
|
||||
await this.context.redactEvent(roomId, event.getId());
|
||||
} catch (err) {
|
||||
// log and swallow errors
|
||||
console.error("Could not redact", event.getId());
|
||||
|
@ -446,11 +443,11 @@ module.exports = createReactClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === this.context.matrixClient.getUserId()) {
|
||||
if (target === this.context.getUserId()) {
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
|
@ -478,7 +475,7 @@ module.exports = createReactClass({
|
|||
|
||||
if (!isNaN(level)) {
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then(
|
||||
this.context.setPowerLevel(roomId, target, level, powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -500,13 +497,13 @@ module.exports = createReactClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
const me = room.getMember(this.context.matrixClient.credentials.userId);
|
||||
const me = room.getMember(this.context.credentials.userId);
|
||||
if (!me) return;
|
||||
|
||||
const defaultLevel = powerLevelEvent.getContent().users_default;
|
||||
|
@ -515,7 +512,7 @@ module.exports = createReactClass({
|
|||
// toggle the level
|
||||
const newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
|
||||
this.context.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -550,7 +547,7 @@ module.exports = createReactClass({
|
|||
danger: true,
|
||||
onFinished: (accepted) => {
|
||||
if (!accepted) return;
|
||||
this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => {
|
||||
this.context.deactivateSynapseUser(this.props.member.userId).catch(e => {
|
||||
console.error("Failed to deactivate user");
|
||||
console.error(e);
|
||||
|
||||
|
@ -566,7 +563,7 @@ module.exports = createReactClass({
|
|||
|
||||
_applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) {
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.context.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
this.context.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
|
@ -587,7 +584,7 @@ module.exports = createReactClass({
|
|||
onPowerChange: async function(powerLevel) {
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
@ -598,7 +595,7 @@ module.exports = createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const myUserId = this.context.matrixClient.getUserId();
|
||||
const myUserId = this.context.getUserId();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
|
@ -650,9 +647,9 @@ module.exports = createReactClass({
|
|||
|
||||
_calculateOpsPermissions: async function(member) {
|
||||
let canDeactivate = false;
|
||||
if (this.context.matrixClient) {
|
||||
if (this.context) {
|
||||
try {
|
||||
canDeactivate = await this.context.matrixClient.isSynapseAdministrator();
|
||||
canDeactivate = await this.context.isSynapseAdministrator();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
@ -665,13 +662,13 @@ module.exports = createReactClass({
|
|||
},
|
||||
muted: false,
|
||||
};
|
||||
const room = this.context.matrixClient.getRoom(member.roomId);
|
||||
const room = this.context.getRoom(member.roomId);
|
||||
if (!room) return defaultPerms;
|
||||
|
||||
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevels) return defaultPerms;
|
||||
|
||||
const me = room.getMember(this.context.matrixClient.credentials.userId);
|
||||
const me = room.getMember(this.context.credentials.userId);
|
||||
if (!me) return defaultPerms;
|
||||
|
||||
const them = member;
|
||||
|
@ -728,7 +725,7 @@ module.exports = createReactClass({
|
|||
|
||||
onCancel: function(e) {
|
||||
dis.dispatch({
|
||||
action: "view_user",
|
||||
action: Action.ViewUser,
|
||||
member: null,
|
||||
});
|
||||
},
|
||||
|
@ -738,7 +735,7 @@ module.exports = createReactClass({
|
|||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
|
@ -767,9 +764,9 @@ module.exports = createReactClass({
|
|||
// still loading
|
||||
devComponents = <Spinner />;
|
||||
} else if (devices === null) {
|
||||
devComponents = _t("Unable to load device list");
|
||||
devComponents = _t("Unable to load session list");
|
||||
} else if (devices.length === 0) {
|
||||
devComponents = _t("No devices with registered encryption keys");
|
||||
devComponents = _t("No sessions with registered encryption keys");
|
||||
} else {
|
||||
devComponents = [];
|
||||
for (let i = 0; i < devices.length; i++) {
|
||||
|
@ -781,7 +778,7 @@ module.exports = createReactClass({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("Devices") }</h3>
|
||||
<h3>{ _t("Sessions") }</h3>
|
||||
<div className="mx_MemberInfo_devices">
|
||||
{ devComponents }
|
||||
</div>
|
||||
|
@ -797,7 +794,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_renderUserOptions: function() {
|
||||
const cli = this.context.matrixClient;
|
||||
const cli = this.context;
|
||||
const member = this.props.member;
|
||||
|
||||
let ignoreButton = null;
|
||||
|
@ -905,9 +902,9 @@ module.exports = createReactClass({
|
|||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
if (this.props.member.userId !== this.context.matrixClient.credentials.userId) {
|
||||
if (this.props.member.userId !== this.context.credentials.userId) {
|
||||
// TODO: Immutable DMs replaces a lot of this
|
||||
const dmRoomMap = new DMRoomMap(this.context.matrixClient);
|
||||
const dmRoomMap = new DMRoomMap(this.context);
|
||||
// dmRooms will not include dmRooms that we have been invited into but did not join.
|
||||
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
|
||||
// XXX: we potentially want DMs we have been invited to, to also show up here :L
|
||||
|
@ -918,7 +915,7 @@ module.exports = createReactClass({
|
|||
|
||||
const tiles = [];
|
||||
for (const roomId of dmRooms) {
|
||||
const room = this.context.matrixClient.getRoom(roomId);
|
||||
const room = this.context.getRoom(roomId);
|
||||
if (room) {
|
||||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
|
@ -1064,12 +1061,12 @@ module.exports = createReactClass({
|
|||
}
|
||||
}
|
||||
|
||||
const room = this.context.matrixClient.getRoom(this.props.member.roomId);
|
||||
const room = this.context.getRoom(this.props.member.roomId);
|
||||
const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
|
||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = this.context.matrixClient.baseUrl;
|
||||
const hsUrl = this.context.baseUrl;
|
||||
let showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
|
@ -1108,16 +1105,17 @@ module.exports = createReactClass({
|
|||
</div>
|
||||
</div>;
|
||||
|
||||
const isEncrypted = this.context.matrixClient.isRoomEncrypted(this.props.member.roomId);
|
||||
const isEncrypted = this.context.isRoomEncrypted(this.props.member.roomId);
|
||||
if (this.state.e2eStatus && isEncrypted) {
|
||||
e2eIconElement = (<E2EIcon status={this.state.e2eStatus} isUser={true} />);
|
||||
}
|
||||
}
|
||||
|
||||
const avatarUrl = this.props.member.getMxcAvatarUrl();
|
||||
const {member} = this.props;
|
||||
const avatarUrl = member.avatarUrl || (member.getMxcAvatarUrl && member.getMxcAvatarUrl());
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = <div className="mx_MemberInfo_avatar">
|
||||
<img src={httpUrl} />
|
||||
</div>;
|
||||
|
|
|
@ -20,19 +20,23 @@ import React from 'react';
|
|||
import createReactClass from 'create-react-class';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const sdk = require('../../../index');
|
||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||
const CallHandler = require("../../../CallHandler");
|
||||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
const SHOW_MORE_INCREMENT = 100;
|
||||
|
||||
module.exports = createReactClass({
|
||||
// Regex applied to filter our punctuation in member names before applying sort, to fuzzy it a little
|
||||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'MemberList',
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -45,7 +49,8 @@ module.exports = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._mounted = true;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
|
@ -187,7 +192,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
_updateList: new rate_limited_func(function() {
|
||||
_updateList: rate_limited_func(function() {
|
||||
this._updateListNow();
|
||||
}, 500),
|
||||
|
||||
|
@ -336,10 +341,13 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name;
|
||||
const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name;
|
||||
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);
|
||||
return nameA.localeCompare(nameB, {
|
||||
ignorePunctuation: true,
|
||||
sensitivity: "base",
|
||||
});
|
||||
},
|
||||
|
||||
onSearchQueryChanged: function(searchQuery) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2020 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.
|
||||
|
@ -15,16 +16,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
const sdk = require('../../../index');
|
||||
const dis = require('../../../dispatcher');
|
||||
import * as sdk from "../../../index";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'MemberTile',
|
||||
|
||||
propTypes: {
|
||||
|
@ -41,29 +42,108 @@ module.exports = createReactClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
statusMessage: this.getStatusMessage(),
|
||||
isRoomEncrypted: false,
|
||||
e2eStatus: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
return;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
const { user } = this.props.member;
|
||||
if (user) {
|
||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
}
|
||||
}
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
return;
|
||||
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
const { roomId } = this.props.member;
|
||||
if (roomId) {
|
||||
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
|
||||
this.setState({
|
||||
isRoomEncrypted,
|
||||
});
|
||||
if (isRoomEncrypted) {
|
||||
cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged);
|
||||
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
this.updateE2EStatus();
|
||||
} else {
|
||||
// Listen for room to become encrypted
|
||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const { user } = this.props.member;
|
||||
if (!user) {
|
||||
if (user) {
|
||||
user.removeListener(
|
||||
"User._unstable_statusMessage",
|
||||
this._onStatusMessageCommitted,
|
||||
);
|
||||
}
|
||||
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
cli.removeListener("userTrustStatusChanged", this.onUserTrustStatusChanged);
|
||||
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev) {
|
||||
if (ev.getType() !== "m.room.encryption") return;
|
||||
const { roomId } = this.props.member;
|
||||
if (ev.getRoomId() !== roomId) return;
|
||||
|
||||
// The room is encrypted now.
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
this.setState({
|
||||
isRoomEncrypted: true,
|
||||
});
|
||||
this.updateE2EStatus();
|
||||
},
|
||||
|
||||
onUserTrustStatusChanged: function(userId, trustStatus) {
|
||||
if (userId !== this.props.member.userId) return;
|
||||
this.updateE2EStatus();
|
||||
},
|
||||
|
||||
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
|
||||
if (userId !== this.props.member.userId) return;
|
||||
this.updateE2EStatus();
|
||||
},
|
||||
|
||||
updateE2EStatus: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const { userId } = this.props.member;
|
||||
const isMe = userId === cli.getUserId();
|
||||
const userTrust = cli.checkUserTrust(userId);
|
||||
if (!userTrust.isCrossSigningVerified()) {
|
||||
this.setState({
|
||||
e2eStatus: userTrust.wasCrossSigningVerified() ? "warning" : "normal",
|
||||
});
|
||||
return;
|
||||
}
|
||||
user.removeListener(
|
||||
"User._unstable_statusMessage",
|
||||
this._onStatusMessageCommitted,
|
||||
);
|
||||
|
||||
const devices = cli.getStoredDevicesForUser(userId);
|
||||
const anyDeviceUnverified = devices.some(device => {
|
||||
const { deviceId } = device;
|
||||
// For your own devices, we use the stricter check of cross-signing
|
||||
// verification to encourage everyone to trust their own devices via
|
||||
// cross-signing so that other users can then safely trust you.
|
||||
// For other people's devices, the more general verified check that
|
||||
// includes locally verified devices can be used.
|
||||
const deviceTrust = cli.checkDeviceTrust(userId, deviceId);
|
||||
return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified();
|
||||
});
|
||||
this.setState({
|
||||
e2eStatus: anyDeviceUnverified ? "warning" : "verified",
|
||||
});
|
||||
},
|
||||
|
||||
getStatusMessage() {
|
||||
|
@ -95,12 +175,18 @@ module.exports = createReactClass({
|
|||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
nextState.isRoomEncrypted !== this.state.isRoomEncrypted ||
|
||||
nextState.e2eStatus !== this.state.e2eStatus
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
action: Action.ViewUser,
|
||||
member: this.props.member,
|
||||
});
|
||||
},
|
||||
|
@ -130,7 +216,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
const av = (
|
||||
<MemberAvatar member={member} width={36} height={36} />
|
||||
<MemberAvatar member={member} width={36} height={36} aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (member.user) {
|
||||
|
@ -154,14 +240,26 @@ module.exports = createReactClass({
|
|||
|
||||
const powerStatus = powerStatusMap.get(powerLevel);
|
||||
|
||||
let e2eStatus;
|
||||
if (this.state.isRoomEncrypted) {
|
||||
e2eStatus = this.state.e2eStatus;
|
||||
}
|
||||
|
||||
return (
|
||||
<EntityTile {...this.props} presenceState={presenceState}
|
||||
<EntityTile
|
||||
{...this.props}
|
||||
presenceState={presenceState}
|
||||
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
|
||||
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
|
||||
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
|
||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||
name={name} powerStatus={powerStatus} showPresence={this.props.showPresence}
|
||||
avatarJsx={av}
|
||||
title={this.getPowerLabel()}
|
||||
name={name}
|
||||
powerStatus={powerStatus}
|
||||
showPresence={this.props.showPresence}
|
||||
subtextLabel={statusMessage}
|
||||
e2eStatus={e2eStatus}
|
||||
onClick={this.onClick}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -18,13 +18,15 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
|
@ -133,14 +135,25 @@ class UploadButton extends React.Component {
|
|||
roomId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
|
||||
|
||||
this._uploadInput = createRef();
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
onAction = payload => {
|
||||
if (payload.action === "upload_file") {
|
||||
this.onUploadClick();
|
||||
}
|
||||
};
|
||||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
|
@ -153,7 +166,7 @@ class UploadButton extends React.Component {
|
|||
if (ev.target.files.length === 0) return;
|
||||
|
||||
// take a copy so we can safely reset the value of the form control
|
||||
// (Note it is a FileList: we can't use slice or sesnible iteration).
|
||||
// (Note it is a FileList: we can't use slice or sensible iteration).
|
||||
const tfiles = [];
|
||||
for (let i = 0; i < ev.target.files.length; ++i) {
|
||||
tfiles.push(ev.target.files[i]);
|
||||
|
@ -191,10 +204,9 @@ class UploadButton extends React.Component {
|
|||
}
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this.onEvent = this.onEvent.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
|
@ -204,15 +216,11 @@ export default class MessageComposer extends React.Component {
|
|||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
|
||||
// for 'event' fires *after* 'RoomEvent', and our room won't have yet been
|
||||
// marked as encrypted.
|
||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||
MatrixClientPeg.get().on("event", this.onEvent);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
|
@ -236,7 +244,6 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
if (this._roomStoreToken) {
|
||||
|
@ -244,13 +251,6 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onEvent(event) {
|
||||
if (event.getType() !== 'm.room.encryption') return;
|
||||
if (event.getRoomId() !== this.props.room.roomId) return;
|
||||
// TODO: put (encryption state??) in state
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
_onRoomStateEvents(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||
|
||||
|
@ -308,18 +308,33 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
renderPlaceholderText() {
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (this.state.isQuoting) {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted reply…');
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
if (this.state.isQuoting) {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply…');
|
||||
}
|
||||
} else {
|
||||
return _t('Send a reply (unencrypted)…');
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted message…');
|
||||
} else {
|
||||
return _t('Send a message…');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted message…');
|
||||
if (this.state.isQuoting) {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply (unencrypted)…');
|
||||
}
|
||||
} else {
|
||||
return _t('Send a message (unencrypted)…');
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted message…');
|
||||
} else {
|
||||
return _t('Send a message (unencrypted)…');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -354,12 +369,22 @@ export default class MessageComposer extends React.Component {
|
|||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
<EmojiButton key='stickerpicker_controls_button' addEmoji={this.addEmoji} />,
|
||||
<EmojiButton key="stickerpicker_controls_button" addEmoji={this.addEmoji} />,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
|
||||
callInProgress ? null : <CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? null : <VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
|
||||
if (this.state.showCallButtons) {
|
||||
if (callInProgress) {
|
||||
controls.push(
|
||||
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
<VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
|
@ -372,7 +397,7 @@ export default class MessageComposer extends React.Component {
|
|||
</a>
|
||||
) : '';
|
||||
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
|
@ -390,7 +415,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer">
|
||||
<div className="mx_MessageComposer mx_GroupLayout">
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
|
|
|
@ -17,14 +17,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
export default class MessageComposerFormatBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onAction: PropTypes.func.isRequired,
|
||||
shortcuts: PropTypes.object.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -64,7 +65,7 @@ class FormatButton extends React.PureComponent {
|
|||
icon: PropTypes.string.isRequired,
|
||||
shortcut: PropTypes.string,
|
||||
visible: PropTypes.bool,
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const InteractiveTooltip = sdk.getComponent('elements.InteractiveTooltip');
|
||||
|
@ -82,11 +83,12 @@ class FormatButton extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<InteractiveTooltip content={tooltipContent} forceHidden={!this.props.visible}>
|
||||
<span aria-label={this.props.label}
|
||||
role="button"
|
||||
onClick={this.props.onClick}
|
||||
className={className}>
|
||||
</span>
|
||||
<AccessibleButton
|
||||
as="span"
|
||||
role="button"
|
||||
onClick={this.props.onClick}
|
||||
aria-label={this.props.label}
|
||||
className={className} />
|
||||
</InteractiveTooltip>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,15 +17,15 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher";
|
||||
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';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'PinnedEventTile',
|
||||
propTypes: {
|
||||
mxRoom: PropTypes.object.isRequired,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -17,13 +18,13 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import PinnedEventTile from "./PinnedEventTile";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PinningUtils from "../../../utils/PinningUtils";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'PinnedEventsPanel',
|
||||
propTypes: {
|
||||
// The Room from the js-sdk we're going to show pinned events for
|
||||
|
|
|
@ -21,7 +21,7 @@ import createReactClass from 'create-react-class';
|
|||
import { _t } from '../../../languageHandler';
|
||||
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'PresenceLabel',
|
||||
|
||||
propTypes: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,18 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
const sdk = require('../../../index');
|
||||
|
||||
const Velociraptor = require('../../../Velociraptor');
|
||||
require('../../../VelocityBounce');
|
||||
import '../../../VelocityBounce';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import {formatDate} from '../../../DateUtils';
|
||||
import Velociraptor from "../../../Velociraptor";
|
||||
import * as sdk from "../../../index";
|
||||
import {toRem} from "../../../utils/units";
|
||||
|
||||
let bounce = false;
|
||||
try {
|
||||
|
@ -35,7 +33,7 @@ try {
|
|||
} catch (e) {
|
||||
}
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'ReadReceiptMarker',
|
||||
|
||||
propTypes: {
|
||||
|
@ -90,6 +88,11 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._avatar = createRef();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// before we remove the rr, store its location in the map, so that if
|
||||
// it reappears, it can be animated from the right place.
|
||||
|
@ -105,7 +108,7 @@ module.exports = createReactClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const avatarNode = ReactDOM.findDOMNode(this);
|
||||
const avatarNode = this._avatar.current;
|
||||
rrInfo.top = avatarNode.offsetTop;
|
||||
rrInfo.left = avatarNode.offsetLeft;
|
||||
rrInfo.parent = avatarNode.offsetParent;
|
||||
|
@ -125,7 +128,7 @@ module.exports = createReactClass({
|
|||
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
const newElement = ReactDOM.findDOMNode(this);
|
||||
const newElement = this._avatar.current;
|
||||
let startTopOffset;
|
||||
if (!newElement.offsetParent) {
|
||||
// this seems to happen sometimes for reasons I don't understand
|
||||
|
@ -146,7 +149,7 @@ module.exports = createReactClass({
|
|||
// start at the old height and in the old h pos
|
||||
|
||||
startStyles.push({ top: startTopOffset+"px",
|
||||
left: oldInfo.left+"px" });
|
||||
left: toRem(oldInfo.left) });
|
||||
|
||||
const reorderTransitionOpts = {
|
||||
duration: 100,
|
||||
|
@ -175,11 +178,11 @@ module.exports = createReactClass({
|
|||
render: function() {
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
if (this.state.suppressDisplay) {
|
||||
return <div />;
|
||||
return <div ref={this._avatar} />;
|
||||
}
|
||||
|
||||
const style = {
|
||||
left: this.props.leftOffset+'px',
|
||||
left: toRem(this.props.leftOffset),
|
||||
top: '0px',
|
||||
visibility: this.props.hidden ? 'hidden' : 'visible',
|
||||
};
|
||||
|
@ -215,6 +218,7 @@ module.exports = createReactClass({
|
|||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
inputRef={this._avatar}
|
||||
/>
|
||||
</Velociraptor>
|
||||
);
|
||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import dis from '../../../dispatcher';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -35,8 +35,8 @@ export default class ReplyPreview extends React.Component {
|
|||
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.unmounted = false;
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -15,13 +15,13 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from "react";
|
||||
import dis from "../../../dispatcher";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import RoomAvatar from '../avatars/RoomAvatar';
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
import * as sdk from "../../../index";
|
||||
import Analytics from "../../../Analytics";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||
|
@ -49,7 +49,8 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
this._scroller = createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
this._dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
const storedRooms = SettingsStore.getValue("breadcrumb_rooms");
|
||||
|
@ -362,17 +363,6 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
|
||||
}
|
||||
|
||||
let dmIndicator;
|
||||
if (this._isDmRoom(r.room)) {
|
||||
dmIndicator = <img
|
||||
src={require("../../../../res/img/icon_person.svg")}
|
||||
className="mx_RoomBreadcrumbs_dmIndicator"
|
||||
width="13"
|
||||
height="15"
|
||||
alt={_t("Direct Chat")}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
className={classes}
|
||||
|
@ -384,7 +374,6 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
>
|
||||
<RoomAvatar room={r.room} width={32} height={32} />
|
||||
{badge}
|
||||
{dmIndicator}
|
||||
{tooltip}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
|
|
|
@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import React, {createRef} from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||
|
@ -54,6 +54,7 @@ export default createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._topic = createRef();
|
||||
},
|
||||
|
@ -101,7 +102,7 @@ export default createReactClass({
|
|||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||
name={name} idName={name}
|
||||
url={ContentRepo.getHttpUriForMxc(
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatarUrl, 24, 24, "crop")} />
|
||||
</td>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -17,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomDropTarget',
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -18,9 +19,9 @@ import React, {createRef} from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import Modal from "../../../Modal";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
|
@ -30,9 +31,11 @@ import ManageIntegsButton from '../elements/ManageIntegsButton';
|
|||
import {CancelButton} from './SimpleRoomHeader';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import InviteOnlyIcon from './InviteOnlyIcon';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomHeader',
|
||||
|
||||
propTypes: {
|
||||
|
@ -55,6 +58,7 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._topic = createRef();
|
||||
},
|
||||
|
@ -159,6 +163,17 @@ module.exports = createReactClass({
|
|||
<E2EIcon status={this.props.e2eStatus} /> :
|
||||
undefined;
|
||||
|
||||
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
let privateIcon;
|
||||
// Don't show an invite-only icon for DMs. Users know they're invite-only.
|
||||
if (!dmUserId && SettingsStore.getValue("feature_cross_signing")) {
|
||||
if (joinRule == "invite") {
|
||||
privateIcon = <InviteOnlyIcon />;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
@ -300,9 +315,9 @@ module.exports = createReactClass({
|
|||
|
||||
return (
|
||||
<div className="mx_RoomHeader light-panel">
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ e2eIcon }
|
||||
<div className="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
|
||||
{ privateIcon }
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ cancelButton }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 Vector Creations Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -17,29 +18,34 @@ limitations under the License.
|
|||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Timer from "../../../utils/Timer";
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import createReactClass from 'create-react-class';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as utils from "matrix-js-sdk/src/utils";
|
||||
import { _t } from '../../../languageHandler';
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const CallHandler = require('../../../CallHandler');
|
||||
const dis = require("../../../dispatcher");
|
||||
const sdk = require('../../../index');
|
||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import * as Rooms from '../../../Rooms';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
const Receipt = require('../../../utils/Receipt');
|
||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||
import RoomListStore from '../../../stores/RoomListStore';
|
||||
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import RoomSubList from '../../structures/RoomSubList';
|
||||
import ResizeHandle from '../elements/ResizeHandle';
|
||||
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../../index";
|
||||
import * as Receipt from "../../../utils/Receipt";
|
||||
import {Resizer} from '../../../resizer';
|
||||
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||
import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex";
|
||||
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import * as Unread from "../../../Unread";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {TAG_DM} from "../../../stores/RoomListStore";
|
||||
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
const HOVER_MOVE_TIMEOUT = 1000;
|
||||
|
@ -49,7 +55,7 @@ function labelForTagName(tagName) {
|
|||
return tagName;
|
||||
}
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomList',
|
||||
|
||||
propTypes: {
|
||||
|
@ -114,7 +120,8 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Replace component with real class, put this in the constructor.
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this.mounted = false;
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -156,7 +163,7 @@ module.exports = createReactClass({
|
|||
this.updateVisibleRooms();
|
||||
});
|
||||
|
||||
this._roomListStoreToken = RoomListStore.addListener(() => {
|
||||
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
|
||||
this._delayedRefreshRoomList();
|
||||
});
|
||||
|
||||
|
@ -241,6 +248,55 @@ module.exports = createReactClass({
|
|||
});
|
||||
}
|
||||
break;
|
||||
case 'view_room_delta': {
|
||||
const currentRoomId = RoomViewStore.getRoomId();
|
||||
const {
|
||||
"im.vector.fake.invite": inviteRooms,
|
||||
"m.favourite": favouriteRooms,
|
||||
[TAG_DM]: dmRooms,
|
||||
"im.vector.fake.recent": recentRooms,
|
||||
"m.lowpriority": lowPriorityRooms,
|
||||
"im.vector.fake.archived": historicalRooms,
|
||||
"m.server_notice": serverNoticeRooms,
|
||||
...tags
|
||||
} = this.state.lists;
|
||||
|
||||
const shownCustomTagRooms = Object.keys(tags).filter(tagName => {
|
||||
return (!this.state.customTags || this.state.customTags[tagName]) &&
|
||||
!tagName.match(STANDARD_TAGS_REGEX);
|
||||
}).map(tagName => tags[tagName]);
|
||||
|
||||
// this order matches the one when generating the room sublists below.
|
||||
let rooms = this._applySearchFilter([
|
||||
...inviteRooms,
|
||||
...favouriteRooms,
|
||||
...dmRooms,
|
||||
...recentRooms,
|
||||
...[].concat.apply([], shownCustomTagRooms), // eslint-disable-line prefer-spread
|
||||
...lowPriorityRooms,
|
||||
...historicalRooms,
|
||||
...serverNoticeRooms,
|
||||
], this.props.searchFilter);
|
||||
|
||||
if (payload.unread) {
|
||||
// filter to only notification rooms (and our current active room so we can index properly)
|
||||
rooms = rooms.filter(room => {
|
||||
return room.roomId === currentRoomId || Unread.doesRoomHaveUnreadMessages(room);
|
||||
});
|
||||
}
|
||||
|
||||
const currentIndex = rooms.findIndex(room => room.roomId === currentRoomId);
|
||||
// use slice to account for looping around the start
|
||||
const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length);
|
||||
if (room) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
show_room_tile: true, // to make sure the room gets scrolled into view
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -384,7 +440,7 @@ module.exports = createReactClass({
|
|||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||
_delayedRefreshRoomList: rate_limited_func(function() {
|
||||
this.refreshRoomList();
|
||||
}, 500),
|
||||
|
||||
|
@ -467,7 +523,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getTagNameForRoomId: function(roomId) {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||
for (const tagName of Object.keys(lists)) {
|
||||
for (const room of lists[tagName]) {
|
||||
// Should be impossible, but guard anyways.
|
||||
|
@ -487,7 +543,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getRoomLists: function() {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const lists = RoomListStoreTempProxy.getRoomLists();
|
||||
|
||||
const filteredLists = {};
|
||||
|
||||
|
@ -589,10 +645,22 @@ module.exports = createReactClass({
|
|||
_applySearchFilter: function(list, filter) {
|
||||
if (filter === "") return list;
|
||||
const lcFilter = filter.toLowerCase();
|
||||
// apply toLowerCase before and after removeHiddenChars because different rules get applied
|
||||
// e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppercase, e.g 𝚮 -> H
|
||||
const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase();
|
||||
// case insensitive if room name includes filter,
|
||||
// or if starts with `#` and one of room's aliases starts with filter
|
||||
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
||||
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||
return list.filter((room) => {
|
||||
if (filter[0] === "#") {
|
||||
if (room.getCanonicalAlias() && room.getCanonicalAlias().toLowerCase().startsWith(lcFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (room.getAltAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter);
|
||||
});
|
||||
},
|
||||
|
||||
_handleCollapsedState: function(key, collapsed) {
|
||||
|
@ -628,7 +696,6 @@ module.exports = createReactClass({
|
|||
const defaultProps = {
|
||||
collapsed: this.props.collapsed,
|
||||
isFiltered: !!this.props.searchFilter,
|
||||
incomingCall: this.state.incomingCall,
|
||||
};
|
||||
|
||||
subListsProps.forEach((p) => {
|
||||
|
@ -641,7 +708,7 @@ module.exports = createReactClass({
|
|||
}));
|
||||
|
||||
return subListsProps.reduce((components, props, i) => {
|
||||
props = Object.assign({}, defaultProps, props);
|
||||
props = {...defaultProps, ...props};
|
||||
const isLast = i === subListsProps.length - 1;
|
||||
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
|
||||
const {key, label, onHeaderClick, ...otherProps} = props;
|
||||
|
@ -652,12 +719,12 @@ module.exports = createReactClass({
|
|||
onHeaderClick(collapsed);
|
||||
}
|
||||
};
|
||||
let startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||
this._layoutSections.push({
|
||||
id: chosenKey,
|
||||
count: len,
|
||||
});
|
||||
let subList = (<RoomSubList
|
||||
const subList = (<RoomSubList
|
||||
ref={this._subListRef.bind(this, chosenKey)}
|
||||
startAsHidden={startAsHidden}
|
||||
forceExpand={!!this.props.searchFilter}
|
||||
|
@ -693,13 +760,11 @@ module.exports = createReactClass({
|
|||
list: [],
|
||||
extraTiles: this._makeGroupInviteTiles(this.props.searchFilter),
|
||||
label: _t('Community Invites'),
|
||||
order: "recent",
|
||||
isInvite: true,
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.invite'],
|
||||
label: _t('Invites'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'),
|
||||
isInvite: true,
|
||||
},
|
||||
|
@ -707,24 +772,22 @@ module.exports = createReactClass({
|
|||
list: this.state.lists['m.favourite'],
|
||||
label: _t('Favourites'),
|
||||
tagName: "m.favourite",
|
||||
order: "manual",
|
||||
incomingCall: incomingCallIfTaggedAs('m.favourite'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.direct'],
|
||||
label: _t('People'),
|
||||
tagName: "im.vector.fake.direct",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'),
|
||||
list: this.state.lists[DefaultTagID.DM],
|
||||
label: _t('Direct Messages'),
|
||||
tagName: DefaultTagID.DM,
|
||||
incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
|
||||
addRoomLabel: _t("Start chat"),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.recent'],
|
||||
label: _t('Rooms'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'),
|
||||
onAddRoom: () => {dis.dispatch({action: 'view_create_room'});},
|
||||
addRoomLabel: _t("Create room"),
|
||||
},
|
||||
];
|
||||
const tagSubLists = Object.keys(this.state.lists)
|
||||
|
@ -737,7 +800,6 @@ module.exports = createReactClass({
|
|||
key: tagName,
|
||||
label: labelForTagName(tagName),
|
||||
tagName: tagName,
|
||||
order: "manual",
|
||||
incomingCall: incomingCallIfTaggedAs(tagName),
|
||||
};
|
||||
});
|
||||
|
@ -747,13 +809,11 @@ module.exports = createReactClass({
|
|||
list: this.state.lists['m.lowpriority'],
|
||||
label: _t('Low priority'),
|
||||
tagName: "m.lowpriority",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('m.lowpriority'),
|
||||
},
|
||||
{
|
||||
list: this.state.lists['im.vector.fake.archived'],
|
||||
label: _t('Historical'),
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'),
|
||||
startAsHidden: true,
|
||||
showSpinner: this.state.isLoadingLeftRooms,
|
||||
|
@ -763,26 +823,31 @@ module.exports = createReactClass({
|
|||
list: this.state.lists['m.server_notice'],
|
||||
label: _t('System Alerts'),
|
||||
tagName: "m.lowpriority",
|
||||
order: "recent",
|
||||
incomingCall: incomingCallIfTaggedAs('m.server_notice'),
|
||||
},
|
||||
]);
|
||||
|
||||
const subListComponents = this._mapSubListProps(subLists);
|
||||
|
||||
const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, ...props} = this.props; // eslint-disable-line
|
||||
const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, onKeyDown, ...props} = this.props; // eslint-disable-line
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={this._collectResizeContainer}
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
onMouseMove={this.onMouseMove}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ subListComponents }
|
||||
</div>
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => <div
|
||||
{...props}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
ref={this._collectResizeContainer}
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex="-1"
|
||||
onMouseMove={this.onMouseMove}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ subListComponents }
|
||||
</div> }
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
246
src/components/views/rooms/RoomList2.tsx
Normal file
246
src/components/views/rooms/RoomList2.tsx
Normal file
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 Vector Creations Ltd
|
||||
Copyright 2020 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 * as React from "react";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { Layout } from '../../../resizer/distributors/roomsublist2';
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
|
||||
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { Dispatcher } from "flux";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist2 from "./RoomSublist2";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
*******************************************************************
|
||||
* This is a work in progress implementation and isn't complete or *
|
||||
* even useful as a component. Please avoid using it until this *
|
||||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
onFocus: (ev: React.FocusEvent) => void;
|
||||
onBlur: (ev: React.FocusEvent) => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
collapsed: boolean;
|
||||
searchFilter: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
sublists: ITagMap;
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
// -- Community Invites Placeholder --
|
||||
|
||||
DefaultTagID.Invite,
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
|
||||
// -- Custom Tags Placeholder --
|
||||
|
||||
DefaultTagID.LowPriority,
|
||||
DefaultTagID.ServerNotice,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite;
|
||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
];
|
||||
|
||||
interface ITagAesthetics {
|
||||
sectionLabel: string;
|
||||
addRoomLabel?: string;
|
||||
onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void;
|
||||
isInvite: boolean;
|
||||
defaultHidden: boolean;
|
||||
}
|
||||
|
||||
const TAG_AESTHETICS: {
|
||||
// @ts-ignore - TS wants this to be a string but we know better
|
||||
[tagId: TagID]: ITagAesthetics;
|
||||
} = {
|
||||
[DefaultTagID.Invite]: {
|
||||
sectionLabel: _td("Invites"),
|
||||
isInvite: true,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.Favourite]: {
|
||||
sectionLabel: _td("Favourites"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.DM]: {
|
||||
sectionLabel: _td("Direct Messages"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Start chat"),
|
||||
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_chat'}),
|
||||
},
|
||||
[DefaultTagID.Untagged]: {
|
||||
sectionLabel: _td("Rooms"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
addRoomLabel: _td("Create room"),
|
||||
onAddRoom: (dispatcher: Dispatcher<ActionPayload>) => dispatcher.dispatch({action: 'view_create_room'}),
|
||||
},
|
||||
[DefaultTagID.LowPriority]: {
|
||||
sectionLabel: _td("Low priority"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.ServerNotice]: {
|
||||
sectionLabel: _td("System Alerts"),
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
},
|
||||
[DefaultTagID.Archived]: {
|
||||
sectionLabel: _td("Historical"),
|
||||
isInvite: false,
|
||||
defaultHidden: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default class RoomList2 extends React.Component<IProps, IState> {
|
||||
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
|
||||
private sublistSizes: { [tagId: string]: number } = {};
|
||||
private sublistCollapseStates: { [tagId: string]: boolean } = {};
|
||||
private unfilteredLayout: Layout;
|
||||
private filteredLayout: Layout;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {sublists: {}};
|
||||
this.loadSublistSizes();
|
||||
this.prepareLayouts();
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
|
||||
console.log("new lists", store.orderedLists);
|
||||
this.setState({sublists: store.orderedLists});
|
||||
});
|
||||
}
|
||||
|
||||
private loadSublistSizes() {
|
||||
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
|
||||
if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
|
||||
|
||||
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
|
||||
if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
|
||||
}
|
||||
|
||||
private saveSublistSizes() {
|
||||
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
|
||||
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
|
||||
}
|
||||
|
||||
private prepareLayouts() {
|
||||
// TODO: Change layout engine for FTUE support
|
||||
this.unfilteredLayout = new Layout((tagId: string, height: number) => {
|
||||
const sublist = this.sublistRefs[tagId];
|
||||
if (sublist) sublist.current.setHeight(height);
|
||||
|
||||
// TODO: Check overflow (see old impl)
|
||||
|
||||
// Don't store a height for collapsed sublists
|
||||
if (!this.sublistCollapseStates[tagId]) {
|
||||
this.sublistSizes[tagId] = height;
|
||||
this.saveSublistSizes();
|
||||
}
|
||||
}, this.sublistSizes, this.sublistCollapseStates, {
|
||||
allowWhitespace: false,
|
||||
handleHeight: 1,
|
||||
});
|
||||
|
||||
this.filteredLayout = new Layout((tagId: string, height: number) => {
|
||||
const sublist = this.sublistRefs[tagId];
|
||||
if (sublist) sublist.current.setHeight(height);
|
||||
}, null, null, {
|
||||
allowWhitespace: false,
|
||||
handleHeight: 0,
|
||||
});
|
||||
}
|
||||
|
||||
private renderSublists(): React.ReactElement[] {
|
||||
const components: React.ReactElement[] = [];
|
||||
|
||||
for (const orderedTagId of TAG_ORDER) {
|
||||
if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate community invites if we have the chance
|
||||
// TODO
|
||||
}
|
||||
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) {
|
||||
// Populate custom tags if needed
|
||||
// TODO
|
||||
}
|
||||
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) {
|
||||
continue; // skip tag - not needed
|
||||
}
|
||||
|
||||
const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||
components.push(<RoomSublist2
|
||||
key={`sublist-${orderedTagId}`}
|
||||
forRooms={true}
|
||||
rooms={orderedRooms}
|
||||
startAsHidden={aesthetics.defaultHidden}
|
||||
label={_t(aesthetics.sectionLabel)}
|
||||
onAddRoom={onAddRoomFn}
|
||||
addRoomLabel={aesthetics.addRoomLabel}
|
||||
isInvite={aesthetics.isInvite}
|
||||
/>);
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const sublists = this.renderSublists();
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
<div
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
className="mx_RoomList"
|
||||
role="tree"
|
||||
aria-label={_t("Rooms")}
|
||||
// Firefox sometimes makes this element focusable due to
|
||||
// overflow:scroll;, so force it out of tab order.
|
||||
tabIndex={-1}
|
||||
>{sublists}</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,11 +17,11 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomNameEditor',
|
||||
|
||||
propTypes: {
|
||||
|
@ -34,7 +34,8 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount: function() {
|
||||
const room = this.props.room;
|
||||
const name = room.currentState.getStateEvents('m.room.name', '');
|
||||
const myId = MatrixClientPeg.get().credentials.userId;
|
||||
|
|
|
@ -19,9 +19,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
|
@ -43,12 +43,13 @@ const MessageCase = Object.freeze({
|
|||
OtherError: "OtherError",
|
||||
});
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomPreviewBar',
|
||||
|
||||
propTypes: {
|
||||
onJoinClick: PropTypes.func,
|
||||
onRejectClick: PropTypes.func,
|
||||
onRejectAndIgnoreClick: PropTypes.func,
|
||||
onForgetClick: PropTypes.func,
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifiying inviterName
|
||||
|
@ -96,7 +97,7 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
this._checkInvitedEmail();
|
||||
},
|
||||
|
||||
|
@ -265,9 +266,9 @@ module.exports = createReactClass({
|
|||
params: {
|
||||
email: this.props.invitedEmail,
|
||||
signurl: this.props.signUrl,
|
||||
room_name: this.props.oobData.room_name,
|
||||
room_avatar_url: this.props.oobData.avatarUrl,
|
||||
inviter_name: this.props.oobData.inviterName,
|
||||
room_name: this.props.oobData ? this.props.oobData.room_name : null,
|
||||
room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null,
|
||||
inviter_name: this.props.oobData ? this.props.oobData.inviterName : null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
@ -282,6 +283,7 @@ module.exports = createReactClass({
|
|||
|
||||
render: function() {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let showSpinner = false;
|
||||
let darkStyle = false;
|
||||
|
@ -292,6 +294,7 @@ module.exports = createReactClass({
|
|||
let secondaryActionHandler;
|
||||
let secondaryActionLabel;
|
||||
let footer;
|
||||
const extraComponents = [];
|
||||
|
||||
const messageCase = this._getMessageCase();
|
||||
switch (messageCase) {
|
||||
|
@ -469,6 +472,14 @@ module.exports = createReactClass({
|
|||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("Reject");
|
||||
secondaryActionHandler = this.props.onRejectClick;
|
||||
|
||||
if (this.props.onRejectAndIgnoreClick) {
|
||||
extraComponents.push(
|
||||
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
|
||||
{ _t("Reject & Ignore user") }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.ViewingRoom: {
|
||||
|
@ -498,15 +509,13 @@ module.exports = createReactClass({
|
|||
"<issueLink>submit a bug report</issueLink>.",
|
||||
{ errcode: this.props.error.errcode },
|
||||
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
||||
target="_blank" rel="noopener">{ label }</a> },
|
||||
target="_blank" rel="noreferrer noopener">{ label }</a> },
|
||||
),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let subTitleElements;
|
||||
if (subTitle) {
|
||||
if (!Array.isArray(subTitle)) {
|
||||
|
@ -554,6 +563,7 @@ module.exports = createReactClass({
|
|||
</div>
|
||||
<div className="mx_RoomPreviewBar_actions">
|
||||
{ secondaryButton }
|
||||
{ extraComponents }
|
||||
{ primaryButton }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_footer">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
|
@ -16,10 +17,10 @@ limitations under the License.
|
|||
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import sdk from "../../../index";
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
|
||||
export default class RoomRecoveryReminder extends React.PureComponent {
|
||||
|
@ -43,7 +44,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this._loadBackupStatus();
|
||||
}
|
||||
|
||||
|
@ -60,7 +61,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
loading: false,
|
||||
error: e,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,10 +70,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
// verified, so restore the backup which will give us the keys from it and
|
||||
// allow us to trust it (ie. upload keys to it)
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {});
|
||||
Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
);
|
||||
} else {
|
||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||
null, null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +123,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
|
||||
let setupCaption;
|
||||
if (this.state.backupInfo) {
|
||||
setupCaption = _t("Connect this device to Key Backup");
|
||||
setupCaption = _t("Connect this session to Key Backup");
|
||||
} else {
|
||||
setupCaption = _t("Start using Key Backup");
|
||||
}
|
||||
|
@ -146,18 +150,18 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
)}</p>
|
||||
</div>
|
||||
<div className="mx_RoomRecoveryReminder_buttons">
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_button"
|
||||
<AccessibleButton kind="primary"
|
||||
onClick={this.onSetupClick}>
|
||||
{setupCaption}
|
||||
</AccessibleButton>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onOnNotNowClick}>
|
||||
{ _t("Not now") }
|
||||
</AccessibleButton></p>
|
||||
<p><AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
|
||||
onClick={this.onDontAskAgainClick}>
|
||||
{ _t("Don't ask me again") }
|
||||
</AccessibleButton></p>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
226
src/components/views/rooms/RoomSublist2.tsx
Normal file
226
src/components/views/rooms/RoomSublist2.tsx
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 Vector Creations Ltd
|
||||
Copyright 2020 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 * as React from "react";
|
||||
import { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import IndicatorScrollbar from "../../structures/IndicatorScrollbar";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton";
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import RoomTile2 from "./RoomTile2";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
*******************************************************************
|
||||
* This is a work in progress implementation and isn't complete or *
|
||||
* even useful as a component. Please avoid using it until this *
|
||||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
interface IProps {
|
||||
forRooms: boolean;
|
||||
rooms?: Room[];
|
||||
startAsHidden: boolean;
|
||||
label: string;
|
||||
onAddRoom?: () => void;
|
||||
addRoomLabel: string;
|
||||
isInvite: boolean;
|
||||
|
||||
// TODO: Collapsed state
|
||||
// TODO: Height
|
||||
// TODO: Group invites
|
||||
// TODO: Calls
|
||||
// TODO: forceExpand?
|
||||
// TODO: Header clicking
|
||||
// TODO: Spinner support for historical
|
||||
}
|
||||
|
||||
interface IState {
|
||||
}
|
||||
|
||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||
private headerButton = createRef();
|
||||
|
||||
public setHeight(size: number) {
|
||||
// TODO: Do a thing (maybe - height changes are different in FTUE)
|
||||
}
|
||||
|
||||
private hasTiles(): boolean {
|
||||
return this.numTiles > 0;
|
||||
}
|
||||
|
||||
private get numTiles(): number {
|
||||
// TODO: Account for group invites
|
||||
return (this.props.rooms || []).length;
|
||||
}
|
||||
|
||||
private onAddRoom = (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||
};
|
||||
|
||||
private renderTiles(): React.ReactElement[] {
|
||||
const tiles: React.ReactElement[] = [];
|
||||
|
||||
if (this.props.rooms) {
|
||||
for (const room of this.props.rooms) {
|
||||
tiles.push(<RoomTile2 room={room} key={`room-${room.roomId}`}/>);
|
||||
}
|
||||
}
|
||||
|
||||
return tiles;
|
||||
}
|
||||
|
||||
private renderHeader(): React.ReactElement {
|
||||
const notifications = !this.props.isInvite
|
||||
? RoomNotifs.aggregateNotificationCount(this.props.rooms)
|
||||
: {count: 0, highlight: true};
|
||||
const notifCount = notifications.count;
|
||||
const notifHighlight = notifications.highlight;
|
||||
|
||||
// TODO: Title on collapsed
|
||||
// TODO: Incoming call box
|
||||
|
||||
let chevron = null;
|
||||
if (this.hasTiles()) {
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': false, // isCollapsed
|
||||
'mx_RoomSubList_chevronDown': true, // !isCollapsed
|
||||
});
|
||||
chevron = (<div className={chevronClasses}/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
||||
{({onFocus, isActive, ref}) => {
|
||||
// TODO: Use onFocus
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
// TODO: Collapsed state
|
||||
let badge;
|
||||
if (true) { // !isCollapsed
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': notifHighlight,
|
||||
});
|
||||
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
|
||||
if (notifCount > 0) {
|
||||
badge = (
|
||||
<AccessibleButton
|
||||
tabIndex={tabIndex}
|
||||
className={badgeClasses}
|
||||
aria-label={_t("Jump to first unread room.")}
|
||||
>
|
||||
<div>
|
||||
{FormattingUtils.formatCount(notifCount)}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (this.props.isInvite && this.hasTiles()) {
|
||||
// Render the `!` badge for invites
|
||||
badge = (
|
||||
<AccessibleButton
|
||||
tabIndex={tabIndex}
|
||||
className={badgeClasses}
|
||||
aria-label={_t("Jump to first invite.")}
|
||||
>
|
||||
<div>
|
||||
{FormattingUtils.formatCount(this.numTiles)}
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let addRoomButton = null;
|
||||
if (!!this.props.onAddRoom) {
|
||||
addRoomButton = (
|
||||
<AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={this.onAddRoom}
|
||||
className="mx_RoomSubList_addRoom"
|
||||
title={this.props.addRoomLabel || _t("Add room")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: a11y (see old component)
|
||||
return (
|
||||
<div className={"mx_RoomSubList_labelContainer"}>
|
||||
<AccessibleButton
|
||||
inputRef={ref}
|
||||
tabIndex={tabIndex}
|
||||
className={"mx_RoomSubList_label"}
|
||||
role="treeitem"
|
||||
aria-level="1"
|
||||
>
|
||||
{chevron}
|
||||
<span>{this.props.label}</span>
|
||||
</AccessibleButton>
|
||||
{badge}
|
||||
{addRoomButton}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</RovingTabIndexWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Proper rendering
|
||||
// TODO: Error boundary
|
||||
|
||||
const tiles = this.renderTiles();
|
||||
|
||||
const classes = classNames({
|
||||
// TODO: Proper collapse support
|
||||
'mx_RoomSubList': true,
|
||||
'mx_RoomSubList_hidden': false, // len && isCollapsed
|
||||
'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed
|
||||
});
|
||||
|
||||
let content = null;
|
||||
if (tiles.length > 0) {
|
||||
// TODO: Lazy list rendering
|
||||
// TODO: Whatever scrolling magic needs to happen here
|
||||
content = (
|
||||
<IndicatorScrollbar className='mx_RoomSubList_scroll'>
|
||||
{tiles}
|
||||
</IndicatorScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: onKeyDown support
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
role="group"
|
||||
aria-label={this.props.label}
|
||||
>
|
||||
{this.renderHeader()}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,14 +17,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import classNames from 'classnames';
|
||||
import dis from '../../../dispatcher';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
|
@ -32,8 +32,14 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
|||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
||||
import E2EIcon from './E2EIcon';
|
||||
import InviteOnlyIcon from './InviteOnlyIcon';
|
||||
// eslint-disable-next-line camelcase
|
||||
import rate_limited_func from '../../../ratelimitedfunc';
|
||||
import { shieldStatusForRoom } from '../../../utils/ShieldUtils';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomTile',
|
||||
|
||||
propTypes: {
|
||||
|
@ -56,7 +62,11 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
|
||||
return ({
|
||||
joinRule,
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
contextMenuPosition: null, // DOM bounding box, null if non-shown
|
||||
|
@ -65,14 +75,10 @@ module.exports = createReactClass({
|
|||
notificationCount: this.props.room.getUnreadNotificationCount(),
|
||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||
statusMessage: this._getStatusMessage(),
|
||||
e2eStatus: null,
|
||||
});
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
return Boolean(dmRooms);
|
||||
},
|
||||
|
||||
_shouldShowStatusMessage() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
|
||||
return false;
|
||||
|
@ -102,6 +108,63 @@ module.exports = createReactClass({
|
|||
return statusUser._unstable_statusMessage;
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
// we only care about leaving users
|
||||
// because trust state will change if someone joins a megolm session anyway
|
||||
if (member.membership !== "leave") {
|
||||
return;
|
||||
}
|
||||
// ignore members in other rooms
|
||||
if (member.roomId !== this.props.room.roomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateE2eStatus();
|
||||
},
|
||||
|
||||
onUserVerificationChanged: function(userId, _trustStatus) {
|
||||
if (!this.props.room.getMember(userId)) {
|
||||
// Not in this room
|
||||
return;
|
||||
}
|
||||
this._updateE2eStatus();
|
||||
},
|
||||
|
||||
onCrossSigningKeysChanged: function() {
|
||||
this._updateE2eStatus();
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room) {
|
||||
if (!room) return;
|
||||
if (room.roomId != this.props.room.roomId) return;
|
||||
if (ev.getType() !== "m.room.encryption") return;
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
this.onFindingRoomToBeEncrypted();
|
||||
},
|
||||
|
||||
onFindingRoomToBeEncrypted: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
this._updateE2eStatus();
|
||||
},
|
||||
|
||||
_updateE2eStatus: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
|
||||
return;
|
||||
}
|
||||
if (!SettingsStore.getValue("feature_cross_signing")) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* At this point, the user has encryption on and cross-signing on */
|
||||
this.setState({
|
||||
e2eStatus: await shieldStatusForRoom(cli, this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
onRoomName: function(room) {
|
||||
if (room !== this.props.room) return;
|
||||
this.setState({
|
||||
|
@ -109,6 +172,12 @@ module.exports = createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
onJoinRule: function(ev) {
|
||||
if (ev.getType() !== "m.room.join_rules") return;
|
||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||
this.setState({ joinRule: ev.getContent().join_rule });
|
||||
},
|
||||
|
||||
onAccountData: function(accountDataEvent) {
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
this.setState({
|
||||
|
@ -135,19 +204,49 @@ module.exports = createReactClass({
|
|||
case 'feature_custom_status_changed':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
|
||||
case 'view_room':
|
||||
// when the room is selected make sure its tile is visible, for breadcrumbs/keyboard shortcut access
|
||||
if (payload.room_id === this.props.room.roomId && payload.show_room_tile) {
|
||||
this._scrollIntoView();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_scrollIntoView: function() {
|
||||
if (!this._roomTile.current) return;
|
||||
this._roomTile.current.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "auto",
|
||||
});
|
||||
},
|
||||
|
||||
_onActiveRoomChange: function() {
|
||||
this.setState({
|
||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._roomTile = createRef();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
/* We bind here rather than in the definition because otherwise we wind up with the
|
||||
method only being callable once every 500ms across all instances, which would be wrong */
|
||||
this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("accountData", this.onAccountData);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("RoomState.events", this.onJoinRule);
|
||||
if (cli.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this.onFindingRoomToBeEncrypted();
|
||||
} else {
|
||||
cli.on("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
|
@ -157,6 +256,11 @@ module.exports = createReactClass({
|
|||
statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
|
||||
}
|
||||
}
|
||||
|
||||
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
|
||||
if (this.state.selected) {
|
||||
this._scrollIntoView();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
|
@ -164,6 +268,11 @@ module.exports = createReactClass({
|
|||
if (cli) {
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
cli.removeListener("RoomState.events", this.onJoinRule);
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
||||
cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
|
||||
cli.removeListener("Room.timeline", this.onRoomTimeline);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
@ -179,7 +288,8 @@ module.exports = createReactClass({
|
|||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(props) {
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(props) {
|
||||
// XXX: This could be a lot better - this makes the assumption that
|
||||
// the notification count may have changed when the properties of
|
||||
// the room tile change.
|
||||
|
@ -297,6 +407,8 @@ module.exports = createReactClass({
|
|||
|
||||
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
|
||||
|
||||
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
|
@ -320,10 +432,9 @@ module.exports = createReactClass({
|
|||
});
|
||||
|
||||
let name = this.state.roomName;
|
||||
if (name == undefined || name == null) name = '';
|
||||
if (typeof name !== 'string') name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
|
||||
let badge;
|
||||
if (badges) {
|
||||
const limitedCount = FormattingUtils.formatCount(notificationCount);
|
||||
|
@ -342,7 +453,8 @@ module.exports = createReactClass({
|
|||
});
|
||||
|
||||
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
|
||||
label = <div title={name} className={nameClasses} dir="auto">{ name }</div>;
|
||||
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
|
||||
label = <div title={name} className={nameClasses} tabIndex={-1} dir="auto">{ name }</div>;
|
||||
} else if (this.state.hover) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
|
@ -371,15 +483,12 @@ module.exports = createReactClass({
|
|||
|
||||
let ariaLabel = name;
|
||||
|
||||
let dmIndicator;
|
||||
if (this._isDirectMessageRoom(this.props.room.roomId)) {
|
||||
dmIndicator = <img
|
||||
src={require("../../../../res/img/icon_person.svg")}
|
||||
className="mx_RoomTile_dm"
|
||||
width="11"
|
||||
height="13"
|
||||
alt="dm"
|
||||
/>;
|
||||
let dmOnline;
|
||||
const { room } = this.props;
|
||||
const member = room.getMember(dmUserId);
|
||||
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
|
||||
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
|
||||
dmOnline = <UserOnlineDot userId={dmUserId} />;
|
||||
}
|
||||
|
||||
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||
|
@ -405,35 +514,55 @@ module.exports = createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let privateIcon = null;
|
||||
if (SettingsStore.getValue("feature_cross_signing")) {
|
||||
if (this.state.joinRule == "invite" && !dmUserId) {
|
||||
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
|
||||
}
|
||||
}
|
||||
|
||||
let e2eIcon = null;
|
||||
if (this.state.e2eStatus) {
|
||||
e2eIcon = <E2EIcon status={this.state.e2eStatus} className="mx_RoomTile_e2eIcon" />;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<AccessibleButton
|
||||
tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
<RovingTabIndexWrapper inputRef={this._roomTile}>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
aria-label={ariaLabel}
|
||||
aria-selected={this.state.selected}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ e2eIcon }
|
||||
</div>
|
||||
</div>
|
||||
{ privateIcon }
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
{ subtextLabel }
|
||||
</div>
|
||||
{ dmOnline }
|
||||
{ contextMenuButton }
|
||||
{ badge }
|
||||
</div>
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
|
|
277
src/components/views/rooms/RoomTile2.tsx
Normal file
277
src/components/views/rooms/RoomTile2.tsx
Normal file
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019, 2020 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, { createRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from "classnames";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
|
||||
import Tooltip from "../../views/elements/Tooltip";
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
|
||||
import * as Unread from '../../../Unread';
|
||||
import * as FormattingUtils from "../../../utils/FormattingUtils";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
/*******************************************************************
|
||||
* CAUTION *
|
||||
*******************************************************************
|
||||
* This is a work in progress implementation and isn't complete or *
|
||||
* even useful as a component. Please avoid using it until this *
|
||||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
enum NotificationColor {
|
||||
// Inverted (None -> Red) because we do integer comparisons on this
|
||||
None, // nothing special
|
||||
Bold, // no badge, show as unread
|
||||
Grey, // unread notified messages
|
||||
Red, // unread pings
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
||||
// TODO: Allow falsifying counts (for invites and stuff)
|
||||
// TODO: Transparency? Was this ever used?
|
||||
// TODO: Incoming call boxes?
|
||||
}
|
||||
|
||||
interface INotificationState {
|
||||
symbol: string;
|
||||
color: NotificationColor;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
notificationState: INotificationState;
|
||||
}
|
||||
|
||||
export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
private roomTile = createRef();
|
||||
|
||||
// TODO: Custom status
|
||||
// TODO: Lock icon
|
||||
// TODO: Presence indicator
|
||||
// TODO: e2e shields
|
||||
// TODO: Handle changes to room aesthetics (name, join rules, etc)
|
||||
// TODO: scrollIntoView?
|
||||
// TODO: hover, badge, etc
|
||||
// TODO: isSelected for hover effects
|
||||
// TODO: Context menu
|
||||
// TODO: a11y
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
notificationState: this.getNotificationState(),
|
||||
};
|
||||
|
||||
this.props.room.on("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.props.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.props.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.props.room) {
|
||||
this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
|
||||
this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||
this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||
}
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
|
||||
// this, but instead we're kinda forced to either duplicate the code or thread a variable
|
||||
// through the code paths. This feels like the least evil option.
|
||||
private get roomIsInvite(): boolean {
|
||||
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
|
||||
}
|
||||
|
||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||
const roomId = event.getRoomId();
|
||||
|
||||
// Sanity check: should never happen
|
||||
if (roomId !== this.props.room.roomId) return;
|
||||
|
||||
this.updateNotificationState();
|
||||
};
|
||||
|
||||
private updateNotificationState() {
|
||||
this.setState({notificationState: this.getNotificationState()});
|
||||
}
|
||||
|
||||
private getNotificationState(): INotificationState {
|
||||
const state: INotificationState = {
|
||||
color: NotificationColor.None,
|
||||
symbol: null,
|
||||
};
|
||||
|
||||
if (this.roomIsInvite) {
|
||||
state.color = NotificationColor.Red;
|
||||
state.symbol = "!";
|
||||
} else {
|
||||
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
|
||||
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total');
|
||||
|
||||
// For a 'true count' we pick the grey notifications first because they include the
|
||||
// red notifications. If we don't have a grey count for some reason we use the red
|
||||
// count. If that count is broken for some reason, assume zero. This avoids us showing
|
||||
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
|
||||
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
|
||||
|
||||
// Note: we only set the symbol if we have an actual count. We don't want to show
|
||||
// zero on badges.
|
||||
|
||||
if (redNotifs > 0) {
|
||||
state.color = NotificationColor.Red;
|
||||
state.symbol = FormattingUtils.formatCount(trueCount);
|
||||
} else if (greyNotifs > 0) {
|
||||
state.color = NotificationColor.Grey;
|
||||
state.symbol = FormattingUtils.formatCount(trueCount);
|
||||
} else {
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room);
|
||||
if (hasUnread) {
|
||||
state.color = NotificationColor.Bold;
|
||||
// no symbol for this state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private onTileMouseEnter = () => {
|
||||
this.setState({hover: true});
|
||||
};
|
||||
|
||||
private onTileMouseLeave = () => {
|
||||
this.setState({hover: false});
|
||||
};
|
||||
|
||||
private onTileClick = (ev: React.KeyboardEvent) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
// TODO: Support show_room_tile in new room list
|
||||
show_room_tile: true, // make sure the room is visible in the list
|
||||
room_id: this.props.room.roomId,
|
||||
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
||||
});
|
||||
};
|
||||
|
||||
public render(): React.ReactElement {
|
||||
// TODO: Collapsed state
|
||||
// TODO: Invites
|
||||
// TODO: a11y proper
|
||||
// TODO: Render more than bare minimum
|
||||
|
||||
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
|
||||
const isUnread = this.state.notificationState.color > NotificationColor.None;
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
// 'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_unread': isUnread,
|
||||
'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
|
||||
'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red,
|
||||
'mx_RoomTile_invited': this.roomIsInvite,
|
||||
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
|
||||
'mx_RoomTile_noBadges': !hasBadge,
|
||||
// 'mx_RoomTile_transparent': this.props.transparent,
|
||||
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const avatarClasses = classNames({
|
||||
'mx_RoomTile_avatar': true,
|
||||
});
|
||||
|
||||
|
||||
let badge;
|
||||
if (hasBadge) {
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
|
||||
});
|
||||
badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>;
|
||||
}
|
||||
|
||||
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||
let name = this.props.room.name;
|
||||
if (typeof name !== 'string') name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
const nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.roomIsInvite,
|
||||
'mx_RoomTile_badgeShown': hasBadge,
|
||||
});
|
||||
|
||||
// TODO: Support collapsed state properly
|
||||
let tooltip = null;
|
||||
if (false) { // isCollapsed
|
||||
if (this.state.hover) {
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<RovingTabIndexWrapper inputRef={this.roomTile}>
|
||||
{({onFocus, isActive, ref}) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onMouseEnter={this.onTileMouseEnter}
|
||||
onMouseLeave={this.onTileMouseLeave}
|
||||
onClick={this.onTileClick}
|
||||
role="treeitem"
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
{badge}
|
||||
</div>
|
||||
{tooltip}
|
||||
</AccessibleButton>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomTopicEditor',
|
||||
|
||||
propTypes: {
|
||||
|
@ -33,7 +33,7 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
const room = this.props.room;
|
||||
const topic = room.currentState.getStateEvents('m.room.topic', '');
|
||||
this.setState({
|
||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'RoomUpgradeWarningBar',
|
||||
|
||||
propTypes: {
|
||||
|
@ -31,7 +31,7 @@ module.exports = createReactClass({
|
|||
recommendation: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
|
||||
|
||||
|
|
|
@ -16,12 +16,12 @@ limitations under the License.
|
|||
|
||||
import React, {createRef} from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
const classNames = require('classnames');
|
||||
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import classNames from "classnames";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'SearchBar',
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -30,6 +30,7 @@ module.exports = createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
|
||||
UNSAFE_componentWillMount: function() {
|
||||
this._search_term = createRef();
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -17,9 +18,10 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import {haveTileForEvent} from "./EventTile";
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'SearchResult',
|
||||
|
||||
propTypes: {
|
||||
|
@ -53,7 +55,7 @@ module.exports = createReactClass({
|
|||
if (!contextual) {
|
||||
highlights = this.props.searchHighlights;
|
||||
}
|
||||
if (EventTile.haveTileForEvent(ev)) {
|
||||
if (haveTileForEvent(ev)) {
|
||||
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
highlightLink={this.props.resultLink}
|
||||
|
|
|
@ -1,185 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 createReactClass from 'create-react-class';
|
||||
import sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// A list capable of displaying entities which conform to the SearchableEntity
|
||||
// interface which is an object containing getJsx(): Jsx and matches(query: string): boolean
|
||||
const SearchableEntityList = createReactClass({
|
||||
displayName: 'SearchableEntityList',
|
||||
|
||||
propTypes: {
|
||||
emptyQueryShowsAll: PropTypes.bool,
|
||||
showInputBox: PropTypes.bool,
|
||||
onQueryChanged: PropTypes.func, // fn(inputText)
|
||||
onSubmit: PropTypes.func, // fn(inputText)
|
||||
entities: PropTypes.array,
|
||||
truncateAt: PropTypes.number,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
showInputBox: true,
|
||||
entities: [],
|
||||
emptyQueryShowsAll: false,
|
||||
onSubmit: function() {},
|
||||
onQueryChanged: function(input) {},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
query: "",
|
||||
focused: false,
|
||||
truncateAt: this.props.truncateAt,
|
||||
results: this.getSearchResults("", this.props.entities),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
// recalculate the search results in case we got new entities
|
||||
this.setState({
|
||||
results: this.getSearchResults(this.state.query, newProps.entities),
|
||||
});
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// pretend the query box was blanked out else filters could still be
|
||||
// applied to other components which rely on onQueryChanged.
|
||||
this.props.onQueryChanged("");
|
||||
},
|
||||
|
||||
/**
|
||||
* Public-facing method to set the input query text to the given input.
|
||||
* @param {string} input
|
||||
*/
|
||||
setQuery: function(input) {
|
||||
this.setState({
|
||||
query: input,
|
||||
results: this.getSearchResults(input, this.props.entities),
|
||||
});
|
||||
},
|
||||
|
||||
onQueryChanged: function(ev) {
|
||||
const q = ev.target.value;
|
||||
this.setState({
|
||||
query: q,
|
||||
// reset truncation if they back out the entire text
|
||||
truncateAt: (q.length === 0 ? this.props.truncateAt : this.state.truncateAt),
|
||||
results: this.getSearchResults(q, this.props.entities),
|
||||
}, () => {
|
||||
// invoke the callback AFTER we've flushed the new state. We need to
|
||||
// do this because onQueryChanged can result in new props being passed
|
||||
// to this component, which will then try to recalculate the search
|
||||
// list. If we do this without flushing, we'll recalc with the last
|
||||
// search term and not the current one!
|
||||
this.props.onQueryChanged(q);
|
||||
});
|
||||
},
|
||||
|
||||
onQuerySubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.props.onSubmit(this.state.query);
|
||||
},
|
||||
|
||||
getSearchResults: function(query, entities) {
|
||||
if (!query || query.length === 0) {
|
||||
return this.props.emptyQueryShowsAll ? entities : [];
|
||||
}
|
||||
return entities.filter(function(e) {
|
||||
return e.matches(query);
|
||||
});
|
||||
},
|
||||
|
||||
_showAll: function() {
|
||||
this.setState({
|
||||
truncateAt: -1,
|
||||
});
|
||||
},
|
||||
|
||||
_createOverflowEntity: function(overflowCount, totalCount) {
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={this._showAll} />
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let inputBox;
|
||||
|
||||
if (this.props.showInputBox) {
|
||||
inputBox = (
|
||||
<form onSubmit={this.onQuerySubmit} autoComplete="off">
|
||||
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
|
||||
onChange={this.onQueryChanged} value={this.state.query}
|
||||
onFocus= {() => { this.setState({ focused: true }); }}
|
||||
onBlur= {() => { this.setState({ focused: false }); }}
|
||||
placeholder={_t("Search")} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
let list;
|
||||
if (this.state.results.length > 1 || this.state.focused) {
|
||||
if (this.props.truncateAt) { // caller wants list truncated
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
list = (
|
||||
<TruncatedList className="mx_SearchableEntityList_list"
|
||||
truncateAt={this.state.truncateAt} // use state truncation as it may be expanded
|
||||
createOverflowElement={this._createOverflowEntity}>
|
||||
{ this.state.results.map((entity) => {
|
||||
return entity.getJsx();
|
||||
}) }
|
||||
</TruncatedList>
|
||||
);
|
||||
} else {
|
||||
list = (
|
||||
<div className="mx_SearchableEntityList_list">
|
||||
{ this.state.results.map((entity) => {
|
||||
return entity.getJsx();
|
||||
}) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
list = (
|
||||
<GeminiScrollbarWrapper autoshow={true}
|
||||
className="mx_SearchableEntityList_listWrapper">
|
||||
{ list }
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"mx_SearchableEntityList " + (list ? "mx_SearchableEntityList_expanded" : "")}>
|
||||
{ inputBox }
|
||||
{ list }
|
||||
{ list ? <div className="mx_SearchableEntityList_hrWrapper"><hr /></div> : '' }
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = SearchableEntityList;
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import {
|
||||
htmlSerializeIfNeeded,
|
||||
|
@ -24,9 +24,10 @@ import {
|
|||
containsEmote,
|
||||
stripEmoteCommand,
|
||||
unescapeMessage,
|
||||
startsWith,
|
||||
stripPrefix,
|
||||
} from '../../../editor/serialize';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import ReplyPreview from "./ReplyPreview";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -34,12 +35,15 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import {processCommandInput} from '../../../SlashCommands';
|
||||
import sdk from '../../../index';
|
||||
import {getCommand} from '../../../SlashCommands';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
|
@ -56,11 +60,15 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
|||
}
|
||||
}
|
||||
|
||||
function createMessageContent(model, permalinkCreator) {
|
||||
// exported for tests
|
||||
export function createMessageContent(model, permalinkCreator) {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
}
|
||||
if (startsWith(model, "//")) {
|
||||
model = stripPrefix(model, "/");
|
||||
}
|
||||
model = unescapeMessage(model);
|
||||
const repliedToEvent = RoomViewStore.getQuotingEvent();
|
||||
|
||||
|
@ -89,15 +97,19 @@ export default class SendMessageComposer extends React.Component {
|
|||
permalinkCreator: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.model = null;
|
||||
this._editorRef = null;
|
||||
this.currentlyComposedEditorState = null;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.isCryptoEnabled() && cli.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this._prepareToEncrypt = new RateLimitedFunc(() => {
|
||||
cli.prepareToEncrypt(this.props.room);
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
|
||||
_setEditorRef = ref => {
|
||||
|
@ -117,14 +129,23 @@ export default class SendMessageComposer extends React.Component {
|
|||
this.onVerticalArrow(event, true);
|
||||
} else if (event.key === Key.ARROW_DOWN) {
|
||||
this.onVerticalArrow(event, false);
|
||||
} else if (this._prepareToEncrypt) {
|
||||
this._prepareToEncrypt();
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onVerticalArrow(e, up) {
|
||||
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
|
||||
// arrows from an initial-caret composer navigates recent messages to edit
|
||||
// ctrl-alt-arrows navigate send history
|
||||
if (e.shiftKey || e.metaKey) return;
|
||||
|
||||
const shouldSelectHistory = e.altKey;
|
||||
const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent();
|
||||
const shouldSelectHistory = e.altKey && e.ctrlKey;
|
||||
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent();
|
||||
|
||||
if (shouldSelectHistory) {
|
||||
// Try select composer history
|
||||
|
@ -177,20 +198,21 @@ export default class SendMessageComposer extends React.Component {
|
|||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === "command") {
|
||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
// be extra resilient when somehow the AutocompleteWrapperModel or
|
||||
// CommandPartCreator fails to insert a command part, so we don't send
|
||||
// a command as a message
|
||||
if (firstPart.text.startsWith("/") && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async _runSlashCommand() {
|
||||
_getSlashCommand() {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
|
@ -198,54 +220,90 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const cmd = processCommandInput(this.props.room.roomId, commandText);
|
||||
return [getCommand(this.props.room.roomId, commandText), commandText];
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
let error = cmd.error;
|
||||
if (cmd.promise) {
|
||||
try {
|
||||
await cmd.promise;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
async _runSlashCommand(fn) {
|
||||
const cmd = fn();
|
||||
let error = cmd.error;
|
||||
if (cmd.promise) {
|
||||
try {
|
||||
await cmd.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 = !!cmd.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
}
|
||||
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 = !!cmd.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,
|
||||
});
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
}
|
||||
}
|
||||
|
||||
_sendMessage() {
|
||||
async _sendMessage() {
|
||||
if (this.model.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldSend = true;
|
||||
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
this._runSlashCommand();
|
||||
} else {
|
||||
const [cmd, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
shouldSend = false;
|
||||
this._runSlashCommand(cmd);
|
||||
} 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) {
|
||||
const isReply = !!RoomViewStore.getQuotingEvent();
|
||||
const {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||
this.context.matrixClient.sendMessage(roomId, content);
|
||||
this.context.sendMessage(roomId, content);
|
||||
if (isReply) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
|
@ -254,7 +312,9 @@ export default class SendMessageComposer extends React.Component {
|
|||
event: null,
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: "message_sent"});
|
||||
}
|
||||
|
||||
this.sendHistoryManager.save(this.model);
|
||||
// clear composer
|
||||
this.model.reset([]);
|
||||
|
@ -272,8 +332,9 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._editorRef.getEditableRootNode().removeEventListener("paste", this._onPaste, true);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient);
|
||||
// TODO: [REACT-WARNING] Move this to constructor
|
||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.context);
|
||||
const parts = this._restoreStoredEditorState(partCreator) || [];
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -331,7 +392,8 @@ export default class SendMessageComposer extends React.Component {
|
|||
member.rawDisplayName : userId;
|
||||
const caret = this._editorRef.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
const insertIndex = position.index + 1;
|
||||
// index is -1 if there are no parts but we only care for if this would be the part in position 0
|
||||
const insertIndex = position.index > 0 ? position.index : 0;
|
||||
const parts = partCreator.createMentionParts(insertIndex, displayName, userId);
|
||||
model.transform(() => {
|
||||
const addedLen = model.insert(parts, position);
|
||||
|
@ -375,7 +437,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
// from Finder) but more images copied from a different website
|
||||
// / word processor etc.
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context.matrixClient,
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// cancel button which is shared between room header and simple room header
|
||||
|
|
|
@ -1,485 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
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, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
|
||||
import E2EIcon from './E2EIcon';
|
||||
|
||||
const formatButtonList = [
|
||||
_td("bold"),
|
||||
_td("italic"),
|
||||
_td("deleted"),
|
||||
_td("underlined"),
|
||||
_td("inline-code"),
|
||||
_td("block-quote"),
|
||||
_td("bulleted-list"),
|
||||
_td("numbered-list"),
|
||||
];
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
return <div className="mx_MessageComposer_avatar">
|
||||
<MemberStatusMessageAvatar member={props.me} width={24} height={24} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
ComposerAvatar.propTypes = {
|
||||
me: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function CallButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onVoiceCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_voicecall"
|
||||
onClick={onVoiceCallClick}
|
||||
title={_t('Voice call')}
|
||||
/>
|
||||
}
|
||||
|
||||
CallButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
function VideoCallButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_videocall"
|
||||
onClick={onCallClick}
|
||||
title={_t('Video call')}
|
||||
/>;
|
||||
}
|
||||
|
||||
VideoCallButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function HangupButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const onHangupClick = () => {
|
||||
const call = CallHandler.getCallForRoom(props.roomId);
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'hangup',
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId,
|
||||
});
|
||||
};
|
||||
return <AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_hangup"
|
||||
onClick={onHangupClick}
|
||||
title={_t('Hangup')}
|
||||
/>;
|
||||
}
|
||||
|
||||
HangupButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
function FormattingButton(props) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return <AccessibleButton
|
||||
element="img"
|
||||
className="mx_MessageComposer_formatting"
|
||||
alt={_t("Show Text Formatting Toolbar")}
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src={require("../../../../res/img/button-text-formatting.svg")}
|
||||
style={{visibility: props.showFormatting ? 'hidden' : 'visible'}}
|
||||
onClick={props.onClickHandler}
|
||||
/>;
|
||||
}
|
||||
|
||||
FormattingButton.propTypes = {
|
||||
showFormatting: PropTypes.bool.isRequired,
|
||||
onClickHandler: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
class UploadButton extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
}
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onUploadClick = this.onUploadClick.bind(this);
|
||||
this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this);
|
||||
|
||||
this._uploadInput = createRef();
|
||||
}
|
||||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
||||
}
|
||||
this._uploadInput.current.click();
|
||||
}
|
||||
|
||||
onUploadFileInputChange(ev) {
|
||||
if (ev.target.files.length === 0) return;
|
||||
|
||||
// take a copy so we can safely reset the value of the form control
|
||||
// (Note it is a FileList: we can't use slice or sesnible iteration).
|
||||
const tfiles = [];
|
||||
for (let i = 0; i < ev.target.files.length; ++i) {
|
||||
tfiles.push(ev.target.files[i]);
|
||||
}
|
||||
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
tfiles, this.props.roomId, MatrixClientPeg.get(),
|
||||
);
|
||||
|
||||
// This is the onChange handler for a file form control, but we're
|
||||
// not keeping any state, so reset the value of the form control
|
||||
// to empty.
|
||||
// NB. we need to set 'value': the 'files' property is immutable.
|
||||
ev.target.value = '';
|
||||
}
|
||||
|
||||
render() {
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_upload"
|
||||
onClick={this.onUploadClick}
|
||||
title={_t('Upload file')}
|
||||
>
|
||||
<input ref={this._uploadInput} type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onChange={this.onUploadFileInputChange}
|
||||
/>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SlateMessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
||||
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this.onEvent = this.onEvent.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
this._onTombstoneClick = this._onTombstoneClick.bind(this);
|
||||
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
|
||||
this.renderFormatBar = this.renderFormatBar.bind(this);
|
||||
|
||||
this.state = {
|
||||
inputState: {
|
||||
marks: [],
|
||||
blockType: null,
|
||||
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||
},
|
||||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// N.B. using 'event' rather than 'RoomEvents' otherwise the crypto handler
|
||||
// for 'event' fires *after* 'RoomEvent', and our room won't have yet been
|
||||
// marked as encrypted.
|
||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||
MatrixClientPeg.get().on("event", this.onEvent);
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
}
|
||||
|
||||
_waitForOwnMember() {
|
||||
// if we have the member already, do that
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
if (me) {
|
||||
this.setState({me});
|
||||
return;
|
||||
}
|
||||
// Otherwise, wait for member loading to finish and then update the member for the avatar.
|
||||
// The members should already be loading, and loadMembersIfNeeded
|
||||
// will return the promise for the existing operation
|
||||
this.props.room.loadMembersIfNeeded().then(() => {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
this.setState({me});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("event", this.onEvent);
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
}
|
||||
|
||||
onEvent(event) {
|
||||
if (event.getType() !== 'm.room.encryption') return;
|
||||
if (event.getRoomId() !== this.props.room.roomId) return;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
_onRoomStateEvents(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||
|
||||
if (ev.getType() === 'm.room.tombstone') {
|
||||
this.setState({tombstone: this._getRoomTombstone()});
|
||||
}
|
||||
if (ev.getType() === 'm.room.power_levels') {
|
||||
this.setState({canSendMessages: this.props.room.maySendMessage()});
|
||||
}
|
||||
}
|
||||
|
||||
_getRoomTombstone() {
|
||||
return this.props.room.currentState.getStateEvents('m.room.tombstone', '');
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate() {
|
||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
if (this.state.isQuoting === isQuoting) return;
|
||||
this.setState({ isQuoting });
|
||||
}
|
||||
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
// Merge the new input state with old to support partial updates
|
||||
inputState = Object.assign({}, this.state.inputState, inputState);
|
||||
this.setState({inputState});
|
||||
}
|
||||
|
||||
_onAutocompleteConfirm(range, completion) {
|
||||
if (this.messageComposerInput) {
|
||||
this.messageComposerInput.setDisplayedCompletion(range, completion);
|
||||
}
|
||||
}
|
||||
|
||||
onFormatButtonClicked(name, event) {
|
||||
event.preventDefault();
|
||||
this.messageComposerInput.onFormatButtonClicked(name, event);
|
||||
}
|
||||
|
||||
onToggleFormattingClicked() {
|
||||
SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
|
||||
this.setState({showFormatting: !this.state.showFormatting});
|
||||
}
|
||||
|
||||
onToggleMarkdownClicked(e) {
|
||||
e.preventDefault(); // don't steal focus from the editor!
|
||||
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled);
|
||||
}
|
||||
|
||||
_onTombstoneClick(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId);
|
||||
let createEventId = null;
|
||||
if (replacementRoom) {
|
||||
const createEvent = replacementRoom.currentState.getStateEvents('m.room.create', '');
|
||||
if (createEvent && createEvent.getId()) createEventId = createEvent.getId();
|
||||
}
|
||||
|
||||
const viaServers = [this.state.tombstone.getSender().split(':').splice(1).join(':')];
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
highlighted: true,
|
||||
event_id: createEventId,
|
||||
room_id: replacementRoomId,
|
||||
auto_join: true,
|
||||
|
||||
// Try to join via the server that sent the event. This converts @something:example.org
|
||||
// into a server domain by splitting on colons and ignoring the first entry ("@something").
|
||||
via_servers: viaServers,
|
||||
opts: {
|
||||
// These are passed down to the js-sdk's /join call
|
||||
viaServers: viaServers,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
renderPlaceholderText() {
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
if (this.state.isQuoting) {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
return _t('Send a reply (unencrypted)…');
|
||||
}
|
||||
} else {
|
||||
if (roomIsEncrypted) {
|
||||
return _t('Send an encrypted message…');
|
||||
} else {
|
||||
return _t('Send a message (unencrypted)…');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderFormatBar() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const {marks, blockType} = this.state.inputState;
|
||||
const formatButtons = formatButtonList.map((name) => {
|
||||
// special-case to match the md serializer and the special-case in MessageComposerInput.js
|
||||
const markName = name === 'inline-code' ? 'code' : name;
|
||||
const active = marks.some(mark => mark.type === markName) || blockType === name;
|
||||
const suffix = active ? '-on' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||
return (
|
||||
<img className={className}
|
||||
title={_t(name)}
|
||||
onMouseDown={onFormatButtonClicked}
|
||||
key={name}
|
||||
src={require(`../../../../res/img/button-text-${name}${suffix}.svg`)}
|
||||
height="17"
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar">
|
||||
{ formatButtons }
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<AccessibleButton
|
||||
className="mx_MessageComposer_formatbar_markdown mx_MessageComposer_markdownDisabled"
|
||||
onClick={this.onToggleMarkdownClicked}
|
||||
title={_t("Markdown is disabled")}
|
||||
/>
|
||||
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src={require("../../../../res/img/icon-text-cancel.svg")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
this.props.e2eStatus ? <E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> : null,
|
||||
];
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
|
||||
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
|
||||
const showFormattingButton = this.state.inputState.isRichTextEnabled;
|
||||
const callInProgress = this.props.callState && this.props.callState !== 'ended';
|
||||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={(c) => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
onInputStateChanged={this.onInputStateChanged}
|
||||
permalinkCreator={this.props.permalinkCreator} />,
|
||||
showFormattingButton ? <FormattingButton key="controls_formatting"
|
||||
showFormatting={this.state.showFormatting} onClickHandler={this.onToggleFormattingClicked} /> : null,
|
||||
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? <HangupButton key="controls_hangup" roomId={this.props.room.roomId} /> : null,
|
||||
callInProgress ? null : <CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
callInProgress ? null : <VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
||||
const continuesLink = replacementRoomId ? (
|
||||
<a href={makeRoomPermalink(replacementRoomId)}
|
||||
className="mx_MessageComposer_roomReplaced_link"
|
||||
onClick={this._onTombstoneClick}
|
||||
>
|
||||
{_t("The conversation continues here.")}
|
||||
</a>
|
||||
) : '';
|
||||
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
{_t("This room has been replaced and is no longer active.")}
|
||||
</span><br />
|
||||
{ continuesLink }
|
||||
</div>
|
||||
</div>);
|
||||
} else {
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
const showFormatBar = this.state.showFormatting && this.state.inputState.isRichTextEnabled;
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer">
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
</div>
|
||||
</div>
|
||||
{ showFormatBar ? this.renderFormatBar() : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SlateMessageComposer.propTypes = {
|
||||
// js-sdk Room object
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// string representing the current voip call state
|
||||
callState: PropTypes.string,
|
||||
|
||||
// string representing the current room app drawer state
|
||||
showApps: PropTypes.bool
|
||||
};
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
@ -26,8 +26,7 @@ import PersistedElement from "../elements/PersistedElement";
|
|||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {ContextMenu} from "../../structures/ContextMenu";
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
import {WidgetType} from "../../../widgets/WidgetType";
|
||||
|
||||
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
|
||||
// We sit in a context menu, so this should be given to the context menu.
|
||||
|
@ -85,11 +84,11 @@ export default class Stickerpicker extends React.Component {
|
|||
|
||||
async _removeStickerpickerWidgets() {
|
||||
const scalarClient = await this._acquireScalarClient();
|
||||
console.warn('Removing Stickerpicker widgets');
|
||||
console.log('Removing Stickerpicker widgets');
|
||||
if (this.state.widgetId) {
|
||||
if (scalarClient) {
|
||||
scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => {
|
||||
console.warn('Assets disabled');
|
||||
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
|
||||
console.log('Assets disabled');
|
||||
}).catch((err) => {
|
||||
console.error('Failed to disable assets');
|
||||
});
|
||||
|
@ -241,6 +240,15 @@ export default class Stickerpicker extends React.Component {
|
|||
// Set default name
|
||||
stickerpickerWidget.content.name = stickerpickerWidget.name || _t("Stickerpack");
|
||||
|
||||
// FIXME: could this use the same code as other apps?
|
||||
const stickerApp = {
|
||||
id: stickerpickerWidget.id,
|
||||
url: stickerpickerWidget.content.url,
|
||||
name: stickerpickerWidget.content.name,
|
||||
type: stickerpickerWidget.content.type,
|
||||
data: stickerpickerWidget.content.data,
|
||||
};
|
||||
|
||||
stickersContent = (
|
||||
<div className='mx_Stickers_content_container'>
|
||||
<div
|
||||
|
@ -254,11 +262,8 @@ export default class Stickerpicker extends React.Component {
|
|||
>
|
||||
<PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<AppTile
|
||||
id={stickerpickerWidget.id}
|
||||
url={stickerpickerWidget.content.url}
|
||||
name={stickerpickerWidget.content.name}
|
||||
app={stickerApp}
|
||||
room={this.props.room}
|
||||
type={stickerpickerWidget.content.type}
|
||||
fullWidth={true}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||
|
@ -316,8 +321,8 @@ export default class Stickerpicker extends React.Component {
|
|||
|
||||
// Offset the chevron location, which is relative to the left of the context menu
|
||||
// (10 = offset when context menu would not be displayed off viewport)
|
||||
// (8 = value required in practice (possibly 10 - 2 where the 2 = context menu borders)
|
||||
const stickerPickerChevronOffset = Math.max(10, 8 + window.pageXOffset + buttonRect.left - x);
|
||||
// (2 = context menu borders)
|
||||
const stickerPickerChevronOffset = Math.max(10, 2 + window.pageXOffset + buttonRect.left - x);
|
||||
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
|
||||
|
@ -359,13 +364,13 @@ export default class Stickerpicker extends React.Component {
|
|||
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
this.props.room,
|
||||
`type_${widgetType}`,
|
||||
`type_${WidgetType.STICKERPICKER.preferred}`,
|
||||
this.state.widgetId,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
this.props.room,
|
||||
`type_${widgetType}`,
|
||||
`type_${WidgetType.STICKERPICKER.preferred}`,
|
||||
this.state.widgetId,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher";
|
||||
import sdk from "../../../index";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
|
||||
|
@ -51,7 +51,7 @@ export default class ThirdPartyMemberInfo extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
componentDidMount(): void {
|
||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
|
|
|
@ -22,11 +22,12 @@ import createReactClass from 'create-react-class';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'TopUnreadMessagesBar',
|
||||
|
||||
propTypes: {
|
||||
onScrollUpClick: PropTypes.func,
|
||||
onCloseClick: PropTypes.func,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -36,6 +37,10 @@ module.exports = createReactClass({
|
|||
title={_t('Jump to first unread message.')}
|
||||
onClick={this.props.onScrollUpClick}>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_TopUnreadMessagesBar_markAsRead"
|
||||
title={_t('Mark all as read')}
|
||||
onClick={this.props.onCloseClick}>
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
48
src/components/views/rooms/UserOnlineDot.js
Normal file
48
src/components/views/rooms/UserOnlineDot.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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, {useContext, useEffect, useMemo, useState, useCallback} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
const UserOnlineDot = ({userId}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const user = useMemo(() => cli.getUser(userId), [cli, userId]);
|
||||
|
||||
const [isOnline, setIsOnline] = useState(false);
|
||||
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsOnline(user && (user.currentlyActive || user.presence === "online"));
|
||||
}, [cli, user]);
|
||||
// Recheck also if we receive a User.currentlyActive event
|
||||
const currentlyActiveHandler = useCallback((ev) => {
|
||||
const content = ev.getContent();
|
||||
setIsOnline(content.currently_active || content.presence === "online");
|
||||
}, []);
|
||||
useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler);
|
||||
useEventEmitter(user, "User.presence", currentlyActiveHandler);
|
||||
|
||||
return isOnline ? <span className="mx_UserOnlineDot" /> : null;
|
||||
};
|
||||
|
||||
UserOnlineDot.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default UserOnlineDot;
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
|
||||
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 createReactClass from 'create-react-class';
|
||||
|
||||
const Avatar = require("../../../Avatar");
|
||||
const sdk = require('../../../index');
|
||||
|
||||
module.exports = createReactClass({
|
||||
displayName: 'UserTile',
|
||||
|
||||
propTypes: {
|
||||
user: PropTypes.any.isRequired, // User
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const user = this.props.user;
|
||||
const name = user.displayName || user.userId;
|
||||
let active = -1;
|
||||
|
||||
// FIXME: make presence data update whenever User.presence changes...
|
||||
active = user.lastActiveAgo ?
|
||||
(Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) : -1;
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const avatarJsx = (
|
||||
<BaseAvatar width={36} height={36} name={name} idName={user.userId}
|
||||
url={Avatar.avatarUrlForUser(user, 36, 36, "crop")} />
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityTile {...this.props} presenceState={user.presence} presenceActiveAgo={active}
|
||||
presenceCurrentlyActive={user.currentlyActive}
|
||||
name={name} title={user.userId} avatarJsx={avatarJsx} />
|
||||
);
|
||||
},
|
||||
});
|
|
@ -18,12 +18,12 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import WhoIsTyping from '../../../WhoIsTyping';
|
||||
import * as WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
|
||||
module.exports = createReactClass({
|
||||
export default createReactClass({
|
||||
displayName: 'WhoIsTypingTile',
|
||||
|
||||
propTypes: {
|
||||
|
@ -54,7 +54,7 @@ module.exports = createReactClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
componentDidMount: function() {
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
},
|
||||
|
@ -213,7 +213,7 @@ module.exports = createReactClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<li className="mx_WhoIsTypingTile">
|
||||
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue