Merge pull request #1215 from matrix-org/luke/remove-old-composer

Remove MessageComposerInputOld
This commit is contained in:
Luke Barnard 2017-07-13 11:33:23 +01:00 committed by GitHub
commit 4d844ebc34
20 changed files with 17 additions and 1092 deletions

View file

@ -1,391 +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 { Entry, MemberEntry, CommandEntry } from './TabCompleteEntries';
import SlashCommands from './SlashCommands';
import MatrixClientPeg from './MatrixClientPeg';
const DELAY_TIME_MS = 1000;
const KEY_TAB = 9;
const KEY_SHIFT = 16;
const KEY_WINDOWS = 91;
// NB: DO NOT USE \b its "words" are roman alphabet only!
//
// Capturing group containing the start
// of line or a whitespace char
// \_______________ __________Capturing group of 0 or more non-whitespace chars
// _|__ _|_ followed by the end of line
// / \/ \
const MATCH_REGEX = /(^|\s)(\S*)$/;
class TabComplete {
constructor(opts) {
opts.allowLooping = opts.allowLooping || false;
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
opts.onClickCompletes = opts.onClickCompletes || false;
this.opts = opts;
this.completing = false;
this.list = []; // full set of tab-completable things
this.matchedList = []; // subset of completable things to loop over
this.currentIndex = 0; // index in matchedList currently
this.originalText = null; // original input text when tab was first hit
this.textArea = opts.textArea; // DOMElement
this.isFirstWord = false; // true if you tab-complete on the first word
this.enterTabCompleteTimerId = null;
this.inPassiveMode = false;
// Map tracking ordering of the room members.
// userId: integer, highest comes first.
this.memberTabOrder = {};
// monotonically increasing counter used for tracking ordering of members
this.memberOrderSeq = 0;
}
/**
* Call this when a a UI element representing a tab complete entry has been clicked
* @param {entry} The entry that was clicked
*/
onEntryClick(entry) {
if (this.opts.onClickCompletes) {
this.completeTo(entry);
}
}
loadEntries(room) {
this._makeEntries(room);
this._initSorting(room);
this._sortEntries();
}
onMemberSpoke(member) {
if (this.memberTabOrder[member.userId] === undefined) {
this.list.push(new MemberEntry(member));
}
this.memberTabOrder[member.userId] = this.memberOrderSeq++;
this._sortEntries();
}
/**
* @param {DOMElement}
*/
setTextArea(textArea) {
this.textArea = textArea;
}
/**
* @return {Boolean}
*/
isTabCompleting() {
// actually have things to tab over
return this.completing && this.matchedList.length > 1;
}
stopTabCompleting() {
this.completing = false;
this.currentIndex = 0;
this._notifyStateChange();
}
startTabCompleting(passive) {
this.originalText = this.textArea.value; // cache starting text
// grab the partial word from the text which we'll be tab-completing
var res = MATCH_REGEX.exec(this.originalText);
if (!res) {
this.matchedList = [];
return;
}
// ES6 destructuring; ignore first element (the complete match)
var [, boundaryGroup, partialGroup] = res;
if (partialGroup.length === 0 && passive) {
return;
}
this.isFirstWord = partialGroup.length === this.originalText.length;
this.completing = true;
this.currentIndex = 0;
this.matchedList = [
new Entry(partialGroup) // first entry is always the original partial
];
// find matching entries in the set of entries given to us
this.list.forEach((entry) => {
if (entry.text.toLowerCase().indexOf(partialGroup.toLowerCase()) === 0) {
this.matchedList.push(entry);
}
});
// console.log("calculated completions => %s", JSON.stringify(this.matchedList));
}
/**
* Do an auto-complete with the given word. This terminates the tab-complete.
* @param {Entry} entry The tab-complete entry to complete to.
*/
completeTo(entry) {
this.textArea.value = this._replaceWith(
entry.getFillText(), true, entry.getSuffix(this.isFirstWord)
);
this.stopTabCompleting();
// keep focus on the text area
this.textArea.focus();
}
/**
* @param {Number} numAheadToPeek Return *up to* this many elements.
* @return {Entry[]}
*/
peek(numAheadToPeek) {
if (this.matchedList.length === 0) {
return [];
}
var peekList = [];
// return the current match item and then one with an index higher, and
// so on until we've reached the requested limit. If we hit the end of
// the list of options we're done.
for (var i = 0; i < numAheadToPeek; i++) {
var nextIndex;
if (this.opts.allowLooping) {
nextIndex = (this.currentIndex + i) % this.matchedList.length;
}
else {
nextIndex = this.currentIndex + i;
if (nextIndex === this.matchedList.length) {
break;
}
}
peekList.push(this.matchedList[nextIndex]);
}
// console.log("Peek list(%s): %s", numAheadToPeek, JSON.stringify(peekList));
return peekList;
}
handleTabPress(passive, shiftKey) {
var wasInPassiveMode = this.inPassiveMode && !passive;
this.inPassiveMode = passive;
if (!this.completing) {
this.startTabCompleting(passive);
}
if (shiftKey) {
this.nextMatchedEntry(-1);
}
else {
// if we were in passive mode we got out of sync by incrementing the
// index to show the peek view but not set the text area. Therefore,
// we want to set the *current* index rather than the *next* index.
this.nextMatchedEntry(wasInPassiveMode ? 0 : 1);
}
this._notifyStateChange();
}
/**
* @param {DOMEvent} e
*/
onKeyDown(ev) {
if (!this.textArea) {
console.error("onKeyDown called before a <textarea> was set!");
return;
}
if (ev.keyCode !== KEY_TAB) {
// pressing any key (except shift, windows, cmd (OSX) and ctrl/alt combinations)
// aborts the current tab completion
if (this.completing && ev.keyCode !== KEY_SHIFT &&
!ev.metaKey && !ev.ctrlKey && !ev.altKey && ev.keyCode !== KEY_WINDOWS) {
// they're resuming typing; reset tab complete state vars.
this.stopTabCompleting();
}
// explicitly pressing any key except tab removes passive mode. Tab doesn't remove
// passive mode because handleTabPress needs to know when passive mode is toggling
// off so it can resync the textarea/peek list. If tab did remove passive mode then
// handleTabPress would never be able to tell when passive mode toggled off.
this.inPassiveMode = false;
// pressing any key at all (except tab) restarts the automatic tab-complete timer
if (this.opts.autoEnterTabComplete) {
const cachedText = ev.target.value;
clearTimeout(this.enterTabCompleteTimerId);
this.enterTabCompleteTimerId = setTimeout(() => {
if (this.completing) {
// If you highlight text and CTRL+X it, tab-completing will not be reset.
// This check makes sure that if something like a cut operation has been
// done, that we correctly refresh the tab-complete list. Normal backspace
// operations get caught by the stopTabCompleting() section above, but
// because the CTRL key is held, this does not execute for CTRL+X.
if (cachedText !== this.textArea.value) {
this.stopTabCompleting();
}
}
if (!this.completing) {
this.handleTabPress(true, false);
}
}, DELAY_TIME_MS);
}
return;
}
// ctrl-tab/alt-tab etc shouldn't trigger a complete
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
// tab key has been pressed at this point
this.handleTabPress(false, ev.shiftKey);
// prevent the default TAB operation (typically focus shifting)
ev.preventDefault();
}
/**
* Set the textarea to the next value in the matched list.
* @param {Number} offset Offset to apply *before* setting the next value.
*/
nextMatchedEntry(offset) {
if (this.matchedList.length === 0) {
return;
}
// work out the new index, wrapping if necessary.
this.currentIndex += offset;
if (this.currentIndex >= this.matchedList.length) {
this.currentIndex = 0;
}
else if (this.currentIndex < 0) {
this.currentIndex = this.matchedList.length - 1;
}
var isTransitioningToOriginalText = (
// impossible to transition if they've never hit tab
!this.inPassiveMode && this.currentIndex === 0
);
if (!this.inPassiveMode) {
// set textarea to this new value
this.textArea.value = this._replaceWith(
this.matchedList[this.currentIndex].getFillText(),
this.currentIndex !== 0, // don't suffix the original text!
this.matchedList[this.currentIndex].getSuffix(this.isFirstWord)
);
}
// visual display to the user that we looped - TODO: This should be configurable
if (isTransitioningToOriginalText) {
this.textArea.style["background-color"] = "#faa";
setTimeout(() => { // yay for lexical 'this'!
this.textArea.style["background-color"] = "";
}, 150);
if (!this.opts.allowLooping) {
this.stopTabCompleting();
}
}
else {
this.textArea.style["background-color"] = ""; // cancel blinks TODO: required?
}
}
_replaceWith(newVal, includeSuffix, suffix) {
// The regex to replace the input matches a character of whitespace AND
// the partial word. If we just use string.replace() with the regex it will
// replace the partial word AND the character of whitespace. We want to
// preserve whatever that character is (\n, \t, etc) so find out what it is now.
var boundaryChar;
var res = MATCH_REGEX.exec(this.originalText);
if (res) {
boundaryChar = res[1]; // the first captured group
}
if (boundaryChar === undefined) {
console.warn("Failed to find boundary char on text: '%s'", this.originalText);
boundaryChar = "";
}
suffix = suffix || "";
if (!includeSuffix) {
suffix = "";
}
var replacementText = boundaryChar + newVal + suffix;
return this.originalText.replace(MATCH_REGEX, function() {
return replacementText; // function form to avoid `$` special-casing
});
}
_notifyStateChange() {
if (this.opts.onStateChange) {
this.opts.onStateChange(this.completing);
}
}
_sortEntries() {
// largest comes first
const KIND_ORDER = {
command: 1,
member: 2,
};
this.list.sort((a, b) => {
const kindOrderDifference = KIND_ORDER[b.kind] - KIND_ORDER[a.kind];
if (kindOrderDifference != 0) {
return kindOrderDifference;
}
if (a.kind == 'member') {
let orderA = this.memberTabOrder[a.member.userId];
let orderB = this.memberTabOrder[b.member.userId];
if (orderA === undefined) orderA = -1;
if (orderB === undefined) orderB = -1;
return orderB - orderA;
}
// anything else we have no ordering for
return 0;
});
}
_makeEntries(room) {
const myUserId = MatrixClientPeg.get().credentials.userId;
const members = room.getJoinedMembers().filter(function(member) {
if (member.userId !== myUserId) return true;
});
this.list = MemberEntry.fromMemberList(members).concat(
CommandEntry.fromCommands(SlashCommands.getCommandList())
);
}
_initSorting(room) {
this.memberTabOrder = {};
this.memberOrderSeq = 0;
for (const ev of room.getLiveTimeline().getEvents()) {
this.memberTabOrder[ev.getSender()] = this.memberOrderSeq++;
}
}
}
module.exports = TabComplete;

