Merge branch 'develop' into matthew/notif-panel
This commit is contained in:
commit
904348e62a
14 changed files with 898 additions and 149 deletions
|
@ -26,10 +26,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.1.2",
|
||||||
"draft-js": "^0.7.0",
|
"draft-js": "^0.8.1",
|
||||||
"draft-js-export-html": "^0.2.2",
|
"draft-js-export-html": "^0.4.0",
|
||||||
"draft-js-export-markdown": "^0.2.0",
|
"draft-js-export-markdown": "^0.2.0",
|
||||||
"draft-js-import-markdown": "^0.1.6",
|
|
||||||
"emojione": "2.2.3",
|
"emojione": "2.2.3",
|
||||||
"favico.js": "^0.3.10",
|
"favico.js": "^0.3.10",
|
||||||
"filesize": "^3.1.2",
|
"filesize": "^3.1.2",
|
||||||
|
|
|
@ -14,64 +14,22 @@ import {
|
||||||
} from 'draft-js';
|
} from 'draft-js';
|
||||||
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';
|
||||||
const BLOCK_RENDER_MAP = DefaultDraftBlockRenderMap.set('unstyled', {
|
|
||||||
element: 'span',
|
|
||||||
/*
|
|
||||||
draft uses <div> by default which we don't really like, so we're using <span>
|
|
||||||
this is probably not a good idea since <span> is not a block level element but
|
|
||||||
we're trying to fix things in contentStateToHTML below
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
|
|
||||||
const STYLES = {
|
|
||||||
BOLD: 'strong',
|
|
||||||
CODE: 'code',
|
|
||||||
ITALIC: 'em',
|
|
||||||
STRIKETHROUGH: 's',
|
|
||||||
UNDERLINE: 'u',
|
|
||||||
};
|
|
||||||
|
|
||||||
const MARKDOWN_REGEX = {
|
const MARKDOWN_REGEX = {
|
||||||
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
|
||||||
ITALIC: /([\*_])([\w\s]+?)\1/g,
|
ITALIC: /([\*_])([\w\s]+?)\1/g,
|
||||||
BOLD: /([\*_])\1([\w\s]+?)\1\1/g,
|
BOLD: /([\*_])\1([\w\s]+?)\1\1/g,
|
||||||
|
HR: /(\n|^)((-|\*|_) *){3,}(\n|$)/g,
|
||||||
|
CODE: /`[^`]*`/g,
|
||||||
|
STRIKETHROUGH: /~{2}[^~]*~{2}/g,
|
||||||
};
|
};
|
||||||
|
|
||||||
const USERNAME_REGEX = /@\S+:\S+/g;
|
const USERNAME_REGEX = /@\S+:\S+/g;
|
||||||
const ROOM_REGEX = /#\S+:\S+/g;
|
const ROOM_REGEX = /#\S+:\S+/g;
|
||||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
|
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');
|
||||||
|
|
||||||
export function contentStateToHTML(contentState: ContentState): string {
|
export const contentStateToHTML = stateToHTML;
|
||||||
return contentState.getBlockMap().map((block) => {
|
|
||||||
let elem = BLOCK_RENDER_MAP.get(block.getType()).element;
|
|
||||||
let content = [];
|
|
||||||
block.findStyleRanges(
|
|
||||||
() => true, // always return true => don't filter any ranges out
|
|
||||||
(start, end) => {
|
|
||||||
// map style names to elements
|
|
||||||
let tags = block.getInlineStyleAt(start).map(style => STYLES[style]).filter(style => !!style);
|
|
||||||
// combine them to get well-nested HTML
|
|
||||||
let open = tags.map(tag => `<${tag}>`).join('');
|
|
||||||
let close = tags.map(tag => `</${tag}>`).reverse().join('');
|
|
||||||
// and get the HTML representation of this styled range (this .substring() should never fail)
|
|
||||||
let text = block.getText().substring(start, end);
|
|
||||||
// http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
|
|
||||||
let div = document.createElement('div');
|
|
||||||
div.appendChild(document.createTextNode(text));
|
|
||||||
let safeText = div.innerHTML;
|
|
||||||
content.push(`${open}${safeText}${close}`);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let result = `<${elem}>${content.join('')}</${elem}>`;
|
|
||||||
|
|
||||||
// dirty hack because we don't want block level tags by default, but breaks
|
|
||||||
if (elem === 'span')
|
|
||||||
result += '<br />';
|
|
||||||
return result;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HTMLtoContentState(html: string): ContentState {
|
export function HTMLtoContentState(html: string): ContentState {
|
||||||
return ContentState.createFromBlockArray(convertFromHTML(html));
|
return ContentState.createFromBlockArray(convertFromHTML(html));
|
||||||
|
@ -98,6 +56,19 @@ function unicodeToEmojiUri(str) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function that looks for regex matches within a ContentBlock and invokes {callback} with (start, end)
|
||||||
|
* From https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html
|
||||||
|
*/
|
||||||
|
function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: number, end: number) => any) {
|
||||||
|
const text = contentBlock.getText();
|
||||||
|
let matchArr, start;
|
||||||
|
while ((matchArr = regex.exec(text)) !== null) {
|
||||||
|
start = matchArr.index;
|
||||||
|
callback(start, start + matchArr[0].length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Workaround for https://github.com/facebook/draft-js/issues/414
|
// Workaround for https://github.com/facebook/draft-js/issues/414
|
||||||
let emojiDecorator = {
|
let emojiDecorator = {
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentBlock, callback) => {
|
||||||
|
@ -151,7 +122,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
let markdownDecorators = ['BOLD', 'ITALIC'].map(
|
let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
|
||||||
(style) => ({
|
(style) => ({
|
||||||
strategy: (contentBlock, callback) => {
|
strategy: (contentBlock, callback) => {
|
||||||
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
|
||||||
|
@ -178,19 +149,6 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator {
|
||||||
return markdownDecorators;
|
return markdownDecorators;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function that looks for regex matches within a ContentBlock and invokes {callback} with (start, end)
|
|
||||||
* From https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html
|
|
||||||
*/
|
|
||||||
function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: number, end: number) => any) {
|
|
||||||
const text = contentBlock.getText();
|
|
||||||
let matchArr, start;
|
|
||||||
while ((matchArr = regex.exec(text)) !== null) {
|
|
||||||
start = matchArr.index;
|
|
||||||
callback(start, start + matchArr[0].length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
|
* Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
|
||||||
*/
|
*/
|
||||||
|
|
57
src/Rooms.js
57
src/Rooms.js
|
@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a room object, return the alias we should use for it,
|
* Given a room object, return the alias we should use for it,
|
||||||
|
@ -75,3 +78,57 @@ export function looksLikeDirectMessageRoom(room, me) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks or unmarks the given room as being as a DM room.
|
||||||
|
* @param {string} roomId The ID of the room to modify
|
||||||
|
* @param {string} userId The user ID of the desired DM
|
||||||
|
room target user or null to un-mark
|
||||||
|
this room as a DM room
|
||||||
|
* @returns {object} A promise
|
||||||
|
*/
|
||||||
|
export function setDMRoom(roomId, userId) {
|
||||||
|
const mDirectEvent = MatrixClientPeg.get().getAccountData('m.direct');
|
||||||
|
let dmRoomMap = {};
|
||||||
|
|
||||||
|
if (mDirectEvent !== undefined) dmRoomMap = mDirectEvent.getContent();
|
||||||
|
|
||||||
|
for (const thisUserId of Object.keys(dmRoomMap)) {
|
||||||
|
const roomList = dmRoomMap[thisUserId];
|
||||||
|
|
||||||
|
if (thisUserId == userId) {
|
||||||
|
if (roomList.indexOf(roomId) == -1) {
|
||||||
|
roomList.push(roomId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const indexOfRoom = roomList.indexOf(roomId);
|
||||||
|
if (indexOfRoom > -1) {
|
||||||
|
roomList.splice(indexOfRoom, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a room, estimate which of its members is likely to
|
||||||
|
* be the target if the room were a DM room and return that user.
|
||||||
|
*/
|
||||||
|
export function guessDMRoomTarget(room, me) {
|
||||||
|
let oldestTs;
|
||||||
|
let oldestUser;
|
||||||
|
|
||||||
|
// Pick the user who's been here longest (and isn't us)
|
||||||
|
for (const user of room.currentState.getMembers()) {
|
||||||
|
if (user.userId == me.userId) continue;
|
||||||
|
|
||||||
|
if (oldestTs === undefined || user.events.member.getTs() < oldestTs) {
|
||||||
|
oldestUser = user;
|
||||||
|
oldestTs = user.events.member.getTs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldestUser === undefined) return me;
|
||||||
|
return oldestUser;
|
||||||
|
}
|
||||||
|
|
|
@ -123,6 +123,8 @@ Example:
|
||||||
|
|
||||||
const SdkConfig = require('./SdkConfig');
|
const SdkConfig = require('./SdkConfig');
|
||||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
||||||
|
const dis = require("./dispatcher");
|
||||||
|
|
||||||
function sendResponse(event, res) {
|
function sendResponse(event, res) {
|
||||||
const data = JSON.parse(JSON.stringify(event.data));
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
@ -188,17 +190,52 @@ function setBotOptions(event, roomId, userId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setBotPower(event, roomId, userId, level) {
|
||||||
|
if (!(Number.isInteger(level) && level >= 0)) {
|
||||||
|
sendError(event, "Power level must be positive integer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, "You need to be logged in.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
||||||
|
let powerEvent = new MatrixEvent(
|
||||||
|
{
|
||||||
|
type: "m.room.power_levels",
|
||||||
|
content: powerLevels,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
sendError(event, err.message ? err.message : "Failed to send request.", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getMembershipState(event, roomId, userId) {
|
function getMembershipState(event, roomId, userId) {
|
||||||
console.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
console.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
||||||
returnStateEvent(event, roomId, "m.room.member", userId);
|
returnStateEvent(event, roomId, "m.room.member", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getJoinRules(event, roomId) {
|
||||||
|
console.log(`join_rules of ${roomId} requested.`);
|
||||||
|
returnStateEvent(event, roomId, "m.room.join_rules", "");
|
||||||
|
}
|
||||||
|
|
||||||
function botOptions(event, roomId, userId) {
|
function botOptions(event, roomId, userId) {
|
||||||
console.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
console.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
||||||
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function returnStateEvent(event, roomId, eventType, stateKey) {
|
function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
@ -218,6 +255,17 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
sendResponse(event, stateEvent.getContent());
|
sendResponse(event, stateEvent.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentRoomId = null;
|
||||||
|
|
||||||
|
// Listen for when a room is viewed
|
||||||
|
dis.register(onAction);
|
||||||
|
function onAction(payload) {
|
||||||
|
if (payload.action !== "view_room") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentRoomId = payload.room_id;
|
||||||
|
}
|
||||||
|
|
||||||
const onMessage = function(event) {
|
const onMessage = function(event) {
|
||||||
if (!event.origin) { // stupid chrome
|
if (!event.origin) { // stupid chrome
|
||||||
event.origin = event.originalEvent.origin;
|
event.origin = event.originalEvent.origin;
|
||||||
|
@ -235,14 +283,29 @@ const onMessage = function(event) {
|
||||||
|
|
||||||
const roomId = event.data.room_id;
|
const roomId = event.data.room_id;
|
||||||
const userId = event.data.user_id;
|
const userId = event.data.user_id;
|
||||||
if (!userId) {
|
|
||||||
sendError(event, "Missing user_id in request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!roomId) {
|
if (!roomId) {
|
||||||
sendError(event, "Missing room_id in request");
|
sendError(event, "Missing room_id in request");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!currentRoomId) {
|
||||||
|
sendError(event, "Must be viewing a room");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (roomId !== currentRoomId) {
|
||||||
|
sendError(event, "Room " + roomId + " not visible");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getting join rules does not require userId
|
||||||
|
if (event.data.action === "join_rules_state") {
|
||||||
|
getJoinRules(event, roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
sendError(event, "Missing user_id in request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (event.data.action) {
|
switch (event.data.action) {
|
||||||
case "membership_state":
|
case "membership_state":
|
||||||
getMembershipState(event, roomId, userId);
|
getMembershipState(event, roomId, userId);
|
||||||
|
@ -256,6 +319,9 @@ const onMessage = function(event) {
|
||||||
case "set_bot_options":
|
case "set_bot_options":
|
||||||
setBotOptions(event, roomId, userId);
|
setBotOptions(event, roomId, userId);
|
||||||
break;
|
break;
|
||||||
|
case "set_bot_power":
|
||||||
|
setBotPower(event, roomId, userId, event.data.level);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -130,9 +130,9 @@ module.exports = {
|
||||||
return event ? event.getContent() : {};
|
return event ? event.getContent() : {};
|
||||||
},
|
},
|
||||||
|
|
||||||
getSyncedSetting: function(type) {
|
getSyncedSetting: function(type, defaultValue = null) {
|
||||||
var settings = this.getSyncedSettings();
|
var settings = this.getSyncedSettings();
|
||||||
return settings[type];
|
return settings.hasOwnProperty(type) ? settings[type] : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
setSyncedSetting: function(type, value) {
|
setSyncedSetting: function(type, value) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ module.exports.components['views.avatars.RoomAvatar'] = require('./components/vi
|
||||||
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
|
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
|
||||||
module.exports.components['views.create_room.Presets'] = require('./components/views/create_room/Presets');
|
module.exports.components['views.create_room.Presets'] = require('./components/views/create_room/Presets');
|
||||||
module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias');
|
module.exports.components['views.create_room.RoomAlias'] = require('./components/views/create_room/RoomAlias');
|
||||||
|
module.exports.components['views.dialogs.ChatInviteDialog'] = require('./components/views/dialogs/ChatInviteDialog');
|
||||||
module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog');
|
module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog');
|
||||||
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
|
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
|
||||||
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
|
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
|
||||||
|
@ -55,6 +56,7 @@ module.exports.components['views.dialogs.NeedToRegisterDialog'] = require('./com
|
||||||
module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
|
module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
|
||||||
module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog');
|
module.exports.components['views.dialogs.SetDisplayNameDialog'] = require('./components/views/dialogs/SetDisplayNameDialog');
|
||||||
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
|
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
|
||||||
|
module.exports.components['views.elements.AddressTile'] = require('./components/views/elements/AddressTile');
|
||||||
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
|
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
|
||||||
module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer');
|
module.exports.components['views.elements.EditableTextContainer'] = require('./components/views/elements/EditableTextContainer');
|
||||||
module.exports.components['views.elements.EmojiText'] = require('./components/views/elements/EmojiText');
|
module.exports.components['views.elements.EmojiText'] = require('./components/views/elements/EmojiText');
|
||||||
|
|
|
@ -370,6 +370,9 @@ module.exports = React.createClass({
|
||||||
this._setPage(this.PageTypes.RoomDirectory);
|
this._setPage(this.PageTypes.RoomDirectory);
|
||||||
this.notifyNewScreen('directory');
|
this.notifyNewScreen('directory');
|
||||||
break;
|
break;
|
||||||
|
case 'view_create_chat':
|
||||||
|
this._createChat();
|
||||||
|
break;
|
||||||
case 'notifier_enabled':
|
case 'notifier_enabled':
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
break;
|
break;
|
||||||
|
@ -506,6 +509,13 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_createChat: function() {
|
||||||
|
var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
|
||||||
|
Modal.createDialog(ChatInviteDialog, {
|
||||||
|
title: "Start a one to one chat",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// update scrollStateMap according to the current scroll state of the
|
// update scrollStateMap according to the current scroll state of the
|
||||||
// room view.
|
// room view.
|
||||||
_updateScrollMap: function() {
|
_updateScrollMap: function() {
|
||||||
|
|
371
src/components/views/dialogs/ChatInviteDialog.js
Normal file
371
src/components/views/dialogs/ChatInviteDialog.js
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var React = require("react");
|
||||||
|
var classNames = require('classnames');
|
||||||
|
var sdk = require("../../../index");
|
||||||
|
var Invite = require("../../../Invite");
|
||||||
|
var createRoom = require("../../../createRoom");
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var DMRoomMap = require('../../../utils/DMRoomMap');
|
||||||
|
var rate_limited_func = require("../../../ratelimitedfunc");
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var Modal = require('../../../Modal');
|
||||||
|
|
||||||
|
const TRUNCATE_QUERY_LIST = 40;
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: "ChatInviteDialog",
|
||||||
|
propTypes: {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
description: React.PropTypes.oneOfType([
|
||||||
|
React.PropTypes.element,
|
||||||
|
React.PropTypes.string,
|
||||||
|
]),
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
placeholder: React.PropTypes.string,
|
||||||
|
button: React.PropTypes.string,
|
||||||
|
focus: React.PropTypes.bool,
|
||||||
|
onFinished: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
title: "Start a chat",
|
||||||
|
description: "Who would you like to communicate with?",
|
||||||
|
value: "",
|
||||||
|
placeholder: "User ID, Name or email",
|
||||||
|
button: "Start Chat",
|
||||||
|
focus: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
user: null,
|
||||||
|
queryList: [],
|
||||||
|
addressSelected: false,
|
||||||
|
selected: 0,
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
if (this.props.focus) {
|
||||||
|
// Set the cursor at the end of the text input
|
||||||
|
this.refs.textinput.value = this.props.value;
|
||||||
|
}
|
||||||
|
this._updateUserList();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function() {
|
||||||
|
// As the user scrolls with the arrow keys keep the selected item
|
||||||
|
// at the top of the window.
|
||||||
|
if (this.scrollElement && !this.state.hover) {
|
||||||
|
var elementHeight = this.queryListElement.getBoundingClientRect().height;
|
||||||
|
this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onStartChat: function() {
|
||||||
|
var addr;
|
||||||
|
|
||||||
|
// Either an address tile was created, or text input is being used
|
||||||
|
if (this.state.user) {
|
||||||
|
addr = this.state.user.userId;
|
||||||
|
} else {
|
||||||
|
addr = this.refs.textinput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the addr is a valid type
|
||||||
|
if (Invite.getAddressType(addr) === "mx") {
|
||||||
|
var room = this._getDirectMessageRoom(addr);
|
||||||
|
if (room) {
|
||||||
|
// A Direct Message room already exists for this user and you
|
||||||
|
// so go straight to that room
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: room.roomId,
|
||||||
|
});
|
||||||
|
this.props.onFinished(true, addr);
|
||||||
|
} else {
|
||||||
|
this._startChat(addr);
|
||||||
|
}
|
||||||
|
} else if (Invite.getAddressType(addr) === "email") {
|
||||||
|
this._startChat(addr);
|
||||||
|
} else {
|
||||||
|
// Nothing to do, so focus back on the textinput
|
||||||
|
this.refs.textinput.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel: function() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown: function(e) {
|
||||||
|
if (e.keyCode === 27) { // escape
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
} else if (e.keyCode === 38) { // up arrow
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.selected > 0) {
|
||||||
|
this.setState({
|
||||||
|
selected: this.state.selected - 1,
|
||||||
|
hover : false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (e.keyCode === 40) { // down arrow
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.selected < this._maxSelected(this.state.queryList)) {
|
||||||
|
this.setState({
|
||||||
|
selected: this.state.selected + 1,
|
||||||
|
hover : false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (e.keyCode === 13) { // enter
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.queryList.length > 0) {
|
||||||
|
this.setState({
|
||||||
|
user: this.state.queryList[this.state.selected],
|
||||||
|
addressSelected: true,
|
||||||
|
queryList: [],
|
||||||
|
hover : false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onQueryChanged: function(ev) {
|
||||||
|
var query = ev.target.value;
|
||||||
|
var queryList = [];
|
||||||
|
|
||||||
|
// Only do search if there is something to search
|
||||||
|
if (query.length > 0) {
|
||||||
|
queryList = this._userList.filter((user) => {
|
||||||
|
return this._matches(query, user);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the selected item isn't outside the list bounds
|
||||||
|
var selected = this.state.selected;
|
||||||
|
var maxSelected = this._maxSelected(queryList);
|
||||||
|
if (selected > maxSelected) {
|
||||||
|
selected = maxSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
queryList: queryList,
|
||||||
|
selected: selected,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onDismissed: function() {
|
||||||
|
this.setState({
|
||||||
|
user: null,
|
||||||
|
addressSelected: false,
|
||||||
|
selected: 0,
|
||||||
|
queryList: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onClick: function(index) {
|
||||||
|
var self = this;
|
||||||
|
return function() {
|
||||||
|
self.setState({
|
||||||
|
user: self.state.queryList[index],
|
||||||
|
addressSelected: true,
|
||||||
|
queryList: [],
|
||||||
|
hover: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseEnter: function(index) {
|
||||||
|
var self = this;
|
||||||
|
return function() {
|
||||||
|
self.setState({
|
||||||
|
selected: index,
|
||||||
|
hover: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseLeave: function() {
|
||||||
|
this.setState({ hover : false });
|
||||||
|
},
|
||||||
|
|
||||||
|
createQueryListTiles: function() {
|
||||||
|
var self = this;
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
var AddressTile = sdk.getComponent("elements.AddressTile");
|
||||||
|
var maxSelected = this._maxSelected(this.state.queryList);
|
||||||
|
var queryList = [];
|
||||||
|
|
||||||
|
// Only create the query elements if there are queries
|
||||||
|
if (this.state.queryList.length > 0) {
|
||||||
|
for (var i = 0; i <= maxSelected; i++) {
|
||||||
|
var classes = classNames({
|
||||||
|
"mx_ChatInviteDialog_queryListElement": true,
|
||||||
|
"mx_ChatInviteDialog_selected": this.state.selected === i,
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Defaulting to "vector" as the network, until the network backend stuff is done.
|
||||||
|
// Saving the queryListElement so we can use it to work out, in the componentDidUpdate
|
||||||
|
// method, how far to scroll when using the arrow keys
|
||||||
|
queryList.push(
|
||||||
|
<div className={classes} onClick={this.onClick(i)} onMouseEnter={this.onMouseEnter(i)} onMouseLeave={this.onMouseLeave} key={i} ref={(ref) => { this.queryListElement = ref; }} >
|
||||||
|
<AddressTile user={this.state.queryList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queryList;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getDirectMessageRoom: function(addr) {
|
||||||
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
|
var dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||||
|
if (dmRooms.length > 0) {
|
||||||
|
// Cycle through all the DM rooms and find the first non forgotten or parted room
|
||||||
|
for (let i = 0; i < dmRooms.length; i++) {
|
||||||
|
let room = MatrixClientPeg.get().getRoom(dmRooms[i]);
|
||||||
|
if (room) {
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
_startChat: function(addr) {
|
||||||
|
// Start the chat
|
||||||
|
createRoom().then(function(roomId) {
|
||||||
|
return Invite.inviteToRoom(roomId, addr);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Failure to invite user",
|
||||||
|
description: err.toString()
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.done();
|
||||||
|
|
||||||
|
// Close - this will happen before the above, as that is async
|
||||||
|
this.props.onFinished(true, addr);
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateUserList: new rate_limited_func(function() {
|
||||||
|
// Get all the users
|
||||||
|
this._userList = MatrixClientPeg.get().getUsers();
|
||||||
|
}, 500),
|
||||||
|
|
||||||
|
_maxSelected: function(list) {
|
||||||
|
var listSize = list.length === 0 ? 0 : list.length - 1;
|
||||||
|
var maxSelected = listSize > (TRUNCATE_QUERY_LIST - 1) ? (TRUNCATE_QUERY_LIST - 1) : listSize
|
||||||
|
return maxSelected;
|
||||||
|
},
|
||||||
|
|
||||||
|
// This is the search algorithm for matching users
|
||||||
|
_matches: function(query, user) {
|
||||||
|
var name = user.displayName.toLowerCase();
|
||||||
|
var uid = user.userId.toLowerCase();
|
||||||
|
query = query.toLowerCase();
|
||||||
|
|
||||||
|
// direct prefix matches
|
||||||
|
if (name.indexOf(query) === 0 || uid.indexOf(query) === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip @ on uid and try matching again
|
||||||
|
if (uid.length > 1 && uid[0] === "@" && uid.substring(1).indexOf(query) === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split spaces in name and try matching constituent parts
|
||||||
|
var parts = name.split(" ");
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i].indexOf(query) === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
this.scrollElement = null;
|
||||||
|
|
||||||
|
var query;
|
||||||
|
if (this.state.addressSelected) {
|
||||||
|
var AddressTile = sdk.getComponent("elements.AddressTile");
|
||||||
|
query = (
|
||||||
|
<AddressTile user={this.state.user} canDismiss={true} onDismissed={this.onDismissed} />
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query = (
|
||||||
|
<textarea rows="1"
|
||||||
|
id="textinput"
|
||||||
|
ref="textinput"
|
||||||
|
className="mx_ChatInviteDialog_input"
|
||||||
|
onChange={this.onQueryChanged}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
defaultValue={this.props.value}
|
||||||
|
autoFocus={this.props.focus}>
|
||||||
|
</textarea>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var queryList;
|
||||||
|
var queryListElements = this.createQueryListTiles();
|
||||||
|
if (queryListElements.length > 0) {
|
||||||
|
queryList = (
|
||||||
|
<div className="mx_ChatInviteDialog_queryList" ref={(ref) => {this.scrollElement = ref}}>
|
||||||
|
{ queryListElements }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||||
|
<div className="mx_Dialog_title">
|
||||||
|
{this.props.title}
|
||||||
|
</div>
|
||||||
|
<div className="mx_ChatInviteDialog_cancel" onClick={this.onCancel} >
|
||||||
|
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||||
|
</div>
|
||||||
|
<div className="mx_ChatInviteDialog_label">
|
||||||
|
<label htmlFor="textinput">{ this.props.description }</label>
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
|
||||||
|
{ queryList }
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button className="mx_Dialog_primary" onClick={this.onStartChat}>
|
||||||
|
{this.props.button}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
93
src/components/views/elements/AddressTile.js
Normal file
93
src/components/views/elements/AddressTile.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var classNames = require('classnames');
|
||||||
|
var sdk = require("../../../index");
|
||||||
|
var Avatar = require('../../../Avatar');
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'AddressTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
user: React.PropTypes.object.isRequired,
|
||||||
|
canDismiss: React.PropTypes.bool,
|
||||||
|
onDismissed: React.PropTypes.func,
|
||||||
|
justified: React.PropTypes.bool,
|
||||||
|
networkName: React.PropTypes.string,
|
||||||
|
networkUrl: React.PropTypes.string,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
canDismiss: false,
|
||||||
|
onDismissed: function() {}, // NOP
|
||||||
|
justified: false,
|
||||||
|
networkName: "",
|
||||||
|
networkUrl: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
var userId = this.props.user.userId;
|
||||||
|
var name = this.props.user.displayName || userId;
|
||||||
|
var imgUrl = Avatar.avatarUrlForUser(this.props.user, 25, 25, "crop");
|
||||||
|
|
||||||
|
var network;
|
||||||
|
if (this.props.networkUrl !== "") {
|
||||||
|
network = (
|
||||||
|
<div className="mx_AddressTile_network">
|
||||||
|
<BaseAvatar width={25} height={25} name={this.props.networkName} title="vector" url={this.props.networkUrl} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dismiss;
|
||||||
|
if (this.props.canDismiss) {
|
||||||
|
dismiss = (
|
||||||
|
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
|
||||||
|
<TintableSvg src="img/icon-address-delete.svg" width="9" height="9" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameClasses = classNames({
|
||||||
|
"mx_AddressTile_name": true,
|
||||||
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
});
|
||||||
|
|
||||||
|
var idClasses = classNames({
|
||||||
|
"mx_AddressTile_id": true,
|
||||||
|
"mx_AddressTile_justified": this.props.justified,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_AddressTile">
|
||||||
|
{ network }
|
||||||
|
<div className="mx_AddressTile_avatar">
|
||||||
|
<BaseAvatar width={25} height={25} name={name} title={name} url={imgUrl} />
|
||||||
|
</div>
|
||||||
|
<div className={nameClasses}>{ name }</div>
|
||||||
|
<div className={idClasses}>{ userId }</div>
|
||||||
|
{ dismiss }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -149,13 +149,13 @@ export default class Autocomplete extends React.Component {
|
||||||
{completionResult.provider.renderCompletions(completions)}
|
{completionResult.provider.renderCompletions(completions)}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
});
|
}).filter(completion => !!completion);
|
||||||
|
|
||||||
return (
|
return 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>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ var Modal = require('../../../Modal');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var dis = require('../../../dispatcher');
|
var dis = require('../../../dispatcher');
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import UserSettingsStore from '../../../UserSettingsStore';
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
|
@ -38,10 +39,20 @@ export default class MessageComposer extends React.Component {
|
||||||
this.onDownArrow = this.onDownArrow.bind(this);
|
this.onDownArrow = this.onDownArrow.bind(this);
|
||||||
this._tryComplete = this._tryComplete.bind(this);
|
this._tryComplete = this._tryComplete.bind(this);
|
||||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||||
|
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
||||||
|
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
||||||
|
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
autocompleteQuery: '',
|
autocompleteQuery: '',
|
||||||
selection: null,
|
selection: null,
|
||||||
|
inputState: {
|
||||||
|
style: [],
|
||||||
|
blockType: null,
|
||||||
|
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true),
|
||||||
|
wordCount: 0,
|
||||||
|
},
|
||||||
|
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false),
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -134,6 +145,10 @@ export default class MessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInputStateChanged(inputState) {
|
||||||
|
this.setState({inputState});
|
||||||
|
}
|
||||||
|
|
||||||
onUpArrow() {
|
onUpArrow() {
|
||||||
return this.refs.autocomplete.onUpArrow();
|
return this.refs.autocomplete.onUpArrow();
|
||||||
}
|
}
|
||||||
|
@ -155,6 +170,21 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.messageComposerInput.onFormatButtonClicked(name, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleFormattingClicked() {
|
||||||
|
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting);
|
||||||
|
this.setState({showFormatting: !this.state.showFormatting});
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleMarkdownClicked(e) {
|
||||||
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
|
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
var uploadInputStyle = {display: 'none'};
|
var uploadInputStyle = {display: 'none'};
|
||||||
|
@ -207,6 +237,16 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const formattingButton = (
|
||||||
|
<img className="mx_MessageComposer_formatting"
|
||||||
|
title="Show Text Formatting Toolbar"
|
||||||
|
src="img/button-text-formatting.svg"
|
||||||
|
onClick={this.onToggleFormattingClicked}
|
||||||
|
style={{visibility: this.state.showFormatting ||
|
||||||
|
!UserSettingsStore.isFeatureEnabled('rich_text_editor') ? 'hidden' : 'visible'}}
|
||||||
|
key="controls_formatting" />
|
||||||
|
);
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<MessageComposerInput
|
<MessageComposerInput
|
||||||
ref={c => this.messageComposerInput = c}
|
ref={c => this.messageComposerInput = c}
|
||||||
|
@ -217,7 +257,9 @@ export default class MessageComposer extends React.Component {
|
||||||
onUpArrow={this.onUpArrow}
|
onUpArrow={this.onUpArrow}
|
||||||
onDownArrow={this.onDownArrow}
|
onDownArrow={this.onDownArrow}
|
||||||
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete
|
||||||
onContentChanged={this.onInputContentChanged} />,
|
onContentChanged={this.onInputContentChanged}
|
||||||
|
onInputStateChanged={this.onInputStateChanged} />,
|
||||||
|
formattingButton,
|
||||||
uploadButton,
|
uploadButton,
|
||||||
hangupButton,
|
hangupButton,
|
||||||
callButton,
|
callButton,
|
||||||
|
@ -242,6 +284,26 @@ export default class MessageComposer extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const {style, blockType} = this.state.inputState;
|
||||||
|
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||||
|
name => {
|
||||||
|
const active = style.includes(name) || blockType === name;
|
||||||
|
const suffix = active ? '-o-n' : '';
|
||||||
|
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||||
|
const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name;
|
||||||
|
const className = classNames("mx_MessageComposer_format_button", {
|
||||||
|
mx_MessageComposer_format_button_disabled: disabled,
|
||||||
|
});
|
||||||
|
return <img className={className}
|
||||||
|
title={name}
|
||||||
|
onMouseDown={disabled ? null : onFormatButtonClicked}
|
||||||
|
key={name}
|
||||||
|
src={`img/button-text-${name}${suffix}.svg`}
|
||||||
|
height="17" />;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
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}
|
{autoComplete}
|
||||||
|
@ -250,6 +312,22 @@ export default class MessageComposer extends React.Component {
|
||||||
{controls}
|
{controls}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{UserSettingsStore.isFeatureEnabled('rich_text_editor') ?
|
||||||
|
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||||
|
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
|
||||||
|
{formatButtons}
|
||||||
|
<div style={{flex: 1}}></div>
|
||||||
|
<img title={`Turn Markdown ${this.state.inputState.isRichtextEnabled ? 'on' : 'off'}`}
|
||||||
|
onMouseDown={this.onToggleMarkdownClicked}
|
||||||
|
className="mx_MessageComposer_formatbar_markdown"
|
||||||
|
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||||
|
<img title="Hide Text Formatting Toolbar"
|
||||||
|
onClick={this.onToggleFormattingClicked}
|
||||||
|
className="mx_MessageComposer_formatbar_cancel"
|
||||||
|
src="img/icon-text-cancel.svg" />
|
||||||
|
</div>
|
||||||
|
</div>: null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,11 @@ marked.setOptions({
|
||||||
|
|
||||||
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
import {Editor, EditorState, RichUtils, CompositeDecorator,
|
||||||
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
convertFromRaw, convertToRaw, Modifier, EditorChangeType,
|
||||||
getDefaultKeyBinding, KeyBindingUtil, ContentState} from 'draft-js';
|
getDefaultKeyBinding, KeyBindingUtil, ContentState, ContentBlock, SelectionState} from 'draft-js';
|
||||||
|
|
||||||
import {stateToMarkdown} from 'draft-js-export-markdown';
|
import {stateToMarkdown} from 'draft-js-export-markdown';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import escape from 'lodash/escape';
|
||||||
|
|
||||||
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';
|
||||||
|
@ -41,6 +43,7 @@ import sdk from '../../../index';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import KeyCode from '../../../KeyCode';
|
import KeyCode from '../../../KeyCode';
|
||||||
|
import UserSettingsStore from '../../../UserSettingsStore';
|
||||||
|
|
||||||
import * as RichText from '../../../RichText';
|
import * as RichText from '../../../RichText';
|
||||||
|
|
||||||
|
@ -80,7 +83,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
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.onInputClick = this.onInputClick.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.setEditorState = this.setEditorState.bind(this);
|
this.setEditorState = this.setEditorState.bind(this);
|
||||||
|
@ -88,15 +90,12 @@ export default class MessageComposerInput extends React.Component {
|
||||||
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.onConfirmAutocompletion = this.onConfirmAutocompletion.bind(this);
|
||||||
|
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
|
||||||
|
|
||||||
let isRichtextEnabled = window.localStorage.getItem('mx_editor_rte_enabled');
|
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', true);
|
||||||
if (isRichtextEnabled == null) {
|
|
||||||
isRichtextEnabled = 'true';
|
|
||||||
}
|
|
||||||
isRichtextEnabled = isRichtextEnabled === 'true';
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRichtextEnabled: isRichtextEnabled,
|
isRichtextEnabled,
|
||||||
editorState: null,
|
editorState: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -236,8 +235,18 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.sentHistory.saveLastTextEntry();
|
this.sentHistory.saveLastTextEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUpdate(nextProps, nextState) {
|
||||||
|
// this is dirty, but moving all this state to MessageComposer is dirtier
|
||||||
|
if (this.props.onInputStateChanged && nextState !== this.state) {
|
||||||
|
const state = this.getSelectionInfo(nextState.editorState);
|
||||||
|
state.isRichtextEnabled = nextState.isRichtextEnabled;
|
||||||
|
this.props.onInputStateChanged(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onAction(payload) {
|
onAction(payload) {
|
||||||
let editor = this.refs.editor;
|
let editor = this.refs.editor;
|
||||||
|
let contentState = this.state.editorState.getCurrentContent();
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'focus_composer':
|
case 'focus_composer':
|
||||||
|
@ -246,35 +255,44 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// TODO change this so we insert a complete user alias
|
// TODO change this so we insert a complete user alias
|
||||||
|
|
||||||
case 'insert_displayname':
|
case 'insert_displayname': {
|
||||||
if (this.state.editorState.getCurrentContent().hasText()) {
|
contentState = Modifier.replaceText(
|
||||||
console.log(payload);
|
contentState,
|
||||||
let contentState = Modifier.replaceText(
|
this.state.editorState.getSelection(),
|
||||||
this.state.editorState.getCurrentContent(),
|
`${payload.displayname}: `
|
||||||
this.state.editorState.getSelection(),
|
);
|
||||||
payload.displayname
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
);
|
editorState = EditorState.forceSelection(editorState, contentState.getSelectionAfter());
|
||||||
this.setState({
|
this.setEditorState(editorState);
|
||||||
editorState: EditorState.push(this.state.editorState, contentState, 'insert-characters'),
|
editor.focus();
|
||||||
});
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'quote': {
|
||||||
|
let {event: {content: {body, formatted_body}}} = payload.event || {};
|
||||||
|
formatted_body = formatted_body || escape(body);
|
||||||
|
if (formatted_body) {
|
||||||
|
let content = RichText.HTMLtoContentState(`<blockquote>${formatted_body}</blockquote>`);
|
||||||
|
if (!this.state.isRichtextEnabled) {
|
||||||
|
content = ContentState.createFromText(stateToMarkdown(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockMap = content.getBlockMap();
|
||||||
|
let startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
|
||||||
|
contentState = Modifier.splitBlock(contentState, startSelection);
|
||||||
|
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
|
||||||
|
contentState = Modifier.replaceWithFragment(contentState,
|
||||||
|
startSelection,
|
||||||
|
blockMap);
|
||||||
|
startSelection = SelectionState.createEmpty(contentState.getFirstBlock().getKey());
|
||||||
|
if (this.state.isRichtextEnabled)
|
||||||
|
contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote');
|
||||||
|
let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters');
|
||||||
|
this.setEditorState(editorState);
|
||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(ev) {
|
|
||||||
if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
|
||||||
var oldSelectionStart = this.refs.textarea.selectionStart;
|
|
||||||
// Remember the keyCode because React will recycle the synthetic event
|
|
||||||
var keyCode = ev.keyCode;
|
|
||||||
// set a callback so we can see if the cursor position changes as
|
|
||||||
// a result of this event. If it doesn't, we cycle history.
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
|
||||||
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,13 +362,10 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputClick(ev) {
|
|
||||||
this.refs.editor.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditorState(editorState: EditorState) {
|
setEditorState(editorState: EditorState, cb = () => null) {
|
||||||
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
editorState = RichText.attachImmutableEntitiesToEmoji(editorState);
|
||||||
this.setState({editorState});
|
this.setState({editorState}, cb);
|
||||||
|
|
||||||
if (editorState.getCurrentContent().hasText()) {
|
if (editorState.getCurrentContent().hasText()) {
|
||||||
this.onTypingActivity();
|
this.onTypingActivity();
|
||||||
|
@ -359,27 +374,34 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onContentChanged) {
|
if (this.props.onContentChanged) {
|
||||||
this.props.onContentChanged(editorState.getCurrentContent().getPlainText(),
|
const textContent = editorState.getCurrentContent().getPlainText();
|
||||||
RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
const selection = RichText.selectionStateToTextOffsets(editorState.getSelection(),
|
||||||
editorState.getCurrentContent().getBlocksAsArray()));
|
editorState.getCurrentContent().getBlocksAsArray());
|
||||||
|
|
||||||
|
this.props.onContentChanged(textContent, selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableRichtext(enabled: boolean) {
|
enableRichtext(enabled: boolean) {
|
||||||
|
let contentState = null;
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
let html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
|
const html = mdownToHtml(this.state.editorState.getCurrentContent().getPlainText());
|
||||||
this.setEditorState(this.createEditorState(enabled, RichText.HTMLtoContentState(html)));
|
contentState = RichText.HTMLtoContentState(html);
|
||||||
} else {
|
} else {
|
||||||
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent()),
|
let markdown = stateToMarkdown(this.state.editorState.getCurrentContent());
|
||||||
contentState = ContentState.createFromText(markdown);
|
if (markdown[markdown.length - 1] === '\n') {
|
||||||
this.setEditorState(this.createEditorState(enabled, contentState));
|
markdown = markdown.substring(0, markdown.length - 1); // stateToMarkdown tacks on an extra newline (?!?)
|
||||||
|
}
|
||||||
|
contentState = ContentState.createFromText(markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.localStorage.setItem('mx_editor_rte_enabled', enabled);
|
this.setEditorState(this.createEditorState(enabled, contentState), () => {
|
||||||
|
this.setState({
|
||||||
this.setState({
|
isRichtextEnabled: enabled,
|
||||||
isRichtextEnabled: enabled
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyCommand(command: string): boolean {
|
handleKeyCommand(command: string): boolean {
|
||||||
|
@ -391,7 +413,17 @@ export default class MessageComposerInput extends React.Component {
|
||||||
let newState: ?EditorState = null;
|
let newState: ?EditorState = null;
|
||||||
|
|
||||||
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
||||||
if (!this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
|
// These are block types, not handled by RichUtils by default.
|
||||||
|
const blockCommands = ['code-block', 'blockquote', 'unordered-list-item', 'ordered-list-item'];
|
||||||
|
|
||||||
|
if (blockCommands.includes(command)) {
|
||||||
|
this.setEditorState(RichUtils.toggleBlockType(this.state.editorState, command));
|
||||||
|
} else if (command === 'strike') {
|
||||||
|
// this is the only inline style not handled by Draft by default
|
||||||
|
this.setEditorState(RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let contentState = this.state.editorState.getCurrentContent(),
|
let contentState = this.state.editorState.getCurrentContent(),
|
||||||
selection = this.state.editorState.getSelection();
|
selection = this.state.editorState.getSelection();
|
||||||
|
|
||||||
|
@ -399,7 +431,11 @@ export default class MessageComposerInput extends React.Component {
|
||||||
bold: text => `**${text}**`,
|
bold: text => `**${text}**`,
|
||||||
italic: text => `*${text}*`,
|
italic: text => `*${text}*`,
|
||||||
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
underline: text => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug*
|
||||||
|
strike: text => `~~${text}~~`,
|
||||||
code: text => `\`${text}\``,
|
code: text => `\`${text}\``,
|
||||||
|
blockquote: text => text.split('\n').map(line => `> ${line}\n`).join(''),
|
||||||
|
'unordered-list-item': text => text.split('\n').map(line => `- ${line}\n`).join(''),
|
||||||
|
'ordered-list-item': text => text.split('\n').map((line, i) => `${i+1}. ${line}\n`).join(''),
|
||||||
}[command];
|
}[command];
|
||||||
|
|
||||||
if (modifyFn) {
|
if (modifyFn) {
|
||||||
|
@ -418,12 +454,14 @@ export default class MessageComposerInput extends React.Component {
|
||||||
this.setEditorState(newState);
|
this.setEditorState(newState);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReturn(ev) {
|
handleReturn(ev) {
|
||||||
if (ev.shiftKey) {
|
if (ev.shiftKey) {
|
||||||
return false;
|
this.setEditorState(RichUtils.insertSoftNewline(this.state.editorState));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentState = this.state.editorState.getCurrentContent();
|
const contentState = this.state.editorState.getCurrentContent();
|
||||||
|
@ -464,7 +502,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.isRichtextEnabled) {
|
if (this.state.isRichtextEnabled) {
|
||||||
contentHTML = RichText.contentStateToHTML(contentState);
|
contentHTML = RichText.contentStateToHTML(contentState);
|
||||||
} else {
|
} else {
|
||||||
contentHTML = mdownToHtml(contentText);
|
contentHTML = mdownToHtml(contentText);
|
||||||
|
@ -536,20 +574,91 @@ export default class MessageComposerInput extends React.Component {
|
||||||
setTimeout(() => this.refs.editor.focus(), 50);
|
setTimeout(() => this.refs.editor.focus(), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", e) {
|
||||||
let className = "mx_MessageComposer_input";
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
|
const command = {
|
||||||
|
code: 'code-block',
|
||||||
|
quote: 'blockquote',
|
||||||
|
bullet: 'unordered-list-item',
|
||||||
|
numbullet: 'ordered-list-item',
|
||||||
|
}[name] || name;
|
||||||
|
this.handleKeyCommand(command);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.isRichtextEnabled) {
|
/* returns inline style and block type of current SelectionState so MessageComposer can render formatting
|
||||||
className += " mx_MessageComposer_input_rte"; // placeholder indicator for RTE mode
|
buttons. */
|
||||||
|
getSelectionInfo(editorState: EditorState) {
|
||||||
|
const styleName = {
|
||||||
|
BOLD: 'bold',
|
||||||
|
ITALIC: 'italic',
|
||||||
|
STRIKETHROUGH: 'strike',
|
||||||
|
UNDERLINE: 'underline',
|
||||||
|
};
|
||||||
|
|
||||||
|
const originalStyle = editorState.getCurrentInlineStyle().toArray();
|
||||||
|
const style = originalStyle
|
||||||
|
.map(style => styleName[style] || null)
|
||||||
|
.filter(styleName => !!styleName);
|
||||||
|
|
||||||
|
const blockName = {
|
||||||
|
'code-block': 'code',
|
||||||
|
blockquote: 'quote',
|
||||||
|
'unordered-list-item': 'bullet',
|
||||||
|
'ordered-list-item': 'numbullet',
|
||||||
|
};
|
||||||
|
const originalBlockType = editorState.getCurrentContent()
|
||||||
|
.getBlockForKey(editorState.getSelection().getStartKey())
|
||||||
|
.getType();
|
||||||
|
const blockType = blockName[originalBlockType] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
style,
|
||||||
|
blockType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMarkdownToggleClicked(e) {
|
||||||
|
e.preventDefault(); // don't steal focus from the editor!
|
||||||
|
this.handleKeyCommand('toggle-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
getBlockStyle(block: ContentBlock): ?string {
|
||||||
|
if (block.getType() === 'strikethrough') {
|
||||||
|
return 'mx_Markdown_STRIKETHROUGH';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {editorState} = this.state;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// either style the placeholder or hide it.
|
||||||
|
let hidePlaceholder = false;
|
||||||
|
const contentState = editorState.getCurrentContent();
|
||||||
|
if (!contentState.hasText()) {
|
||||||
|
if (contentState.getBlockMap().first().getType() !== 'unstyled') {
|
||||||
|
hidePlaceholder = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = classNames('mx_MessageComposer_input', {
|
||||||
|
mx_MessageComposer_input_empty: hidePlaceholder,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}
|
<div className={className}>
|
||||||
onClick={ this.onInputClick }>
|
<img className="mx_MessageComposer_input_markdownIndicator"
|
||||||
|
onMouseDown={this.onMarkdownToggleClicked}
|
||||||
|
title={`Markdown is ${this.state.isRichtextEnabled ? 'disabled' : 'enabled'}`}
|
||||||
|
src={`img/button-md-${!this.state.isRichtextEnabled}.png`} />
|
||||||
<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.setEditorState}
|
||||||
|
blockStyleFn={this.getBlockStyle}
|
||||||
keyBindingFn={MessageComposerInput.getKeyBinding}
|
keyBindingFn={MessageComposerInput.getKeyBinding}
|
||||||
handleKeyCommand={this.handleKeyCommand}
|
handleKeyCommand={this.handleKeyCommand}
|
||||||
handleReturn={this.handleReturn}
|
handleReturn={this.handleReturn}
|
||||||
|
@ -582,4 +691,6 @@ MessageComposerInput.propTypes = {
|
||||||
|
|
||||||
// attempts to confirm currently selected completion, returns whether actually confirmed
|
// attempts to confirm currently selected completion, returns whether actually confirmed
|
||||||
tryComplete: React.PropTypes.func,
|
tryComplete: React.PropTypes.func,
|
||||||
|
|
||||||
|
onInputStateChanged: React.PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -58,6 +58,7 @@ module.exports = React.createClass({
|
||||||
cli.on("Room.receipt", this.onRoomReceipt);
|
cli.on("Room.receipt", this.onRoomReceipt);
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
cli.on("accountData", this.onAccountData);
|
||||||
|
|
||||||
var s = this.getRoomLists();
|
var s = this.getRoomLists();
|
||||||
this.setState(s);
|
this.setState(s);
|
||||||
|
@ -78,8 +79,6 @@ module.exports = React.createClass({
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'view_tooltip':
|
case 'view_tooltip':
|
||||||
this.tooltip = payload.tooltip;
|
this.tooltip = payload.tooltip;
|
||||||
this._repositionTooltip();
|
|
||||||
if (this.tooltip) this.tooltip.style.display = 'block';
|
|
||||||
break;
|
break;
|
||||||
case 'call_state':
|
case 'call_state':
|
||||||
var call = CallHandler.getCall(payload.room_id);
|
var call = CallHandler.getCall(payload.room_id);
|
||||||
|
@ -109,6 +108,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this._delayedRefreshRoomList.cancelPendingCall();
|
this._delayedRefreshRoomList.cancelPendingCall();
|
||||||
|
@ -182,6 +182,12 @@ module.exports = React.createClass({
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onAccountData: function(ev) {
|
||||||
|
if (ev.getType() == 'm.direct') {
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
}, 500),
|
}, 500),
|
||||||
|
@ -309,17 +315,15 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_whenScrolling: function(e) {
|
_whenScrolling: function(e) {
|
||||||
this._repositionTooltip(e);
|
this._hideTooltip(e);
|
||||||
this._repositionIncomingCallBox(e, false);
|
this._repositionIncomingCallBox(e, false);
|
||||||
this._updateStickyHeaders(false);
|
this._updateStickyHeaders(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
_repositionTooltip: function(e) {
|
_hideTooltip: function(e) {
|
||||||
// We access the parent of the parent, as the tooltip is inside a container
|
// Hide tooltip when scrolling, as we'll no longer be over the one we were on
|
||||||
// Needs refactoring into a better multipurpose tooltip
|
if (this.tooltip && this.tooltip.style.display !== "none") {
|
||||||
if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) {
|
this.tooltip.style.display = "none";
|
||||||
var scroll = ReactDOM.findDOMNode(this);
|
|
||||||
this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -368,7 +372,7 @@ module.exports = React.createClass({
|
||||||
var scrollArea = this._getScrollNode();
|
var scrollArea = this._getScrollNode();
|
||||||
// Use the offset of the top of the scroll area from the window
|
// Use the offset of the top of the scroll area from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaOffset = scrollArea.getBoundingClientRect().top;
|
var scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
|
||||||
// Use the offset of the top of the componet from the window
|
// Use the offset of the top of the componet from the window
|
||||||
// as this is used to calculate the CSS fixed top position for the stickies
|
// as this is used to calculate the CSS fixed top position for the stickies
|
||||||
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
var scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
var ReactDOM = require("react-dom");
|
||||||
var classNames = require('classnames');
|
var classNames = require('classnames');
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
@ -245,10 +246,9 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
label = <EmojiText element="div" title={ name } className={ nameClasses }>{name}</EmojiText>;
|
label = <EmojiText element="div" title={ name } className={ nameClasses }>{name}</EmojiText>;
|
||||||
}
|
}
|
||||||
}
|
} else if (this.state.hover) {
|
||||||
else if (this.state.hover) {
|
|
||||||
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
var RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||||
label = <RoomTooltip room={this.props.room}/>;
|
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var incomingCallBox;
|
var incomingCallBox;
|
||||||
|
|
Loading…
Reference in a new issue