├── LICENSE ├── bootstrap.php ├── composer.json ├── js └── admin │ ├── Gulpfile.js │ ├── dist │ └── extension.js │ ├── package.json │ └── src │ ├── addUsersListPane.js │ ├── components │ ├── EmailUserModal.js │ └── UsersListPage.js │ └── main.js ├── less └── admin │ ├── UsersListPage.less │ └── extension.less ├── locale ├── en.yml └── ru.yml ├── readme.md └── src ├── Api └── Controller │ └── SendAdminEmailController.php └── Listener ├── AddAdminMailApi.php └── AddClientAssets.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Italian Space Astronautics Association - ISAA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | subscribe(Listener\AddAdminMailApi::class); 8 | $events->subscribe(Listener\AddClientAssets::class); 9 | }; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "avatar4eg/flarum-ext-users-list", 3 | "description": "Add users list in admin panel.", 4 | "type": "flarum-extension", 5 | "keywords": ["users", "admin"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Nikolai Morozov", 10 | "email": "avanmorozov@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "flarum/core": "^0.1.0-beta.5" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Avatar4eg\\UsersList\\": "src/" 19 | } 20 | }, 21 | "extra": { 22 | "flarum-extension": { 23 | "title": "Users list", 24 | "icon": { 25 | "name": "users", 26 | "backgroundColor": "#FFFFFF", 27 | "color": "#000000" 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /js/admin/Gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('flarum-gulp'); 2 | 3 | gulp({ 4 | modules: { 5 | 'avatar4eg/users-list': [ 6 | '../lib/**/*.js', 7 | 'src/**/*.js' 8 | ] 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /js/admin/dist/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | System.register('avatar4eg/users-list/addUsersListPane', ['flarum/app', 'flarum/extend', 'flarum/components/AdminNav', 'flarum/components/AdminLinkButton', 'avatar4eg/users-list/components/UsersListPage'], function (_export, _context) { 4 | "use strict"; 5 | 6 | var app, extend, AdminNav, AdminLinkButton, CountriesPage; 7 | 8 | _export('default', function () { 9 | app.routes.usersList = { path: '/users-list', component: CountriesPage.component() }; 10 | 11 | app.extensionSettings['avatar4eg-users-list'] = function () { 12 | return m.route(app.route('usersList')); 13 | }; 14 | 15 | extend(AdminNav.prototype, 'items', function (items) { 16 | items.add('users-list', AdminLinkButton.component({ 17 | href: app.route('usersList'), 18 | icon: 'users', 19 | children: app.translator.trans('avatar4eg-users-list.admin.nav.users_button'), 20 | description: app.translator.trans('avatar4eg-users-list.admin.nav.users_text') 21 | })); 22 | }); 23 | }); 24 | 25 | return { 26 | setters: [function (_flarumApp) { 27 | app = _flarumApp.default; 28 | }, function (_flarumExtend) { 29 | extend = _flarumExtend.extend; 30 | }, function (_flarumComponentsAdminNav) { 31 | AdminNav = _flarumComponentsAdminNav.default; 32 | }, function (_flarumComponentsAdminLinkButton) { 33 | AdminLinkButton = _flarumComponentsAdminLinkButton.default; 34 | }, function (_avatar4egUsersListComponentsUsersListPage) { 35 | CountriesPage = _avatar4egUsersListComponentsUsersListPage.default; 36 | }], 37 | execute: function () {} 38 | }; 39 | });; 40 | 'use strict'; 41 | 42 | System.register('avatar4eg/users-list/components/EmailUserModal', ['flarum/app', 'flarum/components/Modal', 'flarum/components/Button'], function (_export, _context) { 43 | "use strict"; 44 | 45 | var app, Modal, Button, EmailUserModal; 46 | return { 47 | setters: [function (_flarumApp) { 48 | app = _flarumApp.default; 49 | }, function (_flarumComponentsModal) { 50 | Modal = _flarumComponentsModal.default; 51 | }, function (_flarumComponentsButton) { 52 | Button = _flarumComponentsButton.default; 53 | }], 54 | execute: function () { 55 | EmailUserModal = function (_Modal) { 56 | babelHelpers.inherits(EmailUserModal, _Modal); 57 | 58 | function EmailUserModal() { 59 | babelHelpers.classCallCheck(this, EmailUserModal); 60 | return babelHelpers.possibleConstructorReturn(this, (EmailUserModal.__proto__ || Object.getPrototypeOf(EmailUserModal)).apply(this, arguments)); 61 | } 62 | 63 | babelHelpers.createClass(EmailUserModal, [{ 64 | key: 'init', 65 | value: function init() { 66 | babelHelpers.get(EmailUserModal.prototype.__proto__ || Object.getPrototypeOf(EmailUserModal.prototype), 'init', this).call(this); 67 | 68 | this.loading = false; 69 | 70 | this.user = this.props.user; 71 | this.forAll = this.props.forAll; 72 | this.subject = m.prop(app.translator.trans('avatar4eg-users-list.admin.modal_mail.default_subject')[0]); 73 | this.messageText = m.prop(''); 74 | 75 | if (!this.forAll) { 76 | this.email = m.prop(this.user.email() || ''); 77 | this.submitDisabled = !this.checkEmail(this.email()); 78 | } else { 79 | this.submitDisabled = false; 80 | } 81 | } 82 | }, { 83 | key: 'className', 84 | value: function className() { 85 | return 'EmailUserModal Modal--large'; 86 | } 87 | }, { 88 | key: 'title', 89 | value: function title() { 90 | var title = app.translator.trans('avatar4eg-users-list.admin.modal_mail.title_text'); 91 | if (this.forAll) { 92 | title += ' ' + app.translator.trans('avatar4eg-users-list.admin.modal_mail.title_all_text'); 93 | } else { 94 | title += ' ' + this.user.username() + ' (' + this.email() + ')'; 95 | } 96 | return title; 97 | } 98 | }, { 99 | key: 'content', 100 | value: function content() { 101 | return [m('div', { className: 'Modal-body' }, [m('form', { 102 | className: 'Form', 103 | onsubmit: this.onsubmit.bind(this) 104 | }, [this.forAll ? '' : m('div', { className: 'Form-group' }, [m('label', {}, app.translator.trans('avatar4eg-users-list.admin.modal_mail.email_label')), m('input', { 105 | className: 'FormControl', 106 | value: this.email(), 107 | oninput: m.withAttr('value', this.oninputEmail.bind(this)) 108 | })]), m('div', { className: 'Form-group' }, [m('label', {}, app.translator.trans('avatar4eg-users-list.admin.modal_mail.subject_label')), m('input', { 109 | className: 'FormControl', 110 | value: this.subject(), 111 | oninput: m.withAttr('value', this.subject) 112 | })]), m('div', { className: 'Form-group' }, [m('label', {}, app.translator.trans('avatar4eg-users-list.admin.modal_mail.message_label')), m('textarea', { 113 | className: 'FormControl', 114 | rows: 10, 115 | style: "resize: vertical;", 116 | value: this.messageText(), 117 | oninput: m.withAttr('value', this.messageText) 118 | })]), Button.component({ 119 | type: 'submit', 120 | className: 'Button Button--primary EditContactModal-save', 121 | loading: this.loading, 122 | children: app.translator.trans('avatar4eg-users-list.admin.modal_mail.submit_button'), 123 | disabled: this.submitDisabled 124 | })])])]; 125 | } 126 | }, { 127 | key: 'oninputEmail', 128 | value: function oninputEmail(value) { 129 | this.email(value); 130 | this.submitDisabled = !this.checkEmail(value); 131 | } 132 | }, { 133 | key: 'checkEmail', 134 | value: function checkEmail(email) { 135 | var emailRegexp = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i; 136 | 137 | var correct = true; 138 | var emails = this.splitEmails(email); 139 | emails.forEach(function (email) { 140 | if (!emailRegexp.test(email)) { 141 | correct = false; 142 | } 143 | }); 144 | return correct; 145 | } 146 | }, { 147 | key: 'splitEmails', 148 | value: function splitEmails(email) { 149 | email = email.replace(/\s*/g, ''); 150 | return email.split(','); 151 | } 152 | }, { 153 | key: 'onsubmit', 154 | value: function onsubmit(e) { 155 | var _this2 = this; 156 | 157 | e.preventDefault(); 158 | 159 | this.loading = true; 160 | 161 | var data = { 162 | emails: this.forAll ? [] : this.splitEmails(this.email()), 163 | subject: this.subject(), 164 | text: this.messageText(), 165 | forAll: this.forAll 166 | }; 167 | 168 | app.request({ 169 | method: 'POST', 170 | url: app.forum.attribute('apiUrl') + '/admin-mail', 171 | data: { data: data } 172 | }).then(function () { 173 | _this2.hide(); 174 | }, function (response) { 175 | _this2.loading = false; 176 | _this2.handleErrors(response); 177 | }); 178 | } 179 | }]); 180 | return EmailUserModal; 181 | }(Modal); 182 | 183 | _export('default', EmailUserModal); 184 | } 185 | }; 186 | });; 187 | 'use strict'; 188 | 189 | System.register('avatar4eg/users-list/components/UsersListPage', ['flarum/app', 'flarum/components/Page', 'flarum/components/Button', 'flarum/components/LoadingIndicator', 'flarum/helpers/humanTime', 'flarum/helpers/icon', 'avatar4eg/users-list/components/EmailUserModal'], function (_export, _context) { 190 | "use strict"; 191 | 192 | var app, Page, Button, LoadingIndicator, humanTime, icon, EmailUserModal, UsersListPage; 193 | 194 | 195 | function UserItem(user) { 196 | var url = app.forum.attribute('baseUrl') + '/u/' + user.id(); 197 | var online = user.isOnline(); 198 | 199 | return [m('li', { "data-id": user.id() }, [m('div', { className: 'UsersListItem-info' }, [m('span', { className: 'UsersListItem-name' }, [user.username()]), m('span', { className: 'UserCard-lastSeen' + (online ? ' online' : '') }, [online ? [icon('circle'), ' ', app.translator.trans('avatar4eg-users-list.admin.page.online_text')] : [icon('clock-o'), ' ', humanTime(user.lastSeenTime())]]), m('span', { className: 'UsersListItem-comments' }, [icon('comment-o'), user.commentsCount()]), m('span', { className: 'UsersListItem-discussions' }, [icon('reorder'), user.discussionsCount()]), m('a', { 200 | className: 'Button Button--link', 201 | target: '_blank', 202 | href: url 203 | }, [icon('eye')]), Button.component({ 204 | className: 'Button Button--link', 205 | icon: 'envelope', 206 | onclick: function onclick(e) { 207 | e.preventDefault(); 208 | app.modal.show(new EmailUserModal({ user: user })); 209 | } 210 | })])])]; 211 | } 212 | 213 | return { 214 | setters: [function (_flarumApp) { 215 | app = _flarumApp.default; 216 | }, function (_flarumComponentsPage) { 217 | Page = _flarumComponentsPage.default; 218 | }, function (_flarumComponentsButton) { 219 | Button = _flarumComponentsButton.default; 220 | }, function (_flarumComponentsLoadingIndicator) { 221 | LoadingIndicator = _flarumComponentsLoadingIndicator.default; 222 | }, function (_flarumHelpersHumanTime) { 223 | humanTime = _flarumHelpersHumanTime.default; 224 | }, function (_flarumHelpersIcon) { 225 | icon = _flarumHelpersIcon.default; 226 | }, function (_avatar4egUsersListComponentsEmailUserModal) { 227 | EmailUserModal = _avatar4egUsersListComponentsEmailUserModal.default; 228 | }], 229 | execute: function () { 230 | UsersListPage = function (_Page) { 231 | babelHelpers.inherits(UsersListPage, _Page); 232 | 233 | function UsersListPage() { 234 | babelHelpers.classCallCheck(this, UsersListPage); 235 | return babelHelpers.possibleConstructorReturn(this, (UsersListPage.__proto__ || Object.getPrototypeOf(UsersListPage)).apply(this, arguments)); 236 | } 237 | 238 | babelHelpers.createClass(UsersListPage, [{ 239 | key: 'init', 240 | value: function init() { 241 | babelHelpers.get(UsersListPage.prototype.__proto__ || Object.getPrototypeOf(UsersListPage.prototype), 'init', this).call(this); 242 | 243 | this.loading = true; 244 | this.moreResults = false; 245 | this.users = []; 246 | this.refresh(); 247 | } 248 | }, { 249 | key: 'view', 250 | value: function view() { 251 | var loading = void 0; 252 | 253 | if (this.loading) { 254 | loading = LoadingIndicator.component(); 255 | } else if (this.moreResults) { 256 | loading = Button.component({ 257 | children: app.translator.trans('avatar4eg-users-list.admin.page.load_more_button'), 258 | className: 'Button', 259 | onclick: this.loadMore.bind(this) 260 | }); 261 | } 262 | 263 | return [m('div', { className: 'UsersListPage' }, [m('div', { className: 'UsersListPage-header' }, [m('div', { className: 'container' }, [m('p', {}, app.translator.trans('avatar4eg-users-list.admin.page.about_text')), Button.component({ 264 | className: 'Button Button--primary', 265 | icon: 'plus', 266 | children: app.translator.trans('avatar4eg-users-list.admin.page.mail_all_button'), 267 | onclick: function onclick() { 268 | return app.modal.show(new EmailUserModal({ 'forAll': true })); 269 | } 270 | })])]), m('div', { className: 'UsersListPage-list' }, [m('div', { className: 'container' }, [m('div', { className: 'UsersListItems' }, [m('label', {}, app.translator.trans('avatar4eg-users-list.admin.page.list_title')), m('ol', { 271 | className: 'UsersList' 272 | }, [this.users.map(UserItem)]), m('div', { className: 'UsersListPage-loadMore' }, [loading])])])])])]; 273 | } 274 | }, { 275 | key: 'refresh', 276 | value: function refresh() { 277 | var _this2 = this; 278 | 279 | var clear = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; 280 | 281 | if (clear) { 282 | this.loading = true; 283 | this.users = []; 284 | } 285 | 286 | return this.loadResults().then(function (results) { 287 | _this2.users = []; 288 | _this2.parseResults(results); 289 | }, function () { 290 | _this2.loading = false; 291 | m.redraw(); 292 | }); 293 | } 294 | }, { 295 | key: 'loadResults', 296 | value: function loadResults(offset) { 297 | var params = {}; 298 | params.page = { 299 | offset: offset, 300 | limit: 50 301 | }; 302 | params.sort = 'username'; 303 | 304 | return app.store.find('users', params); 305 | } 306 | }, { 307 | key: 'loadMore', 308 | value: function loadMore() { 309 | this.loading = true; 310 | 311 | this.loadResults(this.users.length).then(this.parseResults.bind(this)); 312 | } 313 | }, { 314 | key: 'parseResults', 315 | value: function parseResults(results) { 316 | [].push.apply(this.users, results); 317 | 318 | this.loading = false; 319 | this.moreResults = !!results.payload.links.next; 320 | 321 | m.lazyRedraw(); 322 | 323 | return results; 324 | } 325 | }]); 326 | return UsersListPage; 327 | }(Page); 328 | 329 | _export('default', UsersListPage); 330 | } 331 | }; 332 | });; 333 | 'use strict'; 334 | 335 | System.register('avatar4eg/users-list/main', ['flarum/app', 'avatar4eg/users-list/addUsersListPane'], function (_export, _context) { 336 | "use strict"; 337 | 338 | var app, addUsersListPane; 339 | return { 340 | setters: [function (_flarumApp) { 341 | app = _flarumApp.default; 342 | }, function (_avatar4egUsersListAddUsersListPane) { 343 | addUsersListPane = _avatar4egUsersListAddUsersListPane.default; 344 | }], 345 | execute: function () { 346 | 347 | app.initializers.add('avatar4eg-users-list', function (app) { 348 | addUsersListPane(); 349 | }); 350 | } 351 | }; 352 | }); -------------------------------------------------------------------------------- /js/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "gulp": "^3.9.1", 5 | "flarum-gulp": "^0.2.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /js/admin/src/addUsersListPane.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/app'; 2 | import { extend } from 'flarum/extend'; 3 | import AdminNav from 'flarum/components/AdminNav'; 4 | import AdminLinkButton from 'flarum/components/AdminLinkButton'; 5 | 6 | import CountriesPage from 'avatar4eg/users-list/components/UsersListPage'; 7 | 8 | export default function() { 9 | app.routes.usersList = {path: '/users-list', component: CountriesPage.component()}; 10 | 11 | app.extensionSettings['avatar4eg-users-list'] = () => m.route(app.route('usersList')); 12 | 13 | extend(AdminNav.prototype, 'items', items => { 14 | items.add('users-list', AdminLinkButton.component({ 15 | href: app.route('usersList'), 16 | icon: 'users', 17 | children: app.translator.trans('avatar4eg-users-list.admin.nav.users_button'), 18 | description: app.translator.trans('avatar4eg-users-list.admin.nav.users_text') 19 | })); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /js/admin/src/components/EmailUserModal.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/app'; 2 | import Modal from 'flarum/components/Modal'; 3 | import Button from 'flarum/components/Button'; 4 | 5 | /** 6 | * The `EmailUserModal` component shows a modal dialog which allows admin 7 | * to send message to user. 8 | */ 9 | export default class EmailUserModal extends Modal { 10 | init() { 11 | super.init(); 12 | 13 | this.loading = false; 14 | 15 | this.user = this.props.user; 16 | this.forAll = this.props.forAll; 17 | this.subject = m.prop(app.translator.trans('avatar4eg-users-list.admin.modal_mail.default_subject')[0]); 18 | this.messageText = m.prop(''); 19 | 20 | if (!this.forAll) { 21 | this.email = m.prop(this.user.email() || ''); 22 | this.submitDisabled = !this.checkEmail(this.email()); 23 | } else { 24 | this.submitDisabled = false; 25 | } 26 | } 27 | 28 | className() { 29 | return 'EmailUserModal Modal--large'; 30 | } 31 | 32 | title() { 33 | var title = app.translator.trans('avatar4eg-users-list.admin.modal_mail.title_text'); 34 | if (this.forAll) { 35 | title += ' ' + app.translator.trans('avatar4eg-users-list.admin.modal_mail.title_all_text'); 36 | } else { 37 | title += ' ' + this.user.username() + ' (' + this.email() + ')'; 38 | } 39 | return title; 40 | } 41 | 42 | content() { 43 | return [ 44 | m('div', {className: 'Modal-body'}, [ 45 | m('form', { 46 | className: 'Form', 47 | onsubmit: this.onsubmit.bind(this) 48 | }, 49 | [ 50 | this.forAll ? '' : m('div', {className: 'Form-group'}, [ 51 | m('label', {}, app.translator.trans('avatar4eg-users-list.admin.modal_mail.email_label')), 52 | m('input', { 53 | className: 'FormControl', 54 | value: this.email(), 55 | oninput: m.withAttr('value', this.oninputEmail.bind(this)) 56 | }) 57 | ]), 58 | m('div', {className: 'Form-group'}, [ 59 | m('label', {}, app.translator.trans('avatar4eg-users-list.admin.modal_mail.subject_label')), 60 | m('input', { 61 | className: 'FormControl', 62 | value: this.subject(), 63 | oninput: m.withAttr('value', this.subject) 64 | }) 65 | ]), 66 | m('div', {className: 'Form-group'}, [ 67 | m('label', {}, app.translator.trans('avatar4eg-users-list.admin.modal_mail.message_label')), 68 | m('textarea', { 69 | className: 'FormControl', 70 | rows: 10, 71 | style: "resize: vertical;", 72 | value: this.messageText(), 73 | oninput: m.withAttr('value', this.messageText) 74 | }) 75 | ]), 76 | Button.component({ 77 | type: 'submit', 78 | className: 'Button Button--primary EditContactModal-save', 79 | loading: this.loading, 80 | children: app.translator.trans('avatar4eg-users-list.admin.modal_mail.submit_button'), 81 | disabled: this.submitDisabled 82 | }) 83 | ] 84 | ) 85 | ]) 86 | ]; 87 | } 88 | 89 | oninputEmail(value) { 90 | this.email(value); 91 | this.submitDisabled = !this.checkEmail(value); 92 | } 93 | 94 | checkEmail(email) { 95 | const emailRegexp = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i; 96 | 97 | var correct = true; 98 | var emails = this.splitEmails(email); 99 | emails.forEach(function (email) { 100 | if (!emailRegexp.test(email)) { 101 | correct = false; 102 | } 103 | } 104 | ); 105 | return correct; 106 | } 107 | 108 | splitEmails(email) { 109 | email = email.replace(/\s*/g,''); 110 | return email.split(','); 111 | } 112 | 113 | onsubmit(e) { 114 | e.preventDefault(); 115 | 116 | this.loading = true; 117 | 118 | var data = { 119 | emails: this.forAll ? [] : this.splitEmails(this.email()), 120 | subject: this.subject(), 121 | text: this.messageText(), 122 | forAll: this.forAll 123 | }; 124 | 125 | app.request({ 126 | method: 'POST', 127 | url: app.forum.attribute('apiUrl') + '/admin-mail', 128 | data: {data} 129 | }).then( 130 | () => { 131 | this.hide(); 132 | }, 133 | response => { 134 | this.loading = false; 135 | this.handleErrors(response); 136 | } 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /js/admin/src/components/UsersListPage.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/app'; 2 | import Page from 'flarum/components/Page'; 3 | import Button from 'flarum/components/Button'; 4 | import LoadingIndicator from 'flarum/components/LoadingIndicator'; 5 | import humanTime from 'flarum/helpers/humanTime'; 6 | import icon from 'flarum/helpers/icon'; 7 | 8 | import EmailUserModal from 'avatar4eg/users-list/components/EmailUserModal'; 9 | 10 | function UserItem(user) { 11 | const url = app.forum.attribute('baseUrl') + '/u/' + user.id(); 12 | const online = user.isOnline(); 13 | 14 | return [ 15 | m('li', {"data-id": user.id()}, [ 16 | m('div', {className: 'UsersListItem-info'}, [ 17 | m('span', {className: 'UsersListItem-name'}, [ 18 | user.username(), 19 | ]), 20 | m('span', {className: 'UserCard-lastSeen' + (online ? ' online' : '')}, [ 21 | online 22 | ? [icon('circle'), ' ', app.translator.trans('avatar4eg-users-list.admin.page.online_text')] 23 | : [icon('clock-o'), ' ', humanTime(user.lastSeenTime())] 24 | ]), 25 | m('span', {className: 'UsersListItem-comments'}, [ 26 | icon('comment-o'), 27 | user.commentsCount() 28 | ]), 29 | m('span', {className: 'UsersListItem-discussions'}, [ 30 | icon('reorder'), 31 | user.discussionsCount() 32 | ]), 33 | m('a', { 34 | className: 'Button Button--link', 35 | target: '_blank', 36 | href: url 37 | }, [ 38 | icon('eye') 39 | ]), 40 | Button.component({ 41 | className: 'Button Button--link', 42 | icon: 'envelope', 43 | onclick: function (e) { 44 | e.preventDefault(); 45 | app.modal.show(new EmailUserModal({user})); 46 | } 47 | }) 48 | ]) 49 | ]) 50 | ]; 51 | } 52 | 53 | export default class UsersListPage extends Page { 54 | init() { 55 | super.init(); 56 | 57 | this.loading = true; 58 | this.moreResults = false; 59 | this.users = []; 60 | this.refresh(); 61 | } 62 | 63 | view() { 64 | let loading; 65 | 66 | if (this.loading) { 67 | loading = LoadingIndicator.component(); 68 | } else if (this.moreResults) { 69 | loading = Button.component({ 70 | children: app.translator.trans('avatar4eg-users-list.admin.page.load_more_button'), 71 | className: 'Button', 72 | onclick: this.loadMore.bind(this) 73 | }); 74 | } 75 | 76 | return [ 77 | m('div', {className: 'UsersListPage'}, [ 78 | m('div', {className: 'UsersListPage-header'}, [ 79 | m('div', {className: 'container'}, [ 80 | m('p', {}, app.translator.trans('avatar4eg-users-list.admin.page.about_text')), 81 | Button.component({ 82 | className: 'Button Button--primary', 83 | icon: 'plus', 84 | children: app.translator.trans('avatar4eg-users-list.admin.page.mail_all_button'), 85 | onclick: () => app.modal.show(new EmailUserModal({'forAll': true})) 86 | }) 87 | ]) 88 | ]), 89 | m('div', {className: 'UsersListPage-list'}, [ 90 | m('div', {className: 'container'}, [ 91 | m('div', {className: 'UsersListItems'}, [ 92 | m('label', {}, app.translator.trans('avatar4eg-users-list.admin.page.list_title')), 93 | m('ol', { 94 | className: 'UsersList' 95 | }, 96 | [this.users.map(UserItem)] 97 | ), 98 | m('div', {className: 'UsersListPage-loadMore'}, [loading]) 99 | ]) 100 | ]) 101 | ]) 102 | ]) 103 | ]; 104 | } 105 | 106 | refresh(clear = true) { 107 | if (clear) { 108 | this.loading = true; 109 | this.users = []; 110 | } 111 | 112 | return this.loadResults().then( 113 | results => { 114 | this.users = []; 115 | this.parseResults(results); 116 | }, 117 | () => { 118 | this.loading = false; 119 | m.redraw(); 120 | } 121 | ); 122 | } 123 | 124 | loadResults(offset) { 125 | const params = {}; 126 | params.page = { 127 | offset: offset, 128 | limit: 50 129 | }; 130 | params.sort = 'username'; 131 | 132 | return app.store.find('users', params); 133 | } 134 | 135 | loadMore() { 136 | this.loading = true; 137 | 138 | this.loadResults(this.users.length) 139 | .then(this.parseResults.bind(this)); 140 | } 141 | 142 | parseResults(results) { 143 | [].push.apply(this.users, results); 144 | 145 | this.loading = false; 146 | this.moreResults = !!results.payload.links.next; 147 | 148 | m.lazyRedraw(); 149 | 150 | return results; 151 | } 152 | } -------------------------------------------------------------------------------- /js/admin/src/main.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/app'; 2 | import addUsersListPane from 'avatar4eg/users-list/addUsersListPane'; 3 | 4 | app.initializers.add('avatar4eg-users-list', app => { 5 | addUsersListPane(); 6 | }); 7 | -------------------------------------------------------------------------------- /less/admin/UsersListPage.less: -------------------------------------------------------------------------------- 1 | .UsersListPage { 2 | .UsersListPage-header { 3 | background: @control-bg; 4 | color: @control-color; 5 | padding: 20px 0; 6 | 7 | p { 8 | margin-bottom: 20px; 9 | } 10 | .Button { 11 | margin-right: 10px; 12 | } 13 | } 14 | 15 | .UsersListPage-list { 16 | padding: 20px 0; 17 | 18 | .UsersListItems { 19 | padding-left: 150px; 20 | 21 | > label { 22 | margin-left: -150px; 23 | float: left; 24 | font-weight: bold; 25 | margin-top: 14px; 26 | } 27 | 28 | .UsersList { 29 | list-style: none; 30 | padding: 10px 0; 31 | margin: 0; 32 | color: @muted-color; 33 | font-size: 16px; 34 | 35 | > .sortable-placeholder { 36 | height: 34px; 37 | margin-bottom: 10px; 38 | } 39 | 40 | li { 41 | .UsersListItem-info { 42 | padding: 5px 10px; 43 | border-radius: @border-radius; 44 | 45 | &:hover { background: @control-bg; } 46 | & > * { display: inline-block; } 47 | 48 | .UsersListItem-name, 49 | .UserCard-lastSeen { 50 | width: 200px; 51 | margin: 0; 52 | } 53 | 54 | .UsersListItem-comments, 55 | .UsersListItem-discussions { 56 | width: 70px; 57 | } 58 | 59 | .Button { 60 | float: right; 61 | margin: -8px -10px -8px 10px; 62 | } 63 | } 64 | } 65 | } 66 | 67 | @media only screen and (max-width: 529px) { 68 | .UsersList { 69 | li { 70 | .UsersListItem-info { 71 | font-size: 12px; 72 | 73 | .UsersListItem-name { width: 110px; } 74 | .UserCard-lastSeen { display: none; } 75 | 76 | .UsersListItem-comments, 77 | .UsersListItem-discussions { 78 | width: 50px; 79 | } 80 | 81 | .Button { 82 | float: right; 83 | margin-left: 0; 84 | padding: 8px; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | @media only screen and (max-width: 1024px) { 92 | padding-left: 0; 93 | > label { 94 | margin-left: 0; 95 | float: none; 96 | } 97 | } 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /less/admin/extension.less: -------------------------------------------------------------------------------- 1 | @import "UsersListPage.less"; 2 | -------------------------------------------------------------------------------- /locale/en.yml: -------------------------------------------------------------------------------- 1 | avatar4eg-users-list: 2 | admin: 3 | nav: 4 | users_button: => avatar4eg-users-list.ref.users_title 5 | users_text: View users list. 6 | page: 7 | about_text: View list of registered users. 8 | online_text: => core.forum.user.online_text 9 | load_more_button: => core.ref.load_more 10 | list_title: => avatar4eg-users-list.ref.users_title 11 | mail_all_button: Mail to all users 12 | modal_mail: 13 | title_text: Message for 14 | title_all_text: all users 15 | email_label: To 16 | subject_label: Subject 17 | default_subject: => avatar4eg-users-list.ref.default_subject 18 | message_label: Message 19 | submit_button: Send 20 | 21 | email: 22 | default_subject: => avatar4eg-users-list.ref.default_subject 23 | 24 | ref: 25 | default_subject: Message from forum administration 26 | users_title: Users -------------------------------------------------------------------------------- /locale/ru.yml: -------------------------------------------------------------------------------- 1 | avatar4eg-users-list: 2 | admin: 3 | nav: 4 | users_button: => avatar4eg-users-list.ref.users_title 5 | users_text: Список пользователей форума. 6 | page: 7 | about_text: Список зарегистрированных пользователей форума. 8 | online_text: => core.forum.user.online_text 9 | load_more_button: => core.ref.load_more 10 | list_title: => avatar4eg-users-list.ref.users_title 11 | mail_all_button: Сообщение всем пользователям 12 | modal_mail: 13 | title_text: Сообщение для 14 | title_all_text: всех участников форума 15 | email_label: Кому 16 | subject_label: Тема 17 | default_subject: => avatar4eg-users-list.ref.default_subject 18 | message_label: Сообщение 19 | submit_button: Отправить 20 | 21 | email: 22 | default_subject: => avatar4eg-users-list.ref.default_subject 23 | 24 | ref: 25 | default_subject: Сообщение от администрации форума 26 | users_title: Пользователи -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Users-list by Avatar4eg 2 | 3 | A [Flarum](http://flarum.org) extension that adds users list to admin panel. 4 | 5 | ### Screenshots 6 | 7 | Users list page: 8 | ![Imgur](https://i.imgur.com/JSlVsEn.png) 9 | Mail modal: 10 | ![Imgur](https://i.imgur.com/PIHr4mT.png) 11 | 12 | ### Goals 13 | 14 | - Allow admin to view list of users registered users. 15 | - Allow admin to send emails to user or all users. 16 | 17 | ### Installation 18 | 19 | ```bash 20 | composer require avatar4eg/flarum-ext-users-list 21 | ``` 22 | 23 | ### Configuration 24 | 25 | No configuration needed. 26 | 27 | ### Issues 28 | 29 | - For now (while flarum/core[#987](https://github.com/flarum/core/issues/978)) sending mail for all users may have errors due php max_execution_time limit. 30 | 31 | ## End-user usage 32 | 33 | On admin panel click Users button to view users. For each user there are buttons (mail and view user) and short info (last seen and post/discussion counter). 34 | 35 | ### Links 36 | 37 | - [on github](https://github.com/avatar4eg/flarum-ext-users-list) 38 | - [on packagist](https://packagist.com/packages/avatar4eg/flarum-ext-users-list) 39 | - [issues](https://github.com/avatar4eg/flarum-ext-users-list/issues) 40 | -------------------------------------------------------------------------------- /src/Api/Controller/SendAdminEmailController.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 47 | $this->mailer = $mailer; 48 | $this->translator = $translator; 49 | $this->users = $users; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | * @throws \InvalidArgumentException 55 | */ 56 | public function handle(ServerRequestInterface $request) 57 | { 58 | $actor = $request->getAttribute('actor'); 59 | 60 | if ($actor !== null && $actor->isAdmin()) { 61 | $data = array_get($request->getParsedBody(), 'data', []); 62 | 63 | if ($data['forAll']) { 64 | $users = $this->users->query()->whereVisibleTo($actor)->get(); 65 | foreach ($users as $user) { 66 | $this->sendMail($user->email, $data['subject'], $data['text']); 67 | } 68 | } else { 69 | foreach ($data['emails'] as $email) { 70 | $this->sendMail($email, $data['subject'], $data['text']); 71 | } 72 | } 73 | } 74 | 75 | return new EmptyResponse; 76 | } 77 | 78 | protected function sendMail($email, $subject, $text) 79 | { 80 | $this->mailer->queue(['raw' => $text], [], function (Message $message) use ($email, $subject) { 81 | $message->to($email); 82 | $message->subject('[' . $this->settings->get('forum_title') . '] ' . ($subject !== '' ? $subject : $this->translator->trans('avatar4eg-users-list.email.default_subject'))); 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Listener/AddAdminMailApi.php: -------------------------------------------------------------------------------- 1 | listen(ConfigureApiRoutes::class, [$this, 'configureApiRoutes']); 18 | //$events->listen(PrepareApiAttributes::class, [$this, 'prepareApiAttributes']); 19 | } 20 | 21 | /** 22 | * @param ConfigureApiRoutes $event 23 | */ 24 | public function configureApiRoutes(ConfigureApiRoutes $event) 25 | { 26 | $event->post('/admin-mail', 'avatar4eg.users-list.create-mail', SendAdminEmailController::class); 27 | } 28 | 29 | // /** 30 | // * @param PrepareApiAttributes $event 31 | // */ 32 | // public function prepareApiAttributes(PrepareApiAttributes $event) 33 | // { 34 | // if ($event->isSerializer(ForumSerializer::class)) { 35 | // $event->attributes['canAddGeotags'] = $event->actor->can('avatar4eg.geotags.create'); 36 | // } 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /src/Listener/AddClientAssets.php: -------------------------------------------------------------------------------- 1 | listen(ConfigureClientView::class, [$this, 'addAssets']); 14 | $events->listen(ConfigureLocales::class, [$this, 'addLocales']); 15 | } 16 | 17 | public function addAssets(ConfigureClientView $event) 18 | { 19 | if ($event->isAdmin()) { 20 | $event->addAssets([ 21 | __DIR__ . '/../../js/admin/dist/extension.js', 22 | __DIR__ . '/../../less/admin/extension.less' 23 | ]); 24 | $event->addBootstrapper('avatar4eg/users-list/main'); 25 | } 26 | } 27 | 28 | public function addLocales(ConfigureLocales $event) 29 | { 30 | foreach (new DirectoryIterator(__DIR__ .'/../../locale') as $file) { 31 | if ($file->isFile() && in_array($file->getExtension(), ['yml', 'yaml'], false)) { 32 | $event->locales->addTranslations($file->getBasename('.' . $file->getExtension()), $file->getPathname()); 33 | } 34 | } 35 | } 36 | } --------------------------------------------------------------------------------