Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/authentication_password_field
This commit is contained in:
commit
3563b83762
79 changed files with 1697 additions and 732 deletions
|
@ -108,6 +108,8 @@ export default class ModularServerConfig extends React.PureComponent {
|
|||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,7 +134,8 @@ export default class ModularServerConfig extends React.PureComponent {
|
|||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
await this.validateServer();
|
||||
const result = await this.validateServer();
|
||||
if (!result) return; // Do not continue.
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
|
|
|
@ -53,11 +53,13 @@ module.exports = React.createClass({
|
|||
onEditServerDetailsClick: PropTypes.func,
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
||||
canSubmit: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onValidationChange: console.error,
|
||||
canSubmit: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -80,6 +82,8 @@ module.exports = React.createClass({
|
|||
onSubmit: async function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!this.props.canSubmit) return;
|
||||
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
return;
|
||||
|
@ -380,7 +384,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
validateUsernameRules: withValidation({
|
||||
description: () => _t("Use letters, numbers, dashes and underscores only"),
|
||||
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
|
@ -540,7 +544,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} disabled={!this.props.canSubmit} />
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -101,14 +101,25 @@ export default class ServerConfig extends React.PureComponent {
|
|||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
|
||||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (!stateForError.isFatalError) {
|
||||
// carry on anyway
|
||||
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
|
||||
this.props.onServerConfigChange(result);
|
||||
return result;
|
||||
} else {
|
||||
let message = _t("Unable to validate homeserver/identity server");
|
||||
if (e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,7 +148,8 @@ export default class ServerConfig extends React.PureComponent {
|
|||
onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
await this.validateServer();
|
||||
const result = await this.validateServer();
|
||||
if (!result) return; // Do not continue.
|
||||
|
||||
if (this.props.onAfterSubmit) {
|
||||
this.props.onAfterSubmit();
|
||||
|
|
|
@ -205,7 +205,7 @@ module.exports = React.createClass({
|
|||
|
||||
onSelected: function(index) {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.state.suggestedList[index]);
|
||||
selectedList.push(this._getFilteredSuggestions()[index]);
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
|
@ -526,12 +526,7 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
_getFilteredSuggestions: function() {
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({address, addressType}) => {
|
||||
|
@ -540,9 +535,16 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
|
@ -574,6 +576,8 @@ module.exports = React.createClass({
|
|||
</textarea>,
|
||||
);
|
||||
|
||||
const filteredSuggestedList = this._getFilteredSuggestions();
|
||||
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
|
|
|
@ -28,13 +28,14 @@ import {parseEvent} from '../../../editor/deserialize';
|
|||
import Autocomplete from '../rooms/Autocomplete';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import {renderModel} from '../../../editor/render';
|
||||
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class MessageEditor extends React.Component {
|
||||
static propTypes = {
|
||||
// the message event being edited
|
||||
event: PropTypes.instanceOf(MatrixEvent).isRequired,
|
||||
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -44,16 +45,7 @@ export default class MessageEditor extends React.Component {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(
|
||||
() => this._autocompleteRef,
|
||||
query => this.setState({query}),
|
||||
room,
|
||||
);
|
||||
this.model = new EditorModel(
|
||||
parseEvent(this.props.event, room),
|
||||
partCreator,
|
||||
this._updateEditorState,
|
||||
);
|
||||
this.model = null;
|
||||
this.state = {
|
||||
autoComplete: null,
|
||||
room,
|
||||
|
@ -64,7 +56,7 @@ export default class MessageEditor extends React.Component {
|
|||
}
|
||||
|
||||
_getRoom() {
|
||||
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
|
||||
return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
|
||||
}
|
||||
|
||||
_updateEditorState = (caret) => {
|
||||
|
@ -133,7 +125,7 @@ export default class MessageEditor extends React.Component {
|
|||
if (this._hasModifications || !this._isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId());
|
||||
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
|
||||
if (previousEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: previousEvent});
|
||||
event.preventDefault();
|
||||
|
@ -142,7 +134,7 @@ export default class MessageEditor extends React.Component {
|
|||
if (this._hasModifications || !this._isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId());
|
||||
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
|
||||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
|
@ -158,16 +150,28 @@ export default class MessageEditor extends React.Component {
|
|||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
_isEmote() {
|
||||
const firstPart = this.model.parts[0];
|
||||
return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me ");
|
||||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
const isEmote = this._isEmote();
|
||||
let model = this.model;
|
||||
if (isEmote) {
|
||||
// trim "/me "
|
||||
model = model.clone();
|
||||
model.removeText({index: 0, offset: 0}, 4);
|
||||
}
|
||||
const newContent = {
|
||||
"msgtype": "m.text",
|
||||
"body": textSerialize(this.model),
|
||||
"msgtype": isEmote ? "m.emote" : "m.text",
|
||||
"body": textSerialize(model),
|
||||
};
|
||||
const contentBody = {
|
||||
msgtype: newContent.msgtype,
|
||||
body: ` * ${newContent.body}`,
|
||||
};
|
||||
const formattedBody = htmlSerializeIfNeeded(this.model);
|
||||
const formattedBody = htmlSerializeIfNeeded(model);
|
||||
if (formattedBody) {
|
||||
newContent.format = "org.matrix.custom.html";
|
||||
newContent.formatted_body = formattedBody;
|
||||
|
@ -178,11 +182,11 @@ export default class MessageEditor extends React.Component {
|
|||
"m.new_content": newContent,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": this.props.event.getId(),
|
||||
"event_id": this.props.editState.getEvent().getId(),
|
||||
},
|
||||
}, contentBody);
|
||||
|
||||
const roomId = this.props.event.getRoomId();
|
||||
const roomId = this.props.editState.getEvent().getRoomId();
|
||||
this.context.matrixClient.sendMessage(roomId, content);
|
||||
|
||||
dis.dispatch({action: "edit_event", event: null});
|
||||
|
@ -197,12 +201,63 @@ export default class MessageEditor extends React.Component {
|
|||
this.model.autoComplete.onComponentSelectionChange(completion);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const sel = document.getSelection();
|
||||
const {caret} = getCaretOffsetAndText(this._editorRef, sel);
|
||||
const parts = this.model.serializeParts();
|
||||
this.props.editState.setEditorState(caret, parts);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.model = this._createEditorModel();
|
||||
// initial render of model
|
||||
this._updateEditorState();
|
||||
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
|
||||
// initial caret position
|
||||
this._initializeCaret();
|
||||
this._editorRef.focus();
|
||||
}
|
||||
|
||||
_createEditorModel() {
|
||||
const {editState} = this.props;
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(
|
||||
() => this._autocompleteRef,
|
||||
query => this.setState({query}),
|
||||
room,
|
||||
this.context.matrixClient,
|
||||
);
|
||||
let parts;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore serialized parts from the state
|
||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, parse the body of the event
|
||||
parts = parseEvent(editState.getEvent(), room, this.context.matrixClient);
|
||||
}
|
||||
|
||||
return new EditorModel(
|
||||
parts,
|
||||
partCreator,
|
||||
this._updateEditorState,
|
||||
);
|
||||
}
|
||||
|
||||
_initializeCaret() {
|
||||
const {editState} = this.props;
|
||||
let caretPosition;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore caret position from the state
|
||||
const caret = editState.getCaret();
|
||||
caretPosition = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
} else {
|
||||
// otherwise, set it at the end
|
||||
caretPosition = this.model.getPositionAtEnd();
|
||||
}
|
||||
setCaretPosition(this._editorRef, this.model, caretPosition);
|
||||
}
|
||||
|
||||
render() {
|
||||
let autoComplete;
|
||||
if (this.state.autoComplete) {
|
||||
|
|
|
@ -36,6 +36,20 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
onFocusChange: PropTypes.func,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
||||
}
|
||||
|
||||
onDecrypted = () => {
|
||||
// When an event decrypts, it is likely to change the set of available
|
||||
// actions, so we force an update to check again.
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
|
@ -69,10 +83,6 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
|
||||
const { getTile, getReplyThread } = this.props;
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
@ -82,11 +92,9 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
e2eInfoCallback = () => this.onCryptoClicked();
|
||||
}
|
||||
|
||||
createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
const menuOptions = {
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
chevronFace: "none",
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
|
@ -94,7 +102,23 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
onFinished: () => {
|
||||
this.onFocusChange(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const buttonRight = buttonRect.right + window.pageXOffset;
|
||||
const buttonBottom = buttonRect.bottom + window.pageYOffset;
|
||||
const buttonTop = buttonRect.top + window.pageYOffset;
|
||||
// Align the right edge of the menu to the right edge of the button
|
||||
menuOptions.right = window.innerWidth - buttonRight;
|
||||
// Align the menu vertically on whichever side of the button has more
|
||||
// space available.
|
||||
if (buttonBottom < window.innerHeight / 2) {
|
||||
menuOptions.top = buttonBottom;
|
||||
} else {
|
||||
menuOptions.bottom = window.innerHeight - buttonTop;
|
||||
}
|
||||
|
||||
createMenu(MessageContextMenu, menuOptions);
|
||||
|
||||
this.onFocusChange(true);
|
||||
}
|
||||
|
|
|
@ -90,7 +90,7 @@ module.exports = React.createClass({
|
|||
tileShape={this.props.tileShape}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
isEditing={this.props.isEditing}
|
||||
editState={this.props.editState}
|
||||
onHeightChanged={this.props.onHeightChanged} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import sdk from '../../../index';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
import { isSingleEmoji } from '../../../HtmlUtils';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
|
@ -103,6 +104,9 @@ export default class ReactionsRow extends React.PureComponent {
|
|||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
const items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
|
||||
if (!isSingleEmoji(content)) {
|
||||
return null;
|
||||
}
|
||||
const count = events.size;
|
||||
if (!count) {
|
||||
return null;
|
||||
|
|
|
@ -90,7 +90,7 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
if (!this.props.isEditing) {
|
||||
if (!this.props.editState) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
},
|
||||
|
@ -131,8 +131,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidUpdate: function(prevProps) {
|
||||
if (!this.props.isEditing) {
|
||||
const stoppedEditing = prevProps.isEditing && !this.props.isEditing;
|
||||
if (!this.props.editState) {
|
||||
const stoppedEditing = prevProps.editState && !this.props.editState;
|
||||
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
|
||||
if (messageWasEdited || stoppedEditing) {
|
||||
this._applyFormatting();
|
||||
|
@ -153,7 +153,7 @@ module.exports = React.createClass({
|
|||
nextProps.replacingEventId !== this.props.replacingEventId ||
|
||||
nextProps.highlightLink !== this.props.highlightLink ||
|
||||
nextProps.showUrlPreview !== this.props.showUrlPreview ||
|
||||
nextProps.isEditing !== this.props.isEditing ||
|
||||
nextProps.editState !== this.props.editState ||
|
||||
nextState.links !== this.state.links ||
|
||||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
|
||||
nextState.widgetHidden !== this.state.widgetHidden);
|
||||
|
@ -469,9 +469,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
if (this.props.isEditing) {
|
||||
if (this.props.editState) {
|
||||
const MessageEditor = sdk.getComponent('elements.MessageEditor');
|
||||
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />;
|
||||
return <MessageEditor editState={this.props.editState} className="mx_EventTile_content" />;
|
||||
}
|
||||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
|
|
@ -171,26 +171,13 @@ export default class Autocomplete extends React.Component {
|
|||
}
|
||||
|
||||
// called from MessageComposerInput
|
||||
onUpArrow(): ?Completion {
|
||||
moveSelection(delta): ?Completion {
|
||||
const completionCount = this.countCompletions();
|
||||
// completionCount + 1, since 0 means composer is selected
|
||||
const selectionOffset = (completionCount + 1 + this.state.selectionOffset - 1)
|
||||
% (completionCount + 1);
|
||||
if (!completionCount) {
|
||||
return null;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
}
|
||||
if (completionCount === 0) return; // there are no items to move the selection through
|
||||
|
||||
// called from MessageComposerInput
|
||||
onDownArrow(): ?Completion {
|
||||
const completionCount = this.countCompletions();
|
||||
// completionCount + 1, since 0 means composer is selected
|
||||
const selectionOffset = (this.state.selectionOffset + 1) % (completionCount + 1);
|
||||
if (!completionCount) {
|
||||
return null;
|
||||
}
|
||||
this.setSelection(selectionOffset);
|
||||
// Note: selectionOffset 0 represents the unsubstituted text, while 1 means first pill selected
|
||||
const index = (this.state.selectionOffset + delta + completionCount + 1) % (completionCount + 1);
|
||||
this.setSelection(index);
|
||||
}
|
||||
|
||||
onEscape(e): boolean {
|
||||
|
|
|
@ -552,13 +552,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
|
||||
const isEditing = !!this.props.editState;
|
||||
const classes = classNames({
|
||||
mx_EventTile: true,
|
||||
mx_EventTile_isEditing: this.props.isEditing,
|
||||
mx_EventTile_isEditing: isEditing,
|
||||
mx_EventTile_info: isInfoMessage,
|
||||
mx_EventTile_12hr: this.props.isTwelveHour,
|
||||
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
|
||||
mx_EventTile_sending: isSending,
|
||||
mx_EventTile_sending: !isEditing && isSending,
|
||||
mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent',
|
||||
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
|
||||
mx_EventTile_selected: this.props.isSelectedEvent,
|
||||
|
@ -632,7 +633,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||
const actionBar = !this.props.isEditing ? <MessageActionBar
|
||||
const actionBar = !isEditing ? <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
reactions={this.state.reactions}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
|
@ -794,7 +795,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
isEditing={this.props.isEditing}
|
||||
editState={this.props.editState}
|
||||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
|
|
|
@ -533,21 +533,24 @@ export default class MessageComposerInput extends React.Component {
|
|||
// The first matched group includes just the matched plaintext emoji
|
||||
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset));
|
||||
if (emoticonMatch) {
|
||||
const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]);
|
||||
const unicodeEmoji = data ? data.unicode : '';
|
||||
const query = emoticonMatch[1].toLowerCase().replace("-", "");
|
||||
const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false);
|
||||
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - 1,
|
||||
},
|
||||
});
|
||||
change = change.insertTextAtRange(range, unicodeEmoji);
|
||||
editorState = change.value;
|
||||
// only perform replacement if we found a match, otherwise we would be not letting user type
|
||||
if (data) {
|
||||
const range = Range.create({
|
||||
anchor: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - emoticonMatch[1].length - 1,
|
||||
},
|
||||
focus: {
|
||||
key: editorState.startText.key,
|
||||
offset: currentStartOffset - 1,
|
||||
},
|
||||
});
|
||||
change = change.insertTextAtRange(range, data.unicode);
|
||||
editorState = change.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -670,6 +673,31 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => {
|
||||
this.suppressAutoComplete = false;
|
||||
this.direction = '';
|
||||
|
||||
// Navigate autocomplete list with arrow keys
|
||||
if (this.autocomplete.countCompletions() > 0) {
|
||||
if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.LEFT:
|
||||
this.autocomplete.moveSelection(-1);
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
case KeyCode.RIGHT:
|
||||
this.autocomplete.moveSelection(+1);
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
case KeyCode.UP:
|
||||
this.autocomplete.moveSelection(-1);
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
case KeyCode.DOWN:
|
||||
this.autocomplete.moveSelection(+1);
|
||||
ev.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// skip void nodes - see
|
||||
// https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
|
||||
|
@ -677,8 +705,6 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.direction = 'Previous';
|
||||
} else if (ev.keyCode === KeyCode.RIGHT) {
|
||||
this.direction = 'Next';
|
||||
} else {
|
||||
this.direction = '';
|
||||
}
|
||||
|
||||
switch (ev.keyCode) {
|
||||
|
@ -1172,35 +1198,28 @@ export default class MessageComposerInput extends React.Component {
|
|||
};
|
||||
|
||||
onVerticalArrow = (e, up) => {
|
||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
|
||||
|
||||
// Select history only if we are not currently auto-completing
|
||||
if (this.autocomplete.state.completionList.length === 0) {
|
||||
const selection = this.state.editorState.selection;
|
||||
// Select history
|
||||
const selection = this.state.editorState.selection;
|
||||
|
||||
// selection must be collapsed
|
||||
if (!selection.isCollapsed) return;
|
||||
const document = this.state.editorState.document;
|
||||
// selection must be collapsed
|
||||
if (!selection.isCollapsed) return;
|
||||
const document = this.state.editorState.document;
|
||||
|
||||
// and we must be at the edge of the document (up=start, down=end)
|
||||
if (up) {
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
// and we must be at the edge of the document (up=start, down=end)
|
||||
if (up) {
|
||||
if (!selection.anchor.isAtStartOfNode(document)) return;
|
||||
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.moveAutocompleteSelection(up);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1209,23 +1228,19 @@ export default class MessageComposerInput extends React.Component {
|
|||
someCompletions: null,
|
||||
});
|
||||
e.preventDefault();
|
||||
if (this.autocomplete.state.completionList.length === 0) {
|
||||
if (this.autocomplete.countCompletions() === 0) {
|
||||
// Force completions to show for the text currently entered
|
||||
const completionCount = await this.autocomplete.forceComplete();
|
||||
this.setState({
|
||||
someCompletions: completionCount > 0,
|
||||
});
|
||||
// Select the first item by moving "down"
|
||||
await this.moveAutocompleteSelection(false);
|
||||
await this.autocomplete.moveSelection(+1);
|
||||
} else {
|
||||
await this.moveAutocompleteSelection(e.shiftKey);
|
||||
await this.autocomplete.moveSelection(e.shiftKey ? -1 : +1);
|
||||
}
|
||||
};
|
||||
|
||||
moveAutocompleteSelection = (up) => {
|
||||
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||
};
|
||||
|
||||
onEscape = async (e) => {
|
||||
e.preventDefault();
|
||||
if (this.autocomplete) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations 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.
|
||||
|
@ -54,6 +55,12 @@ module.exports = React.createClass({
|
|||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail: PropTypes.string,
|
||||
|
||||
// For third party invites, information passed about the room out-of-band
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// For third party invites, a URL for a 3pid invite signing service
|
||||
signUrl: PropTypes.string,
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error: PropTypes.object,
|
||||
|
@ -87,6 +94,16 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._checkInvitedEmail();
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
||||
this._checkInvitedEmail();
|
||||
}
|
||||
},
|
||||
|
||||
_checkInvitedEmail: function() {
|
||||
// If this is an invite and we've been told what email
|
||||
// address was invited, fetch the user's list of Threepids
|
||||
// so we can check them against the one that was invited
|
||||
|
@ -215,12 +232,25 @@ module.exports = React.createClass({
|
|||
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||
},
|
||||
|
||||
_makeScreenAfterLogin() {
|
||||
return {
|
||||
screen: 'room',
|
||||
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,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
|
||||
},
|
||||
|
||||
onRegisterClick: function() {
|
||||
dis.dispatch({ action: 'start_registration' });
|
||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -335,7 +365,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
case MessageCase.Invite: {
|
||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||
const avatar = <RoomAvatar room={this.props.room} />;
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
|
||||
|
||||
const inviteMember = this._getInviteMember();
|
||||
let inviterElement;
|
||||
|
|
|
@ -135,11 +135,27 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
<ul>
|
||||
<li>
|
||||
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
|
||||
default cover photo</a> is (C)
|
||||
default cover photo</a> is ©
|
||||
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
|
||||
used under the terms of
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
|
||||
CC-BY-SA 4.0</a>. No warranties are given.
|
||||
CC-BY-SA 4.0</a>.
|
||||
</li>
|
||||
<li>
|
||||
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noopener" target="_blank">
|
||||
twemoji-colr</a> font is ©
|
||||
<a href="https://mozilla.org" rel="noopener" target="_blank">Mozilla Foundation</a>{' '}
|
||||
used under the terms of
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener" target="_blank">
|
||||
Apache 2.0</a>.
|
||||
</li>
|
||||
<li>
|
||||
The <a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">
|
||||
Twemoji</a> emoji art is ©
|
||||
<a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">Twitter, Inc and other
|
||||
contributors</a> used under the terms of
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener" target="_blank">
|
||||
CC-BY 4.0</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue