From e466dcc6261c3bc8fbd576eebb86dcc603715cc5 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 24 Jun 2015 11:44:53 +0930 Subject: [PATCH] Significantly improve mobile UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of #137 done. - Use FastClick to make everything feel more responsive - Use transforms for animations to make them silky smooth - Style the drawer the same as the header to keep things simple - Revert to fixed composer, but allow it to be minimised - Add a separate notifications page for mobile so it’s easy to go back - Add indicator to the menu button when there are unread notifications - Close the drawer when navigating away - Make dropdowns/modals scrollable - Many other mobile tweaks and bug fixes Didn’t take much care to keep CSS clean, due to #103 --- js/bower.json | 3 +- js/forum/Gulpfile.js | 3 +- js/forum/src/components/composer-body.js | 4 +- js/forum/src/components/composer.js | 2 +- js/forum/src/components/discussion-page.js | 8 +- js/forum/src/components/form-modal.js | 2 +- js/forum/src/components/notification-list.js | 89 +++++++++++++++ js/forum/src/components/notifications-page.js | 16 +++ js/forum/src/components/post-loading.js | 2 +- js/forum/src/components/post-scrubber.js | 2 +- js/forum/src/components/reply-placeholder.js | 10 +- js/forum/src/components/search-box.js | 1 + js/forum/src/components/settings-page.js | 1 + js/forum/src/components/user-notifications.js | 82 ++------------ js/forum/src/components/user-page.js | 1 + js/forum/src/initializers/boot.js | 6 + js/forum/src/initializers/routes.js | 4 +- js/forum/src/utils/drawer.js | 13 +++ js/lib/components/back-button.js | 9 +- js/lib/components/dropdown-split.js | 2 +- js/lib/components/nav-item.js | 2 +- less/forum/composer.less | 107 ++++++++++++++---- less/forum/discussion.less | 69 ++++++++--- less/forum/index.less | 21 ++-- less/forum/notifications.less | 73 +++++++----- less/forum/signup.less | 2 +- less/forum/user.less | 37 +++++- less/lib/dropdowns.less | 106 +++++++++-------- less/lib/forms.less | 1 + less/lib/layout.less | 107 +++++++++--------- less/lib/modals.less | 31 +++-- less/lib/search.less | 8 +- src/Forum/ForumServiceProvider.php | 6 + 33 files changed, 538 insertions(+), 292 deletions(-) create mode 100644 js/forum/src/components/notification-list.js create mode 100644 js/forum/src/components/notifications-page.js create mode 100644 js/forum/src/utils/drawer.js diff --git a/js/bower.json b/js/bower.json index 9c3b50c07..7e08b7763 100644 --- a/js/bower.json +++ b/js/bower.json @@ -8,6 +8,7 @@ "moment": "~2.8.4", "color-thief": "v2.0", "mithril": "lhorie/mithril.js#next", - "loader.js": "~3.2.1" + "loader.js": "~3.2.1", + "fastclick": "~1.0.6" } } diff --git a/js/forum/Gulpfile.js b/js/forum/Gulpfile.js index 37f0b0332..1ca8aab61 100644 --- a/js/forum/Gulpfile.js +++ b/js/forum/Gulpfile.js @@ -11,7 +11,8 @@ gulp({ '../bower_components/moment/moment.js', '../bower_components/bootstrap/dist/js/bootstrap.js', '../bower_components/spin.js/spin.js', - '../bower_components/spin.js/jquery.spin.js' + '../bower_components/spin.js/jquery.spin.js', + '../bower_components/fastclick/lib/fastclick.js' ], moduleFiles: [ 'src/**/*.js', diff --git a/js/forum/src/components/composer-body.js b/js/forum/src/components/composer-body.js index a37c1ceda..71dab79be 100644 --- a/js/forum/src/components/composer-body.js +++ b/js/forum/src/components/composer-body.js @@ -25,10 +25,12 @@ export default class ComposerBody extends Component { view(className) { this.editor.props.disabled = this.loading() || !this.ready(); + var headerItems = this.headerItems().toArray(); + return m('div', {className, config: this.onload.bind(this)}, [ avatar(this.props.user, {className: 'composer-avatar'}), m('div.composer-body', [ - m('ul.composer-header', listItems(this.headerItems().toArray())), + headerItems.length ? m('ul.composer-header', listItems(headerItems)) : '', m('div.composer-editor', this.editor.view()) ]), LoadingIndicator.component({className: 'composer-loading'+(this.loading() ? ' active' : '')}) diff --git a/js/forum/src/components/composer.js b/js/forum/src/components/composer.js index b8e1b3ee7..609eb2a8f 100644 --- a/js/forum/src/components/composer.js +++ b/js/forum/src/components/composer.js @@ -273,7 +273,7 @@ class Composer extends Component { items.add('exitFullScreen', this.control({ icon: 'compress', title: 'Exit Full Screen', onclick: this.exitFullScreen.bind(this) })); } else { if (this.position() !== Composer.PositionEnum.MINIMIZED) { - items.add('minimize', this.control({ icon: 'minus minimize', title: 'Minimize', onclick: this.minimize.bind(this) })); + items.add('minimize', this.control({ icon: 'minus minimize', title: 'Minimize', onclick: this.minimize.bind(this), wrapperClass: 'back-control' })); items.add('fullScreen', this.control({ icon: 'expand', title: 'Full Screen', onclick: this.fullScreen.bind(this) })); } items.add('close', this.control({ icon: 'times', title: 'Close', onclick: this.close.bind(this) })); diff --git a/js/forum/src/components/discussion-page.js b/js/forum/src/components/discussion-page.js index 8f8d22209..25235430a 100644 --- a/js/forum/src/components/discussion-page.js +++ b/js/forum/src/components/discussion-page.js @@ -208,16 +208,16 @@ export default class DiscussionPage extends mixin(Component, evented) { items.add('controls', DropdownSplit.component({ items: this.discussion().controls(this).toArray(), - icon: 'reply', - buttonClass: 'btn btn-primary', - wrapperClass: 'primary-control' + icon: 'ellipsis-v', + className: 'primary-control', + buttonClass: 'btn btn-primary' }) ); items.add('scrubber', PostScrubber.component({ stream: this.stream, - wrapperClass: 'title-control' + className: 'title-control' }) ); diff --git a/js/forum/src/components/form-modal.js b/js/forum/src/components/form-modal.js index 847b3f1ca..54eb2915e 100644 --- a/js/forum/src/components/form-modal.js +++ b/js/forum/src/components/form-modal.js @@ -19,7 +19,7 @@ export default class FormModal extends Component { return m('div.modal-dialog', {className: options.className, config: this.element}, [ m('div.modal-content', [ - m('a[href=javascript:;].btn.btn-icon.btn-link.close.back-control', {onclick: this.hide.bind(this)}, icon('times')), + m('div.back-control.close', m('a[href=javascript:;].btn.btn-icon.btn-link', {onclick: this.hide.bind(this)}, icon('times icon'))), m('form', {onsubmit: this.onsubmit.bind(this)}, [ m('div.modal-header', m('h3.title-control', options.title)), alert ? m('div.modal-alert', alert) : '', diff --git a/js/forum/src/components/notification-list.js b/js/forum/src/components/notification-list.js new file mode 100644 index 000000000..12d39810a --- /dev/null +++ b/js/forum/src/components/notification-list.js @@ -0,0 +1,89 @@ +import Component from 'flarum/component'; +import avatar from 'flarum/helpers/avatar'; +import icon from 'flarum/helpers/icon'; +import username from 'flarum/helpers/username'; +import DropdownButton from 'flarum/components/dropdown-button'; +import ActionButton from 'flarum/components/action-button'; +import ItemList from 'flarum/utils/item-list'; +import Separator from 'flarum/components/separator'; +import LoadingIndicator from 'flarum/components/loading-indicator'; +import Discussion from 'flarum/models/discussion'; + +export default class NotificationList extends Component { + constructor(props) { + super(props); + + this.loading = m.prop(false); + this.load(); + } + + view() { + var user = this.props.user; + + var groups = []; + if (app.cache.notifications) { + var groupsObject = {}; + app.cache.notifications.forEach(notification => { + var subject = notification.subject(); + var discussion = subject instanceof Discussion ? subject : (subject.discussion && subject.discussion()); + var key = discussion ? discussion.id() : 0; + groupsObject[key] = groupsObject[key] || {discussion: discussion, notifications: []}; + groupsObject[key].notifications.push(notification); + if (groups.indexOf(groupsObject[key]) === -1) { + groups.push(groupsObject[key]); + } + }); + } + + return m('div.notification-list', [ + m('div.notifications-header', [ + m('div.primary-control', + ActionButton.component({ + className: 'btn btn-icon btn-link btn-sm', + icon: 'check', + title: 'Mark All as Read', + onclick: this.markAllAsRead.bind(this) + }) + ), + m('h4.title-control', 'Notifications') + ]), + m('div.notifications-content', groups.length + ? groups.map(group => { + return m('div.notification-group', [ + group.discussion ? m('a.notification-group-header', { + href: app.route.discussion(group.discussion), + config: m.route + }, group.discussion.title()) : m('div.notification-group-header', app.config['forum_title']), + m('ul.notification-group-list', group.notifications.map(notification => { + var NotificationComponent = app.notificationComponentRegistry[notification.contentType()]; + return NotificationComponent ? m('li', NotificationComponent.component({notification})) : ''; + })) + ]) + }) + : (!this.loading() ? m('div.no-notifications', 'No Notifications') : '')), + this.loading() ? LoadingIndicator.component() : '' + ]); + } + + load() { + if (!app.cache.notifications || app.session.user().unreadNotificationsCount()) { + var component = this; + this.loading(true); + m.redraw(); + app.store.find('notifications').then(notifications => { + app.session.user().pushData({unreadNotificationsCount: 0}); + this.loading(false); + app.cache.notifications = notifications.sort((a, b) => b.time() - a.time()); + m.redraw(); + }); + } + } + + markAllAsRead() { + app.cache.notifications.forEach(function(notification) { + if (!notification.isRead()) { + notification.save({isRead: true}); + } + }) + } +} diff --git a/js/forum/src/components/notifications-page.js b/js/forum/src/components/notifications-page.js new file mode 100644 index 000000000..cb02ad01a --- /dev/null +++ b/js/forum/src/components/notifications-page.js @@ -0,0 +1,16 @@ +import Component from 'flarum/component'; +import NotificationList from 'flarum/components/notification-list'; + +export default class NotificationsPage extends Component { + constructor(props) { + super(props); + + app.current = this; + app.history.push('notifications'); + app.drawer.hide(); + } + + view() { + return m('div', NotificationList.component()); + } +} diff --git a/js/forum/src/components/post-loading.js b/js/forum/src/components/post-loading.js index 2a9d1d868..534b55568 100644 --- a/js/forum/src/components/post-loading.js +++ b/js/forum/src/components/post-loading.js @@ -4,7 +4,7 @@ import avatar from 'flarum/helpers/avatar'; export default class PostLoadingComponent extends Component { view() { return m('div.post.comment-post.loading-post.fake-post', - m('header.post-header', avatar(), m('div.fake-text')), + m('header.post-header', avatar(), ' ', m('div.fake-text')), m('div.post-body', m('div.fake-text'), m('div.fake-text'), m('div.fake-text')) ); } diff --git a/js/forum/src/components/post-scrubber.js b/js/forum/src/components/post-scrubber.js index 032761d46..6d137a57d 100644 --- a/js/forum/src/components/post-scrubber.js +++ b/js/forum/src/components/post-scrubber.js @@ -65,7 +65,7 @@ export default class PostScrubber extends Component { var unreadPercent = unreadCount / this.count(); // @todo clean up duplication - return m('div.stream-scrubber.dropdown'+(this.disabled() ? '.disabled' : ''), {config: this.onload.bind(this)}, [ + return m('div.stream-scrubber.dropdown'+(this.disabled() ? '.disabled' : ''), {config: this.onload.bind(this), className: this.props.className}, [ m('a.btn.btn-default.dropdown-toggle[href=javascript:;][data-toggle=dropdown]', [ m('span.index', retain || formatNumber(this.visibleIndex())), ' of ', m('span.count', formatNumber(this.count())), ' posts ', icon('sort icon-glyph') diff --git a/js/forum/src/components/reply-placeholder.js b/js/forum/src/components/reply-placeholder.js index 801dab4c1..52dfed9b3 100644 --- a/js/forum/src/components/reply-placeholder.js +++ b/js/forum/src/components/reply-placeholder.js @@ -3,8 +3,14 @@ import avatar from 'flarum/helpers/avatar'; export default class ReplyPlaceholder extends Component { view() { - return m('article.post.reply-post', {onmousedown: () => this.props.discussion.replyAction(true)}, [ - m('header.post-header', avatar(app.session.user()), 'Write a Reply...'), + return m('article.post.reply-post', { + onclick: () => this.props.discussion.replyAction(true), + onmousedown: (e) => { + $(e.target).trigger('click'); + e.preventDefault(); + } + }, [ + m('header.post-header', avatar(app.session.user()), ' Write a Reply...'), ]); } } diff --git a/js/forum/src/components/search-box.js b/js/forum/src/components/search-box.js index e53b9f3fb..395431955 100644 --- a/js/forum/src/components/search-box.js +++ b/js/forum/src/components/search-box.js @@ -111,6 +111,7 @@ export default class SearchBox extends Component { case 13: // Return this.$('input').blur(); m.route(this.getItem(this.index()).find('a').attr('href')); + app.drawer.hide(); break; case 27: // Escape diff --git a/js/forum/src/components/settings-page.js b/js/forum/src/components/settings-page.js index 06e962d32..c9c7c44e4 100644 --- a/js/forum/src/components/settings-page.js +++ b/js/forum/src/components/settings-page.js @@ -19,6 +19,7 @@ export default class SettingsPage extends UserPage { this.setupUser(app.session.user()); app.setTitle('Settings'); + app.drawer.hide(); } content() { diff --git a/js/forum/src/components/user-notifications.js b/js/forum/src/components/user-notifications.js index 21bc732fb..cd2e4fb12 100644 --- a/js/forum/src/components/user-notifications.js +++ b/js/forum/src/components/user-notifications.js @@ -1,39 +1,18 @@ import Component from 'flarum/component'; -import avatar from 'flarum/helpers/avatar'; import icon from 'flarum/helpers/icon'; -import username from 'flarum/helpers/username'; import DropdownButton from 'flarum/components/dropdown-button'; -import ActionButton from 'flarum/components/action-button'; -import ItemList from 'flarum/utils/item-list'; -import Separator from 'flarum/components/separator'; -import LoadingIndicator from 'flarum/components/loading-indicator'; -import Discussion from 'flarum/models/discussion'; +import NotificationList from 'flarum/components/notification-list'; export default class UserNotifications extends Component { constructor(props) { super(props); - this.loading = m.prop(false); + this.showing = m.prop(false); } view() { var user = this.props.user; - var groups = []; - if (app.cache.notifications) { - var groupsObject = {}; - app.cache.notifications.forEach(notification => { - var subject = notification.subject(); - var discussion = subject instanceof Discussion ? subject : (subject.discussion && subject.discussion()); - var key = discussion ? discussion.id() : 0; - groupsObject[key] = groupsObject[key] || {discussion: discussion, notifications: []}; - groupsObject[key].notifications.push(notification); - if (groups.indexOf(groupsObject[key]) === -1) { - groups.push(groupsObject[key]); - } - }); - } - return DropdownButton.component({ className: 'notifications', buttonClass: 'btn btn-default btn-rounded btn-naked btn-icon'+(user.unreadNotificationsCount() ? ' unread' : ''), @@ -42,55 +21,14 @@ export default class UserNotifications extends Component { m('span.notifications-icon', user.unreadNotificationsCount() || icon('bell icon-glyph')), m('span.label', 'Notifications') ], - buttonClick: this.load.bind(this), - menuContent: [ - m('div.notifications-header', [ - ActionButton.component({ - className: 'btn btn-icon btn-link btn-sm', - icon: 'check', - title: 'Mark All as Read', - onclick: this.markAllAsRead.bind(this) - }), - m('h4', 'Notifications') - ]), - m('div.notifications-content', groups.length - ? groups.map(group => { - return m('div.notification-group', [ - group.discussion ? m('a.notification-group-header', { - href: app.route.discussion(group.discussion), - config: m.route - }, group.discussion.title()) : m('div.notification-group-header', app.config['forum_title']), - m('ul.notifications-list', group.notifications.map(notification => { - var NotificationComponent = app.notificationComponentRegistry[notification.contentType()]; - return NotificationComponent ? m('li', NotificationComponent.component({notification})) : ''; - })) - ]) - }) - : (!this.loading() ? m('div.no-notifications', 'No Notifications') : '')), - this.loading() ? LoadingIndicator.component() : '' - ] + buttonClick: (e) => { + if ($('body').hasClass('drawer-open')) { + m.route(app.route('notifications')); + } else { + this.showing(true); + } + }, + menuContent: this.showing() ? NotificationList.component() : [] }); } - - load() { - if (!app.cache.notifications || this.props.user.unreadNotificationsCount()) { - var component = this; - this.loading(true); - m.redraw(); - app.store.find('notifications').then(notifications => { - this.props.user.pushData({unreadNotificationsCount: 0}); - this.loading(false); - app.cache.notifications = notifications.sort((a, b) => b.time() - a.time()); - m.redraw(); - }) - } - } - - markAllAsRead() { - app.cache.notifications.forEach(function(notification) { - if (!notification.isRead()) { - notification.save({isRead: true}); - } - }) - } } diff --git a/js/forum/src/components/user-page.js b/js/forum/src/components/user-page.js index ce7aa54eb..38d860679 100644 --- a/js/forum/src/components/user-page.js +++ b/js/forum/src/components/user-page.js @@ -23,6 +23,7 @@ export default class UserPage extends Component { app.history.push('user'); app.current = this; + app.drawer.hide(); } /* diff --git a/js/forum/src/initializers/boot.js b/js/forum/src/initializers/boot.js index 3b91061db..6ac66658b 100644 --- a/js/forum/src/initializers/boot.js +++ b/js/forum/src/initializers/boot.js @@ -1,6 +1,7 @@ import ScrollListener from 'flarum/utils/scroll-listener'; import History from 'flarum/utils/history'; import Pane from 'flarum/utils/pane'; +import Drawer from 'flarum/utils/drawer'; import mapRoutes from 'flarum/utils/map-routes'; import BackButton from 'flarum/components/back-button'; @@ -19,6 +20,7 @@ export default function(app) { app.history = new History(); app.pane = new Pane(id('page')); app.search = new SearchBox(); + app.drawer = new Drawer(); app.cache = {}; m.startComputation(); @@ -49,5 +51,9 @@ export default function(app) { new ScrollListener(top => $('body').toggleClass('scrolled', top > 0)).start(); + $(function() { + FastClick.attach(document.body); + }); + app.booted = true; } diff --git a/js/forum/src/initializers/routes.js b/js/forum/src/initializers/routes.js index 57f844bad..b48f708e0 100644 --- a/js/forum/src/initializers/routes.js +++ b/js/forum/src/initializers/routes.js @@ -2,6 +2,7 @@ import IndexPage from 'flarum/components/index-page'; import DiscussionPage from 'flarum/components/discussion-page'; import ActivityPage from 'flarum/components/activity-page'; import SettingsPage from 'flarum/components/settings-page'; +import NotificationsPage from 'flarum/components/notifications-page'; export default function(app) { app.routes = { @@ -16,7 +17,8 @@ export default function(app) { 'user.discussions': ['/u/:username/discussions', ActivityPage.component({filter: 'startedDiscussion'})], 'user.posts': ['/u/:username/posts', ActivityPage.component({filter: 'posted'})], - 'settings': ['/settings', SettingsPage.component()] + 'settings': ['/settings', SettingsPage.component()], + 'notifications': ['/notifications', NotificationsPage.component()] }; app.route.discussion = function(discussion, near) { diff --git a/js/forum/src/utils/drawer.js b/js/forum/src/utils/drawer.js new file mode 100644 index 000000000..88c1883aa --- /dev/null +++ b/js/forum/src/utils/drawer.js @@ -0,0 +1,13 @@ +export default class Drawer { + hide() { + $('body').removeClass('drawer-open'); + } + + show() { + $('body').addClass('drawer-open'); + } + + toggle() { + $('body').toggleClass('drawer-open'); + } +} diff --git a/js/lib/components/back-button.js b/js/lib/components/back-button.js index 0056ef70e..d639d80db 100644 --- a/js/lib/components/back-button.js +++ b/js/lib/components/back-button.js @@ -18,15 +18,14 @@ export default class BackButton extends Component { m('button.btn.btn-default.btn-icon.back', {onclick: history.back.bind(history)}, icon('chevron-left icon')), pane && pane.active ? m('button.btn.btn-default.btn-icon.pin'+(pane.pinned ? '.active' : ''), {onclick: pane.togglePinned.bind(pane)}, icon('thumb-tack icon')) : '', ]) : (this.props.drawer ? [ - m('button.btn.btn-default.btn-icon.drawer-toggle', {onclick: this.toggleDrawer.bind(this)}, icon('reorder icon')) + m('button.btn.btn-default.btn-icon.drawer-toggle', { + onclick: app.drawer.toggle.bind(app.drawer), + className: app.session.user() && app.session.user().unreadNotificationsCount() ? 'unread-notifications' : '' + }, icon('reorder icon')) ] : '')); } onload(element, isInitialized, context) { context.retain = true; } - - toggleDrawer() { - $('body').toggleClass('drawer-open'); - } } diff --git a/js/lib/components/dropdown-split.js b/js/lib/components/dropdown-split.js index 06e0ec090..77544c464 100644 --- a/js/lib/components/dropdown-split.js +++ b/js/lib/components/dropdown-split.js @@ -21,7 +21,7 @@ export default class DropdownSplit extends Component { return m('div', {className: 'dropdown dropdown-split btn-group item-count-'+(items.length)+' '+this.props.className}, [ ActionButton.component(buttonProps), - m('a[href=javascript:;]', {className: 'dropdown-toggle '+this.props.buttonClass, 'data-toggle': 'dropdown'}, [ + m('a[href=javascript:;]', {className: 'dropdown-toggle btn-icon '+this.props.buttonClass, 'data-toggle': 'dropdown'}, [ icon('caret-down icon-caret'), icon((this.props.icon || 'ellipsis-v')+' icon'), ]), diff --git a/js/lib/components/nav-item.js b/js/lib/components/nav-item.js index 594e29516..0a7d4b369 100644 --- a/js/lib/components/nav-item.js +++ b/js/lib/components/nav-item.js @@ -4,7 +4,7 @@ import icon from 'flarum/helpers/icon' export default class NavItem extends Component { view() { var active = this.constructor.active(this.props); - return m('li'+(active ? '.active' : ''), m('a', { + return m('li'+(active ? '.active' : ''), m('a.has-icon', { href: this.props.href, onclick: this.props.onclick, config: m.route diff --git a/less/forum/composer.less b/less/forum/composer.less index 98125912d..352705b5f 100644 --- a/less/forum/composer.less +++ b/less/forum/composer.less @@ -4,23 +4,39 @@ .composer { pointer-events: auto; .box-shadow(0 2px 6px @fl-shadow-color); + + &.minimized { + height: 50px; + cursor: pointer; + } } .composer-controls { list-style: none; padding: 0; margin: 0; } +.composer-content { + .minimized & { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} .composer-header { list-style: none; padding: 1px 0; - margin: 0; + margin: 0 0 10px; + + .minimized & { + pointer-events: none; + } & > li { display: inline-block; margin-right: -4px; } & h3 { - margin: 0 0 10px; + margin: 0; line-height: 1.5em; &, & input, & a { @@ -41,6 +57,9 @@ } } } +.fa-minus.minimize { + vertical-align: -5px; +} .composer-controls { position: absolute; right: 10px; @@ -71,27 +90,85 @@ pointer-events: auto; } } +.composer-editor { + .minimized & { + visibility: hidden; + } +} // On phones, show the composer as a fixed overlay that covers the whole // screen. The controls are hidden (except for the 'x', which is the back- // control), and the avatar hidden. @media @phone { .composer { - position: absolute; + position: fixed; + bottom: 0; left: 0; right: 0; + z-index: @zindex-composer; + background: @fl-body-bg; + + &:not(.minimized) { + top: 0; + height: 100vh !important; + padding-top: @mobile-header-height; + + &:before { + content: " "; + .toolbar(); + opacity: 0; + + .visible& { + opacity: 1; + } + } + + & .composer-controls { + z-index: @zindex-navbar-fixed + 1; + + & li:not(.back-control) { + display: none; + } + } + } } .composer-content { - padding: 15px 15px 0; - } - .composer-controls { - & li:not(:last-child) { - display: none; + .minimized & { + margin-right: 50px; } } .composer-avatar { display: none; } + .composer-header { + margin-bottom: 0; + + & > li { + display: block; + border-bottom: 1px solid @fl-body-secondary-color; + padding: 10px 15px; + + .minimized & { + border-bottom: 0; + padding: 15px; + } + } + & h3 { + &, & a, & input { + font-size: 14px; + } + & input { + width: 100% !important; + } + } + } + .composer-editor { + padding: 15px; + + & textarea { + height: 50vh !important; + } + } } // On larger screens, show the composer as a window at the bottom of the @@ -120,10 +197,6 @@ &.active:not(.full-screen) { box-shadow: 0 0 0 2px @fl-body-primary-color, 0 2px 6px @fl-shadow-color; } - &.minimized { - height: 50px; - cursor: pointer; - } &.full-screen { position: fixed; left: 0; @@ -144,9 +217,6 @@ } } .composer-header { - .minimized & { - pointer-events: none; - } .full-screen & { margin-bottom: 20px; } @@ -172,9 +242,6 @@ display: none; } } - .fa-minus.minimize { - vertical-align: -5px; - } .composer-avatar { float: left; .avatar-size(64px); @@ -191,10 +258,6 @@ } } .composer-editor { - .minimized & { - visibility: hidden; - } - .full-screen & textarea { font-size: 16px; } diff --git a/less/forum/discussion.less b/less/forum/discussion.less index 86eef08d7..954e9cdf2 100644 --- a/less/forum/discussion.less +++ b/less/forum/discussion.less @@ -72,6 +72,11 @@ & .item { &:not(:last-child) { border-bottom: 1px solid @fl-body-secondary-color; + + @media @phone { + margin: 0 -15px; + padding: 0 15px; + } } } } @@ -85,6 +90,8 @@ animation-iteration-count: infinite; } .fake-text { + display: inline-block; + vertical-align: middle; background: @fl-body-secondary-color; height: 12px; width: 100%; @@ -94,6 +101,10 @@ .post-header & { height: 16px; width: 150px; + + @media @phone { + margin-bottom: 0; + } } } @@ -135,6 +146,11 @@ background: @fl-primary-color; } } +@-webkit-keyframes pulsate { + 0% {-webkit-transform: scale(1)} + 50% {-webkit-transform: scale(1.02)} + 100% {-webkit-transform: scale(1)} +} @keyframes pulsate { 0% {transform: scale(1)} 50% {transform: scale(1.02)} @@ -143,10 +159,14 @@ .item.pulsate { animation: pulsate 1s ease-in-out; animation-iteration-count: infinite; + -webkit-animation: pulsate 1s ease-in-out; + -webkit-animation-iteration-count: infinite; } .item.flash { animation: pulsate 0.2s ease-in-out; animation-iteration-count: 1; + -webkit-animation: pulsate 0.2s ease-in-out; + -webkit-animation-iteration-count: 1; } .post-header { margin-bottom: 10px; @@ -356,10 +376,13 @@ } .post-actions { margin-top: 10px; - margin-bottom: -10px; - opacity: 0; .transition(opacity 0.2s); + @media @tablet, @desktop, @desktop-hd { + margin-bottom: -10px; + opacity: 0; + } + & > ul { & > li { margin-right: 10px; @@ -377,6 +400,11 @@ padding: 20px 20px 20px 90px; background: @fl-body-secondary-color; font-size: 12px; + + @media @phone { + margin: 0 -15px; + padding: 20px 15px; + } } .post-preview { @@ -415,8 +443,8 @@ & h3 .badges { position: absolute; - top: -7px; - left: 5px; + top: -12px; + left: 6px; width: 32px; & .badge { @@ -469,26 +497,39 @@ font-size: 22px; } } + .reply-post { font-size: 15px; cursor: text; overflow: hidden; - margin: 50px -20px 0; - border: 2px dashed transparent; + margin-top: 50px; + border: 2px dashed @fl-body-secondary-color; color: @fl-body-muted-color; border-radius: 10px; - padding: 20px 20px 20px 110px; - transition: border-color 0.2s; + padding: 20px; & .post-header { - padding-top: 18px; + margin: 0; color: inherit; } - & .avatar { - margin-top: -18px; - } - &:hover { - border-color: @fl-body-secondary-color; +} +@media @tablet, @desktop, @desktop-hd { + .reply-post { + margin-left: -20px; + margin-right: -20px; + padding-left: 110px; + border-color: transparent; + transition: border-color 0.2s; + + & .post-header { + padding-top: 18px; + } + & .avatar { + margin-top: -18px; + } + &:hover { + border-color: @fl-body-secondary-color; + } } } diff --git a/less/forum/index.less b/less/forum/index.less index 571a5e37d..652c6992c 100644 --- a/less/forum/index.less +++ b/less/forum/index.less @@ -146,11 +146,13 @@ } @media @phone { - .discussion-list > ul > li { - padding-right: 45px; + .discussion-list { + margin: 0 -15px; - & .contextual-controls { - display: none; + & > ul > li { + & .contextual-controls { + display: none; + } } } } @@ -271,8 +273,12 @@ @media @phone { .discussion-summary { - padding-left: 45px; - padding-right: 45px; + padding-left: 15px + 45px; + padding-right: 15px + 35px; + + &:active { + background: @fl-body-secondary-color; + } & .author { margin-left: -45px; @@ -294,9 +300,10 @@ } & .title { font-size: 14px; + text-decoration: none !important; } & .count { - margin-right: -45px; + margin-right: -35px; background: @fl-body-control-bg; color: @fl-body-control-color; border-radius: @border-radius-base; diff --git a/less/forum/notifications.less b/less/forum/notifications.less index 8453540d6..10aefefe6 100644 --- a/less/forum/notifications.less +++ b/less/forum/notifications.less @@ -2,14 +2,22 @@ & .dropdown-menu { padding: 0; overflow: hidden; - } - & .loading-indicator { - height: 100px; + + & .notifications-content { + max-height: 600px; + overflow: auto; + padding-bottom: 10px; + } } & .dropdown-toggle .label { margin-left: 5px; } } +.notification-list { + & .loading-indicator { + height: 100px; + } +} @media @tablet, @desktop, @desktop-hd { .notifications { & .dropdown-menu { @@ -36,26 +44,23 @@ color: #fff; } .notifications-header { - padding: 12px 15px; - border-bottom: 1px solid @fl-body-secondary-color; + @media @tablet, @desktop, @desktop-hd { + padding: 12px 15px; + border-bottom: 1px solid @fl-body-secondary-color; - & h4 { - font-size: 12px; - text-transform: uppercase; - font-weight: bold; - margin: 0; - color: @fl-body-muted-color; + & h4 { + font-size: 12px; + text-transform: uppercase; + font-weight: bold; + margin: 0; + color: @fl-body-muted-color; + } + & .btn { + float: right; + margin-top: -5px; + margin-right: -5px; + } } - & .btn { - float: right; - margin-top: -5px; - margin-right: -5px; - } -} -.notifications-content { - max-height: 600px; - overflow: auto; - padding-bottom: 10px; } .no-notifications { color: @fl-body-muted-color; @@ -80,7 +85,7 @@ overflow: hidden; text-overflow: ellipsis; } -.notifications-list { +.notification-group-list { list-style: none; margin: 0; padding: 0; @@ -123,14 +128,20 @@ text-transform: uppercase; } } -@media @phone { - .notification { - & > a { - padding-left: 60px; - } - & .avatar { - margin-left: -45px; - .avatar-size(32px); - } + +.drawer-toggle.unread-notifications { + position: relative; + + &:after { + content: ' '; + display: block; + position: absolute; + background: @fl-body-primary-color; + top: 4px; + right: 2px; + width: 14px; + height: 14px; + border-radius: 7px; + border: 2px solid @fl-body-bg; } } diff --git a/less/forum/signup.less b/less/forum/signup.less index 342a22ec9..e99e41ccf 100644 --- a/less/forum/signup.less +++ b/less/forum/signup.less @@ -10,7 +10,7 @@ color: #fff; font-size: 14px; - .drawer-components(); + .inverted-components(); & .avatar { .avatar-size(96px); diff --git a/less/forum/user.less b/less/forum/user.less index 011ef9313..a1d230265 100644 --- a/less/forum/user.less +++ b/less/forum/user.less @@ -1,5 +1,5 @@ .user-card { - .drawer-components(); + .inverted-components(); background-size: 100% 100%; &, & .container { @@ -44,6 +44,11 @@ padding-left: 130px; max-width: 800px; + @media @phone { + padding-left: 0; + text-align: center; + } + & .user-identity { display: inline; vertical-align: middle; @@ -51,16 +56,30 @@ & .user-avatar { float: left; margin-left: -130px; + + @media @phone { + float: none; + margin: 0 auto 20px; + width: 64px + 8px; + } } & .avatar-editor .dropdown-toggle { margin: 4px; line-height: 96px; font-size: 26px; + + @media @phone { + line-height: 64px; + } } & .avatar { .avatar-size(96px); border: 4px solid @fl-body-bg; .box-shadow(0 2px 6px @fl-shadow-color); + + @media @phone { + .avatar-size(64px); + } } & .badges { margin-left: 10px; @@ -126,6 +145,10 @@ & > li { margin-bottom: 30px; padding-left: 32px; + + @media @phone { + padding-left: 24px; + } } & .activity-icon { .avatar-size(32px); @@ -133,6 +156,10 @@ margin-left: -50px; .box-shadow(0 0 0 3px #fff); margin-top: -5px; + + @media @phone { + margin-left: -42px; + } } } .activity-info { @@ -213,8 +240,10 @@ right: 0; bottom: 0; } - & .dropdown-menu { - left: 35%; - top: 65%; + @media @tablet, @desktop, @desktop-hd { + & .dropdown-menu { + left: 35%; + top: 65%; + } } } diff --git a/less/lib/dropdowns.less b/less/lib/dropdowns.less index 100ea61d7..6d28a5dc7 100644 --- a/less/lib/dropdowns.less +++ b/less/lib/dropdowns.less @@ -81,59 +81,73 @@ } } -// PHONES @media @phone { - .dropdown-open { - overflow: hidden; - } - .dropdown-menu { - margin: 0; - position: fixed; - left: 0 !important; - right: 0 !important; - width: auto !important; - bottom: -100vh; - top: auto; - padding: 0; + .dropdown.open { z-index: @zindex-modal; - display: block; - max-height: 100vh; - border-radius: 0; - .box-shadow(0 2px 6px @fl-shadow-color); - .translate3d(0, 0, 0); - visibility: hidden; - .transition(~"bottom 0.3s, visibility 0s 0.3s"); + } + .dropdown { + & .dropdown-menu { + margin: 0; + position: fixed; + left: 0 !important; + right: 0 !important; + width: auto !important; + bottom: 0; + top: auto; + padding: 0; + padding-bottom: 40px !important; + display: block; + max-height: 70vh; + border-radius: 0; + .box-shadow(0 2px 6px @fl-shadow-color); + visibility: hidden; + overflow: auto; + -webkit-overflow-scrolling: touch; + .translate3d(0, 70vh, 0); + .transition-transform(~" 0.3s, visibility 0s 0.3s"); - & > li > a { - background: #fff; - font-size: 16px; - padding: 15px 20px 15px 50px; - - & .icon { + & > li > a { + background: #fff; font-size: 16px; - margin-left: -30px; + padding: 15px 20px; + + &.has-icon { + padding-left: 50px; + } + & .icon { + font-size: 16px; + margin-left: -30px; + } + &:hover { + background: @fl-body-secondary-color; + } + } + & .divider { + margin: 0; + } + & > .active > a { + &, &:hover { + background: @fl-body-primary-color !important; + color: #fff !important; + } + } + + .open& { + -webkit-transform: none; + transform: none; + visibility: visible; + transition-delay: 0s; } } - & .divider { - margin: 0; - } - & > .active > a { - color: #fff !important; - } + & .dropdown-backdrop { + background: fade(@fl-secondary-color, 90%); + opacity: 0; + .transition(~"opacity 0.3s"); + .translate3d(0, 0, 0); - .open & { - bottom: 0; - visibility: visible; - transition-delay: 0s; - } - } - .dropdown-backdrop { - background: fade(@fl-body-primary-color, 90%); - opacity: 0; - .transition(~"opacity 0.3s"); - - .open & { - opacity: 1; + .open& { + opacity: 1; + } } } } diff --git a/less/lib/forms.less b/less/lib/forms.less index 1f214c1a4..9e3607562 100644 --- a/less/lib/forms.less +++ b/less/lib/forms.less @@ -4,6 +4,7 @@ .form-control { .box-shadow(none); border-width: 2px; + -webkit-appearance: none; &:focus, &.focus { diff --git a/less/lib/layout.less b/less/lib/layout.less index 4a647877c..552896906 100644 --- a/less/lib/layout.less +++ b/less/lib/layout.less @@ -22,14 +22,14 @@ body { .toolbar() { background: fade(@fl-hdr-bg, 98%); - transform: translateZ(0); // Fix for Chrome bug where a transparent white background is actually gray position: fixed; top: 0; left: 0; right: 0; z-index: @zindex-navbar-fixed; border-bottom: 1px solid @fl-body-control-bg; - .transition(~"box-shadow 0.2s, left 0.2s"); + .translate3d(0, 0, 0); + .transition(~"box-shadow 0.2s, -webkit-transform 0.2s"); @media @phone { height: @mobile-header-height; @@ -55,7 +55,7 @@ body { // PHONES: Push the toolbar to the right when the drawer is open. @media @phone { .drawer-open & { - left: @drawer-width; + .translate3d(@drawer-width, 0, 0); } } } @@ -67,29 +67,35 @@ body { .primary-control, .title-control, .back-control { position: fixed; z-index: @zindex-navbar-fixed + 1; - top: 5px; + top: 0; margin: 0; + visibility: visible; + .transition(visibility 0s 0.4s); & .btn { float: none; background: transparent !important; .box-shadow(~"none !important"); + height: @mobile-header-height; + width: auto; + padding: 13px !important; &:active { opacity: 0.5; } } } + .primary-control, .title-control { + .drawer-open .global-page & { + visibility: hidden; + transition-delay: 0s; + } + } .primary-control { width: auto; - right: 5px; - .transition(right 0.2s); + right: 0; - .drawer-open .global-page & { - right: -@drawer-width; - } - - & .dropdown-split { + &.dropdown-split { & .btn, & .icon-caret { display: none; } @@ -101,12 +107,10 @@ body { .primary-control, .back-control { & .btn { color: @fl-hdr-control-color !important; - padding-left: 5px; - padding-right: 5px; & .icon { display: block; - font-size: 18px; + font-size: 20px; } & .label { display: none; @@ -118,24 +122,27 @@ body { left: 50%; margin-left: -100px; text-align: center; - line-height: 33px; - .transition(margin-left 0.2s); - - .drawer-open .global-page & { - margin-left: -100px + @drawer-width; - } + color: @fl-body-muted-color; &, & .btn { - color: @fl-hdr-color; font-size: 16px; } + & .btn { + color: @fl-hdr-color; + } + } + h3.title-control, h4.title-control { + line-height: 46px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .back-control { - left: 5px; - .transition(left 0.2s); + left: 0; + .transition-transform(0.2s); .drawer-open .global-page & { - left: @drawer-width + 10px; + .translate3d(@drawer-width, 0, 0); } & .pin { @@ -147,41 +154,34 @@ body { // ------------------------------------ // Drawer -// This is a mixin which styles components (buttons, inputs, etc.) for use in -// the drawer. We define it as a mixin because it is also pulled in when -// styling a "colored header". -.drawer-components() { +// This is a mixin which styles components (buttons, inputs, etc.) for use on +// dark backgrounds. +.inverted-components() { .header-title { &, & a { - color: @fl-drawer-color; + color: #fff; } } &, & a, & .btn-link { - color: @fl-drawer-control-color; + color: #fff; } & .form-control { - background: @fl-drawer-control-bg; + background: fade(#000, 10%); border: 0; - color: @fl-drawer-control-color; - .placeholder(@fl-drawer-control-color); + color: #fff; + .placeholder(fade(#fff, 50%)); &:focus { - background: fadein(@fl-drawer-control-bg, 5%); + background: fade(#000, 15%); } } - & .search-input { - color: @fl-drawer-control-color; - } - & .btn-default, & .btn-default:hover { - background: @fl-drawer-control-bg; - color: @fl-drawer-control-color; + & .btn-default:not(.btn-naked), & .btn-default:hover { + background: fade(#000, 10%); + color: #fff; } & .btn-default.active, .open > .dropdown-toggle.btn-default { - background: fadein(@fl-drawer-control-bg, 5%); - color: @fl-drawer-control-color; - } - & .btn-naked { - background: transparent; + background: fade(#000, 15%); + color: #fff; } } @@ -193,8 +193,7 @@ body { overflow: hidden; } .global-drawer { - background: @fl-drawer-bg; - color: @fl-drawer-color; + background: @fl-hdr-bg; width: @drawer-width; position: fixed; left: 0; @@ -202,8 +201,11 @@ body { bottom: 0; visibility: hidden; .transition(visibility 0s 0.2s); + .box-shadow(inset -6px 0 6px -6px @fl-shadow-color); - .drawer-components(); + & when (@fl-colored-hdr = true) { + .inverted-components(); + } .drawer-open & { visibility: visible; @@ -279,7 +281,7 @@ body { .clearfix(); & when (@fl-colored-hdr = true) { - .drawer-components(); + .inverted-components(); } & .back-button { @@ -333,13 +335,12 @@ body { position: relative; width: 100%; min-height: 100vh; - padding-bottom: 15px; + padding-bottom: 50px; margin-top: @mobile-header-height; - .box-shadow(0 0 6px @fl-shadow-color); - .transition(margin-left 0.2s); + .transition-transform(0.2s); .drawer-open & { - margin-left: @drawer-width; + .translate3d(@drawer-width, 0, 0); // Disable all interaction with the content when the drawer is open. When // .global-content is touched, the drawer will be closed. diff --git a/less/lib/modals.less b/less/lib/modals.less index 8312404d3..974c49761 100644 --- a/less/lib/modals.less +++ b/less/lib/modals.less @@ -83,33 +83,32 @@ .modal.fade { opacity: 1; } - .modal-backdrop.in { - opacity: 0; - } - .modal-dialog { + .modal { position: fixed; left: 0; right: 0; bottom: 0; top: 0; - } - .modal-dialog { - margin: 0; + overflow: auto; - .modal.fade & { - .transition(top 0.3s); - top: 100%; - .translate(0, 0); + &.fade { + .transition-transform(0.3s); + .translate3d(0, 100vh, 0); } - .modal.in & { - top: 0; + &.in { + -webkit-transform: none !important; + transform: none !important; } - &:before { content: " "; .toolbar(); } } + .modal-dialog { + margin: 0; + -webkit-transform: none !important; + transform: none !important; + } .modal-content { border-radius: 0; border: 0; @@ -121,10 +120,6 @@ padding: 0; border: 0; min-height: 0; - - & h3 { - line-height: 36px; - } } } diff --git a/less/lib/search.less b/less/lib/search.less index 6eee72cab..35f5052ff 100644 --- a/less/lib/search.less +++ b/less/lib/search.less @@ -1,6 +1,8 @@ -.search-box { - & input:focus, &.active input, & .search-results { - width: 400px; +@media @tablet, @desktop, @desktop-hd { + .search-box { + & input:focus, &.active input, & .search-results { + width: 400px; + } } } .search-results { diff --git a/src/Forum/ForumServiceProvider.php b/src/Forum/ForumServiceProvider.php index 702444cb7..7b809c077 100644 --- a/src/Forum/ForumServiceProvider.php +++ b/src/Forum/ForumServiceProvider.php @@ -74,6 +74,12 @@ class ForumServiceProvider extends ServiceProvider $this->action('Flarum\Forum\Actions\IndexAction') ); + $routes->get( + '/notifications', + 'flarum.forum.notifications', + $this->action('Flarum\Forum\Actions\IndexAction') + ); + $routes->get( '/logout', 'flarum.forum.logout',