Merge branches 'develop' and 't3chguy/add-missing-autocomplete-commands' of github.com:matrix-org/matrix-react-sdk into t3chguy/add-missing-autocomplete-commands
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> # Conflicts: # src/autocomplete/CommandProvider.js # src/i18n/strings/en_EN.json
This commit is contained in:
commit
09f017fdd2
33 changed files with 2509 additions and 1292 deletions
6
.flowconfig
Normal file
6
.flowconfig
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[include]
|
||||||
|
src/**/*.js
|
||||||
|
test/**/*.js
|
||||||
|
|
||||||
|
[ignore]
|
||||||
|
node_modules/
|
|
@ -33,8 +33,9 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"reskindex": "node scripts/reskindex.js -h header",
|
"reskindex": "node scripts/reskindex.js -h header",
|
||||||
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
"reskindex:watch": "node scripts/reskindex.js -h header -w",
|
||||||
"build": "npm run reskindex && babel src -d lib --source-maps",
|
"build": "npm run reskindex && babel src -d lib --source-maps --copy-files",
|
||||||
"build:watch": "babel src -w -d lib --source-maps",
|
"build:watch": "babel src -w -d lib --source-maps --copy-files",
|
||||||
|
"emoji-data-strip": "node scripts/emoji-data-strip.js",
|
||||||
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
"start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
|
@ -51,7 +52,7 @@
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"commonmark": "^0.27.0",
|
"commonmark": "^0.27.0",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.0",
|
||||||
"draft-js": "^0.8.1",
|
"draft-js": "^0.9.1",
|
||||||
"draft-js-export-html": "^0.5.0",
|
"draft-js-export-html": "^0.5.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.3",
|
||||||
|
@ -64,7 +65,7 @@
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"linkifyjs": "^2.1.3",
|
"linkifyjs": "^2.1.3",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"matrix-js-sdk": "0.7.13",
|
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
|
|
23
scripts/emoji-data-strip.js
Normal file
23
scripts/emoji-data-strip.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const EMOJI_DATA = require('emojione/emoji.json');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const output = Object.keys(EMOJI_DATA).map(
|
||||||
|
(key) => {
|
||||||
|
const datum = EMOJI_DATA[key];
|
||||||
|
const newDatum = {
|
||||||
|
name: datum.name,
|
||||||
|
shortname: datum.shortname,
|
||||||
|
category: datum.category,
|
||||||
|
emoji_order: datum.emoji_order,
|
||||||
|
};
|
||||||
|
if (datum.aliases_ascii.length > 0) {
|
||||||
|
newDatum.aliases_ascii = datum.aliases_ascii;
|
||||||
|
}
|
||||||
|
return newDatum;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write to a file in src. Changes should be checked into git. This file is copied by
|
||||||
|
// babel using --copy-files
|
||||||
|
fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output));
|
82
src/ComposerHistoryManager.js
Normal file
82
src/ComposerHistoryManager.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
//@flow
|
||||||
|
/*
|
||||||
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
|
||||||
|
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 {ContentState} from 'draft-js';
|
||||||
|
import * as RichText from './RichText';
|
||||||
|
import Markdown from './Markdown';
|
||||||
|
import _flow from 'lodash/flow';
|
||||||
|
import _clamp from 'lodash/clamp';
|
||||||
|
|
||||||
|
type MessageFormat = 'html' | 'markdown';
|
||||||
|
|
||||||
|
class HistoryItem {
|
||||||
|
message: string = '';
|
||||||
|
format: MessageFormat = 'html';
|
||||||
|
|
||||||
|
constructor(message: string, format: MessageFormat) {
|
||||||
|
this.message = message;
|
||||||
|
this.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
toContentState(format: MessageFormat): ContentState {
|
||||||
|
let {message} = this;
|
||||||
|
if (format === 'markdown') {
|
||||||
|
if (this.format === 'html') {
|
||||||
|
message = _flow([RichText.htmlToContentState, RichText.stateToMarkdown])(message);
|
||||||
|
}
|
||||||
|
return ContentState.createFromText(message);
|
||||||
|
} else {
|
||||||
|
if (this.format === 'markdown') {
|
||||||
|
message = new Markdown(message).toHTML();
|
||||||
|
}
|
||||||
|
return RichText.htmlToContentState(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ComposerHistoryManager {
|
||||||
|
history: Array<HistoryItem> = [];
|
||||||
|
prefix: string;
|
||||||
|
lastIndex: number = 0;
|
||||||
|
currentIndex: number = 0;
|
||||||
|
|
||||||
|
constructor(roomId: string, prefix: string = 'mx_composer_history_') {
|
||||||
|
this.prefix = prefix + roomId;
|
||||||
|
|
||||||
|
// TODO: Performance issues?
|
||||||
|
let item;
|
||||||
|
for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
||||||
|
this.history.push(
|
||||||
|
Object.assign(new HistoryItem(), JSON.parse(item)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.lastIndex = this.currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(message: string, format: MessageFormat) {
|
||||||
|
const item = new HistoryItem(message, format);
|
||||||
|
this.history.push(item);
|
||||||
|
this.currentIndex = this.lastIndex + 1;
|
||||||
|
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
getItem(offset: number, format: MessageFormat): ?ContentState {
|
||||||
|
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
|
||||||
|
const item = this.history[this.currentIndex];
|
||||||
|
return item ? item.toContentState(format) : null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,7 +84,7 @@ export function charactersToImageNode(alt, useSvg, ...unicode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function stripParagraphs(html: string): string {
|
export function processHtmlForSending(html: string): string {
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
|
|
||||||
|
@ -93,10 +93,21 @@ export function stripParagraphs(html: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentHTML = "";
|
let contentHTML = "";
|
||||||
for (let i=0; i<contentDiv.children.length; i++) {
|
for (let i=0; i < contentDiv.children.length; i++) {
|
||||||
const element = contentDiv.children[i];
|
const element = contentDiv.children[i];
|
||||||
if (element.tagName.toLowerCase() === 'p') {
|
if (element.tagName.toLowerCase() === 'p') {
|
||||||
contentHTML += element.innerHTML + '<br />';
|
contentHTML += element.innerHTML;
|
||||||
|
// Don't add a <br /> for the last <p>
|
||||||
|
if (i !== contentDiv.children.length - 1) {
|
||||||
|
contentHTML += '<br />';
|
||||||
|
}
|
||||||
|
} else if (element.tagName.toLowerCase() === 'pre') {
|
||||||
|
// Replace "<br>\n" with "\n" within `<pre>` tags because the <br> is
|
||||||
|
// redundant. This is a workaround for a bug in draft-js-export-html:
|
||||||
|
// https://github.com/sstur/draft-js-export-html/issues/62
|
||||||
|
contentHTML += '<pre>' +
|
||||||
|
element.innerHTML.replace(/<br>\n/g, '\n').trim() +
|
||||||
|
'</pre>';
|
||||||
} else {
|
} else {
|
||||||
const temp = document.createElement('div');
|
const temp = document.createElement('div');
|
||||||
temp.appendChild(element.cloneNode(true));
|
temp.appendChild(element.cloneNode(true));
|
||||||
|
@ -124,6 +135,7 @@ var sanitizeHtmlParams = {
|
||||||
// would make sense if we did
|
// would make sense if we did
|
||||||
img: ['src'],
|
img: ['src'],
|
||||||
ol: ['start'],
|
ol: ['start'],
|
||||||
|
code: ['class'], // We don't actually allow all classes, we filter them in transformTags
|
||||||
},
|
},
|
||||||
// Lots of these won't come up by default because we don't allow them
|
// Lots of these won't come up by default because we don't allow them
|
||||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||||
|
@ -165,6 +177,19 @@ var sanitizeHtmlParams = {
|
||||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
return { tagName: tagName, attribs : attribs };
|
return { tagName: tagName, attribs : attribs };
|
||||||
},
|
},
|
||||||
|
'code': function(tagName, attribs) {
|
||||||
|
if (typeof attribs.class !== 'undefined') {
|
||||||
|
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
||||||
|
let classes = attribs.class.split(/\s+/).filter(function(cl) {
|
||||||
|
return cl.startsWith('language-');
|
||||||
|
});
|
||||||
|
attribs.class = classes.join(' ');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tagName: tagName,
|
||||||
|
attribs: attribs,
|
||||||
|
};
|
||||||
|
},
|
||||||
'*': function(tagName, attribs) {
|
'*': function(tagName, attribs) {
|
||||||
// Delete any style previously assigned, style is an allowedTag for font and span
|
// Delete any style previously assigned, style is an allowedTag for font and span
|
||||||
// because attributes are stripped after transforming
|
// because attributes are stripped after transforming
|
||||||
|
|
|
@ -30,7 +30,30 @@ module.exports = {
|
||||||
RIGHT: 39,
|
RIGHT: 39,
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
DELETE: 46,
|
DELETE: 46,
|
||||||
|
KEY_A: 65,
|
||||||
|
KEY_B: 66,
|
||||||
|
KEY_C: 67,
|
||||||
KEY_D: 68,
|
KEY_D: 68,
|
||||||
KEY_E: 69,
|
KEY_E: 69,
|
||||||
|
KEY_F: 70,
|
||||||
|
KEY_G: 71,
|
||||||
|
KEY_H: 72,
|
||||||
|
KEY_I: 73,
|
||||||
|
KEY_J: 74,
|
||||||
|
KEY_K: 75,
|
||||||
|
KEY_L: 76,
|
||||||
KEY_M: 77,
|
KEY_M: 77,
|
||||||
|
KEY_N: 78,
|
||||||
|
KEY_O: 79,
|
||||||
|
KEY_P: 80,
|
||||||
|
KEY_Q: 81,
|
||||||
|
KEY_R: 82,
|
||||||
|
KEY_S: 83,
|
||||||
|
KEY_T: 84,
|
||||||
|
KEY_U: 85,
|
||||||
|
KEY_V: 86,
|
||||||
|
KEY_W: 87,
|
||||||
|
KEY_X: 88,
|
||||||
|
KEY_Y: 89,
|
||||||
|
KEY_Z: 90,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,6 +16,7 @@ import * as sdk from './index';
|
||||||
import * as emojione from 'emojione';
|
import * as emojione from 'emojione';
|
||||||
import {stateToHTML} from 'draft-js-export-html';
|
import {stateToHTML} from 'draft-js-export-html';
|
||||||
import {SelectionRange} from "./autocomplete/Autocompleter";
|
import {SelectionRange} from "./autocomplete/Autocompleter";
|
||||||
|
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
||||||
|
|
||||||
const MARKDOWN_REGEX = {
|
const MARKDOWN_REGEX = {
|
||||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||||
|
@ -30,9 +31,26 @@ const USERNAME_REGEX = /@\S+:\S+/g;
|
||||||
const ROOM_REGEX = /#\S+:\S+/g;
|
const ROOM_REGEX = /#\S+:\S+/g;
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
|
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
|
||||||
|
|
||||||
export const contentStateToHTML = stateToHTML;
|
const ZWS_CODE = 8203;
|
||||||
|
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
|
||||||
|
export function stateToMarkdown(state) {
|
||||||
|
return __stateToMarkdown(state)
|
||||||
|
.replace(
|
||||||
|
ZWS, // draft-js-export-markdown adds these
|
||||||
|
''); // this is *not* a zero width space, trust me :)
|
||||||
|
}
|
||||||
|
|
||||||
export function HTMLtoContentState(html: string): ContentState {
|
export const contentStateToHTML = (contentState: ContentState) => {
|
||||||
|
return stateToHTML(contentState, {
|
||||||
|
inlineStyles: {
|
||||||
|
UNDERLINE: {
|
||||||
|
element: 'u'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function htmlToContentState(html: string): ContentState {
|
||||||
return ContentState.createFromBlockArray(convertFromHTML(html));
|
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,9 +164,9 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
markdownDecorators.push(emojiDecorator);
|
// markdownDecorators.push(emojiDecorator);
|
||||||
|
// TODO Consider renabling "syntax highlighting" when we can do it properly
|
||||||
return markdownDecorators;
|
return [emojiDecorator];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -109,6 +110,76 @@ Example:
|
||||||
response: 78
|
response: 78
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_widget
|
||||||
|
----------
|
||||||
|
Set a new widget in the room. Clobbers based on the ID.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- `room_id` (String) is the room to set the widget in.
|
||||||
|
- `widget_id` (String) is the ID of the widget to add (or replace if it already exists).
|
||||||
|
It can be an arbitrary UTF8 string and is purely for distinguishing between widgets.
|
||||||
|
- `url` (String) is the URL that clients should load in an iframe to run the widget.
|
||||||
|
All widgets must have a valid URL. If the URL is `null` (not `undefined`), the
|
||||||
|
widget will be removed from the room.
|
||||||
|
- `type` (String) is the type of widget, which is provided as a hint for matrix clients so they
|
||||||
|
can configure/lay out the widget in different ways. All widgets must have a type.
|
||||||
|
- `name` (String) is an optional human-readable string about the widget.
|
||||||
|
- `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs.
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
action: "set_widget",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
widget_id: "abc123",
|
||||||
|
url: "http://widget.url",
|
||||||
|
type: "example",
|
||||||
|
response: {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get_widgets
|
||||||
|
-----------
|
||||||
|
Get a list of all widgets in the room. The response is the `content` field
|
||||||
|
of the state event.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- `room_id` (String) is the room to get the widgets in.
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
$widget_id: {
|
||||||
|
type: "example",
|
||||||
|
url: "http://widget.url",
|
||||||
|
name: "Example Widget",
|
||||||
|
data: {
|
||||||
|
key: "val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$widget_id: { ... }
|
||||||
|
}
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
action: "get_widgets",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
widget_id: "abc123",
|
||||||
|
url: "http://widget.url",
|
||||||
|
type: "example",
|
||||||
|
response: {
|
||||||
|
$widget_id: {
|
||||||
|
type: "example",
|
||||||
|
url: "http://widget.url",
|
||||||
|
name: "Example Widget",
|
||||||
|
data: {
|
||||||
|
key: "val"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
$widget_id: { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
membership_state AND bot_options
|
membership_state AND bot_options
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
@ -191,6 +262,84 @@ function inviteUser(event, roomId, userId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setWidget(event, roomId) {
|
||||||
|
const widgetId = event.data.widget_id;
|
||||||
|
const widgetType = event.data.type;
|
||||||
|
const widgetUrl = event.data.url;
|
||||||
|
const widgetName = event.data.name; // optional
|
||||||
|
const widgetData = event.data.data; // optional
|
||||||
|
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, _t('You need to be logged in.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// both adding/removing widgets need these checks
|
||||||
|
if (!widgetId || widgetUrl === undefined) {
|
||||||
|
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc
|
||||||
|
// check types of fields
|
||||||
|
if (widgetName !== undefined && typeof widgetName !== 'string') {
|
||||||
|
sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (widgetData !== undefined && !(widgetData instanceof Object)) {
|
||||||
|
sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof widgetType !== 'string') {
|
||||||
|
sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof widgetUrl !== 'string') {
|
||||||
|
sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: same dance we do for power levels. It'd be nice if the JS SDK had helper methods to do this.
|
||||||
|
client.getStateEvent(roomId, "im.vector.modular.widgets", "").then((widgets) => {
|
||||||
|
if (widgetUrl === null) {
|
||||||
|
delete widgets[widgetId];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
widgets[widgetId] = {
|
||||||
|
type: widgetType,
|
||||||
|
url: widgetUrl,
|
||||||
|
name: widgetName,
|
||||||
|
data: widgetData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return client.sendStateEvent(roomId, "im.vector.modular.widgets", widgets);
|
||||||
|
}, (err) => {
|
||||||
|
if (err.errcode === "M_NOT_FOUND") {
|
||||||
|
return client.sendStateEvent(roomId, "im.vector.modular.widgets", {
|
||||||
|
[widgetId]: {
|
||||||
|
type: widgetType,
|
||||||
|
url: widgetUrl,
|
||||||
|
name: widgetName,
|
||||||
|
data: widgetData,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}).done(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
sendError(event, _t('Failed to send request.'), err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWidgets(event, roomId) {
|
||||||
|
returnStateEvent(event, roomId, "im.vector.modular.widgets", "");
|
||||||
|
}
|
||||||
|
|
||||||
function setPlumbingState(event, roomId, status) {
|
function setPlumbingState(event, roomId, status) {
|
||||||
if (typeof status !== 'string') {
|
if (typeof status !== 'string') {
|
||||||
throw new Error('Plumbing state status should be a string');
|
throw new Error('Plumbing state status should be a string');
|
||||||
|
@ -367,7 +516,7 @@ const onMessage = function(event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting join rules does not require userId
|
// These APIs don't require userId
|
||||||
if (event.data.action === "join_rules_state") {
|
if (event.data.action === "join_rules_state") {
|
||||||
getJoinRules(event, roomId);
|
getJoinRules(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
@ -377,6 +526,12 @@ const onMessage = function(event) {
|
||||||
} else if (event.data.action === "get_membership_count") {
|
} else if (event.data.action === "get_membership_count") {
|
||||||
getMembershipCount(event, roomId);
|
getMembershipCount(event, roomId);
|
||||||
return;
|
return;
|
||||||
|
} else if (event.data.action === "set_widget") {
|
||||||
|
setWidget(event, roomId);
|
||||||
|
return;
|
||||||
|
} else if (event.data.action === "get_widgets") {
|
||||||
|
getWidgets(event, roomId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
@ -409,12 +564,27 @@ const onMessage = function(event) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let listenerCount = 0;
|
||||||
module.exports = {
|
module.exports = {
|
||||||
startListening: function() {
|
startListening: function() {
|
||||||
|
if (listenerCount === 0) {
|
||||||
window.addEventListener("message", onMessage, false);
|
window.addEventListener("message", onMessage, false);
|
||||||
|
}
|
||||||
|
listenerCount += 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
stopListening: function() {
|
stopListening: function() {
|
||||||
|
listenerCount -= 1;
|
||||||
|
if (listenerCount === 0) {
|
||||||
window.removeEventListener("message", onMessage);
|
window.removeEventListener("message", onMessage);
|
||||||
|
}
|
||||||
|
if (listenerCount < 0) {
|
||||||
|
// Make an error so we get a stack trace
|
||||||
|
const e = new Error(
|
||||||
|
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
||||||
|
" Negative count"
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,7 +37,26 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
doesRoomHaveUnreadMessages: function(room) {
|
doesRoomHaveUnreadMessages: function(room) {
|
||||||
var readUpToId = room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
var myUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
|
// get the most recent read receipt sent by our account.
|
||||||
|
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||||
|
// despite the name of the method :((
|
||||||
|
var readUpToId = room.getEventReadUpTo(myUserId);
|
||||||
|
|
||||||
|
// as we don't send RRs for our own messages, make sure we special case that
|
||||||
|
// if *we* sent the last message into the room, we consider it not unread!
|
||||||
|
// Should fix: https://github.com/vector-im/riot-web/issues/3263
|
||||||
|
// https://github.com/vector-im/riot-web/issues/2427
|
||||||
|
// ...and possibly some of the others at
|
||||||
|
// https://github.com/vector-im/riot-web/issues/3363
|
||||||
|
if (room.timeline.length &&
|
||||||
|
room.timeline[room.timeline.length - 1].sender &&
|
||||||
|
room.timeline[room.timeline.length - 1].sender.userId === myUserId)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// this just looks at whatever history we have, which if we've only just started
|
// this just looks at whatever history we have, which if we've only just started
|
||||||
// up probably won't be very much, so if the last couple of events are ones that
|
// up probably won't be very much, so if the last couple of events are ones that
|
||||||
// don't count, we don't know if there are any events that do count between where
|
// don't count, we don't know if there are any events that do count between where
|
||||||
|
|
|
@ -30,11 +30,17 @@ export default {
|
||||||
id: 'rich_text_editor',
|
id: 'rich_text_editor',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "-",
|
||||||
|
id: 'matrix_apps',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// horrible but it works. The locality makes this somewhat more palatable.
|
// horrible but it works. The locality makes this somewhat more palatable.
|
||||||
doTranslations: function() {
|
doTranslations: function() {
|
||||||
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
|
this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete");
|
||||||
|
this.LABS_FEATURES[1].name = _t("Matrix Apps");
|
||||||
},
|
},
|
||||||
|
|
||||||
loadProfileInfo: function() {
|
loadProfileInfo: function() {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import type {Completion, SelectionRange} from './Autocompleter';
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
constructor(commandRegex?: RegExp) {
|
||||||
if (commandRegex) {
|
if (commandRegex) {
|
||||||
if (!commandRegex.global) {
|
if (!commandRegex.global) {
|
||||||
throw new Error('commandRegex must have global flag set');
|
throw new Error('commandRegex must have global flag set');
|
||||||
|
|
|
@ -59,7 +59,7 @@ export async function getCompletions(query: string, selection: SelectionRange, f
|
||||||
PROVIDERS.map(provider => {
|
PROVIDERS.map(provider => {
|
||||||
return Q(provider.getCompletions(query, selection, force))
|
return Q(provider.getCompletions(query, selection, force))
|
||||||
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return completionsList
|
return completionsList
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
|
||||||
// TODO merge this with the factory mechanics of SlashCommands?
|
// TODO merge this with the factory mechanics of SlashCommands?
|
||||||
|
@ -36,13 +36,13 @@ const COMMANDS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/unban',
|
command: '/unban',
|
||||||
args: '<user-id> [reason]',
|
args: '<user-id>',
|
||||||
description: 'Unbans user with given id',
|
description: 'Unbans user with given id',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/op',
|
command: '/op',
|
||||||
args: '<user-id>',
|
args: '<userId> [<power-level>]',
|
||||||
description: 'Ops user with given id',
|
description: 'Define the power level of a user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/deop',
|
command: '/deop',
|
||||||
|
@ -61,8 +61,8 @@ const COMMANDS = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/part',
|
command: '/part',
|
||||||
args: '<room-alias>',
|
args: '[<room-alias>]',
|
||||||
description: 'Leaves room with given alias',
|
description: 'Leave room',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/topic',
|
command: '/topic',
|
||||||
|
@ -104,7 +104,7 @@ let instance = null;
|
||||||
export default class CommandProvider extends AutocompleteProvider {
|
export default class CommandProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(COMMAND_RE);
|
super(COMMAND_RE);
|
||||||
this.fuse = new Fuse(COMMANDS, {
|
this.matcher = new FuzzyMatcher(COMMANDS, {
|
||||||
keys: ['command', 'args', 'description'],
|
keys: ['command', 'args', 'description'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map((result) => {
|
completions = this.matcher.match(command[0]).map((result) => {
|
||||||
return {
|
return {
|
||||||
completion: result.command + ' ',
|
completion: result.command + ' ',
|
||||||
component: (<TextualCompletion
|
component: (<TextualCompletion
|
||||||
|
|
|
@ -18,21 +18,55 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp} from 'emojione';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import type {SelectionRange, Completion} from './Autocompleter';
|
import type {SelectionRange, Completion} from './Autocompleter';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
import EmojiData from '../stripped-emoji.json';
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
|
||||||
|
const LIMIT = 20;
|
||||||
|
const CATEGORY_ORDER = [
|
||||||
|
'people',
|
||||||
|
'food',
|
||||||
|
'objects',
|
||||||
|
'activity',
|
||||||
|
'nature',
|
||||||
|
'travel',
|
||||||
|
'flags',
|
||||||
|
'symbols',
|
||||||
|
'unicode9',
|
||||||
|
'modifier',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Match for ":wink:" or ascii-style ";-)" provided by emojione
|
||||||
|
const EMOJI_REGEX = new RegExp('(' + asciiRegexp + '|:\\w*:?)', 'g');
|
||||||
|
const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort(
|
||||||
|
(a, b) => {
|
||||||
|
if (a.category === b.category) {
|
||||||
|
return a.emoji_order - b.emoji_order;
|
||||||
|
}
|
||||||
|
return CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
|
||||||
|
},
|
||||||
|
).map((a) => {
|
||||||
|
return {
|
||||||
|
name: a.name,
|
||||||
|
shortname: a.shortname,
|
||||||
|
aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EMOJI_REGEX);
|
super(EMOJI_REGEX);
|
||||||
this.fuse = new Fuse(EMOJI_SHORTNAMES, {});
|
this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, {
|
||||||
|
keys: ['aliases_ascii', 'shortname', 'name'],
|
||||||
|
// For matching against ascii equivalents
|
||||||
|
shouldMatchWordsOnly: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(query: string, selection: SelectionRange) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
|
@ -41,8 +75,8 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map(result => {
|
completions = this.matcher.match(command[0]).map(result => {
|
||||||
const shortname = EMOJI_SHORTNAMES[result];
|
const {shortname} = result;
|
||||||
const unicode = shortnameToUnicode(shortname);
|
const unicode = shortnameToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: unicode,
|
completion: unicode,
|
||||||
|
@ -51,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 8);
|
}).slice(0, LIMIT);
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
107
src/autocomplete/FuzzyMatcher.js
Normal file
107
src/autocomplete/FuzzyMatcher.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
|
||||||
|
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 Levenshtein from 'liblevenshtein';
|
||||||
|
//import _at from 'lodash/at';
|
||||||
|
//import _flatMap from 'lodash/flatMap';
|
||||||
|
//import _sortBy from 'lodash/sortBy';
|
||||||
|
//import _sortedUniq from 'lodash/sortedUniq';
|
||||||
|
//import _keys from 'lodash/keys';
|
||||||
|
//
|
||||||
|
//class KeyMap {
|
||||||
|
// keys: Array<String>;
|
||||||
|
// objectMap: {[String]: Array<Object>};
|
||||||
|
// priorityMap: {[String]: number}
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//const DEFAULT_RESULT_COUNT = 10;
|
||||||
|
//const DEFAULT_DISTANCE = 5;
|
||||||
|
|
||||||
|
// FIXME Until Fuzzy matching works better, we use prefix matching.
|
||||||
|
|
||||||
|
import PrefixMatcher from './QueryMatcher';
|
||||||
|
export default PrefixMatcher;
|
||||||
|
|
||||||
|
//class FuzzyMatcher { // eslint-disable-line no-unused-vars
|
||||||
|
// /**
|
||||||
|
// * @param {object[]} objects the objects to perform a match on
|
||||||
|
// * @param {string[]} keys an array of keys within each object to match on
|
||||||
|
// * Keys can refer to object properties by name and as in JavaScript (for nested properties)
|
||||||
|
// *
|
||||||
|
// * To use, simply presort objects by required criteria, run through this function and create a FuzzyMatcher with the
|
||||||
|
// * resulting KeyMap.
|
||||||
|
// *
|
||||||
|
// * TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it)
|
||||||
|
// * @return {KeyMap}
|
||||||
|
// */
|
||||||
|
// static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap {
|
||||||
|
// const keyMap = new KeyMap();
|
||||||
|
// const map = {};
|
||||||
|
// const priorities = {};
|
||||||
|
//
|
||||||
|
// objects.forEach((object, i) => {
|
||||||
|
// const keyValues = _at(object, keys);
|
||||||
|
// console.log(object, keyValues, keys);
|
||||||
|
// for (const keyValue of keyValues) {
|
||||||
|
// if (!map.hasOwnProperty(keyValue)) {
|
||||||
|
// map[keyValue] = [];
|
||||||
|
// }
|
||||||
|
// map[keyValue].push(object);
|
||||||
|
// }
|
||||||
|
// priorities[object] = i;
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// keyMap.objectMap = map;
|
||||||
|
// keyMap.priorityMap = priorities;
|
||||||
|
// keyMap.keys = _sortBy(_keys(map), [(value) => priorities[value]]);
|
||||||
|
// return keyMap;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
|
||||||
|
// this.options = options;
|
||||||
|
// this.keys = options.keys;
|
||||||
|
// this.setObjects(objects);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// setObjects(objects: Array<Object>) {
|
||||||
|
// this.keyMap = FuzzyMatcher.valuesToKeyMap(objects, this.keys);
|
||||||
|
// console.log(this.keyMap.keys);
|
||||||
|
// this.matcher = new Levenshtein.Builder()
|
||||||
|
// .dictionary(this.keyMap.keys, true)
|
||||||
|
// .algorithm('transposition')
|
||||||
|
// .sort_candidates(false)
|
||||||
|
// .case_insensitive_sort(true)
|
||||||
|
// .include_distance(true)
|
||||||
|
// .maximum_candidates(this.options.resultCount || DEFAULT_RESULT_COUNT) // result count 0 doesn't make much sense
|
||||||
|
// .build();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// match(query: String): Array<Object> {
|
||||||
|
// const candidates = this.matcher.transduce(query, this.options.distance || DEFAULT_DISTANCE);
|
||||||
|
// // TODO FIXME This is hideous. Clean up when possible.
|
||||||
|
// const val = _sortedUniq(_sortBy(_flatMap(candidates, (candidate) => {
|
||||||
|
// return this.keyMap.objectMap[candidate[0]].map((value) => {
|
||||||
|
// return {
|
||||||
|
// distance: candidate[1],
|
||||||
|
// ...value,
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
// }),
|
||||||
|
// [(candidate) => candidate.distance, (candidate) => this.keyMap.priorityMap[candidate]]));
|
||||||
|
// console.log(val);
|
||||||
|
// return val;
|
||||||
|
// }
|
||||||
|
//}
|
92
src/autocomplete/QueryMatcher.js
Normal file
92
src/autocomplete/QueryMatcher.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
//@flow
|
||||||
|
/*
|
||||||
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
|
||||||
|
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 _at from 'lodash/at';
|
||||||
|
import _flatMap from 'lodash/flatMap';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
import _sortedUniq from 'lodash/sortedUniq';
|
||||||
|
import _keys from 'lodash/keys';
|
||||||
|
|
||||||
|
class KeyMap {
|
||||||
|
keys: Array<String>;
|
||||||
|
objectMap: {[String]: Array<Object>};
|
||||||
|
priorityMap = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class QueryMatcher {
|
||||||
|
/**
|
||||||
|
* @param {object[]} objects the objects to perform a match on
|
||||||
|
* @param {string[]} keys an array of keys within each object to match on
|
||||||
|
* Keys can refer to object properties by name and as in JavaScript (for nested properties)
|
||||||
|
*
|
||||||
|
* To use, simply presort objects by required criteria, run through this function and create a QueryMatcher with the
|
||||||
|
* resulting KeyMap.
|
||||||
|
*
|
||||||
|
* TODO: Handle arrays and objects (Fuse did this, RoomProvider uses it)
|
||||||
|
* @return {KeyMap}
|
||||||
|
*/
|
||||||
|
static valuesToKeyMap(objects: Array<Object>, keys: Array<String>): KeyMap {
|
||||||
|
const keyMap = new KeyMap();
|
||||||
|
const map = {};
|
||||||
|
|
||||||
|
objects.forEach((object, i) => {
|
||||||
|
const keyValues = _at(object, keys);
|
||||||
|
for (const keyValue of keyValues) {
|
||||||
|
if (!map.hasOwnProperty(keyValue)) {
|
||||||
|
map[keyValue] = [];
|
||||||
|
}
|
||||||
|
map[keyValue].push(object);
|
||||||
|
}
|
||||||
|
keyMap.priorityMap.set(object, i);
|
||||||
|
});
|
||||||
|
|
||||||
|
keyMap.objectMap = map;
|
||||||
|
keyMap.keys = _keys(map);
|
||||||
|
return keyMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(objects: Array<Object>, options: {[Object]: Object} = {}) {
|
||||||
|
this.options = options;
|
||||||
|
this.keys = options.keys;
|
||||||
|
this.setObjects(objects);
|
||||||
|
|
||||||
|
// By default, we remove any non-alphanumeric characters ([^A-Za-z0-9_]) from the
|
||||||
|
// query and the value being queried before matching
|
||||||
|
if (this.options.shouldMatchWordsOnly === undefined) {
|
||||||
|
this.options.shouldMatchWordsOnly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setObjects(objects: Array<Object>) {
|
||||||
|
this.keyMap = QueryMatcher.valuesToKeyMap(objects, this.keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
match(query: String): Array<Object> {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
if (this.options.shouldMatchWordsOnly) {
|
||||||
|
query = query.replace(/[^\w]/g, '');
|
||||||
|
}
|
||||||
|
const results = _sortedUniq(_sortBy(_flatMap(this.keyMap.keys, (key) => {
|
||||||
|
let resultKey = key.toLowerCase();
|
||||||
|
if (this.options.shouldMatchWordsOnly) {
|
||||||
|
resultKey = resultKey.replace(/[^\w]/g, '');
|
||||||
|
}
|
||||||
|
return resultKey.indexOf(query) !== -1 ? this.keyMap.objectMap[key] : [];
|
||||||
|
}), (candidate) => this.keyMap.priorityMap.get(candidate)));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Fuse from 'fuse.js';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
import {getDisplayAliasForRoom} from '../Rooms';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
@ -30,10 +30,8 @@ let instance = null;
|
||||||
|
|
||||||
export default class RoomProvider extends AutocompleteProvider {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX, {
|
super(ROOM_REGEX);
|
||||||
keys: ['displayName', 'userId'],
|
this.matcher = new FuzzyMatcher([], {
|
||||||
});
|
|
||||||
this.fuse = new Fuse([], {
|
|
||||||
keys: ['name', 'roomId', 'aliases'],
|
keys: ['name', 'roomId', 'aliases'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -46,17 +44,17 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
// the only reason we need to do this is because Fuse only matches on properties
|
// the only reason we need to do this is because Fuse only matches on properties
|
||||||
this.fuse.set(client.getRooms().filter(room => !!room).map(room => {
|
this.matcher.setObjects(client.getRooms().filter(room => !!room && !!getDisplayAliasForRoom(room)).map(room => {
|
||||||
return {
|
return {
|
||||||
room: room,
|
room: room,
|
||||||
name: room.name,
|
name: room.name,
|
||||||
aliases: room.getAliases(),
|
aliases: room.getAliases(),
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
completions = this.fuse.search(command[0]).map(room => {
|
completions = this.matcher.match(command[0]).map(room => {
|
||||||
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
let displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias + ' ',
|
||||||
component: (
|
component: (
|
||||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||||
),
|
),
|
||||||
|
@ -84,8 +82,4 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
{completions}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldForceComplete(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//@flow
|
||||||
/*
|
/*
|
||||||
Copyright 2016 Aviral Dasgupta
|
Copyright 2016 Aviral Dasgupta
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
@ -18,21 +19,27 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Fuse from 'fuse.js';
|
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
|
import _pull from 'lodash/pull';
|
||||||
|
import _sortBy from 'lodash/sortBy';
|
||||||
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
|
import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||||
|
|
||||||
const USER_REGEX = /@\S*/g;
|
const USER_REGEX = /@\S*/g;
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
export default class UserProvider extends AutocompleteProvider {
|
export default class UserProvider extends AutocompleteProvider {
|
||||||
|
users: Array<RoomMember> = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(USER_REGEX, {
|
super(USER_REGEX, {
|
||||||
keys: ['name', 'userId'],
|
keys: ['name', 'userId'],
|
||||||
});
|
});
|
||||||
this.users = [];
|
this.matcher = new FuzzyMatcher([], {
|
||||||
this.fuse = new Fuse([], {
|
|
||||||
keys: ['name', 'userId'],
|
keys: ['name', 'userId'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -43,8 +50,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection, force);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
this.fuse.set(this.users);
|
completions = this.matcher.match(command[0]).map(user => {
|
||||||
completions = this.fuse.search(command[0]).map(user => {
|
|
||||||
let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done
|
||||||
let completion = displayName;
|
let completion = displayName;
|
||||||
if (range.start === 0) {
|
if (range.start === 0) {
|
||||||
|
@ -71,8 +77,31 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return '👥 ' + _t('Users');
|
return '👥 ' + _t('Users');
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserList(users) {
|
setUserListFromRoom(room: Room) {
|
||||||
this.users = users;
|
const events = room.getLiveTimeline().getEvents();
|
||||||
|
const lastSpoken = {};
|
||||||
|
|
||||||
|
for(const event of events) {
|
||||||
|
lastSpoken[event.getSender()] = event.getTs();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
|
this.users = room.getJoinedMembers().filter((member) => {
|
||||||
|
if (member.userId !== currentUserId) return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.users = _sortBy(this.users, (user) => 1E20 - lastSpoken[user.userId] || 1E20);
|
||||||
|
|
||||||
|
this.matcher.setObjects(this.users);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserSpoke(user: RoomMember) {
|
||||||
|
if(user.userId === MatrixClientPeg.get().credentials.userId) return;
|
||||||
|
|
||||||
|
// Probably unsafe to compare by reference here?
|
||||||
|
_pull(this.users, user);
|
||||||
|
this.users.splice(0, 0, user);
|
||||||
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): UserProvider {
|
static getInstance(): UserProvider {
|
||||||
|
|
|
@ -47,13 +47,12 @@ import UserProvider from '../../autocomplete/UserProvider';
|
||||||
|
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
import RoomViewStore from '../../stores/RoomViewStore';
|
||||||
|
|
||||||
var DEBUG = false;
|
let DEBUG = false;
|
||||||
|
let debuglog = function() {};
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// using bind means that we get to keep useful line numbers in the console
|
// using bind means that we get to keep useful line numbers in the console
|
||||||
var debuglog = console.log.bind(console);
|
debuglog = console.log.bind(console);
|
||||||
} else {
|
|
||||||
var debuglog = function() {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -113,6 +112,7 @@ module.exports = React.createClass({
|
||||||
callState: null,
|
callState: null,
|
||||||
guestsCanJoin: false,
|
guestsCanJoin: false,
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
|
showApps: false,
|
||||||
|
|
||||||
// error object, as from the matrix client/server API
|
// error object, as from the matrix client/server API
|
||||||
// If we failed to load information about the room,
|
// If we failed to load information about the room,
|
||||||
|
@ -234,10 +234,9 @@ module.exports = React.createClass({
|
||||||
// making it impossible to indicate a newly joined room.
|
// making it impossible to indicate a newly joined room.
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (room) {
|
if (room) {
|
||||||
this._updateAutoComplete(room);
|
|
||||||
this.tabComplete.loadEntries(room);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
unsentMessageError: this._getUnsentMessageError(room),
|
unsentMessageError: this._getUnsentMessageError(room),
|
||||||
|
showApps: this._shouldShowApps(room),
|
||||||
});
|
});
|
||||||
this._onRoomLoaded(room);
|
this._onRoomLoaded(room);
|
||||||
}
|
}
|
||||||
|
@ -275,6 +274,11 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_shouldShowApps: function(room) {
|
||||||
|
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
return appsStateEvents && Object.keys(appsStateEvents.getContent()).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
var call = this._getCallForRoom();
|
var call = this._getCallForRoom();
|
||||||
var callState = call ? call.call_state : "ended";
|
var callState = call ? call.call_state : "ended";
|
||||||
|
@ -455,9 +459,14 @@ module.exports = React.createClass({
|
||||||
this._updateConfCallNotification();
|
this._updateConfCallNotification();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
callState: callState
|
callState: callState,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'appsDrawer':
|
||||||
|
this.setState({
|
||||||
|
showApps: payload.show,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -500,8 +509,7 @@ module.exports = React.createClass({
|
||||||
// and that has probably just changed
|
// and that has probably just changed
|
||||||
if (ev.sender) {
|
if (ev.sender) {
|
||||||
this.tabComplete.onMemberSpoke(ev.sender);
|
this.tabComplete.onMemberSpoke(ev.sender);
|
||||||
// nb. we don't need to update the new autocomplete here since
|
UserProvider.getInstance().onUserSpoke(ev.sender);
|
||||||
// its results are currently ordered purely by search score.
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -524,6 +532,8 @@ module.exports = React.createClass({
|
||||||
this._warnAboutEncryption(room);
|
this._warnAboutEncryption(room);
|
||||||
this._calculatePeekRules(room);
|
this._calculatePeekRules(room);
|
||||||
this._updatePreviewUrlVisibility(room);
|
this._updatePreviewUrlVisibility(room);
|
||||||
|
this.tabComplete.loadEntries(room);
|
||||||
|
UserProvider.getInstance().setUserListFromRoom(room);
|
||||||
},
|
},
|
||||||
|
|
||||||
_warnAboutEncryption: function(room) {
|
_warnAboutEncryption: function(room) {
|
||||||
|
@ -700,7 +710,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// refresh the tab complete list
|
// refresh the tab complete list
|
||||||
this.tabComplete.loadEntries(this.state.room);
|
this.tabComplete.loadEntries(this.state.room);
|
||||||
this._updateAutoComplete(this.state.room);
|
UserProvider.getInstance().setUserListFromRoom(this.state.room);
|
||||||
|
|
||||||
// if we are now a member of the room, where we were not before, that
|
// if we are now a member of the room, where we were not before, that
|
||||||
// means we have finished joining a room we were previously peeking
|
// means we have finished joining a room we were previously peeking
|
||||||
|
@ -1425,14 +1435,6 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_updateAutoComplete: function(room) {
|
|
||||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
|
||||||
const members = room.getJoinedMembers().filter(function(member) {
|
|
||||||
if (member.userId !== myUserId) return true;
|
|
||||||
});
|
|
||||||
UserProvider.getInstance().setUserList(members);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
const RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||||
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
const MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||||
|
@ -1613,11 +1615,13 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var auxPanel = (
|
var auxPanel = (
|
||||||
<AuxPanel ref="auxPanel" room={this.state.room}
|
<AuxPanel ref="auxPanel" room={this.state.room}
|
||||||
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
conferenceHandler={this.props.ConferenceHandler}
|
conferenceHandler={this.props.ConferenceHandler}
|
||||||
draggingFile={this.state.draggingFile}
|
draggingFile={this.state.draggingFile}
|
||||||
displayConfCallNotification={this.state.displayConfCallNotification}
|
displayConfCallNotification={this.state.displayConfCallNotification}
|
||||||
maxHeight={this.state.auxPanelMaxHeight}
|
maxHeight={this.state.auxPanelMaxHeight}
|
||||||
onResize={this.onChildResize} >
|
onResize={this.onChildResize}
|
||||||
|
showApps={this.state.showApps && !this.state.editingRoomSettings} >
|
||||||
{ aux }
|
{ aux }
|
||||||
</AuxPanel>
|
</AuxPanel>
|
||||||
);
|
);
|
||||||
|
@ -1630,8 +1634,14 @@ module.exports = React.createClass({
|
||||||
if (canSpeak) {
|
if (canSpeak) {
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
room={this.state.room}
|
||||||
callState={this.state.callState} tabComplete={this.tabComplete} opacity={ this.props.opacity }/>;
|
onResize={this.onChildResize}
|
||||||
|
uploadFile={this.uploadFile}
|
||||||
|
callState={this.state.callState}
|
||||||
|
tabComplete={this.tabComplete}
|
||||||
|
opacity={ this.props.opacity }
|
||||||
|
showApps={ this.state.showApps }
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Why aren't we storing the term/scope/count in this format
|
// TODO: Why aren't we storing the term/scope/count in this format
|
||||||
|
|
|
@ -93,6 +93,10 @@ const SETTINGS_LABELS = [
|
||||||
id: 'disableMarkdown',
|
id: 'disableMarkdown',
|
||||||
label: 'Disable markdown formatting',
|
label: 'Disable markdown formatting',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'enableSyntaxHighlightLanguageDetection',
|
||||||
|
label: 'Enable automatic language detection for syntax highlighting',
|
||||||
|
},
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
id: 'useFixedWidthFont',
|
id: 'useFixedWidthFont',
|
||||||
|
@ -642,6 +646,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_renderUserInterfaceSettings: function() {
|
_renderUserInterfaceSettings: function() {
|
||||||
|
// TODO: this ought to be a separate component so that we don't need
|
||||||
|
// to rebind the onChange each time we render
|
||||||
|
const onChange = (e) =>
|
||||||
|
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{ _t("User Interface") }</h3>
|
<h3>{ _t("User Interface") }</h3>
|
||||||
|
@ -649,8 +657,21 @@ module.exports = React.createClass({
|
||||||
{ this._renderUrlPreviewSelector() }
|
{ this._renderUrlPreviewSelector() }
|
||||||
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
|
||||||
{ THEMES.map( this._renderThemeSelector ) }
|
{ THEMES.map( this._renderThemeSelector ) }
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{ this._renderLanguageSetting() }
|
{ this._renderLanguageSetting() }
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
102
src/components/views/elements/AppTile.js
Normal file
102
src/components/views/elements/AppTile.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'AppTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
id: React.PropTypes.string.isRequired,
|
||||||
|
url: React.PropTypes.string.isRequired,
|
||||||
|
name: React.PropTypes.string.isRequired,
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEditClick: function() {
|
||||||
|
console.log("Edit widget %s", this.props.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onDeleteClick: function() {
|
||||||
|
console.log("Delete widget %s", this.props.id);
|
||||||
|
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
if (!appsStateEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const appsStateEvent = appsStateEvents.getContent();
|
||||||
|
if (appsStateEvent[this.props.id]) {
|
||||||
|
delete appsStateEvent[this.props.id];
|
||||||
|
MatrixClientPeg.get().sendStateEvent(
|
||||||
|
this.props.room.roomId,
|
||||||
|
'im.vector.modular.widgets',
|
||||||
|
appsStateEvent,
|
||||||
|
'',
|
||||||
|
).then(() => {
|
||||||
|
console.log('Deleted widget');
|
||||||
|
}, (e) => {
|
||||||
|
console.error('Failed to delete widget', e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatAppTileName: function() {
|
||||||
|
let appTileName = "No name";
|
||||||
|
if(this.props.name && this.props.name.trim()) {
|
||||||
|
appTileName = this.props.name.trim();
|
||||||
|
appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
return appTileName;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||||
|
<div className="mx_AppTileMenuBar">
|
||||||
|
{this.formatAppTileName()}
|
||||||
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
|
{/* Edit widget */}
|
||||||
|
{/* <img
|
||||||
|
src="img/edit.svg"
|
||||||
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||||
|
width="8" height="8" alt="Edit"
|
||||||
|
onClick={this._onEditClick}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
{/* Delete widget */}
|
||||||
|
<img src="img/cancel.svg"
|
||||||
|
className="mx_filterFlipColor mx_AppTileMenuBarWidget"
|
||||||
|
width="8" height="8" alt={_t("Cancel")}
|
||||||
|
onClick={this._onDeleteClick}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_AppTileBody">
|
||||||
|
<iframe ref="appFrame" src={this.props.url} allowFullScreen="true"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -69,12 +69,21 @@ class PasswordLogin extends React.Component {
|
||||||
|
|
||||||
onSubmitForm(ev) {
|
onSubmitForm(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||||
this.props.onSubmit(
|
this.props.onSubmit(
|
||||||
this.state.username,
|
'', // XXX: Synapse breaks if you send null here:
|
||||||
this.state.phoneCountry,
|
this.state.phoneCountry,
|
||||||
this.state.phoneNumber,
|
this.state.phoneNumber,
|
||||||
this.state.password,
|
this.state.password,
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onSubmit(
|
||||||
|
this.state.username,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
this.state.password,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUsernameChanged(ev) {
|
onUsernameChanged(ev) {
|
||||||
|
|
|
@ -79,7 +79,7 @@ module.exports = React.createClass({
|
||||||
const content = this.props.mxEvent.getContent();
|
const content = this.props.mxEvent.getContent();
|
||||||
if (content.file !== undefined) {
|
if (content.file !== undefined) {
|
||||||
return this.state.decryptedThumbnailUrl;
|
return this.state.decryptedThumbnailUrl;
|
||||||
} else if (content.info.thumbnail_url) {
|
} else if (content.info && content.info.thumbnail_url) {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -29,6 +29,7 @@ import Modal from '../../../Modal';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
import UserSettingsStore from "../../../UserSettingsStore";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -90,8 +91,19 @@ module.exports = React.createClass({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._unmounted) return;
|
if (this._unmounted) return;
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) {
|
||||||
|
highlight.highlightBlock(blocks[i])
|
||||||
|
} else {
|
||||||
|
// Only syntax highlight if there's a class starting with language-
|
||||||
|
let classes = blocks[i].className.split(/\s+/).filter(function (cl) {
|
||||||
|
return cl.startsWith('language-');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (classes.length != 0) {
|
||||||
highlight.highlightBlock(blocks[i]);
|
highlight.highlightBlock(blocks[i]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
// add event handlers to the 'copy code' buttons
|
// add event handlers to the 'copy code' buttons
|
||||||
|
|
218
src/components/views/rooms/AppsDrawer.js
Normal file
218
src/components/views/rooms/AppsDrawer.js
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
|
import AppTile from '../elements/AppTile';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||||
|
import ScalarMessaging from '../../../ScalarMessaging';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'AppsDrawer',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
apps: this._getApps(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
ScalarMessaging.startListening();
|
||||||
|
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.scalarClient = null;
|
||||||
|
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
|
||||||
|
this.scalarClient = new ScalarAuthClient();
|
||||||
|
this.scalarClient.connect().done(() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
if (this.state.apps && this.state.apps.length < 1) {
|
||||||
|
this.onClickAddWidget();
|
||||||
|
}
|
||||||
|
// TODO -- Handle Scalar errors
|
||||||
|
// },
|
||||||
|
// (err) => {
|
||||||
|
// this.setState({
|
||||||
|
// scalar_error: err,
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
ScalarMessaging.stopListening();
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a URI according to a set of template variables. Variables will be
|
||||||
|
* passed through encodeURIComponent.
|
||||||
|
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||||
|
* @param {Object} variables The key/value pairs to replace the template
|
||||||
|
* variables with. E.g. { "$bar": "baz" }.
|
||||||
|
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||||
|
*/
|
||||||
|
encodeUri: function(pathTemplate, variables) {
|
||||||
|
for (const key in variables) {
|
||||||
|
if (!variables.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
pathTemplate = pathTemplate.replace(
|
||||||
|
key, encodeURIComponent(variables[key]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return pathTemplate;
|
||||||
|
},
|
||||||
|
|
||||||
|
_initAppConfig: function(appId, app) {
|
||||||
|
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||||
|
const params = {
|
||||||
|
'$matrix_user_id': this.props.userId,
|
||||||
|
'$matrix_room_id': this.props.room.roomId,
|
||||||
|
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||||
|
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if(app.data) {
|
||||||
|
Object.keys(app.data).forEach((key) => {
|
||||||
|
params['$' + key] = app.data[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.id = appId;
|
||||||
|
app.name = app.name || app.type;
|
||||||
|
app.url = this.encodeUri(app.url, params);
|
||||||
|
|
||||||
|
// switch(app.type) {
|
||||||
|
// case 'etherpad':
|
||||||
|
// app.queryParams = '?userName=' + this.props.userId +
|
||||||
|
// '&padId=' + this.props.room.roomId;
|
||||||
|
// break;
|
||||||
|
// case 'jitsi': {
|
||||||
|
//
|
||||||
|
// app.queryParams = '?confId=' + app.data.confId +
|
||||||
|
// '&displayName=' + encodeURIComponent(user.displayName) +
|
||||||
|
// '&avatarUrl=' + encodeURIComponent(MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl)) +
|
||||||
|
// '&email=' + encodeURIComponent(this.props.userId) +
|
||||||
|
// '&isAudioConf=' + app.data.isAudioConf;
|
||||||
|
//
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// case 'vrdemo':
|
||||||
|
// app.queryParams = '?roomAlias=' + encodeURIComponent(app.data.roomAlias);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return app;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRoomStateEvents: function(ev, state) {
|
||||||
|
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._updateApps();
|
||||||
|
},
|
||||||
|
|
||||||
|
_getApps: function() {
|
||||||
|
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
if (!appsStateEvents) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const appsStateEvent = appsStateEvents.getContent();
|
||||||
|
if (Object.keys(appsStateEvent).length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(appsStateEvent).map((appId) => {
|
||||||
|
return this._initAppConfig(appId, appsStateEvent[appId]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateApps: function() {
|
||||||
|
const apps = this._getApps();
|
||||||
|
if (apps.length < 1) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
apps: apps,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickAddWidget: function(e) {
|
||||||
|
if (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
|
||||||
|
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
|
||||||
|
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
|
||||||
|
null;
|
||||||
|
Modal.createDialog(IntegrationsManager, {
|
||||||
|
src: src,
|
||||||
|
}, "mx_IntegrationsManager");
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const apps = this.state.apps.map(
|
||||||
|
(app, index, arr) => {
|
||||||
|
return <AppTile
|
||||||
|
key={app.name}
|
||||||
|
id={app.id}
|
||||||
|
url={app.url}
|
||||||
|
name={app.name}
|
||||||
|
fullWidth={arr.length<2 ? true : false}
|
||||||
|
room={this.props.room}
|
||||||
|
userId={this.props.userId}
|
||||||
|
/>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addWidget = this.state.apps && this.state.apps.length < 2 &&
|
||||||
|
(<div onClick={this.onClickAddWidget}
|
||||||
|
role="button"
|
||||||
|
tabIndex="0"
|
||||||
|
className="mx_AddWidget_button"
|
||||||
|
title={_t('Add a widget')}>
|
||||||
|
[+] {_t('Add a widget')}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_AppsDrawer">
|
||||||
|
<div id="apps" className="mx_AppsContainer">
|
||||||
|
{apps}
|
||||||
|
</div>
|
||||||
|
{addWidget}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import isEqual from 'lodash/isEqual';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import type {Completion} from '../../../autocomplete/Autocompleter';
|
import type {Completion} from '../../../autocomplete/Autocompleter';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
|
@ -39,26 +40,52 @@ export default class Autocomplete extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentWillReceiveProps(props, state) {
|
componentWillReceiveProps(newProps, state) {
|
||||||
if (props.query === this.props.query) {
|
// Query hasn't changed so don't try to complete it
|
||||||
return null;
|
if (newProps.query === this.props.query) {
|
||||||
}
|
|
||||||
|
|
||||||
return await this.complete(props.query, props.selection);
|
|
||||||
}
|
|
||||||
|
|
||||||
async complete(query, selection) {
|
|
||||||
let forceComplete = this.state.forceComplete;
|
|
||||||
const completionPromise = getCompletions(query, selection, forceComplete);
|
|
||||||
this.completionPromise = completionPromise;
|
|
||||||
const completions = await this.completionPromise;
|
|
||||||
|
|
||||||
// There's a newer completion request, so ignore results.
|
|
||||||
if (completionPromise !== this.completionPromise) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const completionList = flatMap(completions, provider => provider.completions);
|
this.complete(newProps.query, newProps.selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(query, selection) {
|
||||||
|
if (this.debounceCompletionsRequest) {
|
||||||
|
clearTimeout(this.debounceCompletionsRequest);
|
||||||
|
}
|
||||||
|
if (query === "") {
|
||||||
|
this.setState({
|
||||||
|
// Clear displayed completions
|
||||||
|
completions: [],
|
||||||
|
completionList: [],
|
||||||
|
// Reset selected completion
|
||||||
|
selectionOffset: COMPOSER_SELECTED,
|
||||||
|
// Hide the autocomplete box
|
||||||
|
hide: true,
|
||||||
|
});
|
||||||
|
return Q(null);
|
||||||
|
}
|
||||||
|
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200);
|
||||||
|
|
||||||
|
// Don't debounce if we are already showing completions
|
||||||
|
if (this.state.completions.length > 0) {
|
||||||
|
autocompleteDelay = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deferred = Q.defer();
|
||||||
|
this.debounceCompletionsRequest = setTimeout(() => {
|
||||||
|
getCompletions(
|
||||||
|
query, selection, this.state.forceComplete,
|
||||||
|
).then((completions) => {
|
||||||
|
this.processCompletions(completions);
|
||||||
|
deferred.resolve();
|
||||||
|
});
|
||||||
|
}, autocompleteDelay);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
processCompletions(completions) {
|
||||||
|
const completionList = flatMap(completions, (provider) => provider.completions);
|
||||||
|
|
||||||
// Reset selection when completion list becomes empty.
|
// Reset selection when completion list becomes empty.
|
||||||
let selectionOffset = COMPOSER_SELECTED;
|
let selectionOffset = COMPOSER_SELECTED;
|
||||||
|
@ -69,21 +96,18 @@ export default class Autocomplete extends React.Component {
|
||||||
const currentSelection = this.state.selectionOffset === 0 ? null :
|
const currentSelection = this.state.selectionOffset === 0 ? null :
|
||||||
this.state.completionList[this.state.selectionOffset - 1].completion;
|
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||||
selectionOffset = completionList.findIndex(
|
selectionOffset = completionList.findIndex(
|
||||||
completion => completion.completion === currentSelection);
|
(completion) => completion.completion === currentSelection);
|
||||||
if (selectionOffset === -1) {
|
if (selectionOffset === -1) {
|
||||||
selectionOffset = COMPOSER_SELECTED;
|
selectionOffset = COMPOSER_SELECTED;
|
||||||
} else {
|
} else {
|
||||||
selectionOffset++; // selectionOffset is 1-indexed!
|
selectionOffset++; // selectionOffset is 1-indexed!
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If no completions were returned, we should turn off force completion.
|
|
||||||
forceComplete = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hide = this.state.hide;
|
let hide = this.state.hide;
|
||||||
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
|
// These are lists of booleans that indicate whether whether the corresponding provider had a matching pattern
|
||||||
const oldMatches = this.state.completions.map(completion => !!completion.command.command),
|
const oldMatches = this.state.completions.map((completion) => !!completion.command.command),
|
||||||
newMatches = completions.map(completion => !!completion.command.command);
|
newMatches = completions.map((completion) => !!completion.command.command);
|
||||||
|
|
||||||
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
|
// So, essentially, we re-show autocomplete if any provider finds a new pattern or stops finding an old one
|
||||||
if (!isEqual(oldMatches, newMatches)) {
|
if (!isEqual(oldMatches, newMatches)) {
|
||||||
|
@ -95,7 +119,8 @@ export default class Autocomplete extends React.Component {
|
||||||
completionList,
|
completionList,
|
||||||
selectionOffset,
|
selectionOffset,
|
||||||
hide,
|
hide,
|
||||||
forceComplete,
|
// Force complete is turned off each time since we can't edit the query in that case
|
||||||
|
forceComplete: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +174,7 @@ export default class Autocomplete extends React.Component {
|
||||||
const done = Q.defer();
|
const done = Q.defer();
|
||||||
this.setState({
|
this.setState({
|
||||||
forceComplete: true,
|
forceComplete: true,
|
||||||
|
hide: false,
|
||||||
}, () => {
|
}, () => {
|
||||||
this.complete(this.props.query, this.props.selection).then(() => {
|
this.complete(this.props.query, this.props.selection).then(() => {
|
||||||
done.resolve();
|
done.resolve();
|
||||||
|
@ -169,7 +195,7 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelection(selectionOffset: number) {
|
setSelection(selectionOffset: number) {
|
||||||
this.setState({selectionOffset});
|
this.setState({selectionOffset, hide: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -185,21 +211,24 @@ export default class Autocomplete extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(state, func) {
|
||||||
|
super.setState(state, func);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let position = 1;
|
let position = 1;
|
||||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
let completions = completionResult.completions.map((completion, i) => {
|
const completions = completionResult.completions.map((completion, i) => {
|
||||||
|
|
||||||
const className = classNames('mx_Autocomplete_Completion', {
|
const className = classNames('mx_Autocomplete_Completion', {
|
||||||
'selected': position === this.state.selectionOffset,
|
'selected': position === this.state.selectionOffset,
|
||||||
});
|
});
|
||||||
let componentPosition = position;
|
const componentPosition = position;
|
||||||
position++;
|
position++;
|
||||||
|
|
||||||
let onMouseOver = () => this.setSelection(componentPosition);
|
const onMouseOver = () => this.setSelection(componentPosition);
|
||||||
let onClick = () => {
|
const onClick = () => {
|
||||||
this.setSelection(componentPosition);
|
this.setSelection(componentPosition);
|
||||||
this.onCompletionClicked();
|
this.onCompletionClicked();
|
||||||
};
|
};
|
||||||
|
@ -220,7 +249,7 @@ export default class Autocomplete extends React.Component {
|
||||||
{completionResult.provider.renderCompletions(completions)}
|
{completionResult.provider.renderCompletions(completions)}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}).filter(completion => !!completion);
|
}).filter((completion) => !!completion);
|
||||||
|
|
||||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||||
|
|
|
@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
import ObjectUtils from '../../../ObjectUtils';
|
import ObjectUtils from '../../../ObjectUtils';
|
||||||
|
import AppsDrawer from './AppsDrawer';
|
||||||
import { _t, _tJsx} from '../../../languageHandler';
|
import { _t, _tJsx} from '../../../languageHandler';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -28,6 +30,8 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// js-sdk room object
|
// js-sdk room object
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
userId: React.PropTypes.string.isRequired,
|
||||||
|
showApps: React.PropTypes.bool,
|
||||||
|
|
||||||
// Conference Handler implementation
|
// Conference Handler implementation
|
||||||
conferenceHandler: React.PropTypes.object,
|
conferenceHandler: React.PropTypes.object,
|
||||||
|
@ -70,10 +74,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var CallView = sdk.getComponent("voip.CallView");
|
const CallView = sdk.getComponent("voip.CallView");
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
var fileDropTarget = null;
|
let fileDropTarget = null;
|
||||||
if (this.props.draggingFile) {
|
if (this.props.draggingFile) {
|
||||||
fileDropTarget = (
|
fileDropTarget = (
|
||||||
<div className="mx_RoomView_fileDropTarget">
|
<div className="mx_RoomView_fileDropTarget">
|
||||||
|
@ -87,14 +91,13 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var conferenceCallNotification = null;
|
let conferenceCallNotification = null;
|
||||||
if (this.props.displayConfCallNotification) {
|
if (this.props.displayConfCallNotification) {
|
||||||
let supportedText = '';
|
let supportedText = '';
|
||||||
let joinNode;
|
let joinNode;
|
||||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||||
supportedText = _t(" (unsupported)");
|
supportedText = _t(" (unsupported)");
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
joinNode = (<span>
|
joinNode = (<span>
|
||||||
{_tJsx(
|
{_tJsx(
|
||||||
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
|
||||||
|
@ -105,7 +108,6 @@ module.exports = React.createClass({
|
||||||
]
|
]
|
||||||
)}
|
)}
|
||||||
</span>);
|
</span>);
|
||||||
|
|
||||||
}
|
}
|
||||||
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
|
||||||
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
// but there are translations for this in the languages we do have so I'm leaving it for now.
|
||||||
|
@ -118,7 +120,7 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
var callView = (
|
const callView = (
|
||||||
<CallView ref="callView" room={this.props.room}
|
<CallView ref="callView" room={this.props.room}
|
||||||
ConferenceHandler={this.props.conferenceHandler}
|
ConferenceHandler={this.props.conferenceHandler}
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
|
@ -126,8 +128,17 @@ module.exports = React.createClass({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let appsDrawer = null;
|
||||||
|
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
|
||||||
|
appsDrawer = <AppsDrawer ref="appsDrawer"
|
||||||
|
room={this.props.room}
|
||||||
|
userId={this.props.userId}
|
||||||
|
maxHeight={this.props.maxHeight}/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
|
||||||
|
{ appsDrawer }
|
||||||
{ fileDropTarget }
|
{ fileDropTarget }
|
||||||
{ callView }
|
{ callView }
|
||||||
{ conferenceCallNotification }
|
{ conferenceCallNotification }
|
||||||
|
|
|
@ -13,16 +13,15 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
var CallHandler = require('../../../CallHandler');
|
import CallHandler from '../../../CallHandler';
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
var Modal = require('../../../Modal');
|
import Modal from '../../../Modal';
|
||||||
var sdk = require('../../../index');
|
import sdk from '../../../index';
|
||||||
var dis = require('../../../dispatcher');
|
import dis from '../../../dispatcher';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +31,8 @@ export default class MessageComposer extends React.Component {
|
||||||
this.onCallClick = this.onCallClick.bind(this);
|
this.onCallClick = this.onCallClick.bind(this);
|
||||||
this.onHangupClick = this.onHangupClick.bind(this);
|
this.onHangupClick = this.onHangupClick.bind(this);
|
||||||
this.onUploadClick = this.onUploadClick.bind(this);
|
this.onUploadClick = this.onUploadClick.bind(this);
|
||||||
|
this.onShowAppsClick = this.onShowAppsClick.bind(this);
|
||||||
|
this.onHideAppsClick = this.onHideAppsClick.bind(this);
|
||||||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||||
this.uploadFiles = this.uploadFiles.bind(this);
|
this.uploadFiles = this.uploadFiles.bind(this);
|
||||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||||
|
@ -57,7 +58,6 @@ export default class MessageComposer extends React.Component {
|
||||||
},
|
},
|
||||||
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -127,7 +127,7 @@ export default class MessageComposer extends React.Component {
|
||||||
if(shouldUpload) {
|
if(shouldUpload) {
|
||||||
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
// MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file
|
||||||
if (files) {
|
if (files) {
|
||||||
for(var i=0; i<files.length; i++) {
|
for(let i=0; i<files.length; i++) {
|
||||||
this.props.uploadFile(files[i]);
|
this.props.uploadFile(files[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onHangupClick() {
|
onHangupClick() {
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
const call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||||
//var call = CallHandler.getAnyActiveCall();
|
//var call = CallHandler.getAnyActiveCall();
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return;
|
return;
|
||||||
|
@ -152,20 +152,68 @@ export default class MessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// _startCallApp(isAudioConf) {
|
||||||
|
// dis.dispatch({
|
||||||
|
// action: 'appsDrawer',
|
||||||
|
// show: true,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||||
|
// let appsStateEvent = {};
|
||||||
|
// if (appsStateEvents) {
|
||||||
|
// appsStateEvent = appsStateEvents.getContent();
|
||||||
|
// }
|
||||||
|
// if (!appsStateEvent.videoConf) {
|
||||||
|
// appsStateEvent.videoConf = {
|
||||||
|
// type: 'jitsi',
|
||||||
|
// // FIXME -- This should not be localhost
|
||||||
|
// url: 'http://localhost:8000/jitsi.html',
|
||||||
|
// data: {
|
||||||
|
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||||
|
// isAudioConf: isAudioConf,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// MatrixClientPeg.get().sendStateEvent(
|
||||||
|
// this.props.room.roomId,
|
||||||
|
// 'im.vector.modular.widgets',
|
||||||
|
// appsStateEvent,
|
||||||
|
// '',
|
||||||
|
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
onCallClick(ev) {
|
onCallClick(ev) {
|
||||||
|
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: ev.shiftKey ? "screensharing" : "video",
|
type: ev.shiftKey ? "screensharing" : "video",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
});
|
});
|
||||||
|
// this._startCallApp(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoiceCallClick(ev) {
|
onVoiceCallClick(ev) {
|
||||||
|
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: 'voice',
|
type: "voice",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
});
|
});
|
||||||
|
// this._startCallApp(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowAppsClick(ev) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onHideAppsClick(ev) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'appsDrawer',
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||||
|
@ -216,19 +264,19 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
var uploadInputStyle = {display: 'none'};
|
const uploadInputStyle = {display: 'none'};
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" +
|
||||||
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
(UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old"));
|
||||||
|
|
||||||
var controls = [];
|
const controls = [];
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||||
<MemberAvatar member={me} width={24} height={24} />
|
<MemberAvatar member={me} width={24} height={24} />
|
||||||
</div>
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
let e2eImg, e2eTitle, e2eClass;
|
let e2eImg, e2eTitle, e2eClass;
|
||||||
|
@ -247,16 +295,15 @@ export default class MessageComposer extends React.Component {
|
||||||
controls.push(
|
controls.push(
|
||||||
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
<img key="e2eIcon" className={e2eClass} src={e2eImg} width="12" height="12"
|
||||||
alt={e2eTitle} title={e2eTitle}
|
alt={e2eTitle} title={e2eTitle}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
var callButton, videoCallButton, hangupButton;
|
let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton;
|
||||||
if (this.props.callState && this.props.callState !== 'ended') {
|
if (this.props.callState && this.props.callState !== 'ended') {
|
||||||
hangupButton =
|
hangupButton =
|
||||||
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
|
||||||
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
<img src="img/hangup.svg" alt={ _t('Hangup') } title={ _t('Hangup') } width="25" height="26"/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
callButton =
|
callButton =
|
||||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={ _t('Voice call') }>
|
||||||
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
<TintableSvg src="img/icon-call.svg" width="35" height="35"/>
|
||||||
|
@ -267,14 +314,29 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
// Apps
|
||||||
|
if (UserSettingsStore.isFeatureEnabled('matrix_apps')) {
|
||||||
|
if (this.props.showApps) {
|
||||||
|
hideAppsButton =
|
||||||
|
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
|
||||||
|
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
showAppsButton =
|
||||||
|
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
|
||||||
|
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSendMessages = this.props.room.currentState.maySendMessage(
|
||||||
MatrixClientPeg.get().credentials.userId);
|
MatrixClientPeg.get().credentials.userId);
|
||||||
|
|
||||||
if (canSendMessages) {
|
if (canSendMessages) {
|
||||||
// This also currently includes the call buttons. Really we should
|
// This also currently includes the call buttons. Really we should
|
||||||
// check separately for whether we can call, but this is slightly
|
// check separately for whether we can call, but this is slightly
|
||||||
// complex because of conference calls.
|
// complex because of conference calls.
|
||||||
var uploadButton = (
|
const uploadButton = (
|
||||||
<div key="controls_upload" className="mx_MessageComposer_upload"
|
<div key="controls_upload" className="mx_MessageComposer_upload"
|
||||||
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
onClick={this.onUploadClick} title={ _t('Upload file') }>
|
||||||
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
<TintableSvg src="img/icons-upload.svg" width="35" height="35"/>
|
||||||
|
@ -300,7 +362,7 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<MessageComposerInput
|
<MessageComposerInput
|
||||||
ref={c => this.messageComposerInput = c}
|
ref={(c) => this.messageComposerInput = c}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
@ -316,13 +378,15 @@ export default class MessageComposer extends React.Component {
|
||||||
uploadButton,
|
uploadButton,
|
||||||
hangupButton,
|
hangupButton,
|
||||||
callButton,
|
callButton,
|
||||||
videoCallButton
|
videoCallButton,
|
||||||
|
showAppsButton,
|
||||||
|
hideAppsButton,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.push(
|
controls.push(
|
||||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||||
{ _t('You do not have permission to post to this room') }
|
{ _t('You do not have permission to post to this room') }
|
||||||
</div>
|
</div>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,7 +404,7 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
const {style, blockType} = this.state.inputState;
|
const {style, blockType} = this.state.inputState;
|
||||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||||
name => {
|
(name) => {
|
||||||
const active = style.includes(name) || blockType === name;
|
const active = style.includes(name) || blockType === name;
|
||||||
const suffix = active ? '-o-n' : '';
|
const suffix = active ? '-o-n' : '';
|
||||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||||
|
@ -403,5 +467,8 @@ MessageComposer.propTypes = {
|
||||||
uploadFile: React.PropTypes.func.isRequired,
|
uploadFile: React.PropTypes.func.isRequired,
|
||||||
|
|
||||||
// opacity for dynamic UI fading effects
|
// opacity for dynamic UI fading effects
|
||||||
opacity: React.PropTypes.number
|
opacity: React.PropTypes.number,
|
||||||
|
|
||||||
|
// string representing the current room app drawer state
|
||||||
|
showApps: React.PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||||
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
||||||
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';
|
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';
|
||||||
|
|
||||||
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import escape from 'lodash/escape';
|
import escape from 'lodash/escape';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
@ -41,6 +40,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
import {Completion} from "../../../autocomplete/Autocompleter";
|
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||||
import Markdown from '../../../Markdown';
|
import Markdown from '../../../Markdown';
|
||||||
|
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||||
import {onSendMessageFailed} from './MessageComposerInputOld';
|
import {onSendMessageFailed} from './MessageComposerInputOld';
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
@ -58,12 +58,42 @@ function stateToMarkdown(state) {
|
||||||
* The textInput part of the MessageComposer
|
* The textInput part of the MessageComposer
|
||||||
*/
|
*/
|
||||||
export default class MessageComposerInput extends React.Component {
|
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,
|
||||||
|
|
||||||
|
// js-sdk Room object
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
|
// called with current plaintext content (as a string) whenever it changes
|
||||||
|
onContentChanged: React.PropTypes.func,
|
||||||
|
|
||||||
|
onUpArrow: React.PropTypes.func,
|
||||||
|
|
||||||
|
onDownArrow: React.PropTypes.func,
|
||||||
|
|
||||||
|
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||||
|
tryComplete: React.PropTypes.func,
|
||||||
|
|
||||||
|
onInputStateChanged: React.PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||||
// C-m => Toggles between rich text and markdown modes
|
// C-m => Toggles between rich text and markdown modes
|
||||||
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||||
return 'toggle-mode';
|
return 'toggle-mode';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
|
||||||
|
if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
|
||||||
|
// When null is returned, draft-js will NOT preventDefault, allowing dev tools
|
||||||
|
// to be toggled when the editor is focussed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return getDefaultKeyBinding(e);
|
return getDefaultKeyBinding(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +107,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
autocomplete: Autocomplete;
|
autocomplete: Autocomplete;
|
||||||
|
historyManager: ComposerHistoryManager;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
@ -84,13 +115,13 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.handleReturn = this.handleReturn.bind(this);
|
this.handleReturn = this.handleReturn.bind(this);
|
||||||
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
this.handleKeyCommand = this.handleKeyCommand.bind(this);
|
||||||
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
this.onEditorContentChanged = this.onEditorContentChanged.bind(this);
|
||||||
this.setEditorState = this.setEditorState.bind(this);
|
|
||||||
this.onUpArrow = this.onUpArrow.bind(this);
|
this.onUpArrow = this.onUpArrow.bind(this);
|
||||||
this.onDownArrow = this.onDownArrow.bind(this);
|
this.onDownArrow = this.onDownArrow.bind(this);
|
||||||
this.onTab = this.onTab.bind(this);
|
this.onTab = this.onTab.bind(this);
|
||||||
this.onEscape = this.onEscape.bind(this);
|
this.onEscape = this.onEscape.bind(this);
|
||||||
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
|
this.onTextPasted = this.onTextPasted.bind(this);
|
||||||
|
|
||||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false);
|
||||||
|
|
||||||
|
@ -103,6 +134,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// the original editor state, before we started tabbing through completions
|
// the original editor state, before we started tabbing through completions
|
||||||
originalEditorState: null,
|
originalEditorState: null,
|
||||||
|
|
||||||
|
// the virtual state "above" the history stack, the message currently being composed that
|
||||||
|
// we want to persist whilst browsing history
|
||||||
|
currentlyComposedEditorState: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
||||||
|
@ -132,110 +167,13 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return EditorState.moveFocusToEnd(editorState);
|
return EditorState.moveFocusToEnd(editorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
const component = 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(
|
|
||||||
"mx_messagecomposer_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(
|
|
||||||
"mx_messagecomposer_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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!
|
|
||||||
let contentJSON = JSON.stringify(convertToRaw(component.state.editorState.getCurrentContent()));
|
|
||||||
window.sessionStorage.setItem("mx_messagecomposer_input_" + this.roomId, contentJSON);
|
|
||||||
},
|
|
||||||
|
|
||||||
setLastTextEntry: function() {
|
|
||||||
let contentJSON = window.sessionStorage.getItem("mx_messagecomposer_input_" + this.roomId);
|
|
||||||
if (contentJSON) {
|
|
||||||
let content = convertFromRaw(JSON.parse(contentJSON));
|
|
||||||
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.sentHistory.init(
|
this.historyManager = new ComposerHistoryManager(this.props.room.roomId);
|
||||||
this.refs.editor,
|
|
||||||
this.props.room.roomId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this.sentHistory.saveLastTextEntry();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUpdate(nextProps, nextState) {
|
componentWillUpdate(nextProps, nextState) {
|
||||||
|
@ -247,8 +185,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction(payload) {
|
onAction = (payload) => {
|
||||||
let editor = this.refs.editor;
|
const editor = this.refs.editor;
|
||||||
let contentState = this.state.editorState.getCurrentContent();
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
|
@ -262,7 +200,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
contentState = Modifier.replaceText(
|
contentState = Modifier.replaceText(
|
||||||
contentState,
|
contentState,
|
||||||
this.state.editorState.getSelection(),
|
this.state.editorState.getSelection(),
|
||||||
`${payload.displayname}: `
|
`${payload.displayname}: `,
|
||||||
);
|
);
|
||||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||||
|
@ -275,9 +213,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let {body, formatted_body} = payload.event.getContent();
|
let {body, formatted_body} = payload.event.getContent();
|
||||||
formatted_body = formatted_body || escape(body);
|
formatted_body = formatted_body || escape(body);
|
||||||
if (formatted_body) {
|
if (formatted_body) {
|
||||||
let content = RichText.HTMLtoContentState(`<blockquote>${formatted_body}</blockquote>`);
|
let content = RichText.htmlToContentState(`<blockquote>${formatted_body}</blockquote>`);
|
||||||
if (!this.state.isRichtextEnabled) {
|
if (!this.state.isRichtextEnabled) {
|
||||||
content = ContentState.createFromText(stateToMarkdown(content));
|
content = ContentState.createFromText(RichText.stateToMarkdown(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockMap = content.getBlockMap();
|
const blockMap = content.getBlockMap();
|
||||||
|
@ -292,13 +230,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
||||||
}
|
}
|
||||||
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
|
editorState = EditorState.moveSelectionToEnd(editorState);
|
||||||
this.onEditorContentChanged(editorState);
|
this.onEditorContentChanged(editorState);
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onTypingActivity() {
|
onTypingActivity() {
|
||||||
this.isTyping = true;
|
this.isTyping = true;
|
||||||
|
@ -318,7 +257,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
startUserTypingTimer() {
|
startUserTypingTimer() {
|
||||||
this.stopUserTypingTimer();
|
this.stopUserTypingTimer();
|
||||||
var self = this;
|
const self = this;
|
||||||
this.userTypingTimer = setTimeout(function() {
|
this.userTypingTimer = setTimeout(function() {
|
||||||
self.isTyping = false;
|
self.isTyping = false;
|
||||||
self.sendTyping(self.isTyping);
|
self.sendTyping(self.isTyping);
|
||||||
|
@ -335,7 +274,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
startServerTypingTimer() {
|
startServerTypingTimer() {
|
||||||
if (!this.serverTypingTimer) {
|
if (!this.serverTypingTimer) {
|
||||||
var self = this;
|
const self = this;
|
||||||
this.serverTypingTimer = setTimeout(function() {
|
this.serverTypingTimer = setTimeout(function() {
|
||||||
if (self.isTyping) {
|
if (self.isTyping) {
|
||||||
self.sendTyping(self.isTyping);
|
self.sendTyping(self.isTyping);
|
||||||
|
@ -356,7 +295,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return;
|
||||||
MatrixClientPeg.get().sendTyping(
|
MatrixClientPeg.get().sendTyping(
|
||||||
this.props.room.roomId,
|
this.props.room.roomId,
|
||||||
this.isTyping, TYPING_SERVER_TIMEOUT
|
this.isTyping, TYPING_SERVER_TIMEOUT,
|
||||||
).done();
|
).done();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,60 +306,80 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called by Draft to change editor contents, and by setEditorState
|
// Called by Draft to change editor contents
|
||||||
onEditorContentChanged(editorState: EditorState, didRespondToUserInput: boolean = true) {
|
onEditorContentChanged = (editorState: EditorState) => {
|
||||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||||
|
|
||||||
const contentChanged = Q.defer();
|
/* Since a modification was made, set originalEditorState to null, since newState is now our original */
|
||||||
/* If a modification was made, set originalEditorState to null, since newState is now our original */
|
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState,
|
editorState,
|
||||||
originalEditorState: didRespondToUserInput ? null : this.state.originalEditorState,
|
originalEditorState: null,
|
||||||
}, () => contentChanged.resolve());
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (editorState.getCurrentContent().hasText()) {
|
/**
|
||||||
|
* We're overriding setState here because it's the most convenient way to monitor changes to the editorState.
|
||||||
|
* Doing it using a separate function that calls setState is a possibility (and was the old approach), but that
|
||||||
|
* approach requires a callback and an extra setState whenever trying to set multiple state properties.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
setState(state, callback) {
|
||||||
|
if (state.editorState != null) {
|
||||||
|
state.editorState = RichText.attachImmutableEntitiesToEmoji(
|
||||||
|
state.editorState);
|
||||||
|
|
||||||
|
if (state.editorState.getCurrentContent().hasText()) {
|
||||||
this.onTypingActivity();
|
this.onTypingActivity();
|
||||||
} else {
|
} else {
|
||||||
this.onFinishedTyping();
|
this.onFinishedTyping();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onContentChanged) {
|
if (!state.hasOwnProperty('originalEditorState')) {
|
||||||
const textContent = editorState.getCurrentContent().getPlainText();
|
state.originalEditorState = null;
|
||||||
const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
}
|
||||||
editorState.getCurrentContent().getBlocksAsArray());
|
}
|
||||||
|
|
||||||
|
super.setState(state, () => {
|
||||||
|
if (callback != null) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onContentChanged) {
|
||||||
|
const textContent = this.state.editorState
|
||||||
|
.getCurrentContent().getPlainText();
|
||||||
|
const selection = RichText.selectionStateToTextOffsets(
|
||||||
|
this.state.editorState.getSelection(),
|
||||||
|
this.state.editorState.getCurrentContent().getBlocksAsArray());
|
||||||
this.props.onContentChanged(textContent, selection);
|
this.props.onContentChanged(textContent, selection);
|
||||||
}
|
}
|
||||||
return contentChanged.promise;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
setEditorState(editorState: EditorState) {
|
|
||||||
return this.onEditorContentChanged(editorState, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enableRichtext(enabled: boolean) {
|
enableRichtext(enabled: boolean) {
|
||||||
|
if (enabled === this.state.isRichtextEnabled) return;
|
||||||
|
|
||||||
let contentState = null;
|
let contentState = null;
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText());
|
const md = new Markdown(this.state.editorState.getCurrentContent().getPlainText());
|
||||||
contentState = RichText.HTMLtoContentState(md.toHTML());
|
contentState = RichText.htmlToContentState(md.toHTML());
|
||||||
} else {
|
} else {
|
||||||
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent());
|
let markdown = RichText.stateToMarkdown(this.state.editorState.getCurrentContent());
|
||||||
if (markdown[markdown.length - 1] === '\n') {
|
if (markdown[markdown.length - 1] === '\n') {
|
||||||
markdown = markdown.substring(0, markdown.length - 1); // stateToMarkdown tacks on an extra newline (?!?)
|
markdown = markdown.substring(0, markdown.length - 1); // stateToMarkdown tacks on an extra newline (?!?)
|
||||||
}
|
}
|
||||||
contentState = ContentState.createFromText(markdown);
|
contentState = ContentState.createFromText(markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setEditorState(this.createEditorState(enabled, contentState)).then(() => {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
editorState: this.createEditorState(enabled, contentState),
|
||||||
isRichtextEnabled: enabled,
|
isRichtextEnabled: enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyCommand(command: string): boolean {
|
handleKeyCommand = (command: string): boolean => {
|
||||||
if (command === 'toggle-mode') {
|
if (command === 'toggle-mode') {
|
||||||
this.enableRichtext(!this.state.isRichtextEnabled);
|
this.enableRichtext(!this.state.isRichtextEnabled);
|
||||||
return true;
|
return true;
|
||||||
|
@ -434,31 +393,35 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'];
|
const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'];
|
||||||
|
|
||||||
if (blockCommands.includes(command)) {
|
if (blockCommands.includes(command)) {
|
||||||
this.setEditorState(RichUtils.toggleBlockType(this.state.editorState, command));
|
this.setState({
|
||||||
|
editorState: RichUtils.toggleBlockType(this.state.editorState, command),
|
||||||
|
});
|
||||||
} else if (command === 'strike') {
|
} else if (command === 'strike') {
|
||||||
// this is the only inline style not handled by Draft by default
|
// this is the only inline style not handled by Draft by default
|
||||||
this.setEditorState(RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'));
|
this.setState({
|
||||||
|
editorState: RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let contentState = this.state.editorState.getCurrentContent(),
|
let contentState = this.state.editorState.getCurrentContent(),
|
||||||
selection = this.state.editorState.getSelection();
|
selection = this.state.editorState.getSelection();
|
||||||
|
|
||||||
let modifyFn = {
|
const modifyFn = {
|
||||||
'bold': text => `**${text}**`,
|
'bold': (text) => `**${text}**`,
|
||||||
'italic': text => `*${text}*`,
|
'italic': (text) => `*${text}*`,
|
||||||
'underline': text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||||
'strike': text => `~~${text}~~`,
|
'strike': (text) => `<del>${text}</del>`,
|
||||||
'code': text => `\`${text}\``,
|
'code-block': (text) => `\`\`\`\n${text}\n\`\`\``,
|
||||||
'blockquote': text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''),
|
||||||
'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''),
|
'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''),
|
||||||
'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''),
|
'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''),
|
||||||
}[command];
|
}[command];
|
||||||
|
|
||||||
if (modifyFn) {
|
if (modifyFn) {
|
||||||
newState = EditorState.push(
|
newState = EditorState.push(
|
||||||
this.state.editorState,
|
this.state.editorState,
|
||||||
RichText.modifyText(contentState, selection, modifyFn),
|
RichText.modifyText(contentState, selection, modifyFn),
|
||||||
'insert-characters'
|
'insert-characters',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -468,19 +431,49 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newState != null) {
|
if (newState != null) {
|
||||||
this.setEditorState(newState);
|
this.setState({editorState: newState});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTextPasted(text: string, html?: string) {
|
||||||
|
const currentSelection = this.state.editorState.getSelection();
|
||||||
|
const currentContent = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
|
let contentState = null;
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
contentState = Modifier.replaceWithFragment(
|
||||||
|
currentContent,
|
||||||
|
currentSelection,
|
||||||
|
RichText.htmlToContentState(html).getBlockMap(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
contentState = Modifier.replaceText(currentContent, currentSelection, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let newEditorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
|
|
||||||
|
newEditorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter());
|
||||||
|
this.onEditorContentChanged(newEditorState);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
handleReturn(ev) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentBlockType = RichUtils.getCurrentBlockType(this.state.editorState);
|
||||||
|
// If we're in any of these three types of blocks, shift enter should insert soft newlines
|
||||||
|
// And just enter should end the block
|
||||||
|
if(['blockquote', 'unordered-list-item', 'ordered-list-item'].includes(currentBlockType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const contentState = this.state.editorState.getCurrentContent();
|
const contentState = this.state.editorState.getCurrentContent();
|
||||||
if (!contentState.hasText()) {
|
if (!contentState.hasText()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -489,11 +482,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
let contentText = contentState.getPlainText(), contentHTML;
|
let contentText = contentState.getPlainText(), contentHTML;
|
||||||
|
|
||||||
var cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
const cmd = SlashCommands.processInput(this.props.room.roomId, contentText);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
this.setState({
|
this.setState({
|
||||||
editorState: this.createEditorState()
|
editorState: this.createEditorState(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (cmd.promise) {
|
if (cmd.promise) {
|
||||||
|
@ -501,16 +494,15 @@ export default class MessageComposerInput extends React.Component {
|
||||||
console.log("Command success.");
|
console.log("Command success.");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Command failure: %s", err);
|
console.error("Command failure: %s", err);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Server error"),
|
title: _t("Server error"),
|
||||||
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
} else if (cmd.error) {
|
||||||
else if (cmd.error) {
|
|
||||||
console.error(cmd.error);
|
console.error(cmd.error);
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Command error"),
|
title: _t("Command error"),
|
||||||
description: cmd.error,
|
description: cmd.error,
|
||||||
|
@ -520,9 +512,30 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
contentHTML = HtmlUtils.stripParagraphs(
|
// We should only send HTML if any block is styled or contains inline style
|
||||||
RichText.contentStateToHTML(contentState)
|
let shouldSendHTML = false;
|
||||||
|
const blocks = contentState.getBlocksAsArray();
|
||||||
|
if (blocks.some((block) => block.getType() !== 'unstyled')) {
|
||||||
|
shouldSendHTML = true;
|
||||||
|
} else {
|
||||||
|
const characterLists = blocks.map((block) => block.getCharacterList());
|
||||||
|
// For each block of characters, determine if any inline styles are applied
|
||||||
|
// and if yes, send HTML
|
||||||
|
characterLists.forEach((characters) => {
|
||||||
|
const numberOfStylesForCharacters = characters.map(
|
||||||
|
(character) => character.getStyle().toArray().length,
|
||||||
|
).toArray();
|
||||||
|
// If any character has more than 0 inline styles applied, send HTML
|
||||||
|
if (numberOfStylesForCharacters.some((styles) => styles > 0)) {
|
||||||
|
shouldSendHTML = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (shouldSendHTML) {
|
||||||
|
contentHTML = HtmlUtils.processHtmlForSending(
|
||||||
|
RichText.contentStateToHTML(contentState),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const md = new Markdown(contentText);
|
const md = new Markdown(contentText);
|
||||||
if (md.isPlainText()) {
|
if (md.isPlainText()) {
|
||||||
|
@ -543,12 +556,20 @@ export default class MessageComposerInput extends React.Component {
|
||||||
sendTextFn = this.client.sendEmoteMessage;
|
sendTextFn = this.client.sendEmoteMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: We don't actually seem to use this history?
|
if (this.state.isRichtextEnabled) {
|
||||||
this.sentHistory.push(contentHTML || contentText);
|
this.historyManager.addItem(
|
||||||
|
contentHTML ? contentHTML : contentText,
|
||||||
|
contentHTML ? 'html' : 'markdown',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Always store MD input as input history
|
||||||
|
this.historyManager.addItem(contentText, 'markdown');
|
||||||
|
}
|
||||||
|
|
||||||
let sendMessagePromise;
|
let sendMessagePromise;
|
||||||
if (contentHTML) {
|
if (contentHTML) {
|
||||||
sendMessagePromise = sendHtmlFn.call(
|
sendMessagePromise = sendHtmlFn.call(
|
||||||
this.client, this.props.room.roomId, contentText, contentHTML
|
this.client, this.props.room.roomId, contentText, contentHTML,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText);
|
sendMessagePromise = sendTextFn.call(this.client, this.props.room.roomId, contentText);
|
||||||
|
@ -569,71 +590,149 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onUpArrow(e) {
|
onUpArrow = (e) => {
|
||||||
const completion = this.autocomplete.onUpArrow();
|
this.onVerticalArrow(e, true);
|
||||||
if (completion != null) {
|
};
|
||||||
e.preventDefault();
|
|
||||||
}
|
onDownArrow = (e) => {
|
||||||
return await this.setDisplayedCompletion(completion);
|
this.onVerticalArrow(e, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
onVerticalArrow = (e, up) => {
|
||||||
|
// Select history only if we are not currently auto-completing
|
||||||
|
if (this.autocomplete.state.completionList.length === 0) {
|
||||||
|
// Don't go back in history if we're in the middle of a multi-line message
|
||||||
|
const selection = this.state.editorState.getSelection();
|
||||||
|
const blockKey = selection.getStartKey();
|
||||||
|
const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock();
|
||||||
|
const lastBlock = this.state.editorState.getCurrentContent().getLastBlock();
|
||||||
|
|
||||||
|
const selectionOffset = selection.getAnchorOffset();
|
||||||
|
let canMoveUp = false;
|
||||||
|
let canMoveDown = false;
|
||||||
|
if (blockKey === firstBlock.getKey()) {
|
||||||
|
const textBeforeCursor = firstBlock.getText().slice(0, selectionOffset);
|
||||||
|
canMoveUp = textBeforeCursor.indexOf('\n') === -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onDownArrow(e) {
|
if (blockKey === lastBlock.getKey()) {
|
||||||
const completion = this.autocomplete.onDownArrow();
|
const textAfterCursor = lastBlock.getText().slice(selectionOffset);
|
||||||
e.preventDefault();
|
canMoveDown = textAfterCursor.indexOf('\n') === -1;
|
||||||
return await this.setDisplayedCompletion(completion);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tab and shift-tab are mapped to down and up arrow respectively
|
if ((up && !canMoveUp) || (!up && !canMoveDown)) return;
|
||||||
async onTab(e) {
|
|
||||||
e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes
|
const selected = this.selectHistory(up);
|
||||||
const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e);
|
if (selected) {
|
||||||
if (!didTab && this.autocomplete) {
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
this.autocomplete.forceComplete().then(() => {
|
e.preventDefault();
|
||||||
this.onDownArrow(e);
|
}
|
||||||
|
} else {
|
||||||
|
this.moveAutocompleteSelection(up);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectHistory = async (up) => {
|
||||||
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
|
// True if we are not currently selecting history, but composing a message
|
||||||
|
if (this.historyManager.currentIndex === this.historyManager.history.length) {
|
||||||
|
// We can't go any further - there isn't any more history, so nop.
|
||||||
|
if (!up) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
currentlyComposedEditorState: this.state.editorState,
|
||||||
});
|
});
|
||||||
}
|
} else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) {
|
||||||
|
// True when we return to the message being composed currently
|
||||||
|
this.setState({
|
||||||
|
editorState: this.state.currentlyComposedEditorState,
|
||||||
|
});
|
||||||
|
this.historyManager.currentIndex = this.historyManager.history.length;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEscape(e) {
|
const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
||||||
|
if (!newContent) return false;
|
||||||
|
let editorState = EditorState.push(
|
||||||
|
this.state.editorState,
|
||||||
|
newContent,
|
||||||
|
'insert-characters',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Move selection to the end of the selected history
|
||||||
|
let newSelection = SelectionState.createEmpty(newContent.getLastBlock().getKey());
|
||||||
|
newSelection = newSelection.merge({
|
||||||
|
focusOffset: newContent.getLastBlock().getLength(),
|
||||||
|
anchorOffset: newContent.getLastBlock().getLength(),
|
||||||
|
});
|
||||||
|
editorState = EditorState.forceSelection(editorState, newSelection);
|
||||||
|
|
||||||
|
this.setState({editorState});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onTab = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.autocomplete.state.completionList.length === 0) {
|
||||||
|
// Force completions to show for the text currently entered
|
||||||
|
await this.autocomplete.forceComplete();
|
||||||
|
// Select the first item by moving "down"
|
||||||
|
await this.moveAutocompleteSelection(false);
|
||||||
|
} else {
|
||||||
|
await this.moveAutocompleteSelection(e.shiftKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
moveAutocompleteSelection = (up) => {
|
||||||
|
const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
|
||||||
|
return this.setDisplayedCompletion(completion);
|
||||||
|
};
|
||||||
|
|
||||||
|
onEscape = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.autocomplete) {
|
if (this.autocomplete) {
|
||||||
this.autocomplete.onEscape(e);
|
this.autocomplete.onEscape(e);
|
||||||
}
|
}
|
||||||
this.setDisplayedCompletion(null); // restore originalEditorState
|
await this.setDisplayedCompletion(null); // restore originalEditorState
|
||||||
}
|
};
|
||||||
|
|
||||||
/* If passed null, restores the original editor content from state.originalEditorState.
|
/* If passed null, restores the original editor content from state.originalEditorState.
|
||||||
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
|
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
|
||||||
*/
|
*/
|
||||||
async setDisplayedCompletion(displayedCompletion: ?Completion): boolean {
|
setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => {
|
||||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||||
|
|
||||||
if (displayedCompletion == null) {
|
if (displayedCompletion == null) {
|
||||||
if (this.state.originalEditorState) {
|
if (this.state.originalEditorState) {
|
||||||
this.setEditorState(this.state.originalEditorState);
|
let editorState = this.state.originalEditorState;
|
||||||
|
// This is a workaround from https://github.com/facebook/draft-js/issues/458
|
||||||
|
// Due to the way we swap editorStates, Draft does not rerender at times
|
||||||
|
editorState = EditorState.forceSelection(editorState,
|
||||||
|
editorState.getSelection());
|
||||||
|
this.setState({editorState});
|
||||||
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {range = {}, completion = ''} = displayedCompletion;
|
const {range = {}, completion = ''} = displayedCompletion;
|
||||||
|
|
||||||
let contentState = Modifier.replaceText(
|
const contentState = Modifier.replaceText(
|
||||||
activeEditorState.getCurrentContent(),
|
activeEditorState.getCurrentContent(),
|
||||||
RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()),
|
RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()),
|
||||||
completion
|
completion,
|
||||||
);
|
);
|
||||||
|
|
||||||
let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters');
|
let editorState = EditorState.push(activeEditorState, contentState, 'insert-characters');
|
||||||
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||||
const originalEditorState = activeEditorState;
|
this.setState({editorState, originalEditorState: activeEditorState});
|
||||||
|
|
||||||
await this.setEditorState(editorState);
|
|
||||||
this.setState({originalEditorState});
|
|
||||||
|
|
||||||
// for some reason, doing this right away does not update the editor :(
|
// for some reason, doing this right away does not update the editor :(
|
||||||
setTimeout(() => this.refs.editor.focus(), 50);
|
// setTimeout(() => this.refs.editor.focus(), 50);
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
||||||
e.preventDefault(); // don't steal focus from the editor!
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
|
@ -658,8 +757,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
||||||
const style = originalStyle
|
const style = originalStyle
|
||||||
.map(style => styleName[style] || null)
|
.map((style) => styleName[style] || null)
|
||||||
.filter(styleName => !!styleName);
|
.filter((styleName) => !!styleName);
|
||||||
|
|
||||||
const blockName = {
|
const blockName = {
|
||||||
'code-block': 'code',
|
'code-block': 'code',
|
||||||
|
@ -678,10 +777,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onMarkdownToggleClicked(e) {
|
onMarkdownToggleClicked = (e) => {
|
||||||
e.preventDefault(); // don't steal focus from the editor!
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
this.handleKeyCommand('toggle-mode');
|
this.handleKeyCommand('toggle-mode');
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||||
|
@ -713,7 +812,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
onConfirm={this.setDisplayedCompletion}
|
onConfirm={this.setDisplayedCompletion}
|
||||||
query={contentText}
|
query={contentText}
|
||||||
selection={selection} />
|
selection={selection}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
<img className="mx_MessageComposer_input_markdownIndicator mx_filterFlipColor"
|
||||||
|
@ -729,13 +828,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
handleReturn={this.handleReturn}
|
handleReturn={this.handleReturn}
|
||||||
|
handlePastedText={this.onTextPasted}
|
||||||
handlePastedFiles={this.props.onFilesPasted}
|
handlePastedFiles={this.props.onFilesPasted}
|
||||||
stripPastedStyles={!this.state.isRichtextEnabled}
|
stripPastedStyles={!this.state.isRichtextEnabled}
|
||||||
onTab={this.onTab}
|
onTab={this.onTab}
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
onDownArrow={this.onDownArrow}
|
onDownArrow={this.onDownArrow}
|
||||||
onEscape={this.onEscape}
|
onEscape={this.onEscape}
|
||||||
spellCheck={true} />
|
spellCheck={true}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,124 +1,125 @@
|
||||||
{
|
{
|
||||||
"af":"Afrikaans",
|
"Add a widget": "Add a widget",
|
||||||
"ar-ae":"Arabic (U.A.E.)",
|
"af": "Afrikaans",
|
||||||
"ar-bh":"Arabic (Bahrain)",
|
"ar-ae": "Arabic (U.A.E.)",
|
||||||
"ar-dz":"Arabic (Algeria)",
|
"ar-bh": "Arabic (Bahrain)",
|
||||||
"ar-eg":"Arabic (Egypt)",
|
"ar-dz": "Arabic (Algeria)",
|
||||||
"ar-iq":"Arabic (Iraq)",
|
"ar-eg": "Arabic (Egypt)",
|
||||||
"ar-jo":"Arabic (Jordan)",
|
"ar-iq": "Arabic (Iraq)",
|
||||||
"ar-kw":"Arabic (Kuwait)",
|
"ar-jo": "Arabic (Jordan)",
|
||||||
"ar-lb":"Arabic (Lebanon)",
|
"ar-kw": "Arabic (Kuwait)",
|
||||||
"ar-ly":"Arabic (Libya)",
|
"ar-lb": "Arabic (Lebanon)",
|
||||||
"ar-ma":"Arabic (Morocco)",
|
"ar-ly": "Arabic (Libya)",
|
||||||
"ar-om":"Arabic (Oman)",
|
"ar-ma": "Arabic (Morocco)",
|
||||||
"ar-qa":"Arabic (Qatar)",
|
"ar-om": "Arabic (Oman)",
|
||||||
"ar-sa":"Arabic (Saudi Arabia)",
|
"ar-qa": "Arabic (Qatar)",
|
||||||
"ar-sy":"Arabic (Syria)",
|
"ar-sa": "Arabic (Saudi Arabia)",
|
||||||
"ar-tn":"Arabic (Tunisia)",
|
"ar-sy": "Arabic (Syria)",
|
||||||
"ar-ye":"Arabic (Yemen)",
|
"ar-tn": "Arabic (Tunisia)",
|
||||||
"be":"Belarusian",
|
"ar-ye": "Arabic (Yemen)",
|
||||||
"bg":"Bulgarian",
|
"be": "Belarusian",
|
||||||
"ca":"Catalan",
|
"bg": "Bulgarian",
|
||||||
"cs":"Czech",
|
"ca": "Catalan",
|
||||||
"da":"Danish",
|
"cs": "Czech",
|
||||||
"de-at":"German (Austria)",
|
"da": "Danish",
|
||||||
"de-ch":"German (Switzerland)",
|
"de-at": "German (Austria)",
|
||||||
"de":"German",
|
"de-ch": "German (Switzerland)",
|
||||||
"de-li":"German (Liechtenstein)",
|
"de": "German",
|
||||||
"de-lu":"German (Luxembourg)",
|
"de-li": "German (Liechtenstein)",
|
||||||
"el":"Greek",
|
"de-lu": "German (Luxembourg)",
|
||||||
"en-au":"English (Australia)",
|
"el": "Greek",
|
||||||
"en-bz":"English (Belize)",
|
"en-au": "English (Australia)",
|
||||||
"en-ca":"English (Canada)",
|
"en-bz": "English (Belize)",
|
||||||
"en":"English",
|
"en-ca": "English (Canada)",
|
||||||
"en-gb":"English (United Kingdom)",
|
"en": "English",
|
||||||
"en-ie":"English (Ireland)",
|
"en-gb": "English (United Kingdom)",
|
||||||
"en-jm":"English (Jamaica)",
|
"en-ie": "English (Ireland)",
|
||||||
"en-nz":"English (New Zealand)",
|
"en-jm": "English (Jamaica)",
|
||||||
"en-tt":"English (Trinidad)",
|
"en-nz": "English (New Zealand)",
|
||||||
"en-us":"English (United States)",
|
"en-tt": "English (Trinidad)",
|
||||||
"en-za":"English (South Africa)",
|
"en-us": "English (United States)",
|
||||||
"es-ar":"Spanish (Argentina)",
|
"en-za": "English (South Africa)",
|
||||||
"es-bo":"Spanish (Bolivia)",
|
"es-ar": "Spanish (Argentina)",
|
||||||
"es-cl":"Spanish (Chile)",
|
"es-bo": "Spanish (Bolivia)",
|
||||||
"es-co":"Spanish (Colombia)",
|
"es-cl": "Spanish (Chile)",
|
||||||
"es-cr":"Spanish (Costa Rica)",
|
"es-co": "Spanish (Colombia)",
|
||||||
"es-do":"Spanish (Dominican Republic)",
|
"es-cr": "Spanish (Costa Rica)",
|
||||||
"es-ec":"Spanish (Ecuador)",
|
"es-do": "Spanish (Dominican Republic)",
|
||||||
"es-gt":"Spanish (Guatemala)",
|
"es-ec": "Spanish (Ecuador)",
|
||||||
"es-hn":"Spanish (Honduras)",
|
"es-gt": "Spanish (Guatemala)",
|
||||||
"es-mx":"Spanish (Mexico)",
|
"es-hn": "Spanish (Honduras)",
|
||||||
"es-ni":"Spanish (Nicaragua)",
|
"es-mx": "Spanish (Mexico)",
|
||||||
"es-pa":"Spanish (Panama)",
|
"es-ni": "Spanish (Nicaragua)",
|
||||||
"es-pe":"Spanish (Peru)",
|
"es-pa": "Spanish (Panama)",
|
||||||
"es-pr":"Spanish (Puerto Rico)",
|
"es-pe": "Spanish (Peru)",
|
||||||
"es-py":"Spanish (Paraguay)",
|
"es-pr": "Spanish (Puerto Rico)",
|
||||||
"es":"Spanish (Spain)",
|
"es-py": "Spanish (Paraguay)",
|
||||||
"es-sv":"Spanish (El Salvador)",
|
"es": "Spanish (Spain)",
|
||||||
"es-uy":"Spanish (Uruguay)",
|
"es-sv": "Spanish (El Salvador)",
|
||||||
"es-ve":"Spanish (Venezuela)",
|
"es-uy": "Spanish (Uruguay)",
|
||||||
"et":"Estonian",
|
"es-ve": "Spanish (Venezuela)",
|
||||||
"eu":"Basque (Basque)",
|
"et": "Estonian",
|
||||||
"fa":"Farsi",
|
"eu": "Basque (Basque)",
|
||||||
"fi":"Finnish",
|
"fa": "Farsi",
|
||||||
"fo":"Faeroese",
|
"fi": "Finnish",
|
||||||
"fr-be":"French (Belgium)",
|
"fo": "Faeroese",
|
||||||
"fr-ca":"French (Canada)",
|
"fr-be": "French (Belgium)",
|
||||||
"fr-ch":"French (Switzerland)",
|
"fr-ca": "French (Canada)",
|
||||||
"fr":"French",
|
"fr-ch": "French (Switzerland)",
|
||||||
"fr-lu":"French (Luxembourg)",
|
"fr": "French",
|
||||||
"ga":"Irish",
|
"fr-lu": "French (Luxembourg)",
|
||||||
"gd":"Gaelic (Scotland)",
|
"ga": "Irish",
|
||||||
"he":"Hebrew",
|
"gd": "Gaelic (Scotland)",
|
||||||
"hi":"Hindi",
|
"he": "Hebrew",
|
||||||
"hr":"Croatian",
|
"hi": "Hindi",
|
||||||
"hu":"Hungarian",
|
"hr": "Croatian",
|
||||||
"id":"Indonesian",
|
"hu": "Hungarian",
|
||||||
"is":"Icelandic",
|
"id": "Indonesian",
|
||||||
"it-ch":"Italian (Switzerland)",
|
"is": "Icelandic",
|
||||||
"it":"Italian",
|
"it-ch": "Italian (Switzerland)",
|
||||||
"ja":"Japanese",
|
"it": "Italian",
|
||||||
"ji":"Yiddish",
|
"ja": "Japanese",
|
||||||
"ko":"Korean",
|
"ji": "Yiddish",
|
||||||
"lt":"Lithuanian",
|
"ko": "Korean",
|
||||||
"lv":"Latvian",
|
"lt": "Lithuanian",
|
||||||
"mk":"Macedonian (FYROM)",
|
"lv": "Latvian",
|
||||||
"ms":"Malaysian",
|
"mk": "Macedonian (FYROM)",
|
||||||
"mt":"Maltese",
|
"ms": "Malaysian",
|
||||||
"nl-be":"Dutch (Belgium)",
|
"mt": "Maltese",
|
||||||
"nl":"Dutch",
|
"nl-be": "Dutch (Belgium)",
|
||||||
"no":"Norwegian",
|
"nl": "Dutch",
|
||||||
"pl":"Polish",
|
"no": "Norwegian",
|
||||||
"pt-br":"Brazilian Portuguese",
|
"pl": "Polish",
|
||||||
"pt":"Portuguese",
|
"pt-br": "Brazilian Portuguese",
|
||||||
"rm":"Rhaeto-Romanic",
|
"pt": "Portuguese",
|
||||||
"ro-mo":"Romanian (Republic of Moldova)",
|
"rm": "Rhaeto-Romanic",
|
||||||
"ro":"Romanian",
|
"ro-mo": "Romanian (Republic of Moldova)",
|
||||||
"ru-mo":"Russian (Republic of Moldova)",
|
"ro": "Romanian",
|
||||||
"ru":"Russian",
|
"ru-mo": "Russian (Republic of Moldova)",
|
||||||
"sb":"Sorbian",
|
"ru": "Russian",
|
||||||
"sk":"Slovak",
|
"sb": "Sorbian",
|
||||||
"sl":"Slovenian",
|
"sk": "Slovak",
|
||||||
"sq":"Albanian",
|
"sl": "Slovenian",
|
||||||
"sr":"Serbian",
|
"sq": "Albanian",
|
||||||
"sv-fi":"Swedish (Finland)",
|
"sr": "Serbian",
|
||||||
"sv":"Swedish",
|
"sv-fi": "Swedish (Finland)",
|
||||||
"sx":"Sutu",
|
"sv": "Swedish",
|
||||||
"sz":"Sami (Lappish)",
|
"sx": "Sutu",
|
||||||
"th":"Thai",
|
"sz": "Sami (Lappish)",
|
||||||
"tn":"Tswana",
|
"th": "Thai",
|
||||||
"tr":"Turkish",
|
"tn": "Tswana",
|
||||||
"ts":"Tsonga",
|
"tr": "Turkish",
|
||||||
"uk":"Ukrainian",
|
"ts": "Tsonga",
|
||||||
"ur":"Urdu",
|
"uk": "Ukrainian",
|
||||||
"ve":"Venda",
|
"ur": "Urdu",
|
||||||
"vi":"Vietnamese",
|
"ve": "Venda",
|
||||||
"xh":"Xhosa",
|
"vi": "Vietnamese",
|
||||||
"zh-cn":"Chinese (PRC)",
|
"xh": "Xhosa",
|
||||||
"zh-hk":"Chinese (Hong Kong SAR)",
|
"zh-cn": "Chinese (PRC)",
|
||||||
"zh-sg":"Chinese (Singapore)",
|
"zh-hk": "Chinese (Hong Kong SAR)",
|
||||||
"zh-tw":"Chinese (Taiwan)",
|
"zh-sg": "Chinese (Singapore)",
|
||||||
"zu":"Zulu",
|
"zh-tw": "Chinese (Taiwan)",
|
||||||
|
"zu": "Zulu",
|
||||||
"a room": "a room",
|
"a room": "a room",
|
||||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
|
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains",
|
||||||
"Accept": "Accept",
|
"Accept": "Accept",
|
||||||
|
@ -187,7 +188,6 @@
|
||||||
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
|
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||||
"Can't load user settings": "Can't load user settings",
|
"Can't load user settings": "Can't load user settings",
|
||||||
"Change Password": "Change Password",
|
"Change Password": "Change Password",
|
||||||
"Change colourscheme of current room": "Change colourscheme of current room",
|
|
||||||
"%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.",
|
"%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.",
|
||||||
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
|
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
|
||||||
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
|
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
|
||||||
|
@ -241,6 +241,7 @@
|
||||||
"demote": "demote",
|
"demote": "demote",
|
||||||
"Deops user with given id": "Deops user with given id",
|
"Deops user with given id": "Deops user with given id",
|
||||||
"Default": "Default",
|
"Default": "Default",
|
||||||
|
"Define the power level of a user": "Define the power level of a user",
|
||||||
"Device already verified!": "Device already verified!",
|
"Device already verified!": "Device already verified!",
|
||||||
"Device ID": "Device ID",
|
"Device ID": "Device ID",
|
||||||
"Device ID:": "Device ID:",
|
"Device ID:": "Device ID:",
|
||||||
|
@ -268,6 +269,7 @@
|
||||||
"Email address (optional)": "Email address (optional)",
|
"Email address (optional)": "Email address (optional)",
|
||||||
"Email, name or matrix ID": "Email, name or matrix ID",
|
"Email, name or matrix ID": "Email, name or matrix ID",
|
||||||
"Emoji": "Emoji",
|
"Emoji": "Emoji",
|
||||||
|
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
|
||||||
"Enable encryption": "Enable encryption",
|
"Enable encryption": "Enable encryption",
|
||||||
"Enable Notifications": "Enable Notifications",
|
"Enable Notifications": "Enable Notifications",
|
||||||
"enabled": "enabled",
|
"enabled": "enabled",
|
||||||
|
@ -336,6 +338,7 @@
|
||||||
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
||||||
"had": "had",
|
"had": "had",
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
|
"Hide Apps": "Hide Apps",
|
||||||
"Hide read receipts": "Hide read receipts",
|
"Hide read receipts": "Hide read receipts",
|
||||||
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
|
@ -377,7 +380,6 @@
|
||||||
"Labs": "Labs",
|
"Labs": "Labs",
|
||||||
"Last seen": "Last seen",
|
"Last seen": "Last seen",
|
||||||
"Leave room": "Leave room",
|
"Leave room": "Leave room",
|
||||||
"Leaves room with given alias": "Leaves room with given alias",
|
|
||||||
"left and rejoined": "left and rejoined",
|
"left and rejoined": "left and rejoined",
|
||||||
"left": "left",
|
"left": "left",
|
||||||
"%(targetName)s left the room.": "%(targetName)s left the room.",
|
"%(targetName)s left the room.": "%(targetName)s left the room.",
|
||||||
|
@ -393,6 +395,7 @@
|
||||||
"Markdown is disabled": "Markdown is disabled",
|
"Markdown is disabled": "Markdown is disabled",
|
||||||
"Markdown is enabled": "Markdown is enabled",
|
"Markdown is enabled": "Markdown is enabled",
|
||||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
||||||
|
"Matrix Apps": "Matrix Apps",
|
||||||
"Members only": "Members only",
|
"Members only": "Members only",
|
||||||
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
||||||
"Missing room_id in request": "Missing room_id in request",
|
"Missing room_id in request": "Missing room_id in request",
|
||||||
|
@ -509,7 +512,7 @@
|
||||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
||||||
"Set": "Set",
|
"Set": "Set",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Sets the room topic": "Sets the room topic",
|
"Show Apps": "Show Apps",
|
||||||
"Show panel": "Show panel",
|
"Show panel": "Show panel",
|
||||||
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
|
"Show Text Formatting Toolbar": "Show Text Formatting Toolbar",
|
||||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||||
|
@ -580,6 +583,7 @@
|
||||||
"Turn Markdown on": "Turn Markdown on",
|
"Turn Markdown on": "Turn Markdown on",
|
||||||
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).",
|
"%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).",
|
||||||
"Unable to add email address": "Unable to add email address",
|
"Unable to add email address": "Unable to add email address",
|
||||||
|
"Unable to create widget.": "Unable to create widget.",
|
||||||
"Unable to remove contact information": "Unable to remove contact information",
|
"Unable to remove contact information": "Unable to remove contact information",
|
||||||
"Unable to restore previous session": "Unable to restore previous session",
|
"Unable to restore previous session": "Unable to restore previous session",
|
||||||
"Unable to verify email address.": "Unable to verify email address.",
|
"Unable to verify email address.": "Unable to verify email address.",
|
||||||
|
@ -744,10 +748,10 @@
|
||||||
"italic": "italic",
|
"italic": "italic",
|
||||||
"strike": "strike",
|
"strike": "strike",
|
||||||
"underline": "underline",
|
"underline": "underline",
|
||||||
"code":"code",
|
"code": "code",
|
||||||
"quote":"quote",
|
"quote": "quote",
|
||||||
"bullet":"bullet",
|
"bullet": "bullet",
|
||||||
"numbullet":"numbullet",
|
"numbullet": "numbullet",
|
||||||
"%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times",
|
"%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times",
|
||||||
"%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times",
|
"%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times",
|
||||||
"%(severalUsers)sjoined": "%(severalUsers)sjoined",
|
"%(severalUsers)sjoined": "%(severalUsers)sjoined",
|
||||||
|
@ -807,7 +811,6 @@
|
||||||
"Analytics": "Analytics",
|
"Analytics": "Analytics",
|
||||||
"Opt out of analytics": "Opt out of analytics",
|
"Opt out of analytics": "Opt out of analytics",
|
||||||
"Options": "Options",
|
"Options": "Options",
|
||||||
"Ops user with given id": "Ops user with given id",
|
|
||||||
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
|
"Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
|
||||||
"Passphrases must match": "Passphrases must match",
|
"Passphrases must match": "Passphrases must match",
|
||||||
"Passphrase must not be empty": "Passphrase must not be empty",
|
"Passphrase must not be empty": "Passphrase must not be empty",
|
||||||
|
@ -840,7 +843,6 @@
|
||||||
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.",
|
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.",
|
||||||
"In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.",
|
"In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.",
|
||||||
"Verify device": "Verify device",
|
"Verify device": "Verify device",
|
||||||
"Verify a user, device, and pubkey tuple": "Verify a user, device, and pubkey tuple",
|
|
||||||
"I verify that the keys match": "I verify that the keys match",
|
"I verify that the keys match": "I verify that the keys match",
|
||||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.",
|
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.",
|
||||||
"Unable to restore session": "Unable to restore session",
|
"Unable to restore session": "Unable to restore session",
|
||||||
|
@ -920,11 +922,12 @@
|
||||||
"Do you want to set an email address?": "Do you want to set an email address?",
|
"Do you want to set an email address?": "Do you want to set an email address?",
|
||||||
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
|
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
|
||||||
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
|
"To return to your account in future you need to set a password": "To return to your account in future you need to set a password",
|
||||||
"Skip":"Skip",
|
"Skip": "Skip",
|
||||||
"Start verification": "Start verification",
|
"Start verification": "Start verification",
|
||||||
"Share without verifying": "Share without verifying",
|
"Share without verifying": "Share without verifying",
|
||||||
"Ignore request": "Ignore request",
|
"Ignore request": "Ignore request",
|
||||||
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
|
"You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.",
|
||||||
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.",
|
"Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.",
|
||||||
"Encryption key request": "Encryption key request"
|
"Encryption key request": "Encryption key request",
|
||||||
|
"Autocomplete Delay (ms):": "Autocomplete Delay (ms):"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{
|
{
|
||||||
|
"Add a widget": "Add a widget",
|
||||||
"af": "Afrikaans",
|
"af": "Afrikaans",
|
||||||
"ar-ae": "Arabic (U.A.E.)",
|
"ar-ae": "Arabic (U.A.E.)",
|
||||||
"ar-bh": "Arabic (Bahrain)",
|
"ar-bh": "Arabic (Bahrain)",
|
||||||
|
@ -311,6 +312,7 @@
|
||||||
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
"Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.",
|
||||||
"had": "had",
|
"had": "had",
|
||||||
"Hangup": "Hangup",
|
"Hangup": "Hangup",
|
||||||
|
"Hide Apps": "Hide Apps",
|
||||||
"Hide read receipts": "Hide read receipts",
|
"Hide read receipts": "Hide read receipts",
|
||||||
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
"Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar",
|
||||||
"Historical": "Historical",
|
"Historical": "Historical",
|
||||||
|
@ -362,6 +364,7 @@
|
||||||
"Markdown is disabled": "Markdown is disabled",
|
"Markdown is disabled": "Markdown is disabled",
|
||||||
"Markdown is enabled": "Markdown is enabled",
|
"Markdown is enabled": "Markdown is enabled",
|
||||||
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
"matrix-react-sdk version:": "matrix-react-sdk version:",
|
||||||
|
"Matrix Apps": "Matrix Apps",
|
||||||
"Members only": "Members only",
|
"Members only": "Members only",
|
||||||
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
|
||||||
"Missing room_id in request": "Missing room_id in request",
|
"Missing room_id in request": "Missing room_id in request",
|
||||||
|
@ -464,6 +467,7 @@
|
||||||
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
||||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
|
"Show Apps": "Show Apps",
|
||||||
"Show panel": "Show panel",
|
"Show panel": "Show panel",
|
||||||
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
|
||||||
"Signed Out": "Signed Out",
|
"Signed Out": "Signed Out",
|
||||||
|
|
1
src/stripped-emoji.json
Normal file
1
src/stripped-emoji.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -104,11 +104,12 @@ describe('MessageComposerInput', () => {
|
||||||
addTextToDraft('a');
|
addTextToDraft('a');
|
||||||
mci.handleKeyCommand('toggle-mode');
|
mci.handleKeyCommand('toggle-mode');
|
||||||
mci.handleReturn(sinon.stub());
|
mci.handleReturn(sinon.stub());
|
||||||
|
|
||||||
expect(spy.calledOnce).toEqual(true);
|
expect(spy.calledOnce).toEqual(true);
|
||||||
expect(spy.args[0][1]).toEqual('a');
|
expect(spy.args[0][1]).toEqual('a');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages in rich text', () => {
|
it('should send emoji messages when rich text is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
mci.enableRichtext(true);
|
mci.enableRichtext(true);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
|
@ -117,7 +118,7 @@ describe('MessageComposerInput', () => {
|
||||||
expect(spy.calledOnce).toEqual(true, 'should send message');
|
expect(spy.calledOnce).toEqual(true, 'should send message');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send emoji messages in Markdown', () => {
|
it('should send emoji messages when Markdown is enabled', () => {
|
||||||
const spy = sinon.spy(client, 'sendTextMessage');
|
const spy = sinon.spy(client, 'sendTextMessage');
|
||||||
mci.enableRichtext(false);
|
mci.enableRichtext(false);
|
||||||
addTextToDraft('☹');
|
addTextToDraft('☹');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue