1
0
mirror of https://github.com/flarum/core.git synced 2025-08-24 09:03:05 +02:00

Compare commits

..

10 Commits

Author SHA1 Message Date
Daniël Klabbers
967cd0e3ca update version constant for beta 14.1 2020-11-02 13:53:20 +01:00
Daniël Klabbers
b79152b977 bundled output for js changes beta 14.1 2020-11-02 11:53:27 +01:00
Daniël Klabbers
ace624db66 changelog for v0.1.0-beta.14.1 2020-11-02 11:51:24 +01:00
Alexander Skvortsov
9b9f2c4bb7 Fix exiting composer while in fullscreen mode. 2020-10-30 20:44:52 -04:00
Alexander Skvortsov
8b1de457bf Fix broken page title logic on subpath installs
The base path needs to be accounted for when calculating whether we're on the default route.
2020-10-30 14:18:09 -04:00
flarum-bot
12c03dc4e1 Bundled output for commit d2927cfdb9 [skip ci] 2020-10-29 16:54:36 +00:00
Alexander Skvortsov
d2927cfdb9 Ensure scripts provided by textformatter are run (#2415) 2020-10-29 12:53:23 -04:00
Daniël Klabbers
24b7a21507 Update Symfony components to v4 (#2407)
This matches the Symfony dependencies of our laravel dependencies.
2020-10-27 17:12:36 -04:00
flarum-bot
c9a04fe009 Bundled output for commit bd7fa11b5a [skip ci] 2020-10-25 17:36:51 +00:00
Alexander Skvortsov
bd7fa11b5a Export SuperTextarea util in compat 2020-10-25 13:35:15 -04:00
23 changed files with 219 additions and 209 deletions

View File

@@ -1,5 +1,15 @@
# Changelog
## [0.1.0-beta.14.1](https://github.com/flarum/core/compare/v0.1.0-beta.14...v0.1.0-beta.14.1)
### Fixed
- SuperTextarea component is not exported.
- Symfony dependencies do not match those depended on by Laravel (#2407)
- Scripts from textformatter aren't executed (#2415)
- Sub path installations have no page title.
- Losing focus of Composer area when coming from fullscreen.
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
### Added

View File

@@ -76,11 +76,11 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"symfony/config": "^4.3.4",
"symfony/console": "^4.3.4",
"symfony/event-dispatcher": "^4.3.4",
"symfony/translation": "^4.3.4",
"symfony/yaml": "^4.3.4",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
},

4
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

6
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
js/shims.d.ts vendored
View File

@@ -4,7 +4,6 @@ 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';
@@ -23,7 +22,6 @@ declare global {
const $: typeof _$;
const m: Mithril.Static;
const dayjs: typeof _dayjs;
const ColorThief: _ColorThief;
}
/**

View File

@@ -270,7 +270,7 @@ export default class Application {
updateTitle() {
const count = this.titleCount ? `(${this.titleCount}) ` : '';
const pageTitleWithSeparator = this.title && m.route.get() !== '/' ? this.title + ' - ' : '';
const pageTitleWithSeparator = this.title && m.route.get() !== this.forum.attribute('basePath') + '/' ? this.title + ' - ' : '';
const title = this.forum.attribute('title');
document.title = count + pageTitleWithSeparator + title;
}

View File

@@ -1,17 +1,3 @@
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.
@@ -19,58 +5,55 @@ interface SaveOptions extends Mithril.RequestOptions<any> {
* @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: ModelData = {}, store = null) {
constructor(data = {}, store = null) {
/**
* The resource object from the API.
*
* @type {Object}
* @public
*/
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 {String}
* @return {Integer}
* @public
* @final
*/
id(): string | undefined {
id() {
return this.data.id;
}
@@ -138,8 +121,8 @@ export default class Model {
* @return {Promise}
* @public
*/
save(attributes, options: SaveOptions = {}) {
const data: ModelData = {
save(attributes, options = {}) {
const data = {
type: this.data.type,
id: this.data.id,
attributes,
@@ -169,7 +152,7 @@ export default class Model {
this.pushData(data);
const request: any = { data };
const request = { data };
if (options.meta) request.meta = options.meta;
return app
@@ -237,11 +220,11 @@ export default class Model {
* @return {String}
* @protected
*/
apiEndpoint(): string {
apiEndpoint() {
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
}
copyData(): ModelData {
copyData() {
return JSON.parse(JSON.stringify(this.data));
}
@@ -253,8 +236,8 @@ export default class Model {
* @return {*}
* @public
*/
static attribute<T>(name: string, transform?: Function) {
return function (this: Model): T | null | undefined {
static attribute(name, transform) {
return function () {
const value = this.data.attributes && this.data.attributes[name];
return transform ? transform(value) : value;
@@ -271,8 +254,8 @@ export default class Model {
* has not been loaded; or the model if it has been loaded.
* @public
*/
static hasOne<T>(name: string) {
return function (this: Model): T | null | false {
static hasOne(name) {
return function () {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -295,8 +278,8 @@ export default class Model {
* loaded, and undefined for those that have not.
* @public
*/
static hasMany<T>(name: string) {
return function (this: Model): T[] | false {
static hasMany(name) {
return function () {
if (this.data.relationships) {
const relationship = this.data.relationships[name];
@@ -316,7 +299,7 @@ export default class Model {
* @return {Date|null}
* @public
*/
static transformDate(value: string): Date | null {
static transformDate(value) {
return value ? new Date(value) : null;
}
@@ -327,7 +310,7 @@ export default class Model {
* @return {Object}
* @protected
*/
static getIdentifier(model: Model) {
static getIdentifier(model) {
return {
type: model.data.type,
id: model.data.id,

View File

@@ -19,6 +19,7 @@ import extract from './utils/extract';
import ScrollListener from './utils/ScrollListener';
import stringToColor from './utils/stringToColor';
import subclassOf from './utils/subclassOf';
import SuperTextarea from './utils/SuperTextarea';
import patchMithril from './utils/patchMithril';
import classList from './utils/classList';
import extractText from './utils/extractText';
@@ -90,6 +91,7 @@ export default {
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/SuperTextarea': SuperTextarea,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/classList': classList,

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

View File

@@ -0,0 +1,17 @@
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

@@ -1,13 +0,0 @@
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

@@ -0,0 +1,15 @@
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

@@ -1,14 +0,0 @@
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

@@ -0,0 +1,29 @@
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

@@ -1,29 +0,0 @@
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,33 +5,34 @@ 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 {
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');
export default class User extends Model {}
avatarUrl = Model.attribute<string>('avatarUrl');
preferences = Model.attribute<any>('preferences');
groups = Model.hasMany<Group>('groups');
Object.assign(User.prototype, {
username: Model.attribute('username'),
displayName: Model.attribute('displayName'),
email: Model.attribute('email'),
isEmailConfirmed: Model.attribute('isEmailConfirmed'),
password: Model.attribute('password'),
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');
avatarUrl: Model.attribute('avatarUrl'),
preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'),
discussionCount = Model.attribute<number>('discussionCount');
commentCount = Model.attribute<number>('commentCount');
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'),
canEdit = Model.attribute<boolean>('canEdit');
canDelete = Model.attribute<boolean>('canDelete');
discussionCount: Model.attribute('discussionCount'),
commentCount: Model.attribute('commentCount'),
avatarColor = null;
color = computed<string>('username', 'avatarUrl', 'avatarColor', (username, avatarUrl, avatarColor) => {
canEdit: Model.attribute('canEdit'),
canDelete: Model.attribute('canDelete'),
avatarColor: null,
color: computed('username', 'avatarUrl', 'avatarColor', function (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
@@ -44,7 +45,7 @@ export default class User extends Model {
}
return '#' + stringToColor(username);
});
}),
/**
* Check whether or not the user has been seen in the last 5 minutes.
@@ -52,16 +53,16 @@ export default class User extends Model {
* @return {Boolean}
* @public
*/
isOnline(): boolean {
isOnline() {
return dayjs().subtract(5, 'minutes').isBefore(this.lastSeenAt());
}
},
/**
* Get the Badge components that apply to this user.
*
* @return {ItemList}
*/
badges(): ItemList {
badges() {
const items = new ItemList();
const groups = this.groups();
@@ -72,7 +73,7 @@ export default class User extends Model {
}
return items;
}
},
/**
* Calculate the dominant color of the user's avatar. The dominant color will
@@ -92,7 +93,7 @@ export default class User extends Model {
};
image.crossOrigin = 'anonymous';
image.src = this.avatarUrl();
}
},
/**
* Update the user's preferences.
@@ -106,5 +107,5 @@ export default class User extends Model {
Object.assign(preferences, newPreferences);
return this.save({ preferences });
}
}
},
});

View File

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

View File

@@ -56,9 +56,7 @@ export default class CommentPost extends Post {
]);
}
onupdate(vnode) {
super.onupdate();
refreshContent() {
const contentHtml = this.isEditing() ? '' : this.attrs.post.contentHtml();
// If the post content has changed since the last render, we'll run through
@@ -66,13 +64,28 @@ export default class CommentPost extends Post {
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
if (this.contentHtml !== contentHtml) {
this.$('.Post-body script').each(function () {
eval.call(window, $(this).text());
const script = document.createElement('script');
script.textContent = this.textContent;
Array.from(this.attributes).forEach((attr) => script.setAttribute(attr.name, attr.value));
this.parentNode.replaceChild(script, this);
});
}
this.contentHtml = contentHtml;
}
oncreate(vnode) {
super.oncreate(vnode);
this.refreshContent();
}
onupdate(vnode) {
super.onupdate(vnode);
this.refreshContent();
}
isEditing() {
return app.composer.bodyMatches(EditPostComposer, { post: this.attrs.post });
}

View File

@@ -199,7 +199,7 @@ export default class Composer extends Component {
*/
animatePositionChange() {
// When exiting full-screen mode: focus content
if (this.prevPosition === ComposerState.Position.FULLSCREEN) {
if (this.prevPosition === ComposerState.Position.FULLSCREEN && this.state.position === ComposerState.Position.NORMAL) {
this.focus();
return;
}

View File

@@ -21,7 +21,7 @@ class Application
*
* @var string
*/
const VERSION = '0.1.0-beta.14';
const VERSION = '0.1.0-beta.14.1';
/**
* The IoC container for the Flarum application.