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

Compare commits

..

8 Commits

Author SHA1 Message Date
David Wheatley
61be08577d fix: .container set to width larger than viewport 2021-12-24 13:37:21 +01:00
David Wheatley
d1dd3d952d feat: use position: sticky on discussion sidenav 2021-12-24 13:33:42 +01:00
flarum-bot
dc661bf144 Bundled output for commit 7a27f494c6
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-12-22 18:58:42 +00:00
David Wheatley
7a27f494c6 fix: hide WelcomeHero when content is empty (#3219) 2021-12-22 13:54:21 -05:00
Sebastian Kessler
edde6be301 docs: fix broken contribution link in README; add screenshot (#3211) 2021-12-20 14:55:25 -05:00
flarum-bot
96fdaac3ef Bundled output for commit e57655553f
Includes transpiled JS/TS, and Typescript declaration files (typings).

[skip ci]
2021-12-20 16:32:17 +00:00
David Wheatley
e57655553f fix: breaking change in Search component - renaming of state property (#3212)
* fix: breaking change in search component's public api

* fix: add setter

* feat: add deprecation warning helper

This reduces bundle size as a result of deprecation warning in our JS, as well as maintaining a consistent format across warnings.

* feat: fire deprecation warning on usage of `Search.state`

* chore: use consistent deprecation warning across core

* fix: `/pull` not `/issue`

* chore: format
2021-12-20 16:28:28 +00:00
David Wheatley
a1cc456f3a fix(postmeta): use app baseUrl instead of location.origin (#3216) 2021-12-20 16:25:04 +00:00
19 changed files with 231 additions and 114 deletions

View File

@@ -9,7 +9,6 @@
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
</p>
## About Flarum
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
@@ -20,13 +19,15 @@
* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarums architecture is amazingly flexible, with a powerful Extension API.
![Screenshot of a Flarum instance, showing multiple discussions and tags.](https://flarum.org/assets/img/home-screenshot.png)
## Installation
This repository contains Flarum's core code. If you want to set up a forum, visit the [Flarum skeleton repository](https://github.com/flarum/flarum).
This repository contains Flarum's core code. If you want to set up a forum, visit the [Flarum skeleton repository](https://github.com/flarum/flarum). For support, refer to the [documentation](https://docs.flarum.org/), and ask questions on [Flarum Discuss](https://discuss.flarum.org/) (our community forum) or [Discord server](https://flarum.org/discord/).
## Contributing
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://docs.flarum.org/contributing)** to learn how you can help.
## Security Vulnerabilities

View File

@@ -10,3 +10,17 @@
* can fix.
*/
export default function fireDebugWarning(...args: Parameters<typeof console.warn>): void;
/**
* Fire a Flarum deprecation warning which is shown in the JS console.
*
* These warnings are only shown when the forum is in debug mode, and the function exists to
* reduce bundle size caused by multiple warnings across our JavaScript.
*
* @param message The message to display. (Short, but sweet, please!)
* @param githubId The PR or Issue ID with more info in relation to this change.
* @param [removedFrom] The version in which this feature will be completely removed. (default: 2.0)
* @param [repo] The repo which the issue or PR is located in. (default: flarum/core)
*
* @see {@link fireDebugWarning}
*/
export declare function fireDeprecationWarning(message: string, githubId: string, removedFrom?: string, repo?: string): void;

View File

@@ -42,12 +42,22 @@ export interface SearchAttrs extends ComponentAttrs {
*
* - state: SearchState instance.
*/
export default class Search<T extends SearchAttrs = SearchAttrs> extends Component<T> {
export default class Search<T extends SearchAttrs = SearchAttrs> extends Component<T, SearchState> {
/**
* The minimum query length before sources are searched.
*/
protected static MIN_SEARCH_LEN: number;
/**
* The instance of `SearchState` for this component.
*/
protected searchState: SearchState;
/**
* The instance of `SearchState` for this component.
*
* @deprecated Replace with`this.searchState` instead.
*/
protected get state(): SearchState;
protected set state(state: SearchState);
/**
* Whether or not the search input has focus.
*/

View File

@@ -1,13 +1,26 @@
import Component from '../../common/Component';
import type Mithril from 'mithril';
export interface IWelcomeHeroAttrs {
}
/**
* The `WelcomeHero` component displays a hero that welcomes the user to the
* forum.
*/
export default class WelcomeHero extends Component<import("../../common/Component").ComponentAttrs, undefined> {
constructor();
hidden: string | boolean | null | undefined;
export default class WelcomeHero extends Component<IWelcomeHeroAttrs> {
/**
* @deprecated Extend the `isHidden` method instead.
*/
hidden: boolean;
oninit(vnode: Mithril.Vnode<IWelcomeHeroAttrs, this>): void;
view(vnode: Mithril.Vnode<IWelcomeHeroAttrs, this>): JSX.Element | null;
/**
* Hide the welcome hero.
*/
hide(): void;
/**
* Determines whether the welcome hero should be hidden.
*
* @returns if the welcome hero is hidden.
*/
isHidden(): boolean;
}
import Component from "../../common/Component";

2
js/dist/admin.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/admin.js.map generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/forum.js generated vendored

File diff suppressed because one or more lines are too long

2
js/dist/forum.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
import app from '../common/app';
import { FlarumRequestOptions } from './Application';
import fireDebugWarning from './helpers/fireDebugWarning';
import { fireDeprecationWarning } from './helpers/fireDebugWarning';
import Model, { ModelData, SavedModelData } from './Model';
export interface MetaInformation {
@@ -123,11 +123,7 @@ export default class Store {
if (!this.models[data.type]) {
if (!allowUnregistered) {
setTimeout(() =>
fireDebugWarning(
`[Flarum 2.0 Deprecation] Cannot push object of type \`${data.type}\`, as that type has not yet been registered in the store. This will throw an error in Flarum 2.0 and later.
For more information, see https://github.com/flarum/core/pull/3206.`
)
fireDeprecationWarning(`Pushing object of type \`${data.type}\` not allowed, as type not yet registered in the store.`, '3206')
);
}

View File

@@ -16,3 +16,21 @@ export default function fireDebugWarning(...args: Parameters<typeof console.warn
console.warn(...args);
}
/**
* Fire a Flarum deprecation warning which is shown in the JS console.
*
* These warnings are only shown when the forum is in debug mode, and the function exists to
* reduce bundle size caused by multiple warnings across our JavaScript.
*
* @param message The message to display. (Short, but sweet, please!)
* @param githubId The PR or Issue ID with more info in relation to this change.
* @param [removedFrom] The version in which this feature will be completely removed. (default: 2.0)
* @param [repo] The repo which the issue or PR is located in. (default: flarum/core)
*
* @see {@link fireDebugWarning}
*/
export function fireDeprecationWarning(message: string, githubId: string, removedFrom: string = '2.0', repo: string = 'flarum/core'): void {
// GitHub auto-redirects between `/pull` and `/issues` for us, so using `/pull` saves 2 bytes!
fireDebugWarning(`[Flarum ${removedFrom} Deprecation] ${message}\n\nSee: https://github.com/${repo}/pull/${githubId}`);
}

View File

@@ -1,15 +1,14 @@
import { fireDeprecationWarning } from '../helpers/fireDebugWarning';
const deprecatedNotice = 'The `evented` util is deprecated and no longer supported.';
const deprecationIssueId = '2547';
/**
* The `evented` mixin provides methods allowing an object to trigger events,
* running externally registered event handlers.
*
* @deprecated v1.2, to be removed in v2.0
*/
import fireDebugWarning from '../helpers/fireDebugWarning';
const deprecatedNotice =
'The `evented` util is deprecated and will be removed in Flarum 2.0. For more info, please see https://github.com/flarum/core/issues/2547';
export default {
/**
* Arrays of registered event handlers, grouped by the event name.
@@ -27,7 +26,7 @@ export default {
* @protected
*/
getHandlers(event) {
fireDebugWarning(deprecatedNotice);
fireDeprecationWarning(deprecatedNotice, deprecationIssueId);
this.handlers = this.handlers || {};
@@ -44,7 +43,7 @@ export default {
* @public
*/
trigger(event, ...args) {
fireDebugWarning(deprecatedNotice);
fireDeprecationWarning(deprecatedNotice, deprecationIssueId);
this.getHandlers(event).forEach((handler) => handler.apply(this, args));
},
@@ -56,7 +55,7 @@ export default {
* @param {function} handler The function to handle the event.
*/
on(event, handler) {
fireDebugWarning(deprecatedNotice);
fireDeprecationWarning(deprecatedNotice, deprecationIssueId);
this.getHandlers(event).push(handler);
},
@@ -69,7 +68,7 @@ export default {
* @param {function} handler The function to handle the event.
*/
one(event, handler) {
fireDebugWarning(deprecatedNotice);
fireDeprecationWarning(deprecatedNotice, deprecationIssueId);
const wrapper = function () {
handler.apply(this, arguments);
@@ -87,7 +86,7 @@ export default {
* @param {function} handler The function that handles the event.
*/
off(event, handler) {
fireDebugWarning(deprecatedNotice);
fireDeprecationWarning(deprecatedNotice, deprecationIssueId);
const handlers = this.getHandlers(event);
const index = handlers.indexOf(handler);

View File

@@ -6,7 +6,6 @@ import PostControls from '../utils/PostControls';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import classList from '../../common/utils/classList';
/**
* The `Post` component displays a single post. The basic post template just
@@ -125,16 +124,24 @@ export default class Post extends Component {
* @returns {string[]}
*/
classes(existing) {
let classes = (existing || '').split(' ').concat(['Post']);
const user = this.attrs.post.user();
const discussion = this.attrs.post.discussion();
return classList(existing, 'Post', {
'Post--loading': this.loading,
'Post--by-actor': user === app.session.user,
'Post--by-start-user': user?.id() === discussion.attribute('startUserId'),
})
.split(' ')
.filter((x) => !!x);
if (this.loading) {
classes.push('Post--loading');
}
if (user && user === app.session.user) {
classes.push('Post--by-actor');
}
if (user && user.id() === discussion.attribute('startUserId')) {
classes.push('Post--by-start-user');
}
return classes;
}
/**

View File

@@ -55,6 +55,6 @@ export default class PostMeta extends Component {
* @returns {String}
*/
getPermalink(post) {
return window.location.origin + app.route.post(post);
return app.forum.attribute('baseUrl') + app.route.post(post);
}
}

View File

@@ -9,8 +9,8 @@ import icon from '../../common/helpers/icon';
import SearchState from '../states/SearchState';
import DiscussionsSearchSource from './DiscussionsSearchSource';
import UsersSearchSource from './UsersSearchSource';
import { fireDeprecationWarning } from '../../common/helpers/fireDebugWarning';
import type Mithril from 'mithril';
import Model from '../../common/Model';
/**
* The `SearchSource` interface defines a section of search results in the
@@ -53,14 +53,33 @@ export interface SearchAttrs extends ComponentAttrs {
*
* - state: SearchState instance.
*/
export default class Search<T extends SearchAttrs = SearchAttrs> extends Component<T> {
export default class Search<T extends SearchAttrs = SearchAttrs> extends Component<T, SearchState> {
/**
* The minimum query length before sources are searched.
*/
protected static MIN_SEARCH_LEN = 3;
/**
* The instance of `SearchState` for this component.
*/
protected searchState!: SearchState;
/**
* The instance of `SearchState` for this component.
*
* @deprecated Replace with`this.searchState` instead.
*/
// TODO: [Flarum 2.0] Remove this.
// @ts-expect-error This is a get accessor, while superclass defines this as a property. This is needed to prevent breaking changes, however.
protected get state() {
fireDeprecationWarning('`state` property of the Search component is deprecated', '3212');
return this.searchState;
}
protected set state(state: SearchState) {
fireDeprecationWarning('`state` property of the Search component is deprecated', '3212');
this.searchState = state;
}
/**
* Whether or not the search input has focus.
*/

View File

@@ -1,50 +0,0 @@
import app from '../../forum/app';
import Component from '../../common/Component';
import Button from '../../common/components/Button';
/**
* The `WelcomeHero` component displays a hero that welcomes the user to the
* forum.
*/
export default class WelcomeHero extends Component {
oninit(vnode) {
super.oninit(vnode);
this.hidden = localStorage.getItem('welcomeHidden');
}
view() {
if (this.hidden) return <div />;
const slideUp = () => {
this.$().slideUp(this.hide.bind(this));
};
return (
<header className="Hero WelcomeHero">
<div class="container">
{Button.component({
icon: 'fas fa-times',
onclick: slideUp,
className: 'Hero-close Button Button--icon Button--link',
'aria-label': app.translator.trans('core.forum.welcome_hero.hide'),
})}
<div className="containerNarrow">
<h2 className="Hero-title">{app.forum.attribute('welcomeTitle')}</h2>
<div className="Hero-subtitle">{m.trust(app.forum.attribute('welcomeMessage'))}</div>
</div>
</div>
</header>
);
}
/**
* Hide the welcome hero.
*/
hide() {
localStorage.setItem('welcomeHidden', 'true');
this.hidden = true;
}
}

View File

@@ -0,0 +1,69 @@
import app from '../app';
import Component from '../../common/Component';
import Button from '../../common/components/Button';
import type Mithril from 'mithril';
export interface IWelcomeHeroAttrs {}
const LOCAL_STORAGE_KEY = 'welcomeHidden';
/**
* The `WelcomeHero` component displays a hero that welcomes the user to the
* forum.
*/
export default class WelcomeHero extends Component<IWelcomeHeroAttrs> {
/**
* @deprecated Extend the `isHidden` method instead.
*/
hidden: boolean = false;
oninit(vnode: Mithril.Vnode<IWelcomeHeroAttrs, this>) {
super.oninit(vnode);
}
view(vnode: Mithril.Vnode<IWelcomeHeroAttrs, this>) {
if (this.isHidden()) return null;
const slideUp = () => {
this.$().slideUp(this.hide.bind(this));
};
return (
<header class="Hero WelcomeHero">
<div class="container">
<Button
icon="fas fa-times"
onclick={slideUp}
className="Hero-close Button Button--icon Button--link"
aria-label={app.translator.trans('core.forum.welcome_hero.hide')}
/>
<div class="containerNarrow">
<h2 class="Hero-title">{app.forum.attribute('welcomeTitle')}</h2>
<div class="Hero-subtitle">{m.trust(app.forum.attribute('welcomeMessage'))}</div>
</div>
</div>
</header>
);
}
/**
* Hide the welcome hero.
*/
hide() {
localStorage.setItem(LOCAL_STORAGE_KEY, 'true');
}
/**
* Determines whether the welcome hero should be hidden.
*
* @returns if the welcome hero is hidden.
*/
isHidden(): boolean {
if (!app.forum.attribute<string>('welcomeTitle')?.trim()) return true;
if (localStorage.getItem(LOCAL_STORAGE_KEY)) return true;
if (this.hidden) return true;
return false;
}
}

View File

@@ -2,8 +2,8 @@
position: relative !important;
padding-top: var(--header-height);
padding-bottom: 50px;
overflow-x: hidden;
min-height: 100vh;
width: 100%;
@media @phone {
padding-top: var(--header-height-phone);
@@ -27,13 +27,16 @@
box-shadow: 0 2px 6px var(--shadow-color);
}
}
.App-primaryControl, .App-titleControl, .App-backControl {
.App-primaryControl,
.App-titleControl,
.App-backControl {
position: absolute !important;
z-index: calc(~"var(--zindex-header) + 1");
top: 0 !important;
margin: 0;
.App.affix &, .Composer & {
.App.affix &,
.Composer & {
position: fixed !important;
}
@@ -59,15 +62,18 @@
right: 0;
&.Dropdown {
.Button, .Button-caret {
.Button,
.Button-caret {
display: none !important;
}
.Dropdown-toggle, .Button-icon {
.Dropdown-toggle,
.Button-icon {
display: block !important;
}
}
}
.App-primaryControl, .App-backControl {
.App-primaryControl,
.App-backControl {
margin: 0 !important;
> .Button {
@@ -90,7 +96,8 @@
text-align: center;
color: var(--header-color) !important;
&, > .Button {
&,
> .Button {
font-size: 16px;
}
> .Button {
@@ -143,7 +150,7 @@
.drawerOpen & {
-webkit-transform: none !important;
transform: none !important;
transform: none !important;
}
}
.drawer-backdrop {
@@ -200,12 +207,15 @@
> li {
padding: 0 10px 0;
}
.FormControl, .ButtonGroup, .Button {
.FormControl,
.ButtonGroup,
.Button {
width: 100%;
text-align: left;
}
.Dropdown-menu {
.ButtonGroup, .Button {
.ButtonGroup,
.Button {
width: auto;
}
}
@@ -221,7 +231,7 @@
@media @phone {
.App-drawer {
& when (@config-colored-header = true) {
.light-contents(@name: 'header-colored');
.light-contents(@name: "header-colored");
}
}
}
@@ -245,7 +255,7 @@
}
& when (@config-colored-header = true) {
.light-contents(@name: 'header-colored');
.light-contents(@name: "header-colored");
}
}
@@ -254,7 +264,8 @@
margin-right: 25px;
}
.Header-controls {
&, > li {
&,
> li {
display: inline-block;
vertical-align: middle;
}

View File

@@ -13,6 +13,7 @@ body {
font-size: 13px;
line-height: 1.5;
overflow-y: scroll;
width: 100%;
}
h1, h2, h3, h4, h5, h6 {
@@ -65,6 +66,8 @@ p {
@media @desktop-hd {
width: @screen-desktop-hd;
}
max-width: 100%;
}
.containerNarrow {

View File

@@ -15,7 +15,8 @@
margin: 15px;
display: inline-block;
&.item-controls, &.item-scrubber {
&.item-controls,
&.item-scrubber {
margin: 0;
display: block;
}
@@ -23,22 +24,27 @@
}
}
@media @tablet-up {
.DiscussionPage-nav {
float: right;
.DiscussionPage-discussion > .container {
display: flex;
flex-direction: row-reverse;
}
.DiscussionPage-nav {
min-width: 150px;
&, > ul {
width: 150px;
}
> ul {
position: fixed;
margin-top: 30px;
margin-top: 24px;
position: sticky;
top: ~"calc(24px + var(--header-height))";
z-index: 1;
width: 100%;
> li {
margin-bottom: 10px;
}
}
.ButtonGroup, .Button {
.ButtonGroup,
.Button {
width: 100%;
}
.ButtonGroup:not(.itemCount1) {
@@ -54,7 +60,7 @@
@media @tablet-up {
.DiscussionPage-stream {
margin-right: 225px;
margin-right: 75px;
}
}
@@ -132,7 +138,8 @@
}
}
// When the pane is pinned, move the other page content inwards
.App-content, .App-footer {
.App-content,
.App-footer {
.hasPane.panePinned & {
margin-left: var(--pane-width);