mirror of
https://github.com/flarum/core.git
synced 2025-08-04 15:37:51 +02:00
Add notifications, and frontend framework rewrite changes changelog file
This commit is contained in:
19
FRONTEND FRAMEWORK REWRITE CHANGES.md
Normal file
19
FRONTEND FRAMEWORK REWRITE CHANGES.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
### Changes
|
||||||
|
|
||||||
|
* Mithril
|
||||||
|
- See changes from v0.2.x @ https://mithril.js.org/migration-v02x.html
|
||||||
|
- Kept `m.prop` and `m.withAttr`
|
||||||
|
- Actual Promises are used now instead of `m.deferred`
|
||||||
|
* Component
|
||||||
|
- Use new Mithril lifecycle hooks (`component.config` is gone)
|
||||||
|
- `component.render` is gone
|
||||||
|
* Application
|
||||||
|
- New different methods
|
||||||
|
- `app.bus` for some event hooking
|
||||||
|
* Translator
|
||||||
|
- Added `app.translator.transText`, automatically extracts text from `translator.trans` output
|
||||||
|
|
||||||
|
#### Forum
|
||||||
|
* Forum Application
|
||||||
|
- Renamed to `Forum`
|
||||||
|
- `app.search` is no longer global, extend using `extend`
|
@@ -7,6 +7,7 @@ import Store from './Store';
|
|||||||
|
|
||||||
import extract from './utils/extract';
|
import extract from './utils/extract';
|
||||||
import mapRoutes from './utils/mapRoutes';
|
import mapRoutes from './utils/mapRoutes';
|
||||||
|
import Drawer from './utils/Drawer';
|
||||||
import {extend} from './extend';
|
import {extend} from './extend';
|
||||||
|
|
||||||
import Forum from './models/Forum';
|
import Forum from './models/Forum';
|
||||||
@@ -14,6 +15,7 @@ import Discussion from './models/Discussion';
|
|||||||
import User from './models/User';
|
import User from './models/User';
|
||||||
import Post from './models/Post';
|
import Post from './models/Post';
|
||||||
import Group from './models/Group';
|
import Group from './models/Group';
|
||||||
|
import Notification from './models/Notification';
|
||||||
|
|
||||||
import RequestError from './utils/RequestError';
|
import RequestError from './utils/RequestError';
|
||||||
import Alert from './components/Alert';
|
import Alert from './components/Alert';
|
||||||
@@ -32,7 +34,7 @@ export default abstract class Application {
|
|||||||
*/
|
*/
|
||||||
forum: Forum;
|
forum: Forum;
|
||||||
|
|
||||||
data: ApplicationData | undefined;
|
data: ApplicationData;
|
||||||
|
|
||||||
translator = new Translator();
|
translator = new Translator();
|
||||||
bus = new Bus();
|
bus = new Bus();
|
||||||
@@ -51,9 +53,17 @@ export default abstract class Application {
|
|||||||
discussions: Discussion,
|
discussions: Discussion,
|
||||||
posts: Post,
|
posts: Post,
|
||||||
groups: Group,
|
groups: Group,
|
||||||
// notifications: Notification
|
notifications: Notification
|
||||||
});
|
});
|
||||||
|
|
||||||
|
drawer = new Drawer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local cache that can be used to store data at the application level, so
|
||||||
|
* that is persists between different routes.
|
||||||
|
*/
|
||||||
|
cache = {};
|
||||||
|
|
||||||
routes = {};
|
routes = {};
|
||||||
|
|
||||||
title = '';
|
title = '';
|
||||||
@@ -69,8 +79,6 @@ export default abstract class Application {
|
|||||||
// this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
|
// this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
|
||||||
// this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
|
// this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
|
||||||
|
|
||||||
// this.drawer = new Drawer();
|
|
||||||
|
|
||||||
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +159,7 @@ export default abstract class Application {
|
|||||||
route(name: string, params: object = {}): string {
|
route(name: string, params: object = {}): string {
|
||||||
const route = this.routes[name];
|
const route = this.routes[name];
|
||||||
|
|
||||||
if (!route) throw new Error(`Route ${name} does not exist`);
|
if (!route) throw new Error(`Route '${name}' does not exist`);
|
||||||
|
|
||||||
const url = route.path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
const url = route.path.replace(/:([^\/]+)/g, (m, key) => extract(params, key));
|
||||||
const queryString = m.buildQueryString(params);
|
const queryString = m.buildQueryString(params);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import extract from './utils/extract';
|
import extract from './utils/extract';
|
||||||
|
import extractText from './utils/extractText';
|
||||||
import username from './helpers/username';
|
import username from './helpers/username';
|
||||||
import extractText from "./utils/extractText";
|
|
||||||
|
|
||||||
type Translations = { [key: string]: string };
|
type Translations = { [key: string]: string };
|
||||||
|
|
||||||
|
18
js/src/common/models/Notification.ts
Normal file
18
js/src/common/models/Notification.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Model from '../Model';
|
||||||
|
import User from './User';
|
||||||
|
|
||||||
|
export default class Notification extends Model {
|
||||||
|
static ADMINISTRATOR_ID = '1';
|
||||||
|
static GUEST_ID = '2';
|
||||||
|
static MEMBER_ID = '3';
|
||||||
|
|
||||||
|
contentType = Model.attribute('contentType') as () => string;
|
||||||
|
content = Model.attribute('content') as () => string;
|
||||||
|
createdAt = Model.attribute('createdAt', Model.transformDate) as () => Date;
|
||||||
|
|
||||||
|
isRead = Model.attribute('isRead') as () => boolean;
|
||||||
|
|
||||||
|
user = Model.hasOne('user') as () => User;
|
||||||
|
fromUser = Model.hasOne('fromUser') as () => User;
|
||||||
|
subject = Model.hasOne('subhect') as () => any;
|
||||||
|
}
|
51
js/src/common/utils/Drawer.ts
Normal file
51
js/src/common/utils/Drawer.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* The `Drawer` class controls the page's drawer. The drawer is the area the
|
||||||
|
* slides out from the left on mobile devices; it contains the header and the
|
||||||
|
* footer.
|
||||||
|
*/
|
||||||
|
export default class Drawer {
|
||||||
|
private $backdrop?: ZeptoCollection;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Set up an event handler so that whenever the content area is tapped,
|
||||||
|
// the drawer will close.
|
||||||
|
$('#content').click(e => {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether or not the drawer is currently open.
|
||||||
|
*/
|
||||||
|
isOpen(): boolean {
|
||||||
|
return $('#app').hasClass('drawerOpen');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide the drawer.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
$('#app').removeClass('drawerOpen');
|
||||||
|
|
||||||
|
if (this.$backdrop) this.$backdrop.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the drawer.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
$('#app').addClass('drawerOpen');
|
||||||
|
|
||||||
|
this.$backdrop = $('<div/>')
|
||||||
|
.addClass('drawer-backdrop fade')
|
||||||
|
.appendTo('body')
|
||||||
|
.click(() => this.hide());
|
||||||
|
|
||||||
|
setTimeout(() => this.$backdrop.addClass('in'));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
class Item {
|
class Item<T> {
|
||||||
content: any;
|
content: T;
|
||||||
priority: number;
|
priority: number;
|
||||||
key: number = 0;
|
key: number = 0;
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ class Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ItemList {
|
export default class ItemList<T = any> {
|
||||||
private items: { [key: string]: Item } = {};
|
private items: { [key: string]: Item<T> } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the list is empty.
|
* Check whether the list is empty.
|
||||||
@@ -30,9 +30,6 @@ export default class ItemList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether an item is present in the list.
|
* Check whether an item is present in the list.
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
*/
|
||||||
has(key: any): boolean {
|
has(key: any): boolean {
|
||||||
return !!this.items[key];
|
return !!this.items[key];
|
||||||
@@ -40,12 +37,8 @@ export default class ItemList {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content of an item.
|
* Get the content of an item.
|
||||||
*
|
|
||||||
* @param {String} key
|
|
||||||
* @return {*}
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
get(key: any) {
|
get(key: any): T {
|
||||||
return this.items[key].content;
|
return this.items[key].content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +58,8 @@ export default class ItemList {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
toArray<T>(): T[] {
|
toArray(): T[] {
|
||||||
const items: Item[] = [];
|
const items: Item<T>[] = [];
|
||||||
|
|
||||||
for (const i in this.items) {
|
for (const i in this.items) {
|
||||||
if (this.items.hasOwnProperty(i)) {
|
if (this.items.hasOwnProperty(i)) {
|
||||||
|
@@ -30,7 +30,7 @@ export default class Forum extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.routes[defaultAction].path = '/';
|
this.routes[defaultAction].path = '/';
|
||||||
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
this.history.push(defaultAction, this.translator.transText('core.forum.header.back_to_index_tooltip'), '/');
|
||||||
|
|
||||||
// m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
// m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
||||||
// m.mount(document.getElementById('header-navigation'), Navigation.component());
|
// m.mount(document.getElementById('header-navigation'), Navigation.component());
|
||||||
@@ -47,7 +47,7 @@ export default class Forum extends Application {
|
|||||||
|
|
||||||
// Route the home link back home when clicked. We do not want it to register
|
// Route the home link back home when clicked. We do not want it to register
|
||||||
// if the user is opening it in a new tab, however.
|
// if the user is opening it in a new tab, however.
|
||||||
$('#home-link').click(e => {
|
$('#home-link').click((e: MouseEvent) => {
|
||||||
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
app.history.home();
|
app.history.home();
|
||||||
|
@@ -2,9 +2,9 @@ import Component from '../../common/Component';
|
|||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
// import LogInModal from './LogInModal';
|
// import LogInModal from './LogInModal';
|
||||||
// import SignUpModal from './SignUpModal';
|
// import SignUpModal from './SignUpModal';
|
||||||
// import SessionDropdown from './SessionDropdown';
|
import SessionDropdown from './SessionDropdown';
|
||||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||||
// import NotificationsDropdown from './NotificationsDropdown';
|
import NotificationsDropdown from './NotificationsDropdown';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export default class HeaderSecondary extends Component {
|
|||||||
|
|
||||||
items.add('search', Search.component(), 30);
|
items.add('search', Search.component(), 30);
|
||||||
|
|
||||||
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 0) {
|
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) {
|
||||||
const locales = [];
|
const locales = [];
|
||||||
|
|
||||||
for (const locale in app.data.locales) {
|
for (const locale in app.data.locales) {
|
||||||
@@ -58,8 +58,8 @@ export default class HeaderSecondary extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (app.session.user) {
|
if (app.session.user) {
|
||||||
// items.add('notifications', NotificationsDropdown.component(), 10);
|
items.add('notifications', NotificationsDropdown.component(), 10);
|
||||||
// items.add('session', SessionDropdown.component(), 0);
|
items.add('session', SessionDropdown.component(), 0);
|
||||||
} else {
|
} else {
|
||||||
if (app.forum.attribute('allowSignUp')) {
|
if (app.forum.attribute('allowSignUp')) {
|
||||||
items.add('signUp',
|
items.add('signUp',
|
||||||
|
201
js/src/forum/components/NotificationList.tsx
Normal file
201
js/src/forum/components/NotificationList.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import Component from '../../common/Component';
|
||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
|
import Notification from '../../common/models/Notification';
|
||||||
|
import Discussion from '../../common/models/Discussion';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `NotificationList` component displays a list of the logged-in user's
|
||||||
|
* notifications, grouped by discussion.
|
||||||
|
*/
|
||||||
|
export default class NotificationList extends Component {
|
||||||
|
/**
|
||||||
|
* Whether or not the notifications are loading.
|
||||||
|
*/
|
||||||
|
loading: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not there are more results that can be loaded.
|
||||||
|
*/
|
||||||
|
moreResults: boolean = false;
|
||||||
|
|
||||||
|
private $scrollParent: ZeptoCollection;
|
||||||
|
private scrollHandler: () => void;
|
||||||
|
|
||||||
|
view() {
|
||||||
|
const pages = app.cache.notifications || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="NotificationList">
|
||||||
|
<div className="NotificationList-header">
|
||||||
|
<div className="App-primaryControl">
|
||||||
|
{Button.component({
|
||||||
|
className: 'Button Button--icon Button--link',
|
||||||
|
icon: 'fas fa-check',
|
||||||
|
title: app.translator.transText('core.forum.notifications.mark_all_as_read_tooltip'),
|
||||||
|
onclick: this.markAllAsRead.bind(this)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="NotificationList-content">
|
||||||
|
{pages.length ? pages.map(notifications => {
|
||||||
|
const groups = [];
|
||||||
|
const discussions = {};
|
||||||
|
|
||||||
|
notifications.forEach(notification => {
|
||||||
|
const subject = notification.subject();
|
||||||
|
|
||||||
|
if (typeof subject === 'undefined') return;
|
||||||
|
|
||||||
|
// Get the discussion that this notification is related to. If it's not
|
||||||
|
// directly related to a discussion, it may be related to a post or
|
||||||
|
// other entity which is related to a discussion.
|
||||||
|
let discussion: any = false;
|
||||||
|
if (subject instanceof Discussion) discussion = subject;
|
||||||
|
else if (subject && subject.discussion) discussion = subject.discussion();
|
||||||
|
|
||||||
|
// If the notification is not related to a discussion directly or
|
||||||
|
// indirectly, then we will assign it to a neutral group.
|
||||||
|
const key = discussion ? discussion.id() : 0;
|
||||||
|
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
|
||||||
|
discussions[key].notifications.push(notification);
|
||||||
|
|
||||||
|
if (groups.indexOf(discussions[key]) === -1) {
|
||||||
|
groups.push(discussions[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups.map(group => {
|
||||||
|
const badges = group.discussion && group.discussion.badges().toArray();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="NotificationGroup">
|
||||||
|
{group.discussion
|
||||||
|
? (
|
||||||
|
<a className="NotificationGroup-header"
|
||||||
|
href={app.route.discussion(group.discussion)}
|
||||||
|
config={m.route}>
|
||||||
|
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
||||||
|
{group.discussion.title()}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="NotificationGroup-header">
|
||||||
|
{app.forum.attribute('title')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul className="NotificationGroup-content">
|
||||||
|
{group.notifications.map(notification => {
|
||||||
|
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
||||||
|
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}) : ''}
|
||||||
|
{this.loading
|
||||||
|
? <LoadingIndicator className="LoadingIndicator--block" />
|
||||||
|
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
const $notifications = this.$('.NotificationList-content');
|
||||||
|
const $scrollParent = this.$scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
|
||||||
|
|
||||||
|
this.scrollHandler = () => {
|
||||||
|
const scrollTop = $scrollParent.scrollTop();
|
||||||
|
const viewportHeight = $scrollParent.height();
|
||||||
|
const contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
|
||||||
|
const contentHeight = $notifications[0].scrollHeight;
|
||||||
|
|
||||||
|
if (this.moreResults && !this.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scrollParent.on('scroll', this.scrollHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
onremove(vnode) {
|
||||||
|
super.onremove(vnode);
|
||||||
|
|
||||||
|
this.$scrollParent.off('scroll', this.scrollHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load notifications into the application's cache if they haven't already
|
||||||
|
* been loaded.
|
||||||
|
*/
|
||||||
|
load() {
|
||||||
|
if (app.session.user.newNotificationCount()) {
|
||||||
|
delete app.cache.notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.cache.notifications) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.session.user.pushAttributes({newNotificationCount: 0});
|
||||||
|
|
||||||
|
this.loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the next page of notification results.
|
||||||
|
*/
|
||||||
|
loadMore() {
|
||||||
|
this.loading = true;
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
const params = app.cache.notifications ? {page: {offset: app.cache.notifications.length * 10}} : null;
|
||||||
|
|
||||||
|
return app.store.find('notifications', params)
|
||||||
|
.then(this.parseResults.bind(this))
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse results and append them to the notification list.
|
||||||
|
*/
|
||||||
|
parseResults(results: Notification[]|any): Notification[]|any {
|
||||||
|
app.cache.notifications = app.cache.notifications || [];
|
||||||
|
|
||||||
|
if (results.length) app.cache.notifications.push(results);
|
||||||
|
|
||||||
|
this.moreResults = !!results.payload.links.next;
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all of the notifications as read.
|
||||||
|
*/
|
||||||
|
markAllAsRead() {
|
||||||
|
if (!app.cache.notifications) return;
|
||||||
|
|
||||||
|
app.session.user.pushAttributes({unreadNotificationCount: 0});
|
||||||
|
|
||||||
|
app.cache.notifications.forEach(notifications => {
|
||||||
|
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
|
||||||
|
});
|
||||||
|
|
||||||
|
app.request({
|
||||||
|
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
73
js/src/forum/components/NotificationsDropdown.tsx
Normal file
73
js/src/forum/components/NotificationsDropdown.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
import NotificationList from './NotificationList';
|
||||||
|
|
||||||
|
export default class NotificationsDropdown extends Dropdown {
|
||||||
|
list = new NotificationList();
|
||||||
|
|
||||||
|
static initProps(props) {
|
||||||
|
props.className = props.className || 'NotificationsDropdown';
|
||||||
|
props.buttonClassName = props.buttonClassName || 'Button Button--flat';
|
||||||
|
props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
|
||||||
|
props.label = props.label || app.translator.trans('core.forum.notifications.tooltip');
|
||||||
|
props.icon = props.icon || 'fas fa-bell';
|
||||||
|
|
||||||
|
super.initProps(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
getButton() {
|
||||||
|
const newNotifications = this.getNewCount();
|
||||||
|
const vdom = super.getButton();
|
||||||
|
|
||||||
|
vdom.attrs.title = this.props.label;
|
||||||
|
|
||||||
|
vdom.attrs.className += (newNotifications ? ' new' : '');
|
||||||
|
vdom.attrs.onclick = this.onclick.bind(this);
|
||||||
|
|
||||||
|
return vdom;
|
||||||
|
}
|
||||||
|
|
||||||
|
getButtonContent() {
|
||||||
|
const unread = this.getUnreadCount();
|
||||||
|
|
||||||
|
return [
|
||||||
|
icon(this.props.icon, {className: 'Button-icon'}),
|
||||||
|
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
|
||||||
|
<span className="Button-label">{this.props.label}</span>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
getMenu() {
|
||||||
|
return (
|
||||||
|
<div className={'Dropdown-menu ' + this.props.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||||
|
{this.showing ? m(this.list) : ''}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onclick() {
|
||||||
|
if (app.drawer.isOpen()) {
|
||||||
|
this.goToRoute();
|
||||||
|
} else {
|
||||||
|
this.list.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goToRoute() {
|
||||||
|
m.route(app.route('notifications'));
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnreadCount() {
|
||||||
|
return app.session.user.unreadNotificationCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewCount() {
|
||||||
|
return app.session.user.newNotificationCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
menuClick(e) {
|
||||||
|
// Don't close the notifications dropdown if the user is opening a link in a
|
||||||
|
// new tab or window.
|
||||||
|
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
@@ -7,6 +7,8 @@ import DiscussionsSearchSource from './DiscussionsSearchSource';
|
|||||||
import UsersSearchSource from './UsersSearchSource';
|
import UsersSearchSource from './UsersSearchSource';
|
||||||
import SearchSource from './SearchSource';
|
import SearchSource from './SearchSource';
|
||||||
|
|
||||||
|
import Stream from 'mithril/stream';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `Search` component displays a menu of as-you-type results from a variety
|
* The `Search` component displays a menu of as-you-type results from a variety
|
||||||
* of sources.
|
* of sources.
|
||||||
@@ -20,7 +22,7 @@ export default class Search extends Component {
|
|||||||
/**
|
/**
|
||||||
* The value of the search input.
|
* The value of the search input.
|
||||||
*/
|
*/
|
||||||
value: Function = m.prop('');
|
value: Stream<string> = m.prop('');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the search input has focus.
|
* Whether or not the search input has focus.
|
||||||
@@ -72,8 +74,6 @@ export default class Search extends Component {
|
|||||||
// Hide the search view if no sources were loaded
|
// Hide the search view if no sources were loaded
|
||||||
if (!this.sources.length) return <div/>;
|
if (!this.sources.length) return <div/>;
|
||||||
|
|
||||||
console.log('Search#view - loading:', this.loadingSources)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'Search ' + classNames({
|
<div className={'Search ' + classNames({
|
||||||
open: this.value() && this.hasFocus,
|
open: this.value() && this.hasFocus,
|
||||||
|
89
js/src/forum/components/SessionDropdown.tsx
Normal file
89
js/src/forum/components/SessionDropdown.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import avatar from '../../common/helpers/avatar';
|
||||||
|
import username from '../../common/helpers/username';
|
||||||
|
import Dropdown from '../../common/components/Dropdown';
|
||||||
|
import LinkButton from '../../common/components/LinkButton';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import Separator from '../../common/components/Separator';
|
||||||
|
import Group from '../../common/models/Group';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `SessionDropdown` component shows a button with the current user's
|
||||||
|
* avatar/name, with a dropdown of session controls.
|
||||||
|
*/
|
||||||
|
export default class SessionDropdown extends Dropdown {
|
||||||
|
static initProps(props) {
|
||||||
|
super.initProps(props);
|
||||||
|
|
||||||
|
props.className = 'SessionDropdown';
|
||||||
|
props.buttonClassName = 'Button Button--user Button--flat';
|
||||||
|
props.menuClassName = 'Dropdown-menu--right';
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
this.props.children = this.items().toArray();
|
||||||
|
|
||||||
|
return super.view();
|
||||||
|
}
|
||||||
|
|
||||||
|
getButtonContent() {
|
||||||
|
const user = app.session.user;
|
||||||
|
|
||||||
|
return [
|
||||||
|
avatar(user), ' ',
|
||||||
|
<span className="Button-label">{username(user)}</span>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the contents of the dropdown menu.
|
||||||
|
*/
|
||||||
|
items(): ItemList {
|
||||||
|
const items = new ItemList();
|
||||||
|
const user = app.session.user;
|
||||||
|
|
||||||
|
// items.add('profile',
|
||||||
|
// LinkButton.component({
|
||||||
|
// icon: 'fas fa-user',
|
||||||
|
// children: app.translator.trans('core.forum.header.profile_button'),
|
||||||
|
// href: app.route.user(user)
|
||||||
|
// }),
|
||||||
|
// 100
|
||||||
|
// );
|
||||||
|
|
||||||
|
// items.add('settings',
|
||||||
|
// LinkButton.component({
|
||||||
|
// icon: 'fas fa-cog',
|
||||||
|
// children: app.translator.trans('core.forum.header.settings_button'),
|
||||||
|
// href: app.route('settings')
|
||||||
|
// }),
|
||||||
|
// 50
|
||||||
|
// );
|
||||||
|
|
||||||
|
if (app.forum.attribute('adminUrl')) {
|
||||||
|
items.add('administration',
|
||||||
|
LinkButton.component({
|
||||||
|
icon: 'fas fa-wrench',
|
||||||
|
children: app.translator.trans('core.forum.header.admin_button'),
|
||||||
|
href: app.forum.attribute('adminUrl'),
|
||||||
|
target: '_blank',
|
||||||
|
config: () => {}
|
||||||
|
}),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add('separator', Separator.component(), -90);
|
||||||
|
|
||||||
|
items.add('logOut',
|
||||||
|
Button.component({
|
||||||
|
icon: 'fas fa-sign-out-alt',
|
||||||
|
children: app.translator.trans('core.forum.header.log_out_button'),
|
||||||
|
onclick: app.session.logout.bind(app.session)
|
||||||
|
}),
|
||||||
|
-100
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user