Bug 1220878-switch Hello from React.addons.classSet to classnames package, r=edilee,gerv
authorDan Mosedale <dmose@meer.net>
Thu, 05 Nov 2015 16:40:11 -0800
changeset 271417 df66cac11155fda2f1d336d71dfc529b22636b81
parent 271416 373ee6962940137f8e71ea423ec456ff17849da8
child 271418 d3d825076a477d6886126e1a7966dc81bda4ec15
push id29640
push usercbook@mozilla.com
push dateFri, 06 Nov 2015 11:21:14 +0000
treeherderautoland@b918dbd0c3a0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedilee, gerv
bugs1220878
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1220878-switch Hello from React.addons.classSet to classnames package, r=edilee,gerv
browser/components/loop/.eslintrc
browser/components/loop/content/conversation.html
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/panel.html
browser/components/loop/content/shared/js/textChatView.js
browser/components/loop/content/shared/js/textChatView.jsx
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/content/shared/libs/classnames-2.2.0.js
browser/components/loop/jar.mn
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/standalone/content/webappEntryPoint.js
browser/components/loop/standalone/package.json
browser/components/loop/test/desktop-local/index.html
browser/components/loop/test/karma/karma.coverage.desktop.js
browser/components/loop/test/karma/karma.coverage.shared_standalone.js
browser/components/loop/test/shared/index.html
browser/components/loop/test/standalone/index.html
browser/components/loop/ui/index.html
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
toolkit/content/license.html
--- a/browser/components/loop/.eslintrc
+++ b/browser/components/loop/.eslintrc
@@ -12,16 +12,17 @@
     "browser": true,
     "mocha": true
   },
   "extends": "eslint:recommended",
   "globals": {
     "_": false,
     "Backbone": false,
     "chai": false,
+    "classNames": false,
     "console": false,
     "loop": true,
     "MozActivity": false,
     "RTCSessionDescription": false,
     "OT": false,
     "performance": false,
     "Promise": false,
     "React": false,
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -18,16 +18,17 @@
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/js/otconfig.js"></script>
     <script type="text/javascript" src="loop/libs/sdk.js"></script>
     <script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
+    <script type="text/javascript" src="loop/shared/libs/classnames-2.2.0.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
     <script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="loop/shared/js/store.js"></script>
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -160,17 +160,17 @@ loop.panel = (function(_, mozL10n) {
       onClick: React.PropTypes.func.isRequired
     },
 
     getDefaultProps: function() {
       return { displayed: true };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       if (!this.props.displayed) {
         return null;
       }
 
       var extraCSSClass = {
         "dropdown-menu-item": true
       };
@@ -240,17 +240,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     openGettingStartedTour: function() {
       this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" :
                                                       "entry-settings-signin";
       var notificationsLabel = this.props.mozLoop.doNotDisturb ? "settings_menu_item_turnnotificationson" :
                                                                  "settings_menu_item_turnnotificationsoff";
 
       return (
         React.createElement("div", {className: "settings-menu dropdown"}, 
           React.createElement("button", {className: "button-settings", 
@@ -431,17 +431,17 @@ loop.panel = (function(_, mozL10n) {
      */
     _handleMouseOut: function() {
       if (this.state.showMenu) {
         this.toggleDropdownMenu();
       }
     },
 
     render: function() {
-      var roomClasses = React.addons.classSet({
+      var roomClasses = classNames({
         "room-entry": true,
         "room-active": this._isActive(),
         "room-opened": this.props.isOpenedRoom
       });
 
       var roomTitle = this.props.room.decryptedContext.roomName ||
         this.props.room.decryptedContext.urls[0].description ||
         this.props.room.decryptedContext.urls[0].location;
@@ -583,17 +583,17 @@ loop.panel = (function(_, mozL10n) {
       } else {
         // Position below click area.
         menuNode.style.top = this.props.eventPosY - listNodeRect.top +
                              offset + "px";
       }
     },
 
     render: function() {
-      var dropdownClasses = React.addons.classSet({
+      var dropdownClasses = classNames({
         "dropdown-menu": true,
         "dropdown-menu-up": this.state.openDirUp
       });
 
       return (
         React.createElement("ul", {className: dropdownClasses}, 
           React.createElement("li", {
             className: "dropdown-menu-item", 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -160,17 +160,17 @@ loop.panel = (function(_, mozL10n) {
       onClick: React.PropTypes.func.isRequired
     },
 
     getDefaultProps: function() {
       return { displayed: true };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       if (!this.props.displayed) {
         return null;
       }
 
       var extraCSSClass = {
         "dropdown-menu-item": true
       };
@@ -240,17 +240,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     openGettingStartedTour: function() {
       this.props.mozLoop.openGettingStartedTour("settings-menu");
       this.closeWindow();
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" :
                                                       "entry-settings-signin";
       var notificationsLabel = this.props.mozLoop.doNotDisturb ? "settings_menu_item_turnnotificationson" :
                                                                  "settings_menu_item_turnnotificationsoff";
 
       return (
         <div className="settings-menu dropdown">
           <button className="button-settings"
@@ -431,17 +431,17 @@ loop.panel = (function(_, mozL10n) {
      */
     _handleMouseOut: function() {
       if (this.state.showMenu) {
         this.toggleDropdownMenu();
       }
     },
 
     render: function() {
-      var roomClasses = React.addons.classSet({
+      var roomClasses = classNames({
         "room-entry": true,
         "room-active": this._isActive(),
         "room-opened": this.props.isOpenedRoom
       });
 
       var roomTitle = this.props.room.decryptedContext.roomName ||
         this.props.room.decryptedContext.urls[0].description ||
         this.props.room.decryptedContext.urls[0].location;
@@ -583,17 +583,17 @@ loop.panel = (function(_, mozL10n) {
       } else {
         // Position below click area.
         menuNode.style.top = this.props.eventPosY - listNodeRect.top +
                              offset + "px";
       }
     },
 
     render: function() {
-      var dropdownClasses = React.addons.classSet({
+      var dropdownClasses = classNames({
         "dropdown-menu": true,
         "dropdown-menu-up": this.state.openDirUp
       });
 
       return (
         <ul className={dropdownClasses}>
           <li
             className="dropdown-menu-item"
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -194,17 +194,17 @@ loop.roomViews = (function(mozL10n) {
     },
 
     render: function() {
       // Don't render a thing when no data has been fetched yet.
       if (!this.props.socialShareProviders) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var shareDropdown = cx({
         "share-service-dropdown": true,
         "dropdown-menu": true,
         "visually-hidden": true,
         "hide": !this.props.show
       });
 
       return (
@@ -325,17 +325,17 @@ loop.roomViews = (function(mozL10n) {
       }
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         React.createElement("div", {className: "room-invitation-overlay"}, 
           React.createElement("div", {className: "room-invitation-content"}, 
             React.createElement("p", {className: cx({ hide: this.props.showEditContext })}, 
               mozL10n.get("invite_header_text2")
             )
           ), 
           React.createElement("div", {className: cx({
@@ -541,17 +541,17 @@ loop.roomViews = (function(mozL10n) {
         return null;
       }
 
       var url = this._getURL();
       var thumbnail = url && url.thumbnail || "loop/shared/img/icons-16x16.svg#globe";
       var urlDescription = url && url.description || "";
       var location = url && url.location || "";
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var availableContext = this.state.availableContext;
       return (
         React.createElement("div", {className: "room-context"}, 
           React.createElement("p", {className: cx({ "error": !!this.props.error,
                             "error-display-area": true })}, 
             mozL10n.get("rooms_change_failed_label")
           ), 
           React.createElement("h2", {className: "room-context-header"}, mozL10n.get("context_inroom_header")), 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -194,17 +194,17 @@ loop.roomViews = (function(mozL10n) {
     },
 
     render: function() {
       // Don't render a thing when no data has been fetched yet.
       if (!this.props.socialShareProviders) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var shareDropdown = cx({
         "share-service-dropdown": true,
         "dropdown-menu": true,
         "visually-hidden": true,
         "hide": !this.props.show
       });
 
       return (
@@ -325,17 +325,17 @@ loop.roomViews = (function(mozL10n) {
       }
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         <div className="room-invitation-overlay">
           <div className="room-invitation-content">
             <p className={cx({ hide: this.props.showEditContext })}>
               {mozL10n.get("invite_header_text2")}
             </p>
           </div>
           <div className={cx({
@@ -541,17 +541,17 @@ loop.roomViews = (function(mozL10n) {
         return null;
       }
 
       var url = this._getURL();
       var thumbnail = url && url.thumbnail || "loop/shared/img/icons-16x16.svg#globe";
       var urlDescription = url && url.description || "";
       var location = url && url.location || "";
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var availableContext = this.state.availableContext;
       return (
         <div className="room-context">
           <p className={cx({ "error": !!this.props.error,
                             "error-display-area": true })}>
             {mozL10n.get("rooms_change_failed_label")}
           </p>
           <h2 className="room-context-header">{mozL10n.get("context_inroom_header")}</h2>
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -12,16 +12,17 @@
   <body class="panel">
 
     <div id="main"></div>
 
     <script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
     <script type="text/javascript" src="loop/libs/l10n.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
+    <script type="text/javascript" src="loop/shared/libs/classnames-2.2.0.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -43,17 +43,17 @@ loop.shared.views.chat = (function(mozL1
           date.toLocaleTimeString(language,
                                    { hour: "numeric", minute: "numeric",
                                    hour12: false })
         )
       );
     },
 
     render: function() {
-      var classes = React.addons.classSet({
+      var classes = classNames({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       var optionalProps = {};
@@ -160,17 +160,17 @@ loop.shared.views.chat = (function(mozL1
         }.bind(this));
       }
     },
 
     render: function() {
       /* Keep track of the last printed timestamp. */
       var lastTimestamp = 0;
 
-      var entriesClasses = React.addons.classSet({
+      var entriesClasses = classNames({
         "text-chat-entries": true
       });
 
       return (
         React.createElement("div", {className: entriesClasses}, 
           React.createElement("div", {className: "text-chat-scroller"}, 
             
               this.props.messageList.map(function(entry, i) {
@@ -390,17 +390,17 @@ loop.shared.views.chat = (function(mozL1
         });
       }
 
       // Only show the placeholder if we've sent messages.
       var hasSentMessages = messageList.some(function(item) {
         return item.type === CHAT_MESSAGE_TYPES.SENT;
       });
 
-      var textChatViewClasses = React.addons.classSet({
+      var textChatViewClasses = classNames({
         "text-chat-view": true,
         "text-chat-entries-empty": !messageList.length,
         "text-chat-disabled": !this.state.textChatEnabled
       });
 
       return (
         React.createElement("div", {className: textChatViewClasses}, 
           React.createElement(TextChatEntriesView, {
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -43,17 +43,17 @@ loop.shared.views.chat = (function(mozL1
           {date.toLocaleTimeString(language,
                                    { hour: "numeric", minute: "numeric",
                                    hour12: false })}
         </span>
       );
     },
 
     render: function() {
-      var classes = React.addons.classSet({
+      var classes = classNames({
         "text-chat-entry": true,
         "received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
         "sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
         "special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
         "room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME
       });
 
       var optionalProps = {};
@@ -160,17 +160,17 @@ loop.shared.views.chat = (function(mozL1
         }.bind(this));
       }
     },
 
     render: function() {
       /* Keep track of the last printed timestamp. */
       var lastTimestamp = 0;
 
-      var entriesClasses = React.addons.classSet({
+      var entriesClasses = classNames({
         "text-chat-entries": true
       });
 
       return (
         <div className={entriesClasses}>
           <div className="text-chat-scroller">
             {
               this.props.messageList.map(function(entry, i) {
@@ -390,17 +390,17 @@ loop.shared.views.chat = (function(mozL1
         });
       }
 
       // Only show the placeholder if we've sent messages.
       var hasSentMessages = messageList.some(function(item) {
         return item.type === CHAT_MESSAGE_TYPES.SENT;
       });
 
-      var textChatViewClasses = React.addons.classSet({
+      var textChatViewClasses = classNames({
         "text-chat-view": true,
         "text-chat-entries-empty": !messageList.length,
         "text-chat-disabled": !this.state.textChatEnabled
       });
 
       return (
         <div className={textChatViewClasses}>
           <TextChatEntriesView
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -65,17 +65,17 @@ loop.shared.views = (function(_, mozL10n
       return { enabled: true, visible: true };
     },
 
     handleClick: function() {
       this.props.action();
     },
 
     _getClasses: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       // classes
       var classesObj = {
         "btn": true,
         "media-control": true,
         "transparent-button": true,
         "local-media": this.props.scope === "local",
         "muted": !this.props.enabled,
         "hide": !this.props.visible
@@ -165,17 +165,17 @@ loop.shared.views = (function(_, mozL10n
       return mozL10n.get(prefix + "_screenshare_button_title");
     },
 
     render: function() {
       if (!this.props.visible) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       var isActive = this.props.state === SCREEN_SHARE_STATES.ACTIVE;
       var screenShareClasses = cx({
         "btn": true,
         "btn-screen-share": true,
         "transparent-button": true,
         "menu-showing": this.state.showMenu,
         "active": isActive,
@@ -299,17 +299,17 @@ loop.shared.views = (function(_, mozL10n
       var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
       this.props.mozLoop.openURL(helloSupportUrl);
     },
 
     /**
      * Recover the needed info for generating an specific menu Item
      */
     getItemInfo: function(menuItem) {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       switch (menuItem.id) {
         case "help":
           return {
             cssClasses: "dropdown-menu-item",
             handler: this.handleHelpEntry,
             label: mozL10n.get("help_label")
           };
         case "edit":
@@ -357,17 +357,17 @@ loop.shared.views = (function(_, mozL10n
       }
       var menuItemRows = this.props.menuItems.map(this.generateMenuItem)
         .filter(function(item) { return item; });
 
       if (!menuItemRows || !menuItemRows.length) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var settingsDropdownMenuClasses = cx({
         "settings-menu": true,
         "dropdown-menu": true,
         "menu-below": this.props.menuBelow,
         "hide": !this.state.showMenu
       });
       return (
         React.createElement("div", {className: "settings-control"}, 
@@ -489,17 +489,17 @@ loop.shared.views = (function(_, mozL10n
       }.bind(this), 6000);
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var conversationToolbarCssClasses = cx({
         "conversation-toolbar": true,
         "idle": this.state.idle
       });
       var showButtons = this.props.video.visible || this.props.audio.visible;
       var mediaButtonGroupCssClasses = cx({
         "conversation-toolbar-media-btn-group-box": true,
         "hide": !showButtons
@@ -637,17 +637,17 @@ loop.shared.views = (function(_, mozL10n
       return {
         disabled: false,
         additionalClass: "",
         htmlId: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         React.createElement("button", {className: cx(classObject), 
                 disabled: this.props.disabled, 
                 id: this.props.htmlId, 
@@ -670,17 +670,17 @@ loop.shared.views = (function(_, mozL10n
 
     getDefaultProps: function() {
       return {
         additionalClass: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { "button-group": true };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         React.createElement("div", {className: cx(classObject)}, 
           this.props.children
         )
@@ -737,17 +737,17 @@ loop.shared.views = (function(_, mozL10n
         checked: !this.state.checked,
         value: this.state.checked ? "" : this.props.value
       };
       this.setState(newState);
       this.props.onChange(newState);
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var wrapperClasses = {
         "checkbox-wrapper": true,
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
@@ -855,17 +855,17 @@ loop.shared.views = (function(_, mozL10n
       var thumbnail = this.props.thumbnail;
 
       if (!thumbnail) {
         thumbnail = this.props.useDesktopPaths ?
           "loop/shared/img/icons-16x16.svg#globe" :
           "shared/img/icons-16x16.svg#globe";
       }
 
-      var wrapperClasses = React.addons.classSet({
+      var wrapperClasses = classNames({
         "context-wrapper": true,
         "clicks-allowed": this.props.allowClick
       });
 
       return (
         React.createElement("div", {className: "context-content"}, 
           React.createElement("a", {className: wrapperClasses, 
              href: this.props.allowClick ? this.props.url : null, 
@@ -1071,27 +1071,27 @@ loop.shared.views = (function(_, mozL10n
             mediaType: "local", 
             posterUrl: this.props.localPosterUrl, 
             srcMediaElement: this.props.localSrcMediaElement})
         )
       );
     },
 
     render: function() {
-      var remoteStreamClasses = React.addons.classSet({
+      var remoteStreamClasses = classNames({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
-      var screenShareStreamClasses = React.addons.classSet({
+      var screenShareStreamClasses = classNames({
         "screen": true,
         "focus-stream": this.props.displayScreenShare
       });
 
-      var mediaWrapperClasses = React.addons.classSet({
+      var mediaWrapperClasses = classNames({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcMediaElement ||
           this.props.localPosterUrl,
         "showing-remote-streams": this.props.remoteSrcMediaElement ||
           this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -65,17 +65,17 @@ loop.shared.views = (function(_, mozL10n
       return { enabled: true, visible: true };
     },
 
     handleClick: function() {
       this.props.action();
     },
 
     _getClasses: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       // classes
       var classesObj = {
         "btn": true,
         "media-control": true,
         "transparent-button": true,
         "local-media": this.props.scope === "local",
         "muted": !this.props.enabled,
         "hide": !this.props.visible
@@ -165,17 +165,17 @@ loop.shared.views = (function(_, mozL10n
       return mozL10n.get(prefix + "_screenshare_button_title");
     },
 
     render: function() {
       if (!this.props.visible) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
 
       var isActive = this.props.state === SCREEN_SHARE_STATES.ACTIVE;
       var screenShareClasses = cx({
         "btn": true,
         "btn-screen-share": true,
         "transparent-button": true,
         "menu-showing": this.state.showMenu,
         "active": isActive,
@@ -299,17 +299,17 @@ loop.shared.views = (function(_, mozL10n
       var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url");
       this.props.mozLoop.openURL(helloSupportUrl);
     },
 
     /**
      * Recover the needed info for generating an specific menu Item
      */
     getItemInfo: function(menuItem) {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       switch (menuItem.id) {
         case "help":
           return {
             cssClasses: "dropdown-menu-item",
             handler: this.handleHelpEntry,
             label: mozL10n.get("help_label")
           };
         case "edit":
@@ -357,17 +357,17 @@ loop.shared.views = (function(_, mozL10n
       }
       var menuItemRows = this.props.menuItems.map(this.generateMenuItem)
         .filter(function(item) { return item; });
 
       if (!menuItemRows || !menuItemRows.length) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var settingsDropdownMenuClasses = cx({
         "settings-menu": true,
         "dropdown-menu": true,
         "menu-below": this.props.menuBelow,
         "hide": !this.state.showMenu
       });
       return (
         <div className="settings-control">
@@ -489,17 +489,17 @@ loop.shared.views = (function(_, mozL10n
       }.bind(this), 6000);
     },
 
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var conversationToolbarCssClasses = cx({
         "conversation-toolbar": true,
         "idle": this.state.idle
       });
       var showButtons = this.props.video.visible || this.props.audio.visible;
       var mediaButtonGroupCssClasses = cx({
         "conversation-toolbar-media-btn-group-box": true,
         "hide": !showButtons
@@ -637,17 +637,17 @@ loop.shared.views = (function(_, mozL10n
       return {
         disabled: false,
         additionalClass: "",
         htmlId: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         <button className={cx(classObject)}
                 disabled={this.props.disabled}
                 id={this.props.htmlId}
@@ -670,17 +670,17 @@ loop.shared.views = (function(_, mozL10n
 
     getDefaultProps: function() {
       return {
         additionalClass: ""
       };
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var classObject = { "button-group": true };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         <div className={cx(classObject)}>
           {this.props.children}
         </div>
@@ -737,17 +737,17 @@ loop.shared.views = (function(_, mozL10n
         checked: !this.state.checked,
         value: this.state.checked ? "" : this.props.value
       };
       this.setState(newState);
       this.props.onChange(newState);
     },
 
     render: function() {
-      var cx = React.addons.classSet;
+      var cx = classNames;
       var wrapperClasses = {
         "checkbox-wrapper": true,
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
@@ -855,17 +855,17 @@ loop.shared.views = (function(_, mozL10n
       var thumbnail = this.props.thumbnail;
 
       if (!thumbnail) {
         thumbnail = this.props.useDesktopPaths ?
           "loop/shared/img/icons-16x16.svg#globe" :
           "shared/img/icons-16x16.svg#globe";
       }
 
-      var wrapperClasses = React.addons.classSet({
+      var wrapperClasses = classNames({
         "context-wrapper": true,
         "clicks-allowed": this.props.allowClick
       });
 
       return (
         <div className="context-content">
           <a className={wrapperClasses}
              href={this.props.allowClick ? this.props.url : null}
@@ -1071,27 +1071,27 @@ loop.shared.views = (function(_, mozL10n
             mediaType="local"
             posterUrl={this.props.localPosterUrl}
             srcMediaElement={this.props.localSrcMediaElement} />
         </div>
       );
     },
 
     render: function() {
-      var remoteStreamClasses = React.addons.classSet({
+      var remoteStreamClasses = classNames({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
-      var screenShareStreamClasses = React.addons.classSet({
+      var screenShareStreamClasses = classNames({
         "screen": true,
         "focus-stream": this.props.displayScreenShare
       });
 
-      var mediaWrapperClasses = React.addons.classSet({
+      var mediaWrapperClasses = classNames({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcMediaElement ||
           this.props.localPosterUrl,
         "showing-remote-streams": this.props.remoteSrcMediaElement ||
           this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/libs/classnames-2.2.0.js
@@ -0,0 +1,48 @@
+/*!
+  Copyright (c) 2015 Jed Watson.
+  Licensed under the MIT License (MIT), see
+  http://jedwatson.github.io/classnames
+*/
+/* global define */
+
+(function () {
+	'use strict';
+
+	var hasOwn = {}.hasOwnProperty;
+
+	function classNames () {
+		var classes = '';
+
+		for (var i = 0; i < arguments.length; i++) {
+			var arg = arguments[i];
+			if (!arg) continue;
+
+			var argType = typeof arg;
+
+			if (argType === 'string' || argType === 'number') {
+				classes += ' ' + arg;
+			} else if (Array.isArray(arg)) {
+				classes += ' ' + classNames.apply(null, arg);
+			} else if (argType === 'object') {
+				for (var key in arg) {
+					if (hasOwn.call(arg, key) && arg[key]) {
+						classes += ' ' + key;
+					}
+				}
+			}
+		}
+
+		return classes.substr(1);
+	}
+
+	if (typeof module !== 'undefined' && module.exports) {
+		module.exports = classNames;
+	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
+		// register as 'classnames', consistent with npm package name
+		define('classnames', function () {
+			return classNames;
+		});
+	} else {
+		window.classNames = classNames;
+	}
+}());
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -109,16 +109,17 @@ browser.jar:
   # Shared libs
 #ifdef DEBUG
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2.js)
 #else
   content/browser/loop/shared/libs/react-0.12.2.js    (content/shared/libs/react-0.12.2-prod.js)
 #endif
   content/browser/loop/shared/libs/lodash-3.9.3.js    (content/shared/libs/lodash-3.9.3.js)
   content/browser/loop/shared/libs/backbone-1.2.1.js  (content/shared/libs/backbone-1.2.1.js)
+  content/browser/loop/shared/libs/classnames-2.2.0.js      (content/shared/libs/classnames-2.2.0.js)
 
   # Shared sounds
   content/browser/loop/shared/sounds/ringtone.ogg       (content/shared/sounds/ringtone.ogg)
   content/browser/loop/shared/sounds/connecting.ogg     (content/shared/sounds/connecting.ogg)
   content/browser/loop/shared/sounds/connected.ogg      (content/shared/sounds/connected.ogg)
   content/browser/loop/shared/sounds/terminated.ogg     (content/shared/sounds/terminated.ogg)
   content/browser/loop/shared/sounds/room-joined.ogg    (content/shared/sounds/room-joined.ogg)
   content/browser/loop/shared/sounds/room-joined-in.ogg (content/shared/sounds/room-joined-in.ogg)
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -75,17 +75,17 @@ loop.standaloneRoomViews = (function(moz
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     _renderJoinButton: function() {
       var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ?
         mozL10n.get("rooms_room_joined_own_conversation_label") :
         mozL10n.get("rooms_room_join_label");
 
-      var buttonClasses = React.addons.classSet({
+      var buttonClasses = classNames({
         btn: true,
         "btn-info": true,
         disabled: this.state.roomState === ROOM_STATES.JOINED
       });
 
       return (
         React.createElement("button", {
           className: buttonClasses, 
@@ -320,17 +320,17 @@ loop.standaloneRoomViews = (function(moz
         }
         case ROOM_STATES.MEDIA_WAIT: {
           var msg = mozL10n.get("call_progress_getting_media_description",
                                 { clientShortname: mozL10n.get("clientShortname2") });
           var utils = loop.shared.utils;
           var isChrome = utils.isChrome(navigator.userAgent);
           var isFirefox = utils.isFirefox(navigator.userAgent);
           var isOpera = utils.isOpera(navigator.userAgent);
-          var promptMediaMessageClasses = React.addons.classSet({
+          var promptMediaMessageClasses = classNames({
             "prompt-media-message": true,
             "chrome": isChrome,
             "firefox": isFirefox,
             "opera": isOpera,
             "other": !isChrome && !isFirefox && !isOpera
           });
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -75,17 +75,17 @@ loop.standaloneRoomViews = (function(moz
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     _renderJoinButton: function() {
       var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ?
         mozL10n.get("rooms_room_joined_own_conversation_label") :
         mozL10n.get("rooms_room_join_label");
 
-      var buttonClasses = React.addons.classSet({
+      var buttonClasses = classNames({
         btn: true,
         "btn-info": true,
         disabled: this.state.roomState === ROOM_STATES.JOINED
       });
 
       return (
         <button
           className={buttonClasses}
@@ -320,17 +320,17 @@ loop.standaloneRoomViews = (function(moz
         }
         case ROOM_STATES.MEDIA_WAIT: {
           var msg = mozL10n.get("call_progress_getting_media_description",
                                 { clientShortname: mozL10n.get("clientShortname2") });
           var utils = loop.shared.utils;
           var isChrome = utils.isChrome(navigator.userAgent);
           var isFirefox = utils.isFirefox(navigator.userAgent);
           var isOpera = utils.isOpera(navigator.userAgent);
-          var promptMediaMessageClasses = React.addons.classSet({
+          var promptMediaMessageClasses = classNames({
             "prompt-media-message": true,
             "chrome": isChrome,
             "firefox": isFirefox,
             "opera": isOpera,
             "other": !isChrome && !isFirefox && !isOpera
           });
           return (
             <div className="room-inner-info-area">
--- a/browser/components/loop/standalone/content/webappEntryPoint.js
+++ b/browser/components/loop/standalone/content/webappEntryPoint.js
@@ -33,19 +33,21 @@ require("exports?_!shared/libs/lodash-3.
 require("expose?Backbone!imports?define=>false!shared/libs/backbone-1.2.1.js");
 
 /* global: __PROD__ */
 if (typeof __PROD__ !== "undefined") {
   // webpack warns if we try to minify some prebuilt libraries, so we
   // pull in the unbuilt version from node_modules
   require("expose?React!react");
   require("expose?React!react/addons");
+  require("expose?classNames!classnames");
 } else {
   // our development server setup doesn't yet handle real modules, so for now...
   require("shared/libs/react-0.12.2.js");
+  require("shared/libs/classnames-2.2.0.js");
 }
 
 
 // Someday, these will be real modules.  For now, we're chaining loaders
 // to teach webpack how to treat them like modules anyway.
 //
 // We do it in this file rather than globally in webpack.config.js
 // because:
--- a/browser/components/loop/standalone/package.json
+++ b/browser/components/loop/standalone/package.json
@@ -7,16 +7,17 @@
     "url": "git@github.com:mozilla/loop-client.git"
   },
   "engines": {
     "node": "0.10.x",
     "npm": "2.14.x"
   },
   "dependencies": {},
   "devDependencies": {
+    "classnames": "2.2.x",
     "compression": "1.5.x",
     "eslint": "1.6.x",
     "eslint-plugin-mozilla": "../../../../testing/eslint-plugin-mozilla",
     "eslint-plugin-react": "3.5.x",
     "exports-loader": "0.6.x",
     "expose-loader": "0.7.x",
     "express": "4.x",
     "imports-loader": "0.6.x",
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -26,16 +26,17 @@
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
   </script>
 
   <!-- libs -->
   <script src="../../content/libs/l10n.js"></script>
   <script src="../../content/shared/libs/react-0.12.2.js"></script>
+  <script src="../../content/shared/libs/classnames-2.2.0.js"></script>
   <script src="../../content/shared/libs/lodash-3.9.3.js"></script>
   <script src="../../content/shared/libs/backbone-1.2.1.js"></script>
 
   <!-- test dependencies -->
   <script src="../shared/vendor/mocha-2.2.5.js"></script>
   <script src="../shared/vendor/chai-3.0.0.js"></script>
   <script src="../shared/vendor/sinon-1.16.1.js"></script>
   <script>
--- a/browser/components/loop/test/karma/karma.coverage.desktop.js
+++ b/browser/components/loop/test/karma/karma.coverage.desktop.js
@@ -7,16 +7,17 @@ module.exports = function(config) {
   "use strict";
 
   var baseConfig = require("./karma.conf.base.js")(config);
 
   // List of files / patterns to load in the browser.
   baseConfig.files = baseConfig.files.concat([
     "content/libs/l10n.js",
     "content/shared/libs/react-0.12.2.js",
+    "content/shared/libs/classnames-2.2.0.js",
     "content/shared/libs/lodash-3.9.3.js",
     "content/shared/libs/backbone-1.2.1.js",
     "test/shared/vendor/*.js",
     "test/karma/head.js", // Stub out DOM event listener due to races.
     "content/shared/js/utils.js",
     "content/shared/js/models.js",
     "content/shared/js/mixins.js",
     "content/shared/js/actions.js",
--- a/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
+++ b/browser/components/loop/test/karma/karma.coverage.shared_standalone.js
@@ -9,16 +9,17 @@ module.exports = function(config) {
   var baseConfig = require("./karma.conf.base.js")(config);
 
   // List of files / patterns to load in the browser.
   baseConfig.files = baseConfig.files.concat([
     "standalone/content/libs/l10n-gaia-02ca67948fe8.js",
     "content/shared/libs/lodash-3.9.3.js",
     "content/shared/libs/backbone-1.2.1.js",
     "content/shared/libs/react-0.12.2.js",
+    "content/shared/libs/classnames-2.2.0.js",
     "content/shared/libs/sdk.js",
     "test/shared/vendor/*.js",
     "test/karma/head.js", // Add test fixture container
     "content/shared/js/utils.js",
     "content/shared/js/store.js",
     "content/shared/js/models.js",
     "content/shared/js/mixins.js",
     "content/shared/js/crypto.js",
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -25,16 +25,17 @@
       var args = Array.slice(arguments);
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
   </script>
 
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.12.2.js"></script>
+  <script src="../../content/shared/libs/classnames-2.2.0.js"></script>
   <script src="../../content/shared/libs/lodash-3.9.3.js"></script>
   <script src="../../content/shared/libs/backbone-1.2.1.js"></script>
   <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
 
   <!-- test dependencies -->
   <script src="vendor/mocha-2.2.5.js"></script>
   <script src="vendor/chai-3.0.0.js"></script>
   <script src="vendor/chai-as-promised-5.1.0.js"></script>
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -26,16 +26,17 @@
       var args = Array.slice(arguments);
       caughtWarnings.push(args);
       consoleWarn.apply(console, args);
     };
   </script>
 
   <!-- libs -->
   <script src="../../content/shared/libs/react-0.12.2.js"></script>
+  <script src="../../content/shared/libs/classnames-2.2.0.js"></script>
   <script src="../../content/shared/libs/lodash-3.9.3.js"></script>
   <script src="../../content/shared/libs/backbone-1.2.1.js"></script>
   <script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
   <!-- test dependencies -->
   <script src="../shared/vendor/mocha-2.2.5.js"></script>
   <script src="../shared/vendor/chai-3.0.0.js"></script>
   <script src="../shared/vendor/sinon-1.16.1.js"></script>
   <script src="../shared/sdk_mock.js"></script>
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -23,16 +23,17 @@
       });
     </script>
 
     <div id="main"></div>
     <div id="results"></div>
     <script src="fake-mozLoop.js"></script>
     <script src="fake-l10n.js"></script>
     <script src="../content/shared/libs/react-0.12.2.js"></script>
+    <script src="../content/shared/libs/classnames-2.2.0.js"></script>
     <script src="../content/shared/libs/lodash-3.9.3.js"></script>
     <script src="../content/shared/libs/backbone-1.2.1.js"></script>
     <script src="../content/shared/js/actions.js"></script>
     <script src="../content/shared/js/utils.js"></script>
     <script src="../content/shared/js/models.js"></script>
     <script src="../content/shared/js/mixins.js"></script>
     <script src="../content/shared/js/validate.js"></script>
     <script src="../content/shared/js/dispatcher.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -551,17 +551,17 @@
       var width = this.props.width;
 
       // make room for a 1-pixel border on each edge
       if (this.props.dashed) {
         height += 2;
         width += 2;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         React.createElement("div", {className: "example"}, 
           React.createElement("h3", {id: this.makeId()}, 
             this.props.summary, 
             React.createElement("a", {href: this.makeId("#")}, " ¶")
           ), 
           React.createElement("div", {className: "comp"}, 
             React.createElement(Frame, {className: cx({ dashed: this.props.dashed }), 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -551,17 +551,17 @@
       var width = this.props.width;
 
       // make room for a 1-pixel border on each edge
       if (this.props.dashed) {
         height += 2;
         width += 2;
       }
 
-      var cx = React.addons.classSet;
+      var cx = classNames;
       return (
         <div className="example">
           <h3 id={this.makeId()}>
             {this.props.summary}
             <a href={this.makeId("#")}>&nbsp;¶</a>
           </h3>
           <div className="comp">
             <Frame className={cx({ dashed: this.props.dashed })}
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -76,16 +76,17 @@
       <li><a href="about:license#apache">Apache License 2.0</a></li>
       <li><a href="about:license#apple">Apple License</a></li>
       <li><a href="about:license#apple-mozilla">Apple/Mozilla NPRuntime License</a></li>
       <li><a href="about:license#arm">ARM License</a></li>
       <li><a href="about:license#backbone">Backbone License</a></li>
       <li><a href="about:license#bspatch">bspatch License</a></li>
       <li><a href="about:license#cairo">Cairo Component Licenses</a></li>
       <li><a href="about:license#chromium">Chromium License</a></li>
+      <li><a href="about:license#classnames">classnames License</a></li>
       <li><a href="about:license#codemirror">CodeMirror License</a></li>
       <li><a href="about:license#cubic-bezier">cubic-bezier License</a></li>
       <li><a href="about:license#d3">D3 License</a></li>
       <li><a href="about:license#dagre-d3">Dagre-D3 License</a></li>
       <li><a href="about:license#dtoa">dtoa License</a></li>
       <li><a href="about:license#hunspell-nl">Dutch Spellchecking Dictionary License</a></li>
       <li><a href="about:license#hunspell-ee">Estonian Spellchecking Dictionary License</a></li>
       <li><a href="about:license#expat">Expat License</a></li>
@@ -2715,16 +2716,46 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTI
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 </pre>
 
 
     <hr>
 
+    <h1><a id="classnames"></a>classnames License</h1>
+
+    <p>This license applies to the file <span class="path">
+      browser/components/loop/content/shared/libs/classnames-*.js</span>.
+    </p>
+
+<pre>
+Copyright (c) 2015 Jed Watson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+</pre>
+
+    <hr>
+
     <h1><a id="codemirror"></a>CodeMirror License</h1>
 
     <p>This license applies to all files in
       <span class="path">devtools/client/sourceeditor/codemirror</span> and
       to specified files in the <span class="path">devtools/client/sourceeditor/test/</span>:
     </p>
     <ul>
       <li><span class="path">cm_comment_test.js</span></li>