Skip to content

Commit a1c56d4

Browse files
committed
refactor: mobile tab item definitions and components
1 parent c25cd1c commit a1c56d4

11 files changed

Lines changed: 213 additions & 70 deletions
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import app from 'flarum/admin/app';
21
import MobileTabItemsRegistry from '../common/MobileTabItemsRegistry';
3-
import MobileTabItem from '../common/components/MobileTabItem';
42

53
export default class MobileTabItemsRegistryAdmin extends MobileTabItemsRegistry {
64
items() {
7-
const items = super.items();
8-
9-
items.add('session', <MobileTabItem icon="fas fa-circle-user">{app.translator.trans('acpl-mobile-tab.admin.item.session')}</MobileTabItem>);
10-
11-
return items;
5+
return super.items();
126
}
137
}

js/src/admin/components/MobileTabSettingsPage.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import app from 'flarum/admin/app';
22
import ExtensionPage, { ExtensionPageAttrs } from 'flarum/admin/components/ExtensionPage';
33
import FormSection from 'flarum/admin/components/FormSection';
44
import FormSectionGroup from 'flarum/admin/components/FormSectionGroup';
5-
import listItems from 'flarum/common/helpers/listItems';
5+
import Button from 'flarum/common/components/Button';
66
import ItemList from 'flarum/common/utils/ItemList';
77
import { Children, VnodeDOM } from 'mithril';
88
import Sortable from 'sortablejs';
9-
import { MobileTabRegistryItem } from '../../common/types';
9+
import { MobileTabItemDefinition } from '../../common/types';
1010
import MobileTabItemsRegistryAdmin from '../MobileTabItemsRegistryAdmin';
1111

