forked from mirrors/gecko-dev
427 lines
14 KiB
JavaScript
427 lines
14 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
var loop = loop || {};
|
|
loop.shared = loop.shared || {};
|
|
loop.shared.views = loop.shared.views || {};
|
|
loop.shared.views.chat = (function(mozL10n) {
|
|
"use strict";
|
|
|
|
var sharedActions = loop.shared.actions;
|
|
var sharedMixins = loop.shared.mixins;
|
|
var sharedViews = loop.shared.views;
|
|
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
|
|
var CHAT_CONTENT_TYPES = loop.shared.utils.CHAT_CONTENT_TYPES;
|
|
|
|
/**
|
|
* Renders an individual entry for the text chat entries view.
|
|
*/
|
|
var TextChatEntry = React.createClass({displayName: "TextChatEntry",
|
|
mixins: [React.addons.PureRenderMixin],
|
|
|
|
propTypes: {
|
|
contentType: React.PropTypes.string.isRequired,
|
|
message: React.PropTypes.string.isRequired,
|
|
showTimestamp: React.PropTypes.bool.isRequired,
|
|
timestamp: React.PropTypes.string.isRequired,
|
|
type: React.PropTypes.string.isRequired
|
|
},
|
|
|
|
/**
|
|
* Pretty print timestamp. From time in milliseconds to HH:MM
|
|
* (or L10N equivalent).
|
|
*
|
|
*/
|
|
_renderTimestamp: function() {
|
|
var date = new Date(this.props.timestamp);
|
|
var language = mozL10n.language ? mozL10n.language.code
|
|
: mozL10n.getLanguage();
|
|
|
|
return (
|
|
React.createElement("span", {className: "text-chat-entry-timestamp"},
|
|
date.toLocaleTimeString(language,
|
|
{hour: "numeric", minute: "numeric",
|
|
hour12: false})
|
|
)
|
|
);
|
|
},
|
|
|
|
render: function() {
|
|
var classes = React.addons.classSet({
|
|
"text-chat-entry": true,
|
|
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
|
|
"sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
|
|
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
|
|
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
|
|
});
|
|
|
|
var optionalProps = {};
|
|
if (navigator.mozLoop) {
|
|
optionalProps.linkClickHandler = navigator.mozLoop.openURL;
|
|
}
|
|
|
|
return (
|
|
React.createElement("div", {className: classes},
|
|
React.createElement(sharedViews.LinkifiedTextView, React.__spread({}, optionalProps,
|
|
{rawText: this.props.message})),
|
|
React.createElement("span", {className: "text-chat-arrow"}),
|
|
this.props.showTimestamp ? this._renderTimestamp() : null
|
|
)
|
|
);
|
|
}
|
|
});
|
|
|
|
var TextChatRoomName = React.createClass({displayName: "TextChatRoomName",
|
|
mixins: [React.addons.PureRenderMixin],
|
|
|
|
propTypes: {
|
|
message: React.PropTypes.string.isRequired
|
|
},
|
|
|
|
render: function() {
|
|
return (
|
|
React.createElement("div", {className: "text-chat-header special room-name"},
|
|
React.createElement("p", null, mozL10n.get("rooms_welcome_title", {conversationName: this.props.message}))
|
|
)
|
|
);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Manages the text entries in the chat entries view. This is split out from
|
|
* TextChatView so that scrolling can be managed more efficiently - this
|
|
* component only updates when the message list is changed.
|
|
*/
|
|
var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
|
|
mixins: [
|
|
React.addons.PureRenderMixin,
|
|
sharedMixins.AudioMixin
|
|
],
|
|
|
|
statics: {
|
|
ONE_MINUTE: 60
|
|
},
|
|
|
|
propTypes: {
|
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
|
messageList: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
|
useDesktopPaths: React.PropTypes.bool.isRequired
|
|
},
|
|
|
|
getInitialState: function() {
|
|
return {
|
|
receivedMessageCount: 0
|
|
};
|
|
},
|
|
|
|
_hasChatMessages: function() {
|
|
return this.props.messageList.some(function(message) {
|
|
return message.contentType === CHAT_CONTENT_TYPES.TEXT;
|
|
});
|
|
},
|
|
|
|
componentWillUpdate: function() {
|
|
var node = this.getDOMNode();
|
|
if (!node) {
|
|
return;
|
|
}
|
|
// Scroll only if we're right at the bottom of the display, or if we've
|
|
// not had any chat messages so far.
|
|
this.shouldScroll = !this._hasChatMessages() ||
|
|
node.scrollHeight === node.scrollTop + node.clientHeight;
|
|
},
|
|
|
|
componentWillReceiveProps: function(nextProps) {
|
|
var receivedMessageCount = nextProps.messageList.filter(function(message) {
|
|
return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
|
|
}).length;
|
|
|
|
// If the number of received messages has increased, we play a sound.
|
|
if (receivedMessageCount > this.state.receivedMessageCount) {
|
|
this.play("message");
|
|
this.setState({receivedMessageCount: receivedMessageCount});
|
|
}
|
|
},
|
|
|
|
componentDidUpdate: function() {
|
|
// Don't scroll if we haven't got any chat messages yet - e.g. for context
|
|
// display, we want to display starting at the top.
|
|
if (this.shouldScroll && this._hasChatMessages()) {
|
|
// This ensures the paint is complete.
|
|
window.requestAnimationFrame(function() {
|
|
try {
|
|
var node = this.getDOMNode();
|
|
node.scrollTop = node.scrollHeight - node.clientHeight;
|
|
} catch (ex) {
|
|
console.error("TextChatEntriesView.componentDidUpdate exception", ex);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
},
|
|
|
|
render: function() {
|
|
/* Keep track of the last printed timestamp. */
|
|
var lastTimestamp = 0;
|
|
|
|
var entriesClasses = React.addons.classSet({
|
|
"text-chat-entries": true
|
|
});
|
|
|
|
return (
|
|
React.createElement("div", {className: entriesClasses},
|
|
React.createElement("div", {className: "text-chat-scroller"},
|
|
|
|
this.props.messageList.map(function(entry, i) {
|
|
if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
|
|
switch (entry.contentType) {
|
|
case CHAT_CONTENT_TYPES.ROOM_NAME:
|
|
return (
|
|
React.createElement(TextChatRoomName, {
|
|
key: i,
|
|
message: entry.message})
|
|
);
|
|
case CHAT_CONTENT_TYPES.CONTEXT:
|
|
return (
|
|
React.createElement("div", {className: "context-url-view-wrapper", key: i},
|
|
React.createElement(sharedViews.ContextUrlView, {
|
|
allowClick: true,
|
|
description: entry.message,
|
|
dispatcher: this.props.dispatcher,
|
|
showContextTitle: true,
|
|
thumbnail: entry.extraData.thumbnail,
|
|
url: entry.extraData.location,
|
|
useDesktopPaths: this.props.useDesktopPaths})
|
|
)
|
|
);
|
|
default:
|
|
console.error("Unsupported contentType",
|
|
entry.contentType);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/* For SENT messages there is no received timestamp. */
|
|
var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
|
|
|
|
var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
|
|
var shouldShowTimestamp = this._shouldShowTimestamp(i,
|
|
timeDiff);
|
|
|
|
if (shouldShowTimestamp) {
|
|
lastTimestamp = timestamp;
|
|
}
|
|
|
|
return (
|
|
React.createElement(TextChatEntry, {contentType: entry.contentType,
|
|
key: i,
|
|
message: entry.message,
|
|
showTimestamp: shouldShowTimestamp,
|
|
timestamp: timestamp,
|
|
type: entry.type})
|
|
);
|
|
}, this)
|
|
|
|
)
|
|
)
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Decide to show timestamp or not on a message.
|
|
* If the time difference between two consecutive messages is bigger than
|
|
* one minute or if message types are different.
|
|
*
|
|
* @param {number} idx Index of message in the messageList.
|
|
* @param {boolean} timeDiff If difference between consecutive messages is
|
|
* bigger than one minute.
|
|
*/
|
|
_shouldShowTimestamp: function(idx, timeDiff) {
|
|
if (!idx) {
|
|
return true;
|
|
}
|
|
|
|
/* If consecutive messages are from different senders */
|
|
if (this.props.messageList[idx].type !==
|
|
this.props.messageList[idx - 1].type) {
|
|
return true;
|
|
}
|
|
|
|
return timeDiff;
|
|
},
|
|
|
|
/**
|
|
* Determines if difference between the two timestamp arguments
|
|
* is bigger that 60 (1 minute)
|
|
*
|
|
* Timestamps are using ISO8601 format.
|
|
*
|
|
* @param {string} currTime Timestamp of message yet to be rendered.
|
|
* @param {string} prevTime Last timestamp printed in the chat view.
|
|
*/
|
|
_isOneMinDelta: function(currTime, prevTime) {
|
|
var date1 = new Date(currTime);
|
|
var date2 = new Date(prevTime);
|
|
var delta = date1 - date2;
|
|
|
|
if (delta / 1000 >= this.constructor.ONE_MINUTE) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Displays a text chat entry input box for sending messages.
|
|
*
|
|
* @property {loop.Dispatcher} dispatcher
|
|
* @property {Boolean} showPlaceholder Set to true to show the placeholder message.
|
|
* @property {Boolean} textChatEnabled Set to true to enable the box. If false, the
|
|
* text chat box won't be displayed.
|
|
*/
|
|
var TextChatInputView = React.createClass({displayName: "TextChatInputView",
|
|
mixins: [
|
|
React.addons.LinkedStateMixin,
|
|
React.addons.PureRenderMixin
|
|
],
|
|
|
|
propTypes: {
|
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
|
showPlaceholder: React.PropTypes.bool.isRequired,
|
|
textChatEnabled: React.PropTypes.bool.isRequired
|
|
},
|
|
|
|
getInitialState: function() {
|
|
return {
|
|
messageDetail: ""
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Handles a key being pressed - looking for the return key for submitting
|
|
* the form.
|
|
*
|
|
* @param {Object} event The DOM event.
|
|
*/
|
|
handleKeyDown: function(event) {
|
|
if (event.which === 13) {
|
|
this.handleFormSubmit(event);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handles submitting of the form - dispatches a send text chat message.
|
|
*
|
|
* @param {Object} event The DOM event.
|
|
*/
|
|
handleFormSubmit: function(event) {
|
|
event.preventDefault();
|
|
|
|
// Don't send empty messages.
|
|
if (!this.state.messageDetail) {
|
|
return;
|
|
}
|
|
|
|
this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
|
|
contentType: CHAT_CONTENT_TYPES.TEXT,
|
|
message: this.state.messageDetail,
|
|
sentTimestamp: (new Date()).toISOString()
|
|
}));
|
|
|
|
// Reset the form to empty, ready for the next message.
|
|
this.setState({ messageDetail: "" });
|
|
},
|
|
|
|
render: function() {
|
|
if (!this.props.textChatEnabled) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
React.createElement("div", {className: "text-chat-box"},
|
|
React.createElement("form", {onSubmit: this.handleFormSubmit},
|
|
React.createElement("input", {
|
|
onKeyDown: this.handleKeyDown,
|
|
placeholder: this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : "",
|
|
type: "text",
|
|
valueLink: this.linkState("messageDetail")})
|
|
)
|
|
)
|
|
);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Displays the text chat view. This includes the text chat messages as well
|
|
* as a field for entering new messages.
|
|
*
|
|
* @property {loop.Dispatcher} dispatcher
|
|
* @property {Boolean} showRoomName Set to true to show the room name
|
|
* special list item.
|
|
*/
|
|
var TextChatView = React.createClass({displayName: "TextChatView",
|
|
mixins: [
|
|
React.addons.LinkedStateMixin,
|
|
loop.store.StoreMixin("textChatStore")
|
|
],
|
|
|
|
propTypes: {
|
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
|
showRoomName: React.PropTypes.bool.isRequired,
|
|
useDesktopPaths: React.PropTypes.bool.isRequired
|
|
},
|
|
|
|
getInitialState: function() {
|
|
return this.getStoreState();
|
|
},
|
|
|
|
render: function() {
|
|
var messageList;
|
|
var showingRoomName = false;
|
|
|
|
if (this.props.showRoomName) {
|
|
messageList = this.state.messageList;
|
|
showingRoomName = this.state.messageList.some(function(item) {
|
|
return item.contentType === CHAT_CONTENT_TYPES.ROOM_NAME;
|
|
});
|
|
} else {
|
|
messageList = this.state.messageList.filter(function(item) {
|
|
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
|
|
item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
|
|
});
|
|
}
|
|
|
|
// Only show the placeholder if we've sent messages.
|
|
var hasSentMessages = messageList.some(function(item) {
|
|
return item.type === CHAT_MESSAGE_TYPES.SENT;
|
|
});
|
|
|
|
var textChatViewClasses = React.addons.classSet({
|
|
"showing-room-name": showingRoomName,
|
|
"text-chat-view": true,
|
|
"text-chat-disabled": !this.state.textChatEnabled,
|
|
"text-chat-entries-empty": !messageList.length
|
|
});
|
|
|
|
return (
|
|
React.createElement("div", {className: textChatViewClasses},
|
|
React.createElement(TextChatEntriesView, {
|
|
dispatcher: this.props.dispatcher,
|
|
messageList: messageList,
|
|
useDesktopPaths: this.props.useDesktopPaths}),
|
|
React.createElement(TextChatInputView, {
|
|
dispatcher: this.props.dispatcher,
|
|
showPlaceholder: !hasSentMessages,
|
|
textChatEnabled: this.state.textChatEnabled})
|
|
)
|
|
);
|
|
}
|
|
});
|
|
|
|
return {
|
|
TextChatEntriesView: TextChatEntriesView,
|
|
TextChatEntry: TextChatEntry,
|
|
TextChatView: TextChatView
|
|
};
|
|
})(navigator.mozL10n || document.mozL10n);
|