diff --git a/browser/components/loop/content/js/conversationViews.js b/browser/components/loop/content/js/conversationViews.js
index d6f007edb733..7b15af74916b 100644
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -14,6 +14,73 @@ loop.conversationViews = (function(mozL10n) {
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
+ /**
+ * Displays information about the call
+ * Caller avatar, name & conversation creation date
+ */
+ var CallIdentifierView = React.createClass({displayName: 'CallIdentifierView',
+ propTypes: {
+ peerIdentifier: React.PropTypes.string,
+ showIcons: React.PropTypes.bool.isRequired,
+ urlCreationDate: React.PropTypes.string,
+ video: React.PropTypes.bool
+ },
+
+ getDefaultProps: function() {
+ return {
+ peerIdentifier: "",
+ showLinkDetail: true,
+ urlCreationDate: "",
+ video: true
+ };
+ },
+
+ getInitialState: function() {
+ return {timestamp: 0};
+ },
+
+ /**
+ * Gets and formats the incoming call creation date
+ */
+ formatCreationDate: function() {
+ if (!this.props.urlCreationDate) {
+ return "";
+ }
+
+ var timestamp = this.props.urlCreationDate;
+ return "(" + loop.shared.utils.formatDate(timestamp) + ")";
+ },
+
+ render: function() {
+ var iconVideoClasses = React.addons.classSet({
+ "fx-embedded-tiny-video-icon": true,
+ "muted": !this.props.video
+ });
+ var callDetailClasses = React.addons.classSet({
+ "fx-embedded-call-detail": true,
+ "hide": !this.props.showIcons
+ });
+
+ return (
+ React.DOM.div({className: "fx-embedded-call-identifier"},
+ React.DOM.div({className: "fx-embedded-call-identifier-avatar fx-embedded-call-identifier-item"}),
+ React.DOM.div({className: "fx-embedded-call-identifier-info fx-embedded-call-identifier-item"},
+ React.DOM.div({className: "fx-embedded-call-identifier-text overflow-text-ellipsis font-bold"},
+ this.props.peerIdentifier
+ ),
+ React.DOM.div({className: callDetailClasses},
+ React.DOM.span({className: "fx-embedded-tiny-audio-icon"}),
+ React.DOM.span({className: iconVideoClasses}),
+ React.DOM.span({className: "fx-embedded-conversation-timestamp"},
+ this.formatCreationDate()
+ )
+ )
+ )
+ )
+ );
+ }
+ });
+
/**
* Displays details of the incoming/outgoing conversation
* (name, link, audio/video type etc).
@@ -51,7 +118,9 @@ loop.conversationViews = (function(mozL10n) {
return (
React.DOM.div({className: "call-window"},
- React.DOM.h2(null, contactName),
+ CallIdentifierView({
+ peerIdentifier: contactName,
+ showIcons: false}),
React.DOM.div(null, this.props.children)
)
);
@@ -382,6 +451,7 @@ loop.conversationViews = (function(mozL10n) {
return {
PendingConversationView: PendingConversationView,
+ CallIdentifierView: CallIdentifierView,
ConversationDetailView: ConversationDetailView,
CallFailedView: CallFailedView,
OngoingConversationView: OngoingConversationView,
diff --git a/browser/components/loop/content/js/conversationViews.jsx b/browser/components/loop/content/js/conversationViews.jsx
index 26ca7afd7639..18822b3f6564 100644
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -14,6 +14,73 @@ loop.conversationViews = (function(mozL10n) {
var sharedActions = loop.shared.actions;
var sharedViews = loop.shared.views;
+ /**
+ * Displays information about the call
+ * Caller avatar, name & conversation creation date
+ */
+ var CallIdentifierView = React.createClass({
+ propTypes: {
+ peerIdentifier: React.PropTypes.string,
+ showIcons: React.PropTypes.bool.isRequired,
+ urlCreationDate: React.PropTypes.string,
+ video: React.PropTypes.bool
+ },
+
+ getDefaultProps: function() {
+ return {
+ peerIdentifier: "",
+ showLinkDetail: true,
+ urlCreationDate: "",
+ video: true
+ };
+ },
+
+ getInitialState: function() {
+ return {timestamp: 0};
+ },
+
+ /**
+ * Gets and formats the incoming call creation date
+ */
+ formatCreationDate: function() {
+ if (!this.props.urlCreationDate) {
+ return "";
+ }
+
+ var timestamp = this.props.urlCreationDate;
+ return "(" + loop.shared.utils.formatDate(timestamp) + ")";
+ },
+
+ render: function() {
+ var iconVideoClasses = React.addons.classSet({
+ "fx-embedded-tiny-video-icon": true,
+ "muted": !this.props.video
+ });
+ var callDetailClasses = React.addons.classSet({
+ "fx-embedded-call-detail": true,
+ "hide": !this.props.showIcons
+ });
+
+ return (
+
+
+
+
+
+
+ {this.formatCreationDate()}
+
+
+
+
+ );
+ }
+ });
+
/**
* Displays details of the incoming/outgoing conversation
* (name, link, audio/video type etc).
@@ -51,7 +118,9 @@ loop.conversationViews = (function(mozL10n) {
return (
-
{contactName}
+
{this.props.children}
);
@@ -382,6 +451,7 @@ loop.conversationViews = (function(mozL10n) {
return {
PendingConversationView: PendingConversationView,
+ CallIdentifierView: CallIdentifierView,
ConversationDetailView: ConversationDetailView,
CallFailedView: CallFailedView,
OngoingConversationView: OngoingConversationView,
diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js
index 422246d004ed..31cf0ad2d8c8 100644
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -308,15 +308,6 @@ loop.panel = (function(_, mozL10n) {
this._fetchCallUrl();
},
- /**
- * Returns a random 5 character string used to identify
- * the conversation.
- * XXX this will go away once the backend changes
- */
- conversationIdentifier: function() {
- return Math.random().toString(36).substring(5);
- },
-
componentDidMount: function() {
// If we've already got a callURL, don't bother requesting a new one.
// As of this writing, only used for visual testing in the UI showcase.
@@ -332,7 +323,9 @@ loop.panel = (function(_, mozL10n) {
*/
_fetchCallUrl: function() {
this.setState({pending: true});
- this.props.client.requestCallUrl(this.conversationIdentifier(),
+ // XXX This is an empty string as a conversation identifier. Bug 1015938 implements
+ // a user-set string.
+ this.props.client.requestCallUrl("",
this._onCallUrlReceived);
},
diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx
index d8c397820230..ea8d7c116c4f 100644
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -308,15 +308,6 @@ loop.panel = (function(_, mozL10n) {
this._fetchCallUrl();
},
- /**
- * Returns a random 5 character string used to identify
- * the conversation.
- * XXX this will go away once the backend changes
- */
- conversationIdentifier: function() {
- return Math.random().toString(36).substring(5);
- },
-
componentDidMount: function() {
// If we've already got a callURL, don't bother requesting a new one.
// As of this writing, only used for visual testing in the UI showcase.
@@ -332,7 +323,9 @@ loop.panel = (function(_, mozL10n) {
*/
_fetchCallUrl: function() {
this.setState({pending: true});
- this.props.client.requestCallUrl(this.conversationIdentifier(),
+ // XXX This is an empty string as a conversation identifier. Bug 1015938 implements
+ // a user-set string.
+ this.props.client.requestCallUrl("",
this._onCallUrlReceived);
},
diff --git a/browser/components/loop/content/shared/css/conversation.css b/browser/components/loop/content/shared/css/conversation.css
index ecdb34bd115b..a33cc20ec13c 100644
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -94,12 +94,14 @@
}
.fx-embedded-btn-icon-video,
-.fx-embedded-btn-video-small {
+.fx-embedded-btn-video-small,
+.fx-embedded-tiny-video-icon {
background-image: url("../img/video-inverse-14x14.png");
}
.fx-embedded-btn-icon-audio,
-.fx-embedded-btn-audio-small {
+.fx-embedded-btn-audio-small,
+.fx-embedded-tiny-audio-icon {
background-image: url("../img/audio-inverse-14x14.png");
}
@@ -484,6 +486,74 @@
min-height: 200px;
}
+.fx-embedded-call-identifier {
+ display: inline;
+ width: 100%;
+ padding: 1.2em;
+}
+
+.fx-embedded-call-identifier-item {
+ height: 50px;
+}
+
+.fx-embedded-call-identifier-avatar {
+ max-width: 50px;
+ min-width: 50px;
+ background: #ccc;
+ border-radius: 50%;
+ background-image: url("../img/audio-call-avatar.svg");
+ background-repeat: no-repeat;
+ background-color: #4ba6e7;
+ background-size: contain;
+ overflow: hidden;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
+ float: left;
+ -moz-margin-end: 1em;
+}
+
+.fx-embedded-call-identifier-text {
+ font-weight: bold;
+}
+
+.fx-embedded-call-identifier-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ -moz-margin-start: 1em;
+}
+
+.fx-embedded-conversation-timestamp {
+ font-size: .6rem;
+ line-height: 17px;
+ display: inline-block;
+ vertical-align: top;
+}
+
+.fx-embedded-call-detail {
+ padding-top: 1.2em;
+}
+
+.fx-embedded-tiny-video-icon {
+ margin: 0 0.8em;
+}
+
+.fx-embedded-tiny-audio-icon,
+.fx-embedded-tiny-video-icon {
+ width: 18px;
+ height: 18px;
+ background-size: 12px 12px;
+ background-color: #4ba6e7;
+ display: inline-block;
+ background-repeat: no-repeat;
+ background-position: center;
+ border-radius: 50%;
+}
+
+ .fx-embedded-tiny-video-icon.muted {
+ background-color: rgba(0,0,0,.2)
+ }
+
@media screen and (min-width:640px) {
/* Force full height on all parents up to the video elements
diff --git a/browser/components/loop/content/shared/js/models.js b/browser/components/loop/content/shared/js/models.js
index f7c2b24bd009..c0e653c9fdc7 100644
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -32,6 +32,7 @@ loop.shared.models = (function(l10n) {
selectedCallType: "audio-video", // The selected type for the call that was
// initiated ("audio" or "audio-video")
callToken: undefined, // Incoming call token.
+ callUrl: undefined, // Incoming call url
// Used for blocking a call url
subscribedStream: false, // Used to indicate that a stream has been
// subscribed to
@@ -142,15 +143,18 @@ loop.shared.models = (function(l10n) {
setIncomingSessionData: function(sessionData) {
// Explicit property assignment to prevent later "surprises"
this.set({
- sessionId: sessionData.sessionId,
- sessionToken: sessionData.sessionToken,
- sessionType: sessionData.sessionType,
- apiKey: sessionData.apiKey,
- callId: sessionData.callId,
- progressURL: sessionData.progressURL,
- websocketToken: sessionData.websocketToken.toString(16),
- callType: sessionData.callType || "audio-video",
- callToken: sessionData.callToken
+ sessionId: sessionData.sessionId,
+ sessionToken: sessionData.sessionToken,
+ sessionType: sessionData.sessionType,
+ apiKey: sessionData.apiKey,
+ callId: sessionData.callId,
+ callerId: sessionData.callerId,
+ urlCreationDate: sessionData.urlCreationDate,
+ progressURL: sessionData.progressURL,
+ websocketToken: sessionData.websocketToken.toString(16),
+ callType: sessionData.callType || "audio-video",
+ callToken: sessionData.callToken,
+ callUrl: sessionData.callUrl
});
},
@@ -198,6 +202,23 @@ loop.shared.models = (function(l10n) {
return undefined;
},
+ /**
+ * Used to remove the scheme from a url.
+ */
+ _removeScheme: function(url) {
+ if (!url) {
+ return "";
+ }
+ return url.replace(/^https?:\/\//, "");
+ },
+
+ /**
+ * Returns a conversation identifier for the incoming call view
+ */
+ getCallIdentifier: function() {
+ return this.get("callerId") || this._removeScheme(this.get("callUrl"));
+ },
+
/**
* Publishes a local stream.
*
diff --git a/browser/components/loop/content/shared/js/utils.js b/browser/components/loop/content/shared/js/utils.js
index 09f004054b63..fc1d2b1f2582 100644
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -17,6 +17,18 @@ loop.shared.utils = (function() {
AUDIO_ONLY: "audio"
};
+ /**
+ * Format a given date into an l10n-friendly string.
+ *
+ * @param {Integer} The timestamp in seconds to format.
+ * @return {String} The formatted string.
+ */
+ function formatDate(timestamp) {
+ var date = (new Date(timestamp * 1000));
+ var options = {year: "numeric", month: "long", day: "numeric"};
+ return date.toLocaleDateString(navigator.language, options);
+ }
+
/**
* Used for adding different styles to the panel
* @returns {String} Corresponds to the client platform
@@ -87,6 +99,7 @@ loop.shared.utils = (function() {
return {
CALL_TYPES: CALL_TYPES,
Helper: Helper,
+ formatDate: formatDate,
getTargetPlatform: getTargetPlatform,
getBoolPreference: getBoolPreference
};
diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js
index e08f9d52da44..e8831895db56 100644
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -446,10 +446,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
if (err) {
this.props.notifications.errorL10n("unable_retrieve_call_info");
} else {
- var date = (new Date(callUrlInfo.urlCreationDate * 1000));
- var options = {year: "numeric", month: "long", day: "numeric"};
- var timestamp = date.toLocaleDateString(navigator.language, options);
- this.setState({urlCreationDateString: timestamp});
+ this.setState({
+ urlCreationDateString: sharedUtils.formatDate(callUrlInfo.urlCreationDate)
+ });
}
},
diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx
index 8d2e92e0c254..7c08edbcbda0 100644
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -446,10 +446,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
if (err) {
this.props.notifications.errorL10n("unable_retrieve_call_info");
} else {
- var date = (new Date(callUrlInfo.urlCreationDate * 1000));
- var options = {year: "numeric", month: "long", day: "numeric"};
- var timestamp = date.toLocaleDateString(navigator.language, options);
- this.setState({urlCreationDateString: timestamp});
+ this.setState({
+ urlCreationDateString: sharedUtils.formatDate(callUrlInfo.urlCreationDate)
+ });
}
},
diff --git a/browser/components/loop/test/desktop-local/conversationViews_test.js b/browser/components/loop/test/desktop-local/conversationViews_test.js
index 4e6394525a94..d1fe0925599a 100644
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -35,6 +35,67 @@ describe("loop.conversationViews", function () {
sandbox.restore();
});
+ describe("CallIdentifierView", function() {
+ function mountTestComponent(props) {
+ return TestUtils.renderIntoDocument(
+ loop.conversationViews.CallIdentifierView(props));
+ }
+
+ it("should set display the peer identifer", function() {
+ view = mountTestComponent({
+ showIcons: false,
+ peerIdentifier: "mrssmith"
+ });
+
+ expect(TestUtils.findRenderedDOMComponentWithClass(
+ view, "fx-embedded-call-identifier-text").props.children).eql("mrssmith");
+ });
+
+ it("should not display the icons if showIcons is false", function() {
+ view = mountTestComponent({
+ showIcons: false,
+ peerIdentifier: "mrssmith"
+ });
+
+ expect(TestUtils.findRenderedDOMComponentWithClass(
+ view, "fx-embedded-call-detail").props.className).to.contain("hide");
+ });
+
+ it("should display the icons if showIcons is true", function() {
+ view = mountTestComponent({
+ showIcons: true,
+ peerIdentifier: "mrssmith"
+ });
+
+ expect(TestUtils.findRenderedDOMComponentWithClass(
+ view, "fx-embedded-call-detail").props.className).to.not.contain("hide");
+ });
+
+ it("should display the url timestamp", function() {
+ sandbox.stub(loop.shared.utils, "formatDate").returns(("October 9, 2014"));
+
+ view = mountTestComponent({
+ showIcons: true,
+ peerIdentifier: "mrssmith",
+ urlCreationDate: (new Date() / 1000).toString()
+ });
+
+ expect(TestUtils.findRenderedDOMComponentWithClass(
+ view, "fx-embedded-conversation-timestamp").props.children).eql("(October 9, 2014)");
+ });
+
+ it("should show video as muted if video is false", function() {
+ view = mountTestComponent({
+ showIcons: true,
+ peerIdentifier: "mrssmith",
+ video: false
+ });
+
+ expect(TestUtils.findRenderedDOMComponentWithClass(
+ view, "fx-embedded-tiny-video-icon").props.className).to.contain("muted");
+ });
+ });
+
describe("ConversationDetailView", function() {
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
@@ -47,23 +108,14 @@ describe("loop.conversationViews", function () {
expect(document.title).eql("mrsmith");
});
- it("should set display the calledId", function() {
- view = mountTestComponent({contact: contact});
-
- expect(TestUtils.findRenderedDOMComponentWithTag(
- view, "h2").props.children).eql("mrsmith");
- });
-
it("should fallback to the email if the contact name is not defined",
function() {
delete contact.name;
- view = mountTestComponent({contact: contact});
+ mountTestComponent({contact: contact});
- expect(TestUtils.findRenderedDOMComponentWithTag(
- view, "h2").props.children).eql("fakeEmail");
- }
- );
+ expect(document.title).eql("fakeEmail");
+ });
});
describe("PendingConversationView", function() {
diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js
index 7d413f200247..5c211e9103a4 100644
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -696,7 +696,9 @@ describe("loop.conversation", function() {
var view, model;
beforeEach(function() {
- var Model = Backbone.Model.extend({});
+ var Model = Backbone.Model.extend({
+ getCallIdentifier: function() {return "fakeId";}
+ });
model = new Model();
sandbox.spy(model, "trigger");
sandbox.stub(model, "set");
diff --git a/browser/components/loop/test/shared/models_test.js b/browser/components/loop/test/shared/models_test.js
index 53998b70f97f..66db6a67a462 100644
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -27,7 +27,9 @@ describe("loop.shared.models", function() {
apiKey: "apiKey",
callType: "callType",
websocketToken: 123,
- callToken: "callToken"
+ callToken: "callToken",
+ callUrl: "http://invalid/callToken",
+ callerId: "mrssmith"
};
fakeSession = _.extend({
connect: function () {},
@@ -360,6 +362,28 @@ describe("loop.shared.models", function() {
expect(model.hasVideoStream("outgoing")).to.eql(true);
});
});
+
+ describe("#getCallIdentifier", function() {
+ var model;
+
+ beforeEach(function() {
+ model = new sharedModels.ConversationModel(fakeSessionData, {
+ sdk: fakeSDK
+ });
+ model.startSession();
+ });
+
+ it("should return the callerId", function() {
+ expect(model.getCallIdentifier()).to.eql("mrssmith");
+ });
+
+ it("should return the shorted callUrl if the callerId does not exist",
+ function() {
+ model.set({"callerId": ""});
+
+ expect(model.getCallIdentifier()).to.eql("invalid/callToken");
+ });
+ });
});
});
diff --git a/browser/components/loop/test/shared/utils_test.js b/browser/components/loop/test/shared/utils_test.js
index ee01efbbb1d6..c46674fca14d 100644
--- a/browser/components/loop/test/shared/utils_test.js
+++ b/browser/components/loop/test/shared/utils_test.js
@@ -88,6 +88,26 @@ describe("loop.shared.utils", function() {
});
});
+ describe("#formatDate", function() {
+ beforeEach(function() {
+ sandbox.stub(Date.prototype, "toLocaleDateString").returns("fake result");
+ });
+
+ it("should call toLocaleDateString with arguments", function() {
+ sharedUtils.formatDate(1000);
+
+ sinon.assert.calledOnce(Date.prototype.toLocaleDateString);
+ sinon.assert.calledWithExactly(Date.prototype.toLocaleDateString,
+ navigator.language,
+ {year: "numeric", month: "long", day: "numeric"}
+ );
+ });
+
+ it("should return the formatted string", function() {
+ expect(sharedUtils.formatDate(1000)).eql("fake result");
+ });
+ });
+
describe("#getBoolPreference", function() {
afterEach(function() {
navigator.mozLoop = undefined;
diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js
index 68fc3ec258e3..d04b11f98a68 100644
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -78,7 +78,10 @@
var mockSDK = {};
- var mockConversationModel = new loop.shared.models.ConversationModel({}, {
+ var mockConversationModel = new loop.shared.models.ConversationModel({
+ callerId: "Mrs Jones",
+ urlCreationDate: (new Date() / 1000).toString()
+ }, {
sdk: mockSDK
});
mockConversationModel.startSession = noop;
diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx
index f642005c3a5d..dafe37a6dca3 100644
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -78,7 +78,10 @@
var mockSDK = {};
- var mockConversationModel = new loop.shared.models.ConversationModel({}, {
+ var mockConversationModel = new loop.shared.models.ConversationModel({
+ callerId: "Mrs Jones",
+ urlCreationDate: (new Date() / 1000).toString()
+ }, {
sdk: mockSDK
});
mockConversationModel.startSession = noop;