Merge branch 'bwindels/redesign' into bwindels/resizehandles

This commit is contained in:
Bruno Windels 2018-10-16 11:57:59 +02:00
commit 01471abdc5
89 changed files with 2635 additions and 1287 deletions

View file

@ -30,6 +30,7 @@ import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@ -197,17 +198,15 @@ module.exports = React.createClass({
if (this.props.showApps &&
this._canUserModify()
) {
addWidget = <div
addWidget = <AccessibleButton
onClick={this.onClickAddWidget}
role='button'
tabIndex='0'
className={this.state.apps.length<2 ?
'mx_AddWidget_button mx_AddWidget_button_full_width' :
'mx_AddWidget_button'
}
title={_t('Add a widget')}>
[+] { _t('Add a widget') }
</div>;
</AccessibleButton>;
}
let spinner;

View file

@ -114,7 +114,7 @@ export default class Autocomplete extends React.Component {
processQuery(query, selection) {
return this.autocompleter.getCompletions(
query, selection, this.state.forceComplete
query, selection, this.state.forceComplete,
).then((completions) => {
// Only ever process the completions for the most recent query being processed
if (query !== this.queryRequested) {

View file

@ -277,7 +277,11 @@ module.exports = withMatrixClient(React.createClass({
return false;
}
for (let j = 0; j < rA.length; j++) {
if (rA[j].roomMember.userId !== rB[j].roomMember.userId) {
if (rA[j].userId !== rB[j].userId) {
return false;
}
// one has a member set and the other doesn't?
if (rA[j].roomMember !== rB[j].roomMember) {
return false;
}
}
@ -359,7 +363,7 @@ module.exports = withMatrixClient(React.createClass({
// else set it proportional to index
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
const userId = receipt.roomMember.userId;
const userId = receipt.userId;
let readReceiptInfo;
if (this.props.readReceiptMap) {
@ -373,6 +377,7 @@ module.exports = withMatrixClient(React.createClass({
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
<ReadReceiptMarker key={userId} member={receipt.roomMember}
fallbackUserId={userId}
leftOffset={left} hidden={hidden}
readReceiptInfo={readReceiptInfo}
checkUnmounting={this.props.checkUnmounting}

View file

@ -777,7 +777,7 @@ module.exports = withMatrixClient(React.createClass({
const myMembership = room.getMyMembership();
// not a DM room if we have are not joined
if (myMembership !== 'join') continue;
const them = this.props.member;
// not a DM room if they are not joined
if (!them.membership || them.membership !== 'join') continue;
@ -935,7 +935,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo">
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" alt={_t('Close')} />
</AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />

View file

@ -292,21 +292,22 @@ export default class MessageComposer extends React.Component {
let videoCallButton;
let hangupButton;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
// Call buttons
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<AccessibleButton 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" />
</div>;
</AccessibleButton>;
} else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
<AccessibleButton key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
<TintableSvg src="img/icon-call.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
<AccessibleButton key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
<TintableSvg src="img/icons-video.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
}
const canSendMessages = !this.state.tombstone &&
@ -317,18 +318,19 @@ export default class MessageComposer extends React.Component {
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
const uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
<AccessibleButton key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title={_t('Upload file')}>
<TintableSvg src="img/icons-upload.svg" width="35" height="35" />
<input ref="uploadInput" type="file"
style={uploadInputStyle}
multiple
onChange={this.onUploadFileSelected} />
</div>
</AccessibleButton>
);
const formattingButton = this.state.inputState.isRichTextEnabled ? (
<img className="mx_MessageComposer_formatting"
<AccessibleButton element="img" className="mx_MessageComposer_formatting"
alt={_t("Show Text Formatting Toolbar")}
title={_t("Show Text Formatting Toolbar")}
src="img/button-text-formatting.svg"
onClick={this.onToggleFormattingClicked}
@ -372,7 +374,6 @@ export default class MessageComposer extends React.Component {
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon" src="img/room_replaced.svg" />
@ -423,7 +424,7 @@ export default class MessageComposer extends React.Component {
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichTextEnabled}.png`} />
<img title={_t("Hide Text Formatting Toolbar")}
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />

View file

@ -106,6 +106,17 @@ const MARK_TAGS = {
s: 'deleted', // deprecated
};
const SLATE_SCHEMA = {
inlines: {
pill: {
isVoid: true,
},
emoji: {
isVoid: true,
},
},
};
function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
@ -116,10 +127,10 @@ function onSendMessageFailed(err, room) {
}
function rangeEquals(a: Range, b: Range): boolean {
return (a.anchorKey === b.anchorKey
&& a.anchorOffset === b.anchorOffset
&& a.focusKey === b.focusKey
&& a.focusOffset === b.focusOffset
return (a.anchor.key === b.anchor.key
&& a.anchor.offset === b.anchorOffset
&& a.focus.key === b.focusKey
&& a.focus.offset === b.focusOffset
&& a.isFocused === b.isFocused
&& a.isBackward === b.isBackward);
}
@ -213,7 +224,7 @@ export default class MessageComposerInput extends React.Component {
object: 'block',
type: type,
nodes: next(el.childNodes),
}
};
}
type = MARK_TAGS[tag];
if (type) {
@ -221,7 +232,7 @@ export default class MessageComposerInput extends React.Component {
object: 'mark',
type: type,
nodes: next(el.childNodes),
}
};
}
// special case links
if (tag === 'a') {
@ -239,16 +250,14 @@ export default class MessageComposerInput extends React.Component {
completion: el.innerText,
completionId: m[1],
},
isVoid: true,
}
}
else {
};
} else {
return {
object: 'inline',
type: 'link',
data: { href },
nodes: next(el.childNodes),
}
};
}
}
},
@ -258,14 +267,12 @@ export default class MessageComposerInput extends React.Component {
node: obj,
children: children,
});
}
else if (obj.object === 'mark') {
} else if (obj.object === 'mark') {
return this.renderMark({
mark: obj,
children: children,
});
}
else if (obj.object === 'inline') {
} else if (obj.object === 'inline') {
// special case links, pills and emoji otherwise we
// end up with React components getting rendered out(!)
switch (obj.type) {
@ -285,9 +292,9 @@ export default class MessageComposerInput extends React.Component {
children: children,
});
}
}
}
]
},
},
],
});
const savedState = MessageComposerStore.getEditorState(this.props.room.roomId);
@ -345,9 +352,13 @@ export default class MessageComposerInput extends React.Component {
dis.unregister(this.dispatcherRef);
}
_collectEditor = (e) => {
this._editor = e;
}
onAction = (payload) => {
const editor = this.refs.editor;
let editorState = this.state.editorState;
const editor = this._editor;
const editorState = this.state.editorState;
switch (payload.action) {
case 'reply_to_event':
@ -361,7 +372,7 @@ export default class MessageComposerInput extends React.Component {
const selection = this.getSelectionRange(this.state.editorState);
const member = this.props.room.getMember(payload.user_id);
const completion = member ?
member.rawDisplayName.replace(' (IRC)', '') : payload.user_id;
member.rawDisplayName : payload.user_id;
this.setDisplayedCompletion({
completion,
completionId: payload.user_id,
@ -402,20 +413,24 @@ export default class MessageComposerInput extends React.Component {
}
// XXX: this is to bring back the focus in a sane place and add a paragraph after it
change = change.select({
anchorKey: quote.key,
focusKey: quote.key,
}).collapseToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus();
change = change.select(Range.create({
anchor: {
key: quote.key,
},
focus: {
key: quote.key,
},
})).moveToEndOfBlock().insertBlock(Block.create(DEFAULT_NODE)).focus();
this.onChange(change);
} else {
let fragmentChange = fragment.change();
fragmentChange.moveToRangeOf(fragment.document)
const fragmentChange = fragment.change();
fragmentChange.moveToRangeOfNode(fragment.document)
.wrapBlock(quote);
// FIXME: handle pills and use commonmark rather than md-serialize
const md = this.md.serialize(fragmentChange.value);
let change = editorState.change()
const change = editorState.change()
.insertText(md + '\n\n')
.focus();
this.onChange(change);
@ -497,15 +512,15 @@ export default class MessageComposerInput extends React.Component {
if (this.direction !== '') {
const focusedNode = editorState.focusInline || editorState.focusText;
if (focusedNode.isVoid) {
if (editorState.schema.isVoid(focusedNode)) {
// XXX: does this work in RTL?
const edge = this.direction === 'Previous' ? 'End' : 'Start';
if (editorState.isCollapsed) {
change = change[`collapseTo${ edge }Of${ this.direction }Text`]();
if (editorState.selection.isCollapsed) {
change = change[`moveTo${ edge }Of${ this.direction }Text`]();
} else {
const block = this.direction === 'Previous' ? editorState.previousText : editorState.nextText;
if (block) {
change = change[`moveFocusTo${ edge }Of`](block);
change = change[`moveFocusTo${ edge }OfNode`](block);
}
}
editorState = change.value;
@ -517,12 +532,11 @@ export default class MessageComposerInput extends React.Component {
if (this.autocomplete.state.completionList.length > 0 && !this.autocomplete.state.hide &&
!rangeEquals(this.state.editorState.selection, editorState.selection) &&
// XXX: the heuristic failed when inlines like pills weren't taken into account. This is inideal
this.state.editorState.document.toJSON() === editorState.document.toJSON())
{
this.state.editorState.document.toJSON() === editorState.document.toJSON()) {
this.autocomplete.hide();
}
if (!editorState.document.isEmpty) {
if (Plain.serialize(editorState) !== '') {
this.onTypingActivity();
} else {
this.onFinishedTyping();
@ -543,10 +557,14 @@ export default class MessageComposerInput extends React.Component {
const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]);
const range = Range.create({
anchorKey: editorState.selection.startKey,
anchorOffset: currentStartOffset - emojiMatch[1].length - 1,
focusKey: editorState.selection.startKey,
focusOffset: currentStartOffset - 1,
anchor: {
key: editorState.selection.startKey,
offset: currentStartOffset - emojiMatch[1].length - 1,
},
focus: {
key: editorState.selection.startKey,
offset: currentStartOffset - 1,
},
});
change = change.insertTextAtRange(range, unicodeEmoji);
editorState = change.value;
@ -560,15 +578,18 @@ export default class MessageComposerInput extends React.Component {
let match;
while ((match = EMOJI_REGEX.exec(node.text)) !== null) {
const range = Range.create({
anchorKey: node.key,
anchorOffset: match.index,
focusKey: node.key,
focusOffset: match.index + match[0].length,
anchor: {
key: node.key,
offset: match.index,
},
focus: {
key: node.key,
offset: match.index + match[0].length,
},
});
const inline = Inline.create({
type: 'emoji',
data: { emojiUnicode: match[0] },
isVoid: true,
});
change = change.insertInlineAtRange(range, inline);
editorState = change.value;
@ -580,10 +601,9 @@ export default class MessageComposerInput extends React.Component {
// emoji picker can leave the selection stuck in the emoji's
// child text. This seems to happen due to selection getting
// moved in the normalisation phase after calculating these changes
if (editorState.anchorKey &&
editorState.document.getParent(editorState.anchorKey).type === 'emoji')
{
change = change.collapseToStartOfNextText();
if (editorState.selection.anchor.key &&
editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') {
change = change.moveToStartOfNextText();
editorState = change.value;
}
@ -595,15 +615,14 @@ export default class MessageComposerInput extends React.Component {
const parent = editorState.document.getParent(editorState.blocks.first().key);
if (parent.type === 'numbered-list') {
blockType = 'numbered-list';
}
else if (parent.type === 'bulleted-list') {
} else if (parent.type === 'bulleted-list') {
blockType = 'bulleted-list';
}
}
const inputState = {
marks: editorState.activeMarks,
isRichTextEnabled: this.state.isRichTextEnabled,
blockType
blockType,
};
this.props.onInputStateChanged(inputState);
}
@ -613,7 +632,7 @@ export default class MessageComposerInput extends React.Component {
this.setState({
editorState,
originalEditorState: originalEditorState || null
originalEditorState: originalEditorState || null,
});
};
@ -653,7 +672,7 @@ export default class MessageComposerInput extends React.Component {
// which doesn't roundtrip symmetrically with commonmark, which we use for
// compiling MD out of the MD editor state above.
this.md.serialize(editorState),
{ defaultBlock: DEFAULT_NODE }
{ defaultBlock: DEFAULT_NODE },
);
}
@ -673,11 +692,11 @@ export default class MessageComposerInput extends React.Component {
editorState: this.createEditorState(enabled, editorState),
isRichTextEnabled: enabled,
}, ()=>{
this.refs.editor.focus();
this._editor.focus();
});
SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
};
}
/**
* Check if the current selection has a mark with `type` in it.
@ -687,8 +706,8 @@ export default class MessageComposerInput extends React.Component {
*/
hasMark = type => {
const { editorState } = this.state
return editorState.activeMarks.some(mark => mark.type === type)
const { editorState } = this.state;
return editorState.activeMarks.some(mark => mark.type === type);
};
/**
@ -699,20 +718,18 @@ export default class MessageComposerInput extends React.Component {
*/
hasBlock = type => {
const { editorState } = this.state
return editorState.blocks.some(node => node.type === type)
const { editorState } = this.state;
return editorState.blocks.some(node => node.type === type);
};
onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => {
this.suppressAutoComplete = false;
// skip void nodes - see
// https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
if (ev.keyCode === KeyCode.LEFT) {
this.direction = 'Previous';
}
else if (ev.keyCode === KeyCode.RIGHT) {
} else if (ev.keyCode === KeyCode.RIGHT) {
this.direction = 'Next';
} else {
this.direction = '';
@ -760,7 +777,9 @@ export default class MessageComposerInput extends React.Component {
// drop a point in history so the user can undo a word
// XXX: this seems nasty but adding to history manually seems a no-go
ev.preventDefault();
return change.setOperationFlag("skip", false).setOperationFlag("merge", false).insertText(ev.key);
return change.withoutMerging(() => {
change.insertText(ev.key);
});
};
onBackspace = (ev: KeyboardEvent, change: Change): Change => {
@ -771,23 +790,24 @@ export default class MessageComposerInput extends React.Component {
const { editorState } = this.state;
// Allow Ctrl/Cmd-Backspace when focus starts at the start of the composer (e.g select-all)
// for some reason if slate sees you Ctrl-backspace and your anchorOffset=0 it just resets your focus
if (!editorState.isCollapsed && editorState.anchorOffset === 0) {
// for some reason if slate sees you Ctrl-backspace and your anchor.offset=0 it just resets your focus
// XXX: Doing this now seems to put slate into a broken state, and it didn't appear to be doing
// what it claims to do on the old version of slate anyway...
/*if (!editorState.isCollapsed && editorState.selection.anchor.offset === 0) {
return change.delete();
}
}*/
if (this.state.isRichTextEnabled) {
// let backspace exit lists
const isList = this.hasBlock('list-item');
if (isList && editorState.anchorOffset == 0) {
if (isList && editorState.selection.anchor.offset == 0) {
change
.setBlocks(DEFAULT_NODE)
.unwrapBlock('bulleted-list')
.unwrapBlock('numbered-list');
return change;
}
else if (editorState.anchorOffset == 0 && editorState.isCollapsed) {
} else if (editorState.selection.anchor.offset == 0 && editorState.isCollapsed) {
// turn blocks back into paragraphs
if ((this.hasBlock('block-quote') ||
this.hasBlock('heading1') ||
@ -796,20 +816,18 @@ export default class MessageComposerInput extends React.Component {
this.hasBlock('heading4') ||
this.hasBlock('heading5') ||
this.hasBlock('heading6') ||
this.hasBlock('code')))
{
this.hasBlock('code'))) {
return change.setBlocks(DEFAULT_NODE);
}
// remove paragraphs entirely if they're nested
const parent = editorState.document.getParent(editorState.anchorBlock.key);
if (editorState.anchorOffset == 0 &&
if (editorState.selection.anchor.offset == 0 &&
this.hasBlock('paragraph') &&
parent.nodes.size == 1 &&
parent.object !== 'document')
{
parent.object !== 'document') {
return change.replaceNodeByKey(editorState.anchorBlock.key, editorState.anchorText)
.collapseToEndOf(parent)
.moveToEndOfNode(parent)
.focus();
}
}
@ -823,7 +841,7 @@ export default class MessageComposerInput extends React.Component {
return true;
}
let newState: ?Value = null;
const newState: ?Value = null;
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
if (this.state.isRichTextEnabled) {
@ -849,7 +867,7 @@ export default class MessageComposerInput extends React.Component {
} else if (isList) {
change
.unwrapBlock(
type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list'
type === 'bulleted-list' ? 'numbered-list' : 'bulleted-list',
)
.wrapBlock(type);
} else {
@ -942,8 +960,8 @@ export default class MessageComposerInput extends React.Component {
const collapseAndOffsetSelection = (selection, offset) => {
const key = selection.endKey();
return new Range({
anchorKey: key, anchorOffset: offset,
focusKey: key, focusOffset: offset,
anchorKey: key, anchor.offset: offset,
focus.key: key, focus.offset: offset,
});
};
@ -1000,18 +1018,16 @@ export default class MessageComposerInput extends React.Component {
.insertFragment(fragment.document);
} else {
// in MD mode we don't want the rich content pasted as the magic was annoying people so paste plain
return change
.setOperationFlag("skip", false)
.setOperationFlag("merge", false)
.insertText(transfer.text);
return change.withoutMerging(() => {
change.insertText(transfer.text);
});
}
}
case 'text':
// don't skip/merge so that multiple consecutive pastes can be undone individually
return change
.setOperationFlag("skip", false)
.setOperationFlag("merge", false)
.insertText(transfer.text);
return change.withoutMerging(() => {
change.insertText(transfer.text);
});
}
};
@ -1054,8 +1070,7 @@ export default class MessageComposerInput extends React.Component {
const firstGrandChild = firstChild && firstChild.nodes.get(0);
if (firstChild && firstGrandChild &&
firstChild.object === 'block' && firstGrandChild.object === 'text' &&
firstGrandChild.text[0] === '/')
{
firstGrandChild.text[0] === '/') {
commandText = this.plainWithIdPills.serialize(editorState);
cmd = processCommandInput(this.props.room.roomId, commandText);
}
@ -1066,7 +1081,7 @@ export default class MessageComposerInput extends React.Component {
this.setState({
editorState: this.createEditorState(),
}, ()=>{
this.refs.editor.focus();
this._editor.focus();
});
}
if (cmd.promise) {
@ -1196,7 +1211,7 @@ export default class MessageComposerInput extends React.Component {
this.setState({
editorState: this.createEditorState(),
}, ()=>{ this.refs.editor.focus() });
}, ()=>{ this._editor.focus(); });
return true;
};
@ -1216,9 +1231,9 @@ export default class MessageComposerInput extends React.Component {
// and we must be at the edge of the document (up=start, down=end)
if (up) {
if (!selection.isAtStartOf(document)) return;
if (!selection.anchor.isAtStartOfNode(document)) return;
} else {
if (!selection.isAtEndOf(document)) return;
if (!selection.anchor.isAtEndOfNode(document)) return;
}
const selected = this.selectHistory(up);
@ -1266,7 +1281,7 @@ export default class MessageComposerInput extends React.Component {
}
// Move selection to the end of the selected history
const change = editorState.change().collapseToEndOf(editorState.document);
const change = editorState.change().moveToEndOfNode(editorState.document);
// We don't call this.onChange(change) now, as fixups on stuff like emoji
// should already have been done and persisted in the history.
@ -1275,7 +1290,7 @@ export default class MessageComposerInput extends React.Component {
this.suppressAutoComplete = true;
this.setState({ editorState }, ()=>{
this.refs.editor.focus();
this._editor.focus();
});
return true;
};
@ -1326,7 +1341,7 @@ export default class MessageComposerInput extends React.Component {
if (displayedCompletion == null) {
if (this.state.originalEditorState) {
let editorState = this.state.originalEditorState;
const editorState = this.state.originalEditorState;
this.setState({editorState});
}
return false;
@ -1337,7 +1352,7 @@ export default class MessageComposerInput extends React.Component {
completion = '',
completionId = '',
href = null,
suffix = ''
suffix = '',
} = displayedCompletion;
let inline;
@ -1345,15 +1360,11 @@ export default class MessageComposerInput extends React.Component {
inline = Inline.create({
type: 'pill',
data: { completion, completionId, href },
// we can't put text in here otherwise the editor tries to select it
isVoid: true,
});
} else if (completion === '@room') {
inline = Inline.create({
type: 'pill',
data: { completion, completionId },
// we can't put text in here otherwise the editor tries to select it
isVoid: true,
});
}
@ -1361,8 +1372,9 @@ export default class MessageComposerInput extends React.Component {
if (range) {
const change = editorState.change()
.collapseToAnchor()
.moveOffsetsTo(range.start, range.end)
.moveToAnchor()
.moveAnchorTo(range.start)
.moveFocusTo(range.end)
.focus();
editorState = change.value;
}
@ -1373,8 +1385,7 @@ export default class MessageComposerInput extends React.Component {
.insertInlineAtRange(editorState.selection, inline)
.insertText(suffix)
.focus();
}
else {
} else {
change = editorState.change()
.insertTextAtRange(editorState.selection, completion)
.insertText(suffix)
@ -1433,17 +1444,17 @@ export default class MessageComposerInput extends React.Component {
room={this.props.room}
shouldShowPillAvatar={shouldShowPillAvatar}
isSelected={isSelected}
{...attributes}
/>;
}
else if (Pill.isPillUrl(url)) {
} else if (Pill.isPillUrl(url)) {
return <Pill
url={url}
room={this.props.room}
shouldShowPillAvatar={shouldShowPillAvatar}
isSelected={isSelected}
{...attributes}
/>;
}
else {
} else {
const { text } = node;
return <a href={url} {...props.attributes}>
{ text }
@ -1456,9 +1467,11 @@ export default class MessageComposerInput extends React.Component {
const uri = RichText.unicodeToEmojiUri(emojiUnicode);
const shortname = toShort(emojiUnicode);
const className = classNames('mx_emojione', {
mx_emojione_selected: isSelected
mx_emojione_selected: isSelected,
});
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode }/>;
const style = {};
if (props.selected) style.border = '1px solid blue';
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode } style={style} />;
}
}
};
@ -1486,7 +1499,7 @@ export default class MessageComposerInput extends React.Component {
// of focusing it doesn't then cancel the format button being pressed
// FIXME: can we just tell handleKeyCommand's change to invoke .focus()?
if (document.activeElement && document.activeElement.className !== 'mx_MessageComposer_editor') {
this.refs.editor.focus();
this._editor.focus();
setTimeout(()=>{
this.handleKeyCommand(name);
}, 500); // can't find any callback to hook this to. onFocus and onChange and willComponentUpdate fire too early.
@ -1503,10 +1516,9 @@ export default class MessageComposerInput extends React.Component {
// This avoids us having to serialize the whole thing to plaintext and convert
// selection offsets in & out of the plaintext domain.
if (editorState.selection.anchorKey) {
return editorState.document.getDescendant(editorState.selection.anchorKey).text;
}
else {
if (editorState.selection.anchor.key) {
return editorState.document.getDescendant(editorState.selection.anchor.key).text;
} else {
return '';
}
}
@ -1518,17 +1530,17 @@ export default class MessageComposerInput extends React.Component {
const firstGrandChild = firstChild && firstChild.nodes.get(0);
beginning = (firstChild && firstGrandChild &&
firstChild.object === 'block' && firstGrandChild.object === 'text' &&
editorState.selection.anchorKey === firstGrandChild.key);
editorState.selection.anchor.key === firstGrandChild.key);
// return a character range suitable for handing to an autocomplete provider.
// the range is relative to the anchor of the current editor selection.
// if the selection spans multiple blocks, then we collapse it for the calculation.
const range = {
beginning, // whether the selection is in the first block of the editor or not
start: editorState.selection.anchorOffset,
end: (editorState.selection.anchorKey == editorState.selection.focusKey) ?
editorState.selection.focusOffset : editorState.selection.anchorOffset,
}
start: editorState.selection.anchor.offset,
end: (editorState.selection.anchor.key == editorState.selection.focus.key) ?
editorState.selection.focus.offset : editorState.selection.anchor.offset,
};
if (range.start > range.end) {
const tmp = range.start;
range.start = range.end;
@ -1543,7 +1555,7 @@ export default class MessageComposerInput extends React.Component {
};
focusComposer = () => {
this.refs.editor.focus();
this._editor.focus();
};
render() {
@ -1553,7 +1565,7 @@ export default class MessageComposerInput extends React.Component {
mx_MessageComposer_input_error: this.state.someCompletions === false,
});
const isEmpty = this.state.editorState.document.isEmpty;
const isEmpty = Plain.serialize(this.state.editorState) === '';
let {placeholder} = this.props;
// XXX: workaround for placeholder being shown when there is a formatting block e.g blockquote but no text
@ -1579,7 +1591,7 @@ export default class MessageComposerInput extends React.Component {
onMouseDown={this.onMarkdownToggleClicked}
title={this.state.isRichTextEnabled ? _t("Markdown is disabled") : _t("Markdown is enabled")}
src={`img/button-md-${!this.state.isRichTextEnabled}.png`} />
<Editor ref="editor"
<Editor ref={this._collectEditor}
dir="auto"
className="mx_MessageComposer_editor"
placeholder={placeholder}
@ -1591,6 +1603,7 @@ export default class MessageComposerInput extends React.Component {
renderMark={this.renderMark}
// disable spell check for the placeholder because browsers don't like "unencrypted"
spellCheck={!isEmpty}
schema={SLATE_SCHEMA}
/>
</div>
</div>

View file

@ -41,7 +41,10 @@ module.exports = React.createClass({
propTypes: {
// the RoomMember to show the RR for
member: PropTypes.object.isRequired,
member: PropTypes.object,
// userId to fallback the avatar to
// if the member hasn't been loaded yet
fallbackUserId: PropTypes.string.isRequired,
// number of pixels to offset the avatar from the right of its parent;
// typically a negative value.
@ -130,8 +133,7 @@ module.exports = React.createClass({
// the docs for `offsetParent` say it may be null if `display` is
// `none`, but I can't see why that would happen.
console.warn(
`ReadReceiptMarker for ${this.props.member.userId} in ` +
`${this.props.member.roomId} has no offsetParent`,
`ReadReceiptMarker for ${this.props.fallbackUserId} in has no offsetParent`,
);
startTopOffset = 0;
} else {
@ -186,17 +188,17 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
if (this.props.member.userId === this.props.member.rawDisplayName) {
if (!this.props.member || this.props.fallbackUserId === this.props.member.rawDisplayName) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId,
{userName: this.props.fallbackUserId,
dateTime: dateString},
);
} else {
title = _t(
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
{displayName: this.props.member.rawDisplayName,
userName: this.props.member.userId,
userName: this.props.fallbackUserId,
dateTime: dateString},
);
}
@ -208,6 +210,7 @@ module.exports = React.createClass({
enterTransitionOpts={this.state.enterTransitionOpts} >
<MemberAvatar
member={this.props.member}
fallbackUserId={this.props.fallbackUserId}
aria-hidden="true"
width={14} height={14} resizeMethod="crop"
style={style}

View file

@ -16,7 +16,7 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
module.exports = React.createClass({
displayName: 'RoomDropTarget',
@ -31,5 +31,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -16,11 +16,11 @@ limitations under the License.
'use strict';
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var classNames = require('classnames');
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const React = require('react');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
const classNames = require('classnames');
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -28,7 +28,7 @@ module.exports = React.createClass({
getInitialState: function() {
return ({
scope: 'Room'
scope: 'Room',
});
},
@ -54,18 +54,18 @@ module.exports = React.createClass({
},
render: function() {
var searchButtonClasses = classNames({ mx_SearchBar_searchButton : true, mx_SearchBar_searching: this.props.searchInProgress });
var thisRoomClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'Room' });
var allRoomsClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'All' });
const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress });
const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' });
const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' });
return (
<div className="mx_SearchBar">
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange}/>
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")}/></AccessibleButton>
<div className="mx_SearchBar">
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")} /></AccessibleButton>
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
</div>
);
}
},
});

View file

@ -151,8 +151,8 @@ export default class Stickerpicker extends React.Component {
<AccessibleButton onClick={this._launchManageIntegrations}
className='mx_Stickers_contentPlaceholder'>
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
<p className='mx_Stickers_addLink'>Add some now</p>
<img src='img/stickerpack-placeholder.png' alt={_t('Add a stickerpack')} />
<p className='mx_Stickers_addLink'>{ _t("Add some now") }</p>
<img src='img/stickerpack-placeholder.png' alt="" />
</AccessibleButton>
);
}
@ -344,7 +344,7 @@ export default class Stickerpicker extends React.Component {
if (this.state.showStickers) {
// Show hide-stickers button
stickersButton =
<div
<AccessibleButton
id='stickersButton'
key="controls_hide_stickers"
className="mx_MessageComposer_stickers mx_Stickers_hideStickers"
@ -352,18 +352,18 @@ export default class Stickerpicker extends React.Component {
ref='target'
title={_t("Hide Stickers")}>
<TintableSvg src="img/icons-hide-stickers.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
} else {
// Show show-stickers button
stickersButton =
<div
<AccessibleButton
id='stickersButton'
key="constrols_show_stickers"
key="controls_show_stickers"
className="mx_MessageComposer_stickers"
onClick={this._onShowStickersClick}
title={_t("Show Stickers")}>
<TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
}
return <div>
{stickersButton}