Bug 1074676 - Allow deleting a Loop room. r=Standard8 a=loop-only
authorNicolas Perriault <nperriault@gmail.com>
Mon, 03 Nov 2014 21:53:56 +0000
changeset 233793 a6f34832bb46ee670b45bd8b17b98711f9f9a54b
parent 233792 e0cd0e35ec15ab0676982c3e2f61cf488dd2a2c1
child 233794 27553a98329521c575e4cf86ab4e14a47d9b4853
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8, loop-only
bugs1074676
milestone35.0a2
Bug 1074676 - Allow deleting a Loop room. r=Standard8 a=loop-only
browser/components/loop/LoopRooms.jsm
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/shared/css/panel.css
browser/components/loop/content/shared/img/icons-16x16.svg
browser/components/loop/content/shared/img/svg/checkmark-16x16.svg
browser/components/loop/content/shared/img/svg/copy-16x16.svg
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/roomListStore.js
browser/components/loop/jar.mn
browser/components/loop/test/desktop-local/panel_test.js
browser/components/loop/test/shared/roomListStore_test.js
browser/components/loop/test/xpcshell/test_looprooms.js
browser/components/loop/ui/ui-showcase.css
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- 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