Merge pull request #384 from NodeBB/pulling

Move chats to a separate slideout menu on the right side

    Switched to the Pulling library
    Moved nav menu to the left side, as is normal elsewhere
    Added chat button on right side
    Unread icon on it shows unread chats
    Added slideout menu on right for only chats
    Both sides will slide open and everything
    Switched to drawer menus instead of the previous reveal style menus
    Restyle menu toggle buttons
main
Peter Jaszkowiak 7 years ago committed by GitHub
commit 0ba6304c0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,4 @@
#menu, .header {
.header, .slideout-menu {
.notification-list {
overflow-x: hidden;
overflow-y: auto;
@ -314,7 +314,7 @@
font-family: @font-family-sans-serif;
}
#mobile-menu [component="notifications/icon"].unread-count:after, .slideout-menu .unread-count:after {
.slideout-menu .unread-count:after {
position: relative;
left: -6px;
top: -7px;

@ -1,15 +1,7 @@
.slideout-menu {
position: fixed;
left: auto;
top: 0;
bottom: 0;
right: 0;
z-index: 0;
width: 256px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
display: none;
}
/* iPhone 5 and other such devices */
@ -58,9 +50,29 @@
-webkit-overflow-scrolling: touch;
}
#menu {
.navbar-toggle {
padding: 10px 17px;
margin: 0;
line-height: 30px;
border: none;
.header & .notification-icon {
left: auto;
right: 7px;
top: 10px;
&.unread-count::after {
position: static;
}
}
}
#menu {
padding-top: 100px;
}
.slideout-menu {
z-index: 10000 !important;
background-color: #1D1F20;
background-image: linear-gradient(145deg, #1D1F20, #404348);
@ -128,14 +140,17 @@
}
}
.menu-section .chat-list, .menu-section .notification-list-mobile {
.menu-section {
.chat-list, .notification-list-mobile {
.user-link {
display: inline;
}
.unread {
background-color: inherit;
a:after {
}
}
.chat-list .unread .room-name::after,
.notification-list-mobile .unread a::after {
content: "new";
text-transform: uppercase;
color: #FFF;
@ -147,7 +162,6 @@
border-radius: 5px;
}
}
}
.counter {
font-style: normal;
@ -204,11 +218,6 @@
}
}
.slideout-panel {
position: relative;
z-index: 1;
}
.slideout-open,
.slideout-open body,
.slideout-open .slideout-panel {
@ -216,13 +225,31 @@
overflow-y: hidden !important;
}
.slideout-open .slideout-menu {
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: .3;
}
}
.subnav-is-opened .main-nav__secondary-nav {
display: block;
animation: fade 250ms ease-in-out both;
}
.slideout-open {
overflow-y: hidden;
height: 100%;
.slideout-open .slideout-panel {
&::after {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1040;
content: ' ';
opacity: 0;
}
}
.menu-profile {

@ -19,7 +19,7 @@ body {
}
@media (max-width: @screen-xs-max) {
#panel.slideout-panel {
.slideout-panel {
min-height: 100vh;
}
}

@ -1,484 +0,0 @@
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Slideout=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
/**
* Module dependencies
*/
var decouple = require('decouple');
var Emitter = require('emitter');
/**
* Privates
*/
var scrollTimeout;
var scrolling = false;
var doc = window.document;
var html = doc.documentElement;
var msPointerSupported = window.navigator.msPointerEnabled;
var touch = {
'start': msPointerSupported ? 'MSPointerDown' : 'touchstart',
'move': msPointerSupported ? 'MSPointerMove' : 'touchmove',
'end': msPointerSupported ? 'MSPointerUp' : 'touchend'
};
var prefix = (function prefix() {
var regex = /^(Webkit|Khtml|Moz|ms|O)(?=[A-Z])/;
var styleDeclaration = doc.getElementsByTagName('script')[0].style;
for (var prop in styleDeclaration) {
if (regex.test(prop)) {
return '-' + prop.match(regex)[0].toLowerCase() + '-';
}
}
// Nothing found so far? Webkit does not enumerate over the CSS properties of the style object.
// However (prop in style) returns the correct value, so we'll have to test for
// the precence of a specific property
if ('WebkitOpacity' in styleDeclaration) { return '-webkit-'; }
if ('KhtmlOpacity' in styleDeclaration) { return '-khtml-'; }
return '';
}());
function extend(destination, from) {
for (var prop in from) {
if (from[prop]) {
destination[prop] = from[prop];
}
}
return destination;
}
function inherits(child, uber) {
child.prototype = extend(child.prototype || {}, uber.prototype);
}
/**
* Slideout constructor
*/
function Slideout(options) {
options = options || {};
// Sets default values
this._startOffsetX = 0;
this._currentOffsetX = 0;
this._opening = false;
this._moved = false;
this._opened = false;
this._preventOpen = false;
this._touch = options.touch === undefined ? true : options.touch && true;
// Sets panel
this.panel = options.panel;
this.menu = options.menu;
// Sets classnames
if(this.panel.className.search('slideout-panel') === -1) { this.panel.className += ' slideout-panel'; }
if(this.menu.className.search('slideout-menu') === -1) { this.menu.className += ' slideout-menu'; }
// Sets options
this._fx = options.fx || 'ease';
this._duration = parseInt(options.duration, 10) || 300;
this._tolerance = parseInt(options.tolerance, 10) || 70;
this._padding = this._translateTo = parseInt(options.padding, 10) || 256;
this._orientation = options.side === 'right' ? -1 : 1;
this._translateTo *= this._orientation;
// Init touch events
if (this._touch) {
this._initTouchEvents();
}
}
/**
* Inherits from Emitter
*/
inherits(Slideout, Emitter);
/**
* Opens the slideout menu.
*/
Slideout.prototype.open = function() {
var self = this;
this.emit('beforeopen');
if (html.className.search('slideout-open') === -1) { html.className += ' slideout-open'; }
this._setTransition();
this._translateXTo(this._translateTo);
this._opened = true;
setTimeout(function() {
self.panel.style.transition = self.panel.style['-webkit-transition'] = '';
self.emit('open');
}, this._duration + 50);
return this;
};
/**
* Closes slideout menu.
*/
Slideout.prototype.close = function() {
var self = this;
if (!this.isOpen() && !this._opening) { return this; }
this.emit('beforeclose');
this._setTransition();
this._translateXTo(0);
this._opened = false;
setTimeout(function() {
html.className = html.className.replace(/ slideout-open/, '');
self.panel.style.transition = self.panel.style['-webkit-transition'] = self.panel.style[prefix + 'transform'] = self.panel.style.transform = '';
self.emit('close');
}, this._duration + 50);
return this;
};
/**
* Toggles (open/close) slideout menu.
*/
Slideout.prototype.toggle = function() {
return this.isOpen() ? this.close() : this.open();
};
/**
* Returns true if the slideout is currently open, and false if it is closed.
*/
Slideout.prototype.isOpen = function() {
return this._opened;
};
/**
* Translates panel and updates currentOffset with a given X point
*/
Slideout.prototype._translateXTo = function(translateX) {
this._currentOffsetX = translateX;
this.panel.style[prefix + 'transform'] = this.panel.style.transform = 'translate3d(' + translateX + 'px, 0, 0)';
};
/**
* Set transition properties
*/
Slideout.prototype._setTransition = function() {
this.panel.style[prefix + 'transition'] = this.panel.style.transition = prefix + 'transform ' + this._duration + 'ms ' + this._fx;
};
/**
* Initializes touch event
*/
Slideout.prototype._initTouchEvents = function() {
var self = this;
/**
* Decouple scroll event
*/
this._onScrollFn = decouple(doc, 'scroll', function() {
if (!self._moved) {
clearTimeout(scrollTimeout);
scrolling = true;
scrollTimeout = setTimeout(function() {
scrolling = false;
}, 250);
}
});
/**
* Prevents touchmove event if slideout is moving
*/
this._preventMove = function(eve) {
if (self._moved) {
eve.preventDefault();
}
};
doc.addEventListener(touch.move, this._preventMove);
/**
* Resets values on touchstart
*/
this._resetTouchFn = function(eve) {
if (typeof eve.touches === 'undefined') { return; }
self._moved = false;
self._opening = false;
self._startOffsetX = eve.touches[0].pageX;
self._preventOpen = (!self._touch || (!self.isOpen() && self.menu.clientWidth !== 0));
};
this.panel.addEventListener(touch.start, this._resetTouchFn);
/**
* Resets values on touchcancel
*/
this._onTouchCancelFn = function() {
self._moved = false;
self._opening = false;
};
this.panel.addEventListener('touchcancel', this._onTouchCancelFn);
/**
* Toggles slideout on touchend
*/
this._onTouchEndFn = function() {
if (self._moved) {
(self._opening && Math.abs(self._currentOffsetX) > self._tolerance) ? self.open() : self.close();
}
self._moved = false;
};
this.panel.addEventListener(touch.end, this._onTouchEndFn);
/**
* Translates panel on touchmove
*/
this._onTouchMoveFn = function(eve) {
self.emit('touchmove', eve.target);
if (scrolling || self._preventOpen || typeof eve.touches === 'undefined') { return; }
var dif_x = eve.touches[0].clientX - self._startOffsetX;
var translateX = self._currentOffsetX = dif_x;
if (Math.abs(translateX) > self._padding) { return; }
if (Math.abs(dif_x) > 20) {
self._opening = true;
var oriented_dif_x = dif_x * self._orientation;
if (self._opened && oriented_dif_x > 0 || !self._opened && oriented_dif_x < 0) { return; }
if (oriented_dif_x <= 0) {
translateX = dif_x + self._padding * self._orientation;
self._opening = false;
}
if (!self._moved && html.className.search('slideout-open') === -1) {
html.className += ' slideout-open';
}
self.panel.style[prefix + 'transform'] = self.panel.style.transform = 'translate3d(' + translateX + 'px, 0, 0)';
self.emit('translate', translateX, eve.target);
self._moved = true;
}
};
this.panel.addEventListener(touch.move, this._onTouchMoveFn);
};
Slideout.prototype.enableTouch = function() {
this._touch = true;
return this;
};
Slideout.prototype.disableTouch = function() {
this._touch = false;
return this;
};
Slideout.prototype.destroy = function() {
// Close before clean
this.close();
// Remove event listeners
doc.removeEventListener(touch.move, this._preventMove);
this.panel.removeEventListener(touch.start, this._resetTouchFn);
this.panel.removeEventListener('touchcancel', this._onTouchCancelFn);
this.panel.removeEventListener(touch.end, this._onTouchEndFn);
this.panel.removeEventListener(touch.move, this._onTouchMoveFn);
doc.removeEventListener('scroll', this._onScrollFn);
// Remove methods
this.open = this.close = function() {};
// Return the instance so it can be easily dereferenced
return this;
};
/**
* Expose Slideout
*/
module.exports = Slideout;
},{"decouple":2,"emitter":3}],2:[function(require,module,exports){
'use strict';
var requestAnimFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
}());
function decouple(node, event, fn) {
var eve,
tracking = false;
function captureEvent(e) {
eve = e;
track();
}
function track() {
if (!tracking) {
requestAnimFrame(update);
tracking = true;
}
}
function update() {
fn.call(node, eve);
tracking = false;
}
node.addEventListener(event, captureEvent, false);
return captureEvent;
}
/**
* Expose decouple
*/
module.exports = decouple;
},{}],3:[function(require,module,exports){
"use strict";
var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
exports.__esModule = true;
/**
* Creates a new instance of Emitter.
* @class
* @returns {Object} Returns a new instance of Emitter.
* @example
* // Creates a new instance of Emitter.
* var Emitter = require('emitter');
*
* var emitter = new Emitter();
*/
var Emitter = (function () {
function Emitter() {
_classCallCheck(this, Emitter);
}
/**
* Adds a listener to the collection for the specified event.
* @memberof! Emitter.prototype
* @function
* @param {String} event - The event name.
* @param {Function} listener - A listener function to add.
* @returns {Object} Returns an instance of Emitter.
* @example
* // Add an event listener to "foo" event.
* emitter.on('foo', listener);
*/
Emitter.prototype.on = function on(event, listener) {
// Use the current collection or create it.
this._eventCollection = this._eventCollection || {};
// Use the current collection of an event or create it.
this._eventCollection[event] = this._eventCollection[event] || [];
// Appends the listener into the collection of the given event
this._eventCollection[event].push(listener);
return this;
};
/**
* Adds a listener to the collection for the specified event that will be called only once.
* @memberof! Emitter.prototype
* @function
* @param {String} event - The event name.
* @param {Function} listener - A listener function to add.
* @returns {Object} Returns an instance of Emitter.
* @example
* // Will add an event handler to "foo" event once.
* emitter.once('foo', listener);
*/
Emitter.prototype.once = function once(event, listener) {
var self = this;
function fn() {
self.off(event, fn);
listener.apply(this, arguments);
}
fn.listener = listener;
this.on(event, fn);
return this;
};
/**
* Removes a listener from the collection for the specified event.
* @memberof! Emitter.prototype
* @function
* @param {String} event - The event name.
* @param {Function} listener - A listener function to remove.
* @returns {Object} Returns an instance of Emitter.
* @example
* // Remove a given listener.
* emitter.off('foo', listener);
*/
Emitter.prototype.off = function off(event, listener) {
var listeners = undefined;
// Defines listeners value.
if (!this._eventCollection || !(listeners = this._eventCollection[event])) {
return this;
}
listeners.forEach(function (fn, i) {
if (fn === listener || fn.listener === listener) {
// Removes the given listener.
listeners.splice(i, 1);
}
});
// Removes an empty event collection.
if (listeners.length === 0) {
delete this._eventCollection[event];
}
return this;
};
/**
* Execute each item in the listener collection in order with the specified data.
* @memberof! Emitter.prototype
* @function
* @param {String} event - The name of the event you want to emit.
* @param {...Object} data - Data to pass to the listeners.
* @returns {Object} Returns an instance of Emitter.
* @example
* // Emits the "foo" event with 'param1' and 'param2' as arguments.
* emitter.emit('foo', 'param1', 'param2');
*/
Emitter.prototype.emit = function emit(event) {
var _this = this;
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
var listeners = undefined;
// Defines listeners value.
if (!this._eventCollection || !(listeners = this._eventCollection[event])) {
return this;
}
// Clone listeners
listeners = listeners.slice(0);
listeners.forEach(function (fn) {
return fn.apply(_this, args);
});
return this;
};
return Emitter;
})();
/**
* Exports Emitter
*/
exports["default"] = Emitter;
module.exports = exports["default"];
},{}]},{},[1])(1)
});

@ -1,6 +1,6 @@
"use strict";
/*globals ajaxify, config, utils, app, socket, Slideout, NProgress*/
/*globals ajaxify, config, utils, app, socket, NProgress*/
$(document).ready(function() {
var env = utils.findBootstrapEnvironment();
@ -113,76 +113,102 @@ $(document).ready(function() {
return;
}
$('#menu').removeClass('hidden');
var slideout = new Slideout({
'panel': document.getElementById('panel'),
'menu': document.getElementById('menu'),
'padding': 256,
'tolerance': 70,
'side': 'right'
});
require(['pulling', 'storage'], function (Pulling, Storage) {
// initialization
var guest = !app.user || !parseInt(app.user.uid, 10);
var legacy = !!Storage.getItem('persona:menus:legacy-layout');
var margin = window.innerWidth;
if (env !== 'xs') {
slideout.disableTouch();
if (legacy) {
$('#mobile-menu').removeClass('pull-left');
$('#mobile-chats').addClass('pull-left');
}
$('#mobile-menu').on('click', function() {
slideout.toggle();
var navSlideout = Pulling.create({
panel: document.getElementById('panel'),
menu: document.getElementById('menu'),
width: 256,
margin: margin,
side: legacy ? 'right' : 'left',
});
$('#menu').removeClass('hidden');
$('#menu a').on('click', function() {
slideout.close();
var chatsSlideout;
if (!guest) {
chatsSlideout = Pulling.create({
panel: document.getElementById('panel'),
menu: document.getElementById('chats-menu'),
width: 256,
margin: margin,
side: legacy ? 'left' : 'right',
});
$('#chats-menu').removeClass('hidden');
}
$(window).on('resize action:ajaxify.start', function() {
slideout.close();
$('.account .cover').css('top', $('[component="navbar"]').height());
});
// all menus
function openingMenuAndLoad() {
openingMenu();
loadNotificationsAndChat();
function closeOnClick() {
navSlideout.close();
if (chatsSlideout) { chatsSlideout.close(); }
}
function openingMenu() {
$('#header-menu').css({
'top': $(window).scrollTop() + 'px',
'position': 'absolute'
});
function onBeforeOpen() {
document.documentElement.classList.add('slideout-open');
}
function loadNotificationsAndChat() {
require(['chat', 'notifications'], function(chat, notifications) {
chat.loadChatsDropdown($('#menu [data-section="chats"] ul'));
notifications.loadNotifications($('#menu [data-section="notifications"] ul'));
});
function onClose() {
document.documentElement.classList.remove('slideout-open');
$('#panel').off('click', closeOnClick);
}
slideout.on('open', openingMenuAndLoad);
slideout.on('touchmove', function(target) {
var $target = $(target);
if ($target.length && ($target.is('table') || $target.parents('table').length || $target.is('code') || $target.parents('code').length || $target.hasClass('preventSlideOut') || $target.parents('.preventSlideOut').length)) {
slideout._preventOpen = true;
}
$(window).on('resize action:ajaxify.start', function () {
navSlideout.close();
if (chatsSlideout) { chatsSlideout.close(); }
$('.account .cover').css('top', $('[component="navbar"]').height());
});
navSlideout
.ignore('code, code *, .preventSlideout, .preventSlideout *')
.on('closed', onClose)
.on('beforeopen', onBeforeOpen)
.on('opened', function () {
$('#panel').one('click', closeOnClick);
});
slideout.on('close', function() {
$('#header-menu').css({
'top': '0px',
'position': 'fixed'
if (!guest) {
chatsSlideout
.ignore('code, code *, .preventSlideout, .preventSlideout *')
.on('closed', onClose)
.on('beforeopen', onBeforeOpen)
.on('opened', function () {
$('#panel').one('click', closeOnClick);
});
$('.slideout-open').removeClass('slideout-open');
$('.topic .pagination-block').css({ bottom: 0 });
}
// left slideout navigation menu
$('#mobile-menu').on('click', function () {
navSlideout.enable().toggle();
});
slideout.on('beforeopen', function() {
var paginator = $('.topic .pagination-block')[0];
if (paginator) {
paginator.style.bottom = 0; // to trigger reflow
paginator.style.bottom = (paginator.getBoundingClientRect().bottom - window.innerHeight).toString() + 'px';
function loadNotifications() {
require(['notifications'], function(notifications) {
notifications.loadNotifications($('#menu [data-section="notifications"] ul'));
});
}
navSlideout.on('opened', loadNotifications);
if (!guest) {
navSlideout.on('beforeopen', function () {
chatsSlideout.close();
chatsSlideout.disable();
}).on('closed', function () {
chatsSlideout.enable();
});
}
$('#menu [data-section="navigation"] ul').html($('#main-nav').html() + ($('#search-menu').html() || '') + ($('#logged-out-menu').html() || ''));
@ -192,8 +218,58 @@ $(document).ready(function() {
socket.on('event:user_status_change', function(data) {
if (parseInt(data.uid, 10) === app.user.uid) {
app.updateUserStatus($('#menu [component="user/status"]'), data.status);
slideout.close();
navSlideout.close();
}
});
// right slideout chats menu
function loadChats() {
require(['chat'], function (chat) {
chat.loadChatsDropdown($('#chats-menu .chat-list'));
});
}
if (!guest) {
$('#mobile-chats').removeClass('hidden').on('click', function() {
navSlideout.close();
chatsSlideout.enable().toggle();
});
$('#chats-menu').on('click', 'li[data-roomid]', function () {
chatsSlideout.close();
});
chatsSlideout
.on('opened', loadChats)
.on('beforeopen', function () {
navSlideout.close().disable();
})
.on('closed', function () {
navSlideout.enable();
});
}
// add a checkbox in the user settings page
// so users can swap the sides the menus appear on
function setupSetting() {
if (ajaxify.data.template['account/settings'] && !document.getElementById('persona:menus:legacy-layout')) {
$('<div class="well checkbox"><label><input type="checkbox" id="persona:menus:legacy-layout"/><strong>Switch which side each mobile menu is on</strong></label></div>')
.appendTo('#content .account > .row > div:first-child')
.find('input')
.prop('checked', Storage.getItem('persona:menus:legacy-layout', 'true'))
.change(function (e) {
if (e.target.checked) {
Storage.setItem('persona:menus:legacy-layout', 'true');
} else {
Storage.removeItem('persona:menus:legacy-layout');
}
});
}
}
$(window).on('action:ajaxify.end', setupSetting);
setupSetting();
});
}

@ -39,6 +39,7 @@
"url": "https://github.com/psychobunny/nodebb-theme-persona/issues"
},
"dependencies": {
"striptags": "^3.1.0"
"striptags": "^3.1.0",
"pulling": "^1.1.0"
}
}

@ -14,9 +14,11 @@
"lib/persona.js",
"lib/modules/nprogress.js",
"lib/modules/autohidingnavbar.min.js",
"lib/modules/slideout.min.js",
"lib/modules/quickreply.js"
],
"modules": {
"pulling.js": "node_modules/pulling/build/pulling-drawer.js"
},
"acpScripts": [
"lib/admin.js"
]

@ -25,7 +25,7 @@
</head>
<body class="{bodyClass} skin-{config.bootswatchSkin}">
<nav id="menu" class="hidden">
<nav id="menu" class="slideout-menu hidden">
<div class="menu-profile">
<!-- IF user.uid -->
<!-- IF user.picture -->
@ -56,7 +56,10 @@
<ul class="menu-section-list notification-list-mobile" component="notifications/list"></ul>
<p class="menu-section-list"><a href="{relative_path}/notifications">[[notifications:see_all]]</a></p>
</section>
<!-- ENDIF config.loggedIn -->
</nav>
<nav id="chats-menu" class="slideout-menu hidden">
<!-- IF config.loggedIn -->
<section class="menu-section" data-section="chats">
<h3 class="menu-section-title">
[[global:header.chats]]
@ -67,7 +70,7 @@
<!-- ENDIF config.loggedIn -->
</nav>
<main id="panel">
<main id="panel" class="slideout-panel">
<nav class="navbar navbar-default navbar-fixed-top header" id="header-menu" component="navbar">
<div class="container">
<!-- IMPORT partials/menu.tpl -->

@ -1,9 +1,11 @@
<div class="navbar-header">
<button type="button" class="navbar-toggle" id="mobile-menu">
<button type="button" class="navbar-toggle pull-left" id="mobile-menu">
<span component="notifications/icon" class="notification-icon fa fa-fw fa-bell-o" data-content="0"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<i class="fa fa-lg fa-bars"></i>
</button>
<button type="button" class="navbar-toggle hidden" id="mobile-chats">
<span component="chat/icon" class="notification-icon fa fa-fw fa-comments" data-content="0"></span>
<i class="fa fa-lg fa-comment-o"></i>
</button>
<!-- IF brand:logo -->

Loading…
Cancel
Save