diff --git a/framework/core/js/admin/dist/app.js b/framework/core/js/admin/dist/app.js
index 3a6e9db20..bee498d02 100644
--- a/framework/core/js/admin/dist/app.js
+++ b/framework/core/js/admin/dist/app.js
@@ -609,7 +609,7 @@ exports.System = System;
 ;
 var m = (function app(window, undefined) {
 	"use strict";
-  	var VERSION = "v0.2.1";
+  	var VERSION = "v0.2.2-rc.1";
 	function isFunction(object) {
 		return typeof object === "function";
 	}
@@ -912,22 +912,19 @@ var m = (function app(window, undefined) {
 		var nodes = cached.nodes;
 		if (!editable || editable !== $document.activeElement) {
 			if (data.$trusted) {
-				clear(nodes, cached);
-				nodes = injectHTML(parentElement, index, data);
-			}
-			//corner case: replacing the nodeValue of a text node that is a child of a textarea/contenteditable doesn't work
-			//we need to update the value property of the parent textarea or the innerHTML of the contenteditable element instead
-			else if (parentTag === "textarea") {
-				parentElement.value = data;
-			}
-			else if (editable) {
-				editable.innerHTML = data;
-			}
-			else {
-				//was a trusted string
-				if (nodes[0].nodeType === 1 || nodes.length > 1) {
-					clear(cached.nodes, cached);
-					nodes = [$document.createTextNode(data)];
+				clear(nodes, cached)
+				nodes = injectHTML(parentElement, index, data)
+			} else if (parentTag === "textarea") {
+				// <textarea> uses `value` instead of `nodeValue`.
+				parentElement.value = data
+			} else if (editable) {
+				// contenteditable nodes use `innerHTML` instead of `nodeValue`.
+				editable.innerHTML = data
+			} else {
+				// was a trusted string
+				if (nodes[0].nodeType === 1 || nodes.length > 1 || (nodes[0].nodeValue.trim && !nodes[0].nodeValue.trim())) {
+					clear(cached.nodes, cached)
+					nodes = [$document.createTextNode(data)]
 				}
 				injectTextNode(parentElement, nodes[0], index, data);
 			}
@@ -1057,7 +1054,7 @@ var m = (function app(window, undefined) {
 		//Faster to coerce to number and check for NaN
 		var key = +(data && data.attrs && data.attrs.key);
 		data = pendingRequests === 0 || forcing || cachedControllers && cachedControllers.indexOf(controller) > -1 ? data.view(controller) : {tag: "placeholder"};
-		if (data.subtree === "retain") return cached;
+		if (data.subtree === "retain") return data;
 		if (key === key) (data.attrs = data.attrs || {}).key = key;
 		updateLists(views, controllers, view, controller);
 		return data;
@@ -1072,6 +1069,7 @@ var m = (function app(window, undefined) {
 	function buildObject(data, cached, editable, parentElement, index, shouldReattach, namespace, configs) {
 		var views = [], controllers = [];
 		data = markViews(data, cached, views, controllers);
+		if (data.subtree === "retain") return cached;
 		if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.");
 		data.attrs = data.attrs || {};
 		cached.attrs = cached.attrs || {};
@@ -1190,7 +1188,11 @@ var m = (function app(window, undefined) {
 					//- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
 					else if (attrName in node && attrName !== "list" && attrName !== "style" && attrName !== "form" && attrName !== "type" && attrName !== "width" && attrName !== "height") {
 						//#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
-						if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr;
+						try {
+							if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr;
+						} catch (e) {
+							node.setAttribute(attrName, dataAttr);
+						}
 					}
 					else node.setAttribute(attrName, dataAttr);
 				}
@@ -1233,23 +1235,13 @@ var m = (function app(window, undefined) {
 			else if (cached.children.tag) unload(cached.children);
 		}
 	}
-
-	var insertAdjacentBeforeEnd = (function () {
-		var rangeStrategy = function (parentElement, data) {
-			parentElement.appendChild($document.createRange().createContextualFragment(data));
-		};
-		var insertAdjacentStrategy = function (parentElement, data) {
-			parentElement.insertAdjacentHTML("beforeend", data);
-		};
-
-		try {
-			$document.createRange().createContextualFragment('x');
-			return rangeStrategy;
-		} catch (e) {
-			return insertAdjacentStrategy;
-		}
-	})();
-
+	function appendTextFragment(parentElement, data) {
+	  try {
+	    parentElement.appendChild($document.createRange().createContextualFragment(data));
+	  } catch (e) {
+	    parentElement.insertAdjacentHTML("beforeend", data);
+	  }
+	}
 	function injectHTML(parentElement, index, data) {
 		var nextSibling = parentElement.childNodes[index];
 		if (nextSibling) {
@@ -1262,8 +1254,9 @@ var m = (function app(window, undefined) {
 			}
 			else nextSibling.insertAdjacentHTML("beforebegin", data);
 		}
-		else insertAdjacentBeforeEnd(parentElement, data);
-
+		else {
+			appendTextFragment(parentElement, data);
+		}
 		var nodes = [];
 		while (parentElement.childNodes[index] !== nextSibling) {
 			nodes.push(parentElement.childNodes[index]);
@@ -1617,8 +1610,7 @@ var m = (function app(window, undefined) {
 	}
 	function routeUnobtrusive(e) {
 		e = e || event;
-
-		if (e.ctrlKey || e.metaKey || e.which === 2) return;
+		if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return;
 
 		if (e.preventDefault) e.preventDefault();
 		else e.returnValue = false;
@@ -1626,6 +1618,8 @@ var m = (function app(window, undefined) {
 		var currentTarget = e.currentTarget || e.srcElement;
 		var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
 		while (currentTarget && currentTarget.nodeName.toUpperCase() !== "A") currentTarget = currentTarget.parentNode;
+		// clear pendingRequests because we want an immediate route change
+		pendingRequests = 0;
 		m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args);
 	}
 	function setScroll() {
@@ -1698,14 +1692,6 @@ var m = (function app(window, undefined) {
 			return propify(promise.then(resolve, reject), initialValue);
 		};
 		prop["catch"] = prop.then.bind(null, null);
-		prop["finally"] = function(callback) {
-			var _callback = function() {return m.deferred().resolve(callback()).promise;};
-			return prop.then(function(value) {
-				return propify(_callback().then(function() {return value;}), initialValue);
-			}, function(reason) {
-				return propify(_callback().then(function() {throw new Error(reason);}), initialValue);
-			});
-		};
 		return prop;
 	}
 	//Promiz.mithril.js | Zolmeister | MIT
@@ -1797,13 +1783,17 @@ var m = (function app(window, undefined) {
 				return fire();
 			}
 
-			thennable(then, function() {
-				state = RESOLVING;
-				fire();
-			}, function() {
-				state = REJECTING;
-				fire();
-			}, function() {
+			if (state === REJECTING) {
+				m.deferred.onerror(promiseValue)
+			}
+
+			thennable(then, function () {
+				state = RESOLVING
+				fire()
+			}, function () {
+				state = REJECTING
+				fire()
+			}, function () {
 				try {
 					if (state === RESOLVING && isFunction(successCallback)) {
 						promiseValue = successCallback(promiseValue);
@@ -1992,15 +1982,19 @@ var m = (function app(window, undefined) {
 					} else if (xhrOptions.type) {
 						response = new xhrOptions.type(response);
 					}
+					deferred.resolve(response)
+				} else {
+					deferred.reject(response)
 				}
 
 				deferred[e.type === "load" ? "resolve" : "reject"](response);
-			} catch (e) {
-				m.deferred.onerror(e);
+			}
+			catch (e) {
 				deferred.reject(e);
 			}
-
-			if (xhrOptions.background !== true) m.endComputation()
+			finally {
+				if (xhrOptions.background !== true) m.endComputation()
+			}
 		}
 
 		ajax(xhrOptions);
@@ -14244,7 +14238,7 @@ return jQuery;
 }).call(this);
 ;
 /* ========================================================================
- * Bootstrap: affix.js v3.3.5
+ * Bootstrap: affix.js v3.3.6
  * http://getbootstrap.com/javascript/#affix
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -14273,7 +14267,7 @@ return jQuery;
     this.checkPosition()
   }
 
-  Affix.VERSION  = '3.3.5'
+  Affix.VERSION  = '3.3.6'
 
   Affix.RESET    = 'affix affix-top affix-bottom'
 
@@ -14407,7 +14401,7 @@ return jQuery;
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: dropdown.js v3.3.5
+ * Bootstrap: dropdown.js v3.3.6
  * http://getbootstrap.com/javascript/#dropdowns
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -14427,7 +14421,7 @@ return jQuery;
     $(element).on('click.bs.dropdown', this.toggle)
   }
 
-  Dropdown.VERSION = '3.3.5'
+  Dropdown.VERSION = '3.3.6'
 
   function getParent($this) {
     var selector = $this.attr('data-target')
@@ -14459,7 +14453,7 @@ return jQuery;
       if (e.isDefaultPrevented()) return
 
       $this.attr('aria-expanded', 'false')
-      $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
+      $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
     })
   }
 
@@ -14493,7 +14487,7 @@ return jQuery;
 
       $parent
         .toggleClass('open')
-        .trigger('shown.bs.dropdown', relatedTarget)
+        .trigger($.Event('shown.bs.dropdown', relatedTarget))
     }
 
     return false
@@ -14573,7 +14567,7 @@ return jQuery;
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: modal.js v3.3.5
+ * Bootstrap: modal.js v3.3.6
  * http://getbootstrap.com/javascript/#modals
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -14607,7 +14601,7 @@ return jQuery;
     }
   }
 
-  Modal.VERSION  = '3.3.5'
+  Modal.VERSION  = '3.3.6'
 
   Modal.TRANSITION_DURATION = 300
   Modal.BACKDROP_TRANSITION_DURATION = 150
@@ -14911,7 +14905,7 @@ return jQuery;
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: tooltip.js v3.3.5
+ * Bootstrap: tooltip.js v3.3.6
  * http://getbootstrap.com/javascript/#tooltip
  * Inspired by the original jQuery.tipsy by Jason Frame
  * ========================================================================
@@ -14938,7 +14932,7 @@ return jQuery;
     this.init('tooltip', element, options)
   }
 
-  Tooltip.VERSION  = '3.3.5'
+  Tooltip.VERSION  = '3.3.6'
 
   Tooltip.TRANSITION_DURATION = 150
 
@@ -15426,7 +15420,7 @@ return jQuery;
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: transition.js v3.3.5
+ * Bootstrap: transition.js v3.3.6
  * http://getbootstrap.com/javascript/#transitions
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -17382,7 +17376,7 @@ System.register('flarum/components/BasicsPage', ['flarum/Component', 'flarum/com
 
             saveSettings(settings).then(function () {
               app.alerts.show(_this4.successAlert = new Alert({ type: 'success', children: app.translator.trans('core.admin.basics.saved_message') }));
-            })['finally'](function () {
+            })['catch'](function () {}).then(function () {
               _this4.loading = false;
               m.redraw();
             });
@@ -20676,6 +20670,10 @@ System.register('flarum/helpers/listItems', ['flarum/components/Separator', 'fla
       var active = item.component && item.component.isActive && item.component.isActive(item.props);
       var className = item.props ? item.props.itemClassName : item.itemClassName;
 
+      if (isListItem) {
+        item.props.key = item.itemName;
+      }
+
       return [isListItem ? item : m(
         'li',
         { className: classList([item.itemName ? 'item-' + item.itemName : '', className, active ? 'active' : '']),
diff --git a/framework/core/js/admin/src/components/BasicsPage.js b/framework/core/js/admin/src/components/BasicsPage.js
index def963cc9..31e064762 100644
--- a/framework/core/js/admin/src/components/BasicsPage.js
+++ b/framework/core/js/admin/src/components/BasicsPage.js
@@ -145,7 +145,8 @@ export default class BasicsPage extends Component {
       .then(() => {
         app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
       })
-      .finally(() => {
+      .catch(() => {})
+      .then(() => {
         this.loading = false;
         m.redraw();
       });
diff --git a/framework/core/js/forum/dist/app.js b/framework/core/js/forum/dist/app.js
index 2adf0e28a..1f63a9d40 100644
--- a/framework/core/js/forum/dist/app.js
+++ b/framework/core/js/forum/dist/app.js
@@ -609,7 +609,7 @@ exports.System = System;
 ;
 var m = (function app(window, undefined) {
 	"use strict";
-  	var VERSION = "v0.2.1";
+  	var VERSION = "v0.2.2-rc.1";
 	function isFunction(object) {
 		return typeof object === "function";
 	}
@@ -912,22 +912,19 @@ var m = (function app(window, undefined) {
 		var nodes = cached.nodes;
 		if (!editable || editable !== $document.activeElement) {
 			if (data.$trusted) {
-				clear(nodes, cached);
-				nodes = injectHTML(parentElement, index, data);
-			}
-			//corner case: replacing the nodeValue of a text node that is a child of a textarea/contenteditable doesn't work
-			//we need to update the value property of the parent textarea or the innerHTML of the contenteditable element instead
-			else if (parentTag === "textarea") {
-				parentElement.value = data;
-			}
-			else if (editable) {
-				editable.innerHTML = data;
-			}
-			else {
-				//was a trusted string
-				if (nodes[0].nodeType === 1 || nodes.length > 1) {
-					clear(cached.nodes, cached);
-					nodes = [$document.createTextNode(data)];
+				clear(nodes, cached)
+				nodes = injectHTML(parentElement, index, data)
+			} else if (parentTag === "textarea") {
+				// <textarea> uses `value` instead of `nodeValue`.
+				parentElement.value = data
+			} else if (editable) {
+				// contenteditable nodes use `innerHTML` instead of `nodeValue`.
+				editable.innerHTML = data
+			} else {
+				// was a trusted string
+				if (nodes[0].nodeType === 1 || nodes.length > 1 || (nodes[0].nodeValue.trim && !nodes[0].nodeValue.trim())) {
+					clear(cached.nodes, cached)
+					nodes = [$document.createTextNode(data)]
 				}
 				injectTextNode(parentElement, nodes[0], index, data);
 			}
@@ -1057,7 +1054,7 @@ var m = (function app(window, undefined) {
 		//Faster to coerce to number and check for NaN
 		var key = +(data && data.attrs && data.attrs.key);
 		data = pendingRequests === 0 || forcing || cachedControllers && cachedControllers.indexOf(controller) > -1 ? data.view(controller) : {tag: "placeholder"};
-		if (data.subtree === "retain") return cached;
+		if (data.subtree === "retain") return data;
 		if (key === key) (data.attrs = data.attrs || {}).key = key;
 		updateLists(views, controllers, view, controller);
 		return data;
@@ -1072,6 +1069,7 @@ var m = (function app(window, undefined) {
 	function buildObject(data, cached, editable, parentElement, index, shouldReattach, namespace, configs) {
 		var views = [], controllers = [];
 		data = markViews(data, cached, views, controllers);
+		if (data.subtree === "retain") return cached;
 		if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.");
 		data.attrs = data.attrs || {};
 		cached.attrs = cached.attrs || {};
@@ -1190,7 +1188,11 @@ var m = (function app(window, undefined) {
 					//- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
 					else if (attrName in node && attrName !== "list" && attrName !== "style" && attrName !== "form" && attrName !== "type" && attrName !== "width" && attrName !== "height") {
 						//#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
-						if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr;
+						try {
+							if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr;
+						} catch (e) {
+							node.setAttribute(attrName, dataAttr);
+						}
 					}
 					else node.setAttribute(attrName, dataAttr);
 				}
@@ -1233,23 +1235,13 @@ var m = (function app(window, undefined) {
 			else if (cached.children.tag) unload(cached.children);
 		}
 	}
-
-	var insertAdjacentBeforeEnd = (function () {
-		var rangeStrategy = function (parentElement, data) {
-			parentElement.appendChild($document.createRange().createContextualFragment(data));
-		};
-		var insertAdjacentStrategy = function (parentElement, data) {
-			parentElement.insertAdjacentHTML("beforeend", data);
-		};
-
-		try {
-			$document.createRange().createContextualFragment('x');
-			return rangeStrategy;
-		} catch (e) {
-			return insertAdjacentStrategy;
-		}
-	})();
-
+	function appendTextFragment(parentElement, data) {
+	  try {
+	    parentElement.appendChild($document.createRange().createContextualFragment(data));
+	  } catch (e) {
+	    parentElement.insertAdjacentHTML("beforeend", data);
+	  }
+	}
 	function injectHTML(parentElement, index, data) {
 		var nextSibling = parentElement.childNodes[index];
 		if (nextSibling) {
@@ -1262,8 +1254,9 @@ var m = (function app(window, undefined) {
 			}
 			else nextSibling.insertAdjacentHTML("beforebegin", data);
 		}
-		else insertAdjacentBeforeEnd(parentElement, data);
-
+		else {
+			appendTextFragment(parentElement, data);
+		}
 		var nodes = [];
 		while (parentElement.childNodes[index] !== nextSibling) {
 			nodes.push(parentElement.childNodes[index]);
@@ -1617,8 +1610,7 @@ var m = (function app(window, undefined) {
 	}
 	function routeUnobtrusive(e) {
 		e = e || event;
-
-		if (e.ctrlKey || e.metaKey || e.which === 2) return;
+		if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return;
 
 		if (e.preventDefault) e.preventDefault();
 		else e.returnValue = false;
@@ -1626,6 +1618,8 @@ var m = (function app(window, undefined) {
 		var currentTarget = e.currentTarget || e.srcElement;
 		var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
 		while (currentTarget && currentTarget.nodeName.toUpperCase() !== "A") currentTarget = currentTarget.parentNode;
+		// clear pendingRequests because we want an immediate route change
+		pendingRequests = 0;
 		m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args);
 	}
 	function setScroll() {
@@ -1698,14 +1692,6 @@ var m = (function app(window, undefined) {
 			return propify(promise.then(resolve, reject), initialValue);
 		};
 		prop["catch"] = prop.then.bind(null, null);
-		prop["finally"] = function(callback) {
-			var _callback = function() {return m.deferred().resolve(callback()).promise;};
-			return prop.then(function(value) {
-				return propify(_callback().then(function() {return value;}), initialValue);
-			}, function(reason) {
-				return propify(_callback().then(function() {throw new Error(reason);}), initialValue);
-			});
-		};
 		return prop;
 	}
 	//Promiz.mithril.js | Zolmeister | MIT
@@ -1797,13 +1783,17 @@ var m = (function app(window, undefined) {
 				return fire();
 			}
 
-			thennable(then, function() {
-				state = RESOLVING;
-				fire();
-			}, function() {
-				state = REJECTING;
-				fire();
-			}, function() {
+			if (state === REJECTING) {
+				m.deferred.onerror(promiseValue)
+			}
+
+			thennable(then, function () {
+				state = RESOLVING
+				fire()
+			}, function () {
+				state = REJECTING
+				fire()
+			}, function () {
 				try {
 					if (state === RESOLVING && isFunction(successCallback)) {
 						promiseValue = successCallback(promiseValue);
@@ -1992,15 +1982,19 @@ var m = (function app(window, undefined) {
 					} else if (xhrOptions.type) {
 						response = new xhrOptions.type(response);
 					}
+					deferred.resolve(response)
+				} else {
+					deferred.reject(response)
 				}
 
 				deferred[e.type === "load" ? "resolve" : "reject"](response);
-			} catch (e) {
-				m.deferred.onerror(e);
+			}
+			catch (e) {
 				deferred.reject(e);
 			}
-
-			if (xhrOptions.background !== true) m.endComputation()
+			finally {
+				if (xhrOptions.background !== true) m.endComputation()
+			}
 		}
 
 		ajax(xhrOptions);
@@ -15055,7 +15049,7 @@ var MMCQ = (function() {
 a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
 ;
 /* ========================================================================
- * Bootstrap: affix.js v3.3.5
+ * Bootstrap: affix.js v3.3.6
  * http://getbootstrap.com/javascript/#affix
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -15084,7 +15078,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
     this.checkPosition()
   }
 
-  Affix.VERSION  = '3.3.5'
+  Affix.VERSION  = '3.3.6'
 
   Affix.RESET    = 'affix affix-top affix-bottom'
 
@@ -15218,7 +15212,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: dropdown.js v3.3.5
+ * Bootstrap: dropdown.js v3.3.6
  * http://getbootstrap.com/javascript/#dropdowns
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -15238,7 +15232,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
     $(element).on('click.bs.dropdown', this.toggle)
   }
 
-  Dropdown.VERSION = '3.3.5'
+  Dropdown.VERSION = '3.3.6'
 
   function getParent($this) {
     var selector = $this.attr('data-target')
@@ -15270,7 +15264,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
       if (e.isDefaultPrevented()) return
 
       $this.attr('aria-expanded', 'false')
-      $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget)
+      $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget))
     })
   }
 
@@ -15304,7 +15298,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
 
       $parent
         .toggleClass('open')
-        .trigger('shown.bs.dropdown', relatedTarget)
+        .trigger($.Event('shown.bs.dropdown', relatedTarget))
     }
 
     return false
@@ -15384,7 +15378,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: modal.js v3.3.5
+ * Bootstrap: modal.js v3.3.6
  * http://getbootstrap.com/javascript/#modals
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -15418,7 +15412,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
     }
   }
 
-  Modal.VERSION  = '3.3.5'
+  Modal.VERSION  = '3.3.6'
 
   Modal.TRANSITION_DURATION = 300
   Modal.BACKDROP_TRANSITION_DURATION = 150
@@ -15722,7 +15716,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: tooltip.js v3.3.5
+ * Bootstrap: tooltip.js v3.3.6
  * http://getbootstrap.com/javascript/#tooltip
  * Inspired by the original jQuery.tipsy by Jason Frame
  * ========================================================================
@@ -15749,7 +15743,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
     this.init('tooltip', element, options)
   }
 
-  Tooltip.VERSION  = '3.3.5'
+  Tooltip.VERSION  = '3.3.6'
 
   Tooltip.TRANSITION_DURATION = 150
 
@@ -16237,7 +16231,7 @@ a+"'"+f+">"+a+"</a>";return""+b+c})}}).call(this);
 }(jQuery);
 ;
 /* ========================================================================
- * Bootstrap: transition.js v3.3.5
+ * Bootstrap: transition.js v3.3.6
  * http://getbootstrap.com/javascript/#transitions
  * ========================================================================
  * Copyright 2011-2015 Twitter, Inc.
@@ -18950,7 +18944,7 @@ System.register('flarum/components/ChangeEmailModal', ['flarum/components/Modal'
 
             app.session.user.save({ email: this.email() }, { errorHandler: this.onerror.bind(this) }).then(function () {
               return _this.success = true;
-            })['finally'](this.loaded.bind(this));
+            })['catch'](function () {}).then(this.loaded.bind(this));
 
             // The save method will update the cached email address on the user model...
             // But in the case of a "sudo" password prompt, we'll still want to have
@@ -22350,7 +22344,7 @@ System.register('flarum/components/ForgotPasswordModal', ['flarum/components/Mod
             }).then(function () {
               _this.success = true;
               _this.alert = null;
-            })['finally'](this.loaded.bind(this));
+            })['catch'](function () {}).then(this.loaded.bind(this));
           }
         }]);
         return ForgotPasswordModal;
@@ -24511,7 +24505,7 @@ System.register('flarum/components/NotificationList', ['flarum/Component', 'flar
               app.cache.notifications = notifications.sort(function (a, b) {
                 return b.time() - a.time();
               });
-            })['finally'](function () {
+            })['catch'](function () {}).then(function () {
               _this.loading = false;
               m.redraw();
             });
@@ -26306,8 +26300,6 @@ System.register('flarum/components/PostStreamScrubber', ['flarum/Component', 'fl
               if (time) period = time;
             });
 
-            console.log(index, visible);
-
             this.index = index;
             this.commentsIndex = commentsIndex;
             this.visible = visible;
@@ -28811,7 +28803,7 @@ System.register('flarum/components/UserBio', ['flarum/Component', 'flarum/compon
             if (user.bio() !== value) {
               this.loading = true;
 
-              user.save({ bio: value })['finally'](function () {
+              user.save({ bio: value })['catch'](function () {}).then(function () {
                 _this.loading = false;
                 m.redraw();
               });
@@ -29800,6 +29792,10 @@ System.register('flarum/helpers/listItems', ['flarum/components/Separator', 'fla
       var active = item.component && item.component.isActive && item.component.isActive(item.props);
       var className = item.props ? item.props.itemClassName : item.itemClassName;
 
+      if (isListItem) {
+        item.props.key = item.itemName;
+      }
+
       return [isListItem ? item : m(
         'li',
         { className: classList([item.itemName ? 'item-' + item.itemName : '', className, active ? 'active' : '']),
@@ -33223,7 +33219,7 @@ System.register('flarum/utils/PostControls', ['flarum/components/EditPostCompose
 
           return this['delete']().then(function () {
             _this2.discussion().removePost(_this2.id());
-          })['finally'](function () {
+          })['catch'](function () {}).then(function () {
             if (context) context.loading = false;
             m.redraw();
           });
diff --git a/framework/core/js/forum/src/components/ChangeEmailModal.js b/framework/core/js/forum/src/components/ChangeEmailModal.js
index eef95577f..d5cd4cf36 100644
--- a/framework/core/js/forum/src/components/ChangeEmailModal.js
+++ b/framework/core/js/forum/src/components/ChangeEmailModal.js
@@ -87,7 +87,8 @@ export default class ChangeEmailModal extends Modal {
 
     app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
       .then(() => this.success = true)
-      .finally(this.loaded.bind(this));
+      .catch(() => {})
+      .then(this.loaded.bind(this));
 
     // The save method will update the cached email address on the user model...
     // But in the case of a "sudo" password prompt, we'll still want to have
diff --git a/framework/core/js/forum/src/components/ForgotPasswordModal.js b/framework/core/js/forum/src/components/ForgotPasswordModal.js
index a610116ec..673b07946 100644
--- a/framework/core/js/forum/src/components/ForgotPasswordModal.js
+++ b/framework/core/js/forum/src/components/ForgotPasswordModal.js
@@ -91,6 +91,7 @@ export default class ForgotPasswordModal extends Modal {
         this.success = true;
         this.alert = null;
       })
-      .finally(this.loaded.bind(this));
+      .catch(() => {})
+      .then(this.loaded.bind(this));
   }
 }
diff --git a/framework/core/js/forum/src/components/NotificationList.js b/framework/core/js/forum/src/components/NotificationList.js
index b147a6d74..23b644344 100644
--- a/framework/core/js/forum/src/components/NotificationList.js
+++ b/framework/core/js/forum/src/components/NotificationList.js
@@ -120,7 +120,8 @@ export default class NotificationList extends Component {
         app.session.user.pushAttributes({newNotificationsCount: 0});
         app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
       })
-      .finally(() => {
+      .catch(() => {})
+      .then(() => {
         this.loading = false;
         m.redraw();
       });
diff --git a/framework/core/js/forum/src/components/UserBio.js b/framework/core/js/forum/src/components/UserBio.js
index 69ebc5297..7f1aa2b04 100644
--- a/framework/core/js/forum/src/components/UserBio.js
+++ b/framework/core/js/forum/src/components/UserBio.js
@@ -91,10 +91,12 @@ export default class UserBio extends Component {
     if (user.bio() !== value) {
       this.loading = true;
 
-      user.save({bio: value}).finally(() => {
-        this.loading = false;
-        m.redraw();
-      });
+      user.save({bio: value})
+        .catch(() => {})
+        .then(() => {
+          this.loading = false;
+          m.redraw();
+        });
     }
 
     this.editing = false;
diff --git a/framework/core/js/forum/src/utils/PostControls.js b/framework/core/js/forum/src/utils/PostControls.js
index ff6b79d1a..69a62a757 100644
--- a/framework/core/js/forum/src/utils/PostControls.js
+++ b/framework/core/js/forum/src/utils/PostControls.js
@@ -147,11 +147,14 @@ export default {
   deleteAction(context) {
     if (context) context.loading = true;
 
-    return this.delete().then(() => {
-      this.discussion().removePost(this.id());
-    }).finally(() => {
-      if (context) context.loading = false;
-      m.redraw();
-    });
+    return this.delete()
+      .then(() => {
+        this.discussion().removePost(this.id());
+      })
+      .catch(() => {})
+      .then(() => {
+        if (context) context.loading = false;
+        m.redraw();
+      });
   }
 };
diff --git a/framework/core/js/lib/helpers/listItems.js b/framework/core/js/lib/helpers/listItems.js
index 7dc3b7469..3e5ad467c 100644
--- a/framework/core/js/lib/helpers/listItems.js
+++ b/framework/core/js/lib/helpers/listItems.js
@@ -34,6 +34,10 @@ export default function listItems(items) {
     const active = item.component && item.component.isActive && item.component.isActive(item.props);
     const className = item.props ? item.props.itemClassName : item.itemClassName;
 
+    if (isListItem) {
+      item.props.key = item.itemName;
+    }
+
     return [
       isListItem
         ? item