diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 52675d4f439a..814f6416f062 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1409,8 +1409,8 @@ pref("devtools.inspector.activeSidebar", "ruleview"); // Enable the markup preview pref("devtools.inspector.markupPreview", false); pref("devtools.inspector.remote", false); -// Expand pseudo-elements by default in the rule-view -pref("devtools.inspector.show_pseudo_elements", true); +// Collapse pseudo-elements by default in the rule-view +pref("devtools.inspector.show_pseudo_elements", false); // The default size for image preview tooltips in the rule-view/computed-view/markup-view pref("devtools.inspector.imagePreviewTooltipSize", 300); // Enable user agent style inspection in rule-view @@ -1836,6 +1836,9 @@ pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox // The remote URL of the FxA OAuth Server pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1"); +// Whether we display profile images in the UI or not. +pref("identity.fxaccounts.profile_image.enabled", true); + // Migrate any existing Firefox Account data from the default profile to the // Developer Edition profile. #ifdef MOZ_DEV_EDITION diff --git a/browser/base/content/browser-fullScreen.js b/browser/base/content/browser-fullScreen.js index 9eed60801a15..49fcf44c6272 100644 --- a/browser/base/content/browser-fullScreen.js +++ b/browser/base/content/browser-fullScreen.js @@ -90,6 +90,12 @@ var FullScreen = { // This is needed if they use the context menu to quit fullscreen this._isPopupOpen = false; this.cleanup(); + // In TabsInTitlebar._update(), we cancel the appearance update on + // resize event for exiting fullscreen, since that happens before we + // change the UI here in the "fullscreen" event. Hence we need to + // call it here to ensure the appearance is properly updated. See + // TabsInTitlebar._update() and bug 1173768. + TabsInTitlebar.updateAppearance(true); } }, diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index f7806a71bb54..a23b353576f7 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -5066,7 +5066,15 @@ var TabsInTitlebar = { if (this._lastSizeMode == sizemode) { return; } + let oldSizeMode = this._lastSizeMode; this._lastSizeMode = sizemode; + // Don't update right now if we are leaving fullscreen, since the UI is + // still changing in the consequent "fullscreen" event. Code there will + // call this function again when everything is ready. + // See browser-fullScreen.js: FullScreen.toggle and bug 1173768. + if (oldSizeMode == "fullscreen") { + return; + } } for (let something in this._disallowed) { diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css index db7edbaa4672..bae22a33ef75 100644 --- a/browser/base/content/newtab/newTab.css +++ b/browser/base/content/newtab/newTab.css @@ -501,6 +501,16 @@ input[type=button] { transform: translate(-30px, -20px) scale(0) translate(30px, 20px); } +#newtab-customize-panel:-moz-locale-dir(rtl) { + transform-origin: 40px top 20px; +} + +#newtab-customize-panel:-moz-locale-dir(rtl), +#newtab-customize-panel-anchor:-moz-locale-dir(rtl) { + left: 15px; + right: auto; +} + #newtab-customize-panel[open="true"] { transform: translate(-30px, -20px) scale(1) translate(30px, 20px); } diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 942739a83236..ddeadb8ed964 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -337,7 +337,7 @@ skip-if = buildapp == 'mulet' [browser_parsable_css.js] skip-if = e10s [browser_parsable_script.js] -skip-if = asan # Disabled because it takes a long time (see test for more information) +skip-if = asan || (os == 'linux' && !debug && (bits == 32)) # disabled on asan because of timeouts, and bug 1172468 for the linux 32-bit pgo issue. [browser_pinnedTabs.js] [browser_plainTextLinks.js] diff --git a/browser/base/content/test/newtab/browser_newtab_enhanced.js b/browser/base/content/test/newtab/browser_newtab_enhanced.js index 02176a6b9ac3..4225f173b512 100644 --- a/browser/base/content/test/newtab/browser_newtab_enhanced.js +++ b/browser/base/content/test/newtab/browser_newtab_enhanced.js @@ -8,7 +8,7 @@ let suggestedLink = { imageURI: "data:image/png;base64,helloWORLD3", title: "title2", type: "affiliate", - frecent_sites: ["classroom.google.com", "codecademy.com", "elearning.ut.ac.id", "khanacademy.org", "learn.jquery.com", "teamtreehouse.com", "tutorialspoint.com", "udacity.com", "w3cschool.cc", "w3schools.com"] + frecent_sites: ["classroom.google.com", "codeacademy.org", "codecademy.com", "codeschool.com", "codeyear.com", "elearning.ut.ac.id", "how-to-build-websites.com", "htmlcodetutorial.com", "htmldog.com", "htmlplayground.com", "learn.jquery.com", "quackit.com", "roseindia.net", "teamtreehouse.com", "tizag.com", "tutorialspoint.com", "udacity.com", "w3schools.com", "webdevelopersnotes.com"] }; gDirectorySource = "data:application/json," + JSON.stringify({ @@ -139,7 +139,7 @@ function runTests() { is(type, "affiliate", "suggested link is affiliate"); is(enhanced, "", "suggested link has no enhanced image"); is(title, "title2"); - ok(suggested.indexOf("Suggested for Web Education visitors") > -1, "Suggested for 'Web Education'"); + ok(suggested.indexOf("Suggested for webdev education visitors") > -1, "Suggested for 'webdev education'"); // Enhanced history link shows up second ({type, enhanced, title, suggested} = getData(1)); @@ -176,7 +176,7 @@ function runTests() { ok(suggested.indexOf("Suggested for Technology enthusiasts who visit sites like classroom.google.com ") > -1, "Suggested for 'Technology' enthusiasts"); - // Test server provided explanation string without category override. + // Test server provided explanation string without category override. delete suggestedLink.adgroup_name; Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))); @@ -185,5 +185,31 @@ function runTests() { yield addNewTabPageTab(); ({type, enhanced, title, suggested} = getData(0)); Cu.reportError("SUGGEST " + suggested); - ok(suggested.indexOf("Suggested for Web Education enthusiasts who visit sites like classroom.google.com ") > -1, "Suggested for 'Web Education' enthusiasts"); + ok(suggested.indexOf("Suggested for webdev education enthusiasts who visit sites like classroom.google.com ") > -1, "Suggested for 'webdev education' enthusiasts"); + + + + // Test with xml entities in category name + suggestedLink.url = "http://example1.com/3"; + suggestedLink.adgroup_name = ">angles< & \"quotes\'"; + Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, + "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))); + yield watchLinksChangeOnce().then(TestRunner.next); + + yield addNewTabPageTab(); + ({type, enhanced, title, suggested} = getData(0)); + Cu.reportError("SUGGEST " + suggested); + ok(suggested.indexOf("Suggested for >angles< & \"quotes\' enthusiasts who visit sites like classroom.google.com ") > -1, "Suggested for 'xml entities' enthusiasts"); + + + // Test with xml entities in explanation. + suggestedLink.explanation = "Testing junk explanation &<>\"'"; + Services.prefs.setCharPref(PREF_NEWTAB_DIRECTORYSOURCE, + "data:application/json," + encodeURIComponent(JSON.stringify({"suggested": [suggestedLink]}))); + yield watchLinksChangeOnce().then(TestRunner.next); + + yield addNewTabPageTab(); + ({type, enhanced, title, suggested} = getData(0)); + Cu.reportError("SUGGEST " + suggested); + ok(suggested.indexOf("Testing junk explanation &<>\"'") > -1, "Junk test"); } diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 2c6d18937826..811619fafcb5 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -730,6 +730,7 @@ loop.panel = (function(_, mozL10n) { var description = metadata.title || metadata.description; var url = metadata.url; this.setState({ + checked: false, previewImage: previewImage, description: description, url: url @@ -775,7 +776,8 @@ loop.panel = (function(_, mozL10n) { return ( React.createElement("div", {className: "new-room-view"}, React.createElement("div", {className: contextClasses}, - React.createElement(Checkbox, {label: mozL10n.get("context_inroom_label"), + React.createElement(Checkbox, {checked: this.state.checked, + label: mozL10n.get("context_inroom_label"), onChange: this.onCheckboxChange}), React.createElement(sharedViews.ContextUrlView, { allowClick: false, diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index 44068e726cbb..c0e8f1b3aa27 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -730,6 +730,7 @@ loop.panel = (function(_, mozL10n) { var description = metadata.title || metadata.description; var url = metadata.url; this.setState({ + checked: false, previewImage: previewImage, description: description, url: url @@ -775,7 +776,8 @@ loop.panel = (function(_, mozL10n) { return (
- .dropdown-menu-item { padding-left: 1em; padding-right: 1em; padding-bottom: 0.5em; + margin-bottom: 0.5em; background-color: #E8F6FE; } @@ -1080,6 +1088,198 @@ html[dir="rtl"] .room-context-btn-edit { left: 20px; } +.media-layout { + /* 50px is the header, 3em is the footer. */ + height: calc(100% - 50px - 3em); +} + +.media-layout > .media-wrapper { + display: flex; + flex-flow: column wrap; + /* 64px for .conversation-toolbar */ + height: calc(100% - 64px); + margin: 0 10px; +} + +.media-wrapper > .focus-stream { + /* We want this to be the width, minus 200px which is for the right-side text + chat and video displays. */ + width: calc(100% - 200px); + /* 100% height to fill up media-layout, thus forcing other elements into the + second column that's 200px wide */ + height: 100%; + background-color: #4E4E4E; +} + +.media-wrapper > .remote { + /* Works around an issue with object-fit: cover in Google Chrome - it doesn't + currently crop but overlaps the surrounding elements. + https://code.google.com/p/chromium/issues/detail?id=400829 */ + overflow: hidden; +} + +.media-wrapper > .remote > .remote-video { + object-fit: cover; +} + +/* Note: we can't use flex for the text-chat-view as this lets it overflow + the expected column heights, and we ca't fix its height. */ +.media-wrapper > .text-chat-view { + flex: 0 0 auto; + /* Text chat is a fixed 200px width for normal displays. */ + width: 200px; + height: 100%; +} + +.media-wrapper.showing-local-streams > .text-chat-view { + /* When we're displaying the local streams, then we need to make the text + chat view a bit shorter to give room. */ + height: calc(100% - 150px); +} + +.media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view { + /* When we're displaying the local streams, then we need to make the text + chat view a bit shorter to give room. */ + height: calc(100% - 300px); +} + +.media-wrapper > .text-chat-view > .text-chat-entries { + /* 40px is the height of .text-chat-box. */ + height: calc(100% - 40px); +} + +.media-wrapper > .local { + flex: 0 1 auto; + width: 200px; + height: 150px; +} + +.media-wrapper.receiving-screen-share > .screen { + order: 1; +} + +.media-wrapper.receiving-screen-share > .text-chat-view { + order: 2; +} + +.media-wrapper.receiving-screen-share > .remote { + order: 3; + flex: 0 1 auto; + width: 200px; + height: 150px; +} + +.media-wrapper.receiving-screen-share > .local { + order: 4; +} + +@media screen and (max-width:640px) { + .media-layout { + /* 50px is height of header, 25px is height of footer. */ + height: calc(100% - 50px - 25px); + } + + .media-layout > .media-wrapper { + flex-direction: row; + margin: 0; + width: 100%; + /* conversation toolbar is 38px in narrow mode */ + height: calc(100% - 38px); + } + + .media-wrapper > .focus-stream { + width: 100%; + /* A reasonable height */ + height: 70%; + } + + .media-wrapper.receiving-screen-share > .focus-stream { + height: 50%; + } + + .media-wrapper > .text-chat-view > .text-chat-entries { + /* 40px is the height of .text-chat-box. */ + height: calc(100% - 40px); + width: 100%; + } + + .media-wrapper > .local { + /* Position over the remote video */ + position: absolute; + /* Make sure its on top */ + z-index: 1001; + margin: 3px; + right: 0; + /* 29px is (30% of 50px high header) + (height toolbar (38px) + + height footer (25px) - height header (50px)) */ + bottom: calc(30% + 29px); + width: 120px; + height: 120px; + } + + html[dir="rtl"] .media-wrapper > .local { + right: auto; + left: 0; + } + + .media-wrapper > .text-chat-view { + order: 3; + flex: 1 1 auto; + width: 100%; + } + + .media-wrapper > .text-chat-view, + .media-wrapper.showing-local-streams > .text-chat-view, + .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view { + /* The remaining 30% that the .focus-stream doesn't use. */ + height: 30%; + } + + .media-wrapper.receiving-screen-share > .screen { + order: 1; + } + + .media-wrapper.receiving-screen-share > .remote { + /* Screen shares have remote & local video side-by-side on narrow screens */ + order: 2; + flex: 1 1 auto; + height: 20%; + /* Ensure no previously specified widths take effect, and we take up no more + than half the width. */ + width: auto; + max-width: 50%; + } + + .media-wrapper.receiving-screen-share > .remote > .remote-video { + /* Reset the object-fit for this. */ + object-fit: contain; + } + + .media-wrapper.receiving-screen-share > .local { + /* Screen shares have remote & local video side-by-side on narrow screens */ + order: 3; + flex: 1 1 auto; + height: 20%; + /* Ensure no previously specified widths take effect, and we take up no more + than half the width. */ + width: auto; + max-width: 50%; + /* This cancels out the absolute positioning when it's just remote video. */ + position: relative; + bottom: auto; + right: auto; + margin: 0; + } + + .media-wrapper.receiving-screen-share > .text-chat-view { + order: 4; + } +} + +.standalone > #main > .room-conversation-wrapper > .media-layout > .conversation-toolbar { + border: none; +} + /* Standalone rooms */ .standalone .room-conversation-wrapper { @@ -1108,6 +1308,11 @@ html[dir="rtl"] .room-context-btn-edit { box-sizing: content-box; } +html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area { + right: 25%; + left: auto; +} + .standalone .prompt-media-message { padding-top: 136px; /* Fallback for browsers that don't support calc() */ /* 122px is 2x the intrinsic height of the background-image, and @@ -1153,23 +1358,6 @@ html[dir="rtl"] .room-context-btn-edit { max-width: 400px; } -.standalone-room-info { - position: absolute; - display: block; - top: 0; - right: 10px; - /* 20px is 10px for left and right margins. */ - width: calc(25% - 20px); - z-index: 2000000; - font-size: 1.2em; - padding: .4em; - height: 100%; -} - -.standalone-room-info > h2 { - color: #fff; -} - .standalone-context-url { color: #fff; /* Try and keep clear of local video */ @@ -1243,7 +1431,7 @@ html[dir="rtl"] .room-context-btn-edit { padding: .7em .5em 0; } -.fx-embedded .text-chat-box { +.text-chat-box { flex: 0 0 auto; max-height: 40px; min-height: 40px; @@ -1309,20 +1497,6 @@ html[dir="rtl"] .room-context-btn-edit { } @media screen and (max-width:640px) { - .standalone-room-info { - /* This isn't perfect, we just center the heading for now. Bug 1141493 - should fix this. */ - position: absolute; - width: 100%; - right: 0px; - - /* Override the 100% specified in the .standalone-room-info selector - block so that this div doesn't take over the _whole_ screen and - transparently occlude UI widgetry (like the Join button), making - it unusable. */ - height: auto; - } - .standalone-context-url { /* XXX We haven't got UX for standalone yet, so temporarily not displaying on narrow window widths. See bug 1153827. */ @@ -1333,9 +1507,7 @@ html[dir="rtl"] .room-context-btn-edit { .standalone .room-conversation { background: #000; } - .room-conversation-wrapper header { - width: 100%; - } + .standalone .room-conversation-wrapper .room-inner-info-area { right: 0; margin: auto; @@ -1357,7 +1529,7 @@ html[dir="rtl"] .room-context-btn-edit { height: 38px; padding: 8px; } - .standalone .focus-stream { + .standalone .conversation .focus-stream { /* Set at maximum height, minus height of conversation toolbar */ height: 100%; } @@ -1418,8 +1590,6 @@ html[dir="rtl"] .room-context-btn-edit { .remote-video { width: 100%; height: 100%; - display: block; - position: absolute; } .screen-share-video { diff --git a/browser/components/loop/content/shared/js/actions.js b/browser/components/loop/content/shared/js/actions.js index 09afadf53391..63ee7c707067 100644 --- a/browser/components/loop/content/shared/js/actions.js +++ b/browser/components/loop/content/shared/js/actions.js @@ -169,6 +169,7 @@ loop.shared.actions = (function() { * Used to notify that the session has a data channel available. */ DataChannelsAvailable: Action.define("dataChannelsAvailable", { + available: Boolean }), /** diff --git a/browser/components/loop/content/shared/js/mixins.js b/browser/components/loop/content/shared/js/mixins.js index a08518189bbd..d02a8ef77502 100644 --- a/browser/components/loop/content/shared/js/mixins.js +++ b/browser/components/loop/content/shared/js/mixins.js @@ -287,16 +287,8 @@ loop.shared.mixins = (function() { * elements and handling updates of the media containers. */ var MediaSetupMixin = { - componentDidMount: function() { this.resetDimensionsCache(); - rootObject.addEventListener("orientationchange", this.updateVideoContainer); - rootObject.addEventListener("resize", this.updateVideoContainer); - }, - - componentWillUnmount: function() { - rootObject.removeEventListener("orientationchange", this.updateVideoContainer); - rootObject.removeEventListener("resize", this.updateVideoContainer); }, /** diff --git a/browser/components/loop/content/shared/js/otSdkDriver.js b/browser/components/loop/content/shared/js/otSdkDriver.js index 8f6cd6d0a50e..4d92c7e1102e 100644 --- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -274,6 +274,10 @@ loop.OTSdkDriver = (function() { disconnectSession: function() { this.endScreenShare(); + this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({ + available: false + })); + if (this.session) { this.session.off("sessionDisconnected streamCreated streamDestroyed connectionCreated connectionDestroyed streamPropertyChanged"); this.session.disconnect(); @@ -295,6 +299,8 @@ loop.OTSdkDriver = (function() { delete this._publishedLocalStream; delete this._subscribedRemoteStream; delete this._mockPublisherEl; + delete this._publisherChannel; + delete this._subscriberChannel; this.connections = {}; this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_UNINITIALIZED); }, @@ -723,7 +729,9 @@ loop.OTSdkDriver = (function() { */ _checkDataChannelsAvailable: function() { if (this._publisherChannel && this._subscriberChannel) { - this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable()); + this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({ + available: true + })); } }, @@ -821,6 +829,10 @@ loop.OTSdkDriver = (function() { this._notifyMetricsEvent("Session.streamDestroyed"); if (event.stream.videoType !== "screen") { + this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({ + available: false + })); + delete this._subscriberChannel; delete this._mockSubscribeEl; return; } @@ -839,6 +851,10 @@ loop.OTSdkDriver = (function() { */ _onLocalStreamDestroyed: function() { this._notifyMetricsEvent("Publisher.streamDestroyed"); + this.dispatcher.dispatch(new sharedActions.DataChannelsAvailable({ + available: false + })); + delete this._publisherChannel; delete this._mockPublisherEl; }, diff --git a/browser/components/loop/content/shared/js/textChatStore.js b/browser/components/loop/content/shared/js/textChatStore.js index 7a55852b591a..d522ef9ca3ec 100644 --- a/browser/components/loop/content/shared/js/textChatStore.js +++ b/browser/components/loop/content/shared/js/textChatStore.js @@ -5,7 +5,7 @@ var loop = loop || {}; loop.store = loop.store || {}; -loop.store.TextChatStore = (function(mozL10n) { +loop.store.TextChatStore = (function() { "use strict"; var sharedActions = loop.shared.actions; @@ -69,10 +69,15 @@ loop.store.TextChatStore = (function(mozL10n) { /** * Handles information for when data channels are available - enables * text chat. + * + * @param {sharedActions.DataChannelsAvailable} actionData */ - dataChannelsAvailable: function() { - this.setStoreState({ textChatEnabled: true }); - window.dispatchEvent(new CustomEvent("LoopChatEnabled")); + dataChannelsAvailable: function(actionData) { + this.setStoreState({ textChatEnabled: actionData.available }); + + if (actionData.available) { + window.dispatchEvent(new CustomEvent("LoopChatEnabled")); + } }, /** @@ -137,13 +142,15 @@ loop.store.TextChatStore = (function(mozL10n) { updateRoomInfo: function(actionData) { // XXX When we add special messages to desktop, we'll need to not post // multiple changes of room name, only the first. Bug 1171940 should fix this. - this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, { - contentType: CHAT_CONTENT_TYPES.ROOM_NAME, - message: mozL10n.get("rooms_welcome_title", {conversationName: actionData.roomName}) - }); + if (actionData.roomName) { + this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, { + contentType: CHAT_CONTENT_TYPES.ROOM_NAME, + message: actionData.roomName + }); + } // Append the context if we have any. - if ("urls" in actionData && actionData.urls.length) { + if (("urls" in actionData) && actionData.urls && actionData.urls.length) { // We only support the first url at the moment. var urlData = actionData.urls[0]; @@ -160,4 +167,4 @@ loop.store.TextChatStore = (function(mozL10n) { }); return TextChatStore; -})(navigator.mozL10n || window.mozL10n); +})(); diff --git a/browser/components/loop/content/shared/js/textChatView.js b/browser/components/loop/content/shared/js/textChatView.js index ee1dee4955d6..8c85f5a104ea 100644 --- a/browser/components/loop/content/shared/js/textChatView.js +++ b/browser/components/loop/content/shared/js/textChatView.js @@ -39,6 +39,22 @@ loop.shared.views.TextChatView = (function(mozL10n) { } }); + 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-entry 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 @@ -81,21 +97,28 @@ loop.shared.views.TextChatView = (function(mozL10n) { React.createElement("div", {className: "text-chat-scroller"}, this.props.messageList.map(function(entry, i) { - if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL && - entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) { - return ( - React.createElement("div", {className: "context-url-view-wrapper"}, - React.createElement(sharedViews.ContextUrlView, { - allowClick: true, - description: entry.message, - dispatcher: this.props.dispatcher, - key: i, - showContextTitle: true, - thumbnail: entry.extraData.thumbnail, - url: entry.extraData.location, - useDesktopPaths: false}) - ) - ); + if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) { + switch (entry.contentType) { + case CHAT_CONTENT_TYPES.ROOM_NAME: + return React.createElement(TextChatRoomName, {message: entry.message}); + case CHAT_CONTENT_TYPES.CONTEXT: + return ( + React.createElement("div", {className: "context-url-view-wrapper"}, + React.createElement(sharedViews.ContextUrlView, { + allowClick: true, + description: entry.message, + dispatcher: this.props.dispatcher, + key: i, + showContextTitle: true, + thumbnail: entry.extraData.thumbnail, + url: entry.extraData.location, + useDesktopPaths: false}) + ) + ); + default: + console.error("Unsupported contentType", entry.contentType); + return null; + } } return ( @@ -158,6 +181,11 @@ loop.shared.views.TextChatView = (function(mozL10n) { 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 diff --git a/browser/components/loop/content/shared/js/textChatView.jsx b/browser/components/loop/content/shared/js/textChatView.jsx index 784181440995..7d7ac69afcfc 100644 --- a/browser/components/loop/content/shared/js/textChatView.jsx +++ b/browser/components/loop/content/shared/js/textChatView.jsx @@ -39,6 +39,22 @@ loop.shared.views.TextChatView = (function(mozL10n) { } }); + var TextChatRoomName = React.createClass({ + mixins: [React.addons.PureRenderMixin], + + propTypes: { + message: React.PropTypes.string.isRequired + }, + + render: function() { + return ( +
+

{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 @@ -81,21 +97,28 @@ loop.shared.views.TextChatView = (function(mozL10n) {
{ this.props.messageList.map(function(entry, i) { - if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL && - entry.contentType === CHAT_CONTENT_TYPES.CONTEXT) { - return ( -
- -
- ); + if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) { + switch (entry.contentType) { + case CHAT_CONTENT_TYPES.ROOM_NAME: + return ; + case CHAT_CONTENT_TYPES.CONTEXT: + return ( +
+ +
+ ); + default: + console.error("Unsupported contentType", entry.contentType); + return null; + } } return ( @@ -158,6 +181,11 @@ loop.shared.views.TextChatView = (function(mozL10n) { 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 diff --git a/browser/components/loop/content/shared/js/views.js b/browser/components/loop/content/shared/js/views.js index 72cb62a64280..c72f3c996691 100644 --- a/browser/components/loop/content/shared/js/views.js +++ b/browser/components/loop/content/shared/js/views.js @@ -633,6 +633,15 @@ loop.shared.views = (function(_, l10n) { }; }, + componentWillReceiveProps: function(nextProps) { + // Only change the state if the prop has changed, and if it is also + // different from the state. + if (this.props.checked !== nextProps.checked && + this.state.checked !== nextProps.checked) { + this.setState({ checked: nextProps.checked }); + } + }, + getInitialState: function() { return { checked: this.props.checked, diff --git a/browser/components/loop/content/shared/js/views.jsx b/browser/components/loop/content/shared/js/views.jsx index 8ee9c337dda6..7887ad81597e 100644 --- a/browser/components/loop/content/shared/js/views.jsx +++ b/browser/components/loop/content/shared/js/views.jsx @@ -633,6 +633,15 @@ loop.shared.views = (function(_, l10n) { }; }, + componentWillReceiveProps: function(nextProps) { + // Only change the state if the prop has changed, and if it is also + // different from the state. + if (this.props.checked !== nextProps.checked && + this.state.checked !== nextProps.checked) { + this.setState({ checked: nextProps.checked }); + } + }, + getInitialState: function() { return { checked: this.props.checked, diff --git a/browser/components/loop/standalone/content/css/webapp.css b/browser/components/loop/standalone/content/css/webapp.css index 9557960a6906..b44dd8214d89 100644 --- a/browser/components/loop/standalone/content/css/webapp.css +++ b/browser/components/loop/standalone/content/css/webapp.css @@ -129,7 +129,7 @@ body, .rooms-footer { background: #000; - margin: 0 20px; + margin: 0 10px; text-align: left; height: 3em; position: relative; @@ -354,34 +354,16 @@ p.standalone-btn-label { right: 35%; } +html[dir="rtl"] .standalone .room-conversation-wrapper .ended-conversation .feedback { + right: auto; + left: 35%; +} + .standalone .ended-conversation .local-stream { /* Hide local media stream when feedback form is shown. */ display: none; } -/** - * The .text-chat-* styles are very temporarily whilst we work on text chat - * (bug 1108892 and dependencies). - */ -.text-chat-view { - height: 60px; - color: black; -} - -.text-chat-entries { - /* XXX Should use flex, this is just for the initial implementation. */ - height: calc(100% - 2em); -} - -.text-chat-box { - width: 30%; - margin: auto; -} - -.text-chat-box > form > input { - width: 100%; -} - @media screen and (max-width:640px) { .standalone .ended-conversation .feedback { width: 92%; diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js index 2474765b927c..12f469600c82 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -229,106 +229,12 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); - var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem", - propTypes: { - dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, - receivingScreenShare: React.PropTypes.bool, - roomContextUrl: React.PropTypes.object - }, - - recordClick: function() { - this.props.dispatcher.dispatch(new sharedActions.RecordClick({ - linkInfo: "Shared URL" - })); - }, - - render: function() { - if (!this.props.roomContextUrl || - !this.props.roomContextUrl.location) { - return null; - } - - var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location); - if (!locationInfo) { - return null; - } - - var cx = React.addons.classSet; - - var classes = cx({ - "standalone-context-url": true, - "screen-share-active": this.props.receivingScreenShare - }); - - return ( - React.createElement("div", {className: classes}, - React.createElement("img", {src: this.props.roomContextUrl.thumbnail || "shared/img/icons-16x16.svg#globe"}), - React.createElement("div", {className: "standalone-context-url-description-wrapper"}, - this.props.roomContextUrl.description, - React.createElement("br", null), React.createElement("a", {href: locationInfo.location, - onClick: this.recordClick, - target: "_blank", - title: locationInfo.location}, locationInfo.hostname) - ) - ) - ); - } - }); - - var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView", - propTypes: { - dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, - receivingScreenShare: React.PropTypes.bool.isRequired, - roomContextUrls: React.PropTypes.array, - roomName: React.PropTypes.string, - roomInfoFailure: React.PropTypes.string - }, - - getInitialState: function() { - return { - failureLogged: false - }; - }, - - _logFailure: function(message) { - if (!this.state.failureLogged) { - console.error(mozL10n.get(message)); - this.state.failureLogged = true; - } - }, - - render: function() { - // For failures, we currently just log the messages - UX doesn't want them - // displayed on primary UI at the moment. - if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) { - this._logFailure("room_information_failure_unsupported_browser"); - return null; - } else if (this.props.roomInfoFailure) { - this._logFailure("room_information_failure_not_available"); - return null; - } - - // We only support one item in the context Urls array for now. - var roomContextUrl = (this.props.roomContextUrls && - this.props.roomContextUrls.length > 0) ? - this.props.roomContextUrls[0] : null; - return ( - React.createElement("div", {className: "standalone-room-info"}, - React.createElement("h2", {className: "room-name"}, this.props.roomName), - React.createElement(StandaloneRoomContextItem, { - dispatcher: this.props.dispatcher, - receivingScreenShare: this.props.receivingScreenShare, - roomContextUrl: roomContextUrl}) - ) - ); - } - }); - var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView", mixins: [ Backbone.Events, sharedMixins.MediaSetupMixin, - sharedMixins.RoomsAudioMixin + sharedMixins.RoomsAudioMixin, + loop.store.StoreMixin("activeRoomStore") ], propTypes: { @@ -352,32 +258,11 @@ loop.standaloneRoomViews = (function(mozL10n) { }); }, - componentWillMount: function() { - this.listenTo(this.props.activeRoomStore, "change", - this._onActiveRoomStateChanged); - }, - - /** - * Handles a "change" event on the roomStore, and updates this.state - * to match the store. - * - * @private - */ - _onActiveRoomStateChanged: function() { - var state = this.props.activeRoomStore.getStoreState(); - this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions); - this.setState(state); - }, - componentDidMount: function() { // Adding a class to the document body element from here to ease styling it. document.body.classList.add("is-standalone-room"); }, - componentWillUnmount: function() { - this.stopListening(this.props.activeRoomStore); - }, - /** * Watches for when we transition to MEDIA_WAIT room state, so we can request * user media access. @@ -393,30 +278,16 @@ loop.standaloneRoomViews = (function(mozL10n) { })); } - if (this.state.roomState !== ROOM_STATES.JOINED && - nextState.roomState === ROOM_STATES.JOINED) { - // This forces the video size to update - creating the publisher - // first, and then connecting to the session doesn't seem to set the - // initial size correctly. - this.updateVideoContainer(); - } - - if (nextState.roomState === ROOM_STATES.INIT || - nextState.roomState === ROOM_STATES.GATHER || - nextState.roomState === ROOM_STATES.READY) { - this.resetDimensionsCache(); - } - - // When screen sharing stops. - if (this.state.receivingScreenShare && !nextState.receivingScreenShare) { - // Remove the custom screenshare styles on the remote camera. - var node = this._getElement(".remote"); - node.removeAttribute("style"); - } - - if (this.state.receivingScreenShare != nextState.receivingScreenShare || - this.state.remoteVideoEnabled != nextState.remoteVideoEnabled) { - this.updateVideoContainer(); + // UX don't want to surface these errors (as they would imply the user + // needs to do something to fix them, when if they're having a conversation + // they just need to connect). However, we do want there to be somewhere to + // find reasonably easily, in case there's issues raised. + if (!this.state.roomInfoFailure && nextState.roomInfoFailure) { + if (nextState.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) { + console.error(mozL10n.get("room_information_failure_unsupported_browser")); + } else { + console.error(mozL10n.get("room_information_failure_not_available")); + } } }, @@ -428,32 +299,6 @@ loop.standaloneRoomViews = (function(mozL10n) { this.props.dispatcher.dispatch(new sharedActions.LeaveRoom()); }, - /** - * Wrapper for window.matchMedia so that we use an appropriate version - * for the ui-showcase, which puts views inside of their own iframes. - * - * Currently, we use an icky hack, and the showcase conspires with - * react-frame-component to set iframe.contentWindow.matchMedia onto - * activeRoomStore. Once React context matures a bit (somewhere between - * 0.14 and 1.0, apparently): - * - * https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based - * - * we should be able to use those to clean this up. - * - * @param queryString - * @returns {MediaQueryList|null} - * @private - */ - _matchMedia: function(queryString) { - if ("matchMedia" in this.state) { - return this.state.matchMedia(queryString); - } else if ("matchMedia" in window) { - return window.matchMedia(queryString); - } - return null; - }, - /** * Toggles streaming status for a given stream type. * @@ -467,131 +312,6 @@ loop.standaloneRoomViews = (function(mozL10n) { })); }, - /** - * Specifically updates the local camera stream size and position, depending - * on the size and position of the remote video stream. - * This method gets called from `updateVideoContainer`, which is defined in - * the `MediaSetupMixin`. - * - * @param {Object} ratio Aspect ratio of the local camera stream - */ - updateLocalCameraPosition: function(ratio) { - // The local stream is a quarter of the remote stream. - var LOCAL_STREAM_SIZE = 0.25; - // The local stream overlaps the remote stream by a quarter of the local stream. - var LOCAL_STREAM_OVERLAP = 0.25; - // The minimum size of video height/width allowed by the sdk css. - var SDK_MIN_SIZE = 48; - - var node = this._getElement(".local"); - var targetWidth; - - node.style.right = "auto"; - if (this._matchMedia("screen and (max-width:640px)").matches) { - // For reduced screen widths, we just go for a fixed size and no overlap. - targetWidth = 180; - node.style.width = (targetWidth * ratio.width) + "px"; - node.style.height = (targetWidth * ratio.height) + "px"; - node.style.left = "auto"; - } else { - // The local camera view should be a quarter of the size of the remote stream - // and positioned to overlap with the remote stream at a quarter of its width. - - // Now position the local camera view correctly with respect to the remote - // video stream or the screen share stream. - var remoteVideoDimensions; - var isScreenShare = this.state.receivingScreenShare; - var videoDisplayed = isScreenShare ? - this.state.screenShareVideoObject || this.props.screenSharePosterUrl : - this.state.remoteSrcVideoObject || this.props.remotePosterUrl; - - if ((isScreenShare || this.shouldRenderRemoteVideo()) && videoDisplayed) { - remoteVideoDimensions = this.getRemoteVideoDimensions( - isScreenShare ? "screen" : "camera"); - } else { - var remoteElement = this.getDOMNode().querySelector(".remote.focus-stream"); - if (!remoteElement) { - return; - } - remoteVideoDimensions = { - streamWidth: remoteElement.offsetWidth, - offsetX: remoteElement.offsetLeft - }; - } - - targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE; - - var realWidth = targetWidth * ratio.width; - var realHeight = targetWidth * ratio.height; - - // If we've hit the min size limits, then limit at the minimum. - if (realWidth < SDK_MIN_SIZE) { - realWidth = SDK_MIN_SIZE; - realHeight = realWidth / ratio.width * ratio.height; - } - if (realHeight < SDK_MIN_SIZE) { - realHeight = SDK_MIN_SIZE; - realWidth = realHeight / ratio.height * ratio.width; - } - - var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX); - // The horizontal offset of the stream, and the width of the resulting - // pillarbox, is determined by the height exponent of the aspect ratio. - // Therefore we multiply the width of the local camera view by the height - // ratio. - node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px"; - node.style.width = realWidth + "px"; - node.style.height = realHeight + "px"; - } - }, - - /** - * Specifically updates the remote camera stream size and position, if - * a screen share is being received. It is slaved from the position of the - * local stream. - * This method gets called from `updateVideoContainer`, which is defined in - * the `MediaSetupMixin`. - * - * @param {Object} ratio Aspect ratio of the remote camera stream - */ - updateRemoteCameraPosition: function(ratio) { - // Nothing to do for screenshare - if (!this.state.receivingScreenShare) { - return; - } - // XXX For the time being, if we're a narrow screen, aka mobile, we don't display - // the remote media (bug 1133534). - if (this._matchMedia("screen and (max-width:640px)").matches) { - return; - } - - // 10px separation between the two streams. - var LOCAL_REMOTE_SEPARATION = 10; - - var node = this._getElement(".remote"); - var localNode = this._getElement(".local"); - - // Match the width to the local video. - node.style.width = localNode.offsetWidth + "px"; - - // The height is then determined from the aspect ratio - var height = ((localNode.offsetWidth / ratio.width) * ratio.height); - node.style.height = height + "px"; - - node.style.right = "auto"; - node.style.bottom = "auto"; - - // Now position the local camera view correctly with respect to the remote - // video stream. - - // The top is measured from the top of the element down the screen, - // so subtract the height of the video and the separation distance. - node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px"; - - // Match the left-hand sides. - node.style.left = localNode.offsetLeft + "px"; - }, - /** * Checks if current room is active. * @@ -647,34 +367,29 @@ loop.standaloneRoomViews = (function(mozL10n) { }, render: function() { - var localStreamClasses = React.addons.classSet({ - local: true, - "local-stream": true, - "local-stream-audio": this.state.videoMuted - }); + var displayScreenShare = this.state.receivingScreenShare || + this.props.screenSharePosterUrl; var remoteStreamClasses = React.addons.classSet({ - "video_inner": true, "remote": true, - "focus-stream": !this.state.receivingScreenShare, - "remote-inset-stream": this.state.receivingScreenShare + "focus-stream": !displayScreenShare }); var screenShareStreamClasses = React.addons.classSet({ "screen": true, - "focus-stream": this.state.receivingScreenShare, - hide: !this.state.receivingScreenShare + "focus-stream": displayScreenShare + }); + + var mediaWrapperClasses = React.addons.classSet({ + "media-wrapper": true, + "receiving-screen-share": displayScreenShare, + "showing-local-streams": this.state.localSrcVideoObject || + this.props.localPosterUrl }); - // XXX Temporarily showAlways = showRoomName = false for TextChatView - // until bug 1168829 is completed. return ( React.createElement("div", {className: "room-conversation-wrapper"}, React.createElement("div", {className: "beta-logo"}), - React.createElement(sharedViews.TextChatView, { - dispatcher: this.props.dispatcher, - showAlways: false, - showRoomName: false}), React.createElement(StandaloneRoomHeader, {dispatcher: this.props.dispatcher}), React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, failureReason: this.state.failureReason, @@ -682,50 +397,44 @@ loop.standaloneRoomViews = (function(mozL10n) { isFirefox: this.props.isFirefox, activeRoomStore: this.props.activeRoomStore, roomUsed: this.state.used}), - React.createElement("div", {className: "video-layout-wrapper"}, - React.createElement("div", {className: "conversation room-conversation"}, - React.createElement(StandaloneRoomContextView, { - dispatcher: this.props.dispatcher, - receivingScreenShare: this.state.receivingScreenShare, - roomContextUrls: this.state.roomContextUrls, - roomName: this.state.roomName, - roomInfoFailure: this.state.roomInfoFailure}), - React.createElement("div", {className: "media nested"}, - React.createElement("span", {className: "self-view-hidden-message"}, - mozL10n.get("self_view_hidden_message") - ), - React.createElement("div", {className: "video_wrapper remote_wrapper"}, - React.createElement("div", {className: remoteStreamClasses}, - React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), - posterUrl: this.props.remotePosterUrl, - mediaType: "remote", - srcVideoObject: this.state.remoteSrcVideoObject}) - ), - React.createElement("div", {className: screenShareStreamClasses}, - React.createElement(sharedViews.MediaView, {displayAvatar: false, - posterUrl: this.props.screenSharePosterUrl, - mediaType: "screen-share", - srcVideoObject: this.state.screenShareVideoObject}) - ) - ), - React.createElement("div", {className: localStreamClasses}, - React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, - posterUrl: this.props.localPosterUrl, - mediaType: "local", - srcVideoObject: this.state.localSrcVideoObject}) - ) + React.createElement("div", {className: "media-layout"}, + React.createElement("div", {className: mediaWrapperClasses}, + React.createElement("span", {className: "self-view-hidden-message"}, + mozL10n.get("self_view_hidden_message") ), - React.createElement(sharedViews.ConversationToolbar, { + React.createElement("div", {className: remoteStreamClasses}, + React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(), + posterUrl: this.props.remotePosterUrl, + mediaType: "remote", + srcVideoObject: this.state.remoteSrcVideoObject}) + ), + React.createElement("div", {className: screenShareStreamClasses}, + React.createElement(sharedViews.MediaView, {displayAvatar: false, + posterUrl: this.props.screenSharePosterUrl, + mediaType: "screen-share", + srcVideoObject: this.state.screenShareVideoObject}) + ), + React.createElement(sharedViews.TextChatView, { dispatcher: this.props.dispatcher, - video: {enabled: !this.state.videoMuted, - visible: this._roomIsActive()}, - audio: {enabled: !this.state.audioMuted, - visible: this._roomIsActive()}, - publishStream: this.publishStream, - hangup: this.leaveRoom, - hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), - enableHangup: this._roomIsActive()}) - ) + showAlways: true, + showRoomName: true}), + React.createElement("div", {className: "local"}, + React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, + posterUrl: this.props.localPosterUrl, + mediaType: "local", + srcVideoObject: this.state.localSrcVideoObject}) + ) + ), + React.createElement(sharedViews.ConversationToolbar, { + dispatcher: this.props.dispatcher, + video: {enabled: !this.state.videoMuted, + visible: this._roomIsActive()}, + audio: {enabled: !this.state.audioMuted, + visible: this._roomIsActive()}, + publishStream: this.publishStream, + hangup: this.leaveRoom, + hangupButtonLabel: mozL10n.get("rooms_leave_button_label"), + enableHangup: this._roomIsActive()}) ), React.createElement(loop.fxOSMarketplaceViews.FxOSHiddenMarketplaceView, { marketplaceSrc: this.state.marketplaceSrc, @@ -737,7 +446,6 @@ loop.standaloneRoomViews = (function(mozL10n) { }); return { - StandaloneRoomContextView: StandaloneRoomContextView, StandaloneRoomFooter: StandaloneRoomFooter, StandaloneRoomHeader: StandaloneRoomHeader, StandaloneRoomView: StandaloneRoomView diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx index 7e6b0a03fe7d..e297c67c9610 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -229,106 +229,12 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); - var StandaloneRoomContextItem = React.createClass({ - propTypes: { - dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, - receivingScreenShare: React.PropTypes.bool, - roomContextUrl: React.PropTypes.object - }, - - recordClick: function() { - this.props.dispatcher.dispatch(new sharedActions.RecordClick({ - linkInfo: "Shared URL" - })); - }, - - render: function() { - if (!this.props.roomContextUrl || - !this.props.roomContextUrl.location) { - return null; - } - - var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location); - if (!locationInfo) { - return null; - } - - var cx = React.addons.classSet; - - var classes = cx({ - "standalone-context-url": true, - "screen-share-active": this.props.receivingScreenShare - }); - - return ( -
- -
- {this.props.roomContextUrl.description} -
{locationInfo.hostname} -
-
- ); - } - }); - - var StandaloneRoomContextView = React.createClass({ - propTypes: { - dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, - receivingScreenShare: React.PropTypes.bool.isRequired, - roomContextUrls: React.PropTypes.array, - roomName: React.PropTypes.string, - roomInfoFailure: React.PropTypes.string - }, - - getInitialState: function() { - return { - failureLogged: false - }; - }, - - _logFailure: function(message) { - if (!this.state.failureLogged) { - console.error(mozL10n.get(message)); - this.state.failureLogged = true; - } - }, - - render: function() { - // For failures, we currently just log the messages - UX doesn't want them - // displayed on primary UI at the moment. - if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) { - this._logFailure("room_information_failure_unsupported_browser"); - return null; - } else if (this.props.roomInfoFailure) { - this._logFailure("room_information_failure_not_available"); - return null; - } - - // We only support one item in the context Urls array for now. - var roomContextUrl = (this.props.roomContextUrls && - this.props.roomContextUrls.length > 0) ? - this.props.roomContextUrls[0] : null; - return ( -
-

{this.props.roomName}

- -
- ); - } - }); - var StandaloneRoomView = React.createClass({ mixins: [ Backbone.Events, sharedMixins.MediaSetupMixin, - sharedMixins.RoomsAudioMixin + sharedMixins.RoomsAudioMixin, + loop.store.StoreMixin("activeRoomStore") ], propTypes: { @@ -352,32 +258,11 @@ loop.standaloneRoomViews = (function(mozL10n) { }); }, - componentWillMount: function() { - this.listenTo(this.props.activeRoomStore, "change", - this._onActiveRoomStateChanged); - }, - - /** - * Handles a "change" event on the roomStore, and updates this.state - * to match the store. - * - * @private - */ - _onActiveRoomStateChanged: function() { - var state = this.props.activeRoomStore.getStoreState(); - this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions); - this.setState(state); - }, - componentDidMount: function() { // Adding a class to the document body element from here to ease styling it. document.body.classList.add("is-standalone-room"); }, - componentWillUnmount: function() { - this.stopListening(this.props.activeRoomStore); - }, - /** * Watches for when we transition to MEDIA_WAIT room state, so we can request * user media access. @@ -393,30 +278,16 @@ loop.standaloneRoomViews = (function(mozL10n) { })); } - if (this.state.roomState !== ROOM_STATES.JOINED && - nextState.roomState === ROOM_STATES.JOINED) { - // This forces the video size to update - creating the publisher - // first, and then connecting to the session doesn't seem to set the - // initial size correctly. - this.updateVideoContainer(); - } - - if (nextState.roomState === ROOM_STATES.INIT || - nextState.roomState === ROOM_STATES.GATHER || - nextState.roomState === ROOM_STATES.READY) { - this.resetDimensionsCache(); - } - - // When screen sharing stops. - if (this.state.receivingScreenShare && !nextState.receivingScreenShare) { - // Remove the custom screenshare styles on the remote camera. - var node = this._getElement(".remote"); - node.removeAttribute("style"); - } - - if (this.state.receivingScreenShare != nextState.receivingScreenShare || - this.state.remoteVideoEnabled != nextState.remoteVideoEnabled) { - this.updateVideoContainer(); + // UX don't want to surface these errors (as they would imply the user + // needs to do something to fix them, when if they're having a conversation + // they just need to connect). However, we do want there to be somewhere to + // find reasonably easily, in case there's issues raised. + if (!this.state.roomInfoFailure && nextState.roomInfoFailure) { + if (nextState.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) { + console.error(mozL10n.get("room_information_failure_unsupported_browser")); + } else { + console.error(mozL10n.get("room_information_failure_not_available")); + } } }, @@ -428,32 +299,6 @@ loop.standaloneRoomViews = (function(mozL10n) { this.props.dispatcher.dispatch(new sharedActions.LeaveRoom()); }, - /** - * Wrapper for window.matchMedia so that we use an appropriate version - * for the ui-showcase, which puts views inside of their own iframes. - * - * Currently, we use an icky hack, and the showcase conspires with - * react-frame-component to set iframe.contentWindow.matchMedia onto - * activeRoomStore. Once React context matures a bit (somewhere between - * 0.14 and 1.0, apparently): - * - * https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based - * - * we should be able to use those to clean this up. - * - * @param queryString - * @returns {MediaQueryList|null} - * @private - */ - _matchMedia: function(queryString) { - if ("matchMedia" in this.state) { - return this.state.matchMedia(queryString); - } else if ("matchMedia" in window) { - return window.matchMedia(queryString); - } - return null; - }, - /** * Toggles streaming status for a given stream type. * @@ -467,131 +312,6 @@ loop.standaloneRoomViews = (function(mozL10n) { })); }, - /** - * Specifically updates the local camera stream size and position, depending - * on the size and position of the remote video stream. - * This method gets called from `updateVideoContainer`, which is defined in - * the `MediaSetupMixin`. - * - * @param {Object} ratio Aspect ratio of the local camera stream - */ - updateLocalCameraPosition: function(ratio) { - // The local stream is a quarter of the remote stream. - var LOCAL_STREAM_SIZE = 0.25; - // The local stream overlaps the remote stream by a quarter of the local stream. - var LOCAL_STREAM_OVERLAP = 0.25; - // The minimum size of video height/width allowed by the sdk css. - var SDK_MIN_SIZE = 48; - - var node = this._getElement(".local"); - var targetWidth; - - node.style.right = "auto"; - if (this._matchMedia("screen and (max-width:640px)").matches) { - // For reduced screen widths, we just go for a fixed size and no overlap. - targetWidth = 180; - node.style.width = (targetWidth * ratio.width) + "px"; - node.style.height = (targetWidth * ratio.height) + "px"; - node.style.left = "auto"; - } else { - // The local camera view should be a quarter of the size of the remote stream - // and positioned to overlap with the remote stream at a quarter of its width. - - // Now position the local camera view correctly with respect to the remote - // video stream or the screen share stream. - var remoteVideoDimensions; - var isScreenShare = this.state.receivingScreenShare; - var videoDisplayed = isScreenShare ? - this.state.screenShareVideoObject || this.props.screenSharePosterUrl : - this.state.remoteSrcVideoObject || this.props.remotePosterUrl; - - if ((isScreenShare || this.shouldRenderRemoteVideo()) && videoDisplayed) { - remoteVideoDimensions = this.getRemoteVideoDimensions( - isScreenShare ? "screen" : "camera"); - } else { - var remoteElement = this.getDOMNode().querySelector(".remote.focus-stream"); - if (!remoteElement) { - return; - } - remoteVideoDimensions = { - streamWidth: remoteElement.offsetWidth, - offsetX: remoteElement.offsetLeft - }; - } - - targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE; - - var realWidth = targetWidth * ratio.width; - var realHeight = targetWidth * ratio.height; - - // If we've hit the min size limits, then limit at the minimum. - if (realWidth < SDK_MIN_SIZE) { - realWidth = SDK_MIN_SIZE; - realHeight = realWidth / ratio.width * ratio.height; - } - if (realHeight < SDK_MIN_SIZE) { - realHeight = SDK_MIN_SIZE; - realWidth = realHeight / ratio.height * ratio.width; - } - - var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX); - // The horizontal offset of the stream, and the width of the resulting - // pillarbox, is determined by the height exponent of the aspect ratio. - // Therefore we multiply the width of the local camera view by the height - // ratio. - node.style.left = (offsetX - (realWidth * LOCAL_STREAM_OVERLAP)) + "px"; - node.style.width = realWidth + "px"; - node.style.height = realHeight + "px"; - } - }, - - /** - * Specifically updates the remote camera stream size and position, if - * a screen share is being received. It is slaved from the position of the - * local stream. - * This method gets called from `updateVideoContainer`, which is defined in - * the `MediaSetupMixin`. - * - * @param {Object} ratio Aspect ratio of the remote camera stream - */ - updateRemoteCameraPosition: function(ratio) { - // Nothing to do for screenshare - if (!this.state.receivingScreenShare) { - return; - } - // XXX For the time being, if we're a narrow screen, aka mobile, we don't display - // the remote media (bug 1133534). - if (this._matchMedia("screen and (max-width:640px)").matches) { - return; - } - - // 10px separation between the two streams. - var LOCAL_REMOTE_SEPARATION = 10; - - var node = this._getElement(".remote"); - var localNode = this._getElement(".local"); - - // Match the width to the local video. - node.style.width = localNode.offsetWidth + "px"; - - // The height is then determined from the aspect ratio - var height = ((localNode.offsetWidth / ratio.width) * ratio.height); - node.style.height = height + "px"; - - node.style.right = "auto"; - node.style.bottom = "auto"; - - // Now position the local camera view correctly with respect to the remote - // video stream. - - // The top is measured from the top of the element down the screen, - // so subtract the height of the video and the separation distance. - node.style.top = (localNode.offsetTop - height - LOCAL_REMOTE_SEPARATION) + "px"; - - // Match the left-hand sides. - node.style.left = localNode.offsetLeft + "px"; - }, - /** * Checks if current room is active. * @@ -647,34 +367,29 @@ loop.standaloneRoomViews = (function(mozL10n) { }, render: function() { - var localStreamClasses = React.addons.classSet({ - local: true, - "local-stream": true, - "local-stream-audio": this.state.videoMuted - }); + var displayScreenShare = this.state.receivingScreenShare || + this.props.screenSharePosterUrl; var remoteStreamClasses = React.addons.classSet({ - "video_inner": true, "remote": true, - "focus-stream": !this.state.receivingScreenShare, - "remote-inset-stream": this.state.receivingScreenShare + "focus-stream": !displayScreenShare }); var screenShareStreamClasses = React.addons.classSet({ "screen": true, - "focus-stream": this.state.receivingScreenShare, - hide: !this.state.receivingScreenShare + "focus-stream": displayScreenShare + }); + + var mediaWrapperClasses = React.addons.classSet({ + "media-wrapper": true, + "receiving-screen-share": displayScreenShare, + "showing-local-streams": this.state.localSrcVideoObject || + this.props.localPosterUrl }); - // XXX Temporarily showAlways = showRoomName = false for TextChatView - // until bug 1168829 is completed. return (
- -
-
- -
- - {mozL10n.get("self_view_hidden_message")} - -
-
- -
-
- -
-
-
- -
+
+
+ + {mozL10n.get("self_view_hidden_message")} + +
+
- + +
+ + showAlways={true} + showRoomName={true} /> +
+ +
+
form > input"); + + TestUtils.Simulate.keyDown(entryNode, { + key: "Enter", + which: 13 + }); + + sinon.assert.notCalled(dispatcher.dispatch); + }); }); }); diff --git a/browser/components/loop/test/shared/views_test.js b/browser/components/loop/test/shared/views_test.js index 3e26c904048d..166475cdaffe 100644 --- a/browser/components/loop/test/shared/views_test.js +++ b/browser/components/loop/test/shared/views_test.js @@ -765,6 +765,26 @@ describe("loop.shared.views", function() { expect(node.classList.contains("disabled")).to.eql(true); expect(node.hasAttribute("disabled")).to.eql(true); }); + + it("should render the checkbox as checked when the prop is set", function() { + view = mountTestComponent({ + checked: true + }); + + var checkbox = view.getDOMNode().querySelector(".checkbox"); + expect(checkbox.classList.contains("checked")).eql(true); + }); + + it("should alter the render state when the props are changed", function() { + view = mountTestComponent({ + checked: true + }); + + view.setProps({checked: false}); + + var checkbox = view.getDOMNode().querySelector(".checkbox"); + expect(checkbox.classList.contains("checked")).eql(false); + }); }); describe("#_handleClick", function() { diff --git a/browser/components/loop/test/standalone/standaloneRoomViews_test.js b/browser/components/loop/test/standalone/standaloneRoomViews_test.js index d55a1b864ef1..13bc3ba5b320 100644 --- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -31,6 +31,7 @@ describe("loop.standaloneRoomViews", function() { feedbackClient: {} }); loop.store.StoreMixin.register({ + activeRoomStore: activeRoomStore, feedbackStore: feedbackStore, textChatStore: textChatStore }); @@ -45,126 +46,6 @@ describe("loop.standaloneRoomViews", function() { sandbox.restore(); }); - describe("StandaloneRoomContextView", function() { - beforeEach(function() { - sandbox.stub(navigator.mozL10n, "get").returnsArg(0); - }); - - function mountTestComponent(extraProps) { - var props = _.extend({ - dispatcher: dispatcher, - receivingScreenShare: false - }, extraProps); - return TestUtils.renderIntoDocument( - React.createElement( - loop.standaloneRoomViews.StandaloneRoomContextView, props)); - } - - it("should display the room name if no failures are known", function() { - var view = mountTestComponent({ - roomName: "Mike's room", - receivingScreenShare: false - }); - - expect(view.getDOMNode().textContent).eql("Mike's room"); - }); - - it("should log an unsupported browser message if crypto is unsupported", function() { - var view = mountTestComponent({ - roomName: "Mark's room", - roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED - }); - - sinon.assert.called(console.error); - sinon.assert.calledWithMatch(console.error, sinon.match("unsupported")); - }); - - it("should display a general error message for any other failure", function() { - var view = mountTestComponent({ - roomName: "Mark's room", - roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA - }); - - sinon.assert.called(console.error); - sinon.assert.calledWithMatch(console.error, sinon.match("not_available")); - }); - - it("should display context information if a url is supplied", function() { - var view = mountTestComponent({ - roomName: "Mike's room", - roomContextUrls: [{ - description: "Mark's super page", - location: "http://invalid.com", - thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" - }] - }); - - expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null); - }); - - it("should format the url for display", function() { - sandbox.stub(sharedUtils, "formatURL").returns({ - location: "location", - hostname: "hostname" - }); - - var view = mountTestComponent({ - roomName: "Mike's room", - roomContextUrls: [{ - description: "Mark's super page", - location: "http://invalid.com", - thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" - }] - }); - - expect(view.getDOMNode() - .querySelector(".standalone-context-url-description-wrapper > a").textContent) - .eql("hostname"); - }); - - it("should not display context information if no urls are supplied", function() { - var view = mountTestComponent({ - roomName: "Mike's room" - }); - - expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null); - }); - - it("should dispatch a RecordClick action when the link is clicked", function() { - var view = mountTestComponent({ - roomName: "Mark's room", - roomContextUrls: [{ - description: "Mark's super page", - location: "http://invalid.com", - thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" - }] - }); - - TestUtils.Simulate.click(view.getDOMNode() - .querySelector(".standalone-context-url-description-wrapper > a")); - - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.RecordClick({ - linkInfo: "Shared URL" - })); - }); - - it("should display the default favicon when no thumbnail is available", function() { - var view = mountTestComponent({ - roomName: "Mike's room", - roomContextUrls: [{ - description: "Mark's super page", - location: "http://invalid.com", - thumbnail: "" - }] - }); - - expect(view.getDOMNode().querySelector(".standalone-context-url > img").src) - .to.match(/shared\/img\/icons-16x16.svg#globe$/); - }); - }); - describe("StandaloneRoomHeader", function() { function mountTestComponent() { return TestUtils.renderIntoDocument( @@ -224,43 +105,6 @@ describe("loop.standaloneRoomViews", function() { expectActionDispatched(view); }); - - it("should updateVideoContainer when the JOINED state is entered", function() { - activeRoomStore.setStoreState({roomState: ROOM_STATES.READY}); - - var view = mountTestComponent(); - - sandbox.stub(view, "updateVideoContainer"); - - activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED}); - - sinon.assert.calledOnce(view.updateVideoContainer); - }); - - it("should updateVideoContainer when the JOINED state is re-entered", function() { - activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED}); - - var view = mountTestComponent(); - - sandbox.stub(view, "updateVideoContainer"); - - activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED}); - - sinon.assert.calledOnce(view.updateVideoContainer); - }); - - it("should reset the video dimensions cache when the gather state is entered", function() { - activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED}); - - var view = mountTestComponent(); - - activeRoomStore.setStoreState({roomState: ROOM_STATES.GATHER}); - - expect(view._videoDimensionsCache).eql({ - local: {}, - remote: {} - }); - }); }); describe("#publishStream", function() { @@ -297,252 +141,6 @@ describe("loop.standaloneRoomViews", function() { }); }); - describe("Local Stream Size Position", function() { - var view, localElement; - - beforeEach(function() { - sandbox.stub(window, "matchMedia").returns({ - matches: false - }); - activeRoomStore.setStoreState({ - remoteSrcVideoObject: {}, - remoteVideoEnabled: true - }); - view = mountTestComponent(); - localElement = view._getElement(".local"); - }); - - it("should be a quarter of the width of the main stream", function() { - sandbox.stub(view, "getRemoteVideoDimensions").returns({ - streamWidth: 640, - offsetX: 0 - }); - - view.updateLocalCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(localElement.style.width).eql("160px"); - expect(localElement.style.height).eql("120px"); - }); - - it("should be a quarter of the width of the remote view element when there is no stream", function() { - activeRoomStore.setStoreState({ - remoteSrcVideoObject: null, - remoteVideoEnabled: false - }); - - sandbox.stub(view, "getDOMNode").returns({ - querySelector: function(selector) { - if (selector === ".local") { - return localElement; - } - - return { - offsetWidth: 640, - offsetLeft: 0 - }; - } - }); - - view.updateLocalCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(localElement.style.width).eql("160px"); - expect(localElement.style.height).eql("120px"); - }); - - it("should be a quarter of the width reduced for aspect ratio", function() { - sandbox.stub(view, "getRemoteVideoDimensions").returns({ - streamWidth: 640, - offsetX: 0 - }); - - view.updateLocalCameraPosition({ - width: 0.75, - height: 1 - }); - - expect(localElement.style.width).eql("120px"); - expect(localElement.style.height).eql("160px"); - }); - - it("should ensure the height is a minimum of 48px", function() { - sandbox.stub(view, "getRemoteVideoDimensions").returns({ - streamWidth: 180, - offsetX: 0 - }); - - view.updateLocalCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(localElement.style.width).eql("64px"); - expect(localElement.style.height).eql("48px"); - }); - - it("should ensure the width is a minimum of 48px", function() { - sandbox.stub(view, "getRemoteVideoDimensions").returns({ - streamWidth: 180, - offsetX: 0 - }); - - view.updateLocalCameraPosition({ - width: 0.75, - height: 1 - }); - - expect(localElement.style.width).eql("48px"); - expect(localElement.style.height).eql("64px"); - }); - - it("should position the stream to overlap the main stream by a quarter", function() { - sandbox.stub(view, "getRemoteVideoDimensions").returns({ - streamWidth: 640, - offsetX: 0 - }); - - view.updateLocalCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(localElement.style.width).eql("160px"); - expect(localElement.style.left).eql("600px"); - }); - - it("should position the stream to overlap the remote view element when there is no stream", function() { - activeRoomStore.setStoreState({ - remoteSrcVideoObject: null, - remoteVideoEnabled: false - }); - - sandbox.stub(view, "getDOMNode").returns({ - querySelector: function(selector) { - if (selector === ".local") { - return localElement; - } - - return { - offsetWidth: 640, - offsetLeft: 0 - }; - } - }); - - view.updateLocalCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(localElement.style.width).eql("160px"); - expect(localElement.style.left).eql("600px"); - }); - - it("should position the stream to overlap the main stream by a quarter when the aspect ratio is vertical", function() { - sandbox.stub(view, "getRemoteVideoDimensions").returns({ - streamWidth: 640, - offsetX: 0 - }); - - view.updateLocalCameraPosition({ - width: 0.75, - height: 1 - }); - - expect(localElement.style.width).eql("120px"); - expect(localElement.style.left).eql("610px"); - }); - }); - - describe("Remote Stream Size Position", function() { - var view, localElement, remoteElement; - - beforeEach(function() { - sandbox.stub(window, "matchMedia").returns({ - matches: false - }); - view = mountTestComponent(); - - localElement = { - style: {} - }; - remoteElement = { - style: {}, - removeAttribute: sinon.spy() - }; - - sandbox.stub(view, "_getElement", function(className) { - return className === ".local" ? localElement : remoteElement; - }); - - view.setState({"receivingScreenShare": true}); - }); - - it("should do nothing if not receiving screenshare", function() { - view.setState({"receivingScreenShare": false}); - remoteElement.style.width = "10px"; - - view.updateRemoteCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(remoteElement.style.width).eql("10px"); - }); - - it("should be the same width as the local video", function() { - localElement.offsetWidth = 100; - - view.updateRemoteCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(remoteElement.style.width).eql("100px"); - }); - - it("should be the same left edge as the local video", function() { - localElement.offsetLeft = 50; - - view.updateRemoteCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(remoteElement.style.left).eql("50px"); - }); - - it("should have a height determined by the aspect ratio", function() { - localElement.offsetWidth = 100; - - view.updateRemoteCameraPosition({ - width: 1, - height: 0.75 - }); - - expect(remoteElement.style.height).eql("75px"); - }); - - it("should have the top be set such that the bottom is 10px above the local video", function() { - localElement.offsetWidth = 100; - localElement.offsetTop = 200; - - view.updateRemoteCameraPosition({ - width: 1, - height: 0.75 - }); - - // 200 (top) - 75 (height) - 10 (spacing) = 115 - expect(remoteElement.style.top).eql("115px"); - }); - - }); - describe("#render", function() { var view; @@ -827,14 +425,14 @@ describe("loop.standaloneRoomViews", function() { }); describe("Mute", function() { - it("should render local media as audio-only if video is muted", + it("should render a local avatar if video is muted", function() { activeRoomStore.setStoreState({ roomState: ROOM_STATES.SESSION_CONNECTED, videoMuted: true }); - expect(view.getDOMNode().querySelector(".local-stream-audio")) + expect(view.getDOMNode().querySelector(".local .avatar")) .not.eql(null); }); diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index 68896a31fda5..c8faa8f35ba6 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -270,6 +270,7 @@ })); loop.store.StoreMixin.register({ + activeRoomStore: activeRoomStore, conversationStore: conversationStore, feedbackStore: feedbackStore, textChatStore: textChatStore @@ -971,6 +972,21 @@ localPosterUrl: "sample-img/video-screen-local.png", remotePosterUrl: "sample-img/video-screen-remote.png"}) ) + ), + + React.createElement(FramedExample, {width: 600, height: 480, + onContentsRendered: updatingSharingRoomStore.forcedUpdate, + summary: "Standalone room convo (has-participants, receivingScreenShare, 600x480)"}, + React.createElement("div", {className: "standalone", cssClass: "standalone"}, + React.createElement(StandaloneRoomView, { + dispatcher: dispatcher, + activeRoomStore: updatingSharingRoomStore, + roomState: ROOM_STATES.HAS_PARTICIPANTS, + isFirefox: true, + localPosterUrl: "sample-img/video-screen-local.png", + remotePosterUrl: "sample-img/video-screen-remote.png", + screenSharePosterUrl: "sample-img/video-screen-terminal.png"}) + ) ) ), diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index a5216b73e7d9..485db13dfede 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -270,6 +270,7 @@ })); loop.store.StoreMixin.register({ + activeRoomStore: activeRoomStore, conversationStore: conversationStore, feedbackStore: feedbackStore, textChatStore: textChatStore @@ -972,6 +973,21 @@ remotePosterUrl="sample-img/video-screen-remote.png" />
+ + +
+ +
+
diff --git a/browser/components/sessionstore/test/browser_595601-restore_hidden.js b/browser/components/sessionstore/test/browser_595601-restore_hidden.js index 9b155bd8a158..5eb669a25c1c 100644 --- a/browser/components/sessionstore/test/browser_595601-restore_hidden.js +++ b/browser/components/sessionstore/test/browser_595601-restore_hidden.js @@ -14,6 +14,7 @@ let state = {windows:[{tabs:[ function test() { waitForExplicitFinish(); + requestLongerTimeout(2); registerCleanupFunction(function () { Services.prefs.clearUserPref("browser.sessionstore.restore_hidden_tabs"); diff --git a/browser/devtools/performance/test/browser.ini b/browser/devtools/performance/test/browser.ini index fbb011b03857..47e46066953f 100644 --- a/browser/devtools/performance/test/browser.ini +++ b/browser/devtools/performance/test/browser.ini @@ -71,6 +71,7 @@ support-files = [browser_perf-loading-01.js] [browser_perf-loading-02.js] [browser_perf-marker-details-01.js] +skip-if = os == 'linux' # Bug 1172120 [browser_perf-options-01.js] [browser_perf-options-02.js] [browser_perf-options-03.js] diff --git a/browser/devtools/styleinspector/ruleview.css b/browser/devtools/styleinspector/ruleview.css index 1fcb602ea7af..9d0f40a56e78 100644 --- a/browser/devtools/styleinspector/ruleview.css +++ b/browser/devtools/styleinspector/ruleview.css @@ -31,10 +31,10 @@ body { #pseudo-class-panel { position: relative; - top: -1px; + margin-top: -1px; + margin-bottom: -1px; overflow-y: hidden; max-height: 24px; - justify-content: space-around; transition-property: max-height; transition-duration: 150ms; transition-timing-function: ease; @@ -46,6 +46,7 @@ body { #pseudo-class-panel > label { -moz-user-select: none; + flex-grow: 1; } .ruleview { diff --git a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js index f11e9828998b..83ba57445792 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js @@ -16,6 +16,7 @@ let PAGE_CONTENT = [ '
Styled Node
', 'This is a span', 'Multiple classes', + 'Multiple classes', '

Empty

', '

Invalid characters in class

', '

Invalid characters in id

' @@ -25,6 +26,7 @@ const TEST_DATA = [ { node: "#testid", expected: "#testid" }, { node: ".testclass2", expected: ".testclass2" }, { node: ".class1.class2", expected: ".class1.class2" }, + { node: ".class3.class4", expected: ".class3.class4" }, { node: "p", expected: "p" }, { node: "h1", expected: ".asd\\@\\@\\@\\@a\\!\\!\\!\\!\\:\\:\\:\\@asd" }, { node: "h2", expected: "#asd\\@\\@\\@a\\!\\!2a" } diff --git a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js index 1ef554956c85..e04ba41fc77d 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_edit-selector_02.js @@ -22,7 +22,12 @@ let PAGE_CONTENT = [ '
B
' ].join("\n"); +const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; + add_task(function*() { + // Expand the pseudo-elements section by default. + Services.prefs.setBoolPref(PSEUDO_PREF, true); + yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(PAGE_CONTENT)); let {inspector, view} = yield openRuleView(); @@ -41,6 +46,9 @@ add_task(function*() { info("Selecting the modified element"); yield selectNode(".testclass2", inspector); yield checkModifiedElement(view, ".testclass2::first-letter"); + + // Reset the pseudo-elements section pref to its default value. + Services.prefs.clearUserPref(PSEUDO_PREF); }); function* testEditSelector(view, name) { diff --git a/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js b/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js index 6050f76f10a3..0100388ed019 100644 --- a/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js +++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js @@ -7,10 +7,13 @@ // Test that pseudoelements are displayed correctly in the rule view const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html"; +const PSEUDO_PREF = "devtools.inspector.show_pseudo_elements"; add_task(function*() { + Services.prefs.setBoolPref(PSEUDO_PREF, true); + yield addTab(TEST_URI); - let {toolbox, inspector, view} = yield openRuleView(); + let {inspector, view} = yield openRuleView(); yield testTopLeft(inspector, view); yield testTopRight(inspector, view); @@ -18,15 +21,13 @@ add_task(function*() { yield testBottomLeft(inspector, view); yield testParagraph(inspector, view); yield testBody(inspector, view); + + Services.prefs.clearUserPref(PSEUDO_PREF); }); function* testTopLeft(inspector, view) { let selector = "#topleft"; - let { - rules, - element, - elementStyle - } = yield assertPseudoElementRulesNumbers(selector, inspector, view, { + let {rules} = yield assertPseudoElementRulesNumbers(selector, inspector, view, { elementRulesNb: 4, firstLineRulesNb: 2, firstLetterRulesNb: 1, @@ -35,83 +36,84 @@ function* testTopLeft(inspector, view) { let gutters = assertGutters(view); - // Make sure that clicking on the twisty hides pseudo elements + info("Make sure that clicking on the twisty hides pseudo elements"); let expander = gutters[0].querySelector(".ruleview-expander"); - ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded"); - expander.click(); - ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by twisty"); - expander.click(); - ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are expanded again"); + ok(view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements are expanded"); - // Make sure that dblclicking on the header container also toggles the pseudo elements - EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, inspector.sidebar.getWindowForTab("ruleview")); - ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking"); + expander.click(); + ok(!view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements are collapsed by twisty"); - let defaultView = element.ownerDocument.defaultView; + expander.click(); + ok(view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements are expanded again"); + + info("Make sure that dblclicking on the header container also toggles " + + "the pseudo elements"); + EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, + view.doc.defaultView); + ok(!view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements are collapsed by dblclicking"); let elementRule = rules.elementRules[0]; let elementRuleView = getRuleViewRuleEditor(view, 3); let elementFirstLineRule = rules.firstLineRules[0]; - let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => { + let elementFirstLineRuleView = [...view.element.children[1].children].filter(e => { return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; })[0]._ruleEditor; - is - ( - convertTextPropsToString(elementFirstLineRule.textProps), - "color: orange", - "TopLeft firstLine properties are correct" - ); + is(convertTextPropsToString(elementFirstLineRule.textProps), + "color: orange", + "TopLeft firstLine properties are correct"); let firstProp = elementFirstLineRuleView.addProperty("background-color", "rgb(0, 255, 0)", ""); let secondProp = elementFirstLineRuleView.addProperty("font-style", "italic", ""); - is (firstProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2], - "First added property is on back of array"); - is (secondProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1], - "Second added property is on back of array"); + is(firstProp, + elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2], + "First added property is on back of array"); + is(secondProp, + elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1], + "Second added property is on back of array"); yield elementFirstLineRule._applyingModifications; is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), - "rgb(0, 255, 0)", "Added property should have been used."); + "rgb(0, 255, 0)", "Added property should have been used."); is((yield getComputedStyleProperty(selector, ":first-line", "font-style")), - "italic", "Added property should have been used."); + "italic", "Added property should have been used."); is((yield getComputedStyleProperty(selector, null, "text-decoration")), - "none", "Added property should not apply to element"); + "none", "Added property should not apply to element"); firstProp.setEnabled(false); yield elementFirstLineRule._applyingModifications; is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), - "rgb(255, 0, 0)", "Disabled property should now have been used."); + "rgb(255, 0, 0)", "Disabled property should now have been used."); is((yield getComputedStyleProperty(selector, null, "background-color")), - "rgb(221, 221, 221)", "Added property should not apply to element"); + "rgb(221, 221, 221)", "Added property should not apply to element"); firstProp.setEnabled(true); yield elementFirstLineRule._applyingModifications; is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), - "rgb(0, 255, 0)", "Added property should have been used."); + "rgb(0, 255, 0)", "Added property should have been used."); is((yield getComputedStyleProperty(selector, null, "text-decoration")), - "none", "Added property should not apply to element"); + "none", "Added property should not apply to element"); firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", ""); yield elementRule._applyingModifications; is((yield getComputedStyleProperty(selector, null, "background-color")), - "rgb(0, 0, 255)", "Added property should have been used."); + "rgb(0, 0, 255)", "Added property should have been used."); is((yield getComputedStyleProperty(selector, ":first-line", "background-color")), - "rgb(0, 255, 0)", "Added prop does not apply to pseudo"); + "rgb(0, 255, 0)", "Added prop does not apply to pseudo"); } function* testTopRight(inspector, view) { - let { - rules, - element, - elementStyle - } = yield assertPseudoElementRulesNumbers("#topright", inspector, view, { + yield assertPseudoElementRulesNumbers("#topright", inspector, view, { elementRulesNb: 4, firstLineRulesNb: 1, firstLetterRulesNb: 1, @@ -121,10 +123,13 @@ function* testTopRight(inspector, view) { let gutters = assertGutters(view); let expander = gutters[0].querySelector(".ruleview-expander"); - ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements remain collapsed after switching element"); + ok(!view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements remain collapsed after switching element"); + expander.scrollIntoView(); expander.click(); - ok (view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are shown again after clicking twisty"); + ok(view.element.firstChild.classList.contains("show-expandable-container"), + "Pseudo Elements are shown again after clicking twisty"); } function* testBottomRight(inspector, view) { @@ -146,61 +151,36 @@ function* testBottomLeft(inspector, view) { } function* testParagraph(inspector, view) { - let { - rules, - element, - elementStyle - } = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, { + let {rules} = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, { elementRulesNb: 3, firstLineRulesNb: 1, firstLetterRulesNb: 1, selectionRulesNb: 1 }); - let gutters = assertGutters(view); + assertGutters(view); let elementFirstLineRule = rules.firstLineRules[0]; - let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => { - return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule; - })[0]._ruleEditor; - - is - ( - convertTextPropsToString(elementFirstLineRule.textProps), - "background: blue none repeat scroll 0% 0%", - "Paragraph first-line properties are correct" - ); + is(convertTextPropsToString(elementFirstLineRule.textProps), + "background: blue none repeat scroll 0% 0%", + "Paragraph first-line properties are correct"); let elementFirstLetterRule = rules.firstLetterRules[0]; - let elementFirstLetterRuleView = [].filter.call(view.element.children[1].children, (e) => { - return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule; - })[0]._ruleEditor; - - is - ( - convertTextPropsToString(elementFirstLetterRule.textProps), - "color: red; font-size: 130%", - "Paragraph first-letter properties are correct" - ); + is(convertTextPropsToString(elementFirstLetterRule.textProps), + "color: red; font-size: 130%", + "Paragraph first-letter properties are correct"); let elementSelectionRule = rules.selectionRules[0]; - let elementSelectionRuleView = [].filter.call(view.element.children[1].children, (e) => { - return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule; - })[0]._ruleEditor; - - is - ( - convertTextPropsToString(elementSelectionRule.textProps), - "color: white; background: black none repeat scroll 0% 0%", - "Paragraph first-letter properties are correct" - ); + is(convertTextPropsToString(elementSelectionRule.textProps), + "color: white; background: black none repeat scroll 0% 0%", + "Paragraph first-letter properties are correct"); } function* testBody(inspector, view) { - let {element, elementStyle} = yield testNode("body", inspector, view); + yield testNode("body", inspector, view); - let gutters = view.element.querySelectorAll(".theme-gutter"); - is (gutters.length, 0, "There are no gutter headings"); + let gutters = getGutters(view); + is(gutters.length, 0, "There are no gutter headings"); } function convertTextPropsToString(textProps) { @@ -224,24 +204,33 @@ function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) { selectionRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":-moz-selection") }; - is(rules.elementRules.length, ruleNbs.elementRulesNb, selector + - " has the correct number of non pseudo element rules"); - is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, selector + - " has the correct number of :first-line rules"); - is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, selector + - " has the correct number of :first-letter rules"); - is(rules.selectionRules.length, ruleNbs.selectionRulesNb, selector + - " has the correct number of :selection rules"); + is(rules.elementRules.length, ruleNbs.elementRulesNb, + selector + " has the correct number of non pseudo element rules"); + is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, + selector + " has the correct number of :first-line rules"); + is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, + selector + " has the correct number of :first-letter rules"); + is(rules.selectionRules.length, ruleNbs.selectionRulesNb, + selector + " has the correct number of :selection rules"); - return {rules: rules, element: element, elementStyle: elementStyle}; + return {rules, element, elementStyle}; +} + +function getGutters(view) { + return view.element.querySelectorAll(".theme-gutter"); } function assertGutters(view) { - let gutters = view.element.querySelectorAll(".theme-gutter"); - is (gutters.length, 3, "There are 3 gutter headings"); - is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct"); - is (gutters[1].textContent, "This Element", "Gutter heading is correct"); - is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct"); + let gutters = getGutters(view); + + is(gutters.length, 3, + "There are 3 gutter headings"); + is(gutters[0].textContent, "Pseudo-elements", + "Gutter heading is correct"); + is(gutters[1].textContent, "This Element", + "Gutter heading is correct"); + is(gutters[2].textContent, "Inherited from body", + "Gutter heading is correct"); return gutters; } diff --git a/browser/extensions/pdfjs/README.mozilla b/browser/extensions/pdfjs/README.mozilla index 64b69962cf50..ac05dcb0f1cd 100644 --- a/browser/extensions/pdfjs/README.mozilla +++ b/browser/extensions/pdfjs/README.mozilla @@ -1,3 +1,3 @@ This is the pdf.js project output, https://github.com/mozilla/pdf.js -Current extension version is: 1.1.165 +Current extension version is: 1.1.215 diff --git a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm index 0847240e1dc2..cce6d276b2f0 100644 --- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm +++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm @@ -161,7 +161,8 @@ function createNewChannel(uri, node, principal) { uri: uri, loadingNode: node, loadingPrincipal: principal, - contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER}); + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); } function asyncFetchChannel(channel, callback) { diff --git a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm index 2e0c4db56cf0..05beba101b05 100644 --- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm +++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm @@ -183,7 +183,8 @@ let PdfjsChromeUtils = { _findbarFromMessage: function(aMsg) { let browser = aMsg.target; let tabbrowser = browser.getTabBrowser(); - let tab = tabbrowser.getTabForBrowser(browser); + let tab; + tab = tabbrowser.getTabForBrowser(browser); return tabbrowser.getFindBar(tab); }, diff --git a/browser/extensions/pdfjs/content/build/pdf.js b/browser/extensions/pdfjs/content/build/pdf.js index 52c77b7dc71e..f2fcd6e03077 100644 --- a/browser/extensions/pdfjs/content/build/pdf.js +++ b/browser/extensions/pdfjs/content/build/pdf.js @@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.1.165'; -PDFJS.build = '39d2103'; +PDFJS.version = '1.1.215'; +PDFJS.build = 'c9a7498'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it @@ -4208,7 +4208,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } var name = fontObj.loadedName || 'sans-serif'; - var bold = fontObj.black ? (fontObj.bold ? 'bolder' : 'bold') : + var bold = fontObj.black ? (fontObj.bold ? '900' : 'bold') : (fontObj.bold ? 'bold' : 'normal'); var italic = fontObj.italic ? 'italic' : 'normal'; @@ -4468,6 +4468,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (isTextInvisible || fontSize === 0) { return; } + this.cachedGetSinglePixelWidth = null; ctx.save(); ctx.transform.apply(ctx, current.textMatrix); diff --git a/browser/extensions/pdfjs/content/build/pdf.worker.js b/browser/extensions/pdfjs/content/build/pdf.worker.js index 3c6b1301ebe5..88be9f00cb86 100644 --- a/browser/extensions/pdfjs/content/build/pdf.worker.js +++ b/browser/extensions/pdfjs/content/build/pdf.worker.js @@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.1.165'; -PDFJS.build = '39d2103'; +PDFJS.version = '1.1.215'; +PDFJS.build = 'c9a7498'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it @@ -2062,17 +2062,33 @@ var Page = (function PageClosure() { return this.pageDict.get(key); }, - getInheritedPageProp: function Page_inheritPageProp(key) { - var dict = this.pageDict; - var value = dict.get(key); - while (value === undefined) { - dict = dict.get('Parent'); - if (!dict) { + getInheritedPageProp: function Page_getInheritedPageProp(key) { + var dict = this.pageDict, valueArray = null, loopCount = 0; + var MAX_LOOP_COUNT = 100; + // Always walk up the entire parent chain, to be able to find + // e.g. \Resources placed on multiple levels of the tree. + while (dict) { + var value = dict.get(key); + if (value) { + if (!valueArray) { + valueArray = []; + } + valueArray.push(value); + } + if (++loopCount > MAX_LOOP_COUNT) { + warn('Page_getInheritedPageProp: maximum loop count exceeded.'); break; } - value = dict.get(key); + dict = dict.get('Parent'); } - return value; + if (!valueArray) { + return Dict.empty; + } + if (valueArray.length === 1 || !isDict(valueArray[0]) || + loopCount > MAX_LOOP_COUNT) { + return valueArray[0]; + } + return Dict.merge(this.xref, valueArray); }, get content() { @@ -2080,14 +2096,10 @@ var Page = (function PageClosure() { }, get resources() { - var value = this.getInheritedPageProp('Resources'); // For robustness: The spec states that a \Resources entry has to be - // present, but can be empty. Some document omit it still. In this case - // return an empty dictionary: - if (value === undefined) { - value = Dict.empty; - } - return shadow(this, 'resources', value); + // present, but can be empty. Some document omit it still, in this case + // we return an empty dictionary. + return shadow(this, 'resources', this.getInheritedPageProp('Resources')); }, get mediaBox() { @@ -2360,6 +2372,10 @@ var PDFDocument = (function PDFDocumentClosure() { PDFDocument.prototype = { parse: function PDFDocument_parse(recoveryMode) { this.setup(recoveryMode); + var version = this.catalog.catDict.get('Version'); + if (isName(version)) { + this.pdfFormatVersion = version.name; + } try { // checking if AcroForm is present this.acroForm = this.catalog.catDict.get('AcroForm'); @@ -2461,8 +2477,10 @@ var PDFDocument = (function PDFDocumentClosure() { } version += String.fromCharCode(ch); } - // removing "%PDF-"-prefix - this.pdfFormatVersion = version.substring(5); + if (!this.pdfFormatVersion) { + // removing "%PDF-"-prefix + this.pdfFormatVersion = version.substring(5); + } return; } // May not be a PDF file, continue anyway. @@ -2739,6 +2757,24 @@ var Dict = (function DictClosure() { Dict.empty = new Dict(null); + Dict.merge = function Dict_merge(xref, dictArray) { + var mergedDict = new Dict(xref); + + for (var i = 0, ii = dictArray.length; i < ii; i++) { + var dict = dictArray[i]; + if (!isDict(dict)) { + continue; + } + for (var keyName in dict.map) { + if (mergedDict.map[keyName]) { + continue; + } + mergedDict.map[keyName] = dict.map[keyName]; + } + } + return mergedDict; + }; + return Dict; })(); @@ -5213,7 +5249,10 @@ var PDFFunction = (function PDFFunctionClosure() { var rmin = encode[2 * i]; var rmax = encode[2 * i + 1]; - tmpBuf[0] = rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); + // Prevent the value from becoming NaN as a result + // of division by zero (fixes issue6113.pdf). + tmpBuf[0] = dmin === dmax ? rmin : + rmin + (v - dmin) * (rmax - rmin) / (dmax - dmin); // call the appropriate function fns[i](tmpBuf, 0, dest, destOffset); @@ -6222,9 +6261,9 @@ var ColorSpace = (function ColorSpaceClosure() { error('unrecognized colorspace ' + mode); } } else if (isArray(cs)) { - mode = cs[0].name; + mode = xref.fetchIfRef(cs[0]).name; this.mode = mode; - var numComps, params; + var numComps, params, alt; switch (mode) { case 'DeviceGray': @@ -6246,6 +6285,17 @@ var ColorSpace = (function ColorSpaceClosure() { var stream = xref.fetchIfRef(cs[1]); var dict = stream.dict; numComps = dict.get('N'); + alt = dict.get('Alternate'); + if (alt) { + var altIR = ColorSpace.parseToIR(alt, xref, res); + // Parse the /Alternate CS to ensure that the number of components + // are correct, and also (indirectly) that it is not a PatternCS. + var altCS = ColorSpace.fromIR(altIR); + if (altCS.numComps === numComps) { + return altIR; + } + warn('ICCBased color space: Ignoring incorrect /Alternate entry.'); + } if (numComps === 1) { return 'DeviceGrayCS'; } else if (numComps === 3) { @@ -6255,7 +6305,7 @@ var ColorSpace = (function ColorSpaceClosure() { } break; case 'Pattern': - var basePatternCS = cs[1]; + var basePatternCS = xref.fetchIfRef(cs[1]) || null; if (basePatternCS) { basePatternCS = ColorSpace.parseToIR(basePatternCS, xref, res); } @@ -6278,11 +6328,11 @@ var ColorSpace = (function ColorSpaceClosure() { } else if (isArray(name)) { numComps = name.length; } - var alt = ColorSpace.parseToIR(cs[2], xref, res); + alt = ColorSpace.parseToIR(cs[2], xref, res); var tintFnIR = PDFFunction.getIR(xref, xref.fetchIfRef(cs[3])); return ['AlternateCS', numComps, alt, tintFnIR]; case 'Lab': - params = cs[1].getAll(); + params = xref.fetchIfRef(cs[1]).getAll(); return ['LabCS', params]; default: error('unimplemented color space object "' + mode + '"'); @@ -16333,12 +16383,14 @@ var Font = (function FontClosure() { case 0x7F: // Control char case 0xA0: // Non breaking space case 0xAD: // Soft hyphen - case 0x0E33: // Thai character SARA AM case 0x2011: // Non breaking hyphen case 0x205F: // Medium mathematical space case 0x25CC: // Dotted circle (combining mark) return true; } + if ((code & ~0xFF) === 0x0E00) { // Thai/Lao chars (with combining mark) + return true; + } return false; } @@ -17762,13 +17814,18 @@ var Font = (function FontClosure() { } } - var charCodeToGlyphId = [], charCode, toUnicode = properties.toUnicode; + var charCodeToGlyphId = [], charCode; + var toUnicode = properties.toUnicode, widths = properties.widths; + var isIdentityUnicode = toUnicode instanceof IdentityToUnicodeMap; - function hasGlyph(glyphId, charCode) { + function hasGlyph(glyphId, charCode, widthCode) { if (!missingGlyphs[glyphId]) { return true; } - if (charCode >= 0 && toUnicode.has(charCode)) { + if (!isIdentityUnicode && charCode >= 0 && toUnicode.has(charCode)) { + return true; + } + if (widths && widthCode >= 0 && isNum(widths[widthCode])) { return true; } return false; @@ -17788,7 +17845,7 @@ var Font = (function FontClosure() { } if (glyphId >= 0 && glyphId < numGlyphs && - hasGlyph(glyphId, charCode)) { + hasGlyph(glyphId, charCode, cid)) { charCodeToGlyphId[charCode] = glyphId; } }); @@ -17849,18 +17906,19 @@ var Font = (function FontClosure() { var found = false; for (i = 0; i < cmapMappingsLength; ++i) { if (cmapMappings[i].charCode === unicodeOrCharCode && - hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode)) { + hasGlyph(cmapMappings[i].glyphId, unicodeOrCharCode, -1)) { charCodeToGlyphId[charCode] = cmapMappings[i].glyphId; found = true; break; } } if (!found && properties.glyphNames) { - // Try to map using the post table. There are currently no known - // pdfs that this fixes. + // Try to map using the post table. var glyphId = properties.glyphNames.indexOf(glyphName); - if (glyphId > 0 && hasGlyph(glyphId, -1)) { + if (glyphId > 0 && hasGlyph(glyphId, -1, -1)) { charCodeToGlyphId[charCode] = glyphId; + } else { + charCodeToGlyphId[charCode] = 0; // notdef } } } diff --git a/browser/extensions/pdfjs/content/web/viewer.css b/browser/extensions/pdfjs/content/web/viewer.css index 80f7d6215bf8..2169851abb6c 100644 --- a/browser/extensions/pdfjs/content/web/viewer.css +++ b/browser/extensions/pdfjs/content/web/viewer.css @@ -803,7 +803,7 @@ html[dir='rtl'] .dropdownToolbarButton { .dropdownToolbarButton { width: 120px; max-width: 120px; - padding: 3px 2px 2px; + padding: 0; overflow: hidden; background: url(images/toolbarButton-menuArrows.png) no-repeat; } @@ -819,7 +819,7 @@ html[dir='rtl'] .dropdownToolbarButton { font-size: 12px; color: hsl(0,0%,95%); margin: 0; - padding: 0; + padding: 3px 2px 2px; border: none; background: rgba(0,0,0,0); /* Opera does not support 'transparent'