feat: implement autocomplete replacement
This commit is contained in:
parent
8961c87cf9
commit
cccc58b47f
13 changed files with 271 additions and 121 deletions
|
@ -37,6 +37,7 @@
|
|||
"glob": "^5.0.14",
|
||||
"highlight.js": "^8.9.1",
|
||||
"linkifyjs": "^2.0.0-beta.4",
|
||||
"lodash": "^4.13.1",
|
||||
"marked": "^0.3.5",
|
||||
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
|
||||
"optimist": "^0.6.1",
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Editor,
|
||||
Modifier,
|
||||
ContentState,
|
||||
ContentBlock,
|
||||
convertFromHTML,
|
||||
DefaultDraftBlockRenderMap,
|
||||
DefaultDraftInlineStyle,
|
||||
CompositeDecorator,
|
||||
SelectionState
|
||||
SelectionState,
|
||||
} from 'draft-js';
|
||||
import * as sdk from './index';
|
||||
import * as emojione from 'emojione';
|
||||
|
@ -25,7 +27,7 @@ const STYLES = {
|
|||
CODE: 'code',
|
||||
ITALIC: 'em',
|
||||
STRIKETHROUGH: 's',
|
||||
UNDERLINE: 'u'
|
||||
UNDERLINE: 'u',
|
||||
};
|
||||
|
||||
const MARKDOWN_REGEX = {
|
||||
|
@ -168,7 +170,7 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
|||
text = "";
|
||||
|
||||
|
||||
for(let currentKey = startKey;
|
||||
for (let currentKey = startKey;
|
||||
currentKey && currentKey !== endKey;
|
||||
currentKey = contentState.getKeyAfter(currentKey)) {
|
||||
let blockText = getText(currentKey);
|
||||
|
@ -189,14 +191,14 @@ export function modifyText(contentState: ContentState, rangeToReplace: Selection
|
|||
* Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
|
||||
* Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
|
||||
*/
|
||||
export function getTextSelectionOffsets(selectionState: SelectionState,
|
||||
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
||||
export function selectionStateToTextOffsets(selectionState: SelectionState,
|
||||
contentBlocks: Array<ContentBlock>): {start: number, end: number} {
|
||||
let offset = 0, start = 0, end = 0;
|
||||
for(let block of contentBlocks) {
|
||||
if (selectionState.getStartKey() == block.getKey()) {
|
||||
if (selectionState.getStartKey() === block.getKey()) {
|
||||
start = offset + selectionState.getStartOffset();
|
||||
}
|
||||
if (selectionState.getEndKey() == block.getKey()) {
|
||||
if (selectionState.getEndKey() === block.getKey()) {
|
||||
end = offset + selectionState.getEndOffset();
|
||||
break;
|
||||
}
|
||||
|
@ -205,6 +207,37 @@ export function getTextSelectionOffsets(selectionState: SelectionState,
|
|||
|
||||
return {
|
||||
start,
|
||||
end
|
||||
}
|
||||
end,
|
||||
};
|
||||
}
|
||||
|
||||
export function textOffsetsToSelectionState({start, end}: {start: number, end: number},
|
||||
contentBlocks: Array<ContentBlock>): SelectionState {
|
||||
let selectionState = SelectionState.createEmpty();
|
||||
|
||||
for (let block of contentBlocks) {
|
||||
let blockLength = block.getLength();
|
||||
|
||||
if (start !== -1 && start < blockLength) {
|
||||
selectionState = selectionState.merge({
|
||||
anchorKey: block.getKey(),
|
||||
anchorOffset: start,
|
||||
});
|
||||
start = -1;
|
||||
} else {
|
||||
start -= blockLength;
|
||||
}
|
||||
|
||||
if (end !== -1 && end <= blockLength) {
|
||||
selectionState = selectionState.merge({
|
||||
focusKey: block.getKey(),
|
||||
focusOffset: end,
|
||||
});
|
||||
end = -1;
|
||||
} else {
|
||||
end -= blockLength;
|
||||
}
|
||||
}
|
||||
|
||||
return selectionState;
|
||||
}
|
||||
|
|
|
@ -14,25 +14,36 @@ export default class AutocompleteProvider {
|
|||
* 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> {
|
||||
if(this.commandRegex == null)
|
||||
if (this.commandRegex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let match = null;
|
||||
while((match = this.commandRegex.exec(query)) != null) {
|
||||
let match;
|
||||
while ((match = this.commandRegex.exec(query)) != null) {
|
||||
let matchStart = match.index,
|
||||
matchEnd = matchStart + match[0].length;
|
||||
|
||||
console.log(match);
|
||||
|
||||
if(selection.start <= matchEnd && selection.end >= matchStart) {
|
||||
return match;
|
||||
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
||||
return {
|
||||
command: match,
|
||||
range: {
|
||||
start: matchStart,
|
||||
end: matchEnd,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
this.commandRegex.lastIndex = 0;
|
||||
return null;
|
||||
return {
|
||||
command: null,
|
||||
range: {
|
||||
start: -1,
|
||||
end: -1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCompletions(query: String, selection: {start: number, end: number}) {
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
return Q.when([]);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,14 +9,14 @@ const PROVIDERS = [
|
|||
CommandProvider,
|
||||
DuckDuckGoProvider,
|
||||
RoomProvider,
|
||||
EmojiProvider
|
||||
EmojiProvider,
|
||||
].map(completer => completer.getInstance());
|
||||
|
||||
export function getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
return PROVIDERS.map(provider => {
|
||||
return {
|
||||
completions: provider.getCompletions(query, selection),
|
||||
provider
|
||||
provider,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,42 +1,45 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
command: '/me',
|
||||
args: '<message>',
|
||||
description: 'Displays action'
|
||||
description: 'Displays action',
|
||||
},
|
||||
{
|
||||
command: '/ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Bans user with given id'
|
||||
description: 'Bans user with given id',
|
||||
},
|
||||
{
|
||||
command: '/deop'
|
||||
command: '/deop',
|
||||
args: '<user-id>',
|
||||
description: 'Deops user with given id',
|
||||
},
|
||||
{
|
||||
command: '/encrypt'
|
||||
},
|
||||
{
|
||||
command: '/invite'
|
||||
command: '/invite',
|
||||
args: '<user-id>',
|
||||
description: 'Invites user with given id to current room'
|
||||
},
|
||||
{
|
||||
command: '/join',
|
||||
args: '<room-alias>',
|
||||
description: 'Joins room with given alias'
|
||||
description: 'Joins room with given alias',
|
||||
},
|
||||
{
|
||||
command: '/kick',
|
||||
args: '<user-id> [reason]',
|
||||
description: 'Kicks user with given id'
|
||||
description: 'Kicks user with given id',
|
||||
},
|
||||
{
|
||||
command: '/nick',
|
||||
args: '<display-name>',
|
||||
description: 'Changes your display nickname'
|
||||
}
|
||||
description: 'Changes your display nickname',
|
||||
},
|
||||
];
|
||||
|
||||
let COMMAND_RE = /(^\/\w*)/g;
|
||||
|
@ -47,19 +50,23 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
constructor() {
|
||||
super(COMMAND_RE);
|
||||
this.fuse = new Fuse(COMMANDS, {
|
||||
keys: ['command', 'args', 'description']
|
||||
keys: ['command', 'args', 'description'],
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
const command = this.getCurrentCommand(query, selection);
|
||||
if(command) {
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
completions = this.fuse.search(command[0]).map(result => {
|
||||
return {
|
||||
title: result.command,
|
||||
subtitle: result.args,
|
||||
description: result.description
|
||||
completion: result.command + ' ',
|
||||
component: (<TextualCompletion
|
||||
title={result.command}
|
||||
subtitle={result.args}
|
||||
description={result.description}
|
||||
/>),
|
||||
range,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -71,7 +78,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
static getInstance(): CommandProvider {
|
||||
if(instance == null)
|
||||
if (instance == null)
|
||||
instance = new CommandProvider();
|
||||
|
||||
return instance;
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
export function TextualCompletion(props: {
|
||||
import React from 'react';
|
||||
|
||||
export function TextualCompletion({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
}: {
|
||||
title: ?string,
|
||||
subtitle: ?string,
|
||||
description: ?string
|
||||
}) {
|
||||
return (
|
||||
<div className="mx_Autocomplete_Completion">
|
||||
<span>{completion.title}</span>
|
||||
<em>{completion.subtitle}</em>
|
||||
<span style={{color: 'gray', float: 'right'}}>{completion.description}</span>
|
||||
<span>{title}</span>
|
||||
<em>{subtitle}</em>
|
||||
<span style={{color: 'gray', float: 'right'}}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||
const REFERER = 'vector';
|
||||
const REFERRER = 'vector';
|
||||
|
||||
let instance = null;
|
||||
|
||||
|
@ -14,42 +17,62 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
|||
|
||||
static getQueryUri(query: String) {
|
||||
return `http://api.duckduckgo.com/?q=${encodeURIComponent(query)}`
|
||||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERER)}`;
|
||||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let command = this.getCurrentCommand(query, selection);
|
||||
if(!query || !command)
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (!query || !command) {
|
||||
return Q.when([]);
|
||||
}
|
||||
|
||||
return fetch(DuckDuckGoProvider.getQueryUri(command[1]), {
|
||||
method: 'GET'
|
||||
method: 'GET',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
let results = json.Results.map(result => {
|
||||
return {
|
||||
title: result.Text,
|
||||
description: result.Result
|
||||
completion: result.Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={result.Text}
|
||||
description={result.Result} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
});
|
||||
if(json.Answer) {
|
||||
if (json.Answer) {
|
||||
results.unshift({
|
||||
title: json.Answer,
|
||||
description: json.AnswerType
|
||||
completion: json.Answer,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.Answer}
|
||||
description={json.AnswerType} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if(json.RelatedTopics && json.RelatedTopics.length > 0) {
|
||||
if (json.RelatedTopics && json.RelatedTopics.length > 0) {
|
||||
results.unshift({
|
||||
title: json.RelatedTopics[0].Text
|
||||
completion: json.RelatedTopics[0].Text,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.RelatedTopics[0].Text} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
if(json.AbstractText) {
|
||||
if (json.AbstractText) {
|
||||
results.unshift({
|
||||
title: json.AbstractText
|
||||
completion: json.AbstractText,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={json.AbstractText} />
|
||||
),
|
||||
range,
|
||||
});
|
||||
}
|
||||
// console.log(results);
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
@ -59,9 +82,9 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
static getInstance(): DuckDuckGoProvider {
|
||||
if(instance == null)
|
||||
if (instance == null) {
|
||||
instance = new DuckDuckGoProvider();
|
||||
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import {emojioneList, shortnameToImage} from 'emojione';
|
||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const EMOJI_REGEX = /:\w*:?/g;
|
||||
|
@ -16,18 +17,19 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
let command = this.getCurrentCommand(query, selection);
|
||||
if(command) {
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
completions = this.fuse.search(command[0]).map(result => {
|
||||
let shortname = EMOJI_SHORTNAMES[result];
|
||||
let imageHTML = shortnameToImage(shortname);
|
||||
return {
|
||||
title: shortname,
|
||||
completion: shortnameToUnicode(shortname),
|
||||
component: (
|
||||
<div className="mx_Autocomplete_Completion">
|
||||
<span dangerouslySetInnerHTML={{__html: imageHTML}}></span> {shortname}
|
||||
</div>
|
||||
)
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
|
@ -39,7 +41,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
static getInstance() {
|
||||
if(instance == null)
|
||||
if (instance == null)
|
||||
instance = new EmojiProvider();
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const ROOM_REGEX = /(?=#)([^\s]*)/g;
|
||||
|
||||
|
@ -10,32 +12,35 @@ let instance = null;
|
|||
export default class RoomProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(ROOM_REGEX, {
|
||||
keys: ['displayName', 'userId']
|
||||
keys: ['displayName', 'userId'],
|
||||
});
|
||||
this.fuse = new Fuse([], {
|
||||
keys: ['name', 'roomId', 'aliases']
|
||||
keys: ['name', 'roomId', 'aliases'],
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let client = MatrixClientPeg.get();
|
||||
let completions = [];
|
||||
const command = this.getCurrentCommand(query, selection);
|
||||
if(command) {
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
// 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 => {
|
||||
return {
|
||||
name: room.name,
|
||||
roomId: room.roomId,
|
||||
aliases: room.getAliases()
|
||||
aliases: room.getAliases(),
|
||||
};
|
||||
}));
|
||||
completions = this.fuse.search(command[0]).map(room => {
|
||||
return {
|
||||
title: room.name,
|
||||
subtitle: room.roomId
|
||||
completion: room.roomId,
|
||||
component: (
|
||||
<TextualCompletion title={room.name} subtitle={room.roomId} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
}).slice(0, 4);;
|
||||
}).slice(0, 4);
|
||||
}
|
||||
return Q.when(completions);
|
||||
}
|
||||
|
@ -45,8 +50,9 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
static getInstance() {
|
||||
if(instance == null)
|
||||
if (instance == null) {
|
||||
instance = new RoomProvider();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import Q from 'q';
|
||||
import Fuse from 'fuse.js';
|
||||
import {TextualCompletion} from './Components';
|
||||
|
||||
const USER_REGEX = /@[^\s]*/g;
|
||||
|
||||
|
@ -9,23 +11,27 @@ let instance = null;
|
|||
export default class UserProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(USER_REGEX, {
|
||||
keys: ['displayName', 'userId']
|
||||
keys: ['displayName', 'userId'],
|
||||
});
|
||||
this.users = [];
|
||||
this.fuse = new Fuse([], {
|
||||
keys: ['displayName', 'userId']
|
||||
})
|
||||
keys: ['displayName', 'userId'],
|
||||
});
|
||||
}
|
||||
|
||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||
let completions = [];
|
||||
let command = this.getCurrentCommand(query, selection);
|
||||
if(command) {
|
||||
let {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
this.fuse.set(this.users);
|
||||
completions = this.fuse.search(command[0]).map(user => {
|
||||
return {
|
||||
title: user.displayName || user.userId,
|
||||
description: user.userId
|
||||
completion: user.userId,
|
||||
component: (
|
||||
<TextualCompletion
|
||||
title={user.displayName || user.userId}
|
||||
description={user.userId} />
|
||||
),
|
||||
};
|
||||
}).slice(0, 4);
|
||||
}
|
||||
|
@ -41,8 +47,9 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
static getInstance(): UserProvider {
|
||||
if(instance == null)
|
||||
if (instance == null) {
|
||||
instance = new UserProvider();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
import React from 'react';
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||
|
||||
export default class Autocomplete extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
|
||||
this.state = {
|
||||
// list of completionResults, each containing completions
|
||||
completions: [],
|
||||
|
||||
// array of completions, so we can look up current selection by offset quickly
|
||||
completionList: [],
|
||||
|
||||
// how far down the completion list we are
|
||||
selectionOffset: 0,
|
||||
};
|
||||
|
@ -31,8 +39,10 @@ export default class Autocomplete extends React.Component {
|
|||
let newCompletions = Object.assign([], this.state.completions);
|
||||
completionResult.completions = completions;
|
||||
newCompletions[i] = completionResult;
|
||||
|
||||
this.setState({
|
||||
completions: newCompletions,
|
||||
completionList: _.flatMap(newCompletions, provider => provider.completions),
|
||||
});
|
||||
}, err => {
|
||||
console.error(err);
|
||||
|
@ -54,7 +64,7 @@ export default class Autocomplete extends React.Component {
|
|||
onUpArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (completionCount + this.state.selectionOffset - 1) % completionCount;
|
||||
this.setState({selectionOffset});
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -62,34 +72,49 @@ export default class Autocomplete extends React.Component {
|
|||
onDownArrow(): boolean {
|
||||
let completionCount = this.countCompletions(),
|
||||
selectionOffset = (this.state.selectionOffset + 1) % completionCount;
|
||||
this.setState({selectionOffset});
|
||||
this.setSelection(selectionOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** called from MessageComposerInput
|
||||
* @returns {boolean} whether confirmation was handled
|
||||
*/
|
||||
onConfirm(): boolean {
|
||||
if (this.countCompletions() === 0)
|
||||
return false;
|
||||
|
||||
let selectedCompletion = this.state.completionList[this.state.selectionOffset];
|
||||
this.props.onConfirm(selectedCompletion.range, selectedCompletion.completion);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setSelection(selectionOffset: number) {
|
||||
this.setState({selectionOffset});
|
||||
}
|
||||
|
||||
render() {
|
||||
let position = 0;
|
||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
let completions = completionResult.completions.map((completion, i) => {
|
||||
let Component = completion.component;
|
||||
let className = classNames('mx_Autocomplete_Completion', {
|
||||
'selected': position === this.state.selectionOffset,
|
||||
});
|
||||
let componentPosition = position;
|
||||
position++;
|
||||
if (Component) {
|
||||
return Component;
|
||||
}
|
||||
|
||||
let onMouseOver = () => this.setState({selectionOffset: componentPosition});
|
||||
|
||||
let onMouseOver = () => this.setSelection(componentPosition),
|
||||
onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onConfirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={i}
|
||||
className={className}
|
||||
onMouseOver={onMouseOver}>
|
||||
<span style={{fontWeight: 600}}>{completion.title}</span>
|
||||
<span>{completion.subtitle}</span>
|
||||
<span style={{flex: 1}} />
|
||||
<span style={{color: 'gray'}}>{completion.description}</span>
|
||||
onMouseOver={onMouseOver}
|
||||
onClick={onClick}>
|
||||
{completion.component}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,16 +40,17 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
this.state = {
|
||||
autocompleteQuery: '',
|
||||
selection: null
|
||||
selection: null,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
onUploadClick(ev) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
|
||||
Modal.createDialog(NeedToRegisterDialog, {
|
||||
title: "Please Register",
|
||||
description: "Guest users can't upload files. Please register to upload."
|
||||
description: "Guest users can't upload files. Please register to upload.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -58,13 +59,13 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
onUploadFileSelected(ev) {
|
||||
var files = ev.target.files;
|
||||
let files = ev.target.files;
|
||||
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
let TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var fileList = [];
|
||||
for(var i=0; i<files.length; i++) {
|
||||
let fileList = [];
|
||||
for (let i=0; i<files.length; i++) {
|
||||
fileList.push(<li>
|
||||
<TintableSvg key={i} src="img/files.svg" width="16" height="16" /> {files[i].name}
|
||||
</li>);
|
||||
|
@ -91,7 +92,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
this.refs.uploadInput.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -105,7 +106,7 @@ export default class MessageComposer extends React.Component {
|
|||
action: 'hangup',
|
||||
// hangup the call for this room, which may not be the room in props
|
||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||
room_id: call.roomId
|
||||
room_id: call.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -113,7 +114,7 @@ export default class MessageComposer extends React.Component {
|
|||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -121,14 +122,14 @@ export default class MessageComposer extends React.Component {
|
|||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: 'voice',
|
||||
room_id: this.props.room.roomId
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
this.setState({
|
||||
autocompleteQuery: content,
|
||||
selection
|
||||
selection,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -171,11 +172,11 @@ export default class MessageComposer extends React.Component {
|
|||
callButton =
|
||||
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
|
||||
<TintableSvg src="img/voice.svg" width="16" height="26"/>
|
||||
</div>
|
||||
</div>;
|
||||
videoCallButton =
|
||||
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
|
||||
<TintableSvg src="img/call.svg" width="30" height="22"/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var canSendMessages = this.props.room.currentState.maySendMessage(
|
||||
|
@ -198,9 +199,11 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
controls.push(
|
||||
<MessageComposerInput
|
||||
ref={c => this.messageComposerInput = c}
|
||||
key="controls_input"
|
||||
onResize={this.props.onResize}
|
||||
room={this.props.room}
|
||||
tryComplete={this.refs.autocomplete && this.refs.autocomplete.onConfirm}
|
||||
onUpArrow={this.onUpArrow}
|
||||
onDownArrow={this.onDownArrow}
|
||||
onTab={this.onTab}
|
||||
|
@ -223,6 +226,7 @@ export default class MessageComposer extends React.Component {
|
|||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
<Autocomplete
|
||||
ref="autocomplete"
|
||||
onConfirm={this.messageComposerInput && this.messageComposerInput.onConfirmAutocompletion}
|
||||
query={this.state.autocompleteQuery}
|
||||
selection={this.state.selection} />
|
||||
</div>
|
||||
|
|
|
@ -76,6 +76,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
this.onUpArrow = this.onUpArrow.bind(this);
|
||||
this.onDownArrow = this.onDownArrow.bind(this);
|
||||
this.onTab = this.onTab.bind(this);
|
||||
this.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
||||
|
||||
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
|
||||
if(isRichtextEnabled == null) {
|
||||
|
@ -85,7 +86,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
this.state = {
|
||||
isRichtextEnabled: isRichtextEnabled,
|
||||
editorState: null
|
||||
editorState: null,
|
||||
};
|
||||
|
||||
// bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled
|
||||
|
@ -96,7 +97,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
static getKeyBinding(e: SyntheticKeyboardEvent): string {
|
||||
// C-m => Toggles between rich text and markdown modes
|
||||
if(e.keyCode == KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
|
||||
return 'toggle-mode';
|
||||
}
|
||||
|
||||
|
@ -212,7 +213,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
let content = convertFromRaw(JSON.parse(contentJSON));
|
||||
component.setEditorState(component.createEditorState(component.state.isRichtextEnabled, content));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -234,7 +235,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
onAction(payload) {
|
||||
var editor = this.refs.editor;
|
||||
let editor = this.refs.editor;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'focus_composer':
|
||||
|
@ -252,7 +253,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
payload.displayname
|
||||
);
|
||||
this.setState({
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters')
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
||||
});
|
||||
editor.focus();
|
||||
}
|
||||
|
@ -356,7 +357,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
|
||||
if(this.props.onContentChanged) {
|
||||
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
|
||||
RichText.getTextSelectionOffsets(editorState.getSelection(),
|
||||
RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||
editorState.getCurrentContent().getBlocksAsArray()));
|
||||
}
|
||||
}
|
||||
|
@ -418,12 +419,21 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
handleReturn(ev) {
|
||||
if(ev.shiftKey)
|
||||
if (ev.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.props.tryComplete) {
|
||||
if(this.props.tryComplete()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const contentState = this.state.editorState.getCurrentContent();
|
||||
if(!contentState.hasText())
|
||||
if (!contentState.hasText()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
let contentText = contentState.getPlainText(), contentHTML;
|
||||
|
||||
|
@ -509,17 +519,32 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
|
||||
onTab(e) {
|
||||
if(this.props.onTab) {
|
||||
if(this.props.onTab()) {
|
||||
if (this.props.onTab) {
|
||||
if (this.props.onTab()) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConfirmAutocompletion(range, content: string) {
|
||||
let contentState = Modifier.replaceText(
|
||||
this.state.editorState.getCurrentContent(),
|
||||
RichText.textOffsetsToSelectionState(range, this.state.editorState.getCurrentContent().getBlocksAsArray()),
|
||||
content
|
||||
);
|
||||
|
||||
this.setState({
|
||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
||||
});
|
||||
|
||||
// for some reason, doing this right away does not update the editor :(
|
||||
setTimeout(() => this.refs.editor.focus(), 50);
|
||||
}
|
||||
|
||||
render() {
|
||||
let className = "mx_MessageComposer_input";
|
||||
|
||||
if(this.state.isRichtextEnabled) {
|
||||
if (this.state.isRichtextEnabled) {
|
||||
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue