Merge fx-team to mozilla-central a=merge
authorWes Kocher <wkocher@mozilla.com>
Mon, 03 Nov 2014 19:11:00 -0800
changeset 238093 1782c0c6aee79fa037bb64040d5b6d3ca6182be1
parent 238089 4e0a21777c55bbd40bb0b11f499cc848113d42b9 (current diff)
parent 238092 f07f3b28d761561b49bb615e1ee97c04448db9c1 (diff)
child 238099 5dde8ea48fef69066cc66f1d4b620071537ea274
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.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
Merge fx-team to mozilla-central a=merge
browser/components/loop/content/shared/img/svg/checkmark-16x16.svg
browser/components/loop/content/shared/img/svg/copy-16x16.svg
--- a/browser/app/macbuild/Contents/MacOS-files.in
+++ b/browser/app/macbuild/Contents/MacOS-files.in
@@ -1,10 +1,9 @@
 /*.app/***
 /*.dylib
 /certutil
 /firefox-bin
 /gtest/***
 /pk12util
 /ssltunnel
-/webapprt-stub
 /xpcshell
 /XUL
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -101,16 +101,21 @@ const checkForParticipantsUpdate = funct
  *
  * Each method that is a member of this class requires the last argument to be a
  * callback Function. MozLoopAPI will cause things to break if this invariant is
  * violated. You'll notice this as well in the documentation for each method.
  */
 let LoopRoomsInternal = {
   rooms: new Map(),
 
+  get sessionType() {
+    return MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
+                                        LOOP_SESSION_TYPE.GUEST;
+  },
+
   /**
    * Fetch a list of rooms that the currently registered user is a member of.
    *
    * @param {String}   [version] If set, we will fetch a list of changed rooms since
    *                             `version`. Optional.
    * @param {Function} callback  Function that will be invoked once the operation
    *                             finished. The first argument passed will be an
    *                             `Error` object or `null`. The second argument will
@@ -126,20 +131,18 @@ let LoopRoomsInternal = {
       yield MozLoopService.promiseRegisteredWithServers();
 
       if (!gDirty) {
         callback(null, [...this.rooms.values()]);
         return;
       }
 
       // Fetch the rooms from the server.
-      let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
-                        LOOP_SESSION_TYPE.GUEST;
       let url = "/rooms" + (version ? "?version=" + encodeURIComponent(version) : "");
-      let response = yield MozLoopService.hawkRequest(sessionType, url, "GET");
+      let response = yield MozLoopService.hawkRequest(this.sessionType, url, "GET");
       let roomsList = JSON.parse(response.body);
       if (!Array.isArray(roomsList)) {
         throw new Error("Missing array of rooms in response.");
       }
 
       for (let room of roomsList) {
         // See if we already have this room in our cache.
         let orig = this.rooms.get(room.roomToken);
@@ -183,19 +186,17 @@ let LoopRoomsInternal = {
     let needsUpdate = !("participants" in room);
     if (!gDirty && !needsUpdate) {
       // Dirty flag is not set AND the necessary data is available, so we can
       // simply return the room.
       callback(null, room);
       return;
     }
 
-    let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
-                      LOOP_SESSION_TYPE.GUEST;
-    MozLoopService.hawkRequest(sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
+    MozLoopService.hawkRequest(this.sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
       .then(response => {
         let data = JSON.parse(response.body);
 
         room.roomToken = roomToken;
         checkForParticipantsUpdate(room, data);
         extend(room, data);
 
         // Remove the `currSize` for posterity.
@@ -222,20 +223,17 @@ let LoopRoomsInternal = {
    */
   create: function(room, callback) {
     if (!("roomName" in room) || !("expiresIn" in room) ||
         !("roomOwner" in room) || !("maxSize" in room)) {
       callback(new Error("Missing required property to create a room"));
       return;
     }
 
-    let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
-                      LOOP_SESSION_TYPE.GUEST;
-
-    MozLoopService.hawkRequest(sessionType, "/rooms", "POST", room)
+    MozLoopService.hawkRequest(this.sessionType, "/rooms", "POST", room)
       .then(response => {
         let data = JSON.parse(response.body);
         extend(room, data);
         // Do not keep this value - it is a request to the server.
         delete room.expiresIn;
         this.rooms.set(room.roomToken, room);
 
         eventEmitter.emit("add", room);
@@ -248,16 +246,38 @@ let LoopRoomsInternal = {
       roomToken: roomToken,
       type: "room"
     };
 
     MozLoopService.openChatWindow(windowData);
   },
 
   /**
+   * Deletes a room.
+   *
+   * @param {String}   roomToken The room token.
+   * @param {Function} callback  Function that will be invoked once the operation
+   *                             finished. The first argument passed will be an
+   *                             `Error` object or `null`.
+   */
+  delete: function(roomToken, callback) {
+    // XXX bug 1092954: Before deleting a room, the client should check room
+    //     membership and forceDisconnect() all current participants.
+    let room = this.rooms.get(roomToken);
+    let url = "/rooms/" + encodeURIComponent(roomToken);
+    MozLoopService.hawkRequest(this.sessionType, url, "DELETE")
+      .then(response => {
+        this.rooms.delete(roomToken);
+        eventEmitter.emit("delete", room);
+        callback(null, room);
+      }, error => callback(error)).catch(error => callback(error));
+  },
+
+
+  /**
    * Callback used to indicate changes to rooms data on the LoopServer.
    *
    * @param {String} version   Version number assigned to this change set.
    * @param {String} channelID Notification channel identifier.
    */
   onNotification: function(version, channelID) {
     gDirty = true;
     this.getAll(version, () => {});
@@ -268,17 +288,17 @@ Object.freeze(LoopRoomsInternal);
 /**
  * Public Loop Rooms API.
  *
  * LoopRooms implements the EventEmitter interface by exposing three methods -
  * `on`, `once` and `off` - to subscribe to events.
  * At this point the following events may be subscribed to:
  *  - 'add[:{room-id}]':    A new room object was successfully added to the data
  *                          store.
- *  - 'remove[:{room-id}]': A room was successfully removed from the data store.
+ *  - 'delete[:{room-id}]': A room was successfully removed from the data store.
  *  - 'update[:{room-id}]': A room object was successfully updated with changed
  *                          properties in the data store.
  *  - 'joined[:{room-id}]': A participant joined a room.
  *  - 'left[:{room-id}]':   A participant left a room.
  *
  * See the internal code for the API documentation.
  */
 this.LoopRooms = {
@@ -293,16 +313,20 @@ this.LoopRooms = {
   create: function(options, callback) {
     return LoopRoomsInternal.create(options, callback);
   },
 
   open: function(roomToken) {
     return LoopRoomsInternal.open(roomToken);
   },
 
+  delete: function(roomToken, callback) {
+    return LoopRoomsInternal.delete(roomToken, callback);
+  },
+
   promise: function(method, ...params) {
     return new Promise((resolve, reject) => {
       this[method](...params, (error, result) => {
         if (error) {
           reject(error);
         } else {
           resolve(result);
         }
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -462,70 +462,82 @@ loop.panel = (function(_, mozL10n) {
     }
   });
 
   /**
    * Room list entry.
    */
   var RoomEntry = React.createClass({displayName: 'RoomEntry',
     propTypes: {
-      openRoom: React.PropTypes.func.isRequired,
-      room:     React.PropTypes.instanceOf(loop.store.Room).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      room:       React.PropTypes.instanceOf(loop.store.Room).isRequired
     },
 
     getInitialState: function() {
       return { urlCopied: false };
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       return (nextProps.room.ctime > this.props.room.ctime) ||
         (nextState.urlCopied !== this.state.urlCopied);
     },
 
-    handleClickRoom: function(event) {
+    handleClickRoomUrl: function(event) {
       event.preventDefault();
-      this.props.openRoom(this.props.room);
+      this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
+        roomToken: this.props.room.roomToken
+      }));
     },
 
     handleCopyButtonClick: function(event) {
       event.preventDefault();
       navigator.mozLoop.copyString(this.props.room.roomUrl);
       this.setState({urlCopied: true});
     },
 
+    handleDeleteButtonClick: function(event) {
+      event.preventDefault();
+      // XXX We should prompt end user for confirmation; see bug 1092953.
+      this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
+        roomToken: this.props.room.roomToken
+      }));
+    },
+
     handleMouseLeave: function(event) {
       this.setState({urlCopied: false});
     },
 
     _isActive: function() {
       // XXX bug 1074679 will implement this properly
       return this.props.room.participants.length > 0;
     },
 
     render: function() {
       var room = this.props.room;
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
       var copyButtonClasses = React.addons.classSet({
-        'copy-link': true,
-        'checked': this.state.urlCopied
+        "copy-link": true,
+        "checked": this.state.urlCopied
       });
 
       return (
         React.DOM.div({className: roomClasses, onMouseLeave: this.handleMouseLeave}, 
           React.DOM.h2(null, 
             React.DOM.span({className: "room-notification"}), 
-              room.roomName, 
+            room.roomName, 
             React.DOM.button({className: copyButtonClasses, 
-              onClick: this.handleCopyButtonClick})
+              onClick: this.handleCopyButtonClick}), 
+            React.DOM.button({className: "delete-link", 
+              onClick: this.handleDeleteButtonClick})
           ), 
           React.DOM.p(null, 
-            React.DOM.a({ref: "room", href: "#", onClick: this.handleClickRoom}, 
+            React.DOM.a({href: "#", onClick: this.handleClickRoomUrl}, 
               room.roomUrl
             )
           )
         )
       );
     }
   });
 
@@ -576,34 +588,32 @@ loop.panel = (function(_, mozL10n) {
 
     handleCreateButtonClick: function() {
       this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
         nameTemplate: mozL10n.get("rooms_default_room_name_template"),
         roomOwner: this.props.userDisplayName
       }));
     },
 
-    openRoom: function(room) {
-      this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
-        roomToken: room.roomToken
-      }));
-    },
-
     render: function() {
       if (this.state.error) {
         // XXX Better end user reporting of errors.
         console.error("RoomList error", this.state.error);
       }
 
       return (
         React.DOM.div({className: "rooms"}, 
           React.DOM.h1(null, this._getListHeading()), 
           React.DOM.div({className: "room-list"}, 
             this.state.rooms.map(function(room, i) {
-              return RoomEntry({key: i, room: room, openRoom: this.openRoom});
+              return RoomEntry({
+                key: room.roomToken, 
+                dispatcher: this.props.dispatcher, 
+                room: room}
+              );
             }, this)
           ), 
           React.DOM.p(null, 
             React.DOM.button({className: "btn btn-info", 
                     onClick: this.handleCreateButtonClick, 
                     disabled: this._hasPendingOperation()}, 
               mozL10n.get("rooms_new_room_button_label")
             )
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -462,70 +462,82 @@ loop.panel = (function(_, mozL10n) {
     }
   });
 
   /**
    * Room list entry.
    */
   var RoomEntry = React.createClass({
     propTypes: {
-      openRoom: React.PropTypes.func.isRequired,
-      room:     React.PropTypes.instanceOf(loop.store.Room).isRequired
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      room:       React.PropTypes.instanceOf(loop.store.Room).isRequired
     },
 
     getInitialState: function() {
       return { urlCopied: false };
     },
 
     shouldComponentUpdate: function(nextProps, nextState) {
       return (nextProps.room.ctime > this.props.room.ctime) ||
         (nextState.urlCopied !== this.state.urlCopied);
     },
 
-    handleClickRoom: function(event) {
+    handleClickRoomUrl: function(event) {
       event.preventDefault();
-      this.props.openRoom(this.props.room);
+      this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
+        roomToken: this.props.room.roomToken
+      }));
     },
 
     handleCopyButtonClick: function(event) {
       event.preventDefault();
       navigator.mozLoop.copyString(this.props.room.roomUrl);
       this.setState({urlCopied: true});
     },
 
+    handleDeleteButtonClick: function(event) {
+      event.preventDefault();
+      // XXX We should prompt end user for confirmation; see bug 1092953.
+      this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({
+        roomToken: this.props.room.roomToken
+      }));
+    },
+
     handleMouseLeave: function(event) {
       this.setState({urlCopied: false});
     },
 
     _isActive: function() {
       // XXX bug 1074679 will implement this properly
       return this.props.room.participants.length > 0;
     },
 
     render: function() {
       var room = this.props.room;
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
       var copyButtonClasses = React.addons.classSet({
-        'copy-link': true,
-        'checked': this.state.urlCopied
+        "copy-link": true,
+        "checked": this.state.urlCopied
       });
 
       return (
         <div className={roomClasses} onMouseLeave={this.handleMouseLeave}>
           <h2>
             <span className="room-notification" />
-              {room.roomName}
+            {room.roomName}
             <button className={copyButtonClasses}
-              onClick={this.handleCopyButtonClick}/>
+              onClick={this.handleCopyButtonClick} />
+            <button className="delete-link"
+              onClick={this.handleDeleteButtonClick} />
           </h2>
           <p>
-            <a ref="room" href="#" onClick={this.handleClickRoom}>
+            <a href="#" onClick={this.handleClickRoomUrl}>
               {room.roomUrl}
             </a>
           </p>
         </div>
       );
     }
   });
 
