- {!!extension.icon && icon(extension.icon.name)}
+ {!!extension.icon && }
{extension.extra['flarum-extension'].title}
@@ -199,17 +199,17 @@ export default class QueueSection extends Component<{}> {
}
operationIcon(operation: TaskOperations): Mithril.Children {
- return icon(
- {
- update_check: 'fas fa-sync-alt',
- update_major: 'fas fa-play',
- update_minor: 'fas fa-play',
- update_global: 'fas fa-play',
- extension_install: 'fas fa-download',
- extension_remove: 'fas fa-times',
- extension_update: 'fas fa-arrow-alt-circle-up',
- why_not: 'fas fa-exclamation-circle',
- }[operation]
- );
+ const iconName = {
+ update_check: 'fas fa-sync-alt',
+ update_major: 'fas fa-play',
+ update_minor: 'fas fa-play',
+ update_global: 'fas fa-play',
+ extension_install: 'fas fa-download',
+ extension_remove: 'fas fa-times',
+ extension_update: 'fas fa-arrow-alt-circle-up',
+ why_not: 'fas fa-exclamation-circle',
+ }[operation];
+
+ return
;
}
}
diff --git a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx
index 49b82e53ba..48c7584963 100644
--- a/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx
+++ b/extensions/statistics/js/src/admin/components/StatisticsWidget.tsx
@@ -6,7 +6,7 @@ import abbreviateNumber from 'flarum/common/utils/abbreviateNumber';
import extractText from 'flarum/common/utils/extractText';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import Placeholder from 'flarum/common/components/Placeholder';
-import icon from 'flarum/common/helpers/icon';
+import Icon from 'flarum/common/components/Icon';
import classList from 'flarum/common/utils/classList';
import DashboardWidget, { IDashboardWidgetAttrs } from 'flarum/admin/components/DashboardWidget';
@@ -291,7 +291,7 @@ export default class StatisticsWidget extends DashboardWidget {
<>
{' '}
0 ? 'up' : 'down')}>
- {icon('fas fa-arrow-' + (periodChange > 0 ? 'up' : 'down'))}
+ 0 ? 'up' : 'down')} />
{Math.abs(periodChange).toFixed(1)}%
>
diff --git a/extensions/statistics/less/admin.less b/extensions/statistics/less/admin.less
index 8d9797896d..d7e1f0dbd7 100644
--- a/extensions/statistics/less/admin.less
+++ b/extensions/statistics/less/admin.less
@@ -11,7 +11,7 @@
&-title {
margin: 0 20px;
- color: @muted-color;
+ color: var(--muted-color);
}
&-entities {
@@ -29,7 +29,7 @@
min-width: 130px;
font-size: 12px;
font-weight: bold;
- color: @muted-color;
+ color: var(--muted-color);
}
&-label {
@@ -39,7 +39,7 @@
&-entity {
min-width: 130px;
padding: 15px 20px;
- color: @text-color;
+ color: var(--text-color);
font-size: 20px;
.StatisticsWidget:not(.StatisticsWidget--mini) & {
@@ -47,12 +47,12 @@
&:hover,
&:focus-visible {
- background: mix(@control-bg, @body-bg, 50%);
+ background: var(--control-body-bg-mix);
text-decoration: none;
}
&.active {
- border-top: 4px solid @primary-color;
+ border-top: 4px solid var(--primary-color);
padding-top: 11px;
}
}
@@ -75,10 +75,10 @@
font-weight: bold;
text-transform: uppercase;
font-size: 12px;
- color: @muted-color;
+ color: var(--muted-color);
.active & {
- color: @primary-color;
+ color: var(--primary-color);
}
}
diff --git a/extensions/subscriptions/js/src/@types/shims.d.ts b/extensions/subscriptions/js/src/@types/shims.d.ts
new file mode 100644
index 0000000000..057f872464
--- /dev/null
+++ b/extensions/subscriptions/js/src/@types/shims.d.ts
@@ -0,0 +1,7 @@
+import 'flarum/common/models/Discussion';
+
+declare module 'flarum/common/models/Discussion' {
+ export default interface Discussion {
+ subscription(): string;
+ }
+}
diff --git a/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js b/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js
index ed950fff1e..bd886731f7 100644
--- a/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js
+++ b/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js
@@ -2,11 +2,12 @@ import app from 'flarum/forum/app';
import { extend } from 'flarum/common/extend';
import LinkButton from 'flarum/common/components/LinkButton';
import IndexPage from 'flarum/forum/components/IndexPage';
+import IndexSidebar from 'flarum/forum/components/IndexSidebar';
import DiscussionListState from 'flarum/forum/states/DiscussionListState';
import GlobalSearchState from 'flarum/forum/states/GlobalSearchState';
export default function addSubscriptionFilter() {
- extend(IndexPage.prototype, 'navItems', function (items) {
+ extend(IndexSidebar.prototype, 'navItems', function (items) {
if (app.session.user) {
const params = app.search.stickyParams();
diff --git a/extensions/subscriptions/js/src/forum/components/NewPostNotification.js b/extensions/subscriptions/js/src/forum/components/NewPostNotification.js
index 785a90141c..93c5ca0946 100644
--- a/extensions/subscriptions/js/src/forum/components/NewPostNotification.js
+++ b/extensions/subscriptions/js/src/forum/components/NewPostNotification.js
@@ -17,4 +17,8 @@ export default class NewPostNotification extends Notification {
content() {
return app.translator.trans('flarum-subscriptions.forum.notifications.new_post_text', { user: this.attrs.notification.fromUser() });
}
+
+ excerpt() {
+ return null;
+ }
}
diff --git a/extensions/subscriptions/js/src/forum/components/SubscriptionMenu.js b/extensions/subscriptions/js/src/forum/components/SubscriptionMenu.js
deleted file mode 100644
index c0ed97875f..0000000000
--- a/extensions/subscriptions/js/src/forum/components/SubscriptionMenu.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import app from 'flarum/forum/app';
-import Dropdown from 'flarum/common/components/Dropdown';
-import Button from 'flarum/common/components/Button';
-import Tooltip from 'flarum/common/components/Tooltip';
-import icon from 'flarum/common/helpers/icon';
-import extractText from 'flarum/common/utils/extractText';
-import classList from 'flarum/common/utils/classList';
-
-import SubscriptionMenuItem from './SubscriptionMenuItem';
-
-export default class SubscriptionMenu extends Dropdown {
- oninit(vnode) {
- super.oninit(vnode);
-
- this.options = [
- {
- subscription: null,
- icon: 'far fa-star',
- label: app.translator.trans('flarum-subscriptions.forum.sub_controls.not_following_button'),
- description: app.translator.trans('flarum-subscriptions.forum.sub_controls.not_following_text'),
- },
- {
- subscription: 'follow',
- icon: 'fas fa-star',
- label: app.translator.trans('flarum-subscriptions.forum.sub_controls.following_button'),
- description: app.translator.trans('flarum-subscriptions.forum.sub_controls.following_text'),
- },
- {
- subscription: 'ignore',
- icon: 'far fa-eye-slash',
- label: app.translator.trans('flarum-subscriptions.forum.sub_controls.ignoring_button'),
- description: app.translator.trans('flarum-subscriptions.forum.sub_controls.ignoring_text'),
- },
- ];
- }
-
- view() {
- const discussion = this.attrs.discussion;
- const subscription = discussion.subscription();
-
- let buttonLabel = app.translator.trans('flarum-subscriptions.forum.sub_controls.follow_button');
- let buttonIcon = 'far fa-star';
- const buttonClass = 'SubscriptionMenu-button--' + subscription;
-
- switch (subscription) {
- case 'follow':
- buttonLabel = app.translator.trans('flarum-subscriptions.forum.sub_controls.following_button');
- buttonIcon = 'fas fa-star';
- break;
-
- case 'ignore':
- buttonLabel = app.translator.trans('flarum-subscriptions.forum.sub_controls.ignoring_button');
- buttonIcon = 'far fa-eye-slash';
- break;
-
- default:
- // no default
- }
-
- const preferences = app.session.user.preferences();
- const notifyEmail = preferences['notify_newPost_email'];
- const notifyAlert = preferences['notify_newPost_alert'];
- const tooltipText = extractText(
- app.translator.trans(
- notifyEmail ? 'flarum-subscriptions.forum.sub_controls.notify_email_tooltip' : 'flarum-subscriptions.forum.sub_controls.notify_alert_tooltip'
- )
- );
-
- const shouldShowTooltip = (notifyEmail || notifyAlert) && subscription === null;
-
- const button = (
-
- {buttonLabel}
-
- );
-
- return (
-
- {shouldShowTooltip ? (
-
- {button}
-
- ) : (
- button
- )}
-
-
- {icon('fas fa-caret-down', { className: 'Button-icon' })}
-
-
-
- {this.options.map((attrs) => (
-
-
-
- ))}
-
-
- );
- }
-
- saveSubscription(discussion, subscription) {
- discussion.save({ subscription });
-
- this.$('.SubscriptionMenu-button').tooltip('hide');
- }
-}
diff --git a/extensions/subscriptions/js/src/forum/components/SubscriptionMenu.tsx b/extensions/subscriptions/js/src/forum/components/SubscriptionMenu.tsx
new file mode 100644
index 0000000000..ba6014e066
--- /dev/null
+++ b/extensions/subscriptions/js/src/forum/components/SubscriptionMenu.tsx
@@ -0,0 +1,99 @@
+import app from 'flarum/forum/app';
+import Dropdown, { IDropdownAttrs } from 'flarum/common/components/Dropdown';
+import Button from 'flarum/common/components/Button';
+import extractText from 'flarum/common/utils/extractText';
+import DetailedDropdownItem from 'flarum/common/components/DetailedDropdownItem';
+import SplitDropdown from 'flarum/common/components/SplitDropdown';
+import type Discussion from 'flarum/common/models/Discussion';
+
+export interface ISubscriptionMenuAttrs extends IDropdownAttrs {
+ discussion: Discussion;
+}
+
+export default class SubscriptionMenu
extends Dropdown {
+ private options: any[] = [
+ {
+ subscription: null,
+ icon: 'far fa-star',
+ label: app.translator.trans('flarum-subscriptions.forum.sub_controls.not_following_button'),
+ description: app.translator.trans('flarum-subscriptions.forum.sub_controls.not_following_text'),
+ },
+ {
+ subscription: 'follow',
+ icon: 'fas fa-star',
+ label: app.translator.trans('flarum-subscriptions.forum.sub_controls.following_button'),
+ description: app.translator.trans('flarum-subscriptions.forum.sub_controls.following_text'),
+ },
+ {
+ subscription: 'ignore',
+ icon: 'far fa-eye-slash',
+ label: app.translator.trans('flarum-subscriptions.forum.sub_controls.ignoring_button'),
+ description: app.translator.trans('flarum-subscriptions.forum.sub_controls.ignoring_text'),
+ },
+ ];
+
+ private possibleButtonAttrs: any = {
+ null: {
+ icon: 'far fa-star',
+ label: app.translator.trans('flarum-subscriptions.forum.sub_controls.follow_button'),
+ },
+ follow: {
+ icon: 'fas fa-star',
+ label: app.translator.trans('flarum-subscriptions.forum.sub_controls.following_button'),
+ },
+ ignore: {
+ icon: 'far fa-eye-slash',
+ label: app.translator.trans('flarum-subscriptions.forum.sub_controls.ignoring_button'),
+ },
+ };
+
+ view() {
+ const discussion = this.attrs.discussion;
+ const subscription = discussion.subscription();
+
+ const buttonAttrs = this.possibleButtonAttrs[subscription];
+
+ const preferences = app.session.user!.preferences()!;
+ const notifyEmail = preferences['notify_newPost_email'];
+ const notifyAlert = preferences['notify_newPost_alert'];
+ const tooltipText = extractText(
+ app.translator.trans(
+ notifyEmail ? 'flarum-subscriptions.forum.sub_controls.notify_email_tooltip' : 'flarum-subscriptions.forum.sub_controls.notify_alert_tooltip'
+ )
+ );
+
+ const shouldShowTooltip = (notifyEmail || notifyAlert) && subscription === null;
+
+ return (
+
+ {buttonAttrs.label}
+
+ }
+ >
+ {this.options.map((attrs) => (
+
+ ))}
+
+ );
+ }
+
+ saveSubscription(discussion: Discussion, subscription: string | null): void {
+ discussion.save({ subscription });
+
+ // @ts-ignore
+ this.$('.SubscriptionMenu-button').tooltip('hide');
+ }
+}
diff --git a/extensions/subscriptions/js/src/forum/components/SubscriptionMenuItem.js b/extensions/subscriptions/js/src/forum/components/SubscriptionMenuItem.js
deleted file mode 100644
index f9ff0ec277..0000000000
--- a/extensions/subscriptions/js/src/forum/components/SubscriptionMenuItem.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Component from 'flarum/common/Component';
-import icon from 'flarum/common/helpers/icon';
-
-export default class SubscriptionMenuItem extends Component {
- view() {
- return (
-
- {this.attrs.active && icon('fas fa-check', { className: 'Button-icon' })}
-
- {icon(this.attrs.icon, { className: 'Button-icon' })}
- {this.attrs.label}
- {this.attrs.description}
-
-
- );
- }
-}
diff --git a/extensions/subscriptions/less/forum.less b/extensions/subscriptions/less/forum.less
index 7194a53e61..c35f76e0a8 100644
--- a/extensions/subscriptions/less/forum.less
+++ b/extensions/subscriptions/less/forum.less
@@ -22,18 +22,3 @@
.SubscriptionMenu .Dropdown-menu {
min-width: 260px;
}
-.SubscriptionMenuItem-label {
- padding-left: 25px;
- display: block;
- white-space: normal;
-
- & strong {
- display: block;
- }
-}
-.SubscriptionMenuItem-description {
- display: block;
- color: @muted-color;
- font-size: 12px;
- margin-top: 3px;
-}
diff --git a/extensions/suspend/js/src/forum/components/SuspendUserModal.js b/extensions/suspend/js/src/forum/components/SuspendUserModal.js
index faeb03bb16..c581c803ef 100644
--- a/extensions/suspend/js/src/forum/components/SuspendUserModal.js
+++ b/extensions/suspend/js/src/forum/components/SuspendUserModal.js
@@ -6,6 +6,8 @@ import withAttr from 'flarum/common/utils/withAttr';
import ItemList from 'flarum/common/utils/ItemList';
import { getPermanentSuspensionDate } from '../helpers/suspensionHelper';
+import Form from '@flarum/core/src/common/components/Form';
+import FieldSet from '@flarum/core/src/common/components/FieldSet';
export default class SuspendUserModal extends Modal {
oninit(vnode) {
@@ -40,18 +42,15 @@ export default class SuspendUserModal extends Modal {
content() {
return (
-
-
-
{app.translator.trans('flarum-suspend.forum.suspend_user.status_heading')}
-
{this.formItems().toArray()}
-
+
);
}
@@ -109,20 +108,22 @@ export default class SuspendUserModal extends Modal {
formItems() {
const items = new ItemList();
- items.add('radioItems',
{this.radioItems().toArray()}
, 100);
+ items.add(
+ 'radioItems',
+
{this.radioItems().toArray()} ,
+ 100
+ );
items.add(
'reason',
-
- {app.translator.trans('flarum-suspend.forum.suspend_user.reason')}
-
-
+ {app.translator.trans('flarum-suspend.forum.suspend_user.reason')}
+
,
90
);
@@ -130,15 +131,13 @@ export default class SuspendUserModal extends Modal {
items.add(
'message',
-
- {app.translator.trans('flarum-suspend.forum.suspend_user.display_message')}
-
-
+ {app.translator.trans('flarum-suspend.forum.suspend_user.display_message')}
+
,
80
);
diff --git a/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js b/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js
index 557851bae3..b404258af5 100644
--- a/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js
+++ b/extensions/suspend/js/src/forum/components/SuspensionInfoModal.js
@@ -3,6 +3,7 @@ import Modal from 'flarum/common/components/Modal';
import Button from 'flarum/common/components/Button';
import fullTime from 'flarum/common/helpers/fullTime';
import { isPermanentSuspensionDate, localStorageKey } from '../helpers/suspensionHelper';
+import Form from '@flarum/core/src/common/components/Form';
export default class SuspensionInfoModal extends Modal {
oninit(vnode) {
@@ -27,16 +28,16 @@ export default class SuspensionInfoModal extends Modal {
return (
-
);
}
diff --git a/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js b/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js
index 612e582b80..d980464a87 100644
--- a/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js
+++ b/extensions/suspend/js/src/forum/components/UserSuspendedNotification.js
@@ -23,4 +23,8 @@ export default class UserSuspendedNotification extends Notification {
timeReadable,
});
}
+
+ excerpt() {
+ return null;
+ }
}
diff --git a/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js b/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js
index a09e83a9d5..0f63ffeecc 100644
--- a/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js
+++ b/extensions/suspend/js/src/forum/components/UserUnsuspendedNotification.js
@@ -15,4 +15,8 @@ export default class UserUnsuspendedNotification extends Notification {
return app.translator.trans('flarum-suspend.forum.notifications.user_unsuspended_text');
}
+
+ excerpt() {
+ return null;
+ }
}
diff --git a/extensions/tags/js/src/@types/shims.d.ts b/extensions/tags/js/src/@types/shims.d.ts
index b1056ac770..6b021365d3 100644
--- a/extensions/tags/js/src/@types/shims.d.ts
+++ b/extensions/tags/js/src/@types/shims.d.ts
@@ -20,11 +20,11 @@ declare module 'flarum/common/models/Discussion' {
}
}
-declare module 'flarum/forum/components/IndexPage' {
- export default interface IndexPage {
+declare module 'flarum/forum/ForumApplication' {
+ export default interface ForumApplication {
currentActiveTag?: Tag;
currentTagLoading?: boolean;
- currentTag: () => Tag | undefined;
+ currentTag: (reload?: boolean) => Tag | undefined;
}
}
diff --git a/extensions/tags/js/src/admin/components/EditTagModal.tsx b/extensions/tags/js/src/admin/components/EditTagModal.tsx
index 489c8e03cc..62c412ebe8 100644
--- a/extensions/tags/js/src/admin/components/EditTagModal.tsx
+++ b/extensions/tags/js/src/admin/components/EditTagModal.tsx
@@ -5,12 +5,12 @@ import ColorPreviewInput from 'flarum/common/components/ColorPreviewInput';
import ItemList from 'flarum/common/utils/ItemList';
import { slug } from 'flarum/common/utils/string';
import Stream from 'flarum/common/utils/Stream';
+import extractText from 'flarum/common/utils/extractText';
+import Form from 'flarum/common/components/Form';
+import type Mithril from 'mithril';
import tagLabel from '../../common/helpers/tagLabel';
-import type Mithril from 'mithril';
import type Tag from '../../common/models/Tag';
-import extractText from 'flarum/common/utils/extractText';
-import { ModelIdentifier } from 'flarum/common/Model';
export interface EditTagModalAttrs extends IInternalModalAttrs {
primary?: boolean;
@@ -59,7 +59,7 @@ export default class EditTagModal extends Modal
{
content() {
return (
-
{this.fields().toArray()}
+
);
}
@@ -139,7 +139,7 @@ export default class EditTagModal extends Modal {
items.add(
'submit',
-
+
{app.translator.trans('flarum-tags.admin.edit_tag.submit_button')}
diff --git a/extensions/tags/js/src/admin/components/TagsPage.js b/extensions/tags/js/src/admin/components/TagsPage.js
index 4564869bb9..21becc24d9 100644
--- a/extensions/tags/js/src/admin/components/TagsPage.js
+++ b/extensions/tags/js/src/admin/components/TagsPage.js
@@ -5,6 +5,7 @@ import ExtensionPage from 'flarum/admin/components/ExtensionPage';
import Button from 'flarum/common/components/Button';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
import withAttr from 'flarum/common/utils/withAttr';
+import Form from 'flarum/common/components/Form';
import EditTagModal from './EditTagModal';
import tagIcon from '../../common/helpers/tagIcon';
@@ -86,8 +87,7 @@ export default class TagsPage extends ExtensionPage {
{app.translator.trans('flarum-tags.admin.tags.create_secondary_tag_button')}
-
-
{app.translator.trans('flarum-tags.admin.tags.settings_heading')}
+
-
{this.submitButton()}
-
+ {this.submitButton()}
+
{app.translator.trans('flarum-tags.admin.tags.about_tags_text')}
diff --git a/extensions/tags/js/src/common/helpers/tagIcon.js b/extensions/tags/js/src/common/helpers/tagIcon.js
index 37bfb2a632..990b5a7587 100644
--- a/extensions/tags/js/src/common/helpers/tagIcon.js
+++ b/extensions/tags/js/src/common/helpers/tagIcon.js
@@ -4,15 +4,11 @@ export default function tagIcon(tag, attrs = {}, settings = {}) {
const hasIcon = tag && tag.icon();
const { useColor = true } = settings;
- attrs.className = classList([attrs.className, 'icon', hasIcon ? tag.icon() : 'TagIcon']);
+ attrs.className = classList([attrs.className, 'icon text-colored', hasIcon ? tag.icon() : 'TagIcon']);
if (tag && useColor) {
attrs.style = attrs.style || {};
attrs.style['--color'] = tag.color();
-
- if (hasIcon) {
- attrs.style.color = tag.color();
- }
} else if (!tag) {
attrs.className += ' untagged';
}
diff --git a/extensions/tags/js/src/forum/addTagComposer.js b/extensions/tags/js/src/forum/addTagComposer.js
index adb11309ea..7cffe7ce16 100644
--- a/extensions/tags/js/src/forum/addTagComposer.js
+++ b/extensions/tags/js/src/forum/addTagComposer.js
@@ -1,14 +1,15 @@
+import app from 'flarum/forum/app';
import { extend, override } from 'flarum/common/extend';
-import IndexPage from 'flarum/forum/components/IndexPage';
+import IndexSidebar from 'flarum/forum/components/IndexSidebar';
import classList from 'flarum/common/utils/classList';
import tagsLabel from '../common/helpers/tagsLabel';
import getSelectableTags from './utils/getSelectableTags';
export default function addTagComposer() {
- extend(IndexPage.prototype, 'newDiscussionAction', function (promise) {
+ extend(IndexSidebar.prototype, 'newDiscussionAction', function (promise) {
// From `addTagFilter
- const tag = this.currentTag();
+ const tag = app.currentTag();
if (tag) {
const parent = tag.parent();
diff --git a/extensions/tags/js/src/forum/addTagFilter.tsx b/extensions/tags/js/src/forum/addTagFilter.tsx
index b2742bdb72..dd8d7cd69b 100644
--- a/extensions/tags/js/src/forum/addTagFilter.tsx
+++ b/extensions/tags/js/src/forum/addTagFilter.tsx
@@ -2,24 +2,25 @@ import app from 'flarum/forum/app';
import type Mithril from 'mithril';
import { extend, override } from 'flarum/common/extend';
import IndexPage from 'flarum/forum/components/IndexPage';
+import IndexSidebar from 'flarum/forum/components/IndexSidebar';
import DiscussionListState from 'flarum/forum/states/DiscussionListState';
import GlobalSearchState from 'flarum/forum/states/GlobalSearchState';
import classList from 'flarum/common/utils/classList';
import textContrastClass from 'flarum/common/helpers/textContrastClass';
+import type { ComponentAttrs } from 'flarum/common/Component';
import TagHero from './components/TagHero';
-import Tag from '../common/models/Tag';
-import { ComponentAttrs } from 'flarum/common/Component';
+import type Tag from '../common/models/Tag';
const findTag = (slug: string) => app.store.all
('tags').find((tag) => tag.slug().localeCompare(slug, undefined, { sensitivity: 'base' }) === 0);
export default function addTagFilter() {
- IndexPage.prototype.currentTag = function () {
- if (this.currentActiveTag) {
+ app.currentTag = function (reload?: boolean) {
+ if (this.currentActiveTag && !reload) {
return this.currentActiveTag;
}
- const slug = app.search.params().tags;
+ const slug = this.search.params().tags;
let tag = null;
if (slug) {
@@ -37,7 +38,7 @@ export default function addTagFilter() {
// a child tag page, then either:
// - We loaded in that child tag (and its siblings) in the API document
// - We first navigated to the current tag's parent, which would have loaded in the current tag's siblings.
- app.store
+ this.store
.find('tags', slug, { include: 'children,children.parent,parent,state' })
.then(() => {
this.currentActiveTag = findTag(slug);
@@ -54,12 +55,18 @@ export default function addTagFilter() {
return this.currentActiveTag;
}
+ this.currentActiveTag = undefined;
+
return;
};
+ extend(IndexPage.prototype, 'view', function (vdom: Mithril.Vnode) {
+ app.currentTag(true);
+ });
+
// If currently viewing a tag, insert a tag hero at the top of the view.
override(IndexPage.prototype, 'hero', function (original) {
- const tag = this.currentTag();
+ const tag = app.currentTag();
if (tag) return ;
@@ -67,13 +74,13 @@ export default function addTagFilter() {
});
extend(IndexPage.prototype, 'view', function (vdom: Mithril.Vnode) {
- const tag = this.currentTag();
+ const tag = app.currentTag();
if (tag) vdom.attrs.className += ' IndexPage--tag' + tag.id();
});
extend(IndexPage.prototype, 'setTitle', function () {
- const tag = this.currentTag();
+ const tag = app.currentTag();
if (tag) {
app.setTitle(tag.name());
@@ -82,8 +89,8 @@ export default function addTagFilter() {
// If currently viewing a tag, restyle the 'new discussion' button to use
// the tag's color, and disable if the user isn't allowed to edit.
- extend(IndexPage.prototype, 'sidebarItems', function (items) {
- const tag = this.currentTag();
+ extend(IndexSidebar.prototype, 'items', function (items) {
+ const tag = app.currentTag();
if (tag) {
const color = tag.color();
diff --git a/extensions/tags/js/src/forum/addTagList.js b/extensions/tags/js/src/forum/addTagList.js
index 54d72a229f..590aae3d4d 100644
--- a/extensions/tags/js/src/forum/addTagList.js
+++ b/extensions/tags/js/src/forum/addTagList.js
@@ -1,17 +1,17 @@
+import app from 'flarum/forum/app';
import { extend } from 'flarum/common/extend';
-import IndexPage from 'flarum/forum/components/IndexPage';
+import IndexSidebar from 'flarum/forum/components/IndexSidebar';
import Separator from 'flarum/common/components/Separator';
import LinkButton from 'flarum/common/components/LinkButton';
import TagLinkButton from './components/TagLinkButton';
import TagsPage from './components/TagsPage';
-import app from 'flarum/forum/app';
import sortTags from '../common/utils/sortTags';
export default function addTagList() {
// Add a link to the tags page, as well as a list of all the tags,
// to the index page's sidebar.
- extend(IndexPage.prototype, 'navItems', function (items) {
+ extend(IndexSidebar.prototype, 'navItems', function (items) {
items.add(
'tags',
@@ -26,7 +26,7 @@ export default function addTagList() {
const params = app.search.stickyParams();
const tags = app.store.all('tags');
- const currentTag = this.currentTag();
+ const currentTag = app.currentTag();
const addTag = (tag) => {
let active = currentTag === tag;
diff --git a/extensions/tags/js/src/forum/components/TagsPage.js b/extensions/tags/js/src/forum/components/TagsPage.tsx
similarity index 60%
rename from extensions/tags/js/src/forum/components/TagsPage.js
rename to extensions/tags/js/src/forum/components/TagsPage.tsx
index c21adf5d84..4620e08dfd 100755
--- a/extensions/tags/js/src/forum/components/TagsPage.js
+++ b/extensions/tags/js/src/forum/components/TagsPage.tsx
@@ -1,26 +1,38 @@
+import app from 'flarum/forum/app';
import Page from 'flarum/common/components/Page';
-import IndexPage from 'flarum/forum/components/IndexPage';
+import type { IPageAttrs } from 'flarum/common/components/Page';
+import PageStructure from 'flarum/forum/components/PageStructure';
+import WelcomeHero from 'flarum/forum/components/WelcomeHero';
+import IndexSidebar from 'flarum/forum/components/IndexSidebar';
import Link from 'flarum/common/components/Link';
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
-import listItems from 'flarum/common/helpers/listItems';
import ItemList from 'flarum/common/utils/ItemList';
import humanTime from 'flarum/common/helpers/humanTime';
import textContrastClass from 'flarum/common/helpers/textContrastClass';
import classList from 'flarum/common/utils/classList';
+import extractText from 'flarum/common/utils/extractText';
import tagIcon from '../../common/helpers/tagIcon';
import tagLabel from '../../common/helpers/tagLabel';
import sortTags from '../../common/utils/sortTags';
+import Mithril from 'mithril';
-export default class TagsPage extends Page {
- oninit(vnode) {
+import type Tag from '../../common/models/Tag';
+
+export interface ITagsPageAttrs extends IPageAttrs {}
+
+export default class TagsPage extends Page {
+ private tags!: Tag[];
+ private loading!: boolean;
+
+ oninit(vnode: Mithril.Vnode) {
super.oninit(vnode);
- app.history.push('tags', app.translator.trans('flarum-tags.forum.header.back_to_tags_tooltip'));
+ app.history.push('tags', extractText(app.translator.trans('flarum-tags.forum.header.back_to_tags_tooltip')));
this.tags = [];
- const preloaded = app.preloadedApiDocument();
+ const preloaded = app.preloadedApiDocument();
if (preloaded) {
this.tags = sortTags(preloaded.filter((tag) => !tag.isChild()));
@@ -30,7 +42,7 @@ export default class TagsPage extends Page {
this.loading = true;
app.tagList.load(['children', 'lastPostedDiscussion', 'parent']).then(() => {
- this.tags = sortTags(app.store.all('tags').filter((tag) => !tag.isChild()));
+ this.tags = sortTags(app.store.all('tags').filter((tag) => !tag.isChild()));
this.loading = false;
@@ -38,37 +50,19 @@ export default class TagsPage extends Page {
});
}
- oncreate(vnode) {
+ oncreate(vnode: Mithril.VnodeDOM) {
super.oncreate(vnode);
- app.setTitle(app.translator.trans('flarum-tags.forum.all_tags.meta_title_text'));
+ app.setTitle(extractText(app.translator.trans('flarum-tags.forum.all_tags.meta_title_text')));
app.setTitleCount(0);
}
view() {
- return {this.pageContent().toArray()}
;
- }
-
- pageContent() {
- const items = new ItemList();
-
- items.add('hero', this.hero(), 100);
- items.add('main', {this.mainContent().toArray()}
, 10);
-
- return items;
- }
-
- mainContent() {
- const items = new ItemList();
-
- items.add('sidebar', this.sidebar(), 100);
- items.add('content', this.content(), 10);
-
- return items;
- }
-
- content() {
- return {this.contentItems().toArray()}
;
+ return (
+
+ {this.contentItems().toArray()}
+
+ );
}
contentItems() {
@@ -91,43 +85,37 @@ export default class TagsPage extends Page {
}
hero() {
- return IndexPage.prototype.hero();
+ return ;
}
sidebar() {
- return (
-
- {listItems(this.sidebarItems().toArray())}
-
- );
- }
-
- sidebarItems() {
- return IndexPage.prototype.sidebarItems();
+ return ;
}
- tagTileListView(pinned) {
+ tagTileListView(pinned: Tag[]) {
return {pinned.map(this.tagTileView.bind(this))} ;
}
- tagTileView(tag) {
+ tagTileView(tag: Tag) {
const lastPostedDiscussion = tag.lastPostedDiscussion();
- const children = sortTags(tag.children() || []);
+ const children = sortTags((tag.children() || []) as Tag[]);
return (
- {tag.icon() && tagIcon(tag, {}, { useColor: false })}
- {tag.name()}
+
+ {tag.icon() && tagIcon(tag, {}, { useColor: false })}
+
{tag.name()}
+
{tag.description()}
{!!children && (
{children.map((child) => [ {child.name()}, ' '])}
)}
{lastPostedDiscussion ? (
-
+
{lastPostedDiscussion.title()}
- {humanTime(lastPostedDiscussion.lastPostedAt())}
+ {humanTime(lastPostedDiscussion.lastPostedAt()!)}
) : (
@@ -136,7 +124,7 @@ export default class TagsPage extends Page {
);
}
- cloudView(cloud) {
+ cloudView(cloud: Tag[]) {
return {cloud.map((tag) => [tagLabel(tag, { link: true }), ' '])}
;
}
}
diff --git a/extensions/tags/less/admin/EditTagModal.less b/extensions/tags/less/admin/EditTagModal.less
index 49150870a5..7ca9b252c6 100644
--- a/extensions/tags/less/admin/EditTagModal.less
+++ b/extensions/tags/less/admin/EditTagModal.less
@@ -1,8 +1,3 @@
-.EditTagModal {
- .Form-group:not(:last-child) {
- margin-bottom: 30px;
- }
-}
.EditTagModal-delete {
- float: right;
+ margin-left: auto;
}
diff --git a/extensions/tags/less/admin/TagsPage.less b/extensions/tags/less/admin/TagsPage.less
index 285abbd53f..20c9ca9322 100644
--- a/extensions/tags/less/admin/TagsPage.less
+++ b/extensions/tags/less/admin/TagsPage.less
@@ -3,7 +3,7 @@
}
.TagsContent-footer {
- color: @control-color;
+ color: var(--control-color);
padding: 20px 0;
p {
@@ -20,7 +20,7 @@
.TagList ol {
list-style: none;
padding: 0;
- color: @muted-color;
+ color: var(--muted-color);
font-size: 13px;
>li {
@@ -37,11 +37,11 @@
}
.TagListItem-info {
- border-radius: @border-radius;
+ border-radius: var(--border-radius);
padding: 5px;
&:hover {
- background: @control-bg;
+ background: var(--control-bg);
}
.Button {
@@ -75,8 +75,8 @@ li:not(.sortable-dragging)>.TagListItem-info:hover>.Button {
}
.sortable-placeholder {
- border: 2px dashed @control-bg;
- border-radius: @border-radius;
+ border: 2px dashed var(--control-bg);
+ border-radius: var(--border-radius);
height: 34px;
}
@@ -120,8 +120,8 @@ li:not(.sortable-dragging)>.TagListItem-info:hover>.Button {
min-height: 20vh;
max-width: 400px;
grid-template-rows: min-content;
- border: 1px solid @control-bg;
- border-radius: @border-radius;
+ border: 1px solid var(--control-bg);
+ border-radius: var(--border-radius);
flex: 1 1 160px;
@media (max-width: 1209px) {
@@ -142,7 +142,7 @@ li:not(.sortable-dragging)>.TagListItem-info:hover>.Button {
.TagList-button {
background: none;
- border: 1px dashed @control-bg;
+ border: 1px dashed var(--control-bg);
height: 40px;
margin: auto auto 0 0;
}
@@ -150,7 +150,7 @@ li:not(.sortable-dragging)>.TagListItem-info:hover>.Button {
>label {
float: left;
font-weight: bold;
- color: @muted-color;
+ color: var(--muted-color);
}
}
}
diff --git a/extensions/tags/less/common/TagIcon.less b/extensions/tags/less/common/TagIcon.less
index 4ebfac50bb..2b8354a70f 100644
--- a/extensions/tags/less/common/TagIcon.less
+++ b/extensions/tags/less/common/TagIcon.less
@@ -1,14 +1,14 @@
.TagIcon {
- border-radius: @border-radius;
+ border-radius: var(--border-radius);
width: 16px;
height: 16px;
display: inline-block;
vertical-align: -3px;
margin-left: 1px;
- background: var(--color, @control-bg);
+ background: var(--color, var(--control-bg));
&.untagged {
- border: 1px dotted @muted-color;
+ border: 1px dotted var(--muted-color);
background: transparent;
}
}
diff --git a/extensions/tags/less/common/TagLabel.less b/extensions/tags/less/common/TagLabel.less
index 373b18910c..a9e4d7305e 100644
--- a/extensions/tags/less/common/TagLabel.less
+++ b/extensions/tags/less/common/TagLabel.less
@@ -34,7 +34,7 @@
.DiscussionHero .TagsLabel & {
background: transparent;
- border-radius: @border-radius !important;
+ border-radius: var(--border-radius) !important;
font-size: 14px;
&.colored, &--colored {
@@ -58,13 +58,13 @@
border-radius: 0;
&:first-child {
- border-radius: @border-radius 0 0 @border-radius;
+ border-radius: var(--border-radius) 0 0 var(--border-radius);
}
&:last-child {
- border-radius: 0 @border-radius @border-radius 0;
+ border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
&:first-child:last-child {
- border-radius: @border-radius;
+ border-radius: var(--border-radius);
}
}
}
diff --git a/extensions/tags/less/common/TagSelectionModal.less b/extensions/tags/less/common/TagSelectionModal.less
index 788b3fbe88..184f52bc9e 100644
--- a/extensions/tags/less/common/TagSelectionModal.less
+++ b/extensions/tags/less/common/TagSelectionModal.less
@@ -1,12 +1,12 @@
.TagSelectionModal {
@media @tablet-up {
.Modal-header {
- background: @control-bg;
+ background: var(--control-bg);
padding: 20px 20px 0;
& h3 {
text-align: left;
- color: @control-color;
+ color: var(--control-color);
font-size: 16px;
}
}
@@ -103,7 +103,7 @@
}
}
&.pinned + li:not(.pinned) {
- border-top: 2px solid @control-bg;
+ border-top: 2px solid var(--control-bg);
}
&.child {
padding-left: 45px;
@@ -113,7 +113,7 @@
}
}
&.active {
- background: @control-bg;
+ background: var(--control-bg);
}
.icon::before {
@@ -127,7 +127,7 @@
.icon::before {
.fas();
content: @fa-var-check !important;
- color: @muted-color;
+ color: var(--muted-color);
font-size: 14px;
text-align: center;
vertical-align: 1px;
@@ -159,7 +159,7 @@
}
}
.SelectTagListItem-description {
- color: @muted-color;
+ color: var(--muted-color);
font-size: 12px;
width: 370px;
display: inline-block;
diff --git a/extensions/tags/less/common/root.less b/extensions/tags/less/common/root.less
index 97005c0933..662178e438 100644
--- a/extensions/tags/less/common/root.less
+++ b/extensions/tags/less/common/root.less
@@ -1,4 +1,4 @@
:root {
- --tag-bg: @control-bg;
- --tag-color: @control-color;
+ --tag-bg: var(--control-bg);
+ --tag-color: var(--control-color);
}
diff --git a/extensions/tags/less/forum.less b/extensions/tags/less/forum.less
index 287e0fad1f..bca726db6b 100644
--- a/extensions/tags/less/forum.less
+++ b/extensions/tags/less/forum.less
@@ -19,14 +19,10 @@
}
.TagLinkButton {
&.child {
- @media @tablet-up {
- padding-top: 4px;
- padding-bottom: 4px;
- }
margin-left: 10px;
.TagIcon {
- display: none;
+ visibility: hidden;
}
}
@@ -72,11 +68,13 @@
}
@media @desktop-up {
.TagsPage {
+ --sidebar-width: 100%;
+ --gap: 30px;
+
.sideNav {
.sideNav--horizontal();
- float: none;
width: auto;
- padding-top: 0;
+ padding: 0;
&:after {
display: none;
@@ -86,8 +84,13 @@
width: 190px;
}
}
- .sideNavOffset {
- margin: 15px 0 0;
+
+ .Page-container {
+ flex-direction: column;
+ }
+
+ .Page-content {
+ margin-top: 0;
}
}
}
diff --git a/extensions/tags/less/forum/TagTiles.less b/extensions/tags/less/forum/TagTiles.less
index 8f03e6e79d..c2d084228b 100755
--- a/extensions/tags/less/forum/TagTiles.less
+++ b/extensions/tags/less/forum/TagTiles.less
@@ -3,64 +3,70 @@
padding: 0;
margin: 0;
overflow: hidden;
+ display: grid;
+ grid-template-columns: 1fr;
@media @phone {
margin: -15px -15px 0;
}
+ @media @tablet {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ @media @desktop-up {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+
> li {
height: 200px;
overflow: hidden;
@media @tablet {
- float: left;
- width: 50%;
-
&:first-child {
- border-top-left-radius: @border-radius;
+ border-top-left-radius: var(--border-radius);
}
&:nth-child(2) {
- border-top-right-radius: @border-radius;
+ border-top-right-radius: var(--border-radius);
}
&:nth-last-child(2):nth-child(even), &:last-child {
- border-bottom-right-radius: @border-radius;
+ border-bottom-right-radius: var(--border-radius);
}
&:nth-last-child(2):nth-child(odd), &:last-child:nth-child(odd) {
- border-bottom-left-radius: @border-radius;
+ border-bottom-left-radius: var(--border-radius);
}
}
@media @desktop-up {
- float: left;
- width: 33.333%;
-
&:first-child {
- border-top-left-radius: @border-radius;
+ border-top-left-radius: var(--border-radius);
}
&:nth-child(3),
&:nth-child(2):last-child,
&:first-child:last-child {
- border-top-right-radius: @border-radius;
+ border-top-right-radius: var(--border-radius);
}
&:nth-child(3n):nth-last-child(2),
&:nth-child(3n):nth-last-child(3),
&:last-child {
- border-bottom-right-radius: @border-radius;
+ border-bottom-right-radius: var(--border-radius);
}
&:nth-child(3n+1):last-child,
&:nth-child(3n+1):nth-last-child(2),
&:nth-child(3n+1):nth-last-child(3) {
- border-bottom-left-radius: @border-radius;
+ border-bottom-left-radius: var(--border-radius);
}
}
}
}
.TagTile {
+ display: flex;
+ flex-direction: column;
position: relative;
background: var(--tag-bg);
&, a {
- color: @control-color;
+ color: var(--control-color);
}
&.colored {
&, a {
@@ -72,34 +78,36 @@
padding: 20px;
text-decoration: none !important;
display: block;
- position: absolute;
- left: 0;
- right: 0;
}
.TagTile-info {
- top: 0;
- bottom: 42px;
padding-right: 20px;
transition: background 0.2s;
overflow: auto;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
&:hover {
background: fade(#000, 5%);
}
.icon {
font-size: 24px;
- float: left;
- margin-right: 10px;
}
}
+.TagTile-heading {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
.TagTile-name {
font-size: 18px;
- margin: 0 0 10px;
+ margin: 0;
font-weight: bold;
}
.TagTile-description {
font-size: 12px;
- margin: 0 0 10px;
+ margin: 0;
opacity: 70%;
}
.TagTile-children {
@@ -111,7 +119,6 @@
}
}
.TagTile-lastPostedDiscussion {
- bottom: 0;
height: 42px;
padding: 7px 0;
white-space: nowrap;
@@ -122,6 +129,7 @@
border-top: 1px solid rgba(0, 0, 0, 0.15);
margin: 0 20px;
opacity: 70%;
+ flex-shrink: 0;
&:hover .TagTile-lastPostedDiscussion-title {
text-decoration: underline;
diff --git a/framework/core/js/src/admin/components/AdminHeader.js b/framework/core/js/src/admin/components/AdminHeader.js
index 5472a8064e..f05a5602bc 100644
--- a/framework/core/js/src/admin/components/AdminHeader.js
+++ b/framework/core/js/src/admin/components/AdminHeader.js
@@ -1,6 +1,7 @@
import Component from '../../common/Component';
import classList from '../../common/utils/classList';
-import icon from '../../common/helpers/icon';
+
+import Icon from '../../common/components/Icon';
export default class AdminHeader extends Component {
view(vnode) {
@@ -8,7 +9,7 @@ export default class AdminHeader extends Component {
- {icon(this.attrs.icon)}
+
{vnode.children}
{this.attrs.description}
diff --git a/framework/core/js/src/admin/components/AdminPage.tsx b/framework/core/js/src/admin/components/AdminPage.tsx
index 4d1d47add3..a705c6b047 100644
--- a/framework/core/js/src/admin/components/AdminPage.tsx
+++ b/framework/core/js/src/admin/components/AdminPage.tsx
@@ -12,6 +12,8 @@ import AdminHeader from './AdminHeader';
import generateElementId from '../utils/generateElementId';
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
import ItemList from '../../common/utils/ItemList';
+import type { IUploadImageButtonAttrs } from './UploadImageButton';
+import UploadImageButton from './UploadImageButton';
export interface AdminHeaderOptions {
title: Mithril.Children;
@@ -79,6 +81,7 @@ const BooleanSettingTypes = ['bool', 'checkbox', 'switch', 'boolean'] as const;
const SelectSettingTypes = ['select', 'dropdown', 'selectdropdown'] as const;
const TextareaSettingTypes = ['textarea'] as const;
const ColorPreviewSettingType = 'color-preview' as const;
+const ImageUploadSettingType = 'image-upload' as const;
/**
* Valid options for the setting component builder to generate a Switch.
@@ -113,6 +116,10 @@ export interface ColorPreviewSettingComponentOptions extends CommonSettingsItemO
type: typeof ColorPreviewSettingType;
}
+export interface ImageUploadSettingComponentOptions extends CommonSettingsItemOptions, IUploadImageButtonAttrs {
+ type: typeof ImageUploadSettingType;
+}
+
export interface CustomSettingComponentOptions extends CommonSettingsItemOptions {
type: string;
[key: string]: unknown;
@@ -127,6 +134,7 @@ export type SettingsComponentOptions =
| SelectSettingComponentOptions
| TextareaSettingComponentOptions
| ColorPreviewSettingComponentOptions
+ | ImageUploadSettingComponentOptions
| CustomSettingComponentOptions;
/**
@@ -295,7 +303,7 @@ export default abstract class AdminPage
{label}
- {help}
+ {help ? {help}
: null}
);
} else if ((SelectSettingTypes as readonly string[]).includes(type)) {
@@ -311,6 +319,10 @@ export default abstract class AdminPage
);
+ } else if (type === ImageUploadSettingType) {
+ const { value, ...otherAttrs } = componentAttrs;
+
+ settingElement =
;
} else if (customSettingComponents.has(type)) {
return customSettingComponents.get(type)({ setting, help, label, ...componentAttrs });
} else {
@@ -334,9 +346,11 @@ export default abstract class AdminPage
{label && {label} }
-
- {help}
-
+ {help && (
+
+ {help}
+
+ )}
{settingElement}
);
diff --git a/framework/core/js/src/admin/components/AppearancePage.tsx b/framework/core/js/src/admin/components/AppearancePage.tsx
index 12cfb7c9cf..54c2c3a60a 100644
--- a/framework/core/js/src/admin/components/AppearancePage.tsx
+++ b/framework/core/js/src/admin/components/AppearancePage.tsx
@@ -7,6 +7,8 @@ import UploadImageButton from './UploadImageButton';
import AdminPage from './AdminPage';
import ItemList from '../../common/utils/ItemList';
import type Mithril from 'mithril';
+import Form from '../../common/components/Form';
+import FieldSet from '../../common/components/FieldSet';
export default class AppearancePage extends AdminPage {
headerInfo() {
@@ -21,48 +23,53 @@ export default class AppearancePage extends AdminPage {
content() {
return (
<>
-
-
- {app.translator.trans('core.admin.appearance.colors_heading')}
+
+
+
-
- {app.translator.trans('core.admin.appearance.logo_heading')}
- {app.translator.trans('core.admin.appearance.logo_text')}
-
-
+
>
);
}
@@ -70,8 +77,6 @@ export default class AppearancePage extends AdminPage {
colorItems() {
const items = new ItemList();
- items.add('helpText', {app.translator.trans('core.admin.appearance.colors_text')}
, 80);
-
items.add(
'theme-colors',
@@ -111,7 +116,7 @@ export default class AppearancePage extends AdminPage {
50
);
- items.add('submit', this.submitButton(), 0);
+ items.add('submit',
{this.submitButton()}
, 0);
return items;
}
diff --git a/framework/core/js/src/admin/components/BasicsPage.tsx b/framework/core/js/src/admin/components/BasicsPage.tsx
index fb1e83c342..abfa9f77c8 100644
--- a/framework/core/js/src/admin/components/BasicsPage.tsx
+++ b/framework/core/js/src/admin/components/BasicsPage.tsx
@@ -4,6 +4,7 @@ import ItemList from '../../common/utils/ItemList';
import AdminPage from './AdminPage';
import type { IPageAttrs } from '../../common/components/Page';
import type Mithril from 'mithril';
+import Form from '../../common/components/Form';
export type HomePageItem = { path: string; label: Mithril.Children };
@@ -43,7 +44,7 @@ export default class BasicsPage
ext
content() {
return [
- ,
+ {this.submitButton()}
+ ,
];
}
diff --git a/framework/core/js/src/admin/components/CreateUserModal.tsx b/framework/core/js/src/admin/components/CreateUserModal.tsx
index c0e21d5127..7b8dfa9601 100644
--- a/framework/core/js/src/admin/components/CreateUserModal.tsx
+++ b/framework/core/js/src/admin/components/CreateUserModal.tsx
@@ -7,6 +7,7 @@ import Stream from '../../common/utils/Stream';
import type Mithril from 'mithril';
import Switch from '../../common/components/Switch';
import { generateRandomString } from '../../common/utils/string';
+import Form from '../../common/components/Form';
export interface ICreateUserModalAttrs extends IInternalModalAttrs {
username?: string;
@@ -79,7 +80,7 @@ export default class CreateUserModal
- {this.fields().toArray()}
+ {this.fields().toArray()}
>
);
}
diff --git a/framework/core/js/src/admin/components/EditGroupModal.tsx b/framework/core/js/src/admin/components/EditGroupModal.tsx
index a8597737e5..a77e684df8 100644
--- a/framework/core/js/src/admin/components/EditGroupModal.tsx
+++ b/framework/core/js/src/admin/components/EditGroupModal.tsx
@@ -9,6 +9,7 @@ import Stream from '../../common/utils/Stream';
import Mithril from 'mithril';
import extractText from '../../common/utils/extractText';
import ColorPreviewInput from '../../common/components/ColorPreviewInput';
+import Form from '../../common/components/Form';
export interface IEditGroupModalAttrs extends IInternalModalAttrs {
group?: Group;
@@ -54,7 +55,7 @@ export default class EditGroupModal
- {this.fields().toArray()}
+ {this.fields().toArray()}
);
}
@@ -66,7 +67,7 @@ export default class EditGroupModal
{app.translator.trans('core.admin.edit_group.name_label')}
-
+
@@ -107,7 +108,7 @@ export default class EditGroupModal
+
{app.translator.trans('core.admin.edit_group.submit_button')}
diff --git a/framework/core/js/src/admin/components/ExtensionLinkButton.js b/framework/core/js/src/admin/components/ExtensionLinkButton.js
index 3c9b774f63..d97d883899 100644
--- a/framework/core/js/src/admin/components/ExtensionLinkButton.js
+++ b/framework/core/js/src/admin/components/ExtensionLinkButton.js
@@ -1,8 +1,8 @@
import app from '../../admin/app';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import LinkButton from '../../common/components/LinkButton';
-import icon from '../../common/helpers/icon';
import ItemList from '../../common/utils/ItemList';
+import Icon from '../../common/components/Icon';
export default class ExtensionLinkButton extends LinkButton {
getButtonContent(children) {
@@ -12,7 +12,7 @@ export default class ExtensionLinkButton extends LinkButton {
content.unshift(
- {!!extension.icon && icon(extension.icon.name)}
+ {!!extension.icon && }
);
content.push(statuses);
diff --git a/framework/core/js/src/admin/components/ExtensionPage.tsx b/framework/core/js/src/admin/components/ExtensionPage.tsx
index 5fe6da1232..6eb820697b 100644
--- a/framework/core/js/src/admin/components/ExtensionPage.tsx
+++ b/framework/core/js/src/admin/components/ExtensionPage.tsx
@@ -3,7 +3,6 @@ import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import LinkButton from '../../common/components/LinkButton';
import Switch from '../../common/components/Switch';
-import icon from '../../common/helpers/icon';
import punctuateSeries from '../../common/helpers/punctuateSeries';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
@@ -17,6 +16,8 @@ import { Extension } from '../AdminApplication';
import { IPageAttrs } from '../../common/components/Page';
import type Mithril from 'mithril';
import extractText from '../../common/utils/extractText';
+import Form from '../../common/components/Form';
+import Icon from '../../common/components/Icon';
export interface ExtensionPageAttrs extends IPageAttrs {
id: string;
@@ -79,7 +80,7 @@ export default class ExtensionPage
- {!!this.extension.icon && icon(this.extension.icon.name)}
+ {!!this.extension.icon && }
{this.extension.extra['flarum-extension'].title}
@@ -139,10 +140,10 @@ export default class ExtensionPage
{settings ? (
-
+
{settings.map(this.buildSettingComponent.bind(this))}
- {this.submitButton()}
-
+
{this.submitButton()}
+
) : (
{app.translator.trans('core.admin.extension.no_settings')}
)}
@@ -194,7 +195,7 @@ export default class ExtensionPage
));
- items.add('authors', [icon('fas fa-user'), {punctuateSeries(authors)} ]);
+ items.add('authors', [ , {punctuateSeries(authors)} ]);
}
(Object.keys(this.infoFields) as (keyof ExtensionPage['infoFields'])[]).map((field) => {
diff --git a/framework/core/js/src/admin/components/ExtensionsWidget.js b/framework/core/js/src/admin/components/ExtensionsWidget.js
index 32ee0e5405..5ad0d41dc7 100644
--- a/framework/core/js/src/admin/components/ExtensionsWidget.js
+++ b/framework/core/js/src/admin/components/ExtensionsWidget.js
@@ -3,8 +3,8 @@ import DashboardWidget from './DashboardWidget';
import isExtensionEnabled from '../utils/isExtensionEnabled';
import getCategorizedExtensions from '../utils/getCategorizedExtensions';
import Link from '../../common/components/Link';
-import icon from '../../common/helpers/icon';
import classList from '../../common/utils/classList';
+import Icon from '../../common/components/Icon';
export default class ExtensionsWidget extends DashboardWidget {
oninit(vnode) {
@@ -42,7 +42,7 @@ export default class ExtensionsWidget extends DashboardWidget {
- {!!extension.icon && icon(extension.icon.name)}
+ {!!extension.icon && }
{extension.extra['flarum-extension'].title}
diff --git a/framework/core/js/src/admin/components/HeaderSecondary.js b/framework/core/js/src/admin/components/HeaderSecondary.js
index 255815be26..439d7aaaff 100644
--- a/framework/core/js/src/admin/components/HeaderSecondary.js
+++ b/framework/core/js/src/admin/components/HeaderSecondary.js
@@ -23,7 +23,13 @@ export default class HeaderSecondary extends Component {
items.add(
'help',
-
+
{app.translator.trans('core.admin.header.get_help')}
);
diff --git a/framework/core/js/src/admin/components/MailPage.tsx b/framework/core/js/src/admin/components/MailPage.tsx
index c3e40c8820..b0946b20d3 100644
--- a/framework/core/js/src/admin/components/MailPage.tsx
+++ b/framework/core/js/src/admin/components/MailPage.tsx
@@ -8,6 +8,7 @@ import type { IPageAttrs } from '../../common/components/Page';
import type { AlertIdentifier } from '../../common/states/AlertManagerState';
import type Mithril from 'mithril';
import type { SaveSubmitEvent } from './AdminPage';
+import Form from '../../common/components/Form';
import ItemList from '../../common/utils/ItemList';
export interface MailSettings {
@@ -69,17 +70,23 @@ export default class MailPage exten
const mailSettings = this.mailSettingItems().toArray();
return (
-
- {mailSettings.map((settingComponent) => settingComponent)}
- {this.submitButton()}
-
-
- {app.translator.trans('core.admin.email.send_test_mail_text', { email: app.session.user!.email() })}
- this.sendTestEmail()}>
- {app.translator.trans('core.admin.email.send_test_mail_button')}
-
-
-
+ <>
+
+ {mailSettings.map((settingComponent) => settingComponent)}
+ {this.submitButton()}
+
+
+
+ this.sendTestEmail()}>
+ {app.translator.trans('core.admin.email.send_test_mail_button')}
+
+
+
+ >
);
}
@@ -89,6 +96,8 @@ export default class MailPage exten
const fields = this.driverFields![this.setting('mail_driver')()];
const fieldKeys = Object.keys(fields);
+ items.add('status', this.status!.sending || {app.translator.trans('core.admin.email.not_sending_message')} );
+
items.add(
'mail_from',
this.buildSettingComponent({
@@ -117,40 +126,42 @@ export default class MailPage exten
items.add(
'mail_driver',
-
- {this.buildSettingComponent({
- type: 'select',
- setting: 'mail_driver',
- options: Object.keys(this.driverFields!).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
- label: app.translator.trans('core.admin.email.driver_heading'),
- })}
- {this.status!.sending ||
{app.translator.trans('core.admin.email.not_sending_message')} }
-
- {!!fieldKeys.length && (
-
-
- {fieldKeys.map((field) => {
- const fieldInfo = fields[field];
-
- return (
- <>
- {this.buildSettingComponent({
- type: typeof fieldInfo === 'string' ? 'text' : 'select',
- label: app.translator.trans(`core.admin.email.${field}_label`),
- setting: field,
- options: fieldInfo,
- })}
- {this.status!.errors[field] &&
{this.status!.errors[field]}
}
- >
- );
- })}
-
-
- )}
-
,
+ this.buildSettingComponent({
+ type: 'select',
+ setting: 'mail_driver',
+ options: Object.keys(this.driverFields!).reduce((memo, val) => ({ ...memo, [val]: val }), {}),
+ label: app.translator.trans('core.admin.email.driver_heading'),
+ }),
60
);
+ if (!!fieldKeys.length) {
+ items.add(
+ 'mail_driver_settings',
+
+ {fieldKeys.map((field) => {
+ const fieldInfo = fields[field];
+
+ return (
+ <>
+ {this.buildSettingComponent({
+ type: typeof fieldInfo === 'string' ? 'text' : 'select',
+ label: app.translator.trans(`core.admin.email.${field}_label`),
+ setting: field,
+ options: fieldInfo,
+ })}
+ {this.status!.errors[field] && {this.status!.errors[field]}
}
+ >
+ );
+ })}
+ ,
+ 50
+ );
+ }
+
return items;
}
diff --git a/framework/core/js/src/admin/components/PermissionGrid.tsx b/framework/core/js/src/admin/components/PermissionGrid.tsx
index 76ae8d1cb7..57262d08d5 100644
--- a/framework/core/js/src/admin/components/PermissionGrid.tsx
+++ b/framework/core/js/src/admin/components/PermissionGrid.tsx
@@ -4,8 +4,8 @@ import PermissionDropdown from './PermissionDropdown';
import SettingDropdown from './SettingDropdown';
import Button from '../../common/components/Button';
import ItemList from '../../common/utils/ItemList';
-import icon from '../../common/helpers/icon';
import type Mithril from 'mithril';
+import Icon from '../../common/components/Icon';
export interface PermissionConfig {
permission: string;
@@ -76,7 +76,7 @@ export default class PermissionGrid (
- {icon(child.icon)}
+
{child.label}
{permissionCells(child)}
diff --git a/framework/core/js/src/admin/components/PermissionsPage.tsx b/framework/core/js/src/admin/components/PermissionsPage.tsx
index 53b7833d2b..04f965510b 100644
--- a/framework/core/js/src/admin/components/PermissionsPage.tsx
+++ b/framework/core/js/src/admin/components/PermissionsPage.tsx
@@ -2,9 +2,9 @@ import app from '../../admin/app';
import GroupBadge from '../../common/components/GroupBadge';
import EditGroupModal from './EditGroupModal';
import Group from '../../common/models/Group';
-import icon from '../../common/helpers/icon';
import PermissionGrid from './PermissionGrid';
import AdminPage from './AdminPage';
+import Icon from '../../common/components/Icon';
export default class PermissionsPage extends AdminPage {
headerInfo() {
@@ -30,7 +30,7 @@ export default class PermissionsPage extends AdminPage {
))}
app.modal.show(EditGroupModal)}>
- {icon('fas fa-plus', { className: 'Group-icon' })}
+
{app.translator.trans('core.admin.permissions.new_group_button')}
diff --git a/framework/core/js/src/admin/components/SessionDropdown.tsx b/framework/core/js/src/admin/components/SessionDropdown.tsx
index 999e3a456c..c809997983 100644
--- a/framework/core/js/src/admin/components/SessionDropdown.tsx
+++ b/framework/core/js/src/admin/components/SessionDropdown.tsx
@@ -1,10 +1,10 @@
import app from '../../admin/app';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import ItemList from '../../common/utils/ItemList';
import type Mithril from 'mithril';
+import Avatar from '../../common/components/Avatar';
export interface ISessionDropdownAttrs extends IDropdownAttrs {}
@@ -28,7 +28,7 @@ export default class SessionDropdown{username(user)}];
+ return [ , ' ', {username(user)} ];
}
/**
diff --git a/framework/core/js/src/admin/components/SettingsModal.tsx b/framework/core/js/src/admin/components/SettingsModal.tsx
index 4d5e46c3ab..d86617b39e 100644
--- a/framework/core/js/src/admin/components/SettingsModal.tsx
+++ b/framework/core/js/src/admin/components/SettingsModal.tsx
@@ -5,6 +5,7 @@ import Stream from '../../common/utils/Stream';
import saveSettings from '../utils/saveSettings';
import Mithril from 'mithril';
import { MutableSettings, SettingValue } from './AdminPage';
+import Form from '../../common/components/Form';
export interface ISettingsModalAttrs extends IInternalModalAttrs {}
@@ -19,11 +20,11 @@ export default abstract class SettingsModal
-
+
{this.form()}
{this.submitButton()}
-
+
);
}
diff --git a/framework/core/js/src/admin/components/UploadImageButton.js b/framework/core/js/src/admin/components/UploadImageButton.js
deleted file mode 100644
index 1a7a1a7210..0000000000
--- a/framework/core/js/src/admin/components/UploadImageButton.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import app from '../../admin/app';
-import Button from '../../common/components/Button';
-import classList from '../../common/utils/classList';
-
-export default class UploadImageButton extends Button {
- loading = false;
-
- view(vnode) {
- this.attrs.loading = this.loading;
- this.attrs.className = classList(this.attrs.className, 'Button');
-
- if (app.data.settings[this.attrs.name + '_path']) {
- this.attrs.onclick = this.remove.bind(this);
-
- return (
-
-
-
-
-
{super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.remove_button') })}
-
- );
- } else {
- this.attrs.onclick = this.upload.bind(this);
- }
-
- return super.view({ ...vnode, children: app.translator.trans('core.admin.upload_image.upload_button') });
- }
-
- /**
- * Prompt the user to upload an image.
- */
- upload() {
- if (this.loading) return;
-
- const $input = $(' ');
-
- $input
- .appendTo('body')
- .hide()
- .trigger('click')
- .on('change', (e) => {
- const body = new FormData();
- body.append(this.attrs.name, $(e.target)[0].files[0]);
-
- this.loading = true;
- m.redraw();
-
- app
- .request({
- method: 'POST',
- url: this.resourceUrl(),
- serialize: (raw) => raw,
- body,
- })
- .then(this.success.bind(this), this.failure.bind(this));
- });
- }
-
- /**
- * Remove the logo.
- */
- remove() {
- this.loading = true;
- m.redraw();
-
- app
- .request({
- method: 'DELETE',
- url: this.resourceUrl(),
- })
- .then(this.success.bind(this), this.failure.bind(this));
- }
-
- resourceUrl() {
- return app.forum.attribute('apiUrl') + '/' + this.attrs.name;
- }
-
- /**
- * After a successful upload/removal, reload the page.
- *
- * @param {object} response
- * @protected
- */
- success(response) {
- window.location.reload();
- }
-
- /**
- * If upload/removal fails, stop loading.
- *
- * @param {object} response
- * @protected
- */
- failure(response) {
- this.loading = false;
- m.redraw();
- }
-}
diff --git a/framework/core/js/src/admin/components/UploadImageButton.tsx b/framework/core/js/src/admin/components/UploadImageButton.tsx
new file mode 100644
index 0000000000..f1e4ab452e
--- /dev/null
+++ b/framework/core/js/src/admin/components/UploadImageButton.tsx
@@ -0,0 +1,110 @@
+import app from '../../admin/app';
+import Button from '../../common/components/Button';
+import type { IButtonAttrs } from '../../common/components/Button';
+import classList from '../../common/utils/classList';
+import type Mithril from 'mithril';
+import Component from '../../common/Component';
+
+export interface IUploadImageButtonAttrs extends IButtonAttrs {
+ name: string;
+ routePath: string;
+ value?: string | null | (() => string | null);
+ url?: string | null | (() => string | null);
+}
+
+export default class UploadImageButton extends Component {
+ loading = false;
+
+ view(vnode: Mithril.Vnode) {
+ let { name, value, url, ...attrs } = vnode.attrs as IButtonAttrs;
+
+ attrs.loading = this.loading;
+ attrs.className = classList(attrs.className, 'Button');
+
+ if (typeof value === 'function') {
+ value = value();
+ }
+
+ if (typeof url === 'function') {
+ url = url();
+ }
+
+ return (
+
+ {value ? (
+ <>
+
+
+
+
+ {app.translator.trans('core.admin.upload_image.remove_button')}
+
+ >
+ ) : (
+
+ {app.translator.trans('core.admin.upload_image.upload_button')}
+
+ )}
+
+ );
+ }
+
+ upload() {
+ if (this.loading) return;
+
+ const $input = $(' ');
+
+ $input
+ .appendTo('body')
+ .hide()
+ .trigger('click')
+ .on('change', (e) => {
+ const body = new FormData();
+ // @ts-ignore
+ body.append(this.attrs.name, $(e.target)[0].files[0]);
+
+ this.loading = true;
+ m.redraw();
+
+ app
+ .request({
+ method: 'POST',
+ url: this.resourceUrl(),
+ serialize: (raw) => raw,
+ body,
+ })
+ .then(this.success.bind(this), this.failure.bind(this));
+ });
+ }
+
+ remove() {
+ this.loading = true;
+ m.redraw();
+
+ app
+ .request({
+ method: 'DELETE',
+ url: this.resourceUrl(),
+ })
+ .then(this.success.bind(this), this.failure.bind(this));
+ }
+
+ resourceUrl() {
+ return app.forum.attribute('apiUrl') + '/' + this.attrs.routePath;
+ }
+
+ /**
+ * After a successful upload/removal, reload the page.
+ */
+ protected success(response: any) {
+ window.location.reload();
+ }
+
+ /**
+ * If upload/removal fails, stop loading.
+ */
+ protected failure(response: any) {
+ this.loading = false;
+ m.redraw();
+ }
+}
diff --git a/framework/core/js/src/admin/components/UserListPage.tsx b/framework/core/js/src/admin/components/UserListPage.tsx
index 3519165e11..8cd8ff4da7 100644
--- a/framework/core/js/src/admin/components/UserListPage.tsx
+++ b/framework/core/js/src/admin/components/UserListPage.tsx
@@ -5,7 +5,6 @@ import app from '../../admin/app';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Button from '../../common/components/Button';
-import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
import type User from '../../common/models/User';
@@ -17,6 +16,7 @@ import extractText from '../../common/utils/extractText';
import AdminPage from './AdminPage';
import { debounce } from '../../common/utils/throttleDebounce';
import CreateUserModal from './CreateUserModal';
+import Icon from '../../common/components/Icon';
type ColumnData = {
/**
@@ -414,7 +414,7 @@ export default class UserListPage extends AdminPage {
className="Button Button--text UserList-emailIconBtn"
title={app.translator.trans('core.admin.users.grid.columns.email.visibility_show')}
>
- {icon('far fa-eye-slash fa-fw', { className: 'icon' })}
+
);
diff --git a/framework/core/js/src/common/common.ts b/framework/core/js/src/common/common.ts
index 7d7531ed02..4e1d8c4e2f 100644
--- a/framework/core/js/src/common/common.ts
+++ b/framework/core/js/src/common/common.ts
@@ -45,10 +45,12 @@ import './components/AlertManager';
import './components/Page';
import './components/Switch';
import './components/Badge';
+import './components/Icon';
import './components/LoadingIndicator';
import './components/Placeholder';
import './components/Separator';
import './components/Dropdown';
+import './components/DetailedDropdownItem';
import './components/SplitDropdown';
import './components/RequestErrorModal';
import './components/FieldSet';
@@ -64,12 +66,12 @@ import './components/ModalManager';
import './components/Button';
import './components/Modal';
import './components/GroupBadge';
+import './components/TextEditor';
import './components/TextEditorButton';
import './components/Tooltip';
import './helpers/fullTime';
-import './helpers/avatar';
-import './helpers/icon';
+import './components/Avatar';
import './helpers/humanTime';
import './helpers/punctuateSeries';
import './helpers/highlight';
diff --git a/framework/core/js/src/common/components/Alert.tsx b/framework/core/js/src/common/components/Alert.tsx
index 77d12ef4d6..d04c9e8294 100644
--- a/framework/core/js/src/common/components/Alert.tsx
+++ b/framework/core/js/src/common/components/Alert.tsx
@@ -5,7 +5,7 @@ import extract from '../utils/extract';
import type Mithril from 'mithril';
import classList from '../utils/classList';
import app from '../app';
-import iconHelper from '../helpers/icon';
+import Icon from './Icon';
export interface AlertAttrs extends ComponentAttrs {
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
@@ -20,6 +20,8 @@ export interface AlertAttrs extends ComponentAttrs {
dismissible?: boolean;
/** A callback to run when the alert is dismissed */
ondismiss?: Function;
+ /** A class to assign to the container element */
+ containerClassName?: string;
}
/**
@@ -58,14 +60,20 @@ export default class Alert extends Component<
return (
- {!!title && (
-
- {!!icon && {iconHelper(icon)} }
- {title}
-
- )}
-
{content}
-
{listItems(controls.concat(dismissControl))}
+
+ {!!title && (
+
+ {!!icon && (
+
+
+
+ )}
+ {title}
+
+ )}
+
{content}
+
{listItems(controls.concat(dismissControl))}
+
);
}
diff --git a/framework/core/js/src/common/components/Avatar.tsx b/framework/core/js/src/common/components/Avatar.tsx
new file mode 100644
index 0000000000..f47c428ed6
--- /dev/null
+++ b/framework/core/js/src/common/components/Avatar.tsx
@@ -0,0 +1,44 @@
+import type User from '../models/User';
+import type { ComponentAttrs } from '../Component';
+import type Mithril from 'mithril';
+import classList from '../utils/classList';
+import Component from '../Component';
+
+export interface IAvatarAttrs extends ComponentAttrs {
+ user: User | null;
+}
+
+export default class Avatar extends Component {
+ view(vnode: Mithril.Vnode): Mithril.Children {
+ const { user, ...attrs } = vnode.attrs as IAvatarAttrs;
+
+ attrs.className = classList('Avatar', attrs.className);
+ attrs.loading ??= 'lazy';
+ let content: string = '';
+
+ // If the `title` attribute is set to null or false, we don't want to give the
+ // avatar a title. On the other hand, if it hasn't been given at all, we can
+ // safely default it to the user's username.
+ const hasTitle: boolean | string = attrs.title === 'undefined' || attrs.title;
+ if (!hasTitle) delete attrs.title;
+
+ // If a user has been passed, then we will set up an avatar using their
+ // uploaded image, or the first letter of their username if they haven't
+ // uploaded one.
+ if (user) {
+ const username = user.displayName() || '?';
+ const avatarUrl = user.avatarUrl();
+
+ if (hasTitle) attrs.title = attrs.title || username;
+
+ if (avatarUrl) {
+ return ;
+ }
+
+ content = username.charAt(0).toUpperCase();
+ attrs.style = { '--avatar-bg': user.color() };
+ }
+
+ return {content} ;
+ }
+}
diff --git a/framework/core/js/src/common/components/Badge.tsx b/framework/core/js/src/common/components/Badge.tsx
index 6529490d03..fa8844bb3b 100644
--- a/framework/core/js/src/common/components/Badge.tsx
+++ b/framework/core/js/src/common/components/Badge.tsx
@@ -1,8 +1,8 @@
import Tooltip from './Tooltip';
import Component, { ComponentAttrs } from '../Component';
-import icon from '../helpers/icon';
import classList from '../utils/classList';
import textContrastClass from '../helpers/textContrastClass';
+import Icon from './Icon';
export interface IBadgeAttrs extends ComponentAttrs {
icon: string;
@@ -30,7 +30,7 @@ export default class Badge extend
const className = classList('Badge', [type && `Badge--${type}`], attrs.className, textContrastClass(color));
- const iconChild = iconName ? icon(iconName, { className: 'Badge-icon' }) : m.trust(' ');
+ const iconChild = iconName ? : m.trust(' ');
const newStyle = { ...style, '--badge-bg': color };
diff --git a/framework/core/js/src/common/components/Button.tsx b/framework/core/js/src/common/components/Button.tsx
index 86e9ce0017..009e67ef5d 100644
--- a/framework/core/js/src/common/components/Button.tsx
+++ b/framework/core/js/src/common/components/Button.tsx
@@ -1,10 +1,10 @@
import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../Component';
import fireDebugWarning from '../helpers/fireDebugWarning';
-import icon from '../helpers/icon';
import classList from '../utils/classList';
import extractText from '../utils/extractText';
import LoadingIndicator from './LoadingIndicator';
+import Icon from './Icon';
export interface IButtonAttrs extends ComponentAttrs {
/**
@@ -122,7 +122,7 @@ export default class Button ext
const iconName = this.attrs.icon;
return [
- iconName && icon(iconName, { className: 'Button-icon' }),
+ iconName && ,
children && {children} ,
this.attrs.loading && ,
];
diff --git a/framework/core/js/src/common/components/Checkbox.tsx b/framework/core/js/src/common/components/Checkbox.tsx
index 975dc17c31..e67780df7b 100644
--- a/framework/core/js/src/common/components/Checkbox.tsx
+++ b/framework/core/js/src/common/components/Checkbox.tsx
@@ -1,9 +1,9 @@
import Component, { ComponentAttrs } from '../Component';
import LoadingIndicator from './LoadingIndicator';
-import icon from '../helpers/icon';
import classList from '../utils/classList';
import withAttr from '../utils/withAttr';
import type Mithril from 'mithril';
+import Icon from './Icon';
export interface ICheckboxAttrs extends ComponentAttrs {
state?: boolean;
@@ -49,7 +49,11 @@ export default class Checkbox : icon(this.attrs.state ? 'fas fa-check' : 'fas fa-times');
+ return this.attrs.loading ? (
+
+ ) : (
+
+ );
}
/**
diff --git a/framework/core/js/src/common/components/ColorPreviewInput.tsx b/framework/core/js/src/common/components/ColorPreviewInput.tsx
index bd6f8598f2..3b643419e9 100644
--- a/framework/core/js/src/common/components/ColorPreviewInput.tsx
+++ b/framework/core/js/src/common/components/ColorPreviewInput.tsx
@@ -2,7 +2,8 @@ import type Mithril from 'mithril';
import Component, { ComponentAttrs } from '../Component';
import classList from '../utils/classList';
-import icon from '../helpers/icon';
+
+import Icon from './Icon';
export default class ColorPreviewInput extends Component {
view(vnode: Mithril.Vnode) {
@@ -20,7 +21,7 @@ export default class ColorPreviewInput extends Component {
- {icon('fas fa-exclamation-circle')}
+
diff --git a/framework/core/js/src/common/components/DetailedDropdownItem.tsx b/framework/core/js/src/common/components/DetailedDropdownItem.tsx
new file mode 100644
index 0000000000..5663fa877b
--- /dev/null
+++ b/framework/core/js/src/common/components/DetailedDropdownItem.tsx
@@ -0,0 +1,36 @@
+import Component from '../Component';
+import type { ComponentAttrs } from '../Component';
+
+import Icon from './Icon';
+
+export interface IDetailedDropdownItemAttrs extends ComponentAttrs {
+ /** The name of an icon to show in the dropdown item. */
+ icon: string;
+ /** The label of the dropdown item. */
+ label: string;
+ /** The description of the item. */
+ description: string;
+ /** An action to take when the item is clicked. */
+ onclick: () => void;
+ /** Whether the item is the current active/selected option. */
+ active?: boolean;
+}
+
+export default class DetailedDropdownItem<
+ CustomAttrs extends IDetailedDropdownItemAttrs = IDetailedDropdownItemAttrs
+> extends Component {
+ view() {
+ return (
+
+
+
+
+
+ {this.attrs.label}
+ {this.attrs.description}
+
+
+
+ );
+ }
+}
diff --git a/framework/core/js/src/common/components/Dropdown.tsx b/framework/core/js/src/common/components/Dropdown.tsx
index 9592fb2564..31d0a74ab3 100644
--- a/framework/core/js/src/common/components/Dropdown.tsx
+++ b/framework/core/js/src/common/components/Dropdown.tsx
@@ -1,9 +1,10 @@
import app from '../../common/app';
import Component, { ComponentAttrs } from '../Component';
-import icon from '../helpers/icon';
import listItems, { ModdedChildrenWithItemName } from '../helpers/listItems';
import extractText from '../utils/extractText';
import type Mithril from 'mithril';
+import Tooltip from './Tooltip';
+import Icon from './Icon';
export interface IDropdownAttrs extends ComponentAttrs {
/** A class name to apply to the dropdown toggle button. */
@@ -18,6 +19,8 @@ export interface IDropdownAttrs extends ComponentAttrs {
label: Mithril.Children;
/** The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'. */
accessibleToggleLabel?: string;
+ /** An optional tooltip to show when hovering over the dropdown toggle button. */
+ tooltip?: string;
/** An action to take when the dropdown is collapsed. */
onhide?: () => void;
/** An action to take when the dropdown is opened. */
@@ -122,7 +125,7 @@ export default class Dropdown {
- return (
+ let button = (
);
+
+ if (this.attrs.tooltip) {
+ button = (
+
+ {button}
+
+ );
+ }
+
+ return button;
}
/**
@@ -140,9 +153,9 @@ export default class Dropdown : '',
{this.attrs.label} ,
- this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : '',
+ this.attrs.caretIcon ? : '',
];
}
diff --git a/framework/core/js/src/common/components/EditUserModal.tsx b/framework/core/js/src/common/components/EditUserModal.tsx
index c626e882ac..d23e07c92f 100644
--- a/framework/core/js/src/common/components/EditUserModal.tsx
+++ b/framework/core/js/src/common/components/EditUserModal.tsx
@@ -9,6 +9,7 @@ import Stream from '../utils/Stream';
import type Mithril from 'mithril';
import type User from '../models/User';
import type { SaveAttributes, SaveRelationships } from '../Model';
+import Form from './Form';
export interface IEditUserModalAttrs extends IInternalModalAttrs {
user: User;
@@ -53,7 +54,7 @@ export default class EditUserModal
- {fields.length > 1 ? {this.fields().toArray()}
: app.translator.trans('core.lib.edit_user.nothing_available')}
+ {fields.length > 1 ? {this.fields().toArray()} : app.translator.trans('core.lib.edit_user.nothing_available')}
);
}
@@ -169,7 +170,7 @@ export default class EditUserModal
+
{app.translator.trans('core.lib.edit_user.submit_button')}
diff --git a/framework/core/js/src/common/components/FieldSet.js b/framework/core/js/src/common/components/FieldSet.js
deleted file mode 100644
index 9b829fbe06..0000000000
--- a/framework/core/js/src/common/components/FieldSet.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Component from '../Component';
-import listItems from '../helpers/listItems';
-
-/**
- * The `FieldSet` component defines a collection of fields, displayed in a list
- * underneath a title. Accepted properties are:
- *
- * - `className` The class name for the fieldset.
- * - `label` The title of this group of fields.
- *
- * The children should be an array of items to show in the fieldset.
- */
-export default class FieldSet extends Component {
- view(vnode) {
- return (
-
- {this.attrs.label}
- {listItems(vnode.children)}
-
- );
- }
-}
diff --git a/framework/core/js/src/common/components/FieldSet.tsx b/framework/core/js/src/common/components/FieldSet.tsx
new file mode 100644
index 0000000000..41246563a5
--- /dev/null
+++ b/framework/core/js/src/common/components/FieldSet.tsx
@@ -0,0 +1,29 @@
+import Component, { ComponentAttrs } from '../Component';
+import listItems from '../helpers/listItems';
+import classList from '../utils/classList';
+import Mithril from 'mithril';
+
+export interface IFieldSetAttrs extends ComponentAttrs {
+ label: string;
+ description?: string;
+}
+
+/**
+ * The `FieldSet` component defines a collection of fields, displayed in a list
+ * underneath a title.
+ *
+ * The children should be an array of items to show in the fieldset.
+ */
+export default class FieldSet
extends Component {
+ view(vnode: Mithril.Vnode) {
+ return (
+
+
+ {this.attrs.label}
+
+ {this.attrs.description ?
{this.attrs.description}
: null}
+
{vnode.children}
+
+ );
+ }
+}
diff --git a/framework/core/js/src/common/components/Form.tsx b/framework/core/js/src/common/components/Form.tsx
new file mode 100644
index 0000000000..b32f92214c
--- /dev/null
+++ b/framework/core/js/src/common/components/Form.tsx
@@ -0,0 +1,25 @@
+import type { ComponentAttrs } from '../Component';
+import Component from '../Component';
+import type Mithril from 'mithril';
+import classList from '../utils/classList';
+
+export interface IFormAttrs extends ComponentAttrs {
+ label?: string;
+ description?: string;
+}
+
+export default class Form extends Component {
+ view(vnode: Mithril.Vnode) {
+ const { label, description, className, ...attrs } = vnode.attrs;
+
+ return (
+
+
+ {label &&
{label} }
+ {description &&
{description}
}
+
+
{vnode.children}
+
+ );
+ }
+}
diff --git a/framework/core/js/src/common/components/Icon.tsx b/framework/core/js/src/common/components/Icon.tsx
new file mode 100644
index 0000000000..8e19253a41
--- /dev/null
+++ b/framework/core/js/src/common/components/Icon.tsx
@@ -0,0 +1,20 @@
+import Mithril from 'mithril';
+import classList from '../utils/classList';
+import type { ComponentAttrs } from '../Component';
+import Component from '../Component';
+
+export interface IIconAttrs extends ComponentAttrs {
+ /** The full icon class, prefix and the icon’s name. */
+ name: string;
+}
+
+export default class Icon extends Component {
+ view(vnode: Mithril.Vnode): Mithril.Children {
+ const { name, ...attrs } = vnode.attrs;
+
+ // @ts-ignore
+ attrs.className = classList('icon', name, attrs.className);
+
+ return ;
+ }
+}
diff --git a/framework/core/js/src/common/components/LinkButton.js b/framework/core/js/src/common/components/LinkButton.js
index cde7c9d818..d23086fb4e 100644
--- a/framework/core/js/src/common/components/LinkButton.js
+++ b/framework/core/js/src/common/components/LinkButton.js
@@ -1,5 +1,6 @@
import Button from './Button';
import Link from './Link';
+import classList from '../utils/classList';
/**
* The `LinkButton` component defines a `Button` which links to a route.
@@ -19,6 +20,7 @@ export default class LinkButton extends Button {
super.initAttrs(attrs);
attrs.active = this.isActive(attrs);
+ attrs.className = classList('LinkButton', attrs.className);
if (attrs.force === undefined) attrs.force = true;
}
diff --git a/framework/core/js/src/common/components/Select.js b/framework/core/js/src/common/components/Select.js
index 07866012a6..b8a6cc7420 100644
--- a/framework/core/js/src/common/components/Select.js
+++ b/framework/core/js/src/common/components/Select.js
@@ -1,7 +1,7 @@
import Component from '../Component';
-import icon from '../helpers/icon';
import withAttr from '../utils/withAttr';
import classList from '../utils/classList';
+import Icon from './Icon';
/**
* The `Select` component displays a input, surrounded with some extra
@@ -45,7 +45,7 @@ export default class Select extends Component {
{options[key]}
))}
- {icon('fas fa-sort', { className: 'Select-caret' })}
+
);
}
diff --git a/framework/core/js/src/common/components/SelectDropdown.tsx b/framework/core/js/src/common/components/SelectDropdown.tsx
index d38e26d710..b9e9df1d40 100644
--- a/framework/core/js/src/common/components/SelectDropdown.tsx
+++ b/framework/core/js/src/common/components/SelectDropdown.tsx
@@ -1,8 +1,8 @@
import Dropdown, { IDropdownAttrs } from './Dropdown';
-import icon from '../helpers/icon';
import classList from '../utils/classList';
import type Component from '../Component';
import type Mithril from 'mithril';
+import Icon from './Icon';
/**
* Determines via a vnode is currently "active".
@@ -49,6 +49,9 @@ export default class SelectDropdown{label}, this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : null];
+ return [
+ {label} ,
+ this.attrs.caretIcon ? : null,
+ ];
}
}
diff --git a/framework/core/js/src/common/components/SplitDropdown.tsx b/framework/core/js/src/common/components/SplitDropdown.tsx
index a848a25af4..2275c8604e 100644
--- a/framework/core/js/src/common/components/SplitDropdown.tsx
+++ b/framework/core/js/src/common/components/SplitDropdown.tsx
@@ -1,20 +1,25 @@
import Dropdown, { IDropdownAttrs } from './Dropdown';
import Button from './Button';
-import icon from '../helpers/icon';
import Mithril from 'mithril';
import classList from '../utils/classList';
+import Tooltip from './Tooltip';
+import Icon from './Icon';
-export interface ISplitDropdownAttrs extends IDropdownAttrs {}
+export interface ISplitDropdownAttrs extends IDropdownAttrs {
+ /** An optional main control button, which will be displayed instead of the first child. */
+ mainAction?: Mithril.Vnode;
+}
/**
* The `SplitDropdown` component is similar to `Dropdown`, but the first child
- * is displayed as its own button prior to the toggle button.
+ * is displayed as its own button prior to the toggle button. Unless a custom
+ * `mainAction` is provided as the main control.
*/
-export default class SplitDropdown extends Dropdown {
+export default class SplitDropdown extends Dropdown {
static initAttrs(attrs: ISplitDropdownAttrs) {
super.initAttrs(attrs);
- attrs.className = classList(attrs.className, 'Dropdown--split');
+ attrs.className = classList(attrs.className, 'Dropdown--split', { 'Dropdown--withMainAction': attrs.mainAction });
attrs.menuClassName = classList(attrs.menuClassName, 'Dropdown-menu--right');
}
@@ -22,21 +27,31 @@ export default class SplitDropdown extends Dropdown {
// Make a copy of the attrs of the first child component. We will assign
// these attrs to a new button, so that it has exactly the same behaviour as
// the first child.
- const firstChild = this.getFirstChild(children);
+ const firstChild = this.attrs.mainAction || this.getFirstChild(children);
const buttonAttrs = Object.assign({}, firstChild?.attrs);
buttonAttrs.className = classList(buttonAttrs.className, 'SplitDropdown-button Button', this.attrs.buttonClassName);
+ let button = {firstChild.children} ;
+
+ if (this.attrs.tooltip) {
+ button = (
+
+ {button}
+
+ );
+ }
+
return (
<>
- {firstChild.children}
+ {button}
- {this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : null}
- {icon('fas fa-caret-down', { className: 'Button-caret' })}
+ {this.attrs.icon ? : null}
+
>
);
diff --git a/framework/core/js/src/common/helpers/avatar.tsx b/framework/core/js/src/common/helpers/avatar.tsx
deleted file mode 100644
index aab82696d2..0000000000
--- a/framework/core/js/src/common/helpers/avatar.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import type Mithril from 'mithril';
-import type { ComponentAttrs } from '../Component';
-import User from '../models/User';
-import classList from '../utils/classList';
-
-export interface AvatarAttrs extends ComponentAttrs {}
-
-/**
- * The `avatar` helper displays a user's avatar.
- *
- * @param user
- * @param attrs Attributes to apply to the avatar element
- */
-export default function avatar(user: User | null, attrs: ComponentAttrs = {}): Mithril.Vnode {
- attrs.className = classList('Avatar', attrs.className);
- attrs.loading ??= 'lazy';
- let content: string = '';
-
- // If the `title` attribute is set to null or false, we don't want to give the
- // avatar a title. On the other hand, if it hasn't been given at all, we can
- // safely default it to the user's username.
- const hasTitle: boolean | string = attrs.title === 'undefined' || attrs.title;
- if (!hasTitle) delete attrs.title;
-
- // If a user has been passed, then we will set up an avatar using their
- // uploaded image, or the first letter of their username if they haven't
- // uploaded one.
- if (user) {
- const username = user.displayName() || '?';
- const avatarUrl = user.avatarUrl();
-
- if (hasTitle) attrs.title = attrs.title || username;
-
- if (avatarUrl) {
- return ;
- }
-
- content = username.charAt(0).toUpperCase();
- attrs.style = { '--avatar-bg': user.color() };
- }
-
- return {content} ;
-}
diff --git a/framework/core/js/src/common/helpers/icon.tsx b/framework/core/js/src/common/helpers/icon.tsx
deleted file mode 100644
index eb7177ac6a..0000000000
--- a/framework/core/js/src/common/helpers/icon.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import type Mithril from 'mithril';
-import classList from '../utils/classList';
-
-/**
- * The `icon` helper displays an icon.
- *
- * @param fontClass The full icon class, prefix and the icon’s name.
- * @param attrs Any other attributes to apply.
- */
-export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
- attrs.className = classList('icon', fontClass, attrs.className);
-
- return ;
-}
diff --git a/framework/core/js/src/common/helpers/userOnline.tsx b/framework/core/js/src/common/helpers/userOnline.tsx
index 202f7ef4fa..e742b99c16 100644
--- a/framework/core/js/src/common/helpers/userOnline.tsx
+++ b/framework/core/js/src/common/helpers/userOnline.tsx
@@ -1,13 +1,17 @@
import type Mithril from 'mithril';
import User from '../models/User';
-import icon from './icon';
+import Icon from '../components/Icon';
/**
* The `useronline` helper displays a green circle if the user is online.
*/
export default function userOnline(user: User): Mithril.Vnode<{}, {}> | null {
if (user.lastSeenAt() && user.isOnline()) {
- return {icon('fas fa-circle')} ;
+ return (
+
+
+
+ );
}
return null;
diff --git a/framework/core/js/src/forum/ForumApplication.tsx b/framework/core/js/src/forum/ForumApplication.tsx
index e4ce5584f9..c37d0c1395 100644
--- a/framework/core/js/src/forum/ForumApplication.tsx
+++ b/framework/core/js/src/forum/ForumApplication.tsx
@@ -9,7 +9,6 @@ import DiscussionRenamedNotification from './components/DiscussionRenamedNotific
import CommentPost from './components/CommentPost';
import DiscussionRenamedPost from './components/DiscussionRenamedPost';
import routes, { ForumRoutes, makeRouteHelpers } from './routes';
-import alertEmailConfirmation from './utils/alertEmailConfirmation';
import Application, { ApplicationData } from '../common/Application';
import Navigation from '../common/components/Navigation';
import NotificationListState from './states/NotificationListState';
@@ -24,6 +23,8 @@ import type Discussion from '../common/models/Discussion';
import type NotificationModel from '../common/models/Notification';
import type PostModel from '../common/models/Post';
import extractText from '../common/utils/extractText';
+import Notices from './components/Notices';
+import Footer from './components/Footer';
export interface ForumApplicationData extends ApplicationData {}
@@ -117,8 +118,8 @@ export default class ForumApplication extends Application {
m.mount(document.getElementById('header-navigation')!, Navigation);
m.mount(document.getElementById('header-primary')!, HeaderPrimary);
m.mount(document.getElementById('header-secondary')!, HeaderSecondary);
-
- alertEmailConfirmation(this);
+ m.mount(document.getElementById('notices')!, Notices);
+ m.mount(document.getElementById('footer')!, Footer);
// 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.
diff --git a/framework/core/js/src/forum/components/AccessTokensList.tsx b/framework/core/js/src/forum/components/AccessTokensList.tsx
index 09499458a4..5d58250817 100644
--- a/framework/core/js/src/forum/components/AccessTokensList.tsx
+++ b/framework/core/js/src/forum/components/AccessTokensList.tsx
@@ -1,6 +1,5 @@
import app from '../app';
import Component, { ComponentAttrs } from '../../common/Component';
-import icon from '../../common/helpers/icon';
import Button from '../../common/components/Button';
import humanTime from '../../common/helpers/humanTime';
import ItemList from '../../common/utils/ItemList';
@@ -11,6 +10,7 @@ import Tooltip from '../../common/components/Tooltip';
import type Mithril from 'mithril';
import type AccessToken from '../../common/models/AccessToken';
import { NestedStringArray } from '@askvortsov/rich-icu-message-formatter';
+import Icon from '../../common/components/Icon';
export interface IAccessTokensListAttrs extends ComponentAttrs {
tokens: AccessToken[];
@@ -52,7 +52,13 @@ export default class AccessTokensList {
const items = new ItemList();
- items.add('icon', {icon(this.attrs.icon || 'fas fa-key')}
, 50);
+ items.add(
+ 'icon',
+
+
+
,
+ 50
+ );
items.add('info', {this.tokenInfoItems(token).toArray()}
, 40);
diff --git a/framework/core/js/src/forum/components/AvatarEditor.js b/framework/core/js/src/forum/components/AvatarEditor.js
index 7f92bdfff1..fd1da14226 100644
--- a/framework/core/js/src/forum/components/AvatarEditor.js
+++ b/framework/core/js/src/forum/components/AvatarEditor.js
@@ -1,12 +1,12 @@
import app from '../../forum/app';
import Component from '../../common/Component';
-import avatar from '../../common/helpers/avatar';
-import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import classList from '../../common/utils/classList';
import Button from '../../common/components/Button';
import LoadingIndicator from '../../common/components/LoadingIndicator';
+import Icon from '../../common/components/Icon';
+import Avatar from '../../common/components/Avatar';
/**
* The `AvatarEditor` component displays a user's avatar along with a dropdown
@@ -41,7 +41,7 @@ export default class AvatarEditor extends Component {
return (
- {avatar(user, { loading: 'eager' })}
+
) : user.avatarUrl() ? (
- icon('fas fa-pencil-alt')
+
) : (
- icon('fas fa-plus-circle')
+
)}
{listItems(this.controlItems().toArray())}
diff --git a/framework/core/js/src/forum/components/ChangeEmailModal.tsx b/framework/core/js/src/forum/components/ChangeEmailModal.tsx
index 3f45065443..7760163dfd 100644
--- a/framework/core/js/src/forum/components/ChangeEmailModal.tsx
+++ b/framework/core/js/src/forum/components/ChangeEmailModal.tsx
@@ -5,6 +5,7 @@ import Stream from '../../common/utils/Stream';
import type Mithril from 'mithril';
import RequestError from '../../common/utils/RequestError';
import ItemList from '../../common/utils/ItemList';
+import Form from '../../common/components/Form';
/**
* The `ChangeEmailModal` component shows a modal dialog which allows the user
@@ -44,7 +45,7 @@ export default class ChangeEmailModal
- {this.fields().toArray()}
+ {this.fields().toArray()}
);
}
@@ -102,7 +103,7 @@ export default class ChangeEmailModal
+
{app.translator.trans('core.forum.change_email.submit_button')}
diff --git a/framework/core/js/src/forum/components/ChangePasswordModal.tsx b/framework/core/js/src/forum/components/ChangePasswordModal.tsx
index ef6a2fe834..9e92102f11 100644
--- a/framework/core/js/src/forum/components/ChangePasswordModal.tsx
+++ b/framework/core/js/src/forum/components/ChangePasswordModal.tsx
@@ -3,6 +3,7 @@ import Modal, { IInternalModalAttrs } from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Mithril from 'mithril';
import ItemList from '../../common/utils/ItemList';
+import Form from '../../common/components/Form';
/**
* The `ChangePasswordModal` component shows a modal dialog which allows the
@@ -20,7 +21,7 @@ export default class ChangePasswordModal
- {this.fields().toArray()}
+ {this.fields().toArray()}
);
}
@@ -32,7 +33,7 @@ export default class ChangePasswordModal
+
{app.translator.trans('core.forum.change_password.send_button')}
diff --git a/framework/core/js/src/forum/components/CommentPost.js b/framework/core/js/src/forum/components/CommentPost.js
index 1a941670dc..74da778d4e 100644
--- a/framework/core/js/src/forum/components/CommentPost.js
+++ b/framework/core/js/src/forum/components/CommentPost.js
@@ -8,6 +8,9 @@ import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import Button from '../../common/components/Button';
import ComposerPostPreview from './ComposerPostPreview';
+import Link from '../../common/components/Link';
+import UserCard from './UserCard.js';
+import Avatar from '../../common/components/Avatar';
/**
* The `CommentPost` component displays a standard `comment`-typed post. This
@@ -45,10 +48,25 @@ export default class CommentPost extends Post {
);
}
+ avatar() {
+ const user = this.attrs.post.user();
+ const view =
;
+
+ if (user) {
+ return
{view};
+ }
+
+ return view;
+ }
+
content() {
return super.content().concat([
{listItems(this.headerItems().toArray())}
+
+ {!this.attrs.post.isHidden() && this.cardVisible && (
+
+ )}
,
{this.isEditing() ?
: m.trust(this.attrs.post.contentHtml())}
@@ -77,6 +95,7 @@ export default class CommentPost extends Post {
oncreate(vnode) {
super.oncreate(vnode);
+ this.listenForCard();
this.refreshContent();
}
@@ -127,22 +146,7 @@ export default class CommentPost extends Post {
const items = new ItemList();
const post = this.attrs.post;
- items.add(
- 'user',
-
{
- this.cardVisible = true;
- m.redraw();
- }}
- oncardhide={() => {
- this.cardVisible = false;
- m.redraw();
- }}
- />,
- 100
- );
+ items.add('user', , 100);
items.add('meta', );
if (post.isEdited() && !post.isHidden()) {
@@ -160,4 +164,40 @@ export default class CommentPost extends Post {
return items;
}
+
+ listenForCard() {
+ let timeout;
+
+ this.$()
+ .on('mouseover', '.PostUser-name a, .UserCard, .Post-avatar', () => {
+ clearTimeout(timeout);
+ timeout = setTimeout(this.showCard.bind(this), 500);
+ })
+ .on('mouseout', '.PostUser-name a, .UserCard, .Post-avatar', () => {
+ clearTimeout(timeout);
+ timeout = setTimeout(this.hideCard.bind(this), 250);
+ });
+ }
+
+ /**
+ * Show the user card.
+ */
+ showCard() {
+ this.cardVisible = true;
+ m.redraw();
+
+ setTimeout(() => this.$('.UserCard').addClass('in'));
+ }
+
+ /**
+ * Hide the user card.
+ */
+ hideCard() {
+ this.$('.UserCard')
+ .removeClass('in')
+ .one('transitionend webkitTransitionEnd oTransitionEnd', () => {
+ this.cardVisible = false;
+ m.redraw();
+ });
+ }
}
diff --git a/framework/core/js/src/forum/components/ComposerBody.js b/framework/core/js/src/forum/components/ComposerBody.js
index 096cf416bb..337217f1aa 100644
--- a/framework/core/js/src/forum/components/ComposerBody.js
+++ b/framework/core/js/src/forum/components/ComposerBody.js
@@ -2,10 +2,10 @@ import Component from '../../common/Component';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import ConfirmDocumentUnload from '../../common/components/ConfirmDocumentUnload';
import TextEditor from '../../common/components/TextEditor';
-import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import classList from '../../common/utils/classList';
+import Avatar from '../../common/components/Avatar';
/**
* The `ComposerBody` component handles the body, or the content, of the
@@ -51,7 +51,7 @@ export default class ComposerBody extends Component {
return (
- {avatar(this.attrs.user, { className: 'ComposerBody-avatar' })}
+
{listItems(this.headerItems().toArray())}
diff --git a/framework/core/js/src/forum/components/DiscussionListItem.tsx b/framework/core/js/src/forum/components/DiscussionListItem.tsx
index 27e8e996d9..fd5decba8e 100644
--- a/framework/core/js/src/forum/components/DiscussionListItem.tsx
+++ b/framework/core/js/src/forum/components/DiscussionListItem.tsx
@@ -1,10 +1,8 @@
import app from '../../forum/app';
import Component, { ComponentAttrs } from '../../common/Component';
import Link from '../../common/components/Link';
-import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import highlight from '../../common/helpers/highlight';
-import icon from '../../common/helpers/icon';
import humanTime from '../../common/utils/humanTime';
import ItemList from '../../common/utils/ItemList';
import abbreviateNumber from '../../common/utils/abbreviateNumber';
@@ -20,6 +18,8 @@ import Tooltip from '../../common/components/Tooltip';
import type Discussion from '../../common/models/Discussion';
import type Mithril from 'mithril';
import type { DiscussionListParams } from '../states/DiscussionListState';
+import Icon from '../../common/components/Icon';
+import Avatar from '../../common/components/Avatar';
export interface IDiscussionListItemAttrs extends ComponentAttrs {
discussion: Discussion;
@@ -63,32 +63,36 @@ export default class DiscussionListItem
- {this.controlsView(controls)}
- {this.slidableUnderneathView()}
- {this.contentView()}
-
- );
+ return
{this.viewItems().toArray()}
;
+ }
+
+ viewItems(): ItemList
{
+ const items = new ItemList();
+
+ const controls = DiscussionControls.controls(this.attrs.discussion, this).toArray();
+
+ if (controls.length) {
+ items.add('controls', this.controlsView(controls), 100);
+ }
+
+ items.add('slidableUnderneath', this.slidableUnderneathView(), 90);
+ items.add('content', this.contentView(), 80);
+
+ return items;
}
controlsView(controls: Mithril.ChildArray): Mithril.Children {
return (
- !!controls.length && (
-
- {controls}
-
- )
+
+ {controls}
+
);
}
@@ -101,7 +105,7 @@ export default class DiscussionListItem
- {icon('fas fa-check')}
+
);
}
@@ -113,28 +117,47 @@ export default class DiscussionListItem
- {this.authorAvatarView()}
- {this.badgesView()}
- {this.mainView()}
- {this.replyCountItem()}
+ {this.contentItems().toArray()}
);
}
- authorAvatarView(): Mithril.Children {
+ contentItems(): ItemList
{
+ const items = new ItemList();
+
+ items.add('author', this.authorView(), 100);
+ items.add('main', this.mainView(), 80);
+ items.add('stats', this.statsView(), 70);
+
+ return items;
+ }
+
+ authorView(): Mithril.Children {
+ return {this.authorItems().toArray()}
;
+ }
+
+ authorItems(): ItemList {
+ const items = new ItemList();
+
const discussion = this.attrs.discussion;
const user = discussion.user();
- return (
+ items.add(
+ 'avatar',
-
- {avatar(user || null, { title: '' })}
+
+
-
+ ,
+ 100
);
+
+ items.add('badges', this.badgesView(), 90);
+
+ return items;
}
badgesView(): Mithril.Children {
@@ -252,30 +275,54 @@ export default class DiscussionListItem{this.statsItems().toArray()} ;
+ }
+
+ statsItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('replyCount', this.replyCountItem(), 70);
+
+ return items;
+ }
+
replyCountItem() {
const discussion = this.attrs.discussion;
const showUnread = !this.showRepliesCount() && discussion.isUnread();
+ const a11yKey = showUnread ? 'core.forum.discussion_list.unread_replies_a11y_label' : 'core.forum.discussion_list.total_replies_a11y_label';
- if (showUnread) {
- return (
-
- {abbreviateNumber(discussion.unreadCount())}
+ return (
+ , ] : }
+ label={showUnread ? abbreviateNumber(discussion.unreadCount()) : abbreviateNumber(discussion.replyCount())}
+ a11yLabel={app.translator.trans(a11yKey, { count: discussion.replyCount() })}
+ onclick={showUnread ? this.markAsRead.bind(this) : undefined}
+ />
+ );
+ }
+}
-
- {app.translator.trans('core.forum.discussion_list.unread_replies_a11y_label', { count: discussion.replyCount() })}
-
-
- );
- }
+export interface DiscussionListItemStatsItemAttrs extends ComponentAttrs {
+ icon: string;
+ label: string;
+ a11yLabel?: string;
+}
- return (
-
- {abbreviateNumber(discussion.replyCount())}
+export class DiscussionListItemStatsItem extends Component {
+ view(vnode: Mithril.Vnode) {
+ const { icon, label, a11yLabel, className, ...attrs } = vnode.attrs;
+ const Tag = attrs.onclick ? 'button' : 'span';
-
- {app.translator.trans('core.forum.discussion_list.total_replies_a11y_label', { count: discussion.replyCount() })}
+ return (
+
+ {icon}
+
+ {label}
+ {a11yLabel ?? label}
-
+
);
}
}
diff --git a/framework/core/js/src/forum/components/DiscussionListPane.js b/framework/core/js/src/forum/components/DiscussionListPane.js
index f48d0ad2a4..a79d2ca6db 100644
--- a/framework/core/js/src/forum/components/DiscussionListPane.js
+++ b/framework/core/js/src/forum/components/DiscussionListPane.js
@@ -22,7 +22,7 @@ export default class DiscussionListPane extends Component {
return;
}
- return ;
+ return ;
}
oncreate(vnode) {
diff --git a/framework/core/js/src/forum/components/DiscussionPage.tsx b/framework/core/js/src/forum/components/DiscussionPage.tsx
index 73685448dc..343f56e7bd 100644
--- a/framework/core/js/src/forum/components/DiscussionPage.tsx
+++ b/framework/core/js/src/forum/components/DiscussionPage.tsx
@@ -13,6 +13,7 @@ import PostStreamState from '../states/PostStreamState';
import Discussion from '../../common/models/Discussion';
import Post from '../../common/models/Post';
import { ApiResponseSingle } from '../../common/Store';
+import PageStructure from './PageStructure';
export interface IDiscussionPageAttrs extends IPageAttrs {
id: string;
@@ -84,24 +85,22 @@ export default class DiscussionPage
-
- {!this.loading ? this.pageContent().toArray() : this.loadingItems().toArray()}
-
+
}
+ >
+ {this.loading || (
+
+
+
+ )}
+
);
}
- /**
- * List of components shown while the discussion is loading.
- */
- loadingItems(): ItemList
{
- const items = new ItemList();
-
- items.add('spinner', , 100);
-
- return items;
- }
-
/**
* Function that renders the `sidebarItems` ItemList.
*/
@@ -120,37 +119,6 @@ export default class DiscussionPage ;
}
- /**
- * List of items rendered as the main page content.
- */
- pageContent(): ItemList {
- const items = new ItemList();
-
- items.add('hero', this.hero(), 100);
- items.add('main', {this.mainContent().toArray()}
, 10);
-
- return items;
- }
-
- /**
- * List of items rendered inside the main page content container.
- */
- mainContent(): ItemList {
- const items = new ItemList();
-
- items.add('sidebar', this.sidebar(), 100);
-
- items.add(
- 'poststream',
-
-
-
,
- 10
- );
-
- return items;
- }
-
/**
* Load the discussion from the API or use the preloaded one.
*/
diff --git a/framework/core/js/src/forum/components/EditPostComposer.js b/framework/core/js/src/forum/components/EditPostComposer.js
index 878a366282..25f3bf9f1d 100644
--- a/framework/core/js/src/forum/components/EditPostComposer.js
+++ b/framework/core/js/src/forum/components/EditPostComposer.js
@@ -2,7 +2,8 @@ import app from '../../forum/app';
import ComposerBody from './ComposerBody';
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
-import icon from '../../common/helpers/icon';
+
+import Icon from '../../common/components/Icon';
function minimizeComposerIfFullScreen(e) {
if (app.composer.isFullScreen()) {
@@ -40,7 +41,7 @@ export default class EditPostComposer extends ComposerBody {
items.add(
'title',
- {icon('fas fa-pencil-alt')}{' '}
+ {' '}
{app.translator.trans('core.forum.composer_edit.post_link', { number: post.number(), discussion: post.discussion().title() })}
diff --git a/framework/core/js/src/forum/components/EventPost.js b/framework/core/js/src/forum/components/EventPost.js
index 90d1e9c6a1..95aea07e28 100644
--- a/framework/core/js/src/forum/components/EventPost.js
+++ b/framework/core/js/src/forum/components/EventPost.js
@@ -2,10 +2,10 @@ import app from '../../forum/app';
import Post from './Post';
import { ucfirst } from '../../common/utils/string';
import usernameHelper from '../../common/helpers/username';
-import icon from '../../common/helpers/icon';
import Link from '../../common/components/Link';
import humanTime from '../../common/helpers/humanTime';
import classList from '../../common/utils/classList';
+import Icon from '../../common/components/Icon';
/**
* The `EventPost` component displays a post which indicating a discussion
@@ -27,6 +27,10 @@ export default class EventPost extends Post {
return attrs;
}
+ avatar() {
+ return ;
+ }
+
content() {
const user = this.attrs.post.user();
const username = usernameHelper(user);
@@ -42,9 +46,7 @@ export default class EventPost extends Post {
time: humanTime(this.attrs.post.createdAt()),
});
- return super
- .content()
- .concat([icon(this.icon(), { className: 'EventPost-icon' }), {this.description(data)}
]);
+ return super.content().concat([{this.description(data)}
]);
}
/**
diff --git a/framework/core/js/src/forum/components/Footer.tsx b/framework/core/js/src/forum/components/Footer.tsx
new file mode 100644
index 0000000000..1758033ceb
--- /dev/null
+++ b/framework/core/js/src/forum/components/Footer.tsx
@@ -0,0 +1,7 @@
+import Component from '../../common/Component';
+
+export default class Footer extends Component {
+ view() {
+ return null;
+ }
+}
diff --git a/framework/core/js/src/forum/components/ForgotPasswordModal.tsx b/framework/core/js/src/forum/components/ForgotPasswordModal.tsx
index 83bab6dfc9..9475273538 100644
--- a/framework/core/js/src/forum/components/ForgotPasswordModal.tsx
+++ b/framework/core/js/src/forum/components/ForgotPasswordModal.tsx
@@ -6,6 +6,7 @@ import Stream from '../../common/utils/Stream';
import Mithril from 'mithril';
import RequestError from '../../common/utils/RequestError';
import ItemList from '../../common/utils/ItemList';
+import Form from '../../common/components/Form';
export interface IForgotPasswordModalAttrs extends IInternalModalAttrs {
email?: string;
@@ -41,24 +42,23 @@ export default class ForgotPasswordModal
-
+
{app.translator.trans('core.forum.forgot_password.email_sent_message')}
-
+
{app.translator.trans('core.forum.forgot_password.dismiss_button')}
-
+
);
}
return (
-
-
{app.translator.trans('core.forum.forgot_password.text')}
+
{this.fields().toArray()}
-
+
);
}
@@ -86,7 +86,7 @@ export default class ForgotPasswordModal
+
{app.translator.trans('core.forum.forgot_password.submit_button')}
diff --git a/framework/core/js/src/forum/components/HeaderDropdown.tsx b/framework/core/js/src/forum/components/HeaderDropdown.tsx
new file mode 100644
index 0000000000..2a4f773360
--- /dev/null
+++ b/framework/core/js/src/forum/components/HeaderDropdown.tsx
@@ -0,0 +1,77 @@
+import app from '../app';
+import Dropdown from '../../common/components/Dropdown';
+import type { IDropdownAttrs } from '../../common/components/Dropdown';
+import extractText from '../../common/utils/extractText';
+import type Mithril from 'mithril';
+import classList from '../../common/utils/classList';
+
+import Icon from '../../common/components/Icon';
+
+export interface IHeaderDropdownAttrs extends IDropdownAttrs {
+ state: any;
+}
+
+export default abstract class HeaderDropdown
extends Dropdown {
+ static initAttrs(attrs: IHeaderDropdownAttrs) {
+ attrs.className = classList('HeaderDropdown', attrs.className);
+ attrs.buttonClassName ||= 'Button Button--flat';
+ attrs.menuClassName ||= 'Dropdown-menu--right';
+ attrs.label ||= extractText(app.translator.trans('core.forum.notifications.tooltip'));
+ // attrs.icon ||= 'fas fa-bell';
+ //
+ // // For best a11y support, both `title` and `aria-label` should be used
+ // attrs.accessibleToggleLabel ||= extractText(app.translator.trans('core.forum.notifications.toggle_dropdown_accessible_label'));
+
+ super.initAttrs(attrs);
+ }
+
+ getButton(children: Mithril.ChildArray): Mithril.Vnode {
+ const newCount = this.getNewCount();
+
+ const vdom = super.getButton(children);
+
+ vdom.attrs.title = this.attrs.label;
+
+ vdom.attrs.className = classList(vdom.attrs.className, [newCount && 'new']);
+ vdom.attrs.onclick = this.onclick.bind(this);
+
+ return vdom;
+ }
+
+ getButtonContent(): Mithril.ChildArray {
+ const unread = this.getUnreadCount();
+
+ return [
+ this.attrs.icon ? : null,
+ unread !== 0 && {unread} ,
+ {this.attrs.label} ,
+ ];
+ }
+
+ getMenu() {
+ return (
+
+ {this.showing && this.getContent()}
+
+ );
+ }
+
+ menuClick(e: MouseEvent) {
+ // 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.button === 1) e.stopPropagation();
+ }
+
+ onclick() {
+ if (app.drawer.isOpen()) {
+ this.goToRoute();
+ } else {
+ this.attrs.state?.load();
+ }
+ }
+
+ abstract getNewCount(): number;
+ abstract getUnreadCount(): number;
+ abstract getContent(): Mithril.Children;
+ abstract goToRoute(): void;
+}
diff --git a/framework/core/js/src/forum/components/HeaderList.tsx b/framework/core/js/src/forum/components/HeaderList.tsx
new file mode 100644
index 0000000000..3f41f10eba
--- /dev/null
+++ b/framework/core/js/src/forum/components/HeaderList.tsx
@@ -0,0 +1,87 @@
+import type { ComponentAttrs } from '../../common/Component';
+import Component from '../../common/Component';
+import type ItemList from '../../common/utils/ItemList';
+import type Mithril from 'mithril';
+import classList from '../../common/utils/classList';
+import LoadingIndicator from '../../common/components/LoadingIndicator';
+
+export interface IHeaderListAttrs extends ComponentAttrs {
+ title: string;
+ controls?: ItemList;
+ hasItems: boolean;
+ loading?: boolean;
+ emptyText: string;
+ loadMore?: () => void;
+}
+
+export default class HeaderList extends Component {
+ $content: JQuery | null = null;
+ $scrollParent: JQuery | null = null;
+ boundScrollHandler: (() => void) | null = null;
+
+ view(vnode: Mithril.Vnode) {
+ const { title, controls, hasItems, loading = false, emptyText, className, ...attrs } = vnode.attrs;
+
+ return (
+
+
+
{title}
+
{controls?.toArray()}
+
+
+ {loading ? (
+
+ ) : hasItems ? (
+ vnode.children
+ ) : (
+
{emptyText}
+ )}
+
+
+ );
+ }
+
+ oncreate(vnode: Mithril.VnodeDOM) {
+ super.oncreate(vnode);
+
+ if (this.attrs.loadMore) {
+ this.$content = this.$('.HeaderList-content');
+
+ // If we are on the notifications page, the window will be scrolling and not the $notifications element.
+ this.$scrollParent = this.inPanel() ? this.$content : $(window);
+
+ this.boundScrollHandler = this.scrollHandler.bind(this);
+ this.$scrollParent.on('scroll', this.boundScrollHandler);
+ }
+ }
+
+ onremove(vnode: Mithril.VnodeDOM) {
+ super.onremove(vnode);
+
+ if (this.attrs.loadMore) {
+ this.$scrollParent!.off('scroll', this.boundScrollHandler!);
+ }
+ }
+
+ scrollHandler() {
+ // Whole-page scroll events are listened to on `window`, but we need to get the actual
+ // scrollHeight, scrollTop, and clientHeight from the document element.
+ const scrollParent = this.inPanel() ? this.$scrollParent![0] : document.documentElement;
+
+ // On very short screens, the scrollHeight + scrollTop might not reach the clientHeight
+ // by a fraction of a pixel, so we compensate for that.
+ const atBottom = Math.abs(scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight) <= 1;
+
+ if (atBottom) {
+ this.attrs.loadMore?.();
+ }
+ }
+
+ /**
+ * If the NotificationList component isn't in a panel (e.g. on NotificationPage when mobile),
+ * we need to listen to scroll events on the window, and get scroll state from the body.
+ */
+ inPanel() {
+ return this.$content!.css('overflow') === 'auto';
+ }
+}
diff --git a/framework/core/js/src/forum/components/HeaderListGroup.tsx b/framework/core/js/src/forum/components/HeaderListGroup.tsx
new file mode 100644
index 0000000000..6606e8537b
--- /dev/null
+++ b/framework/core/js/src/forum/components/HeaderListGroup.tsx
@@ -0,0 +1,19 @@
+import type { ComponentAttrs } from '../../common/Component';
+import Component from '../../common/Component';
+import type Mithril from 'mithril';
+import listItems from '../../common/helpers/listItems';
+
+export interface IHeaderListGroupAttrs extends ComponentAttrs {
+ label: Mithril.Children;
+}
+
+export default class HeaderListGroup extends Component {
+ view(vnode: Mithril.Vnode) {
+ return (
+
+
{vnode.attrs.label}
+
{listItems(vnode.children as any[])}
+
+ );
+ }
+}
diff --git a/framework/core/js/src/forum/components/HeaderListItem.tsx b/framework/core/js/src/forum/components/HeaderListItem.tsx
new file mode 100644
index 0000000000..d2ec789b19
--- /dev/null
+++ b/framework/core/js/src/forum/components/HeaderListItem.tsx
@@ -0,0 +1,39 @@
+import type { ComponentAttrs } from '../../common/Component';
+import type Mithril from 'mithril';
+import Component from '../../common/Component';
+import classList from '../../common/utils/classList';
+import Link from '../../common/components/Link';
+import humanTime from '../../common/helpers/humanTime';
+
+import Icon from '../../common/components/Icon';
+
+export interface IHeaderListItemAttrs extends ComponentAttrs {
+ avatar: Mithril.Children;
+ icon: string;
+ content: string;
+ excerpt: string;
+ datetime?: Date;
+ href: string;
+ onclick?: (e: Event) => void;
+ actions?: Mithril.Children;
+}
+
+export default class HeaderListItem extends Component {
+ view(vnode: Mithril.Vnode) {
+ const { avatar, icon: iconName, content, excerpt, datetime, href, className, onclick, actions, ...attrs } = vnode.attrs;
+
+ return (
+
+ {avatar}
+
+
+ {content}
+
+ {datetime && humanTime(datetime)}
+
+ {actions}
+ {excerpt}
+
+ );
+ }
+}
diff --git a/framework/core/js/src/forum/components/IndexPage.tsx b/framework/core/js/src/forum/components/IndexPage.tsx
index 87829d281f..f236d502ea 100644
--- a/framework/core/js/src/forum/components/IndexPage.tsx
+++ b/framework/core/js/src/forum/components/IndexPage.tsx
@@ -7,11 +7,11 @@ import WelcomeHero from './WelcomeHero';
import DiscussionPage from './DiscussionPage';
import Dropdown from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
-import LinkButton from '../../common/components/LinkButton';
-import SelectDropdown from '../../common/components/SelectDropdown';
import extractText from '../../common/utils/extractText';
import type Mithril from 'mithril';
import type Discussion from '../../common/models/Discussion';
+import PageStructure from './PageStructure';
+import IndexSidebar from './IndexSidebar';
export interface IIndexPageAttrs extends IPageAttrs {}
@@ -51,23 +51,13 @@ export default class IndexPage
- {this.hero()}
-
-
-
- {listItems(this.sidebarItems().toArray())}
-
-
-
-
{listItems(this.viewItems().toArray())}
-
{listItems(this.actionItems().toArray())}
-
-
-
-
+
+
+
{listItems(this.viewItems().toArray())}
+
{listItems(this.actionItems().toArray())}
-
+
+
);
}
@@ -142,63 +132,8 @@ export default class IndexPage ;
}
- /**
- * Build an item list for the sidebar of the index page. By default this is a
- * "New Discussion" button, and then a DropdownSelect component containing a
- * list of navigation items.
- */
- sidebarItems() {
- const items = new ItemList();
- const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
-
- items.add(
- 'newDiscussion',
- {
- // If the user is not logged in, the promise rejects, and a login modal shows up.
- // Since that's already handled, we dont need to show an error message in the console.
- return this.newDiscussionAction().catch(() => {});
- }}
- disabled={!canStartDiscussion}
- >
- {app.translator.trans(`core.forum.index.${canStartDiscussion ? 'start_discussion_button' : 'cannot_start_discussion_button'}`)}
-
- );
-
- items.add(
- 'nav',
-
- {this.navItems().toArray()}
-
- );
-
- return items;
- }
-
- /**
- * Build an item list for the navigation in the sidebar of the index page. By
- * default this is just the 'All Discussions' link.
- */
- navItems() {
- const items = new ItemList();
- const params = app.search.stickyParams();
-
- items.add(
- 'allDiscussions',
-
- {app.translator.trans('core.forum.index.all_discussions_link')}
- ,
- 100
- );
-
- return items;
+ sidebar() {
+ return ;
}
/**
@@ -276,23 +211,6 @@ export default class IndexPage {
- return new Promise((resolve, reject) => {
- if (app.session.user) {
- app.composer.load(() => import('./DiscussionComposer'), { user: app.session.user }).then(() => app.composer.show());
-
- return resolve(app.composer);
- } else {
- app.modal.show(() => import('./LogInModal'));
-
- return reject();
- }
- });
- }
-
/**
* Mark all discussions as read.
*/
diff --git a/framework/core/js/src/forum/components/IndexSidebar.tsx b/framework/core/js/src/forum/components/IndexSidebar.tsx
new file mode 100644
index 0000000000..242ad9fd7e
--- /dev/null
+++ b/framework/core/js/src/forum/components/IndexSidebar.tsx
@@ -0,0 +1,97 @@
+import Component from '../../common/Component';
+import type { ComponentAttrs } from '../../common/Component';
+import type Mithril from 'mithril';
+import ItemList from '../../common/utils/ItemList';
+import app from '../app';
+import Button from '../../common/components/Button';
+import SelectDropdown from '../../common/components/SelectDropdown';
+import listItems from '../../common/helpers/listItems';
+import LinkButton from '../../common/components/LinkButton';
+
+export interface IndexSidebarAttrs extends ComponentAttrs {}
+
+export default class IndexSidebar extends Component {
+ view(vnode: Mithril.Vnode): Mithril.Children {
+ return (
+
+ {listItems(this.items().toArray())}
+
+ );
+ }
+
+ /**
+ * Build an item list for the sidebar of the index page. By default this is a
+ * "New Discussion" button, and then a DropdownSelect component containing a
+ * list of navigation items.
+ */
+ items() {
+ const items = new ItemList();
+ const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
+
+ items.add(
+ 'newDiscussion',
+ {
+ // If the user is not logged in, the promise rejects, and a login modal shows up.
+ // Since that's already handled, we dont need to show an error message in the console.
+ return this.newDiscussionAction().catch(() => {});
+ }}
+ disabled={!canStartDiscussion}
+ >
+ {app.translator.trans(`core.forum.index.${canStartDiscussion ? 'start_discussion_button' : 'cannot_start_discussion_button'}`)}
+
+ );
+
+ items.add(
+ 'nav',
+
+ {this.navItems().toArray()}
+
+ );
+
+ return items;
+ }
+
+ /**
+ * Build an item list for the navigation in the sidebar of the index page. By
+ * default this is just the 'All Discussions' link.
+ */
+ navItems() {
+ const items = new ItemList();
+ const params = app.search.stickyParams();
+
+ items.add(
+ 'allDiscussions',
+
+ {app.translator.trans('core.forum.index.all_discussions_link')}
+ ,
+ 100
+ );
+
+ return items;
+ }
+
+ /**
+ * Open the composer for a new discussion or prompt the user to login.
+ */
+ newDiscussionAction(): Promise {
+ return new Promise((resolve, reject) => {
+ if (app.session.user) {
+ app.composer.load(() => import('./DiscussionComposer'), { user: app.session.user }).then(() => app.composer.show());
+
+ return resolve(app.composer);
+ } else {
+ app.modal.show(() => import('./LogInModal'));
+
+ return reject();
+ }
+ });
+ }
+}
diff --git a/framework/core/js/src/forum/components/LoadingPost.js b/framework/core/js/src/forum/components/LoadingPost.js
index c902ec883e..9e127e4155 100644
--- a/framework/core/js/src/forum/components/LoadingPost.js
+++ b/framework/core/js/src/forum/components/LoadingPost.js
@@ -1,5 +1,6 @@
import Component from '../../common/Component';
-import avatar from '../../common/helpers/avatar';
+
+import Avatar from '../../common/components/Avatar';
/**
* The `LoadingPost` component shows a placeholder that looks like a post,
@@ -10,7 +11,7 @@ export default class LoadingPost extends Component {
return (
- {avatar(null, { className: 'PostUser-avatar' })}
+
diff --git a/framework/core/js/src/forum/components/NewAccessTokenModal.tsx b/framework/core/js/src/forum/components/NewAccessTokenModal.tsx
index 2734645e7e..3f54cd98de 100644
--- a/framework/core/js/src/forum/components/NewAccessTokenModal.tsx
+++ b/framework/core/js/src/forum/components/NewAccessTokenModal.tsx
@@ -5,6 +5,7 @@ import Stream from '../../common/utils/Stream';
import type AccessToken from '../../common/models/AccessToken';
import type { SaveAttributes } from '../../common/Model';
import type Mithril from 'mithril';
+import Form from '../../common/components/Form';
export interface INewAccessTokenModalAttrs extends IInternalModalAttrs {
onsuccess: (token: AccessToken) => void;
@@ -26,16 +27,16 @@ export default class NewAccessTokenModal
-
+
-
+
{app.translator.trans('core.forum.security.new_access_token_modal.submit_button')}
-
+
);
}
diff --git a/framework/core/js/src/forum/components/Notices.tsx b/framework/core/js/src/forum/components/Notices.tsx
new file mode 100644
index 0000000000..80c7efe8fe
--- /dev/null
+++ b/framework/core/js/src/forum/components/Notices.tsx
@@ -0,0 +1,67 @@
+import app from '../app';
+import Component from '../../common/Component';
+import type Mithril from 'mithril';
+import ItemList from '../../common/utils/ItemList';
+import Alert from '../../common/components/Alert';
+import Button from '../../common/components/Button';
+import Icon from '../../common/components/Icon';
+
+export default class Notices extends Component {
+ private loading: boolean = false;
+ private sent: boolean = false;
+
+ view(): Mithril.Children {
+ return {this.items().toArray()}
;
+ }
+
+ items(): ItemList {
+ const items = new ItemList();
+
+ const user = app.session.user;
+
+ if (user && !user.isEmailConfirmed()) {
+ items.add(
+ 'emailConfirmation',
+
+ {this.sent
+ ? [ , ' ', app.translator.trans('core.forum.user_email_confirmation.sent_message')]
+ : app.translator.trans('core.forum.user_email_confirmation.resend_button')}
+ ,
+ ]}
+ className="Alert--emailConfirmation"
+ containerClassName="container"
+ >
+ {app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: {user.email()} })}
+ ,
+ 100
+ );
+ }
+
+ return items;
+ }
+
+ onclickEmailConfirmation() {
+ const user = app.session.user!;
+
+ this.loading = true;
+ m.redraw();
+
+ app
+ .request({
+ method: 'POST',
+ url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/send-confirmation',
+ })
+ .then(() => {
+ this.loading = false;
+ this.sent = true;
+ m.redraw();
+ })
+ .catch(() => {
+ this.loading = false;
+ m.redraw();
+ });
+ }
+}
diff --git a/framework/core/js/src/forum/components/Notification.tsx b/framework/core/js/src/forum/components/Notification.tsx
index c91ff8df50..a2a3ac8615 100644
--- a/framework/core/js/src/forum/components/Notification.tsx
+++ b/framework/core/js/src/forum/components/Notification.tsx
@@ -1,20 +1,17 @@
import app from '../../forum/app';
import type NotificationModel from '../../common/models/Notification';
import Component, { ComponentAttrs } from '../../common/Component';
-import avatar from '../../common/helpers/avatar';
-import icon from '../../common/helpers/icon';
-import humanTime from '../../common/helpers/humanTime';
import Button from '../../common/components/Button';
-import Link from '../../common/components/Link';
import classList from '../../common/utils/classList';
import type Mithril from 'mithril';
+import HeaderListItem from './HeaderListItem';
+import ItemList from '../../common/utils/ItemList';
+import Avatar from '../../common/components/Avatar';
export interface INotificationAttrs extends ComponentAttrs {
notification: NotificationModel;
}
-// TODO [Flarum 2.0]: Remove `?.` from abstract function calls.
-
/**
* The `Notification` component abstract displays a single notification.
* Subclasses should implement the `icon`, `href`, and `content` methods.
@@ -22,42 +19,48 @@ export interface INotificationAttrs extends ComponentAttrs {
export default abstract class Notification extends Component {
view(vnode: Mithril.Vnode) {
const notification = this.attrs.notification;
- const href = this.href?.() ?? '';
-
+ const href = this.href() ?? '';
const fromUser = notification.fromUser();
return (
- }
+ icon={this.icon()}
+ content={this.content()}
+ excerpt={this.excerpt()}
+ datetime={notification.createdAt()}
href={href}
- external={href.includes('://')}
onclick={this.markAsRead.bind(this)}
- >
- {avatar(fromUser || null)}
- {icon(this.icon?.(), { className: 'Notification-icon' })}
-
- {this.content?.()}
-
- {humanTime(notification.createdAt())}
-
- {!notification.isRead() && (
- {
- e.preventDefault();
- e.stopPropagation();
-
- this.markAsRead();
- }}
- />
- )}
- {this.excerpt?.()}
-
+ actions={this.actionItems().toArray()}
+ />
);
}
+ actionItems(): ItemList {
+ const items = new ItemList();
+
+ if (!this.attrs.notification.isRead()) {
+ items.add(
+ 'markAsRead',
+ {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.markAsRead();
+ }}
+ />,
+ 100
+ );
+ }
+
+ return items;
+ }
+
/**
* Get the name of the icon that should be displayed in the notification.
*/
diff --git a/framework/core/js/src/forum/components/NotificationGrid.js b/framework/core/js/src/forum/components/NotificationGrid.js
index 95ea13eaa2..81a22ebfbe 100644
--- a/framework/core/js/src/forum/components/NotificationGrid.js
+++ b/framework/core/js/src/forum/components/NotificationGrid.js
@@ -1,8 +1,8 @@
import app from '../../forum/app';
import Component from '../../common/Component';
import Checkbox from '../../common/components/Checkbox';
-import icon from '../../common/helpers/icon';
import ItemList from '../../common/utils/ItemList';
+import Icon from '../../common/components/Icon';
/**
* The `NotificationGrid` component displays a table of notification types and
@@ -48,7 +48,7 @@ export default class NotificationGrid extends Component {
{this.methods.map((method) => (
- {icon(method.icon)} {method.label}
+ {method.label}
))}
@@ -58,7 +58,7 @@ export default class NotificationGrid extends Component {
{this.types.map((type) => (
- {icon(type.icon)} {type.label}
+ {type.label}
{this.methods.map((method) => {
const key = this.preferenceKey(type.name, method.name);
diff --git a/framework/core/js/src/forum/components/NotificationList.js b/framework/core/js/src/forum/components/NotificationList.js
index c43118be2e..b0ae5fc664 100644
--- a/framework/core/js/src/forum/components/NotificationList.js
+++ b/framework/core/js/src/forum/components/NotificationList.js
@@ -3,10 +3,11 @@ import Component from '../../common/Component';
import listItems from '../../common/helpers/listItems';
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
-import LoadingIndicator from '../../common/components/LoadingIndicator';
import Discussion from '../../common/models/Discussion';
import ItemList from '../../common/utils/ItemList';
import Tooltip from '../../common/components/Tooltip';
+import HeaderList from './HeaderList';
+import HeaderListGroup from './HeaderListGroup';
/**
* The `NotificationList` component displays a list of the logged-in user's
@@ -17,15 +18,17 @@ export default class NotificationList extends Component {
const state = this.attrs.state;
return (
-
-
-
{app.translator.trans('core.forum.notifications.title')}
-
-
{this.controlItems().toArray()}
-
-
-
{this.content(state)}
-
+ state.hasNext() && !state.isLoadingNext() && state.loadNext()}
+ >
+ {this.content(state)}
+
);
}
@@ -69,11 +72,7 @@ export default class NotificationList extends Component {
}
content(state) {
- if (state.isLoading()) {
- return ;
- }
-
- if (state.hasItems()) {
+ if (!state.isLoading() && state.hasItems()) {
return state.getPages().map((page) => {
const groups = [];
const discussions = {};
@@ -105,76 +104,30 @@ export default class NotificationList extends Component {
const badges = group.discussion && group.discussion.badges().toArray();
return (
-
- {group.discussion ? (
-
- {badges && !!badges.length &&
}
-
{group.discussion.title()}
-
- ) : (
-
{app.forum.attribute('title')}
- )}
-
-
- {group.notifications.map((notification) => {
+
+ {badges && !!badges.length && }
+ {group.discussion.title()}
+
+ ) : (
+ app.forum.attribute('title')
+ )
+ }
+ >
+ {group.notifications
+ .map((notification) => {
const NotificationComponent = app.notificationComponents[notification.contentType()];
- return (
- !!NotificationComponent && (
-
-
-
- )
- );
- })}
-
-
+ return !!NotificationComponent ? : null;
+ })
+ .filter((component) => !!component)}
+
);
});
});
}
- return {app.translator.trans('core.forum.notifications.empty_text')}
;
- }
-
- oncreate(vnode) {
- super.oncreate(vnode);
-
- this.$notifications = this.$('.NotificationList-content');
-
- // If we are on the notifications page, the window will be scrolling and not the $notifications element.
- this.$scrollParent = this.inPanel() ? this.$notifications : $(window);
-
- this.boundScrollHandler = this.scrollHandler.bind(this);
- this.$scrollParent.on('scroll', this.boundScrollHandler);
- }
-
- onremove(vnode) {
- super.onremove(vnode);
-
- this.$scrollParent.off('scroll', this.boundScrollHandler);
- }
-
- scrollHandler() {
- const state = this.attrs.state;
-
- // Whole-page scroll events are listened to on `window`, but we need to get the actual
- // scrollHeight, scrollTop, and clientHeight from the document element.
- const scrollParent = this.inPanel() ? this.$scrollParent[0] : document.documentElement;
-
- // On very short screens, the scrollHeight + scrollTop might not reach the clientHeight
- // by a fraction of a pixel, so we compensate for that.
- const atBottom = Math.abs(scrollParent.scrollHeight - scrollParent.scrollTop - scrollParent.clientHeight) <= 1;
-
- if (state.hasNext() && !state.isLoadingNext() && atBottom) {
- state.loadNext();
- }
- }
-
- /**
- * If the NotificationList component isn't in a panel (e.g. on NotificationPage when mobile),
- * we need to listen to scroll events on the window, and get scroll state from the body.
- */
- inPanel() {
- return this.$notifications.css('overflow') === 'auto';
+ return null;
}
}
diff --git a/framework/core/js/src/forum/components/NotificationsDropdown.tsx b/framework/core/js/src/forum/components/NotificationsDropdown.tsx
index efd49e9cf7..bc23a2ec25 100644
--- a/framework/core/js/src/forum/components/NotificationsDropdown.tsx
+++ b/framework/core/js/src/forum/components/NotificationsDropdown.tsx
@@ -1,19 +1,14 @@
import app from '../../forum/app';
-import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
-import icon from '../../common/helpers/icon';
-import classList from '../../common/utils/classList';
import NotificationList from './NotificationList';
import extractText from '../../common/utils/extractText';
-import type Mithril from 'mithril';
+import HeaderDropdown, { IHeaderDropdownAttrs } from './HeaderDropdown';
+import classList from '../../common/utils/classList';
-export interface INotificationsDropdown extends IDropdownAttrs {}
+export interface INotificationsDropdown extends IHeaderDropdownAttrs {}
-export default class NotificationsDropdown extends Dropdown {
+export default class NotificationsDropdown extends HeaderDropdown {
static initAttrs(attrs: INotificationsDropdown) {
- attrs.className ||= 'NotificationsDropdown';
- attrs.buttonClassName ||= 'Button Button--flat';
- attrs.menuClassName ||= 'Dropdown-menu--right';
- attrs.label ||= extractText(app.translator.trans('core.forum.notifications.tooltip'));
+ attrs.className = classList('NotificationsDropdown', attrs.className);
attrs.icon ||= 'fas fa-bell';
// For best a11y support, both `title` and `aria-label` should be used
@@ -22,43 +17,8 @@ export default class NotificationsDropdown {
- const newNotifications = this.getNewCount();
-
- const vdom = super.getButton(children);
-
- vdom.attrs.title = this.attrs.label;
-
- vdom.attrs.className = classList(vdom.attrs.className, [newNotifications && 'new']);
- vdom.attrs.onclick = this.onclick.bind(this);
-
- return vdom;
- }
-
- getButtonContent(): Mithril.ChildArray {
- const unread = this.getUnreadCount();
-
- return [
- this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : null,
- unread !== 0 && {unread} ,
- {this.attrs.label} ,
- ];
- }
-
- getMenu() {
- return (
-
- {this.showing && }
-
- );
- }
-
- onclick() {
- if (app.drawer.isOpen()) {
- this.goToRoute();
- } else {
- this.attrs.state.load();
- }
+ getContent() {
+ return ;
}
goToRoute() {
@@ -66,16 +26,10 @@ export default class NotificationsDropdown Mithril.Children;
+ sidebar?: () => Mithril.Children;
+ pane?: () => Mithril.Children;
+ loading?: boolean;
+ className: string;
+}
+
+export default class PageStructure extends Component {
+ private content?: Mithril.Children;
+
+ view(vnode: Mithril.Vnode): Mithril.Children {
+ const { className } = vnode.attrs;
+
+ this.content = vnode.children;
+
+ return {this.rootItems().toArray()}
;
+ }
+
+ rootItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('pane', this.providedPane(), 100);
+ items.add('main', this.main(), 10);
+
+ return items;
+ }
+
+ mainItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('hero', this.providedHero(), 100);
+ items.add('container', this.container(), 10);
+
+ return items;
+ }
+
+ loadingItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('spinner', , 100);
+
+ return items;
+ }
+
+ main(): Mithril.Children {
+ return {this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}
;
+ }
+
+ containerItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('sidebar', this.sidebar(), 100);
+ items.add('content', this.providedContent(), 10);
+
+ return items;
+ }
+
+ container(): Mithril.Children {
+ return {this.containerItems().toArray()}
;
+ }
+
+ sidebarItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('sidebar', (this.attrs.sidebar && this.attrs.sidebar()) || null, 100);
+
+ return items;
+ }
+
+ sidebar(): Mithril.Children {
+ return {this.sidebarItems().toArray()}
;
+ }
+
+ providedPane(): Mithril.Children {
+ return {(this.attrs.pane && this.attrs.pane()) || null}
;
+ }
+
+ providedHero(): Mithril.Children {
+ return {(this.attrs.hero && this.attrs.hero()) || null}
;
+ }
+
+ providedContent(): Mithril.Children {
+ return {this.content}
;
+ }
+}
diff --git a/framework/core/js/src/forum/components/Post.tsx b/framework/core/js/src/forum/components/Post.tsx
index cfe610ffd3..02c53b22b0 100644
--- a/framework/core/js/src/forum/components/Post.tsx
+++ b/framework/core/js/src/forum/components/Post.tsx
@@ -55,29 +55,33 @@ export default abstract class Post
return (
-
- {this.loading ?
: this.content()}
-
-
- {listItems(this.actionItems().toArray())}
- {!!controls.length && (
-
- this.$('.Post-controls').addClass('open')}
- onhide={() => this.$('.Post-controls').removeClass('open')}
- accessibleToggleLabel={app.translator.trans('core.forum.post_controls.toggle_dropdown_accessible_label')}
- >
- {controls}
-
-
- )}
-
-
-
{footerItems.length ? : null}
+ {this.header()}
+
+
{this.sideItems().toArray()}
+
+ {this.loading ?
: this.content()}
+
+
+ {listItems(this.actionItems().toArray())}
+ {!!controls.length && (
+
+ this.$('.Post-controls').addClass('open')}
+ onhide={() => this.$('.Post-controls').removeClass('open')}
+ accessibleToggleLabel={app.translator.trans('core.forum.post_controls.toggle_dropdown_accessible_label')}
+ >
+ {controls}
+
+
+ )}
+
+
+
{footerItems.length ? : null}
+
);
@@ -105,11 +109,14 @@ export default abstract class Post
return {};
}
+ header(): Mithril.Children {
+ return null;
+ }
+
/**
* Get the post's content.
*/
content(): Mithril.Children {
- // TODO: [Flarum 2.0] return `null`
return [];
}
@@ -150,4 +157,16 @@ export default abstract class Post
footerItems(): ItemList {
return new ItemList();
}
+
+ sideItems(): ItemList {
+ const items = new ItemList();
+
+ items.add('avatar', this.avatar(), 100);
+
+ return items;
+ }
+
+ avatar(): Mithril.Children {
+ return null;
+ }
}
diff --git a/framework/core/js/src/forum/components/PostPreview.js b/framework/core/js/src/forum/components/PostPreview.js
index 5293bd3c17..f840113330 100644
--- a/framework/core/js/src/forum/components/PostPreview.js
+++ b/framework/core/js/src/forum/components/PostPreview.js
@@ -1,9 +1,9 @@
import app from '../../forum/app';
import Component from '../../common/Component';
import Link from '../../common/components/Link';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import highlight from '../../common/helpers/highlight';
+import Avatar from '../../common/components/Avatar';
/**
* The `PostPreview` component shows a link to a post containing the avatar and
@@ -23,7 +23,7 @@ export default class PostPreview extends Component {
return (
- {avatar(user)}
+
{username(user)} {excerpt}
diff --git a/framework/core/js/src/forum/components/PostStreamScrubber.js b/framework/core/js/src/forum/components/PostStreamScrubber.js
index 4fe6b3ace7..6880bc9abc 100644
--- a/framework/core/js/src/forum/components/PostStreamScrubber.js
+++ b/framework/core/js/src/forum/components/PostStreamScrubber.js
@@ -1,8 +1,8 @@
import app from '../../forum/app';
import Component from '../../common/Component';
-import icon from '../../common/helpers/icon';
import formatNumber from '../../common/utils/formatNumber';
import ScrollListener from '../../common/utils/ScrollListener';
+import Icon from '../../common/components/Icon';
/**
* The `PostStreamScrubber` component displays a scrubber which can be used to
@@ -58,13 +58,13 @@ export default class PostStreamScrubber extends Component {
return (
- {viewing} {icon('fas fa-sort')}
+ {viewing}
diff --git a/framework/core/js/src/forum/components/PostUser.js b/framework/core/js/src/forum/components/PostUser.js
index 4c7308f681..3e335fcaa1 100644
--- a/framework/core/js/src/forum/components/PostUser.js
+++ b/framework/core/js/src/forum/components/PostUser.js
@@ -2,10 +2,10 @@ import app from '../../forum/app';
import Component from '../../common/Component';
import Link from '../../common/components/Link';
import UserCard from './UserCard';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import userOnline from '../../common/helpers/userOnline';
import listItems from '../../common/helpers/listItems';
+import Avatar from '../../common/components/Avatar';
/**
* The `PostUser` component shows the avatar and username of a post's author.
@@ -23,7 +23,7 @@ export default class PostUser extends Component {
return (
- {avatar(user, { className: 'PostUser-avatar' })} {username(user)}
+ {username(user)}
);
@@ -33,53 +33,13 @@ export default class PostUser extends Component {
- {avatar(user, { className: 'PostUser-avatar' })}
+
{userOnline(user)}
{username(user)}
{listItems(user.badges().toArray())}
-
- {!post.isHidden() && this.attrs.cardVisible && (
-
- )}
);
}
-
- oncreate(vnode) {
- super.oncreate(vnode);
-
- let timeout;
-
- this.$()
- .on('mouseover', '.PostUser-name a, .UserCard', () => {
- clearTimeout(timeout);
- timeout = setTimeout(this.showCard.bind(this), 500);
- })
- .on('mouseout', '.PostUser-name a, .UserCard', () => {
- clearTimeout(timeout);
- timeout = setTimeout(this.hideCard.bind(this), 250);
- });
- }
-
- /**
- * Show the user card.
- */
- showCard() {
- this.attrs.oncardshow();
-
- setTimeout(() => this.$('.UserCard').addClass('in'));
- }
-
- /**
- * Hide the user card.
- */
- hideCard() {
- this.$('.UserCard')
- .removeClass('in')
- .one('transitionend webkitTransitionEnd oTransitionEnd', () => {
- this.attrs.oncardhide();
- });
- }
}
diff --git a/framework/core/js/src/forum/components/RenameDiscussionModal.tsx b/framework/core/js/src/forum/components/RenameDiscussionModal.tsx
index b88d4dba97..12a0e865ac 100644
--- a/framework/core/js/src/forum/components/RenameDiscussionModal.tsx
+++ b/framework/core/js/src/forum/components/RenameDiscussionModal.tsx
@@ -4,6 +4,7 @@ import Button from '../../common/components/Button';
import Stream from '../../common/utils/Stream';
import Mithril from 'mithril';
import Discussion from '../../common/models/Discussion';
+import Form from '../../common/components/Form';
export interface IRenameDiscussionModalAttrs extends IInternalModalAttrs {
discussion: Discussion;
@@ -37,16 +38,16 @@ export default class RenameDiscussionModal
-
+
-
+
{app.translator.trans('core.forum.rename_discussion.submit_button')}
-
+
);
}
diff --git a/framework/core/js/src/forum/components/ReplyComposer.js b/framework/core/js/src/forum/components/ReplyComposer.js
index 1f5300d8b1..e2111cc8e0 100644
--- a/framework/core/js/src/forum/components/ReplyComposer.js
+++ b/framework/core/js/src/forum/components/ReplyComposer.js
@@ -2,8 +2,8 @@ import app from '../../forum/app';
import ComposerBody from './ComposerBody';
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
-import icon from '../../common/helpers/icon';
import extractText from '../../common/utils/extractText';
+import Icon from '../../common/components/Icon';
function minimizeComposerIfFullScreen(e) {
if (app.composer.isFullScreen()) {
@@ -37,7 +37,7 @@ export default class ReplyComposer extends ComposerBody {
items.add(
'title',
- {icon('fas fa-reply')}{' '}
+ {' '}
{discussion.title()}
diff --git a/framework/core/js/src/forum/components/ReplyPlaceholder.js b/framework/core/js/src/forum/components/ReplyPlaceholder.js
index c0a8148d9b..dd018ce3e9 100644
--- a/framework/core/js/src/forum/components/ReplyPlaceholder.js
+++ b/framework/core/js/src/forum/components/ReplyPlaceholder.js
@@ -1,10 +1,10 @@
import app from '../../forum/app';
import Component from '../../common/Component';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import DiscussionControls from '../utils/DiscussionControls';
import ComposerPostPreview from './ComposerPostPreview';
import listItems from '../../common/helpers/listItems';
+import Avatar from '../../common/components/Avatar';
/**
* The `ReplyPlaceholder` component displays a placeholder for a reply, which,
@@ -19,16 +19,22 @@ export default class ReplyPlaceholder extends Component {
if (app.composer.composingReplyTo(this.attrs.discussion)) {
return (
-
-
-
- {avatar(app.session.user, { className: 'PostUser-avatar' })}
- {username(app.session.user)}
-
-
{listItems(app.session.user.badges().toArray())}
+
+
-
-
+
+
+
+
{username(app.session.user)}
+
{listItems(app.session.user.badges().toArray())}
+
+
+
+
+
+
+
);
}
@@ -39,9 +45,14 @@ export default class ReplyPlaceholder extends Component {
return (
-
- {avatar(app.session.user, { className: 'PostUser-avatar' })} {app.translator.trans('core.forum.post_stream.reply_placeholder')}
-
+
+
+
+ {app.translator.trans('core.forum.post_stream.reply_placeholder')}
+
+
);
}
diff --git a/framework/core/js/src/forum/components/Search.tsx b/framework/core/js/src/forum/components/Search.tsx
index 07fca3f455..75e8152f25 100644
--- a/framework/core/js/src/forum/components/Search.tsx
+++ b/framework/core/js/src/forum/components/Search.tsx
@@ -5,12 +5,12 @@ import ItemList from '../../common/utils/ItemList';
import classList from '../../common/utils/classList';
import extractText from '../../common/utils/extractText';
import KeyboardNavigatable from '../../common/utils/KeyboardNavigatable';
-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 Icon from '../../common/components/Icon';
/**
* The `SearchSource` interface defines a section of search results in the
@@ -163,7 +163,7 @@ export default class Search
extends Compone
aria-label={app.translator.trans('core.forum.header.search_clear_button_accessible_label')}
type="button"
>
- {icon('fas fa-times-circle')}
+
)}
diff --git a/framework/core/js/src/forum/components/SessionDropdown.tsx b/framework/core/js/src/forum/components/SessionDropdown.tsx
index e77fe3ed9b..0da69c2d8f 100644
--- a/framework/core/js/src/forum/components/SessionDropdown.tsx
+++ b/framework/core/js/src/forum/components/SessionDropdown.tsx
@@ -1,5 +1,4 @@
import app from '../../forum/app';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
import LinkButton from '../../common/components/LinkButton';
@@ -8,6 +7,7 @@ import ItemList from '../../common/utils/ItemList';
import Separator from '../../common/components/Separator';
import extractText from '../../common/utils/extractText';
import type Mithril from 'mithril';
+import Avatar from '../../common/components/Avatar';
export interface ISessionDropdownAttrs extends IDropdownAttrs {}
@@ -33,7 +33,7 @@ export default class SessionDropdown{username(user)}];
+ return [ , ' ', {username(user)} ];
}
/**
diff --git a/framework/core/js/src/forum/components/SettingsPage.tsx b/framework/core/js/src/forum/components/SettingsPage.tsx
index b096efa994..15e18b95a8 100644
--- a/framework/core/js/src/forum/components/SettingsPage.tsx
+++ b/framework/core/js/src/forum/components/SettingsPage.tsx
@@ -10,6 +10,7 @@ import ChangeEmailModal from './ChangeEmailModal';
import listItems from '../../common/helpers/listItems';
import extractText from '../../common/utils/extractText';
import type Mithril from 'mithril';
+import classList from '../../common/utils/classList';
/**
* The `SettingsPage` component displays the user's settings control panel, in
@@ -45,7 +46,10 @@ export default class SettingsPage
+
{this[sectionItems]().toArray()}
,
100 - index * 10
diff --git a/framework/core/js/src/forum/components/TerminalPost.js b/framework/core/js/src/forum/components/TerminalPost.js
index 86e9f079c3..06f7c5181c 100644
--- a/framework/core/js/src/forum/components/TerminalPost.js
+++ b/framework/core/js/src/forum/components/TerminalPost.js
@@ -1,7 +1,8 @@
import app from '../../forum/app';
import Component from '../../common/Component';
import humanTime from '../../common/helpers/humanTime';
-import icon from '../../common/helpers/icon';
+
+import Icon from '../../common/components/Icon';
/**
* Displays information about a the first or last post in a discussion.
@@ -21,7 +22,7 @@ export default class TerminalPost extends Component {
return (
- {!!lastPost && icon('fas fa-reply')}{' '}
+ {!!lastPost && }{' '}
{app.translator.trans('core.forum.discussion_list.' + (lastPost ? 'replied' : 'started') + '_text', {
user,
ago: humanTime(time),
diff --git a/framework/core/js/src/forum/components/UserCard.js b/framework/core/js/src/forum/components/UserCard.js
index c80914f2c8..4e15c3fbc3 100644
--- a/framework/core/js/src/forum/components/UserCard.js
+++ b/framework/core/js/src/forum/components/UserCard.js
@@ -3,14 +3,14 @@ import Component from '../../common/Component';
import humanTime from '../../common/utils/humanTime';
import ItemList from '../../common/utils/ItemList';
import UserControls from '../utils/UserControls';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
-import icon from '../../common/helpers/icon';
import Dropdown from '../../common/components/Dropdown';
import Link from '../../common/components/Link';
import AvatarEditor from './AvatarEditor';
import listItems from '../../common/helpers/listItems';
import classList from '../../common/utils/classList';
+import Icon from '../../common/components/Icon';
+import Avatar from '../../common/components/Avatar';
/**
* The `UserCard` component displays a user's profile card. This is used both on
@@ -27,51 +27,64 @@ import classList from '../../common/utils/classList';
export default class UserCard extends Component {
view() {
const user = this.attrs.user;
- const controls = UserControls.controls(user, this).toArray();
const color = user.color();
- const badges = user.badges().toArray();
return (
- {!!controls.length && (
-
- {controls}
-
- )}
-
-
-
- {this.attrs.editable ? (
- <>
- {username(user)}
- >
- ) : (
-
- {avatar(user, { loading: 'eager' })}
- {username(user)}
-
- )}
-
-
- {!!badges.length &&
}
-
-
{listItems(this.infoItems().toArray())}
-
+
{this.profileItems().toArray()}
+
{this.controlsItems().toArray()}
);
}
+ profileItems() {
+ const items = new ItemList();
+
+ items.add('avatar', this.avatar(), 100);
+ items.add('content', this.content(), 10);
+
+ return items;
+ }
+
+ avatar() {
+ const user = this.attrs.user;
+
+ return this.attrs.editable ? (
+
+ ) : (
+
+
+
+ );
+ }
+
+ content() {
+ return {this.contentItems().toArray()}
;
+ }
+
+ contentItems() {
+ const items = new ItemList();
+
+ const user = this.attrs.user;
+ const badges = user.badges().toArray();
+
+ items.add('identity', {username(user)} , 100);
+
+ if (badges.length) {
+ items.add('badges', , 90);
+ }
+
+ items.add('info', {listItems(this.infoItems().toArray())} , 80);
+
+ return items;
+ }
+
/**
* Build an item list of tidbits of info to show on this user's profile.
*
@@ -89,8 +102,8 @@ export default class UserCard extends Component {
'lastSeen',
{online
- ? [icon('fas fa-circle'), ' ', app.translator.trans('core.forum.user.online_text')]
- : [icon('far fa-clock'), ' ', humanTime(lastSeenAt)]}
+ ? [ , ' ', app.translator.trans('core.forum.user.online_text')]
+ : [ , ' ', humanTime(lastSeenAt)]}
,
100
);
@@ -100,4 +113,30 @@ export default class UserCard extends Component {
return items;
}
+
+ controlsItems() {
+ const items = new ItemList();
+
+ const user = this.attrs.user;
+ const controls = UserControls.controls(user, this).toArray();
+
+ if (controls.length) {
+ items.add(
+ 'controls',
+
+ {controls}
+ ,
+ 100
+ );
+ }
+
+ return items;
+ }
}
diff --git a/framework/core/js/src/forum/components/UserPage.tsx b/framework/core/js/src/forum/components/UserPage.tsx
index 16d2c60f07..990d1d3245 100644
--- a/framework/core/js/src/forum/components/UserPage.tsx
+++ b/framework/core/js/src/forum/components/UserPage.tsx
@@ -2,7 +2,6 @@ import app from '../../forum/app';
import Page, { IPageAttrs } from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList';
import UserCard from './UserCard';
-import LoadingIndicator from '../../common/components/LoadingIndicator';
import SelectDropdown from '../../common/components/SelectDropdown';
import LinkButton from '../../common/components/LinkButton';
import Separator from '../../common/components/Separator';
@@ -10,6 +9,7 @@ import listItems from '../../common/helpers/listItems';
import AffixedSidebar from './AffixedSidebar';
import type User from '../../common/models/User';
import type Mithril from 'mithril';
+import PageStructure from './PageStructure';
export interface IUserPageAttrs extends IPageAttrs {}
@@ -37,28 +37,30 @@ export default class UserPage
- {this.user
- ? [
- ,
-
-
-
-
- {listItems(this.sidebarItems().toArray())}
-
-
-
{this.content()}
-
-
,
- ]
- : [ ]}
-
+
+ {this.user && this.content()}
+
+ );
+ }
+
+ hero() {
+ return (
+
+ );
+ }
+
+ sidebar() {
+ return (
+
+
+ {listItems(this.sidebarItems().toArray())}
+
+
);
}
diff --git a/framework/core/js/src/forum/components/UserSecurityPage.tsx b/framework/core/js/src/forum/components/UserSecurityPage.tsx
index df9a35e7e1..7af57ac0ee 100644
--- a/framework/core/js/src/forum/components/UserSecurityPage.tsx
+++ b/framework/core/js/src/forum/components/UserSecurityPage.tsx
@@ -75,8 +75,11 @@ export default class UserSecurityPage
- {app.translator.trans('core.forum.security.global_logout.help_text')}
+
('canCreateAccessToken')}
- onclick={() =>
- app.modal.show(NewAccessTokenModal, {
- onsuccess: (token: AccessToken) => {
- this.state.pushToken(token);
- m.redraw();
- },
- })
- }
- >
- {app.translator.trans('core.forum.security.new_access_token_button')}
-
+
+ ('canCreateAccessToken')}
+ onclick={() =>
+ app.modal.show(NewAccessTokenModal, {
+ onsuccess: (token: AccessToken) => {
+ this.state.pushToken(token);
+ m.redraw();
+ },
+ })
+ }
+ >
+ {app.translator.trans('core.forum.security.new_access_token_button')}
+
+
);
}
@@ -186,7 +191,7 @@ export default class UserSecurityPage{terminateAllOthersButton} );
}
return items;
diff --git a/framework/core/js/src/forum/components/UsersSearchSource.tsx b/framework/core/js/src/forum/components/UsersSearchSource.tsx
index 646c1d7406..1ae165bc62 100644
--- a/framework/core/js/src/forum/components/UsersSearchSource.tsx
+++ b/framework/core/js/src/forum/components/UsersSearchSource.tsx
@@ -2,11 +2,11 @@ import type Mithril from 'mithril';
import app from '../../forum/app';
import highlight from '../../common/helpers/highlight';
-import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Link from '../../common/components/Link';
import { SearchSource } from './Search';
import User from '../../common/models/User';
+import Avatar from '../../common/components/Avatar';
/**
* The `UsersSearchSource` finds and displays user search results in the search
@@ -49,7 +49,7 @@ export default class UsersSearchResults implements SearchSource {
return (
- {avatar(user)}
+
{name}
diff --git a/framework/core/js/src/forum/forum.ts b/framework/core/js/src/forum/forum.ts
index 8534f547d8..7a74d158b0 100644
--- a/framework/core/js/src/forum/forum.ts
+++ b/framework/core/js/src/forum/forum.ts
@@ -5,7 +5,6 @@ import './utils/PostControls';
import './utils/slidable';
import './utils/History';
import './utils/DiscussionControls';
-import './utils/alertEmailConfirmation';
import './utils/UserControls';
import './utils/Pane';
import './states/ComposerState';
diff --git a/framework/core/js/src/forum/utils/alertEmailConfirmation.js b/framework/core/js/src/forum/utils/alertEmailConfirmation.js
deleted file mode 100644
index 373ce0d213..0000000000
--- a/framework/core/js/src/forum/utils/alertEmailConfirmation.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import Alert from '../../common/components/Alert';
-import Button from '../../common/components/Button';
-import icon from '../../common/helpers/icon';
-import Component from '../../common/Component';
-
-/**
- * Shows an alert if the user has not yet confirmed their email address.
- *
- * @param {import('../ForumApplication').default} app
- */
-export default function alertEmailConfirmation(app) {
- const user = app.session.user;
-
- if (!user || user.isEmailConfirmed()) return;
-
- class ResendButton extends Component {
- oninit(vnode) {
- super.oninit(vnode);
-
- this.loading = false;
- this.sent = false;
- }
-
- view() {
- return (
-
- {this.sent
- ? [icon('fas fa-check'), ' ', app.translator.trans('core.forum.user_email_confirmation.sent_message')]
- : app.translator.trans('core.forum.user_email_confirmation.resend_button')}
-
- );
- }
-
- onclick() {
- this.loading = true;
- m.redraw();
-
- app
- .request({
- method: 'POST',
- url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/send-confirmation',
- })
- .then(() => {
- this.loading = false;
- this.sent = true;
- m.redraw();
- })
- .catch(() => {
- this.loading = false;
- m.redraw();
- });
- }
- }
-
- class ContainedAlert extends Alert {
- view(vnode) {
- const vdom = super.view(vnode);
- return { ...vdom, children: [{vdom.children}
] };
- }
- }
-
- m.mount($('
').insertBefore('#content')[0], {
- view: () => (
- ]} className="Alert--emailConfirmation">
- {app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: {user.email()} })}
-
- ),
- });
-}
diff --git a/framework/core/less/admin/AdminNav.less b/framework/core/less/admin/AdminNav.less
index 5206047e7f..91ca57f6fc 100644
--- a/framework/core/less/admin/AdminNav.less
+++ b/framework/core/less/admin/AdminNav.less
@@ -17,6 +17,10 @@
}
}
+.Header-navigation {
+ position: relative;
+}
+
@media @phone {
.Dropdown-menu {
height: 70vh;
@@ -90,6 +94,8 @@
margin-left: var(--admin-pane-width);
}
.App-nav .AdminNav {
+ --extension-icon-size: 25px;
+
.Dropdown-menu {
.item-search {
margin-top: 10px;
@@ -104,8 +110,8 @@
> li {
> a {
- padding: 10px 10px 10px 45px;
- display: block;
+ padding: 10px 10px 10px 15px;
+ gap: 9px;
text-decoration: none;
}
@@ -131,14 +137,11 @@
}
.Button-icon {
- float: left;
font-size: 13px !important;
- margin-left: -25px !important;
- margin-top: 4px !important;
+ width: var(--extension-icon-size);
}
.Button-label {
- padding-left: 5px;
font-size: 14px;
font-weight: normal;
}
@@ -150,8 +153,7 @@
}
.ExtensionIcon {
- --size: 25px;
- margin-left: -29px;
+ --size: var(--extension-icon-size);
}
}
}
@@ -194,12 +196,9 @@
.ExtensionNavButton {
.Button-label {
- display: inline-block;
- max-width: ~"calc(100% - 18px)";
+ max-width: ~"calc(100% - 72px)";
overflow: hidden;
text-overflow: ellipsis;
- vertical-align: middle;
- margin-left: 5px;
}
}
diff --git a/framework/core/less/admin/AppearancePage.less b/framework/core/less/admin/AppearancePage.less
index 6aaec43b46..8aeca71751 100644
--- a/framework/core/less/admin/AppearancePage.less
+++ b/framework/core/less/admin/AppearancePage.less
@@ -14,20 +14,9 @@
}
.AppearancePage-colors-input {
display: flex;
+ gap: 10px;
overflow: hidden;
-
- .Form-group {
- display: inline-block;
- }
-
- .Form-group:last-child {
- margin-bottom: 24px !important;
- margin-left: 10px;
- }
-}
-
-.AppearancePage-colors .Checkbox {
- margin-bottom: 15px;
+ margin-bottom: 10px;
}
.TextareaCodeModal {
diff --git a/framework/core/less/admin/BasicsPage.less b/framework/core/less/admin/BasicsPage.less
index e68ac6398a..a3a4e171c5 100644
--- a/framework/core/less/admin/BasicsPage.less
+++ b/framework/core/less/admin/BasicsPage.less
@@ -8,10 +8,6 @@
}
}
- .Form-group {
- margin-bottom: 20px;
- }
-
ul {
list-style: none;
margin: 0;
@@ -20,15 +16,10 @@
}
.BasicsPage-welcomeBanner-input {
- input {
- margin-bottom: 1px;
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
+ .StackedFormControl > :first-child {
font-weight: bold;
}
textarea {
- border-top-right-radius: 0;
- border-top-left-radius: 0;
- margin-bottom: 10px;
+ min-width: 100%;
}
}
diff --git a/framework/core/less/admin/EditGroupModal.less b/framework/core/less/admin/EditGroupModal.less
index 79e49201e3..a252d8e2aa 100644
--- a/framework/core/less/admin/EditGroupModal.less
+++ b/framework/core/less/admin/EditGroupModal.less
@@ -1,22 +1,8 @@
.EditGroupModal {
- .Form-group:not(:last-child) {
- margin-bottom: 30px;
- }
.Badge {
margin-right: 5px;
}
}
-.EditGroupModal-name-input {
- :first-child {
- margin-bottom: 1px;
- border-bottom-right-radius: 0;
- border-bottom-left-radius: 0;
- }
- :last-child {
- border-top-right-radius: 0;
- border-top-left-radius: 0;
- }
-}
.EditGroupModal-delete {
- float: right;
+ margin-left: auto;
}
diff --git a/framework/core/less/admin/ExtensionPage.less b/framework/core/less/admin/ExtensionPage.less
index f60cd1ca08..feafa80234 100644
--- a/framework/core/less/admin/ExtensionPage.less
+++ b/framework/core/less/admin/ExtensionPage.less
@@ -27,6 +27,10 @@
}
}
}
+
+ .container {
+ .clearfix();
+ }
}
&-header,
diff --git a/framework/core/less/admin/MailPage.less b/framework/core/less/admin/MailPage.less
index 3cf2bc6e60..30d6df617f 100644
--- a/framework/core/less/admin/MailPage.less
+++ b/framework/core/less/admin/MailPage.less
@@ -7,26 +7,4 @@
margin: 0;
}
}
-
- button, .Alert {
- margin-bottom: 20px;
- }
-
- ul {
- list-style: none;
- margin: 0;
- padding: 0;
- }
-}
-
-.MailPage-MailSettings-input {
-
- label {
- display: block;
- margin-bottom: 7px;
- }
-
- .Select {
- display: block;
- }
}
diff --git a/framework/core/less/common/App.less b/framework/core/less/common/App.less
index e3f0e8eb4c..0162ef2ed7 100644
--- a/framework/core/less/common/App.less
+++ b/framework/core/less/common/App.less
@@ -169,6 +169,12 @@
// ------------------------------------
// Header
+.App-header {
+ display: flex;
+}
+.Header-navigation {
+ position: absolute;
+}
.Header-controls {
margin: 0;
padding: 0;
@@ -176,7 +182,6 @@
}
.Header-logo {
max-height: 30px;
- vertical-align: middle;
}
// On phones, the header is displayed inside of the drawer. We lay its
@@ -239,6 +244,12 @@
position: absolute;
border-bottom: 0;
+ .container {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ }
+
.affix & {
position: fixed;
}
@@ -252,29 +263,20 @@
}
}
- .Header-navigation {
- float: left;
- margin-right: 25px;
- }
.Header-controls {
- &, > li {
- display: inline-block;
- vertical-align: middle;
- }
+ display: flex;
+ align-items: center;
}
.Header-primary {
- float: left;
+ //
}
.Header-title {
- float: left;
- vertical-align: top;
font-size: 18px;
font-weight: normal;
- margin: 0 15px 0 0;
line-height: 34px;
}
.Header-secondary {
- float: right;
+ margin-left: auto;
.Search {
margin-right: 10px;
@@ -294,6 +296,22 @@
}
}
+@media @desktop-hd {
+ .Header-navigation {
+ .hasPane.panePinned & {
+ position: relative;
+ width: ~"min(20%, var(--pane-width))";
+ }
+ }
+}
+
+@media (max-width: calc(@screen-desktop-hd + 80px)) {
+ .Header-navigation {
+ position: relative;
+ width: auto;
+ }
+}
+
// ------------------------------------
// Content Area
diff --git a/framework/core/less/common/Badge.less b/framework/core/less/common/Badge.less
index 9cc57318cc..29e62d0c8c 100644
--- a/framework/core/less/common/Badge.less
+++ b/framework/core/less/common/Badge.less
@@ -8,7 +8,6 @@
display: inline-flex;
align-items: center;
justify-content: center;
- vertical-align: middle;
box-shadow: 0 2px 4px var(--shadow-color);
.Badge-label {
diff --git a/framework/core/less/common/Button.less b/framework/core/less/common/Button.less
index b0f54ee231..f6b829d9a0 100644
--- a/framework/core/less/common/Button.less
+++ b/framework/core/less/common/Button.less
@@ -5,12 +5,14 @@
// Make the div behave like a button
.ButtonGroup {
position: relative;
- display: inline-block;
- vertical-align: middle;
+ display: inline-flex;
+ gap: 1px;
> .Button {
position: relative;
- float: left;
+ flex-shrink: 0;
+ flex-grow: 0;
+
// Bring the "active" button to the front
&:hover,
&:focus,
@@ -19,31 +21,33 @@
z-index: 2;
}
- &:not(:first-child):not(:last-child):not(.Dropdown-toggle) {
+ &:not(:first-of-type):not(:last-of-type):not(.Dropdown-toggle) {
border-radius: 0;
}
- &:first-child:not(:last-child):not(.Dropdown-toggle) {
+ &:first-of-type:not(:last-of-type):not(.Dropdown-toggle) {
margin-left: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
- &:last-child:not(:first-child), &.Dropdown-toggle:not(:first-child) {
+ &:last-of-type:not(:first-of-type), &.Dropdown-toggle:not(:first-of-type) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
-
- .Button + .Button {
- margin-left: 1px;
- }
}
//
// Buttons
// --------------------------------------------------
+.Button, .LinkButton {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+
.Button {
- display: inline-block;
+ justify-content: center;
margin-bottom: 0; // For input.btn
text-align: center;
vertical-align: middle;
@@ -159,10 +163,6 @@
font-weight: bold;
padding-left: 20px;
padding-right: 20px;
-
- .Button-icon {
- display: none;
- }
}
.Button--inverted {
.Button--color-auto('button-inverted');
@@ -179,7 +179,7 @@
}
}
.Button--block {
- display: block;
+ display: flex;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
@@ -212,8 +212,11 @@
.Avatar--size(24px);
}
}
+.Button-label {
+ line-height: inherit;
+}
.Button-icon {
- margin-right: 7px;
+ line-height: inherit;
}
.Button-icon,
.Button-caret {
diff --git a/framework/core/less/common/ColorInput.less b/framework/core/less/common/ColorInput.less
index baf9591b8a..fd6e523175 100644
--- a/framework/core/less/common/ColorInput.less
+++ b/framework/core/less/common/ColorInput.less
@@ -31,6 +31,6 @@
&-icon {
text-align: center;
- color: @validation-error-color;
+ color: var(--validation-error-color);
}
}
diff --git a/framework/core/less/common/DetailedDropdownItem.less b/framework/core/less/common/DetailedDropdownItem.less
new file mode 100644
index 0000000000..77d8f7c27e
--- /dev/null
+++ b/framework/core/less/common/DetailedDropdownItem.less
@@ -0,0 +1,22 @@
+.DetailedDropdownItem {
+ .Dropdown-menu > li > & {
+ align-items: start;
+ }
+
+ .Button-icon {
+ margin-top: 2px;
+ }
+}
+
+.DetailedDropdownItem-content {
+ display: grid;
+ grid-template-columns: 25px 1fr;
+ white-space: normal;
+}
+
+.DetailedDropdownItem-description {
+ display: block;
+ color: var(--muted-color);
+ font-size: 12px;
+ margin-top: 3px;
+}
diff --git a/framework/core/less/common/Dropdown.less b/framework/core/less/common/Dropdown.less
index da3163fa86..d62efc72bb 100644
--- a/framework/core/less/common/Dropdown.less
+++ b/framework/core/less/common/Dropdown.less
@@ -26,7 +26,9 @@
> li {
> a, > button, > span {
padding: 8px 15px;
- display: block;
+ display: flex;
+ align-items: center;
+ gap: 9px;
width: 100%;
color: var(--text-color);
border-radius: 0;
@@ -41,17 +43,12 @@
font-weight: normal;
text-decoration: none;
cursor: pointer;
-
- &.hasIcon {
- padding-left: 40px;
- }
+ line-height: 20px;
.Button-icon {
- float: left;
- margin-left: -25px;
- margin-top: 2px;
width: 16px;
text-align: center;
+ flex-shrink: 0;
}
&.disabled {
@@ -135,7 +132,7 @@
}
@media @tablet-up {
- .Dropdown-menu li:first-child {
+ &:not(.Dropdown--withMainAction) .Dropdown-menu li:first-child {
&, + li.Dropdown-separator {
display: none;
}
diff --git a/framework/core/less/common/Form.less b/framework/core/less/common/Form.less
index 85bca9817e..a6789fca73 100644
--- a/framework/core/less/common/Form.less
+++ b/framework/core/less/common/Form.less
@@ -1,8 +1,25 @@
-.Form-group {
- margin-bottom: 24px;
+.Form {
+ --gap: 24px;
+ margin-bottom: 32px;
&:last-child {
- margin-bottom: 0 !important;
+ margin-bottom: 0;
+ }
+}
+.Form, .Form-body {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--gap);
+}
+.Form-header {
+ &:empty {
+ display: none;
+ }
+
+ > label {
+ font-weight: bold;
+ color: var(--muted-color);
}
}
@@ -24,18 +41,63 @@
}
}
- .Form-group {
- margin-bottom: 12px;
+ > *, > .Form-body > * {
+ width: 100%;
+ }
+
+ &, .Form-body {
+ --gap: 12px;
}
.checkbox {
text-align: left;
}
}
-.Form-group > label {
+.Form-group, .FieldSet, .FieldSet-items {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.FieldSet, .FieldSet-items, .Form, .Form-body {
+ > * {
+ min-width: 100%;
+ margin-bottom: 0;
+ }
+}
+
+.FieldSet--col .FieldSet-items, .Form-controls {
+ flex-wrap: wrap;
+ flex-direction: row;
+ gap: 5px;
+
+ > * {
+ min-width: unset;
+ }
+}
+
+.Form-group, .FieldSet {
+ gap: 10px;
+}
+
+.FieldSet-items {
+ width: 100%;
+ gap: 5px;
+
+ .FieldSet--form & {
+ gap: 24px;
+ }
+}
+
+.Form-group > label, .FieldSet-label {
font-size: 14px;
font-weight: bold;
- margin-bottom: 10px;
color: var(--text-color);
display: block;
}
+
+.helpText {
+ font-size: 12px;
+ line-height: 1.5em;
+ color: var(--muted-color);
+}
diff --git a/framework/core/less/common/FormControl.less b/framework/core/less/common/FormControl.less
index 07d374e4a6..1216a66719 100644
--- a/framework/core/less/common/FormControl.less
+++ b/framework/core/less/common/FormControl.less
@@ -1,7 +1,6 @@
.FormControl {
--transition: border-color .15s, background .15s;
display: block;
- width: 100%;
height: 36px;
padding: 8px 13px;
font-size: 13px;
@@ -44,9 +43,18 @@
}
}
-.helpText {
- font-size: 12px;
- line-height: 1.5em;
- margin-bottom: 10px;
- color: var(--muted-color);
+.StackedFormControl {
+ > :not(:last-child) {
+ margin-bottom: 1px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ > :last-child {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0;
+ }
+}
+
+.FormControl, .StackedFormControl, .ColorInput {
+ width: 100%;
}
diff --git a/framework/core/less/common/Modal.less b/framework/core/less/common/Modal.less
index a5ff47a42c..3e9b7014ab 100644
--- a/framework/core/less/common/Modal.less
+++ b/framework/core/less/common/Modal.less
@@ -102,7 +102,6 @@
.helpText {
font-size: 14px;
line-height: 1.5em;
- margin-bottom: 25px;
text-align: left;
}
}
diff --git a/framework/core/less/common/Search.less b/framework/core/less/common/Search.less
index 1e332aee2a..903cb12b3f 100644
--- a/framework/core/less/common/Search.less
+++ b/framework/core/less/common/Search.less
@@ -61,18 +61,16 @@
&:before {
.fas();
content: @fa-var-search;
- float: left;
margin-right: -36px;
width: 36px;
font-size: 14px;
text-align: center;
- position: relative;
+ position: absolute;
padding: 8px 0;
line-height: 1.5;
pointer-events: none;
}
input {
- float: left;
width: 225px;
padding-left: 32px;
padding-right: 32px;
@@ -85,7 +83,9 @@
}
.Button {
- float: left;
+ position: absolute;
+ right: 0;
+ top: 0;
margin-left: -36px;
width: 36px !important;
diff --git a/framework/core/less/common/common.less b/framework/core/less/common/common.less
index 92f1738d77..32c48a8e7c 100644
--- a/framework/core/less/common/common.less
+++ b/framework/core/less/common/common.less
@@ -16,6 +16,7 @@
@import "ColorInput";
@import "LabelValue";
@import "Dropdown";
+@import "DetailedDropdownItem";
@import "EditUserModal";
@import "Form";
@import "FormControl";
diff --git a/framework/core/less/common/root.less b/framework/core/less/common/root.less
index 21cdbbc710..3f20666248 100644
--- a/framework/core/less/common/root.less
+++ b/framework/core/less/common/root.less
@@ -27,6 +27,7 @@
--control-color: @control-color;
--control-danger-bg: @control-danger-bg;
--control-danger-color: @control-danger-color;
+ --control-body-bg-mix: mix(@control-bg, @body-bg, 50%);
--error-color: @error-color;
@@ -44,7 +45,7 @@
}
// ---------------------------------
- // COMPONENTS
+ // COMPONENT COLORS
--header-bg: @header-bg;
--header-color: @header-color;
@@ -86,7 +87,7 @@
--online-user-circle-color: @online-user-circle-color;
--discussion-title-color: mix(@heading-color, @body-bg, 55%);
- --discussion-list-item-bg-hover: mix(@control-bg, @body-bg, 50%);
+ --discussion-list-item-bg-hover: var(--control-body-bg-mix);
.Button--color-vars(@control-color, @control-bg, 'button');
.Button--color-vars(@body-bg, @primary-color, 'button-primary');
@@ -120,15 +121,21 @@
// available to the JS code.
--flarum-screen: none;
- --screen-phone-max: @screen-phone-max;
- --screen-tablet: @screen-tablet;
- --screen-tablet-max: @screen-tablet-max;
- --screen-desktop: @screen-desktop;
- --screen-desktop-max: @screen-desktop-max;
- --screen-desktop-hd: @screen-desktop-hd;
+ --screen-phone-max: @screen-phone-max;
+ --screen-tablet: @screen-tablet;
+ --screen-tablet-max: @screen-tablet-max;
+ --screen-desktop: @screen-desktop;
+ --screen-desktop-max: @screen-desktop-max;
+ --screen-desktop-hd: @screen-desktop-hd;
@media @phone { --flarum-screen: phone; }
@media @tablet { --flarum-screen: tablet; }
@media @desktop { --flarum-screen: desktop; }
@media @desktop-hd { --flarum-screen: desktop-hd; }
+
+ // ---------------------------------
+ // COMPONENT LAYOUTS
+
+ --avatar-column-width: 85px;
+ --post-padding: 20px;
}
diff --git a/framework/core/less/common/scaffolding.less b/framework/core/less/common/scaffolding.less
index f6d2b79970..732e291869 100644
--- a/framework/core/less/common/scaffolding.less
+++ b/framework/core/less/common/scaffolding.less
@@ -62,7 +62,6 @@ p {
margin-left: auto;
padding-left: 15px;
padding-right: 15px;
- .clearfix();
@media @tablet {
width: @screen-tablet;
@@ -91,16 +90,6 @@ fieldset {
padding: 0;
margin: 0;
border: 0;
-
- > ul > li {
- margin-bottom: 10px;
- }
-}
-legend {
- font-size: 14px;
- font-weight: bold;
- margin-bottom: 10px;
- color: var(--text-color);
}
input[type="search"] {
-webkit-appearance: none;
@@ -183,3 +172,7 @@ blockquote ol:last-child {
--contrast-color: var(--unchanged-color);
}
}
+
+.text-colored {
+ color: var(--color);
+}
diff --git a/framework/core/less/common/sideNav.less b/framework/core/less/common/sideNav.less
index e5c169663e..3fb6ec83fa 100644
--- a/framework/core/less/common/sideNav.less
+++ b/framework/core/less/common/sideNav.less
@@ -18,8 +18,9 @@
& .Dropdown-menu {
& > li > a {
- padding: 8px 0 8px 30px;
color: var(--sidenav-color, var(--muted-color));
+ gap: 15px;
+ padding-left: 0;
&:hover {
background: none;
@@ -28,8 +29,6 @@
}
& .Button-icon {
- float: left;
- margin-left: -30px;
font-size: 15px;
}
}
@@ -58,14 +57,11 @@
}
.sideNav {
flex-shrink: 0;
- margin-right: 50px;
&, > ul {
width: 190px;
}
> ul {
- margin-top: 30px;
-
&.affix {
top: var(--header-height);
}
@@ -101,11 +97,8 @@
}
.Dropdown--select .Dropdown-menu > li > a {
- padding-left: 25px;
-
- .icon {
- margin-left: -25px;
- }
+ gap: 8px;
+ padding-right: 0;
}
.affix {
diff --git a/framework/core/less/forum.less b/framework/core/less/forum.less
index fa4da3148b..0e09310387 100644
--- a/framework/core/less/forum.less
+++ b/framework/core/less/forum.less
@@ -1,19 +1,21 @@
@import "common/common";
+@import "forum/PageStructure";
@import "forum/ActivityPage";
@import "forum/AvatarEditor";
@import "forum/Composer";
@import "forum/DiscussionHero";
@import "forum/DiscussionList";
@import "forum/DiscussionListItem";
+@import "forum/DiscussionListPane";
@import "forum/DiscussionPage";
@import "forum/Hero";
@import "forum/IndexPage";
@import "forum/LogInButton";
@import "forum/LogInModal";
@import "forum/NotificationGrid";
-@import "forum/NotificationList";
-@import "forum/NotificationsDropdown";
+@import "forum/HeaderDropdown";
+@import "forum/HeaderList";
@import "forum/Post";
@import "forum/PostStream";
@import "forum/Scrubber";
diff --git a/framework/core/less/forum/AvatarEditor.less b/framework/core/less/forum/AvatarEditor.less
index cbd2e5746b..af78b1a3a5 100644
--- a/framework/core/less/forum/AvatarEditor.less
+++ b/framework/core/less/forum/AvatarEditor.less
@@ -1,13 +1,18 @@
.AvatarEditor {
position: relative;
+ .Dropdown-toggle {
+ margin: 4px;
+ font-size: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
.Dropdown-toggle {
opacity: 0;
position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
+ inset: 0;
border-radius: 100%;
background: rgba(0, 0, 0, 0.6);
text-align: center;
@@ -26,10 +31,7 @@
.LoadingIndicator-container {
color: #fff;
position: absolute;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
+ inset: 0;
}
@media @tablet-up {
.Dropdown-menu {
diff --git a/framework/core/less/forum/DiscussionListItem.less b/framework/core/less/forum/DiscussionListItem.less
index 2fc02ca607..dd10a7e8ff 100644
--- a/framework/core/less/forum/DiscussionListItem.less
+++ b/framework/core/less/forum/DiscussionListItem.less
@@ -15,33 +15,35 @@
.DiscussionListItem-content {
position: relative;
color: var(--muted-color);
+ display: grid;
+ grid-template-columns: auto minmax(50%, 1fr) auto;
+ gap: 16px;
+ padding: 12px 26px 12px 0;
}
.DiscussionListItem-main {
color: inherit;
text-decoration: none;
+ display: block;
}
-.DiscussionListItem-author {
- float: left;
- margin-top: 13px;
+.DiscussionListItem-author-avatar {
+ display: block;
}
.DiscussionListItem-badges {
- float: left;
margin-top: 10px;
text-align: right;
white-space: nowrap;
pointer-events: none;
+ position: absolute;
+ top: 0;
+ left: -2px;
+
.Badge {
margin-left: -10px;
position: relative;
pointer-events: auto;
}
}
-.DiscussionListItem-main {
- display: inline-block;
- width: 100%;
- padding: 12px 0;
-}
.DiscussionListItem-title {
margin: 0 0 3px;
line-height: 1.3;
@@ -89,9 +91,6 @@
display: block;
word-break: break-word;
- .DiscussionPage-list & {
- margin-right: 0;
- }
mark {
background: none;
box-shadow: none;
@@ -100,10 +99,16 @@
}
}
}
-.DiscussionListItem-count {
- float: right;
- margin-top: 12px;
+.DiscussionListItem-stats {
+ width: 40px;
+ display: flex;
+ flex-direction: column;
+}
+.DiscussionListItem-stats-item {
text-decoration: none;
+ display: flex;
+ align-items: center;
+ gap: 6px;
.unread & {
cursor: pointer;
@@ -121,23 +126,21 @@
display: none;
}
.DiscussionListItem-content {
- padding-left: 15px + 45px;
- padding-right: 15px + 35px;
+ padding-left: 15px;
&:active {
background: var(--control-bg);
}
}
- .DiscussionListItem-author {
- margin-left: -45px;
-
+ .DiscussionListItem-author-avatar {
.Avatar {
.Avatar--size(32px);
}
}
.DiscussionListItem-badges {
- margin-left: -45px;
width: 38px;
+ left: 19px;
+ top: -4px;
.badge {
.Badge--size(20px);
@@ -145,7 +148,7 @@
}
}
.DiscussionListItem-main {
- margin-right: -45px;
+ //
}
.DiscussionListItem-title {
font-size: 14px;
@@ -156,13 +159,16 @@
overflow: hidden;
text-overflow: ellipsis;
}
+ .DiscussionListItem-stats {
+ align-items: end;
+ }
.DiscussionListItem-count {
- margin-right: -35px;
background: var(--control-bg);
color: var(--control-color);
border-radius: var(--border-radius);
font-size: 12px;
padding: 2px 6px;
+ gap: 0;
.unread & {
background: var(--primary-color);
@@ -173,6 +179,10 @@
opacity: 0.5;
}
}
+
+ .icon {
+ display: none;
+ }
}
}
@@ -225,52 +235,42 @@
}
}
.DiscussionListItem-content {
- padding-left: 52px;
- padding-right: 80px;
+ //
}
- .DiscussionListItem-author {
- margin-left: -52px;
-
+ .DiscussionListItem-author-avatar {
.Avatar {
.Avatar--size(36px);
}
}
.DiscussionListItem-badges {
- margin-left: -55px;
width: 48px;
}
.DiscussionListItem-main {
- margin-right: -65px;
+ //
}
.DiscussionListItem-title {
font-size: 16px;
}
- .DiscussionListItem-count {
- margin-top: 12px;
- margin-right: -70px;
- width: 55px;
+ .DiscussionListItem-stats-item {
color: var(--muted-color);
font-size: 14px;
- padding-left: 21px;
-
- &:before {
- .far();
- content: @fa-var-comment;
- float: left;
- margin-left: -21px;
- margin-top: 3px;
- }
+ }
+ .DiscussionListItem-count {
.unread & {
color: var(--heading-color);
font-weight: bold;
- &:before {
- .fas();
- content: @fa-var-comment;
+ ._checkmark {
+ display: none;
}
- &:hover:before {
- .fas();
- content: @fa-var-check;
+
+ &:hover {
+ ._checkmark {
+ display: block;
+ }
+ ._comment {
+ display: none;
+ }
}
}
}
diff --git a/framework/core/less/forum/DiscussionListPane.less b/framework/core/less/forum/DiscussionListPane.less
new file mode 100644
index 0000000000..c10c79d727
--- /dev/null
+++ b/framework/core/less/forum/DiscussionListPane.less
@@ -0,0 +1,87 @@
+@media @phone {
+ .DiscussionListPane {
+ display: none;
+ }
+}
+
+@media @tablet-up {
+ .DiscussionListPane {
+ left: calc(~"-6px - var(--pane-width)");
+ position: absolute;
+ z-index: var(--zindex-pane);
+ overflow: auto;
+ top: var(--header-height);
+ height: ~"calc(100vh - var(--header-height))";
+ width: var(--pane-width);
+ background: var(--body-bg);
+ padding-bottom: 40px;
+ border-top: 1px solid var(--control-bg);
+ box-shadow: 0 6px 6px var(--shadow-color);
+ transition: left 0.2s;
+
+ .affix & {
+ position: fixed;
+ bottom: 0;
+ height: auto;
+ }
+ .paneShowing & {
+ left: 0;
+ }
+ .DiscussionListItem {
+ margin: 0;
+ padding: 0;
+ border-radius: 0;
+
+ &.active {
+ background: var(--control-bg);
+ }
+ }
+ .DiscussionListItem-content {
+ padding-left: 16px;
+ padding-right: 28px;
+ }
+ .DiscussionListItem-title {
+ font-size: 14px;
+ }
+ .DiscussionListItem-info {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ .DiscussionListItem-badges {
+ left: 12px;
+ }
+ }
+}
+
+@media @desktop-hd {
+ .DiscussionListPane {
+ .panePinned & {
+ left: 0;
+ transition: none;
+ }
+ }
+ // When the pane is pinned, move the other page content inwards
+ .App-content, .App-footer {
+ .hasPane.panePinned & {
+ margin-left: var(--pane-width);
+
+ .container {
+ max-width: 100%;
+ padding-left: 30px;
+ padding-right: 30px;
+ }
+ }
+ }
+ .App-header .container {
+ transition: width 0.2s;
+ }
+}
+
+.DiscussionListPane {
+ .DiscussionListItem-info {
+ .item-excerpt {
+ margin-right: 0;
+ }
+ }
+}
diff --git a/framework/core/less/forum/DiscussionPage.less b/framework/core/less/forum/DiscussionPage.less
index 5aee15228d..f4626d786c 100644
--- a/framework/core/less/forum/DiscussionPage.less
+++ b/framework/core/less/forum/DiscussionPage.less
@@ -23,15 +23,23 @@
}
}
@media @tablet-up {
- .DiscussionPage-discussion {
- > .container {
- display: grid;
- grid-gap: 75px;
- grid-template-columns: 1fr 150px;
- grid-template-areas: 'stream nav';
+ .DiscussionPage {
+ --sidebar-width: 150px;
+ --gap: 55px;
- &::before, &::after {
- content: none;
+ .Page {
+ .Page--cols();
+
+ &-container {
+ flex-direction: row-reverse;
+ }
+
+ &-content {
+ margin-top: 10px;
+ }
+
+ &-sidebar {
+ margin-top: 0;
}
}
}
@@ -39,7 +47,6 @@
.DiscussionPage-nav {
align-self: start;
position: sticky;
- grid-area: nav;
top: var(--header-height);
padding-top: 32px;
z-index: 1;
@@ -49,11 +56,11 @@
margin-bottom: 10px;
}
}
- .ButtonGroup, .Button {
+ .ButtonGroup, :not(.ButtonGroup) > .Button {
width: 100%;
}
.ButtonGroup:not(.itemCount1) {
- .Button:first-child {
+ .Button:first-of-type {
width: 77%;
}
.Dropdown-toggle {
@@ -61,104 +68,4 @@
}
}
}
-
- .DiscussionPage-stream {
- grid-area: stream;
- max-width: 100%;
- min-width: 0;
- }
-}
-
-// ------------------------------------
-// Discussions Pane
-
-@media @phone {
- .DiscussionPage-list {
- display: none;
- }
-}
-
-@media @tablet-up {
- .DiscussionPage-list {
- left: calc(~"-6px - var(--pane-width)");
- position: absolute;
- z-index: var(--zindex-pane);
- overflow: auto;
- top: var(--header-height);
- height: ~"calc(100vh - var(--header-height))";
- width: var(--pane-width);
- background: var(--body-bg);
- padding-bottom: 40px;
- border-top: 1px solid var(--control-bg);
- box-shadow: 0 6px 6px var(--shadow-color);
- transition: left 0.2s;
-
- .affix & {
- position: fixed;
- bottom: 0;
- height: auto;
- }
- .paneShowing & {
- left: 0;
- }
- .DiscussionListItem {
- margin: 0;
- padding: 0;
- border-radius: 0;
-
- &.active {
- background: var(--control-bg);
- }
- }
- .DiscussionListItem-controls {
- top: 5px;
- }
- .DiscussionListItem-content {
- padding-left: 52px + 15px;
- padding-right: 65px + 15px;
- }
- .DiscussionListItem-title {
- font-size: 14px;
- }
- .DiscussionListItem-info {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .DiscussionListItem-relevantPosts {
- margin-left: -52px;
- margin-right: -65px;
- }
- .DiscussionListItem-count {
- margin-top: 11px;
- }
- }
-}
-
-@media @desktop-hd {
- .DiscussionPage-list {
- .panePinned & {
- left: 0;
- transition: none;
- }
- }
- // When the pane is pinned, move the other page content inwards
- .App-content, .App-footer {
- .hasPane.panePinned & {
- margin-left: var(--pane-width);
-
- .container {
- max-width: 100%;
- padding-left: 30px;
- padding-right: 30px;
- }
- }
- }
- .App-header .container {
- transition: width 0.2s;
-
- .hasPane.panePinned & {
- width: 100%;
- }
- }
}
diff --git a/framework/core/less/forum/NotificationsDropdown.less b/framework/core/less/forum/HeaderDropdown.less
similarity index 81%
rename from framework/core/less/forum/NotificationsDropdown.less
rename to framework/core/less/forum/HeaderDropdown.less
index 75dd1eb6d9..5859f56170 100644
--- a/framework/core/less/forum/NotificationsDropdown.less
+++ b/framework/core/less/forum/HeaderDropdown.less
@@ -1,8 +1,8 @@
-.NotificationsDropdown {
+.HeaderDropdown {
.Dropdown-menu {
padding: 0;
- .NotificationList-content {
+ .HeaderList-content {
max-height: ~"min(70vh, 800px)";
overflow: auto;
}
@@ -12,7 +12,7 @@
}
}
@media @tablet-up {
- .NotificationsDropdown {
+ .HeaderDropdown {
.Dropdown-menu {
width: 425px;
}
@@ -22,11 +22,11 @@
}
}
-.NotificationsDropdown .Dropdown-toggle.new .Button-icon {
+.HeaderDropdown .Dropdown-toggle.new .Button-icon {
color: var(--header-color);
}
-.NotificationsDropdown-unread {
+.HeaderDropdown-unread {
position: absolute;
top: 2px;
left: 18px;
diff --git a/framework/core/less/forum/NotificationList.less b/framework/core/less/forum/HeaderList.less
similarity index 82%
rename from framework/core/less/forum/NotificationList.less
rename to framework/core/less/forum/HeaderList.less
index 5c65287847..125135fa5b 100644
--- a/framework/core/less/forum/NotificationList.less
+++ b/framework/core/less/forum/HeaderList.less
@@ -1,4 +1,4 @@
-.NotificationList {
+.HeaderList {
overflow: hidden;
.App-primaryControl > button:not(:last-of-type) {
@@ -28,7 +28,7 @@
padding: 0;
text-decoration: none;
- // The NotificationList may be displayed inside of the drawer as a
+ // The HeaderList may be displayed inside of the drawer as a
// dropdown menu – but the drawer may have .light-contents() applied to
// it. In this case we will need to reset the button's styles back to
// normal.
@@ -59,7 +59,7 @@
}
}
-.NotificationGroup {
+.HeaderListGroup {
border-top: 1px solid var(--control-bg);
margin-top: -1px;
@@ -69,7 +69,6 @@
&-header {
font-weight: bold;
- color: var(--heading-color) !important;
padding: 8px 16px;
white-space: nowrap;
@@ -83,6 +82,9 @@
overflow: hidden;
text-overflow: ellipsis;
}
+ &, a {
+ color: var(--heading-color) !important;
+ }
}
&-badges {
@@ -105,7 +107,7 @@
}
}
-.Notification {
+.HeaderListItem {
padding: 8px 16px;
color: var(--muted-color) !important; // required to override .light-contents applied to header
overflow: hidden;
@@ -114,7 +116,7 @@
grid-template-columns: auto auto 1fr auto;
grid-template-areas:
- "avatar icon title button"
+ "avatar icon title actions"
"x x excerpt excerpt";
align-items: baseline;
@@ -134,7 +136,7 @@
text-decoration: none;
background: var(--control-bg);
- .Notification-action {
+ .HeaderListItem-actions > .Button {
opacity: 1;
}
}
@@ -175,37 +177,37 @@
}
}
- time {
- line-height: inherit;
+ &-time {
font-size: 11px;
line-height: 19px;
font-weight: bold;
text-transform: uppercase;
}
- &-action {
- line-height: inherit;
- padding: 0;
- opacity: 0;
+ &-actions {
+ grid-area: actions;
- .add-keyboard-focus-ring();
- .add-keyboard-focus-ring-offset(4px);
+ > .Button {
+ line-height: inherit;
+ padding: 0;
+ opacity: 0;
- grid-area: button;
+ .add-keyboard-focus-ring();
+ .add-keyboard-focus-ring-offset(4px);
- // Needs more specificity to fix hover/focus styles not applying in dropdown
- .Notification & when (@config-colored-header = true) {
- color: var(--control-color);
+ // Needs more specificity to fix hover/focus styles not applying in dropdown
+ .HeaderListItem & when (@config-colored-header = true) {
+ color: var(--control-color);
- &:hover,
- &:focus {
- color: var(--link-color);
+ &:hover,
+ &:focus {
+ color: var(--link-color);
+ }
}
- }
- .icon {
- font-size: 13px;
- margin-right: 0;
+ .icon {
+ font-size: 13px;
+ }
}
}
diff --git a/framework/core/less/forum/IndexPage.less b/framework/core/less/forum/IndexPage.less
index 01f2939b92..0affc68145 100644
--- a/framework/core/less/forum/IndexPage.less
+++ b/framework/core/less/forum/IndexPage.less
@@ -18,7 +18,6 @@
margin-bottom: 15px;
}
.IndexPage-toolbar-view, .IndexPage-toolbar-action {
- display: inline-block;
margin: 0;
list-style: none;
padding: 0;
@@ -27,13 +26,10 @@
display: inline-block;
}
}
-.IndexPage-toolbar-view > li {
- margin-right: 5px;
+.IndexPage-toolbar, .IndexPage-toolbar-view, .IndexPage-toolbar-action {
+ display: flex;
+ gap: 5px;
}
.IndexPage-toolbar-action {
- float: right;
-
- > li {
- margin-left: 5px;
- }
+ margin-inline-start: auto;
}
diff --git a/framework/core/less/forum/PageStructure.less b/framework/core/less/forum/PageStructure.less
new file mode 100644
index 0000000000..6b22dfe9fe
--- /dev/null
+++ b/framework/core/less/forum/PageStructure.less
@@ -0,0 +1,38 @@
+.Page {
+ --content-width: 100%;
+ --sidebar-width: 190px;
+ --gap: 50px;
+
+ &-container {
+ gap: var(--gap);
+ }
+
+ @media @desktop-up {
+ .Page--cols()
+ }
+
+ @media @phone, @tablet {
+ &-content {
+ margin-top: 15px;
+ }
+ }
+}
+
+.Page--cols {
+ &-container {
+ display: flex;
+ }
+
+ &-content {
+ width: var(--content-width);
+ }
+
+ &-sidebar {
+ width: var(--sidebar-width);
+ flex-shrink: 0;
+ }
+
+ &-content, &-sidebar {
+ margin-top: 30px;
+ }
+}
diff --git a/framework/core/less/forum/Post.less b/framework/core/less/forum/Post.less
index 29204901c9..264a1f3a98 100644
--- a/framework/core/less/forum/Post.less
+++ b/framework/core/less/forum/Post.less
@@ -2,8 +2,7 @@
// Posts
.Post {
- padding: 20px;
- margin: -1px -20px;
+ padding: var(--post-padding) var(--post-padding) var(--post-padding) 0;
transition: 0.2s box-shadow, top 0.2s, opacity 0.2s;
position: relative;
top: 0;
@@ -18,7 +17,6 @@
.Post-header {
margin-bottom: 15px;
- color: var(--muted-color);
&, a {
color: var(--muted-color);
@@ -33,6 +31,28 @@
margin-right: 10px;
}
}
+
+ .UserCard {
+ position: absolute;
+ top: 15px;
+ left: 16px;
+ z-index: var(--zindex-dropdown);
+ transition: opacity 0.2s, transform 0.2s;
+ transform: scale(0.95);
+ transform-origin: left top;
+ opacity: 0;
+
+ &.in {
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+
+ .Avatar {
+ .Avatar--size(32px);
+ vertical-align: middle;
+ margin-right: 5px;
+ }
}
.PostUser {
margin: 0;
@@ -40,13 +60,9 @@
font-weight: normal;
position: relative;
- // TODO: remove styles for h3 on Flarum 2.0 cleanup - they may be used by extensions, but they should target `.PostUser-name` class instead
- h3,
.PostUser-name {
display: inline;
}
- // TODO: remove styles for h3 on Flarum 2.0 cleanup - they may be used by extensions, but they should target `.PostUser-name` class instead
- h3, h3 a,
.PostUser-name, .PostUser-name a {
color: var(--heading-color);
font-weight: bold;
@@ -63,22 +79,6 @@
color: var(--online-user-circle-color);
}
}
-
- .UserCard {
- position: absolute;
- top: -10px;
- left: -100px;
- z-index: var(--zindex-dropdown);
- transition: opacity 0.2s, transform 0.2s;
- transform: scale(0.95);
- transform-origin: left top;
- opacity: 0;
-
- &.in {
- transform: scale(1);
- opacity: 1;
- }
- }
}
.PostUser-badges {
text-align: right;
@@ -195,8 +195,6 @@
.Post--hidden {
.Post-header, .Post-header a,
- // TODO: remove styles for h3 on Flarum 2.0 cleanup - they may be used by extensions, but they should target `.PostUser-name` class instead
- .PostUser h3, .PostUser h3 a,
.PostUser .PostUser-name, .PostUser .PostUser-name a {
color: var(--muted-more-color);
}
@@ -206,18 +204,14 @@
}
.Post-body,
.Post-footer,
- // TODO: remove styles for h3 on Flarum 2.0 cleanup - they may be used by extensions, but they should target `.PostUser-name` class instead
- h3 .Avatar,
- .PostUser-name .Avatar,
+ .Post-side > *,
.PostUser-badges {
display: none;
}
}
.Post-body,
.Post-footer,
- // TODO: remove styles for h3 on Flarum 2.0 cleanup - they may be used by extensions, but they should target `.PostUser-name` class instead
- h3 .Avatar,
- .PostUser-name .Avatar,
+ .Post-side > *,
.PostUser-badges {
opacity: 0.5;
}
@@ -375,11 +369,9 @@
}
@media @phone {
- .Post-header {
- .Avatar {
- .Avatar--size(32px);
- vertical-align: middle;
- margin-right: 5px;
+ .Post-side {
+ .Post-avatar {
+ display: none;
}
}
.PostUser-badges {
@@ -403,30 +395,29 @@
}
}
-@avatar-column-width: 85px;
-
@media @tablet-up {
- .Post {
- padding-left: 20px + @avatar-column-width;
+ .Post-container {
+ display: grid;
+ grid-template-columns: var(--avatar-column-width) 1fr;
}
.CommentPost:not(.Post--hidden), .ReplyPlaceholder {
min-height: 64px + 40px; // avatar height + padding
}
- .PostUser-avatar {
- left: -@avatar-column-width;
- position: absolute;
+ .Post-avatar {
.Avatar--size(64px);
}
+ .Post-header .Post-avatar {
+ display: none;
+ }
.PostUser-badges {
float: left;
position: relative;
- margin-left: -@avatar-column-width + 5px;
+ margin-left: calc(~"5px - var(--avatar-column-width)");
margin-top: -3px;
width: 64px;
}
.EventPost-icon {
text-align: right;
- margin-left: -@avatar-column-width;
width: 64px;
font-size: 22px;
}
@@ -437,17 +428,42 @@
cursor: text;
overflow: hidden;
margin-top: 50px;
+ margin-left: calc(0px - var(--post-padding));
+ padding-left: var(--post-padding);
border: 2px dashed var(--control-bg);
color: var(--muted-color);
border-radius: 10px;
background-color: transparent;
- width: calc(~"100% + 20px * 2");
- display: flex;
+ display: block;
+ appearance: none;
+ -webkit-appearance: none;
+ text-align: left;
+ width: calc(100% + var(--post-padding));
+
+ .Post-container {
+ display: grid;
+ grid-template-columns: var(--avatar-column-width) 1fr;
+
+ @media @phone {
+ grid-template-columns: auto 1fr;
+ gap: 10px;
+ }
+ }
.Post-header {
margin: 0;
color: inherit;
}
+
+ .Post-avatar {
+ .Avatar--size(32px);
+ display: block;
+ }
+
+ .Post-main {
+ display: flex;
+ align-items: center;
+ }
}
@media @tablet-up {
.ReplyPlaceholder {
@@ -455,15 +471,14 @@
transition: border-color 0.2s;
.Post-header {
- padding-top: 18px;
position: relative;
}
- .Avatar {
- margin-top: -18px;
- }
&:hover {
border-color: var(--control-bg);
}
+ .Post-avatar {
+ .Avatar--size(64px);
+ }
}
.LoadingPost .Post-header {
position: relative;
diff --git a/framework/core/less/forum/PostStream.less b/framework/core/less/forum/PostStream.less
index 59ffecdec4..afd8ce3a13 100644
--- a/framework/core/less/forum/PostStream.less
+++ b/framework/core/less/forum/PostStream.less
@@ -1,14 +1,16 @@
// ------------------------------------
// Stream
-.PostStream {
- @media @tablet-up {
- margin-top: 10px;
- }
-}
.PostStream-item {
&:not(:last-child) {
- border-bottom: 1px solid var(--control-bg);
+ &::after {
+ content: "";
+ display: block;
+ width: calc(~"100% - var(--post-padding)");
+ height: 1px;
+ background-color: var(--control-bg);
+ margin-right: auto;
+ }
@media @phone {
margin: 0 -15px;
@@ -48,7 +50,7 @@
text-transform: uppercase;
font-weight: bold;
color: var(--muted-color);
- padding: 20px 20px 20px @avatar-column-width;
+ padding: 20px 20px 20px calc(~"var(--avatar-column-width) + 20px");
font-size: 12px;
@media @phone {
diff --git a/framework/core/less/forum/SettingsPage.less b/framework/core/less/forum/SettingsPage.less
index 4598375e7a..cb0dfbdfed 100644
--- a/framework/core/less/forum/SettingsPage.less
+++ b/framework/core/less/forum/SettingsPage.less
@@ -20,10 +20,4 @@
}
}
}
-.Settings-account {
- li {
- display: inline-block;
- margin-right: 5px;
- }
-}
diff --git a/framework/core/less/forum/UserCard.less b/framework/core/less/forum/UserCard.less
index 6319cfa55f..18f101b20a 100644
--- a/framework/core/less/forum/UserCard.less
+++ b/framework/core/less/forum/UserCard.less
@@ -3,9 +3,11 @@
.light-contents();
background-size: 100% 100%;
}
+.UserCard .container {
+ display: grid;
+ grid-template-columns: 1fr auto;
+}
.UserCard-controls {
- float: right;
-
.Dropdown-menu {
left: auto;
right: 0;
@@ -26,15 +28,17 @@
font-size: 22px;
}
}
-
.UserCard-profile {
+ display: grid;
+ grid-template-columns: 104px 1fr;
+ gap: 26px;
text-align: left;
- padding-left: 130px;
- max-width: 800px;
+ padding-right: 20px;
@media @phone {
- padding-left: 0;
+ grid-template-columns: 1fr;
text-align: center;
+ padding-right: 0;
}
}
.UserCard-identity {
@@ -43,24 +47,12 @@
vertical-align: middle;
}
.UserCard-avatar {
- float: left;
- margin-left: -130px;
-
@media @phone {
display: block;
float: none;
margin: 0 auto 20px;
width: 64px + 8px;
}
- .Dropdown-toggle {
- margin: 4px;
- line-height: 96px;
- font-size: 26px;
-
- @media @phone {
- line-height: 64px;
- }
- }
.Avatar {
.Avatar--size(96px);
border: 4px solid #fff;
diff --git a/framework/core/less/forum/UserSecurityPage.less b/framework/core/less/forum/UserSecurityPage.less
index 8bad3158cc..8c3b4865b1 100644
--- a/framework/core/less/forum/UserSecurityPage.less
+++ b/framework/core/less/forum/UserSecurityPage.less
@@ -15,6 +15,7 @@
flex-direction: column;
border-radius: var(--border-radius);
overflow: hidden;
+ width: 100%;
&-item {
display: flex;
diff --git a/framework/core/views/frontend/forum.blade.php b/framework/core/views/frontend/forum.blade.php
index c0b0c1d17a..133c65b969 100644
--- a/framework/core/views/frontend/forum.blade.php
+++ b/framework/core/views/frontend/forum.blade.php
@@ -26,6 +26,8 @@
+
+
{!! $content !!}
@@ -37,6 +39,8 @@
+
+
{!! $forum['footerHtml'] !!}
diff --git a/js-packages/webpack-config/src/RegisterAsyncChunksPlugin.cjs b/js-packages/webpack-config/src/RegisterAsyncChunksPlugin.cjs
index f7337ac0b8..7f2bc144ff 100644
--- a/js-packages/webpack-config/src/RegisterAsyncChunksPlugin.cjs
+++ b/js-packages/webpack-config/src/RegisterAsyncChunksPlugin.cjs
@@ -20,86 +20,91 @@ class RegisterAsyncChunksPlugin {
alreadyOptimized = true;
const chunks = Array.from(compilation.chunks);
- const chunkModuleMemory = [];
-
- const modulesToCheck = [];
+ const chunkModuleMemory = {};
+ const modulesToCheck = {};
for (const chunk of chunks) {
for (const module of compilation.chunkGraph.getChunkModulesIterable(chunk)) {
+ modulesToCheck[chunk.id] = modulesToCheck[chunk.id] || [];
+
// A normal module.
if (module?.resource && module.resource.split(path.sep).includes('src') && module._source?._value.includes("webpackChunkName: ")) {
- modulesToCheck.push(module);
+ modulesToCheck[chunk.id].push(module);
}
// A ConcatenatedModule.
if (module?.modules) {
module.modules.forEach((module) => {
if (module.resource && module.resource.split(path.sep).includes('src') && module._source?._value.includes("webpackChunkName: ")) {
- modulesToCheck.push(module);
+ modulesToCheck[chunk.id].push(module);
}
});
}
}
}
- for (const module of modulesToCheck) {
- // If the module source has an async webpack chunk, add the chunk id to flarum.reg
- // at the end of the module source.
-
- const reg = [];
-
- // Each line that has a webpackChunkName comment.
- [...module._source._value.matchAll(/.*\/\* webpackChunkName: .* \*\/.*/gm)].forEach(([match]) => {
- [...match.matchAll(/(.*?) webpackChunkName: '([^']*)'.*? \*\/ '([^']+)'.*?/gm)]
- .forEach(([match, _, urlPath, importPath]) => {
- // Import path is relative to module.resource, so we need to resolve it
- const importPathResolved = path.resolve(path.dirname(module.resource), importPath);
- const thisComposerJson = require(path.resolve(process.cwd(), '../composer.json'));
- const namespace = extensionId(thisComposerJson.name);
-
- const chunkModules = (c) => Array.from(compilation.chunkGraph.getChunkModulesIterable(c));
-
- const relevantChunk = chunks.find(
- (chunk) => chunkModules(chunk)?.find(
- (module) => module.resource?.split('.')[0] === importPathResolved || module.rootModule?.resource?.split('.')[0] === importPathResolved
- )
- );
-
- if (! relevantChunk) {
- console.error(`Could not find chunk for ${importPathResolved}`);
- return match;
- }
-
- let concatenatedModule = chunkModules(relevantChunk)[0];
- const moduleId = compilation.chunkGraph.getModuleId(concatenatedModule);
- const registrableModulesUrlPaths = new Map();
- registrableModulesUrlPaths.set(urlPath, [relevantChunk.id, moduleId, namespace, urlPath]);
-
- if (concatenatedModule?.rootModule) {
- // This is a chunk with many modules, we need to register all of them.
- concatenatedModule.modules?.forEach((module) => {
- // The path right after the src/ directory, without the extension.
- const regPathSep = `\\${path.sep}`;
- const urlPath = module.resource.replace(`/.*${regPathSep}src(.*)${regPathSep}\..*/`, '$1');
-
- if (! registrableModulesUrlPaths.has(urlPath)) {
- registrableModulesUrlPaths.set(urlPath, [relevantChunk.id, moduleId, namespace, urlPath]);
- }
- });
- }
-
- registrableModulesUrlPaths.forEach(([chunkId, moduleId, namespace, urlPath]) => {
- if (! chunkModuleMemory.includes(urlPath)) {
- reg.push(`flarum.reg.addChunkModule('${chunkId}', '${moduleId}', '${namespace}', '${urlPath}');`);
- chunkModuleMemory.push(urlPath);
- }
+ for (const sourceChunkId in modulesToCheck) {
+ for (const module of modulesToCheck[sourceChunkId]) {
+ // If the module source has an async webpack chunk, add the chunk id to flarum.reg
+ // at the end of the module source.
+
+ const reg = [];
+
+ // Each line that has a webpackChunkName comment.
+ [...module._source._value.matchAll(/.*\/\* webpackChunkName: .* \*\/.*/gm)].forEach(([match]) => {
+ [...match.matchAll(/(.*?) webpackChunkName: '([^']*)'.*? \*\/ '([^']+)'.*?/gm)]
+ .forEach(([match, _, urlPath, importPath]) => {
+ // Import path is relative to module.resource, so we need to resolve it
+ const importPathResolved = path.resolve(path.dirname(module.resource), importPath);
+ const thisComposerJson = require(path.resolve(process.cwd(), '../composer.json'));
+ const namespace = extensionId(thisComposerJson.name);
+
+ const chunkModules = (c) => Array.from(compilation.chunkGraph.getChunkModulesIterable(c));
+
+ const relevantChunk = chunks.find(
+ (chunk) => chunkModules(chunk)?.find(
+ (module) => module.resource?.split('.')[0] === importPathResolved || module.rootModule?.resource?.split('.')[0] === importPathResolved
+ )
+ );
+
+ if (! relevantChunk) {
+ console.error(`Could not find chunk for ${importPathResolved}`);
+ return match;
+ }
+
+ let concatenatedModule = chunkModules(relevantChunk)[0];
+ const moduleId = compilation.chunkGraph.getModuleId(concatenatedModule);
+ const registrableModulesUrlPaths = new Map();
+ registrableModulesUrlPaths.set(urlPath, [relevantChunk.id, moduleId, namespace, urlPath]);
+
+ if (concatenatedModule?.rootModule) {
+ // This is a chunk with many modules, we need to register all of them.
+ concatenatedModule.modules?.forEach((module) => {
+ // The path right after the src/ directory, without the extension.
+ const regPathSep = `\\${path.sep}`;
+ const urlPath = module.resource.replace(`/.*${regPathSep}src(.*)${regPathSep}\..*/`, '$1');
+
+ if (! registrableModulesUrlPaths.has(urlPath)) {
+ registrableModulesUrlPaths.set(urlPath, [relevantChunk.id, moduleId, namespace, urlPath]);
+ }
+ });
+ }
+
+ registrableModulesUrlPaths.forEach(([chunkId, moduleId, namespace, urlPath]) => {
+ chunkModuleMemory[sourceChunkId] = chunkModuleMemory[sourceChunkId] || [];
+
+ if (! chunkModuleMemory[sourceChunkId].includes(urlPath)) {
+ reg.push(`flarum.reg.addChunkModule('${chunkId}', '${moduleId}', '${namespace}', '${urlPath}');`);
+ chunkModuleMemory[sourceChunkId].push(urlPath);
+ }
+ });
+
+ return match;
+ });
});
- return match;
- });
- });
-
- module._source._value += reg.join('\n');
+ module._source._value += reg.join('\n');
+ }
}
}
);