Bug 1168833: introduce different sizing modes to docked social chat windows and re-style text chat UI indide the Hello conversation window. r=Standard8

This commit is contained in:
Mike de Boer 2015-06-12 11:21:35 +02:00
Родитель cba59000ab
Коммит 393b9d5a26
14 изменённых файлов: 308 добавлений и 85 удалений

Просмотреть файл

@ -34,8 +34,8 @@
* along any dimension beyond the point at which an overflow event would
* occur". But none of -moz-{fit,max,min}-content do what we want here. So..
*/
min-width: 320px;
min-height: 280px;
min-width: 260px;
min-height: 315px;
}
#main-window[customize-entered] {
@ -907,8 +907,32 @@ chatbox {
width: 260px; /* CHAT_WIDTH_OPEN in socialchat.xml */
}
chatbox[large="true"] {
width: 300px;
chatbox[customSize] {
width: 300px; /* CHAT_WIDTH_OPEN_ALT in socialchat.xml */
}
#chat-window[customSize] {
min-width: 300px;
}
chatbox[customSize="loopChatEnabled"] {
/* 325px as defined per UX */
height: 325px;
}
#chat-window[customSize="loopChatEnabled"] {
/* 325px + 30px top bar height. */
min-height: calc(325px + 30px);
}
chatbox[customSize="loopChatMessageAppended"] {
/* 445px as defined per UX */
height: 445px;
}
#chat-window[customSize="loopChatMessageAppended"] {
/* 445px + 30px top bar height. */
min-height: calc(445px + 30px);
}
chatbox[minimized="true"] {
@ -922,6 +946,15 @@ chatbar {
max-height: 0;
}
.chatbar-innerbox {
margin: -285px 0 0;
}
chatbar[customSize] > .chatbar-innerbox {
/* 425px to make room for the maximum custom-size chatbox; currently 'loopChatMessageAppended'. */
margin-top: -425px;
}
/* Apply crisp rendering for favicons at exactly 2dppx resolution */
@media (resolution: 2dppx) {
#social-sidebar-favico,

Просмотреть файл

@ -160,8 +160,7 @@
<parameter name="aTarget"/>
<body><![CDATA[
aTarget.setAttribute("label", this.contentDocument.title);
if (this.getAttribute("dark") == "true")
aTarget.setAttribute("dark", "true");
aTarget.src = this.src;
aTarget.content.setAttribute("origin", this.content.getAttribute("origin"));
aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
@ -169,6 +168,16 @@
]]></body>
</method>
<method name="setDecorationAttributes">
<parameter name="aTarget"/>
<body><![CDATA[
for (let attr of ["dark", "customSize"]) {
if (this.hasAttribute(attr))
aTarget.setAttribute(attr, this.getAttribute(attr));
}
]]></body>
</method>
<method name="onTitlebarClick">
<parameter name="aEvent"/>
<body><![CDATA[
@ -211,6 +220,8 @@
let chatbar = win.document.getElementById("pinnedchats");
let origin = this.content.getAttribute("origin");
let cb = chatbar.openChat(origin, title, "about:blank");
this.setDecorationAttributes(cb);
cb.promiseChatLoaded.then(
() => {
this.swapDocShells(cb);
@ -225,6 +236,12 @@
chatbar.chatboxForURL.delete("about:blank");
chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
let attachEvent = new cb.contentWindow.CustomEvent("socialFrameAttached", {
bubbles: true,
cancelable: true,
});
cb.contentDocument.dispatchEvent(attachEvent);
deferred.resolve(cb);
}
);
@ -457,8 +474,10 @@
// These are from the CSS for the chatbox and must be kept in sync.
// We can't use calcTotalWidthOf due to the transitions...
const CHAT_WIDTH_OPEN = 260;
const CHAT_WIDTH_OPEN_ALT = 300;
const CHAT_WIDTH_MINIMIZED = 160;
let openWidth = aChatbox.hasAttribute("large") ? 300 : CHAT_WIDTH_OPEN;
let openWidth = aChatbox.hasAttribute("customSize") ?
CHAT_WIDTH_OPEN_ALT : CHAT_WIDTH_OPEN;
return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : openWidth;
]]></body>
@ -684,18 +703,27 @@
if (event.target != otherWin.document)
return;
if (aChatbox.hasAttribute("customSize")) {
otherWin.document.getElementById("chat-window").
setAttribute("customSize", aChatbox.getAttribute("customSize"));
}
let document = aChatbox.contentDocument;
let detachEvent = new aChatbox.contentWindow.CustomEvent("socialFrameDetached", {
bubbles: true,
cancelable: true,
});
aChatbox.contentDocument.dispatchEvent(detachEvent);
otherWin.removeEventListener("load", _chatLoad, true);
let otherChatbox = otherWin.document.getElementById("chatter");
aChatbox.setDecorationAttributes(otherChatbox);
aChatbox.swapDocShells(otherChatbox);
aChatbox.close();
chatbar.chatboxForURL.set(aChatbox.src, Cu.getWeakReference(otherChatbox));
// All processing is done, now we can fire the event.
document.dispatchEvent(detachEvent);
deferred.resolve(otherChatbox);
}, true);
return deferred.promise;