@@ -576,34 +588,32 @@ loop.panel = (function(_, mozL10n) {
 
     handleCreateButtonClick: function() {
       this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
         nameTemplate: mozL10n.get("rooms_default_room_name_template"),
         roomOwner: this.props.userDisplayName
       }));
     },
 
-    openRoom: function(room) {
-      this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
-        roomToken: room.roomToken
-      }));
-    },
-
     render: function() {
       if (this.state.error) {
         // XXX Better end user reporting of errors.
         console.error("RoomList error", this.state.error);
       }
 
       return (
         <div className="rooms">
           <h1>{this._getListHeading()}</h1>
           <div className="room-list">{
             this.state.rooms.map(function(room, i) {
-              return <RoomEntry key={i} room={room} openRoom={this.openRoom} />;
+              return <RoomEntry
+                key={room.roomToken}
+                dispatcher={this.props.dispatcher}
+                room={room}
+              />;
             }, this)
           }</div>
           <p>
             <button className="btn btn-info"
                     onClick={this.handleCreateButtonClick}
                     disabled={this._hasPendingOperation()}>
               {mozL10n.get("rooms_new_room_button_label")}
             </button>
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -217,47 +217,58 @@ body {
   text-decoration: none;
 }
 
 .room-list > .room-entry > p > a:hover {
   opacity: 1;
   text-decoration: underline;
 }
 
-.room-list > .room-entry > h2 > .copy-link {
+@keyframes drop-and-fade-in {
+  0%   {opacity: 0; top: -15px;}
+  25%  {opacity: 0; top: -15px;}
+  100% {opacity: 1; top: 0px;}
+}
+
+.room-list > .room-entry > h2 > button {
   display: inline-block;
+  position: relative;
   width: 24px;
   height: 24px;
   border: none;
-  margin: .1em .5em; /* relative to _this_ line's font, not the document's */
+  margin: .1em .1em .1em .5em; /* relative to _this_ line's font, not the document's */
   background-color: transparent;  /* override browser default for button tags */
+  top: -15px;
 }
 
-@keyframes drop-and-fade-in {
-  from { opacity: 0; transform: translateY(-10px); }
-  to { opacity: 100; transform: translateY(0); }
+.room-list > .room-entry:hover > h2 > button {
+  animation: drop-and-fade-in 0.250s;
+  animation-fill-mode: forwards;
+  cursor: pointer;
 }
 
 .room-list > .room-entry:hover > h2 > .copy-link {
-  background: transparent url(../img/svg/copy-16x16.svg);
-  cursor: pointer;
-  animation: drop-and-fade-in 0.4s;
-  animation-fill-mode: forwards;
+  background-image: url(../img/icons-16x16.svg#copy);
+}
+
+.room-list > .room-entry:hover > h2 > .delete-link {
+  background-image: url(../img/icons-16x16.svg#trash);
 }
 
 /* scale this up to 1.1x and then back to the original size */
 @keyframes pulse {
   0%, 100% { transform: scale(1.0); }
-  50% { transform: scale(1.1); }
+  50%      { transform: scale(1.1); }
 }
 
 .room-list > .room-entry > h2 > .copy-link.checked {
-  background: transparent url(../img/svg/checkmark-16x16.svg);
-  animation: pulse .250s;
+  background: transparent url(../img/icons-16x16.svg#checkmark);
+  animation: pulse .150s;
   animation-timing-function: ease-in-out;
+  top: 0px;
 }
 
 .room-list > .room-entry > h2 {
   display: inline-block;
 }
 
 /* keep the various room-entry row pieces aligned with each other */
 .room-list > .room-entry > h2 > button,
--- a/browser/components/loop/content/shared/img/icons-16x16.svg
+++ b/browser/components/loop/content/shared/img/icons-16x16.svg
@@ -82,41 +82,75 @@ use[id$="-red"] {
     c-0.511,0.511-1.339,0.511-1.85,0c-0.511-0.511-0.511-1.339,0-1.85c0.511-0.511,1.339-0.511,1.85,0
     C4.733,2.823,4.733,3.652,4.222,4.163z"/>
   <path id="unblock-shape" fill-rule="evenodd" clip-rule="evenodd" d="M8,16c-4.418,0-8-3.582-8-8c0-4.418,3.582-8,8-8
     c4.418,0,8,3.582,8,8C16,12.418,12.418,16,8,16z M8,2.442C4.911,2.442,2.408,4.931,2.408,8c0,3.069,2.504,5.557,5.592,5.557
     S13.592,11.069,13.592,8C13.592,4.931,11.089,2.442,8,2.442z"/>
   <path id="video-shape" fill-rule="evenodd" clip-rule="evenodd" d="M14.9,3.129l-3.476,3.073V3.873c0-0.877-0.663-1.587-1.482-1.587
     H1.482C0.663,2.286,0,2.996,0,3.873v8.254c0,0.877,0.663,1.587,1.482,1.587h8.461c0.818,0,1.482-0.711,1.482-1.587V9.762
     l3.476,3.073c0.3,0.321,0.714,0.416,1.1,0.331V2.798C15.614,2.713,15.2,2.808,14.9,3.129z"/>
+  <g id="copy-shape">
+    <circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8" cy="8" r="8"/>
+    <path fill-rule="evenodd" clip-rule="evenodd" fill="none"
+          stroke="#FFFFFF" stroke-width="0.75" stroke-miterlimit="10"
+          d="M10.815,6.286H7.556c-0.164,0-0.296,0.128-0.296,0.286v5.143C7.259,11.872,7.392,12,7.556,12h4.148
+             C11.867,12,12,11.872,12,11.714V7.429L10.815,6.286z
+             M8.741,6.275V5.143L7.556,4H7.528C6.509,4,4.593,4,4.593,4H4.296
+             C4.133,4,4,4.128,4,4.286v5.143c0,0.158,0.133,0.286,0.296,0.286H7.25V6.561c0-0.158,0.133-0.286,0.296-0.286H8.741z"/>
+    <polygon fill-rule="evenodd" clip-rule="evenodd"
+             fill="#FFFFFF" points="10.222,8 10.222,6.857 11.407,8"/>
+    <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
+             points="6.963,5.714 6.963,4.571 8.148,5.714"/>
+  </g>
+  <g id="checkmark-shape">
+    <circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8"
+            cy="8" r="8"/>
+    <path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
+          d="M7.236,12L12,5.007L10.956,4L7.224,9.465l-2.14-2.326L4,8.146L7.236,12z"/>
+  </g>
+  <g id="trash-shape">
+    <circle fill-rule="evenodd" clip-rule="evenodd" fill="#D74345" cx="8" cy="8" r="8"/>
+    <path fill="#FFFFFF" d="M12,5.79c0-0.742-0.537-1.344-1.2-1.344h-0.583V4.121c0-0.713-0.516-1.29-1.152-1.29h-2.13
+      c-0.636,0-1.152,0.578-1.152,1.29v0.324H5.2C4.537,4.446,4,5.048,4,5.79v0.898h0.687l0.508,5.438
+      c0.054,0.585,0.543,1.044,1.114,1.044h3.38c0.57,0,1.06-0.458,1.114-1.043l0.509-5.439H12V5.79z M6.407,4.264V4.165
+      c0-0.375,0.271-0.678,0.606-0.678h1.974c0.334,0,0.606,0.304,0.606,0.678v0.099c0,0.063-0.01,0.123-0.025,0.181H6.432
+      C6.417,4.387,6.407,4.328,6.407,4.264z M10.057,12.056c-0.019,0.197-0.188,0.363-0.368,0.363h-3.38
+      c-0.182,0-0.35-0.166-0.368-0.363L5.44,6.687h5.12L10.057,12.056z"/>
+    <rect x="7.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/>
+    <polyline fill="#FFFFFF" points="9.25,7.542 8.75,7.542 8.75,11.542 9.25,11.542  "/>
+    <rect x="6.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/>
+  </g>
 </defs>
 <use id="audio"               xlink:href="#audio-shape"/>
 <use id="audio-hover"         xlink:href="#audio-shape"/>
 <use id="audio-active"        xlink:href="#audio-shape"/>
 <use id="block"               xlink:href="#block-shape"/>
 <use id="block-red"           xlink:href="#block-shape"/>
 <use id="block-hover"         xlink:href="#block-shape"/>
 <use id="block-active"        xlink:href="#block-shape"/>
 <use id="contacts"            xlink:href="#contacts-shape"/>
 <use id="contacts-hover"      xlink:href="#contacts-shape"/>
 <use id="contacts-active"     xlink:href="#contacts-shape"/>
+<use id="copy"                xlink:href="#copy-shape"/>
+<use id="checkmark"           xlink:href="#checkmark-shape"/>
 <use id="google"              xlink:href="#google-shape"/>
 <use id="google-hover"        xlink:href="#google-shape"/>
 <use id="google-active"       xlink:href="#google-shape"/>
 <use id="history"             xlink:href="#history-shape"/>
 <use id="history-hover"       xlink:href="#history-shape"/>
 <use id="history-active"      xlink:href="#history-shape"/>
 <use id="precall"             xlink:href="#precall-shape"/>
 <use id="precall-hover"       xlink:href="#precall-shape"/>
 <use id="precall-active"      xlink:href="#precall-shape"/>
 <use id="settings"            xlink:href="#settings-shape"/>
 <use id="settings-hover"      xlink:href="#settings-shape"/>
 <use id="settings-active"     xlink:href="#settings-shape"/>
 <use id="tag"                 xlink:href="#tag-shape"/>
 <use id="tag-hover"           xlink:href="#tag-shape"/>
 <use id="tag-active"          xlink:href="#tag-shape"/>
+<use id="trash"               xlink:href="#trash-shape"/>
 <use id="unblock"             xlink:href="#unblock-shape"/>
 <use id="unblock-hover"       xlink:href="#unblock-shape"/>
 <use id="unblock-active"      xlink:href="#unblock-shape"/>
 <use id="video"               xlink:href="#video-shape"/>
 <use id="video-hover"         xlink:href="#video-shape"/>
 <use id="video-active"        xlink:href="#video-shape"/>
 </svg>
deleted file mode 100644
--- a/browser/components/loop/content/shared/img/svg/checkmark-16x16.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
-     xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-     viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
-  <circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8"
-          cy="8" r="8"/>
-  <path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
-        d="M7.236,12L12,5.007L10.956,4L7.224,9.465l-2.14-2.326L4,8.146L7.236,12z"/>
-</svg>
deleted file mode 100644
--- a/browser/components/loop/content/shared/img/svg/copy-16x16.svg
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
-     xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-     viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
-  <circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8" cy="8"
-           r="8"/>
-  <g>
-    <g>
-      <g>
-        <path fill-rule="evenodd" clip-rule="evenodd" fill="none"
-              stroke="#FFFFFF" stroke-width="0.75" stroke-miterlimit="10"
-              d="M10.815,6.286H7.556c-0.164,0-0.296,0.128-0.296,0.286v5.143C7.259,11.872,7.392,12,7.556,12h4.148
-                 C11.867,12,12,11.872,12,11.714V7.429L10.815,6.286z
-                 M8.741,6.275V5.143L7.556,4H7.528C6.509,4,4.593,4,4.593,4H4.296
-                 C4.133,4,4,4.128,4,4.286v5.143c0,0.158,0.133,0.286,0.296,0.286H7.25V6.561c0-0.158,0.133-0.286,0.296-0.286H8.741z"/>
-      </g>
-    </g>
-    <g>
-      <polygon fill-rule="evenodd" clip-rule="evenodd"
-               fill="#FFFFFF" points="10.222,8 10.222,6.857 11.407,8"/>
-    </g>
-    <g>
-      <polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
-               points="6.963,5.714 6.963,4.571 8.148,5.714"/>
-    </g>
-  </g>
-</svg>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -154,16 +154,32 @@ loop.shared.actions = (function() {
      * Rooms creation error.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     CreateRoomError: Action.define("createRoomError", {
       error: Error
     }),
 
     /**
+     * Deletes a room.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    DeleteRoom: Action.define("deleteRoom", {
+      roomToken: String
+    }),
+
+    /**
+     * Room deletion error.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    DeleteRoomError: Action.define("deleteRoomError", {
+      error: Error
+    }),
+
+    /**
      * Retrieves room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     GetAllRooms: Action.define("getAllRooms", {
     }),
 
     /**
      * An error occured while trying to fetch the room list.
--- a/browser/components/loop/content/shared/js/roomListStore.js
+++ b/browser/components/loop/content/shared/js/roomListStore.js
@@ -66,16 +66,18 @@ loop.store = loop.store || {};
     if (!options.mozLoop) {
       throw new Error("Missing option mozLoop");
     }
     this._mozLoop = options.mozLoop;
 
     this._dispatcher.register(this, [
       "createRoom",
       "createRoomError",
+      "deleteRoom",
+      "deleteRoomError",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
       "updateRoomList"
     ]);
   }
 
   RoomListStore.prototype = _.extend({
@@ -134,17 +136,17 @@ loop.store = loop.store || {};
 
     /**
      * Registers mozLoop.rooms events.
      */
     startListeningToRoomEvents: function() {
       // Rooms event registration
       this._mozLoop.rooms.on("add", this._onRoomAdded.bind(this));
       this._mozLoop.rooms.on("update", this._onRoomUpdated.bind(this));
-      this._mozLoop.rooms.on("remove", this._onRoomRemoved.bind(this));
+      this._mozLoop.rooms.on("delete", this._onRoomRemoved.bind(this));
     },
 
     /**
      * Local proxy helper to dispatch an action.
      *
      * @param {Action} action The action to dispatch.
      */
     _dispatchAction: function(action) {
@@ -176,30 +178,29 @@ loop.store = loop.store || {};
         roomList: this._storeState.rooms.map(function(room) {
           return room.roomToken === updatedRoomData.roomToken ?
                  updatedRoomData : room;
         })
       }));
     },
 
     /**
-     * Executed when a room is removed.
+     * Executed when a room is deleted.
      *
      * @param {String} eventName       The event name (unused).
      * @param {Object} removedRoomData The removed room data.
      */
     _onRoomRemoved: function(eventName, removedRoomData) {
       this._dispatchAction(new sharedActions.UpdateRoomList({
         roomList: this._storeState.rooms.filter(function(room) {
           return room.roomToken !== removedRoomData.roomToken;
         })
       }));
     },
 
-
     /**
      * Maps and sorts the raw room list received from the mozLoop API.
      *
      * @param  {Array} rawRoomList Raw room list.
      * @return {Array}
      */
     _processRoomList: function(rawRoomList) {
       if (!rawRoomList) {
@@ -262,17 +263,17 @@ loop.store = loop.store || {};
         roomOwner: actionData.roomOwner,
         maxSize:   this.maxRoomCreationSize,
         expiresIn: this.defaultExpiresIn
       };
 
       this._mozLoop.rooms.create(roomCreationData, function(err) {
         this.setStoreState({pendingCreation: false});
         if (err) {
-         this._dispatchAction(new sharedActions.CreateRoomError({error: err}));
+          this._dispatchAction(new sharedActions.CreateRoomError({error: err}));
         }
       }.bind(this));
     },
 
     /**
      * Executed when a room creation error occurs.
      *
      * @param {sharedActions.CreateRoomError} actionData The action data.
@@ -280,16 +281,38 @@ loop.store = loop.store || {};
     createRoomError: function(actionData) {
       this.setStoreState({
         error: actionData.error,
         pendingCreation: false
       });
     },
 
     /**
+     * Creates a new room.
+     *
+     * @param {sharedActions.DeleteRoom} actionData The action data.
+     */
+    deleteRoom: function(actionData) {
+      this._mozLoop.rooms.delete(actionData.roomToken, function(err) {
+        if (err) {
+         this._dispatchAction(new sharedActions.DeleteRoomError({error: err}));
+        }
+      }.bind(this));
+    },
+
+    /**
+     * Executed when a room deletion error occurs.
+     *
+     * @param {sharedActions.DeleteRoomError} actionData The action data.
+     */
+    deleteRoomError: function(actionData) {
+      this.setStoreState({error: actionData.error});
+    },
+
+    /**
      * Gather the list of all available rooms from the MozLoop API.
      */
     getAllRooms: function() {
       this.setStoreState({pendingInitialRetrieval: true});
       this._mozLoop.rooms.getAll(null, function(err, rawRoomList) {
         var action;
 
         this.setStoreState({pendingInitialRetrieval: false});
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -44,18 +44,16 @@ browser.jar:
   content/browser/loop/shared/img/video-inverse-14x14.png       (content/shared/img/video-inverse-14x14.png)
   content/browser/loop/shared/img/video-inverse-14x14@2x.png    (content/shared/img/video-inverse-14x14@2x.png)
   content/browser/loop/shared/img/dropdown-inverse.png          (content/shared/img/dropdown-inverse.png)
   content/browser/loop/shared/img/dropdown-inverse@2x.png       (content/shared/img/dropdown-inverse@2x.png)
   content/browser/loop/shared/img/svg/glyph-settings-16x16.svg  (content/shared/img/svg/glyph-settings-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-account-16x16.svg   (content/shared/img/svg/glyph-account-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-signin-16x16.svg    (content/shared/img/svg/glyph-signin-16x16.svg)
   content/browser/loop/shared/img/svg/glyph-signout-16x16.svg   (content/shared/img/svg/glyph-signout-16x16.svg)
-  content/browser/loop/shared/img/svg/copy-16x16.svg            (content/shared/img/svg/copy-16x16.svg)
-  content/browser/loop/shared/img/svg/checkmark-16x16.svg       (content/shared/img/svg/checkmark-16x16.svg)
   content/browser/loop/shared/img/audio-call-avatar.svg         (content/shared/img/audio-call-avatar.svg)
   content/browser/loop/shared/img/beta-ribbon.svg               (content/shared/img/beta-ribbon.svg)
   content/browser/loop/shared/img/icons-10x10.svg               (content/shared/img/icons-10x10.svg)
   content/browser/loop/shared/img/icons-14x14.svg               (content/shared/img/icons-14x14.svg)
   content/browser/loop/shared/img/icons-16x16.svg               (content/shared/img/icons-16x16.svg)
 
   # Shared scripts
   content/browser/loop/shared/js/actions.js           (content/shared/js/actions.js)
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -634,79 +634,131 @@ describe("loop.panel", function() {
         sinon.assert.calledOnce(notifications.errorL10n);
         sinon.assert.calledWithExactly(notifications.errorL10n,
                                        "unable_retrieve_url");
       });
     });
   });
 
   describe("loop.panel.RoomEntry", function() {
-    var buttonNode, roomData, roomEntry, roomStore, dispatcher;
+    var dispatcher, roomData;
 
     beforeEach(function() {
       dispatcher = new loop.Dispatcher();
       roomData = {
         roomToken: "QzBbvGmIZWU",
         roomUrl: "http://sample/QzBbvGmIZWU",
         roomName: "Second Room Name",
         maxSize: 2,
         participants: [
           { displayName: "Alexis", account: "alexis@example.com",
             roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
           { displayName: "Adam",
             roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
         ],
         ctime: 1405517418
       };
-      roomStore = new loop.store.Room(roomData);
-      roomEntry = mountRoomEntry();
-      buttonNode = roomEntry.getDOMNode().querySelector("button.copy-link");
-    });
-
-    function mountRoomEntry() {
-      return TestUtils.renderIntoDocument(loop.panel.RoomEntry({
-        openRoom: sandbox.stub(),
-        room: roomStore
-      }));
-    }
-
-    it("should not display copy-link button by default", function() {
-      expect(buttonNode).to.not.equal(null);
     });
 
-    it("should copy the URL when the click event fires", function() {
-      TestUtils.Simulate.click(buttonNode);
+    function mountRoomEntry(props) {
+      return TestUtils.renderIntoDocument(loop.panel.RoomEntry(props));
+    }
+
+    describe("Copy button", function() {
+      var roomEntry, copyButton;
+
+      beforeEach(function() {
+        roomEntry = mountRoomEntry({
+          dispatcher: dispatcher,
+          deleteRoom: sandbox.stub(),
+          room: new loop.store.Room(roomData)
+        });
+        copyButton = roomEntry.getDOMNode().querySelector("button.copy-link");
+      });
+
+      it("should not display a copy button by default", function() {
+        expect(copyButton).to.not.equal(null);
+      });
+
+      it("should copy the URL when the click event fires", function() {
+        TestUtils.Simulate.click(copyButton);
 
-      sinon.assert.calledOnce(navigator.mozLoop.copyString);
-      sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
-        roomData.roomUrl);
-    });
+        sinon.assert.calledOnce(navigator.mozLoop.copyString);
+        sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
+          roomData.roomUrl);
+      });
+
+      it("should set state.urlCopied when the click event fires", function() {
+        TestUtils.Simulate.click(copyButton);
+
+        expect(roomEntry.state.urlCopied).to.equal(true);
+      });
 
-    it("should set state.urlCopied when the click event fires", function() {
-      TestUtils.Simulate.click(buttonNode);
+      it("should switch to displaying a check icon when the URL has been copied",
+        function() {
+          TestUtils.Simulate.click(copyButton);
+
+          expect(copyButton.classList.contains("checked")).eql(true);
+        });
 
-      expect(roomEntry.state.urlCopied).to.equal(true);
+      it("should not display a check icon after mouse leaves the entry",
+        function() {
+          var roomNode = roomEntry.getDOMNode();
+          TestUtils.Simulate.click(copyButton);
+
+          TestUtils.SimulateNative.mouseOut(roomNode);
+
+          expect(copyButton.classList.contains("checked")).eql(false);
+        });
     });
 
-    it("should switch to displaying a check icon when the URL has been copied",
-      function() {
-        TestUtils.Simulate.click(buttonNode);
+    describe("Delete button click", function() {
+      var roomEntry, deleteButton;
 
-        expect(buttonNode.classList.contains("checked")).eql(true);
+      beforeEach(function() {
+        roomEntry = mountRoomEntry({
+          dispatcher: dispatcher,
+          room: new loop.store.Room(roomData)
+        });
+        deleteButton = roomEntry.getDOMNode().querySelector("button.delete-link");
+      });
+
+      it("should not display a delete button by default", function() {
+        expect(deleteButton).to.not.equal(null);
       });
 
-    it("should not display a check icon after mouse leaves the entry",
-      function() {
-        var roomNode = roomEntry.getDOMNode();
-        TestUtils.Simulate.click(buttonNode);
+      it("should call the delete function when clicked", function() {
+        sandbox.stub(dispatcher, "dispatch");
+
+        TestUtils.Simulate.click(deleteButton);
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch,
+          new sharedActions.DeleteRoom({roomToken: roomData.roomToken}));
+      });
+    });
+
+    describe("Room URL click", function() {
+      var roomEntry;
 
-        TestUtils.SimulateNative.mouseOut(roomNode);
+      it("should dispatch an OpenRoom action", function() {
+        sandbox.stub(dispatcher, "dispatch");
+        roomEntry = mountRoomEntry({
+          dispatcher: dispatcher,
+          room: new loop.store.Room(roomData)
+        });
+        var urlLink = roomEntry.getDOMNode().querySelector("p > a");
 
-        expect(buttonNode.classList.contains("checked")).eql(false);
+        TestUtils.Simulate.click(urlLink);
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithExactly(dispatcher.dispatch,
+          new sharedActions.OpenRoom({roomToken: roomData.roomToken}));
       });
+    });
   });
 
   describe("loop.panel.RoomList", function() {
     var roomListStore, dispatcher, fakeEmail;
 
     beforeEach(function() {
       fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
@@ -770,30 +822,16 @@ describe("loop.panel", function() {
         var dispatch = sandbox.stub(dispatcher, "dispatch");
         roomListStore.setStoreState({pendingInitialRetrieval: true});
 
         var view = createTestComponent();
 
         var buttonNode = view.getDOMNode().querySelector("button[disabled]");
         expect(buttonNode).to.not.equal(null);
       });
-
-    describe("#openRoom", function() {
-      it("should dispatch an OpenRoom action", function() {
-        var view = createTestComponent();
-        var dispatch = sandbox.stub(dispatcher, "dispatch");
-
-        view.openRoom({roomToken: "42cba"});
-
-        sinon.assert.calledOnce(dispatch);
-        sinon.assert.calledWithExactly(dispatch, new sharedActions.OpenRoom({
-          roomToken: "42cba"
-        }));
-      });
-    });
   });
 
   describe('loop.panel.ToSView', function() {
 
     it("should render when the value of loop.seenToS is not set", function() {
       var view = TestUtils.renderIntoDocument(loop.panel.ToSView());
 
       TestUtils.findRenderedDOMComponentWithClass(view, "terms-service");
--- a/browser/components/loop/test/shared/roomListStore_test.js
+++ b/browser/components/loop/test/shared/roomListStore_test.js
@@ -130,19 +130,19 @@ describe("loop.store.RoomListStore", fun
 
           expect(store.getStoreState().rooms).to.have.length.of(3);
           expect(store.getStoreState().rooms.some(function(room) {
             return room.roomName === "Changed First Room Name";
           })).eql(true);
         });
       });
 
-      describe("remove", function() {
-        it("should remove a room from the list", function() {
-          fakeMozLoop.rooms.trigger("remove", "remove", {
+      describe("delete", function() {
+        it("should delete a room from the list", function() {
+          fakeMozLoop.rooms.trigger("delete", "delete", {
             roomToken: "_nxD4V4FflQ"
           });
 
           expect(store.getStoreState().rooms).to.have.length.of(2);
           expect(store.getStoreState().rooms.some(function(room) {
             return room.roomToken === "_nxD4V4FflQ";
           })).eql(false);
         });
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -257,16 +257,25 @@ add_task(function* test_errorStates() {
 
 // Test if creating a new room works as expected.
 add_task(function* test_createRoom() {
   gExpectedAdds.push(kCreateRoomProps);
   let room = yield LoopRooms.promise("create", kCreateRoomProps);
   compareRooms(room, kCreateRoomProps);
 });
 
+// Test if deleting a room works as expected.
+add_task(function* test_deleteRoom() {
+  let roomToken = "QzBbvGmIZWU";
+  let deletedRoom = yield LoopRooms.promise("delete", roomToken);
+  Assert.equal(deletedRoom.roomToken, roomToken);
+  let rooms = yield LoopRooms.promise("getAll");
+  Assert.ok(!rooms.some((room) => room.roomToken == roomToken));
+});
+
 // Test if opening a new room window works correctly.
 add_task(function* test_openRoom() {
   let openedUrl;
   Chat.open = function(contentWindow, origin, title, url) {
     openedUrl = url;
   };
 
   LoopRooms.open("fakeToken");
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -157,8 +157,29 @@
   bottom: auto;
 }
 
 .standalone .ended-conversation .remote_wrapper,
 .standalone .video-layout-wrapper {
   /* Removes the fake video image for ended conversations */
   background: none;
 }
+
+/* SVG icons showcase */
+
+.svg-icon-entry {
+  width: 180px;
+  float: left;
+}
+
+.svg-icon-entry > p {
+  float: left;
+  margin-right: .5rem;
+}
+
+.svg-icon {
+  display: inline-block;
+  width:  16px;
+  height: 16px;
+  background-repeat: no-repeat;
+  background-size: 16px 16px;
+  background-position: center;
+}
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -96,16 +96,51 @@
   var errNotifications = new loop.shared.models.NotificationCollection();
   errNotifications.add({
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry",
   });
 
+  var SVGIcon = React.createClass({displayName: 'SVGIcon',
+    render: function() {
+      return (
+        React.DOM.span({className: "svg-icon", style: {
+          "background-image": "url(/content/shared/img/icons-16x16.svg#" + this.props.shapeId + ")"
+        }})
+      );
+    }
+  });
+
+  var SVGIcons = React.createClass({displayName: 'SVGIcons',
+    shapes: [
+      "audio", "audio-hover", "audio-active", "block",
+      "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
+      "contacts-active", "copy", "checkmark", "google", "google-hover",
+      "google-active", "history", "history-hover", "history-active",
+      "precall", "precall-hover", "precall-active", "settings", "settings-hover",
+      "settings-active", "tag", "tag-hover", "tag-active", "trash", "unblock",
+      "unblock-hover", "unblock-active", "video", "video-hover", "video-active"
+    ],
+
+    render: function() {
+      return (
+        React.DOM.div({className: "svg-icon-list"}, 
+          this.shapes.map(function(shapeId, i) {
+            return React.DOM.div({className: "svg-icon-entry"}, 
+              React.DOM.p(null, SVGIcon({key: i, shapeId: shapeId})), 
+              React.DOM.p(null, shapeId)
+            );
+          }, this)
+        )
+      );
+    }
+  });
+
   var Example = React.createClass({displayName: 'Example',
     makeId: function(prefix) {
       return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
     },
 
     render: function() {
       var cx = React.addons.classSet;
       return (
@@ -485,16 +520,22 @@
           ), 
 
           Section({name: "UnsupportedDeviceView"}, 
             Example({summary: "Standalone Unsupported Device"}, 
               React.DOM.div({className: "standalone"}, 
                 UnsupportedDeviceView(null)
               )
             )
+          ), 
+
+          Section({name: "SVG icons preview"}, 
+            Example({summary: "16x16"}, 
+              SVGIcons(null)
+            )
           )
 
         )
       );
     }
   });
 
   /**
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -96,16 +96,51 @@
   var errNotifications = new loop.shared.models.NotificationCollection();
   errNotifications.add({
     level: "error",
     message: "Could Not Authenticate",
     details: "Did you change your password?",
     detailsButtonLabel: "Retry",
   });
 
+  var SVGIcon = React.createClass({
+    render: function() {
+      return (
+        <span className="svg-icon" style={{
+          "background-image": "url(/content/shared/img/icons-16x16.svg#" + this.props.shapeId + ")"
+        }} />
+      );
+    }
+  });
+
+  var SVGIcons = React.createClass({
+    shapes: [
+      "audio", "audio-hover", "audio-active", "block",
+      "block-red", "block-hover", "block-active", "contacts", "contacts-hover",
+      "contacts-active", "copy", "checkmark", "google", "google-hover",
+      "google-active", "history", "history-hover", "history-active",
+      "precall", "precall-hover", "precall-active", "settings", "settings-hover",
+      "settings-active", "tag", "tag-hover", "tag-active", "trash", "unblock",
+      "unblock-hover", "unblock-active", "video", "video-hover", "video-active"
+    ],
+
+    render: function() {
+      return (
+        <div className="svg-icon-list">{
+          this.shapes.map(function(shapeId, i) {
+            return <div className="svg-icon-entry">
+              <p><SVGIcon key={i} shapeId={shapeId} /></p>
+              <p>{shapeId}</p>
+            </div>;
+          }, this)
+        }</div>
+      );
+    }
+  });
+
   var Example = React.createClass({
     makeId: function(prefix) {
       return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
     },
 
     render: function() {
       var cx = React.addons.classSet;
       return (
@@ -487,16 +522,22 @@
           <Section name="UnsupportedDeviceView">
             <Example summary="Standalone Unsupported Device">
               <div className="standalone">
                 <UnsupportedDeviceView />
               </div>
             </Example>
           </Section>
 
+          <Section name="SVG icons preview">
+            <Example summary="16x16">
+              <SVGIcons />
+            </Example>
+          </Section>
+
         </ShowCase>
       );
     }
   });
 
   /**
    * Render components that have different styles across
    * CSS media rules in their own iframe to mimic the viewport
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -851,21 +851,17 @@ bin/libfreebl_32int64_3.so
 #endif
 #endif
 
 #ifdef MOZ_WEBAPP_RUNTIME
 [WebappRuntime]
 #ifdef XP_WIN
 @BINPATH@/webapp-uninstaller@BIN_SUFFIX@
 #endif
-#ifdef XP_MACOSX
-@APPNAME@/Contents/MacOS/webapprt-stub@BIN_SUFFIX@
-#else
 @BINPATH@/webapprt-stub@BIN_SUFFIX@
-#endif
 @BINPATH@/webapprt/webapprt.ini
 @BINPATH@/webapprt/chrome.manifest
 @BINPATH@/webapprt/chrome/webapprt@JAREXT@
 @BINPATH@/webapprt/chrome/webapprt.manifest
 @BINPATH@/webapprt/chrome/@AB_CD@@JAREXT@
 @BINPATH@/webapprt/chrome/@AB_CD@.manifest
 @BINPATH@/webapprt/components/CommandLineHandler.js
 @BINPATH@/webapprt/components/ContentPermission.js
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -87,17 +87,17 @@ class MochitestRunner(MozbuildObject):
     to hook up result parsing, etc.
     """
 
     def get_webapp_runtime_path(self):
         import mozinfo
         appname = 'webapprt-stub' + mozinfo.info.get('bin_suffix', '')
         if sys.platform.startswith('darwin'):
             appname = os.path.join(self.distdir, self.substs['MOZ_MACBUNDLE_NAME'],
-            'Contents', 'MacOS', appname)
+            'Contents', 'Resources', appname)
         else:
             appname = os.path.join(self.distdir, 'bin', appname)
         return appname
 
     def __init__(self, *args, **kwargs):
         MozbuildObject.__init__(self, *args, **kwargs)
 
         # TODO Bug 794506 remove once mach integrates with virtualenv.
--- a/testing/testsuite-targets.mk
+++ b/testing/testsuite-targets.mk
@@ -164,17 +164,17 @@ ifeq (powerpc,$(TARGET_CPU))
 	$(RUN_MOCHITEST) --setpref=dom.ipc.plugins.enabled.ppc.test.plugin=false $(IPCPLUGINS_PATH_ARG)
 endif
 else
 	$(RUN_MOCHITEST) --setpref=dom.ipc.plugins.enabled=false --test-path=dom/plugins/test
 endif
 	$(CHECK_TEST_ERROR)
 
 ifeq ($(OS_ARCH),Darwin)
-webapprt_stub_path = $(TARGET_DIST)/$(MOZ_MACBUNDLE_NAME)/Contents/MacOS/webapprt-stub$(BIN_SUFFIX)
+webapprt_stub_path = $(TARGET_DIST)/$(MOZ_MACBUNDLE_NAME)/Contents/Resources/webapprt-stub$(BIN_SUFFIX)
 endif
 ifeq ($(OS_ARCH),WINNT)
 webapprt_stub_path = $(TARGET_DIST)/bin/webapprt-stub$(BIN_SUFFIX)
 endif
 ifeq ($(MOZ_WIDGET_TOOLKIT),gtk2)
 webapprt_stub_path = $(TARGET_DIST)/bin/webapprt-stub$(BIN_SUFFIX)
 endif
 
--- a/toolkit/webapps/MacNativeApp.js
+++ b/toolkit/webapps/MacNativeApp.js
@@ -217,17 +217,18 @@ NativeApp.prototype = {
                           { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
 
     yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir),
                           { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
   }),
 
   _copyPrebuiltFiles: function(aDir) {
     let destDir = getFile(aDir, this.macOSDir);
-    let stub = getFile(this.runtimeFolder, "webapprt-stub");
+    let stub = getFile(OS.Path.join(OS.Path.dirname(this.runtimeFolder),
+                                    "Resources"), "webapprt-stub");
     stub.copyTo(destDir, "webapprt");
   },
 
   _createConfigFiles: function(aDir) {
     // ${ProfileDir}/webapp.json
     yield writeToFile(OS.Path.join(aDir, this.configJson),
                       JSON.stringify(this.webappJson));
 
--- a/toolkit/webapps/tests/test_webapp_runtime_executable_update.xul
+++ b/toolkit/webapps/tests/test_webapp_runtime_executable_update.xul
@@ -89,17 +89,17 @@ let runTest = Task.async(function*() {
   yield nativeApp.install(app, manifest);
   while (!WebappOSUtils.isLaunchable(app)) {
     yield wait(1000);
   }
   ok(true, "App launchable");
 
   let fakeInstallDir;
   if (MAC) {
-    fakeInstallDir = getFile(testAppInfo.installPath, "Contents", "MacOS");
+    fakeInstallDir = getFile(testAppInfo.installPath, "Contents", "Resources");
   } else {
     fakeInstallDir = getFile(OS.Constants.Path.profileDir, "fakeInstallDir");
     fakeInstallDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
   }
 
   let fakeAppIniFile = fakeInstallDir.clone();
   fakeAppIniFile.append("application.ini");
 
--- a/webapprt/mac/webapprt.mm
+++ b/webapprt/mac/webapprt.mm
@@ -161,17 +161,17 @@ main(int argc, char **argv)
       NSLog(@"### This Application has an old webrt. Updating it.");
       NSLog(@"### My webapprt path: %@", myWebRTPath);
 
       NSFileManager* fileClerk = [[NSFileManager alloc] init];
       NSError *errorDesc = nil;
 
       //we know the firefox path, so copy the new webapprt here
       NSString *newWebRTPath =
-        [NSString stringWithFormat: @"%@%s%s", firefoxPath, APP_MACOS_PATH,
+        [NSString stringWithFormat: @"%@%s%s", firefoxPath, APP_RESOURCES_PATH,
                                                WEBAPPRT_EXECUTABLE];
       NSLog(@"### Firefox webapprt path: %@", newWebRTPath);
       if (![fileClerk fileExistsAtPath:newWebRTPath]) {
         NSString* msg = [NSString stringWithFormat: @"This version of Firefox (%@) cannot run web applications, because it is not recent enough or damaged", firefoxVersion];
         @throw MakeException(@"Missing Web Runtime Files", msg);
       }
 
       [fileClerk removeItemAtPath: myWebRTPath error: &errorDesc];