mirror of
https://github.com/flarum/core.git
synced 2025-08-11 10:55:47 +02:00
forum: add NotificationGrid and SettingsPage - missing modals
This commit is contained in:
2
js/dist/admin.js
vendored
2
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
6
js/dist/forum.js
vendored
6
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -5,6 +5,14 @@ import computed from '../utils/computed';
|
|||||||
import GroupBadge from '../components/GroupBadge';
|
import GroupBadge from '../components/GroupBadge';
|
||||||
import Group from './Group';
|
import Group from './Group';
|
||||||
|
|
||||||
|
export interface UserPreferences {
|
||||||
|
discloseOnline?: boolean;
|
||||||
|
indexProfile?: boolean;
|
||||||
|
locale?: string;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export default class User extends Model {
|
export default class User extends Model {
|
||||||
username = Model.attribute('username') as () => string;
|
username = Model.attribute('username') as () => string;
|
||||||
|
|
||||||
@@ -14,7 +22,7 @@ export default class User extends Model {
|
|||||||
password = Model.attribute('password') as () => string;
|
password = Model.attribute('password') as () => string;
|
||||||
|
|
||||||
avatarUrl = Model.attribute('avatarUrl') as () => string;
|
avatarUrl = Model.attribute('avatarUrl') as () => string;
|
||||||
preferences = Model.attribute('preferences') as () => string;
|
preferences = Model.attribute('preferences') as () => UserPreferences;
|
||||||
groups = Model.hasMany('groups') as () => Group[];
|
groups = Model.hasMany('groups') as () => Group[];
|
||||||
|
|
||||||
joinTime = Model.attribute('joinTime', Model.transformDate) as () => Date;
|
joinTime = Model.attribute('joinTime', Model.transformDate) as () => Date;
|
||||||
|
@@ -7,6 +7,8 @@ import HeaderSecondary from './components/HeaderSecondary';
|
|||||||
import Page from './components/Page';
|
import Page from './components/Page';
|
||||||
import IndexPage from './components/IndexPage';
|
import IndexPage from './components/IndexPage';
|
||||||
import PostsUserPage from './components/PostsUserPage';
|
import PostsUserPage from './components/PostsUserPage';
|
||||||
|
import SettingsPage from './components/SettingsPage';
|
||||||
|
|
||||||
import User from '../common/models/User';
|
import User from '../common/models/User';
|
||||||
import Post from '../common/models/Post';
|
import Post from '../common/models/Post';
|
||||||
import Discussion from '../common/models/Discussion';
|
import Discussion from '../common/models/Discussion';
|
||||||
@@ -22,7 +24,7 @@ export default class Forum extends Application {
|
|||||||
'user.posts': { path: '/u/:username', component: PostsUserPage },
|
'user.posts': { path: '/u/:username', component: PostsUserPage },
|
||||||
'user.discussions': { path: '/u/:username/discussions', component: PostsUserPage },
|
'user.discussions': { path: '/u/:username/discussions', component: PostsUserPage },
|
||||||
|
|
||||||
settings: { path: '/settings', component: PostsUserPage },
|
settings: { path: '/settings', component: SettingsPage },
|
||||||
|
|
||||||
'index.filter': { path: '/:filter', component: IndexPage },
|
'index.filter': { path: '/:filter', component: IndexPage },
|
||||||
};
|
};
|
||||||
|
208
js/src/forum/components/NotificationGrid.tsx
Normal file
208
js/src/forum/components/NotificationGrid.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import Component, { ComponentProps } from '../../common/Component';
|
||||||
|
import Checkbox from '../../common/components/Checkbox';
|
||||||
|
import icon from '../../common/helpers/icon';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import User from '../../common/models/User';
|
||||||
|
|
||||||
|
export interface NotificationGridProps extends ComponentProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationItem = {
|
||||||
|
/**
|
||||||
|
* The name of the notification type/method.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The icon to display in the column header/notificatio grid row.
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The label to display in the column header/notification grid row.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `NotificationGrid` component displays a table of notification types and
|
||||||
|
* methods, allowing the user to toggle each combination.
|
||||||
|
*/
|
||||||
|
export default class NotificationGrid extends Component<NotificationGridProps> {
|
||||||
|
/**
|
||||||
|
* Information about the available notification methods.
|
||||||
|
*/
|
||||||
|
methods = this.notificationMethods().toArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of notification type-method combinations to the checkbox instances
|
||||||
|
* that represent them.
|
||||||
|
*/
|
||||||
|
inputs = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the available notification types.
|
||||||
|
*/
|
||||||
|
types = this.notificationTypes().toArray();
|
||||||
|
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
// For each of the notification type-method combinations, create and store a
|
||||||
|
// new checkbox component instance, which we will render in the view.
|
||||||
|
this.types.forEach(type =>
|
||||||
|
this.methods.forEach(method => {
|
||||||
|
const key = this.preferenceKey(type.name, method.name);
|
||||||
|
const preference = this.props.user.preferences()[key];
|
||||||
|
|
||||||
|
this.inputs[key] = new Checkbox({
|
||||||
|
state: !!preference,
|
||||||
|
disabled: typeof preference === 'undefined',
|
||||||
|
onchange: () => this.toggle([key]),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<table className="NotificationGrid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td />
|
||||||
|
{this.methods.map(method => (
|
||||||
|
<th className="NotificationGrid-groupToggle" onclick={this.toggleMethod.bind(this, method.name)}>
|
||||||
|
{icon(method.icon)} {method.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{this.types.map(type => (
|
||||||
|
<tr>
|
||||||
|
<td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}>
|
||||||
|
{icon(type.icon)} {type.label}
|
||||||
|
</td>
|
||||||
|
{this.methods.map(method => (
|
||||||
|
<td className="NotificationGrid-checkbox">{this.inputs[this.preferenceKey(type.name, method.name)].render()}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
|
||||||
|
const i = parseInt($(this).index(), 10) + 1;
|
||||||
|
$(this)
|
||||||
|
.parents('table')
|
||||||
|
.find('td:nth-child(' + i + ')')
|
||||||
|
.toggleClass('highlighted', e.type === 'mouseenter');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
|
||||||
|
$(this)
|
||||||
|
.parent()
|
||||||
|
.find('td')
|
||||||
|
.toggleClass('highlighted', e.type === 'mouseenter');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the state of the given preferences, based on the value of the first
|
||||||
|
* one.
|
||||||
|
*/
|
||||||
|
toggle(keys: string[]) {
|
||||||
|
const user = this.props.user;
|
||||||
|
const preferences = user.preferences();
|
||||||
|
const enabled = !preferences[keys[0]];
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
const control = this.inputs[key];
|
||||||
|
|
||||||
|
control.loading = true;
|
||||||
|
control.props.state = enabled;
|
||||||
|
preferences[key] = control.props.state = enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
user.save({ preferences }).then(() => {
|
||||||
|
keys.forEach(key => (this.inputs[key].loading = false));
|
||||||
|
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle all notification types for the given method.
|
||||||
|
*/
|
||||||
|
toggleMethod(method: string) {
|
||||||
|
const keys = this.types.map(type => this.preferenceKey(type.name, method)).filter(key => !this.inputs[key].props.disabled);
|
||||||
|
|
||||||
|
this.toggle(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle all notification methods for the given type.
|
||||||
|
*/
|
||||||
|
toggleType(type: string) {
|
||||||
|
const keys = this.methods.map(method => this.preferenceKey(type, method.name)).filter(key => !this.inputs[key].props.disabled);
|
||||||
|
|
||||||
|
this.toggle(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the preference key for the given notification type-method
|
||||||
|
* combination.
|
||||||
|
*/
|
||||||
|
preferenceKey(type: string, method: string): string {
|
||||||
|
return `notify_${type}_${method}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the notification methods to display in the grid.
|
||||||
|
*
|
||||||
|
* @see {NotificationItem}
|
||||||
|
*/
|
||||||
|
notificationMethods() {
|
||||||
|
const items = new ItemList<NotificationItem>();
|
||||||
|
|
||||||
|
items.add('alert', {
|
||||||
|
name: 'alert',
|
||||||
|
icon: 'fas fa-bell',
|
||||||
|
label: app.translator.trans('core.forum.settings.notify_by_web_heading'),
|
||||||
|
});
|
||||||
|
|
||||||
|
items.add('email', {
|
||||||
|
name: 'email',
|
||||||
|
icon: 'far fa-envelope',
|
||||||
|
label: app.translator.trans('core.forum.settings.notify_by_email_heading'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the notification types to display in the grid.
|
||||||
|
*
|
||||||
|
* @see {NotificationItem}
|
||||||
|
*/
|
||||||
|
notificationTypes() {
|
||||||
|
const items = new ItemList<NotificationItem>();
|
||||||
|
|
||||||
|
items.add('discussionRenamed', {
|
||||||
|
name: 'discussionRenamed',
|
||||||
|
icon: 'fas fa-pencil-alt',
|
||||||
|
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
134
js/src/forum/components/SettingsPage.tsx
Normal file
134
js/src/forum/components/SettingsPage.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import listItems from '../../common/helpers/listItems';
|
||||||
|
import ItemList from '../../common/utils/ItemList';
|
||||||
|
import Button from '../../common/components/Button';
|
||||||
|
import FieldSet from '../../common/components/FieldSet';
|
||||||
|
import Switch from '../../common/components/Switch';
|
||||||
|
import UserPage from './UserPage';
|
||||||
|
import NotificationGrid from './NotificationGrid';
|
||||||
|
|
||||||
|
export default class SettingsPage extends UserPage {
|
||||||
|
oninit(vnode) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
this.show(app.session.user);
|
||||||
|
app.setTitle(app.translator.transText('core.forum.settings.title'));
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
return (
|
||||||
|
<div className="SettingsPage">
|
||||||
|
<ul>{listItems(this.settingsItems().toArray())}</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the user's settings controls.
|
||||||
|
*/
|
||||||
|
settingsItems(): ItemList {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'account',
|
||||||
|
FieldSet.component({
|
||||||
|
label: app.translator.trans('core.forum.settings.account_heading'),
|
||||||
|
className: 'Settings-account',
|
||||||
|
children: this.accountItems().toArray(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'notifications',
|
||||||
|
FieldSet.component({
|
||||||
|
label: app.translator.trans('core.forum.settings.notifications_heading'),
|
||||||
|
className: 'Settings-notifications',
|
||||||
|
children: this.notificationsItems().toArray(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'privacy',
|
||||||
|
FieldSet.component({
|
||||||
|
label: app.translator.trans('core.forum.settings.privacy_heading'),
|
||||||
|
className: 'Settings-privacy',
|
||||||
|
children: this.privacyItems().toArray(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the user's account settings.
|
||||||
|
*/
|
||||||
|
accountItems(): ItemList {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'changePassword',
|
||||||
|
Button.component({
|
||||||
|
children: app.translator.trans('core.forum.settings.change_password_button'),
|
||||||
|
className: 'Button',
|
||||||
|
onclick: () => app.modal.show(new ChangePasswordModal()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'changeEmail',
|
||||||
|
Button.component({
|
||||||
|
children: app.translator.trans('core.forum.settings.change_email_button'),
|
||||||
|
className: 'Button',
|
||||||
|
onclick: () => app.modal.show(new ChangeEmailModal()),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the user's notification settings.
|
||||||
|
*/
|
||||||
|
notificationsItems(): ItemList {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add('notificationGrid', NotificationGrid.component({ user: this.user }));
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a callback that will save a value to the given preference.
|
||||||
|
*/
|
||||||
|
preferenceSaver(key: string): Function {
|
||||||
|
return (value, component) => {
|
||||||
|
if (component) component.loading = true;
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
this.user.savePreferences({ [key]: value }).then(() => {
|
||||||
|
if (component) component.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an item list for the user's privacy settings.
|
||||||
|
*/
|
||||||
|
privacyItems(): ItemList {
|
||||||
|
const items = new ItemList();
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
'discloseOnline',
|
||||||
|
Switch.component({
|
||||||
|
children: app.translator.trans('core.forum.settings.privacy_disclose_online_label'),
|
||||||
|
state: this.user.preferences().discloseOnline,
|
||||||
|
onchange: (value, component) => {
|
||||||
|
this.user.pushAttributes({ lastSeenAt: null });
|
||||||
|
this.preferenceSaver('discloseOnline')(value, component);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user