1212
export default class MobileTabSettingsPage extends ExtensionPage {
@@ -52,7 +52,17 @@ export default class MobileTabSettingsPage extends ExtensionPage {
5252
availableItemsContent(): Children {
5353
return (
5454
<FormSection className="MobileTabAvailableItems" label={app.translator.trans('acpl-mobile-tab.admin.available_items')}>
55-
<ul className="MobileTabAvailableItems-list MobileTab-items">{listItems(this.availableItems().toArray())}</ul>
55+
<ul className="MobileTabAvailableItems-list MobileTab-items">
56+
{this.availableItems()
57+
.toArray()
58+
.map((item) => (
59+
<li className={`item-${item.itemName}`} key={item.key}>
60+
<Button className="Button MobileTab-item" icon={item.icon}>
61+
{item.label}
62+
</Button>
63+
</li>
64+
))}
65+
</ul>
5666
</FormSection>
5767
);
5868
}
@@ -61,7 +71,17 @@ export default class MobileTabSettingsPage extends ExtensionPage {
6171
return (
6272
<FormSection label={app.translator.trans('acpl-mobile-tab.admin.active_items')}>
6373
<nav className="MobileTab MobileTabPreview">
64-
<ul className="MobileTab-items MobileTabPreview-items">{listItems(this.enabledItems().toArray())}</ul>
74+
<ul className="MobileTab-items MobileTabPreview-items">
75+
{this.enabledItems()
76+
.toArray()
77+
.map((item) => (
78+
<li className={`item-${item.itemName}`} key={item.key}>
79+
<Button className="Button MobileTab-item" icon={item.icon}>
80+
{item.label}
81+
</Button>
82+
</li>
83+
))}
84+
</ul>
6585
</nav>
6686
</FormSection>
6787
);
@@ -79,7 +99,7 @@ export default class MobileTabSettingsPage extends ExtensionPage {
7999

80100
enabledItems() {
81101
const registeredItems = new MobileTabItemsRegistryAdmin().items();
82-
const enabledItems = new ItemList<MobileTabRegistryItem>();
102+
const enabledItems = new ItemList<MobileTabItemDefinition>();
83103

84104
this.activeKeys.forEach((key: string) => {
85105
if (registeredItems.has(key)) {
@@ -108,10 +128,15 @@ export default class MobileTabSettingsPage extends ExtensionPage {
108128
group: this.sortableKey,
109129
animation: 120,
110130
onAdd: (event) => {
131+
if (event.newIndex == null) return;
132+
111133
const key = this.getSortableItemKey(event);
112134
if (!key) return;
135+
113136
const activeKeys = [...this.activeKeys];
114-
this.activeKeys = [...activeKeys, key];
137+
activeKeys.splice(event.newIndex, 0, key);
138+
139+
this.activeKeys = activeKeys;
115140
this.forcedRefreshKey++;
116141
m.redraw();
117142
},

js/src/common/MobileTabItemsRegistry.tsx

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,63 @@
11
import app from 'flarum/common/app';
22
import ItemList from 'flarum/common/utils/ItemList';
3-
import MobileTabItem from './components/MobileTabItem';
4-
import { MobileTabRegistryItem } from './types';
3+
import ForumNotificationsTabItem from '../forum/components/ForumNotificationsTabItem';
4+
import { MobileTabItemDefinition } from './types';
55

66
export default class MobileTabItemsRegistry {
77
items() {
8-
const itemList = new ItemList<MobileTabRegistryItem>();
9-
10-
itemList.add(
11-
'home',
12-
<MobileTabItem href={app.routes?.index?.path ?? '/'} icon="fas fa-home">
13-
{app.translator.trans('acpl-mobile-tab.lib.item.home')}
14-
</MobileTabItem>
15-
);
16-
17-
itemList.add(
18-
'all_discussions',
19-
<MobileTabItem href={app.routes?.index?.path ?? '/all'} icon="fas fa-comments">
20-
{app.translator.trans('acpl-mobile-tab.lib.item.all_discussions')}
21-
</MobileTabItem>
22-
);
23-
24-
if (app.session.user) {
25-
const unread = app.session.user.unreadNotificationCount();
26-
itemList.add(
27-
'notifications',
28-
<MobileTabItem href={app.routes?.notifications?.path ?? '/notifications'} icon="fas fa-bell">
29-
{unread ? <span className="Bubble">{unread}</span> : ''}
30-
{app.translator.trans('acpl-mobile-tab.lib.item.notifications')}
31-
</MobileTabItem>
32-
);
33-
}
8+
const itemList = new ItemList<MobileTabItemDefinition>();
9+
10+
itemList.add('home', {
11+
key: 'home',
12+
icon: 'fas fa-home',
13+
href: () => app.route('index'),
14+
label: app.translator.trans('acpl-mobile-tab.lib.item.home'),
15+
source: 'core',
16+
});
17+
18+
itemList.add('all_discussions', {
19+
key: 'all_discussions',
20+
icon: 'fas fa-comments',
21+
href: () => app.route('index'),
22+
label: app.translator.trans('acpl-mobile-tab.lib.item.all_discussions'),
23+
source: 'core',
24+
});
25+
26+
itemList.add('notifications', {
27+
key: 'notifications',
28+
icon: 'fas fa-bell',
29+
label: app.translator.trans('acpl-mobile-tab.lib.item.notifications'),
30+
canView: !!app.session.user,
31+
forumComponent: ForumNotificationsTabItem,
32+
source: 'core',
33+
});
34+
35+
itemList.add('session', {
36+
key: 'session',
37+
icon: 'fas fa-circle-user',
38+
label: app.translator.trans('acpl-mobile-tab.lib.item.session'),
39+
source: 'core',
40+
});
3441

3542
if ('flarum-tags' in flarum.extensions) {
36-
itemList.add(
37-
'tags',
38-
<MobileTabItem href={app.routes?.tags?.path ?? '/tags'} icon="fas fa-tags">
39-
{app.translator.trans('acpl-mobile-tab.lib.item.tags')}
40-
</MobileTabItem>
41-
);
43+
itemList.add('tags', {
44+
key: 'tags',
45+
icon: 'fas fa-tags',
46+
href: () => app.route('tags'),
47+
label: app.translator.trans('acpl-mobile-tab.lib.item.tags'),
48+
source: 'core',
49+
});
50+
}
51+
52+
if ('flarum-messages' in flarum.extensions) {
53+
itemList.add('messages', {
54+
key: 'messages',
55+
icon: 'fas fa-envelope',
56+
href: () => app.route('messages'),
57+
label: app.translator.trans('acpl-mobile-tab.lib.item.messages'),
58+
canView: !!app.session.user,
59+
source: 'core',
60+
});
4261
}
4362

4463
return itemList;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Component, { ComponentAttrs } from 'flarum/common/Component';
2+
import { MobileTabItemDefinition } from '../types';
3+
4+
export interface MobileTabComponentAttrs extends ComponentAttrs {
5+
definition: MobileTabItemDefinition;
6+
}
7+
8+
export default abstract class MobileTabComponent extends Component<MobileTabComponentAttrs> {}

js/src/common/types.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,39 @@
1+
import Component from 'flarum/common/Component';
12
import { Children } from 'mithril';
23

3-
export type MobileTabRegistryItem = Children;
4+
export interface MobileTabItemDefinition {
5+
/**
6+
* A unique key for the item.
7+
* @example 'my_extension_item'
8+
*/
9+
key: string;
10+
label: string | Children;
11+
/**
12+
* The FontAwesome icon class.
13+
* @example icon: 'fas fa-home'
14+
*/
15+
icon: string;
16+
priority?: number;
17+
/**
18+
* A boolean or a function to determine if the item should be displayed.
19+
* This is checked on the forum frontend before rendering.
20+
* Defaults to `true` if not provided.
21+
*/
22+
canView?: boolean | (() => boolean);
23+
/**
24+
* A custom component to render for this item on the forum fronted.
25+
*
26+
* If provided, this component will be rendered instead of the default `MobileTabItem`.
27+
* The component will receive all other properties of the `MobileTabItemDefinition` interface (key, label, icon, href, etc.) as its `attrs.definition`.
28+
*/
29+
forumComponent?: typeof Component<any> | Children;
30+
/**
31+
* The URL (or a function returning a URL).
32+
* This is only used if a custom `forumComponent` is not provided, as it's an attribute for the default `MobileTabItem` component.
33+
*/
34+
href?: string | (() => string);
35+
/**
36+
* The source of the item, used to distinguish core items from extension-added or user-created items (from admin settings).
37+
*/
38+
source?: 'core' | 'extension' | 'user';
39+
}

js/src/forum/MobileTabItemsRegistryForum.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
import Button from 'flarum/common/components/Button';
2-
import app from 'flarum/forum/app';
31
import MobileTabItemsRegistry from '../common/MobileTabItemsRegistry';
4-
import MobileTabSessionDropdown from './components/MobileTabSessionDropdown';
2+
import ForumMobileTabSessionItem from './components/ForumMobileTabSessionItem';
53

4+
/**
5+
* Extends the base `MobileTabItemsRegistry` to safely add forum-specific logic.
6+
*
7+
* Its primary purpose is to assign `forumComponent` handlers or add new items
8+
* that depend on components imported from the `flarum/forum` application (e.g. `SessionDropdown`).
9+
*
10+
* These forum-specific imports cannot be placed in `common/MobileTabItemsRegistry`
11+
* because that file is also loaded by the admin panel,
12+
* which would cause a crash as the imports are not available in the admin context.
13+
*/
614
export default class MobileTabItemsRegistryForum extends MobileTabItemsRegistry {
715
items() {
816
const items = super.items();
917

10-
if (app.session.user) {
11-
items.add('session', <MobileTabSessionDropdown />);
12-
} else {
13-
items.add(
14-
'session',
15-
<Button icon="fas fa-user" className="Button Button--link" onclick={() => app.modal.show(() => import('flarum/forum/components/LogInModal'))}>
16-
{app.translator.trans('acpl-mobile-tab.lib.item.log_in')}
17-
</Button>
18-
);
19-
}
18+
items.setContent('session', {
19+
...items.get('session'),
20+
forumComponent: ForumMobileTabSessionItem,
21+
});
2022

2123
return items;
2224
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import app from 'flarum/common/app';
2+
import Button from 'flarum/common/components/Button';
3+
import { Children } from 'mithril';
4+
import MobileTabComponent from '../../common/components/MobileTabComponent';
5+
import MobileTabSessionDropdown from './MobileTabSessionDropdown';
6+
7+
export default class ForumMobileTabSessionItem extends MobileTabComponent {
8+
view(): Children {
9+
console.log(this.attrs.definition);
10+
11+
if (app.session.user) {
12+
return <MobileTabSessionDropdown />;
13+
}
14+
15+
return (
16+
<Button
17+
icon={this.attrs.definition}
18+
className="Button Button--link"
19+
onclick={() => app.modal.show(() => import('flarum/forum/components/LogInModal'))}
20+
>
21+
{app.translator.trans('acpl-mobile-tab.lib.item.log_in')}
22+
</Button>
23+
);
24+
}
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import app from 'flarum/common/app';
2+
import { Children } from 'mithril';
3+
import MobileTabComponent from '../../common/components/MobileTabComponent';
4+
import MobileTabItem from '../../common/components/MobileTabItem';
5+
6+
export default class ForumNotificationsTabItem extends MobileTabComponent {
7+
view(): Children {
8+
if (!app.session.user) return;
9+
const unread = app.session.user.unreadNotificationCount();
10+
11+
const { icon, label } = this.attrs.definition;
12+
13+
return (
14+
<MobileTabItem href={app.route('notifications')} icon={icon}>
15+
{unread ? <span className="Bubble">{unread}</span> : ''}
16+
{label}
17+
</MobileTabItem>
18+
);
19+
}
20+
}

js/src/forum/components/MobileTab.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import type { ComponentAttrs } from 'flarum/common/Component';
22
import Component from 'flarum/common/Component';
3-
import listItems from 'flarum/common/helpers/listItems';
4-
import type { Children, Vnode } from 'mithril';
5-
63
import ItemList from 'flarum/common/utils/ItemList';
74
import app from 'flarum/forum/app';
5+
import type { Children, Vnode } from 'mithril';
6+
import MobileTabItem from '../../common/components/MobileTabItem';
7+
import { MobileTabItemDefinition } from '../../common/types';
88
import MobileTabItemsRegistryForum from '../MobileTabItemsRegistryForum';
99

1010
export default class MobileTab extends Component {
1111
view(vnode: Vnode<ComponentAttrs, this>): Children {
1212
return (
1313
<nav className="MobileTab">
14-
<ul className="MobileTab-items">{listItems(this.items().toArray())}</ul>
14+
<ul className="MobileTab-items">
15+
{this.items()
16+
.toArray()
17+
.map((item) => (
18+
<li key={item.itemName}>
19+
{item.forumComponent ? (
20+
m(item.forumComponent, { definition: item })
21+
) : (
22+
<MobileTabItem href={typeof item.href === 'function' ? item.href() : item.href} icon={item.icon}>
23+
{item.label}
24+
</MobileTabItem>
25+
)}
26+
</li>
27+
))}
28+
</ul>
1529
</nav>
1630
);
1731
}
@@ -20,7 +34,7 @@ export default class MobileTab extends Component {
2034
const registeredItems = new MobileTabItemsRegistryForum().items();
2135

2236
const settings = app.forum.attribute<string[]>('acplMobileTabItems');
23-
const items = new ItemList<Children>();
37+
const items = new ItemList<MobileTabItemDefinition>();
2438
settings.forEach((item: string) => {
2539
if (registeredItems.has(item)) {
2640
items.add(item, registeredItems.get(item));

js/src/forum/components/MobileTabSessionDropdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import app from 'flarum/common/app';
12
import Avatar from 'flarum/common/components/Avatar';
2-
import app from 'flarum/forum/app';
33
import SessionDropdown from 'flarum/forum/components/SessionDropdown';
44

55
export default class MobileTabSessionDropdown extends SessionDropdown {

0 commit comments

Comments
 (0)