View file

@ -1,125 +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.
*/
var sdk = require("./index");
class Entry {
constructor(text) {
this.text = text;
}
/**
* @return {string} The text to display in this entry.
*/
getText() {
return this.text;
}
/**
* @return {string} The text to insert into the input box. Most of the time
* this is the same as getText().
*/
getFillText() {
return this.text;
}
/**
* @return {ReactClass} Raw JSX
*/
getImageJsx() {
return null;
}
/**
* @return {?string} The unique key= prop for React dedupe
*/
getKey() {
return null;
}
/**
* @return {?string} The suffix to append to the tab-complete, or null to
* not do this.
*/
getSuffix(isFirstWord) {
return null;
}
/**
* Called when this entry is clicked.
*/
onClick() {
// NOP
}
}
class CommandEntry extends Entry {
constructor(cmd, cmdWithArgs) {
super(cmdWithArgs);
this.kind = 'command';
this.cmd = cmd;
}
getFillText() {
return this.cmd;
}
getKey() {
return this.getFillText();
}
getSuffix(isFirstWord) {
return " "; // force a space after the command.
}
}
CommandEntry.fromCommands = function(commandArray) {
return commandArray.map(function(cmd) {
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
});
};
class MemberEntry extends Entry {
constructor(member) {
super((member.name || member.userId).replace(' (IRC)', ''));
this.member = member;
this.kind = 'member';
}
getImageJsx() {
var MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
return (
<MemberAvatar member={this.member} width={24} height={24} />
);
}
getKey() {
return this.member.userId;
}
getSuffix(isFirstWord) {
return isFirstWord ? ": " : " ";
}
}
MemberEntry.fromMemberList = function(members) {
return members.map(function(m) {
return new MemberEntry(m);
});
};
module.exports.Entry = Entry;
module.exports.MemberEntry = MemberEntry;
module.exports.CommandEntry = CommandEntry;

