parent
ce40fa1a8f
commit
b62622a814
15 changed files with 407 additions and 194 deletions
16
.eslintrc
16
.eslintrc
|
@ -78,18 +78,26 @@
|
||||||
/** react **/
|
/** react **/
|
||||||
|
|
||||||
// bind or arrow function in props causes performance issues
|
// bind or arrow function in props causes performance issues
|
||||||
"react/jsx-no-bind": ["error"],
|
"react/jsx-no-bind": ["error", {
|
||||||
|
"ignoreRefs": true
|
||||||
|
}],
|
||||||
"react/jsx-key": ["error"],
|
"react/jsx-key": ["error"],
|
||||||
"react/prefer-stateless-function": ["warn"],
|
"react/prefer-stateless-function": ["warn"],
|
||||||
"react/sort-comp": ["warn"],
|
|
||||||
|
|
||||||
/** flowtype **/
|
/** flowtype **/
|
||||||
"flowtype/require-parameter-type": 1,
|
"flowtype/require-parameter-type": [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
"excludeArrowFunctions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flowtype/define-flow-type": 1,
|
||||||
"flowtype/require-return-type": [
|
"flowtype/require-return-type": [
|
||||||
1,
|
1,
|
||||||
"always",
|
"always",
|
||||||
{
|
{
|
||||||
"annotateUndefined": "never"
|
"annotateUndefined": "never",
|
||||||
|
"excludeArrowFunctions": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"flowtype/space-after-type-colon": [
|
"flowtype/space-after-type-colon": [
|
||||||
|
|
|
@ -158,5 +158,11 @@ React
|
||||||
<Foo onClick={this.doStuff}> // Better
|
<Foo onClick={this.doStuff}> // Better
|
||||||
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
|
<Foo onClick={this.onFooClick}> // Best, if onFooClick would do anything other than directly calling doStuff
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Not doing so is acceptable in a single case; in function-refs:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<Foo ref={(self) => this.component = self}>
|
||||||
|
```
|
||||||
- Think about whether your component really needs state: are you duplicating
|
- Think about whether your component really needs state: are you duplicating
|
||||||
information in component state that could be derived from the model?
|
information in component state that could be derived from the model?
|
||||||
|
|
|
@ -62,8 +62,8 @@
|
||||||
"babel-loader": "^5.4.0",
|
"babel-loader": "^5.4.0",
|
||||||
"babel-polyfill": "^6.5.0",
|
"babel-polyfill": "^6.5.0",
|
||||||
"eslint": "^2.13.1",
|
"eslint": "^2.13.1",
|
||||||
"eslint-plugin-flowtype": "^2.3.0",
|
"eslint-plugin-flowtype": "^2.17.0",
|
||||||
"eslint-plugin-react": "^5.2.2",
|
"eslint-plugin-react": "^6.2.1",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"json-loader": "^0.5.3",
|
"json-loader": "^0.5.3",
|
||||||
"karma": "^0.13.22",
|
"karma": "^0.13.22",
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
import * as sdk from './index';
|
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";
|
||||||
|
|
||||||
const MARKDOWN_REGEX = {
|
const MARKDOWN_REGEX = {
|
||||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||||
|
@ -203,7 +204,7 @@ export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textOffsetsToSelectionState({start, end}: {start: number, end: number},
|
export function textOffsetsToSelectionState({start, end}: SelectionRange,
|
||||||
contentBlocks: Array<ContentBlock>): SelectionState {
|
contentBlocks: Array<ContentBlock>): SelectionState {
|
||||||
let selectionState = SelectionState.createEmpty();
|
let selectionState = SelectionState.createEmpty();
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var dis = require("./dispatcher");
|
var dis = require("./dispatcher");
|
||||||
var Tinter = require("./Tinter");
|
var Tinter = require("./Tinter");
|
||||||
|
import sdk from './index';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
|
@ -56,6 +58,16 @@ var success = function(promise) {
|
||||||
};
|
};
|
||||||
|
|
||||||
var commands = {
|
var commands = {
|
||||||
|
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
||||||
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "/ddg is not a command",
|
||||||
|
description: "To use it, just wait for autocomplete results to load and tab through them.",
|
||||||
|
});
|
||||||
|
return success();
|
||||||
|
}),
|
||||||
|
|
||||||
// Change your nickname
|
// Change your nickname
|
||||||
nick: new Command("nick", "<display_name>", function(room_id, args) {
|
nick: new Command("nick", "<display_name>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Q from 'q';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import type {Completion, SelectionRange} from './Autocompleter';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
||||||
|
@ -14,18 +14,24 @@ export default class AutocompleteProvider {
|
||||||
/**
|
/**
|
||||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
||||||
*/
|
*/
|
||||||
getCurrentCommand(query: string, selection: {start: number, end: number}): ?Array<string> {
|
getCurrentCommand(query: string, selection: {start: number, end: number}, force: boolean = false): ?string {
|
||||||
if (this.commandRegex == null) {
|
let commandRegex = this.commandRegex;
|
||||||
|
|
||||||
|
if (force && this.shouldForceComplete()) {
|
||||||
|
console.log('forcing complete');
|
||||||
|
commandRegex = /[^\W]+/g;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandRegex == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.commandRegex.lastIndex = 0;
|
commandRegex.lastIndex = 0;
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
while ((match = this.commandRegex.exec(query)) != null) {
|
while ((match = commandRegex.exec(query)) != null) {
|
||||||
let matchStart = match.index,
|
let matchStart = match.index,
|
||||||
matchEnd = matchStart + match[0].length;
|
matchEnd = matchStart + match[0].length;
|
||||||
|
|
||||||
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
||||||
return {
|
return {
|
||||||
command: match,
|
command: match,
|
||||||
|
@ -45,8 +51,8 @@ export default class AutocompleteProvider {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||||
return Q.when([]);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getName(): string {
|
getName(): string {
|
||||||
|
@ -57,4 +63,9 @@ export default class AutocompleteProvider {
|
||||||
console.error('stub; should be implemented in subclasses');
|
console.error('stub; should be implemented in subclasses');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether we should provide completions even if triggered forcefully, without a sigil.
|
||||||
|
shouldForceComplete(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,63 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import type {Component} from 'react';
|
||||||
import CommandProvider from './CommandProvider';
|
import CommandProvider from './CommandProvider';
|
||||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
import RoomProvider from './RoomProvider';
|
import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
|
import Q from 'q';
|
||||||
|
|
||||||
|
export type SelectionRange = {
|
||||||
|
start: number,
|
||||||
|
end: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Completion = {
|
||||||
|
completion: string,
|
||||||
|
component: ?Component,
|
||||||
|
range: SelectionRange,
|
||||||
|
command: ?string,
|
||||||
|
};
|
||||||
|
|
||||||
const PROVIDERS = [
|
const PROVIDERS = [
|
||||||
UserProvider,
|
UserProvider,
|
||||||
CommandProvider,
|
|
||||||
DuckDuckGoProvider,
|
|
||||||
RoomProvider,
|
RoomProvider,
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
|
CommandProvider,
|
||||||
|
DuckDuckGoProvider,
|
||||||
].map(completer => completer.getInstance());
|
].map(completer => completer.getInstance());
|
||||||
|
|
||||||
export function getCompletions(query: string, selection: {start: number, end: number}) {
|
// Providers will get rejected if they take longer than this.
|
||||||
return PROVIDERS.map(provider => {
|
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
export async function getCompletions(query: string, selection: SelectionRange, force: boolean = false): Array<Completion> {
|
||||||
|
/* Note: That this waits for all providers to return is *intentional*
|
||||||
|
otherwise, we run into a condition where new completions are displayed
|
||||||
|
while the user is interacting with the list, which makes it difficult
|
||||||
|
to predict whether an action will actually do what is intended
|
||||||
|
|
||||||
|
It ends up containing a list of Q promise states, which are objects with
|
||||||
|
state (== "fulfilled" || "rejected") and value. */
|
||||||
|
const completionsList = await Q.allSettled(
|
||||||
|
PROVIDERS.map(provider => {
|
||||||
|
return Q(provider.getCompletions(query, selection, force))
|
||||||
|
.timeout(PROVIDER_COMPLETION_TIMEOUT);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return completionsList
|
||||||
|
.filter(completion => completion.state === "fulfilled")
|
||||||
|
.map((completionsState, i) => {
|
||||||
return {
|
return {
|
||||||
completions: provider.getCompletions(query, selection),
|
completions: completionsState.value,
|
||||||
provider,
|
provider: PROVIDERS[i],
|
||||||
|
|
||||||
|
/* the currently matched "command" the completer tried to complete
|
||||||
|
* we pass this through so that Autocomplete can figure out when to
|
||||||
|
* re-show itself once hidden.
|
||||||
|
*/
|
||||||
|
command: PROVIDERS[i].getCurrentCommand(query, selection, force),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ const COMMANDS = [
|
||||||
{
|
{
|
||||||
command: '/invite',
|
command: '/invite',
|
||||||
args: '<user-id>',
|
args: '<user-id>',
|
||||||
description: 'Invites user with given id to current room'
|
description: 'Invites user with given id to current room',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: '/join',
|
command: '/join',
|
||||||
|
@ -40,6 +39,11 @@ const COMMANDS = [
|
||||||
args: '<display-name>',
|
args: '<display-name>',
|
||||||
description: 'Changes your display nickname',
|
description: 'Changes your display nickname',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
command: '/ddg',
|
||||||
|
args: '<query>',
|
||||||
|
description: 'Searches DuckDuckGo for results',
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
let COMMAND_RE = /(^\/\w*)/g;
|
let COMMAND_RE = /(^\/\w*)/g;
|
||||||
|
@ -54,7 +58,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
|
@ -70,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Q.when(completions);
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
@ -20,17 +19,16 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!query || !command) {
|
if (!query || !command) {
|
||||||
return Q.when([]);
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
|
const response = await fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
})
|
});
|
||||||
.then(response => response.json())
|
const json = await response.json();
|
||||||
.then(json => {
|
|
||||||
let results = json.Results.map(result => {
|
let results = json.Results.map(result => {
|
||||||
return {
|
return {
|
||||||
completion: result.Text,
|
completion: result.Text,
|
||||||
|
@ -74,7 +72,6 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
|
import type {SelectionRange, Completion} from './Autocompleter';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
const EMOJI_REGEX = /:\w*:?/g;
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
|
@ -17,7 +17,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
this.fuse = new Fuse(EMOJI_SHORTNAMES);
|
this.fuse = new Fuse(EMOJI_SHORTNAMES);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: SelectionRange) {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
|
@ -35,7 +35,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
};
|
};
|
||||||
}).slice(0, 8);
|
}).slice(0, 8);
|
||||||
}
|
}
|
||||||
return Q.when(completions);
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
|
@ -21,12 +20,12 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
let client = MatrixClientPeg.get();
|
let client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
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.fuse.set(client.getRooms().filter(room => !!room).map(room => {
|
||||||
|
@ -48,7 +47,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
}).slice(0, 4);
|
||||||
}
|
}
|
||||||
return Q.when(completions);
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
@ -68,4 +67,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
{completions}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldForceComplete(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
|
||||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
this.fuse.set(this.users);
|
this.fuse.set(this.users);
|
||||||
completions = this.fuse.search(command[0]).map(user => {
|
completions = this.fuse.search(command[0]).map(user => {
|
||||||
|
@ -37,11 +37,11 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
title={displayName}
|
title={displayName}
|
||||||
description={user.userId} />
|
description={user.userId} />
|
||||||
),
|
),
|
||||||
range
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
}).slice(0, 4);
|
||||||
}
|
}
|
||||||
return Q.when(completions);
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
@ -64,4 +64,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
{completions}
|
{completions}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldForceComplete(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,17 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import flatMap from 'lodash/flatMap';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
|
const COMPOSER_SELECTED = 0;
|
||||||
|
|
||||||
export default class Autocomplete extends React.Component {
|
export default class Autocomplete extends React.Component {
|
||||||
|
completionPromise: Promise = null;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -19,79 +25,137 @@ export default class Autocomplete extends React.Component {
|
||||||
// array of completions, so we can look up current selection by offset quickly
|
// array of completions, so we can look up current selection by offset quickly
|
||||||
completionList: [],
|
completionList: [],
|
||||||
|
|
||||||
// how far down the completion list we are
|
// how far down the completion list we are (THIS IS 1-INDEXED!)
|
||||||
selectionOffset: 0,
|
selectionOffset: COMPOSER_SELECTED,
|
||||||
|
|
||||||
|
// whether we should show completions if they're available
|
||||||
|
shouldShowCompletions: true,
|
||||||
|
|
||||||
|
hide: false,
|
||||||
|
|
||||||
|
forceComplete: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props, state) {
|
async componentWillReceiveProps(props, state) {
|
||||||
if (props.query === this.props.query) {
|
if (props.query === this.props.query) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(props.query, props.selection).forEach(completionResult => {
|
const completionList = flatMap(completions, provider => provider.completions);
|
||||||
try {
|
|
||||||
completionResult.completions.then(completions => {
|
|
||||||
let i = this.state.completions.findIndex(
|
|
||||||
completion => completion.provider === completionResult.provider
|
|
||||||
);
|
|
||||||
|
|
||||||
i = i === -1 ? this.state.completions.length : i;
|
// Reset selection when completion list becomes empty.
|
||||||
let newCompletions = Object.assign([], this.state.completions);
|
let selectionOffset = COMPOSER_SELECTED;
|
||||||
completionResult.completions = completions;
|
if (completionList.length > 0) {
|
||||||
newCompletions[i] = completionResult;
|
/* If the currently selected completion is still in the completion list,
|
||||||
|
try to find it and jump to it. If not, select composer.
|
||||||
|
*/
|
||||||
|
const currentSelection = this.state.selectionOffset === 0 ? null :
|
||||||
|
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||||
|
selectionOffset = completionList.findIndex(
|
||||||
|
completion => completion.completion === currentSelection);
|
||||||
|
if (selectionOffset === -1) {
|
||||||
|
selectionOffset = COMPOSER_SELECTED;
|
||||||
|
} else {
|
||||||
|
selectionOffset++; // selectionOffset is 1-indexed!
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no completions were returned, we should turn off force completion.
|
||||||
|
forceComplete = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hide = this.state.hide;
|
||||||
|
// 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),
|
||||||
|
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
|
||||||
|
if (!isEqual(oldMatches, newMatches)) {
|
||||||
|
hide = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
completions: newCompletions,
|
completions,
|
||||||
completionList: flatMap(newCompletions, provider => provider.completions),
|
completionList,
|
||||||
});
|
selectionOffset,
|
||||||
}, err => {
|
hide,
|
||||||
console.error(err);
|
forceComplete,
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// An error in one provider shouldn't mess up the rest.
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
countCompletions(): number {
|
countCompletions(): number {
|
||||||
return this.state.completions.map(completionResult => {
|
return this.state.completionList.length;
|
||||||
return completionResult.completions.length;
|
|
||||||
}).reduce((l, r) => l + r);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// called from MessageComposerInput
|
// called from MessageComposerInput
|
||||||
onUpArrow(): boolean {
|
onUpArrow(): ?Completion {
|
||||||
let completionCount = this.countCompletions(),
|
const completionCount = this.countCompletions();
|
||||||
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
|
// completionCount + 1, since 0 means composer is selected
|
||||||
|
const selectionOffset = (completionCount + 1 + this.state.selectionOffset - 1)
|
||||||
|
% (completionCount + 1);
|
||||||
if (!completionCount) {
|
if (!completionCount) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
this.setSelection(selectionOffset);
|
this.setSelection(selectionOffset);
|
||||||
return true;
|
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// called from MessageComposerInput
|
// called from MessageComposerInput
|
||||||
onDownArrow(): boolean {
|
onDownArrow(): ?Completion {
|
||||||
let completionCount = this.countCompletions(),
|
const completionCount = this.countCompletions();
|
||||||
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
|
// completionCount + 1, since 0 means composer is selected
|
||||||
|
const selectionOffset = (this.state.selectionOffset + 1) % (completionCount + 1);
|
||||||
if (!completionCount) {
|
if (!completionCount) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
this.setSelection(selectionOffset);
|
this.setSelection(selectionOffset);
|
||||||
return true;
|
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
onEscape(e): boolean {
|
||||||
|
const completionCount = this.countCompletions();
|
||||||
|
if (completionCount === 0) {
|
||||||
|
// autocomplete is already empty, so don't preventDefault
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// selectionOffset = 0, so we don't end up completing when autocomplete is hidden
|
||||||
|
this.setState({hide: true, selectionOffset: 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
forceComplete() {
|
||||||
|
this.setState({
|
||||||
|
forceComplete: true,
|
||||||
|
}, () => {
|
||||||
|
this.complete(this.props.query, this.props.selection);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** called from MessageComposerInput
|
/** called from MessageComposerInput
|
||||||
* @returns {boolean} whether confirmation was handled
|
* @returns {boolean} whether confirmation was handled
|
||||||
*/
|
*/
|
||||||
onConfirm(): boolean {
|
onConfirm(): boolean {
|
||||||
if (this.countCompletions() === 0) {
|
if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
|
let selectedCompletion = this.state.completionList[this.state.selectionOffset - 1];
|
||||||
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
|
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -117,7 +181,7 @@ export default class Autocomplete extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let position = 0;
|
let position = 1;
|
||||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
let completions = completionResult.completions.map((completion, i) => {
|
let completions = completionResult.completions.map((completion, i) => {
|
||||||
|
|
||||||
|
@ -135,7 +199,7 @@ export default class Autocomplete extends React.Component {
|
||||||
|
|
||||||
return React.cloneElement(completion.component, {
|
return React.cloneElement(completion.component, {
|
||||||
key: i,
|
key: i,
|
||||||
ref: `completion${i}`,
|
ref: `completion${position - 1}`,
|
||||||
className,
|
className,
|
||||||
onMouseOver,
|
onMouseOver,
|
||||||
onClick,
|
onClick,
|
||||||
|
@ -151,7 +215,7 @@ export default class Autocomplete extends React.Component {
|
||||||
) : null;
|
) : null;
|
||||||
}).filter(completion => !!completion);
|
}).filter(completion => !!completion);
|
||||||
|
|
||||||
return 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}>
|
||||||
{renderedCompletions}
|
{renderedCompletions}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
_onAutocompleteConfirm(range, completion) {
|
_onAutocompleteConfirm(range, completion) {
|
||||||
if (this.messageComposerInput) {
|
if (this.messageComposerInput) {
|
||||||
this.messageComposerInput.onConfirmAutocompletion(range, completion);
|
this.messageComposerInput.setDisplayedCompletion(range, completion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,7 +313,6 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
|
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
|
||||||
{autoComplete}
|
|
||||||
<div className="mx_MessageComposer_wrapper">
|
<div className="mx_MessageComposer_wrapper">
|
||||||
<div className="mx_MessageComposer_row">
|
<div className="mx_MessageComposer_row">
|
||||||
{controls}
|
{controls}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||||
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';
|
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 MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
|
@ -46,6 +47,8 @@ import KeyCode from '../../../KeyCode';
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
import * as RichText from '../../../RichText';
|
import * as RichText from '../../../RichText';
|
||||||
|
import Autocomplete from './Autocomplete';
|
||||||
|
import {Completion} from "../../../autocomplete/Autocompleter";
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
@ -88,34 +91,52 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return getDefaultKeyBinding(e);
|
return getDefaultKeyBinding(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getBlockStyle(block: ContentBlock): ?string {
|
||||||
|
if (block.getType() === 'strikethrough') {
|
||||||
|
return 'mx_Markdown_STRIKETHROUGH';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
|
autocomplete: Autocomplete;
|
||||||
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.onAction = this.onAction.bind(this);
|
this.onAction = this.onAction.bind(this);
|
||||||
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.setEditorState = this.setEditorState.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.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
this.onEscape = this.onEscape.bind(this);
|
||||||
|
this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this);
|
||||||
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
|
|
||||||
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
|
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
// whether we're in rich text or markdown mode
|
||||||
isRichtextEnabled,
|
isRichtextEnabled,
|
||||||
|
|
||||||
|
// the currently displayed editor state (note: this is always what is modified on input)
|
||||||
editorState: null,
|
editorState: null,
|
||||||
|
|
||||||
|
// the original editor state, before we started tabbing through completions
|
||||||
|
originalEditorState: 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
|
||||||
|
/* eslint react/no-direct-mutation-state:0 */
|
||||||
this.state.editorState = this.createEditorState();
|
this.state.editorState = this.createEditorState();
|
||||||
|
|
||||||
this.client = MatrixClientPeg.get();
|
this.client = MatrixClientPeg.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* "Does the right thing" to create an EditorState, based on:
|
* "Does the right thing" to create an EditorState, based on:
|
||||||
* - whether we've got rich text mode enabled
|
* - whether we've got rich text mode enabled
|
||||||
* - contentState was passed in
|
* - contentState was passed in
|
||||||
|
@ -234,10 +255,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.refs.editor,
|
this.refs.editor,
|
||||||
this.props.room.roomId
|
this.props.room.roomId
|
||||||
);
|
);
|
||||||
// this is disabled for now, since https://github.com/matrix-org/matrix-react-sdk/pull/296 will land soon
|
|
||||||
// if (this.props.tabComplete) {
|
|
||||||
// this.props.tabComplete.setEditor(this.refs.editor);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -273,7 +290,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
);
|
);
|
||||||
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());
|
||||||
this.setEditorState(editorState);
|
this.onEditorContentChanged(editorState);
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -295,10 +312,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
startSelection,
|
startSelection,
|
||||||
blockMap);
|
blockMap);
|
||||||
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
|
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
|
||||||
if (this.state.isRichtextEnabled)
|
if (this.state.isRichtextEnabled) {
|
||||||
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');
|
||||||
this.setEditorState(editorState);
|
this.onEditorContentChanged(editorState);
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -372,10 +390,16 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called by Draft to change editor contents, and by setEditorState
|
||||||
setEditorState(editorState: EditorState, cb = () => null) {
|
onEditorContentChanged(editorState: EditorState, didRespondToUserInput: boolean = true) {
|
||||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||||
this.setState({editorState}, cb);
|
|
||||||
|
const setPromise = Q.defer();
|
||||||
|
/* If a modification was made, set originalEditorState to null, since newState is now our original */
|
||||||
|
this.setState({
|
||||||
|
editorState,
|
||||||
|
originalEditorState: didRespondToUserInput ? null : this.state.originalEditorState,
|
||||||
|
}, () => setPromise.resolve());
|
||||||
|
|
||||||
if (editorState.getCurrentContent().hasText()) {
|
if (editorState.getCurrentContent().hasText()) {
|
||||||
this.onTypingActivity();
|
this.onTypingActivity();
|
||||||
|
@ -390,6 +414,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
this.props.onContentChanged(textContent, selection);
|
this.props.onContentChanged(textContent, selection);
|
||||||
}
|
}
|
||||||
|
return setPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditorState(editorState: EditorState) {
|
||||||
|
this.onEditorContentChanged(editorState, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
enableRichtext(enabled: boolean) {
|
enableRichtext(enabled: boolean) {
|
||||||
|
@ -470,7 +499,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
handleReturn(ev) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
this.setEditorState(RichUtils.insertSoftNewline(this.state.editorState));
|
this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -547,41 +576,68 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpArrow(e) {
|
async onUpArrow(e) {
|
||||||
if (this.props.onUpArrow && this.props.onUpArrow()) {
|
const completion = this.autocomplete.onUpArrow();
|
||||||
|
if (completion != null) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
return await this.setDisplayedCompletion(completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onDownArrow(e) {
|
||||||
|
const completion = this.autocomplete.onDownArrow();
|
||||||
|
e.preventDefault();
|
||||||
|
return await this.setDisplayedCompletion(completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tab and shift-tab are mapped to down and up arrow respectively
|
||||||
|
async onTab(e) {
|
||||||
|
e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes
|
||||||
|
const didTab = await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e);
|
||||||
|
if (!didTab && this.autocomplete) {
|
||||||
|
this.autocomplete.forceComplete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDownArrow(e) {
|
onEscape(e) {
|
||||||
if (this.props.onDownArrow && this.props.onDownArrow()) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (this.autocomplete) {
|
||||||
|
this.autocomplete.onEscape(e);
|
||||||
}
|
}
|
||||||
|
this.setDisplayedCompletion(null); // restore originalEditorState
|
||||||
}
|
}
|
||||||
|
|
||||||
onTab(e) {
|
/* If passed null, restores the original editor content from state.originalEditorState.
|
||||||
if (this.props.tryComplete) {
|
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
|
||||||
if (this.props.tryComplete()) {
|
*/
|
||||||
e.preventDefault();
|
async setDisplayedCompletion(displayedCompletion: ?Completion): boolean {
|
||||||
}
|
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||||
|
|
||||||
|
if (displayedCompletion == null) {
|
||||||
|
if (this.state.originalEditorState) {
|
||||||
|
this.setEditorState(this.state.originalEditorState);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfirmAutocompletion(range, content: string) {
|
const {range = {}, completion = ''} = displayedCompletion;
|
||||||
|
|
||||||
let contentState = Modifier.replaceText(
|
let contentState = Modifier.replaceText(
|
||||||
this.state.editorState.getCurrentContent(),
|
activeEditorState.getCurrentContent(),
|
||||||
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
|
RichText.textOffsetsToSelectionState(range, activeEditorState.getCurrentContent().getBlocksAsArray()),
|
||||||
content
|
completion
|
||||||
);
|
);
|
||||||
|
|
||||||
let editorState = EditorState.push(this.state.editorState, 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.setEditorState(editorState);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
||||||
|
@ -632,22 +688,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.handleKeyCommand('toggle-mode');
|
this.handleKeyCommand('toggle-mode');
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockStyle(block: ContentBlock): ?string {
|
|
||||||
if (block.getType() === 'strikethrough') {
|
|
||||||
return 'mx_Markdown_STRIKETHROUGH';
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {editorState} = this.state;
|
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||||
|
|
||||||
// From https://github.com/facebook/draft-js/blob/master/examples/rich/rich.html#L92
|
// From https://github.com/facebook/draft-js/blob/master/examples/rich/rich.html#L92
|
||||||
// If the user changes block type before entering any text, we can
|
// If the user changes block type before entering any text, we can
|
||||||
// either style the placeholder or hide it.
|
// either style the placeholder or hide it.
|
||||||
let hidePlaceholder = false;
|
let hidePlaceholder = false;
|
||||||
const contentState = editorState.getCurrentContent();
|
const contentState = activeEditorState.getCurrentContent();
|
||||||
if (!contentState.hasText()) {
|
if (!contentState.hasText()) {
|
||||||
if (contentState.getBlockMap().first().getType() !== 'unstyled') {
|
if (contentState.getBlockMap().first().getType() !== 'unstyled') {
|
||||||
hidePlaceholder = true;
|
hidePlaceholder = true;
|
||||||
|
@ -658,7 +706,20 @@ export default class MessageComposerInput extends React.Component {
|
||||||
mx_MessageComposer_input_empty: hidePlaceholder,
|
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const content = activeEditorState.getCurrentContent();
|
||||||
|
const contentText = content.getPlainText();
|
||||||
|
const selection = RichText.selectionStateToTextOffsets(activeEditorState.getSelection(),
|
||||||
|
activeEditorState.getCurrentContent().getBlocksAsArray());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="mx_MessageComposer_input_wrapper">
|
||||||
|
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
|
<Autocomplete
|
||||||
|
ref={(e) => this.autocomplete = e}
|
||||||
|
onConfirm={this.setDisplayedCompletion}
|
||||||
|
query={contentText}
|
||||||
|
selection={selection} />
|
||||||
|
</div>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<img className="mx_MessageComposer_input_markdownIndicator"
|
<img className="mx_MessageComposer_input_markdownIndicator"
|
||||||
onMouseDown={this.onMarkdownToggleClicked}
|
onMouseDown={this.onMarkdownToggleClicked}
|
||||||
|
@ -667,8 +728,8 @@ export default class MessageComposerInput extends React.Component {
|
||||||
<Editor ref="editor"
|
<Editor ref="editor"
|
||||||
placeholder="Type a message…"
|
placeholder="Type a message…"
|
||||||
editorState={this.state.editorState}
|
editorState={this.state.editorState}
|
||||||
onChange={this.setEditorState}
|
onChange={this.onEditorContentChanged}
|
||||||
blockStyleFn={this.getBlockStyle}
|
blockStyleFn={MessageComposerInput.getBlockStyle}
|
||||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
handleReturn={this.handleReturn}
|
handleReturn={this.handleReturn}
|
||||||
|
@ -676,8 +737,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
onTab={this.onTab}
|
onTab={this.onTab}
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
onDownArrow={this.onDownArrow}
|
onDownArrow={this.onDownArrow}
|
||||||
|
onEscape={this.onEscape}
|
||||||
spellCheck={true} />
|
spellCheck={true} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue