1
0
mirror of https://github.com/flarum/core.git synced 2025-08-05 16:07:34 +02:00

Drop mixin-like attributes and convert to typescript

This commit is contained in:
Clark Winkelmann
2020-10-25 14:29:41 +01:00
parent 7055f6d941
commit df77ccf7ac
12 changed files with 190 additions and 175 deletions

2
js/shims.d.ts vendored
View File

@@ -4,6 +4,7 @@ import Mithril from 'mithril';
// Other third-party libs
import * as _dayjs from 'dayjs';
import * as _$ from 'jquery';
import * as _ColorThief from 'color-thief-browser';
// Globals from flarum/core
import Application from './src/common/Application';
@@ -22,6 +23,7 @@ declare global {
const $: typeof _$;
const m: Mithril.Static;
const dayjs: typeof _dayjs;
const ColorThief: _ColorThief;
}
/**

View File

@@ -1,3 +1,17 @@
import Store from './Store';
import Mithril from 'mithril';
interface ModelData {
type?: string;
id?: string;
attributes?: any;
relationships?: any;
}
interface SaveOptions extends Mithril.RequestOptions<any> {
meta?: any;
}
/**
* The `Model` class represents a local data resource. It provides methods to
* persist changes via the API.
@@ -5,55 +19,58 @@
* @abstract
*/
export default class Model {
/**
* The resource object from the API.
*
* @type {Object}
* @public
*/
data: ModelData = {};
/**
* The time at which the model's data was last updated. Watching the value
* of this property is a fast way to retain/cache a subtree if data hasn't
* changed.
*
* @type {Date}
* @public
*/
freshness: Date = new Date();
/**
* Whether or not the resource exists on the server.
*
* @type {Boolean}
* @public
*/
exists: boolean = false;
/**
* The data store that this resource should be persisted to.
*
* @type {Store}
* @protected
*/
store?: Store = null;
/**
* @param {Object} data A resource object from the API.
* @param {Store} store The data store that this model should be persisted to.
* @public
*/
constructor(data = {}, store = null) {
/**
* The resource object from the API.
*
* @type {Object}
* @public
*/
constructor(data: ModelData = {}, store = null) {
this.data = data;
/**
* The time at which the model's data was last updated. Watching the value
* of this property is a fast way to retain/cache a subtree if data hasn't
* changed.
*
* @type {Date}
* @public
*/
this.freshness = new Date();
/**
* Whether or not the resource exists on the server.
*
* @type {Boolean}
* @public
*/
this.exists = false;
/**
* The data store that this resource should be persisted to.
*
* @type {Store}
* @protected
*/
this.store = store;
}
/**
* Get the model's ID.
*
* @return {Integer}
* @return {String}
* @public
* @final
*/
id() {
id(): string | undefined {
return this.data.id;
}
@@ -121,8 +138,8 @@ export default class Model {
* @return {Promise}
* @public
*/
save(attributes, options = {}) {
const data = {
save(attributes, options: SaveOptions = {}) {
const data: ModelData = {
type: this.data.type,
id: this.data.id,
attributes,
@@ -152,7 +169,7 @@ export default class Model {
this.pushData(data);
const request = { data };
const request: any = { data };
if (options.meta) request.meta = options.meta;
return app
@@ -220,11 +237,11 @@ export default class Model {
* @return {String}
* @protected
*/
apiEndpoint() {
apiEndpoint(): string {
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
}
copyData() {
copyData(): ModelData {
return JSON.parse(JSON.stringify(this.data));
}
@@ -236,8 +253,8 @@ export default class Model {
* @return {*}
* @public
*/
static attribute(name, transform) {
return function () {
static attribute<T>(name: string, transform?: Function) {
return function (this: Model): T | null | undefined {
const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value;
@@ -254,8 +271,8 @@ export default class Model {
* has not been loaded; or the model if it has been loaded.
* @public
*/
static hasOne(name) {
return function () {
static hasOne<T>(name: string) {
return function (this: Model): T | null | false {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -278,8 +295,8 @@ export default class Model {
* loaded, and undefined for those that have not.
* @public
*/
static hasMany(name) {
return function () {
static hasMany<T>(name: string) {
return function (this: Model): T[] | false {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -299,7 +316,7 @@ export default class Model {
* @return {Date|null}
* @public
*/
static transformDate(value) {
static transformDate(value: string): Date | null {
return value ? new Date(value) : null;
}
@@ -310,7 +327,7 @@ export default class Model {
* @return {Object}
* @protected
*/
static getIdentifier(model) {
static getIdentifier(model: Model) {
return {
type: model.data.type,
id: model.data.id,

View File

@@ -2,40 +2,40 @@ import Model from '../Model';
import computed from '../utils/computed';
import ItemList from '../utils/ItemList';
import Badge from '../components/Badge';
import User from './User';
import Post from './Post';
export default class Discussion extends Model {}
export default class Discussion extends Model {
title = Model.attribute<string>('title');
slug = Model.attribute<string>('slug');
Object.assign(Discussion.prototype, {
title: Model.attribute('title'),
slug: Model.attribute('slug'),
createdAt = Model.attribute<Date>('createdAt', Model.transformDate);
user = Model.hasOne<User>('user');
firstPost = Model.hasOne<Post>('firstPost');
createdAt: Model.attribute('createdAt', Model.transformDate),
user: Model.hasOne('user'),
firstPost: Model.hasOne('firstPost'),
lastPostedAt = Model.attribute<Date>('lastPostedAt', Model.transformDate);
lastPostedUser = Model.hasOne<User>('lastPostedUser');
lastPost = Model.hasOne<Post>('lastPost');
lastPostNumber = Model.attribute<number>('lastPostNumber');
lastPostedAt: Model.attribute('lastPostedAt', Model.transformDate),
lastPostedUser: Model.hasOne('lastPostedUser'),
lastPost: Model.hasOne('lastPost'),
lastPostNumber: Model.attribute('lastPostNumber'),
commentCount = Model.attribute<number>('commentCount');
replyCount = computed<number>('commentCount', (commentCount) => Math.max(0, commentCount - 1));
posts = Model.hasMany<Post>('posts');
mostRelevantPost = Model.hasOne<Post>('mostRelevantPost');
commentCount: Model.attribute('commentCount'),
replyCount: computed('commentCount', (commentCount) => Math.max(0, commentCount - 1)),
posts: Model.hasMany('posts'),
mostRelevantPost: Model.hasOne('mostRelevantPost'),
lastReadAt = Model.attribute<Date>('lastReadAt', Model.transformDate);
lastReadPostNumber = Model.attribute<number>('lastReadPostNumber');
isUnread = computed<boolean>('unreadCount', (unreadCount) => !!unreadCount);
isRead = computed<boolean>('unreadCount', (unreadCount) => app.session.user && !unreadCount);
lastReadAt: Model.attribute('lastReadAt', Model.transformDate),
lastReadPostNumber: Model.attribute('lastReadPostNumber'),
isUnread: computed('unreadCount', (unreadCount) => !!unreadCount),
isRead: computed('unreadCount', (unreadCount) => app.session.user && !unreadCount),
hiddenAt = Model.attribute<Date>('hiddenAt', Model.transformDate);
hiddenUser = Model.hasOne<User>('hiddenUser');
isHidden = computed<boolean>('hiddenAt', (hiddenAt) => !!hiddenAt);
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canReply: Model.attribute('canReply'),
canRename: Model.attribute('canRename'),
canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete'),
canReply = Model.attribute<boolean>('canReply');
canRename = Model.attribute<boolean>('canRename');
canHide = Model.attribute<boolean>('canHide');
canDelete = Model.attribute<boolean>('canDelete');
/**
* Remove a post from the discussion's posts relationship.
@@ -55,7 +55,7 @@ Object.assign(Discussion.prototype, {
}
});
}
},
}
/**
* Get the estimated number of unread posts in this discussion for the current
@@ -64,7 +64,7 @@ Object.assign(Discussion.prototype, {
* @return {Integer}
* @public
*/
unreadCount() {
unreadCount(): number {
const user = app.session.user;
if (user && user.markedAllAsReadAt() < this.lastPostedAt()) {
@@ -75,7 +75,7 @@ Object.assign(Discussion.prototype, {
}
return 0;
},
}
/**
* Get the Badge components that apply to this discussion.
@@ -83,7 +83,7 @@ Object.assign(Discussion.prototype, {
* @return {ItemList}
* @public
*/
badges() {
badges(): ItemList {
const items = new ItemList();
if (this.isHidden()) {
@@ -91,7 +91,7 @@ Object.assign(Discussion.prototype, {
}
return items;
},
}
/**
* Get a list of all of the post IDs in this discussion.
@@ -99,9 +99,9 @@ Object.assign(Discussion.prototype, {
* @return {Array}
* @public
*/
postIds() {
postIds(): string[] {
const posts = this.data.relationships.posts;
return posts ? posts.data.map((link) => link.id) : [];
},
});
}
}

View File

@@ -1,17 +0,0 @@
import Model from '../Model';
class Group extends Model {}
Object.assign(Group.prototype, {
nameSingular: Model.attribute('nameSingular'),
namePlural: Model.attribute('namePlural'),
color: Model.attribute('color'),
icon: Model.attribute('icon'),
isHidden: Model.attribute('isHidden'),
});
Group.ADMINISTRATOR_ID = '1';
Group.GUEST_ID = '2';
Group.MEMBER_ID = '3';
export default Group;

View File

@@ -0,0 +1,13 @@
import Model from '../Model';
export default class Group extends Model {
static ADMINISTRATOR_ID = '1';
static GUEST_ID = '2';
static MEMBER_ID = '3';
nameSingular = Model.attribute<string>('nameSingular');
namePlural = Model.attribute<string>('namePlural');
color = Model.attribute<string>('color');
icon = Model.attribute<string>('icon');
isHidden = Model.attribute<boolean>('isHidden');
}

View File

@@ -1,15 +0,0 @@
import Model from '../Model';
export default class Notification extends Model {}
Object.assign(Notification.prototype, {
contentType: Model.attribute('contentType'),
content: Model.attribute('content'),
createdAt: Model.attribute('createdAt', Model.transformDate),
isRead: Model.attribute('isRead'),
user: Model.hasOne('user'),
fromUser: Model.hasOne('fromUser'),
subject: Model.hasOne('subject'),
});

View File

@@ -0,0 +1,14 @@
import Model from '../Model';
import User from './User';
export default class Notification extends Model {
contentType = Model.attribute<string>('contentType');
content = Model.attribute<any>('content');
createdAt = Model.attribute<Date>('createdAt', Model.transformDate);
isRead = Model.attribute<boolean>('isRead');
user = Model.hasOne<User>('user');
fromUser = Model.hasOne<User>('fromUser');
subject = Model.hasOne<any>('subject');
}

View File

@@ -1,29 +0,0 @@
import Model from '../Model';
import computed from '../utils/computed';
import { getPlainContent } from '../utils/string';
export default class Post extends Model {}
Object.assign(Post.prototype, {
number: Model.attribute('number'),
discussion: Model.hasOne('discussion'),
createdAt: Model.attribute('createdAt', Model.transformDate),
user: Model.hasOne('user'),
contentType: Model.attribute('contentType'),
content: Model.attribute('content'),
contentHtml: Model.attribute('contentHtml'),
contentPlain: computed('contentHtml', getPlainContent),
editedAt: Model.attribute('editedAt', Model.transformDate),
editedUser: Model.hasOne('editedUser'),
isEdited: computed('editedAt', (editedAt) => !!editedAt),
hiddenAt: Model.attribute('hiddenAt', Model.transformDate),
hiddenUser: Model.hasOne('hiddenUser'),
isHidden: computed('hiddenAt', (hiddenAt) => !!hiddenAt),
canEdit: Model.attribute('canEdit'),
canHide: Model.attribute('canHide'),
canDelete: Model.attribute('canDelete'),
});

View File

@@ -0,0 +1,29 @@
import Model from '../Model';
import computed from '../utils/computed';
import { getPlainContent } from '../utils/string';
import Discussion from './Discussion';
import User from './User';
export default class Post extends Model {
number = Model.attribute<number>('number');
discussion = Model.hasOne<Discussion>('discussion');
createdAt = Model.attribute<Date>('createdAt', Model.transformDate);
user = Model.hasOne<User>('user');
contentType = Model.attribute<string>('contentType');
content = Model.attribute<string>('content');
contentHtml = Model.attribute<string>('contentHtml');
contentPlain = computed<string>('contentHtml', getPlainContent);
editedAt = Model.attribute<Date>('editedAt', Model.transformDate);
editedUser = Model.hasOne<User>('editedUser');
isEdited = computed<boolean>('editedAt', (editedAt) => !!editedAt);
hiddenAt = Model.attribute<Date>('hiddenAt', Model.transformDate);
hiddenUser = Model.hasOne<User>('hiddenUser');
isHidden = computed<boolean>('hiddenAt', (hiddenAt) => !!hiddenAt);
canEdit = Model.attribute<boolean>('canEdit');
canHide = Model.attribute<boolean>('canHide');
canDelete = Model.attribute<boolean>('canDelete');
}

View File

@@ -5,34 +5,33 @@ import stringToColor from '../utils/stringToColor';
import ItemList from '../utils/ItemList';
import computed from '../utils/computed';
import GroupBadge from '../components/GroupBadge';
import Group from './Group';
export default class User extends Model {}
export default class User extends Model {
username = Model.attribute<string>('username');
displayName = Model.attribute<string>('displayName');
email = Model.attribute<string>('email');
isEmailConfirmed = Model.attribute<boolean>('isEmailConfirmed');
password = Model.attribute<string>('password');
Object.assign(User.prototype, {
username: Model.attribute('username'),
displayName: Model.attribute('displayName'),
email: Model.attribute('email'),
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
password: Model.attribute('password'),
avatarUrl = Model.attribute<string>('avatarUrl');
preferences = Model.attribute<any>('preferences');
groups = Model.hasMany<Group>('groups');
avatarUrl: Model.attribute('avatarUrl'),
preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'),
joinTime = Model.attribute<Date>('joinTime', Model.transformDate);
lastSeenAt = Model.attribute<Date>('lastSeenAt', Model.transformDate);
markedAllAsReadAt = Model.attribute<Date>('markedAllAsReadAt', Model.transformDate);
unreadNotificationCount = Model.attribute<number>('unreadNotificationCount');
newNotificationCount = Model.attribute<number>('newNotificationCount');
joinTime: Model.attribute('joinTime', Model.transformDate),
lastSeenAt: Model.attribute('lastSeenAt', Model.transformDate),
markedAllAsReadAt: Model.attribute('markedAllAsReadAt', Model.transformDate),
unreadNotificationCount: Model.attribute('unreadNotificationCount'),
newNotificationCount: Model.attribute('newNotificationCount'),
discussionCount = Model.attribute<number>('discussionCount');
commentCount = Model.attribute<number>('commentCount');
discussionCount: Model.attribute('discussionCount'),
commentCount: Model.attribute('commentCount'),
canEdit = Model.attribute<boolean>('canEdit');
canDelete = Model.attribute<boolean>('canDelete');
canEdit: Model.attribute('canEdit'),
canDelete: Model.attribute('canDelete'),
avatarColor: null,
color: computed('username', 'avatarUrl', 'avatarColor', function (username, avatarUrl, avatarColor) {
avatarColor = null;
color = computed<string>('username', 'avatarUrl', 'avatarColor', (username, avatarUrl, avatarColor) => {
// If we've already calculated and cached the dominant color of the user's
// avatar, then we can return that in RGB format. If we haven't, we'll want
// to calculate it. Unless the user doesn't have an avatar, in which case
@@ -45,7 +44,7 @@ Object.assign(User.prototype, {
}
return '#' + stringToColor(username);
}),
});
/**
* Check whether or not the user has been seen in the last 5 minutes.
@@ -53,16 +52,16 @@ Object.assign(User.prototype, {
* @return {Boolean}
* @public
*/
isOnline() {
isOnline(): boolean {
return dayjs().subtract(5, 'minutes').isBefore(this.lastSeenAt());
},
}
/**
* Get the Badge components that apply to this user.
*
* @return {ItemList}
*/
badges() {
badges(): ItemList {
const items = new ItemList();
const groups = this.groups();
@@ -73,7 +72,7 @@ Object.assign(User.prototype, {
}
return items;
},
}
/**
* Calculate the dominant color of the user's avatar. The dominant color will
@@ -93,7 +92,7 @@ Object.assign(User.prototype, {
};
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl();
},
}
/**
* Update the user's preferences.
@@ -107,5 +106,5 @@ Object.assign(User.prototype, {
Object.assign(preferences, newPreferences);
return this.save({ preferences });
},
});
}
}

View File

@@ -1,3 +1,5 @@
import Model from '../Model';
/**
* The `computed` utility creates a function that will cache its output until
* any of the dependent values are dirty.
@@ -7,14 +9,14 @@
* dependent values.
* @return {Function}
*/
export default function computed(...dependentKeys) {
export default function computed<T, M = Model>(...dependentKeys: any[]) {
const keys = dependentKeys.slice(0, -1);
const compute = dependentKeys.slice(-1)[0];
const dependentValues = {};
let computedValue;
return function () {
return function (this: M): T {
let recompute = false;
// Read all of the dependent values. If any of them have changed since last