View file

@ -33,9 +33,6 @@ module.exports = React.createClass({
// the room this statusbar is representing.
room: React.PropTypes.object.isRequired,
// a TabComplete object
tabComplete: React.PropTypes.object.isRequired,
// the number of messages which have arrived since we've been scrolled up
numUnreadMessages: React.PropTypes.number,
@ -143,12 +140,9 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline ||
this.props.hasActiveCall ||
this.props.tabComplete.isTabCompleting()
this.props.hasActiveCall
) {
return STATUS_BAR_EXPANDED;
} else if (this.props.tabCompleteEntries) {
return STATUS_BAR_HIDDEN;
} else if (this.props.unsentMessageError) {
return STATUS_BAR_EXPANDED_LARGE;
}
@ -237,8 +231,6 @@ module.exports = React.createClass({
// return suitable content for the main (text) part of the status bar.
_getContent: function() {
var TabCompleteBar = sdk.getComponent('rooms.TabCompleteBar');
var TintableSvg = sdk.getComponent("elements.TintableSvg");
const EmojiText = sdk.getComponent('elements.EmojiText');
// no conn bar trumps unread count since you can't get unread messages
@ -259,20 +251,6 @@ module.exports = React.createClass({
);
}
if (this.props.tabComplete.isTabCompleting()) {
return (
<div className="mx_RoomStatusBar_tabCompleteBar">
<div className="mx_RoomStatusBar_tabCompleteWrapper">
<TabCompleteBar tabComplete={this.props.tabComplete} />
<div className="mx_RoomStatusBar_tabCompleteEol" title="->|">
<TintableSvg src="img/eol.svg" width="22" height="16"/>
{_t('Auto-complete')}
</div>
</div>
</div>
);
}
if (this.props.unsentMessageError) {
return (
<div className="mx_RoomStatusBar_connectionLostBar">

View file

@ -33,7 +33,6 @@ var ContentMessages = require("../../ContentMessages");
var Modal = require("../../Modal");
var sdk = require('../../index');
var CallHandler = require('../../CallHandler');
var TabComplete = require("../../TabComplete");
var Resend = require("../../Resend");
var dis = require("../../dispatcher");
var Tinter = require("../../Tinter");
@ -144,15 +143,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
this.tabComplete = new TabComplete({
allowLooping: false,
autoEnterTabComplete: true,
onClickCompletes: true,
onStateChange: (isCompleting) => {
this.forceUpdate();
},
});
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
@ -518,7 +508,6 @@ module.exports = React.createClass({
// update the tab complete list as it depends on who most recently spoke,
// and that has probably just changed
if (ev.sender) {
this.tabComplete.onMemberSpoke(ev.sender);
UserProvider.getInstance().onUserSpoke(ev.sender);
}
},
@ -542,7 +531,6 @@ module.exports = React.createClass({
this._warnAboutEncryption(room);
this._calculatePeekRules(room);
this._updatePreviewUrlVisibility(room);
this.tabComplete.loadEntries(room);
UserProvider.getInstance().setUserListFromRoom(room);
},
@ -719,7 +707,6 @@ module.exports = React.createClass({
this._updateConfCallNotification();
// refresh the tab complete list
this.tabComplete.loadEntries(this.state.room);
UserProvider.getInstance().setUserListFromRoom(this.state.room);
// if we are now a member of the room, where we were not before, that
@ -1572,7 +1559,6 @@ module.exports = React.createClass({
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar
room={this.state.room}
tabComplete={this.tabComplete}
numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
@ -1648,7 +1634,6 @@ module.exports = React.createClass({
onResize={this.onChildResize}
uploadFile={this.uploadFile}
callState={this.state.callState}
tabComplete={this.tabComplete}
opacity={ this.props.opacity }
showApps={ this.state.showApps }
/>;

View file

@ -408,8 +408,6 @@ export default class MessageComposer extends React.Component {
}
MessageComposer.propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,

View file

@ -41,8 +41,6 @@ import Autocomplete from './Autocomplete';
import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import {onSendMessageFailed} from './MessageComposerInputOld';
import MessageComposerStore from '../../../stores/MessageComposerStore';
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
@ -56,13 +54,27 @@ function stateToMarkdown(state) {
''); // this is *not* a zero width space, trust me :)
}
function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({
action: 'message_send_failed',
});
}
/*
* The textInput part of the MessageComposer
*/
export default class MessageComposerInput extends React.Component {
static propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
@ -889,8 +901,6 @@ export default class MessageComposerInput extends React.Component {
}
MessageComposerInput.propTypes = {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,

View file

@ -1,470 +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.
*/
var React = require("react");
var MatrixClientPeg = require("../../../MatrixClientPeg");
var SlashCommands = require("../../../SlashCommands");
var Modal = require("../../../Modal");
var MemberEntry = require("../../../TabCompleteEntries").MemberEntry;
var sdk = require('../../../index');
import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
var dis = require("../../../dispatcher");
var KeyCode = require("../../../KeyCode");
var Markdown = require("../../../Markdown");
var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
export function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
if (err.name === "UnknownDeviceError") {
dis.dispatch({
action: 'unknown_device_error',
err: err,
room: room,
});
}
dis.dispatch({
action: 'message_send_failed',
});
}
/*
* The textInput part of the MessageComposer
*/
export default React.createClass({
displayName: 'MessageComposerInput',
statics: {
// the height we limit the composer to
MAX_HEIGHT: 100,
},
propTypes: {
tabComplete: React.PropTypes.any,
// a callback which is called when the height of the composer is
// changed due to a change in content.
onResize: React.PropTypes.func,
// js-sdk Room object
room: React.PropTypes.object.isRequired,
// The text to use a placeholder in the input box
placeholder: React.PropTypes.string.isRequired,
// callback to handle files pasted into the composer
onFilesPasted: React.PropTypes.func,
},
componentWillMount: function() {
this.oldScrollHeight = 0;
this.markdownEnabled = !UserSettingsStore.getSyncedSetting('disableMarkdown', false);
var self = this;
this.sentHistory = {
// The list of typed messages. Index 0 is more recent
data: [],
// The position in data currently displayed
position: -1,
// The room the history is for.
roomId: null,
// The original text before they hit UP
originalText: null,
// The textarea element to set text to.
element: null,
init: function(element, roomId) {
this.roomId = roomId;
this.element = element;
this.position = -1;
var storedData = window.sessionStorage.getItem(
"history_" + roomId
);
if (storedData) {
this.data = JSON.parse(storedData);
}
if (this.roomId) {
this.setLastTextEntry();
}
},
push: function(text) {
// store a message in the sent history
this.data.unshift(text);
window.sessionStorage.setItem(
"history_" + this.roomId,
JSON.stringify(this.data)
);
// reset history position
this.position = -1;
this.originalText = null;
},
// move in the history. Returns true if we managed to move.
next: function(offset) {
if (this.position === -1) {
// user is going into the history, save the current line.
this.originalText = this.element.value;
}
else {
// user may have modified this line in the history; remember it.
this.data[this.position] = this.element.value;
}
if (offset > 0 && this.position === (this.data.length - 1)) {
// we've run out of history
return false;
}
// retrieve the next item (bounded).
var newPosition = this.position + offset;
newPosition = Math.max(-1, newPosition);
newPosition = Math.min(newPosition, this.data.length - 1);
this.position = newPosition;
if (this.position !== -1) {
// show the message
this.element.value = this.data[this.position];
}
else if (this.originalText !== undefined) {
// restore the original text the user was typing.
this.element.value = this.originalText;
}
self.resizeInput();
return true;
},
saveLastTextEntry: function() {
// save the currently entered text in order to restore it later.
// NB: This isn't 'originalText' because we want to restore
// sent history items too!
var text = this.element.value;
window.sessionStorage.setItem("input_" + this.roomId, text);
},
setLastTextEntry: function() {
var text = window.sessionStorage.getItem("input_" + this.roomId);
if (text) {
this.element.value = text;
self.resizeInput();
}
}
};
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.sentHistory.init(
this.refs.textarea,
this.props.room.roomId
);
this.resizeInput();
if (this.props.tabComplete) {
this.props.tabComplete.setTextArea(this.refs.textarea);
}
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
this.sentHistory.saveLastTextEntry();
},
onAction: function(payload) {
var textarea = this.refs.textarea;
switch (payload.action) {
case 'focus_composer':
textarea.focus();
break;
case 'insert_displayname':
if (textarea.value.length) {
var left = textarea.value.substring(0, textarea.selectionStart);
var right = textarea.value.substring(textarea.selectionEnd);
if (right.length) {
left += payload.displayname;
}
else {
left = left.replace(/( ?)$/, " " + payload.displayname);
}
textarea.value = left + right;
textarea.focus();
textarea.setSelectionRange(left.length, left.length);
}
else {
textarea.value = payload.displayname + ": ";
textarea.focus();
}
break;
}
},
onKeyDown: function(ev) {
if (ev.keyCode === KeyCode.ENTER && !ev.shiftKey) {
var input = this.refs.textarea.value;
if (input.length === 0) {
ev.preventDefault();
return;
}
this.sentHistory.push(input);
this.onEnter(ev);
}
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
var oldSelectionStart = this.refs.textarea.selectionStart;
// Remember the keyCode because React will recycle the synthetic event
var keyCode = ev.keyCode;
// set a callback so we can see if the cursor position changes as
// a result of this event. If it doesn't, we cycle history.
setTimeout(() => {
if (this.refs.textarea.selectionStart == oldSelectionStart) {
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
this.resizeInput();
}
}, 0);
}
if (this.props.tabComplete) {
this.props.tabComplete.onKeyDown(ev);
}
var self = this;
setTimeout(function() {
if (self.refs.textarea && self.refs.textarea.value != '') {
self.onTypingActivity();
} else {
self.onFinishedTyping();
}
}, 10); // XXX: what is this 10ms setTimeout doing? Looks hacky :(
},
resizeInput: function() {
// scrollHeight is at least equal to clientHeight, so we have to
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
this.refs.textarea.style.height = "20px"; // 20 hardcoded from CSS
var newHeight = Math.min(this.refs.textarea.scrollHeight,
this.constructor.MAX_HEIGHT);
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
this.oldScrollHeight = this.refs.textarea.scrollHeight;
if (this.props.onResize) {
// kick gemini-scrollbar to re-layout
this.props.onResize();
}
},
onKeyUp: function(ev) {
if (this.refs.textarea.scrollHeight !== this.oldScrollHeight ||
ev.keyCode === KeyCode.DELETE ||
ev.keyCode === KeyCode.BACKSPACE)
{
this.resizeInput();
}
},
onEnter: function(ev) {
var contentText = this.refs.textarea.value;
// bodge for now to set markdown state on/off. We probably want a separate
// area for "local" commands which don't hit out to the server.
if (contentText.indexOf("/markdown") === 0) {
ev.preventDefault();
this.refs.textarea.value = '';
if (contentText.indexOf("/markdown on") === 0) {
this.markdownEnabled = true;
}
else if (contentText.indexOf("/markdown off") === 0) {
this.markdownEnabled = false;
}
else {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Unknown command"),
description: _t("Usage") + ": /markdown on|off",
});
}
return;
}
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
if (cmd) {
ev.preventDefault();
if (!cmd.error) {
this.refs.textarea.value = '';
}
if (cmd.promise) {
cmd.promise.done(function() {
console.log("Command success.");
}, function(err) {
console.error("Command failure: %s", err);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Server error"),
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
});
});
}
else if (cmd.error) {
console.error(cmd.error);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Command error"),
description: cmd.error,
});
}
return;
}
var isEmote = /^\/me( |$)/i.test(contentText);
var sendMessagePromise;
if (isEmote) {
contentText = contentText.substring(4);
}
else if (contentText[0] === '/') {
contentText = contentText.substring(1);
}
let send_markdown = false;
let mdown;
if (this.markdownEnabled) {
mdown = new Markdown(contentText);
send_markdown = !mdown.isPlainText();
}
if (send_markdown) {
const htmlText = mdown.toHTML();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendHtmlEmote(this.props.room.roomId, contentText, htmlText) :
MatrixClientPeg.get().sendHtmlMessage(this.props.room.roomId, contentText, htmlText);
}
else {
if (mdown) contentText = mdown.toPlaintext();
sendMessagePromise = isEmote ?
MatrixClientPeg.get().sendEmoteMessage(this.props.room.roomId, contentText) :
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
}
sendMessagePromise.done(function(res) {
dis.dispatch({
action: 'message_sent'
});
}, (e) => onSendMessageFailed(e, this.props.room));
this.refs.textarea.value = '';
this.resizeInput();
ev.preventDefault();
},
onTypingActivity: function() {
this.isTyping = true;
if (!this.userTypingTimer) {
this.sendTyping(true);
}
this.startUserTypingTimer();
this.startServerTypingTimer();
},
onFinishedTyping: function() {
this.isTyping = false;
this.sendTyping(false);
this.stopUserTypingTimer();
this.stopServerTypingTimer();
},
startUserTypingTimer: function() {
this.stopUserTypingTimer();
var self = this;
this.userTypingTimer = setTimeout(function() {
self.isTyping = false;
self.sendTyping(self.isTyping);
self.userTypingTimer = null;
}, TYPING_USER_TIMEOUT);
},
stopUserTypingTimer: function() {
if (this.userTypingTimer) {
clearTimeout(this.userTypingTimer);
this.userTypingTimer = null;
}
},
startServerTypingTimer: function() {
if (!this.serverTypingTimer) {
var self = this;
this.serverTypingTimer = setTimeout(function() {
if (self.isTyping) {
self.sendTyping(self.isTyping);
self.startServerTypingTimer();
}
}, TYPING_SERVER_TIMEOUT / 2);
}
},
stopServerTypingTimer: function() {
if (this.serverTypingTimer) {
clearTimeout(this.servrTypingTimer);
this.serverTypingTimer = null;
}
},
sendTyping: function(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
MatrixClientPeg.get().sendTyping(
this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT
).done();
},
refreshTyping: function() {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
this.typingTimeout = null;
}
},
onInputClick: function(ev) {
this.refs.textarea.focus();
},
_onPaste: function(ev) {
const items = ev.clipboardData.items;
const files = [];
for (const item of items) {
if (item.kind === 'file') {
files.push(item.getAsFile());
}
}
if (files.length && this.props.onFilesPasted) {
this.props.onFilesPasted(files);
return true;
}
return false;
},
render: function() {
return (
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
<textarea dir="auto" autoFocus ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder={this.props.placeholder}
onPaste={this._onPaste}
/>
</div>
);
}
});

View file

@ -1,48 +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.
*/
'use strict';
var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
module.exports = React.createClass({
displayName: 'TabCompleteBar',
propTypes: {
tabComplete: React.PropTypes.object.isRequired
},
render: function() {
return (
<div className="mx_TabCompleteBar">
{this.props.tabComplete.peek(6).map((entry, i) => {
return (
<div key={entry.getKey() || i + ""}
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
onClick={this.props.tabComplete.onEntryClick.bind(this.props.tabComplete, entry)} >
{entry.getImageJsx()}
<span className="mx_TabCompleteBar_text">
{entry.getText()}
</span>
</div>
);
})}
</div>
);
}
});

View file

@ -619,7 +619,6 @@
"Turn Markdown off": "Markdown deaktiveren",
"Turn Markdown on": "Markdown einschalten",
"Unable to load device list": "Geräteliste konnte nicht geladen werden",
"Unknown command": "Unbekannter Befehl",
"Unknown room %(roomId)s": "Unbekannter Raum %(roomId)s",
"Usage: /markdown on|off": "Verwendung: /markdown on|off",
"You seem to be in a call, are you sure you want to quit?": "Du scheinst in einem Anruf zu sein. Bist du sicher schließen zu wollen?",

View file

@ -499,7 +499,6 @@
"unencrypted": "μη κρυπτογραφημένο",
"Unencrypted message": "Μη κρυπτογραφημένο μήνυμα",
"unknown caller": "άγνωστος καλών",
"Unknown command": "Άγνωστη εντολή",
"unknown device": "άγνωστη συσκευή",
"Unknown room %(roomId)s": "Άγνωστο δωμάτιο %(roomId)s",
"unknown": "άγνωστο",

View file

@ -603,7 +603,6 @@
"unencrypted": "unencrypted",
"Unencrypted message": "Unencrypted message",
"unknown caller": "unknown caller",
"Unknown command": "Unknown command",
"unknown device": "unknown device",
"unknown error code": "unknown error code",
"Unknown room %(roomId)s": "Unknown room %(roomId)s",

View file

@ -547,7 +547,6 @@
"Unable to load device list": "Unable to load device list",
"Unencrypted room": "Unencrypted room",
"unencrypted": "unencrypted",
"Unknown command": "Unknown command",
"unknown device": "unknown device",
"unknown error code": "unknown error code",
"Unknown room %(roomId)s": "Unknown room %(roomId)s",

View file

@ -520,7 +520,6 @@
"Unable to load device list": "Impossible de charger la liste d'appareils",
"Unencrypted room": "Salon non chiffré",
"unencrypted": "non chiffré",
"Unknown command": "Commande inconnue",
"unknown device": "appareil inconnu",
"Unknown room %(roomId)s": "Salon inconnu %(roomId)s",
"unknown": "inconnu",

View file

@ -602,7 +602,6 @@
"unencrypted": "titkosítatlan",
"Unencrypted message": "Titkosítatlan üzenet",
"unknown caller": "ismeretlen hívó",
"Unknown command": "Ismeretlen parancs",
"unknown device": "ismeretlen eszköz",
"Unknown room %(roomId)s": "Ismeretlen szoba %(roomId)s",
"Unknown (user, device) pair:": "Ismeretlen (felhasználó, eszköz) pár:",

View file

@ -607,7 +607,6 @@
"unencrypted": "암호화하지 않음",
"Unencrypted message": "암호화하지 않은 메시지",
"unknown caller": "알 수 없는 발신자",
"Unknown command": "알 수 없는 명령",
"unknown device": "알 수 없는 장치",
"Unknown room %(roomId)s": "알 수 없는 방 %(roomId)s",
"Unknown (user, device) pair:": "알 수 없는 (사용자, 장치) 연결:",

View file

@ -637,7 +637,6 @@
"Turn Markdown off": "Desabilitar a formatação 'Markdown'",
"Turn Markdown on": "Habilitar a marcação 'Markdown'",
"Unable to load device list": "Não foi possível carregar a lista de dispositivos",
"Unknown command": "Comando desconhecido",
"Unknown room %(roomId)s": "A sala %(roomId)s é desconhecida",
"You have been invited to join this room by %(inviterName)s": "Você foi convidada/o por %(inviterName)s a ingressar nesta sala",
"You seem to be in a call, are you sure you want to quit?": "Parece que você está em uma chamada. Tem certeza que quer sair?",

View file

@ -638,7 +638,6 @@
"Turn Markdown off": "Desabilitar a formatação 'Markdown'",
"Turn Markdown on": "Habilitar a marcação 'Markdown'",
"Unable to load device list": "Não foi possível carregar a lista de dispositivos",
"Unknown command": "Comando desconhecido",
"Unknown room %(roomId)s": "A sala %(roomId)s é desconhecida",
"You have been invited to join this room by %(inviterName)s": "Você foi convidada/o por %(inviterName)s a ingressar nesta sala",
"You seem to be in a call, are you sure you want to quit?": "Parece que você está em uma chamada. Tem certeza que quer sair?",

View file

@ -591,7 +591,6 @@
"to restore": "восстановить",
"Turn Markdown off": "Выключить Markdown",
"Turn Markdown on": "Включить Markdown",
"Unknown command": "Неизвестная команда",
"Unknown room %(roomId)s": "Неизвестная комната %(roomId)s",
"You have been invited to join this room by %(inviterName)s": "Вы были приглашены войти в эту комнату от %(inviterName)s",
"You seem to be uploading files, are you sure you want to quit?": "Похоже вы передаёте файлы, вы уверены, что хотите выйти?",

View file

@ -361,7 +361,6 @@
"Unable to load device list": "ไม่สามารถโหลดรายชื่ออุปกรณ์",
"Unencrypted room": "ห้องที่ไม่เข้ารหัส",
"unencrypted": "ยังไม่ได้เข้ารหัส",
"Unknown command": "คำสั่งที่ไม่รู้จัก",
"unknown device": "อุปกรณ์ที่ไม่รู้จัก",
"Unknown room %(roomId)s": "ห้องที่ไม่รู้จัก %(roomId)s",
"Unknown (user, device) pair:": "คู่ (ผู้ใช้, อุปกรณ์) ที่ไม่รู้จัก:",

View file

@ -589,7 +589,6 @@
"unencrypted": "şifrelenmemiş",
"Unencrypted message": "Şifrelenmemiş mesaj",
"unknown caller": "bilinmeyen arayıcı",
"Unknown command": "Bilinmeyen komut",
"unknown device": "bilinmeyen cihaz",
"unknown error code": "bilinmeyen hata kodu",
"Unknown room %(roomId)s": "Bilinmeyen oda %(roomId)s",