Просмотреть файл

@ -718,7 +718,6 @@ loop.roomViews = (function(mozL10n) {
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher}),
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
@ -761,7 +760,8 @@ loop.roomViews = (function(mozL10n) {
savingContext: this.state.savingContext,
mozLoop: this.props.mozLoop,
roomData: roomData,
show: !shouldRenderInvitationOverlay && shouldRenderContextView})
show: !shouldRenderInvitationOverlay && shouldRenderContextView}),
React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher})
)
);
}

Просмотреть файл

@ -718,7 +718,6 @@ loop.roomViews = (function(mozL10n) {
return (
<div className="room-conversation-wrapper">
<sharedViews.TextChatView dispatcher={this.props.dispatcher} />
<DesktopRoomInvitationView
dispatcher={this.props.dispatcher}
error={this.state.error}
@ -762,6 +761,7 @@ loop.roomViews = (function(mozL10n) {
mozLoop={this.props.mozLoop}
roomData={roomData}
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
<sharedViews.TextChatView dispatcher={this.props.dispatcher} />
</div>
);
}

Просмотреть файл

@ -651,27 +651,6 @@ html, .fx-embedded, #main,
height: 100%;
}
/**
* The .fx-embbeded .text-chat-* styles are very temporarily whilst we work on
* text chat (bug 1108892 and dependencies).
*/
.fx-embedded .text-chat-view {
height: 60px;
color: white;
background-color: black;
}
.fx-embedded .text-chat-entries {
/* XXX Should use flex, this is just for the initial implementation. */
height: calc(100% - 2em);
width: 100%;
}
.fx-embedded .text-chat-box {
width: 100%;
margin: auto;
}
/* We use 641px rather than 640, as min-width and max-width are inclusive */
@media screen and (min-width:641px) {
.standalone .conversation-toolbar {
@ -681,10 +660,6 @@ html, .fx-embedded, #main,
right: 0;
}
.fx-embedded .local-stream {
position: fixed;
}
.standalone .local-stream,
.standalone .remote-inset-stream {
position: absolute;
@ -697,7 +672,7 @@ html, .fx-embedded, #main,
}
/* Nested video elements */
.conversation .media.nested {
.standalone .conversation .media.nested {
position: relative;
height: 100%;
}
@ -744,7 +719,7 @@ html, .fx-embedded, #main,
}
/* Nested video elements */
.conversation .media.nested {
.standalone .conversation .media.nested {
display: flex;
flex-direction: column;
align-items: center;
@ -1239,18 +1214,64 @@ html[dir="rtl"] .room-context-btn-edit {
height: auto;
}
/* Text chat in rooms styles */
.fx-embedded .room-conversation-wrapper {
display: flex;
flex-flow: column nowrap;
}
.fx-embedded .video-layout-wrapper {
flex: 1 1 auto;
}
.text-chat-view {
background: #fff;
}
.fx-embedded .text-chat-view {
flex: 1 0 auto;
display: flex;
flex-flow: column nowrap;
}
.fx-embedded .text-chat-entries {
flex: 1 1 auto;
max-height: 120px;
min-height: 60px;
padding: .7em .5em 0;
}
.fx-embedded .text-chat-box {
flex: 0 0 auto;
max-height: 40px;
min-height: 40px;
width: 100%;
}
.text-chat-entries {
margin: auto;
overflow: scroll;
border: 1px solid red;
}
.text-chat-entry {
text-align: left;
text-align: end;
margin-bottom: 1.5em;
}
.text-chat-entry > span {
border-width: 1px;
border-style: solid;
border-color: #0095dd;
border-radius: 10000px;
padding: .5em 1em;
}
.text-chat-entry.received {
text-align: right;
text-align: start;
}
.text-chat-entry.received > span {
border-color: #d8d8d8;
}
.text-chat-box {
@ -1259,6 +1280,14 @@ html[dir="rtl"] .room-context-btn-edit {
.text-chat-box > form > input {
width: 100%;
height: 40px;
padding: 0 .5em .5em;
font-size: 1.1em;
}
.fx-embedded .text-chat-box > form > input {
border: 0;
border-top: 1px solid #999;
}
@media screen and (max-width:640px) {

Просмотреть файл

@ -68,6 +68,27 @@ loop.store.TextChatStore = (function() {
*/
dataChannelsAvailable: function() {
this.setStoreState({ textChatEnabled: true });
window.dispatchEvent(new CustomEvent("LoopChatEnabled"));
},
/**
* Appends a message to the store, which may be of type 'sent' or 'received'.
*
* @param {String} type
* @param {sharedActions.ReceivedTextChatMessage|sharedActions.SendTextChatMessage} actionData
*/
_appendTextChatMessage: function(type, actionData) {
// We create a new list to avoid updating the store's state directly,
// which confuses the views.
var message = {
type: type,
contentType: actionData.contentType,
message: actionData.message
};
var newList = this._storeState.messageList.concat(message);
this.setStoreState({ messageList: newList });
window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
},
/**
@ -81,14 +102,8 @@ loop.store.TextChatStore = (function() {
if (actionData.contentType != CHAT_CONTENT_TYPES.TEXT) {
return;
}
// We create a new list to avoid updating the store's state directly,
// which confuses the views.
var newList = this._storeState.messageList.concat({
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: actionData.contentType,
message: actionData.message
});
this.setStoreState({ messageList: newList });
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, actionData);
},
/**
@ -97,15 +112,8 @@ loop.store.TextChatStore = (function() {
* @param {sharedActions.SendTextChatMessage} actionData
*/
sendTextChatMessage: function(actionData) {
// We create a new list to avoid updating the store's state directly,
// which confuses the views.
var newList = this._storeState.messageList.concat({
type: CHAT_MESSAGE_TYPES.SENT,
contentType: actionData.contentType,
message: actionData.message
});
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SENT, actionData);
this._sdkDriver.sendTextChatMessage(actionData);
this.setStoreState({ messageList: newList });
}
});

Просмотреть файл

@ -29,7 +29,7 @@ loop.shared.views.TextChatView = (function(mozl10n) {
return (
React.createElement("div", {className: classes},
this.props.message
React.createElement("span", null, this.props.message)
)
);
}
@ -49,6 +49,9 @@ loop.shared.views.TextChatView = (function(mozl10n) {
componentWillUpdate: function() {
var node = this.getDOMNode();
if (!node) {
return;
}
// Scroll only if we're right at the bottom of the display.
this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
},
@ -64,17 +67,23 @@ loop.shared.views.TextChatView = (function(mozl10n) {
},
render: function() {
if (!this.props.messageList.length) {
return null;
}
return (
React.createElement("div", {className: "text-chat-entries"},
this.props.messageList.map(function(entry, i) {
return (
React.createElement(TextChatEntry, {key: i,
message: entry.message,
type: entry.type})
);
}, this)
React.createElement("div", {className: "text-chat-scroller"},
this.props.messageList.map(function(entry, i) {
return (
React.createElement(TextChatEntry, {key: i,
message: entry.message,
type: entry.type})
);
}, this)
)
)
);
}
@ -134,12 +143,15 @@ loop.shared.views.TextChatView = (function(mozl10n) {
return null;
}
var messageList = this.state.messageList;
return (
React.createElement("div", {className: "text-chat-view"},
React.createElement(TextChatEntriesView, {messageList: this.state.messageList}),
React.createElement(TextChatEntriesView, {messageList: messageList}),
React.createElement("div", {className: "text-chat-box"},
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("input", {type: "text",
placeholder: messageList.length ? "" : mozl10n.get("chat_textbox_placeholder"),
onKeyDown: this.handleKeyDown,
valueLink: this.linkState("messageDetail")})
)

Просмотреть файл

@ -29,7 +29,7 @@ loop.shared.views.TextChatView = (function(mozl10n) {
return (
<div className={classes}>
{this.props.message}
<span>{this.props.message}</span>
</div>
);
}
@ -49,6 +49,9 @@ loop.shared.views.TextChatView = (function(mozl10n) {
componentWillUpdate: function() {
var node = this.getDOMNode();
if (!node) {
return;
}
// Scroll only if we're right at the bottom of the display.
this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
},
@ -64,17 +67,23 @@ loop.shared.views.TextChatView = (function(mozl10n) {
},
render: function() {
if (!this.props.messageList.length) {
return null;
}
return (
<div className="text-chat-entries">
{
this.props.messageList.map(function(entry, i) {
return (
<TextChatEntry key={i}
message={entry.message}
type={entry.type} />
);
}, this)
}
<div className="text-chat-scroller">
{
this.props.messageList.map(function(entry, i) {
return (
<TextChatEntry key={i}
message={entry.message}
type={entry.type} />
);
}, this)
}
</div>
</div>
);
}
@ -134,12 +143,15 @@ loop.shared.views.TextChatView = (function(mozl10n) {
return null;
}
var messageList = this.state.messageList;
return (
<div className="text-chat-view">
<TextChatEntriesView messageList={this.state.messageList} />
<TextChatEntriesView messageList={messageList} />
<div className="text-chat-box">
<form onSubmit={this.handleFormSubmit}>
<input type="text"
placeholder={messageList.length ? "" : mozl10n.get("chat_textbox_placeholder")}
onKeyDown={this.handleKeyDown}
valueLink={this.linkState("messageDetail")} />
</form>

Просмотреть файл

@ -853,27 +853,51 @@ let MozLoopServiceInternal = {
return;
}
chatbox.setAttribute("dark", true);
chatbox.setAttribute("large", true);
chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
if (event.target != chatbox.contentDocument) {
return;
}
chatbox.removeEventListener("DOMContentLoaded", loaded, true);
let chatbar = chatbox.parentNode;
let window = chatbox.contentWindow;
function socialFrameChanged(eventName) {
UITour.availableTargetsCache.clear();
UITour.notify(eventName);
if (eventName == "Loop:ChatWindowDetached" || eventName == "Loop:ChatWindowAttached") {
// After detach, re-attach of the chatbox, refresh its reference so
// we can keep using it here.
let ref = chatbar.chatboxForURL.get(chatbox.src);
chatbox = ref && ref.get() || chatbox;
}
}
window.addEventListener("socialFrameHide", socialFrameChanged.bind(null, "Loop:ChatWindowHidden"));
window.addEventListener("socialFrameShow", socialFrameChanged.bind(null, "Loop:ChatWindowShown"));
window.addEventListener("socialFrameDetached", socialFrameChanged.bind(null, "Loop:ChatWindowDetached"));
window.addEventListener("socialFrameAttached", socialFrameChanged.bind(null, "Loop:ChatWindowAttached"));
window.addEventListener("unload", socialFrameChanged.bind(null, "Loop:ChatWindowClosed"));
const kSizeMap = {
LoopChatEnabled: "loopChatEnabled",
LoopChatMessageAppended: "loopChatMessageAppended"
};
function onChatEvent(event) {
// When the chat box or messages are shown, resize the panel or window
// to be slightly higher to accomodate them.
let customSize = kSizeMap[event.type];
if (customSize) {
chatbox.setAttribute("customSize", customSize);
chatbox.parentNode.setAttribute("customSize", customSize);
}
}
window.addEventListener("LoopChatEnabled", onChatEvent);
window.addEventListener("LoopChatMessageAppended", onChatEvent);
injectLoopAPI(window);
let ourID = window.QueryInterface(Ci.nsIInterfaceRequestor)
@ -918,8 +942,16 @@ let MozLoopServiceInternal = {
}.bind(this), true);
};
if (!Chat.open(null, origin, "", url, undefined, undefined, callback)) {
let chatbox = Chat.open(null, origin, "", url, undefined, undefined, callback);
if (!chatbox) {
return null;
// It's common for unit tests to overload Chat.open.
} else if (chatbox.setAttribute) {
// Set properties that influence visual appeara nce of the chatbox right
// away to circumvent glitches.
chatbox.setAttribute("dark", true);
chatbox.setAttribute("customSize", "loopDefault");
chatbox.parentNode.setAttribute("customSize", "loopDefault");
}
return windowId;
},

Просмотреть файл

@ -137,3 +137,7 @@ status_in_conversation=In conversation
status_conversation_ended=Conversation ended
status_error=Something went wrong
support_link=Get Help
# Text chat strings
chat_textbox_placeholder=Type here…

Просмотреть файл

@ -25,6 +25,11 @@ describe("loop.store.TextChatStore", function () {
store = new loop.store.TextChatStore(dispatcher, {
sdkDriver: fakeSdkDriver
});
sandbox.stub(window, "dispatchEvent");
sandbox.stub(window, "CustomEvent", function(name) {
this.name = name;
});
});
afterEach(function() {
@ -37,6 +42,14 @@ describe("loop.store.TextChatStore", function () {
expect(store.getStoreState("textChatEnabled")).eql(true);
});
it("should dispatch a LoopChatEnabled event", function() {
store.dataChannelsAvailable();
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatEnabled"));
});
});
describe("#receivedTextChatMessage", function() {
@ -63,6 +76,17 @@ describe("loop.store.TextChatStore", function () {
expect(store.getStoreState("messageList").length).eql(0);
});
it("should dispatch a LoopChatMessageAppended event", function() {
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatMessageAppended"));
});
});
describe("#sendTextChatMessage", function() {
@ -92,5 +116,16 @@ describe("loop.store.TextChatStore", function () {
message: messageData.message
}]);
});
it("should dipatch a LoopChatMessageAppended event", function() {
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatMessageAppended"));
});
});
});

Просмотреть файл

@ -80,5 +80,32 @@ describe("loop.shared.views.TextChatView", function () {
message: "Hello!"
}));
});
it("should not render message entries when none are sent/ received yet", function() {
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".text-chat-entries")).to.eql(null);
});
it("should render message entries when message were sent/ received", function() {
view = mountTestComponent();
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Is it me you're looking for?"
});
var node = view.getDOMNode();
expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
var entries = node.querySelectorAll(".text-chat-entry");
expect(entries.length).to.eql(2);
expect(entries[0].classList.contains("received")).to.eql(true);
expect(entries[1].classList.contains("received")).to.not.eql(true);
});
});
});

Просмотреть файл

@ -354,3 +354,7 @@ context_show_tooltip=Show Context
context_save_label2=Save
context_link_modified=This link was modified.
context_learn_more_link_label=Learn more.
# Text chat strings
chat_textbox_placeholder=Type here…

Просмотреть файл

@ -195,7 +195,6 @@ chatbox[dark=true] > .chat-titlebar > hbox > .chat-title {
.chatbar-innerbox {
background: transparent;
margin: -285px 0 0;
overflow: hidden;
}