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

Compare commits

..

313 Commits

Author SHA1 Message Date
Franz Liedke
dd17fa3342 Remove unnecessary change 2020-09-18 22:27:11 +02:00
Franz Liedke
db8755d5d9 Remove unrelated change 2020-09-18 22:27:11 +02:00
Franz Liedke
5c2a233b74 Remove unnecessary conditional
The filter(Boolean) covers this.
2020-09-18 22:27:10 +02:00
Alexander Skvortsov
4dd0361ccf Remove unnecessary class name 2020-09-18 22:27:10 +02:00
Alexander Skvortsov
ccd2b0ee0b Revert diff reduction change
Sometimes, items passed to listItems might be null. We still need that filter check
2020-09-18 22:27:10 +02:00
Alexander Skvortsov
b954e79ec8 Move data from vnode to vnode.state
`vnode.state` is persisted, `vnode` might not be
2020-09-18 22:27:09 +02:00
Franz Liedke
bdc64f98a5 I want my small diff back! 2020-09-18 22:27:09 +02:00
Franz Liedke
f9e5aa4193 Email confirmation alert: Simplify implementation
Now that we don't mess with component instances anymore, we can follow
normal Mithril patterns.
2020-09-18 22:27:09 +02:00
Franz Liedke
801220cc10 Fix type annotation 2020-09-18 22:27:08 +02:00
Franz Liedke
bd52aaaa61 Fragment: Update documentation 2020-09-18 22:27:08 +02:00
Franz Liedke
4a5d12626b Revert "Remove deprecated "BC Layers", since BC is broken anyway"
This reverts commit 37c2956efae828e4f25448f4fdff33a79ab0aace.
2020-09-18 22:27:08 +02:00
Franz Liedke
1e4a7f3282 props -> attrs in code 2020-09-18 22:27:07 +02:00
Franz Liedke
cbcb0429d0 props -> attrs in comments 2020-09-18 22:27:07 +02:00
Franz Liedke
14b12f8ae8 Revert "Fix Post-actions being on top of Post Controls Dropdown"
This reverts commit fa29937d9e4dd868198bc2b003320c8431afed6f.
2020-09-18 22:27:07 +02:00
Franz Liedke
37d7068569 Revert "Run npm audit fix"
This reverts commit 791fd306e999066180cb67337566e17fa5e4bb1b.
2020-09-18 22:27:07 +02:00
Franz Liedke
6e1a9193ac Revert "infrastructure: Run npm audit fix"
This reverts commit ff1c52dcf84a4408fdc156a140f8718b401e7d7f.
2020-09-18 22:27:06 +02:00
Franz Liedke
ea6aea409d TS: Merge shims and tsconfig for now
This makes the structure less confusing, as long as the two parts are
not needed.

Once we actually have decided where to move the shared parts (to share
them with bundled and community extensions), and which parts to share,
this can easily be extracted again.
2020-09-18 22:27:06 +02:00
Franz Liedke
d572200cfb TS: Document shims files 2020-09-18 22:27:06 +02:00
Franz Liedke
e10ebf0489 TS config / shims: Fix indentation / whitespace 2020-09-18 22:27:05 +02:00
Franz Liedke
b251ff0469 Extend documentation 2020-09-18 22:27:05 +02:00
Franz Liedke
4a0947db4b TS: Be more specific in type assertion
See https://stackoverflow.com/a/33404548.
2020-09-18 22:27:05 +02:00
Alexander Skvortsov
32767d6321 Fix typescript errors 2020-09-18 22:27:04 +02:00
Alexander Skvortsov
27017b7181 Register jQuery with shims 2020-09-18 22:27:04 +02:00
Franz Liedke
bdd30ceecc Add documentation, keep lifecycle methods on top 2020-09-18 22:27:04 +02:00
Franz Liedke
abc98645a0 Fix typo, reduce diff 2020-09-18 22:27:03 +02:00
Franz Liedke
5d538adc33 Tweak Mithril documentation link 2020-09-18 22:27:03 +02:00
Franz Liedke
6f11567f91 Update comment
Who cares about Mithril 0.2? ;)
2020-09-18 22:27:03 +02:00
Alexander Skvortsov
537e2690e8 Replace get first element from filter with find 2020-09-18 22:27:03 +02:00
Franz Liedke
0e715a8c40 Restore comment
I love small diffs.
2020-09-18 22:27:02 +02:00
Franz Liedke
383a7e559f DiscussionListPane: Inject state, extract method 2020-09-18 22:27:02 +02:00
Franz Liedke
aeaa9a4b73 Reuse ComposerPostPreview in ReplyPlaceholder 2020-09-18 22:27:02 +02:00
Franz Liedke
85f8fc52b7 ComposerPostPreview: Add scroll anchoring callback
This will be needed to replace the `ReplyPlaceholderPreview` component.
2020-09-18 22:27:01 +02:00
Franz Liedke
174f3aba90 ComposerPostPreview: Allow injecting CSS class 2020-09-18 22:27:01 +02:00
Franz Liedke
47ce93d2fd Stop rendering previews when composer is hidden
This brings this component in line with the `ReplyPlaceholder` one.
2020-09-18 22:27:01 +02:00
Franz Liedke
24935eacaf ComposerPostPreview: Inject entire composer 2020-09-18 22:27:00 +02:00
Franz Liedke
0f99e7c015 Rename post preview component again
I want to reuse it for the reply placeholder as well, and that will
require injecting a composer state instance, so this naming seems to
make more sense.
2020-09-18 22:27:00 +02:00
Franz Liedke
691ae85e50 TextFormatterPreview: Add documentation 2020-09-18 22:27:00 +02:00
Franz Liedke
fb83f8c59c TextFormatterPreview: Pass in content callback
This makes the component itself less dependent on global state, which
has benefits for testability and reusability.
2020-09-18 22:26:59 +02:00
Franz Liedke
a6e6c54972 TextFormatterPreview: Expose in compat 2020-09-18 22:26:59 +02:00
Franz Liedke
6b5bdb5c41 Rename CommentPostPreview -> TextFormatterPreview
The idea is to be a bit more precise as to what this component does.
Also, there's already the `PostPreview` component which is actually a
link *to* a preview. Very confusing.
2020-09-18 22:26:59 +02:00
Alexander Skvortsov
d7ef260c54 Run code to rebuild tooltip oncreate AND onupdate 2020-09-18 22:26:58 +02:00
Alexander Skvortsov
571ed8d8e5 Don't store stuff on DOM elements. 2020-09-18 22:26:58 +02:00
Franz Liedke
42ad490096 mapRoutes: Fix outdated link to Mithril docs 2020-09-18 22:26:58 +02:00
Franz Liedke
09bead3ba2 Extend / correct documentation of SubtreeRetainer 2020-09-18 22:26:58 +02:00
Franz Liedke
abb896d430 Extend documentation of withAttr 2020-09-18 22:26:57 +02:00
Alexander Skvortsov
6e7c86ac50 Format 2020-09-18 22:26:57 +02:00
Alexander Skvortsov
2673dd2ee3 Don't show an error in console on promise rejection when not logged in and trying to reply / start a discussion 2020-09-18 22:26:57 +02:00
Alexander Skvortsov
cbd9c8dd4f Add a workaround for the blank SelectDropdown issue.
Workaround explained in the code.
2020-09-18 22:26:56 +02:00
Alexander Skvortsov
29d995de45 Fix NotificationList storing jquery objects 2020-09-18 22:26:42 +02:00
Franz Liedke
a5b2768836 Revert "Use some for needsRebuild in SubtreeRetainer"
This reverts commit 5318bd456856407dd92f44583bed5c88f0813a80.

My bad, I did not realize there was a side effect (storing stuff in
this.data) when suggesting this change. With that, using some() is much
less useful - plus it probably causes a bug because it does not always
iterate over all items once the callback returns true.
2020-09-18 22:26:42 +02:00
Alexander Skvortsov
398951f282 Fix SubtreeRetainer docblock 2020-09-18 22:26:42 +02:00
Alexander Skvortsov
07f04c7ba7 Use some for needsRebuild in SubtreeRetainer 2020-09-18 22:26:42 +02:00
Alexander Skvortsov
c8d5ca51bb Document AdminApplication location fix 2020-09-18 22:26:41 +02:00
Alexander Skvortsov
60dbd3f26c Significantly increase documentation for Component and Fragment 2020-09-18 22:26:41 +02:00
Alexander Skvortsov
e7f6e37799 Store $scrollParent and $notifications in the component state so we don't need to reobtain them 3 separate times 2020-09-18 22:26:41 +02:00
Alexander Skvortsov
27ffeb204e Add AffixedSidebar to compat 2020-09-18 22:26:40 +02:00
Alexander Skvortsov
884a5cf3b9 Run npm audit fix 2020-09-18 22:26:40 +02:00
Alexander Skvortsov
e895ca738d Move setRouteWithForcedRefresh util to common, add it to compat 2020-09-18 22:26:40 +02:00
Alexander Skvortsov
f41aec1043 Replace stray m.lazyRedraw and m.deferred on Model.js 2020-09-18 22:26:09 +02:00
Alexander Skvortsov
4c73d76668 Fix SuperTextarea instantiation: provide a DOM element, not a jquery collection 2020-09-18 22:26:09 +02:00
Alexander Skvortsov
f67194484b Fix going from one user page directly to another 2020-09-18 22:26:08 +02:00
Alexander Skvortsov
c8f47d519d Use the current route when when applying changeSort instead of always going to index 2020-09-18 22:26:08 +02:00
Alexander Skvortsov
e9b267a33a Call onRoute before refreshParams, so we can use app.current.get('routeName') in discussion params 2020-09-18 22:26:08 +02:00
Alexander Skvortsov
554c72c6db IndexPage: change routeName on app.current 2020-09-18 22:26:07 +02:00
Alexander Skvortsov
fb2b0a1d3e Replace leftover m.redraw(true) with m.redraw.sync() 2020-09-18 22:26:07 +02:00
Alexander Skvortsov
2ac18a39ed Make routeName available to pages again 2020-09-18 22:26:07 +02:00
Alexander Skvortsov
6ed3cb56d4 Fix ellipses button on comment post
The subtree retainer should take revealContent into account, so that when revealContent is toggled, it redraws the post
2020-09-18 22:26:06 +02:00
Alexander Skvortsov
038744f092 Introduce setRouteWithForcedRefresh util 2020-09-18 22:26:06 +02:00
Alexander Skvortsov
2edbd4508a Remove unnecessary vnode argument 2020-09-18 22:26:06 +02:00
Alexander Skvortsov
a6d4658dff Simplify TextEditor by moving oncreate from the textarea vnode in view to the component itself, and removing the redundant vnode lifecycle hooks (since those can be applied on the component as well) 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
955c8121d3 Resolve import typo 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
fd2dcd38d6 format 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
520c7e7d0f Made texteditor actual textarea lifecycle methods extensible 2020-09-18 22:26:05 +02:00
Alexander Skvortsov
5ec9c52b04 formatting fix 2020-09-18 22:26:04 +02:00
Alexander Skvortsov
0341e64057 Make app.composer.show redraw synchronous, implement promises in reply, edit, and new discussion actions properly 2020-09-18 22:26:04 +02:00
Alexander Skvortsov
f1a480d3d7 Rename leftover props to attrs. ALthough this piece of code will probably be removed soon as per https://github.com/flarum/core/pull/2263 2020-09-18 22:25:42 +02:00
Alexander Skvortsov
aceac88013 Store bound handlers in this 2020-09-18 22:25:42 +02:00
Alexander Skvortsov
8cc9e18990 Fix undefined $scrollParent 2020-09-18 22:25:42 +02:00
Alexander Skvortsov
94d3bea53e Fix initAttrs being made static 2020-09-18 22:25:41 +02:00
Alexander Skvortsov
9e6cfcf05a Revert fix for opening modal from modal. Will be split to separate PR. 2020-09-18 22:25:41 +02:00
Alexander Skvortsov
b8abd2522e Add documentation for withAttr util 2020-09-18 22:25:41 +02:00
Alexander Skvortsov
376a00f24f make initAttrs static again 2020-09-18 22:25:40 +02:00
Alexander Skvortsov
43bfaa7400 Update SubtreeRetainer docblock 2020-09-18 22:25:40 +02:00
Alexander Skvortsov
7ba8a7122b Don't pass attrs to this.isProvided unnecessarily 2020-09-18 22:25:40 +02:00
Alexander Skvortsov
88bc291c86 Don't accept unnecessary vnode arguments in view 2020-09-18 22:25:39 +02:00
Alexander Skvortsov
e699ada1cc Don't pass attrs to Modal.content (we forgot to remove it, were experimenting with something else previously) 2020-09-18 22:25:39 +02:00
Alexander Skvortsov
2186584878 Fix m.route that should be m.route.set 2020-09-18 22:25:39 +02:00
Alexander Skvortsov
7beeae6269 typo fix 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
e0e3d6ecae provide vnode to onupdate where it was actually needed 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
f8bc58fd1a format 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
764f50f469 Re-implement UsersSearchSource highlight logic without overriding vnode.children 2020-09-18 22:25:38 +02:00
Alexander Skvortsov
e8485db484 Revert removal of "controls" postfix in UserControls 2020-09-18 22:25:37 +02:00
Alexander Skvortsov
f017d7afbe Fix method that should have been oncreate initially being made as onupdate 2020-09-18 22:25:37 +02:00
Alexander Skvortsov
44c1e91f05 Call logic from onupdate in oncreate where relevant 2020-09-18 22:25:37 +02:00
Alexander Skvortsov
4b05e0073a Don't accept unnecessary vnode argument where it isn't used 2020-09-18 22:25:36 +02:00
Alexander Skvortsov
3764abee51 format 2020-09-18 22:25:36 +02:00
Alexander Skvortsov
015cedb29d Create new objects with spread when modifying vnode children instead of directly setting children when possible. 2020-09-18 22:25:36 +02:00
Alexander Skvortsov
32e9fa1f0b format 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
2bdf0d7096 More consistent use of bidi (don't unecessarily define value 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
b889fa1bbf format 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
76c3494f9b Alert.js: move content definition back where it used to be 2020-09-18 22:25:35 +02:00
Alexander Skvortsov
6bcb76b914 Fix typo (and remove the whole unnecessary method) 2020-09-18 22:25:34 +02:00
Alexander Skvortsov
63984b43f9 Revert changes that have been separated out into https://github.com/flarum/core/pull/2262/files 2020-09-18 22:25:34 +02:00
Alexander Skvortsov
389bc59745 Formatting, docblock for Fragment 2020-09-18 22:25:18 +02:00
Alexander Skvortsov
2eb28ea396 Add fragment as parallel to component 2020-09-18 22:25:18 +02:00
Alexander Skvortsov
169b0fbd9b Fix modal backdrop not closing on app.modal.close() 2020-09-18 22:25:17 +02:00
Alexander Skvortsov
6b178b8204 Get rid of index filter route resolved (no longer necessary since filter route isn't a thing anymore) 2020-09-18 22:25:17 +02:00
Alexander Skvortsov
1d6e985107 Get rid of index.filter route 2020-09-18 22:24:27 +02:00
Alexander Skvortsov
f1fc0fecb7 Move title setting for index page to its own method so it can be used in reloads caused by route changes 2020-09-18 22:24:27 +02:00
David Sevilla Martin
94d8f7e726 remove unused attribute in DiscussionComposer input 2020-09-18 22:24:27 +02:00
David Sevilla Martin
b0891c42da create route resolver for /:filter (route overrides any others such as /tags, etc) 2020-09-18 22:24:26 +02:00
David Sevilla Martin
7d5bebb70a do not use custom route resolver solution for plain index page 2020-09-18 22:22:57 +02:00
David Sevilla Martin
b78db0268a update Component to error if 'tag' attribute is passed - messes with mithril 2020-09-18 22:22:56 +02:00
David Sevilla Martin
fcda092558 fix tsconfig.json 2020-09-18 22:22:56 +02:00
Alexander Skvortsov
f6dd87f72f Fix double-fade of posts in post stream by moving fadeIn to css 2020-09-18 22:22:56 +02:00
Alexander Skvortsov
d8d43d95e0 Remove unnecessary options=true, which breaks with patchMithril (used for a previous but now discarded patch) 2020-09-18 22:21:37 +02:00
David Sevilla Martin
51c7b17305 fix: add 'force' to UserPage sidebar buttons to refresh post/discussion lists 2020-09-18 22:21:37 +02:00
David Sevilla Martin
b592ceb199 update patchMithril to add 'force' attribute & not add endless browser history for same page 2020-09-18 22:21:36 +02:00
David Sevilla Martin
a083876b5f fix: DiscussionPage not updating when switching to a separate discussion (using pane) 2020-09-18 22:21:18 +02:00
David Sevilla Martin
b0cbe277c2 update IndexPage to clear discussions & refresh params using custom route solution 2020-09-18 22:21:18 +02:00
David Sevilla Martin
b1d948becc revert patchMithril changes for all links to re-call oninit 2020-09-18 22:21:18 +02:00
Alexander Skvortsov
a48568b17a Fix back button moving between posts in discussion 2020-09-18 22:21:17 +02:00
Alexander Skvortsov
e5cebd85ed update: admin/components/ExtensionsPage 2020-09-18 22:21:17 +02:00
Alexander Skvortsov
fbd5f6245b update: admin/components/AppearancePage 2020-09-18 22:21:17 +02:00
Alexander Skvortsov
3ce63bc035 update admin/components/SettingsModal 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
bac5e7c94c update: admin/components/UploadImageButton 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
97b0e61f61 update: admin/components/EditGroupModal 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
604989be72 update: admin/components/PermissionGrid 2020-09-18 22:21:16 +02:00
Alexander Skvortsov
f9b1dfe499 update: admin/components/PermissionDropdown 2020-09-18 22:21:15 +02:00
Alexander Skvortsov
f611a44a08 update: admin/components/SettingDropdown 2020-09-18 22:21:15 +02:00
Alexander Skvortsov
911f1fd5c9 update: admin/components/MailPage 2020-09-18 22:21:15 +02:00
Alexander Skvortsov
0087b956ef update: admin/components/BasicsPage
- Not tested: locale selector, homepage selector, display name driver selector
2020-09-18 22:21:14 +02:00
Alexander Skvortsov
99b119f1fa update: admin/utils/saveSettings 2020-09-18 22:21:14 +02:00
Alexander Skvortsov
6be37fd376 update: admin/routes 2020-09-18 22:21:14 +02:00
Alexander Skvortsov
46741f63fe update: admin/components/AdminLinkButton 2020-09-18 22:21:13 +02:00
Alexander Skvortsov
cec00c0dd6 update: admin/components/AdminNav 2020-09-18 22:21:13 +02:00
Alexander Skvortsov
5f2c9da2f5 update: admin/components/SessionDropdown 2020-09-18 22:21:13 +02:00
Alexander Skvortsov
92de05e911 update: admin/components/HeaderSecondary 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
68aa6e26da update: admin/AdminApplication 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
8b7fa012c7 Remove workaround for m.route.Link vnode.text issue, as a more global patch has been added to patchMithril 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
61231debd3 fix forum/components/DiscussionPage: page title not being updated when opening a discussion 2020-09-18 22:21:12 +02:00
Alexander Skvortsov
e771ec90c4 Remove patch for double oninit on DiscussionPage: we need to find and fix the underlying issue, or it will affect ALL links to posts/discussions 2020-09-18 22:21:11 +02:00
Alexander Skvortsov
ab85b49845 fix patchMithril: add Fix for m.route.Link with vnode.children not showing anything. 2020-09-18 22:21:11 +02:00
Alexander Skvortsov
04e5d5884f Patch DiscussionListItem to avoid double re-oninit when going to a discussion page 2020-09-18 22:21:11 +02:00
Alexander Skvortsov
ddc1141106 Fix empty error alerts. We might want to bring back the old order for alerts.show (children after attrs) 2020-09-18 22:21:10 +02:00
Alexander Skvortsov
a48cc19814 Remove unused config method from SessionDropdown 2020-09-18 22:21:10 +02:00
Alexander Skvortsov
6d18b700ec Rename config to oncreate for NotificationGrid 2020-09-18 22:21:10 +02:00
Alexander Skvortsov
b2bc427b3f fix: forum/components/ForgotPasswordModal (fix alert content) 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
1f94ffc842 cleanup: common/components/Navigation (remove unused config 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
3d91268493 Fix showing alert on user deletion (UserControls) 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
81fd986881 forum/components/Notification: Support external links 2020-09-18 22:21:09 +02:00
Alexander Skvortsov
d1b0030292 update: forum/utils/alertEmailConfirmation
- ResendButton has been expanded into a full(er) component to avoid storing component instances, ContainedAlert has been removed
2020-09-18 22:21:08 +02:00
Alexander Skvortsov
2c395a781c update: forum/components/NotificationsPage 2020-09-18 22:21:08 +02:00
Alexander Skvortsov
21861f231b Remove deprecated "BC Layers", since BC is broken anyway 2020-09-18 22:21:08 +02:00
Alexander Skvortsov
537f5e833e update: common/components/Select 2020-09-18 22:21:07 +02:00
Alexander Skvortsov
4d45aaa9ae update: forum/components/LoginButton 2020-09-18 22:21:07 +02:00
Alexander Skvortsov
529f8e5f32 SubtreeRetainer: change docblock "this.props" to "this.attrs" 2020-09-18 22:21:07 +02:00
Alexander Skvortsov
991d90bf4a LoginModal: change references from props to attrs 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
38fed603f8 Remove deprecated preferenceSaver from settingspage 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
9691a6ab92 SignupModal: rename props to attrs 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
e8e4b64d7d mapRoutes: remove obsolete setting of routeName attr on components 2020-09-18 22:21:06 +02:00
Alexander Skvortsov
35d76515d3 update: forum/components/TextEditorButton 2020-09-18 22:21:05 +02:00
Alexander Skvortsov
5d34124a02 replace config and href with route on Notification and NotificationList 2020-09-18 22:21:05 +02:00
Alexander Skvortsov
16a6f82e8f replace href and config with route on PostPreview (which is unused, also, update the component 2020-09-18 22:21:05 +02:00
Alexander Skvortsov
8a0c241a8e remove affixSidebar from compat.js 2020-09-18 22:21:04 +02:00
Alexander Skvortsov
a376c0e596 Rename UserPageSidebar to AffixedSidebar, make it a much more reusable component 2020-09-18 22:21:04 +02:00
Alexander Skvortsov
5ccf9d420e Remove affixSidebar util (now contained in UserPageSidebar component 2020-09-18 22:21:04 +02:00
Alexander Skvortsov
742f89f660 add: forum/components/UserPageSidebar
- extracted from UserPage, replaces affixSidebar util
2020-09-18 22:21:03 +02:00
Alexander Skvortsov
916cf4b546 fix: Composer height handle 2020-09-18 22:21:03 +02:00
Alexander Skvortsov
f127e67fd4 Update: EventPost, DiscussionRenamedPost 2020-09-18 22:21:03 +02:00
Alexander Skvortsov
3e79c3e3ff update: forum/components/EditUserModal 2020-09-18 22:21:02 +02:00
Alexander Skvortsov
0172dfd79c update: forum/components/ChangeEmailModal 2020-09-18 22:21:02 +02:00
Alexander Skvortsov
eb627544fa update:forum/components/ChangePasswordModal 2020-09-18 22:21:02 +02:00
Alexander Skvortsov
73c0a90da7 update: forum/components/RenameDiscussionModal 2020-09-18 22:21:01 +02:00
Alexander Skvortsov
ca0f8f2d72 Fix preview functionality of ReplyComposer and EditPostComposer
- options= true means that the init method of DiscussionPage will not be recalled (as per patchMithril
- DiscussionPage now caches the previous route, and compares it to the current one onupdate. If the route has change. We update this cached prevRoute in positionChanged to avoid unneessary goToNumber calls.
2020-09-18 22:21:01 +02:00
Alexander Skvortsov
090df13e7f fix: ComposerState (use m.redraw.sync() instead of m.redraw(true)) 2020-09-18 22:21:01 +02:00
Alexander Skvortsov
d1a1277f88 update: forum/components/EditPostComponent 2020-09-18 22:21:01 +02:00
Alexander Skvortsov
31b2ab1b2b update: forum/components/ReplyComposer 2020-09-18 22:21:00 +02:00
Alexander Skvortsov
2590073a50 fix: forum/components/TextEditor 2020-09-18 22:21:00 +02:00
Alexander Skvortsov
4402dc81ac wip fix: forum/components/DiscussionPage
- rename onunload to onremove. We are still unable to cancel the unload though.
2020-09-18 22:21:00 +02:00
Alexander Skvortsov
f93a255a2f fix: ConfirmDocumentUnload 2020-09-18 22:20:59 +02:00
Alexander Skvortsov
ddb0a9f1ce update/fix: forum/components/SplitDropdown 2020-09-18 22:20:59 +02:00
Alexander Skvortsov
f44caf1600 update: forum/components/PostStreamScrubber 2020-09-18 22:20:59 +02:00
Alexander Skvortsov
89b6847710 update: forum/components/PostStream 2020-09-18 22:20:58 +02:00
Alexander Skvortsov
2fb885175a update: forum/components/TextEditor 2020-09-18 22:20:04 +02:00
Alexander Skvortsov
7d9db2f4ae update: forum/components/CommentPost
- removed now irrelevant Mithril 0.1 workaround for ul jsx
- Move CommentPostPreview into its own class
2020-09-18 22:20:04 +02:00
Alexander Skvortsov
5d073941c9 formatting fix 2020-09-18 22:20:03 +02:00
Alexander Skvortsov
f664fa5be7 update: forum/utils/PostControls 2020-09-18 22:20:03 +02:00
Alexander Skvortsov
aa4b58d7aa update: forum/components/PostMeta 2020-09-18 22:20:03 +02:00
Alexander Skvortsov
3120eb6f63 update: forum/components/PostEdited 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
aac54a1d28 update: forum/components/DiscussionHero 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
0d0841d019 formatting fix 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
83d2dbd290 Fix: extend Component in DiscussionListPane 2020-09-18 22:20:02 +02:00
Alexander Skvortsov
7e5b40c532 Fix: extend Component in ReplyPlaceholderComponent 2020-09-18 22:20:01 +02:00
Alexander Skvortsov
6c9971eeba add: forum/components/CommentPostPreview
- This has been extracted from CommentPost
2020-09-18 22:20:01 +02:00
Alexander Skvortsov
37a690833a update: forum/components/Post
- attrs has been renamed to elementAttrs
- As subtree retention is now implemented via onbeforeupdate, view no longer needs to return a retain vnode, and as such, has been significantly simplified
2020-09-18 22:20:01 +02:00
Alexander Skvortsov
fa2301b5c1 update: forum/components/DiscussionPage
- The discussion list pane has been extracted to the DiscussionListPane component
2020-09-18 22:20:00 +02:00
Alexander Skvortsov
edca7b93ec add forum/components/DiscussionListPane
- Extract this from DiscussionPage
2020-09-18 22:20:00 +02:00
Alexander Skvortsov
095dce9a3e Mount AlertManager in Application 2020-09-18 22:20:00 +02:00
Alexander Skvortsov
479f655bb3 update: common/components/AlertManager 2020-09-18 22:19:59 +02:00
Alexander Skvortsov
41d6e91318 update: common/states/AlertManagerState
- Children is now the first argument of the alerts.show function, since it's no longer managed through attrs. It's also more relvant than attrs, which is why I put it first, but we can definitely reverse the two.
2020-09-18 22:19:59 +02:00
Alexander Skvortsov
87414995b6 fix: forum/components/UserSearchSource
- Use route attr instead of href and config
2020-09-18 22:19:59 +02:00
Alexander Skvortsov
2c93b5f801 update: forum/components/PostUser 2020-09-18 22:19:59 +02:00
Alexander Skvortsov
7498f5e506 fix: forum/components/UserCard
- Use route attr instead of href and config
2020-09-18 22:19:58 +02:00
Alexander Skvortsov
79f5291f04 update: forum/states/PostStreamState
- Change m.deferred to native promise
2020-09-18 22:19:58 +02:00
Alexander Skvortsov
ed3b923f58 update: forum/components/ReplyPlaceholder
- The preview has been extracted to ReplyPlaceholderPreview
2020-09-18 22:19:58 +02:00
Alexander Skvortsov
1ce06611ce add: Extract ReplyPlaceholderPreview from ReplyPlaceholder 2020-09-18 22:19:57 +02:00
Alexander Skvortsov
27bacd779b Update patchMithril modified link documentation with a better link to mithril's docs. 2020-09-18 22:19:57 +02:00
Alexander Skvortsov
02acacfdcb Merge branch 'mithril-2-update' of github.com:flarum/core into mithril-2-update 2020-09-18 22:19:57 +02:00
Alexander Skvortsov
23d95a7566 Add Pane to ForumApplication 2020-09-18 22:19:56 +02:00
Alexander Skvortsov
43164df79e formatting fix 2020-09-18 22:19:56 +02:00
Alexander Skvortsov
46e704b27b update: IndexPage (move onunload contents into onremove) 2020-09-18 22:19:56 +02:00
David Sevilla Martin
e55867acb4 format 2020-09-18 22:19:55 +02:00
Alexander Skvortsov
3596425bde update: History (re-init component even if already on index page) 2020-09-18 22:19:55 +02:00
David Sevilla Martin
b43452223f fix: Button not appearing disabled when loading 2020-09-18 22:19:55 +02:00
Alexander Skvortsov
70697be8c0 infrastructure: (mostly) force re-calling oninit when a route change is handled by the same component
- Due to mithril 2.0, setting a route will not re-call oninit if the component handling the route has not changed.
- Mithril allows us to provide an options parameter to m.route.set, which, if has the state.key parameter changed, will force a re-oninit. However, manually implementing this on every button and component is both tedious, and will make further changes in functionality difficult
- To that end, we can add in this patch here, which will take care of most cases. Code that explicitly calls m.route.set will still need to include this options parameter.
2020-09-18 22:19:55 +02:00
Alexander Skvortsov
c20ae678f5 Move config method of IndexPage into oncreate and onremove 2020-09-18 22:19:54 +02:00
Alexander Skvortsov
30a61b8b42 fix: forum/components/DiscussionListItem (use route instead of config) 2020-09-18 22:19:54 +02:00
Alexander Skvortsov
824fe95346 update: DisussionSearchSource (use route attr to support linking without refreshing) 2020-09-18 22:19:54 +02:00
David Sevilla Martin
8475d176e0 fix: forum/routes /:filter handling /settings 2020-09-18 22:19:53 +02:00
David Sevilla Martin
95f4dc771d update: forum/components/SettingsPage 2020-09-18 22:19:53 +02:00
David Sevilla Martin
68caf45f33 update: forum/components/NotificationGrid 2020-09-18 22:19:53 +02:00
David Sevilla Martin
f72d118bec cleanup: common/components/Button 2020-09-18 22:19:52 +02:00
David Sevilla Martin
652d961907 update: common/components/FieldSet 2020-09-18 22:19:52 +02:00
David Sevilla Martin
71178245fc update: common/components/Switch 2020-09-18 22:19:52 +02:00
David Sevilla Martin
cfd1f01299 update: common/components/Checkbox 2020-09-18 22:19:52 +02:00
David Sevilla Martin
674f55e91d fix: support text-only vnodes with extractText
Also seems to fix not having a space between 'Posts' and the number of posts in the UserPage tooltips
2020-09-18 22:19:51 +02:00
David Sevilla Martin
f897b58f29 add: common/utils/withAttr
Replaces m.withAttr
2020-09-18 22:19:51 +02:00
David Sevilla Martin
5c49b71c02 update: forum/utils/UserControls 2020-09-18 22:19:51 +02:00
David Sevilla Martin
b9ba5b63f1 update: forum/components/DiscussionsUserPage 2020-09-18 22:19:50 +02:00
David Sevilla Martin
6e88dfb2cb update: forum/components/PostsUserPage 2020-09-18 22:19:50 +02:00
David Sevilla Martin
c899e11070 update: forum/components/UserPage 2020-09-18 22:19:50 +02:00
David Sevilla Martin
784b5cc03c update: forum/components/AvatarEditor 2020-09-18 22:19:49 +02:00
David Sevilla Martin
c17d7cd23f fix: Application#updateTitle using m.route() instead of m.route.get() 2020-09-18 22:19:49 +02:00
David Sevilla Martin
4af34265cc update: common/components/GroupBadge 2020-09-18 22:19:49 +02:00
Alexander Skvortsov
1616d8f1c7 Fix patchMithril for route 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
57b85f501a Add composer mount to ForumApplication 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
91609b8a71 update: forum/components/DiscussionComposer 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
9d8b466d57 update: forum/states/ComposerState 2020-09-18 22:19:48 +02:00
Alexander Skvortsov
02154d05e5 update: forum/components/TextEditor 2020-09-18 22:19:47 +02:00
Alexander Skvortsov
a454b185e9 update forum/components/ComposerButton 2020-09-18 22:19:47 +02:00
Alexander Skvortsov
84c5248872 update: forum/components/ComposerBody 2020-09-18 22:19:47 +02:00
Alexander Skvortsov
2a784009fb update: Composer 2020-09-18 22:19:46 +02:00
Alexander Skvortsov
070865f825 update: IndexPage to fix newDiscussionAction (use promise instead of deferred) 2020-09-18 22:19:46 +02:00
Alexander Skvortsov
9615fd3e39 update: common/components/ConfirmDocumentUnload 2020-09-18 22:19:46 +02:00
Alexander Skvortsov
dde9c9c51b Mount navigation in ForumApplication 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
6547290472 update: common/components/Navigation 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
74f6a3e6ce Set empty object as default attrs for all components 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
98740472a8 update: common/Model (use body instead of data on m.request) 2020-09-18 22:19:45 +02:00
Alexander Skvortsov
1e9825de4f update: forum/components/WelcomeHero 2020-09-18 22:19:44 +02:00
Alexander Skvortsov
d14ca79dfa update: listItems (filter out null/falsy values) 2020-09-18 22:19:44 +02:00
Alexander Skvortsov
5cd5f27769 update: forum/components/HeaderSecondary (add search, finishing the header) 2020-09-18 22:19:44 +02:00
Alexander Skvortsov
d05b63eddd update: forum/states/GlobalSearchState 2020-09-18 22:19:43 +02:00
Alexander Skvortsov
48c7354c08 update: forum/components/Search 2020-09-18 22:19:43 +02:00
Alexander Skvortsov
0fe635d32c update: common/components/Badge 2020-09-18 22:19:43 +02:00
Alexander Skvortsov
2c2a42030a update: UsersSearchSource
- mithril 2 prefers to store single text children in vnode.text, so we need to move it back to children to give us a consistent API
2020-09-18 22:19:42 +02:00
Alexander Skvortsov
37d2902cd1 Fix m.route => m.route.set in NotificationsDropdown 2020-09-18 22:19:42 +02:00
Alexander Skvortsov
66c2f6b76a update: forum/components/DiscussionSearchSource 2020-09-18 22:19:42 +02:00
Alexander Skvortsov
4d68636544 update: forum/components/SessionDropdown 2020-09-18 22:19:41 +02:00
Alexander Skvortsov
73891b751b update: forum/components/Notification, forum/components/DiscussionRenamedNotification
- Not yet tested
2020-09-18 22:19:41 +02:00
Alexander Skvortsov
fe011bf285 Add notifications to HeaderSecondary 2020-09-18 22:19:41 +02:00
Alexander Skvortsov
524dde31d4 update: forum/components/NotificationList 2020-09-18 22:19:41 +02:00
Alexander Skvortsov
b69fc01ab3 Uncomment notifications in ForumApplication 2020-09-18 22:19:40 +02:00
Alexander Skvortsov
20e69e6351 update: NotificationsDropdown 2020-09-18 22:19:40 +02:00
Alexander Skvortsov
d097e7ed4b Remove stray console.log 2020-09-18 22:19:40 +02:00
Alexander Skvortsov
63e07b2044 update: forum/utils/DiscussionControls
- replyAction has not yet been tested.
2020-09-18 22:19:39 +02:00
Alexander Skvortsov
60c3f23667 update: forum/components/DiscussionListItem 2020-09-18 22:19:39 +02:00
Alexander Skvortsov
9793c10610 infrastructure: For SubtreeRetainer, return a boolean, not a vnode, as subtree retaining is now managed by onbeforeupdate 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
29af3e6d8b infrastructure: use params instead of data for get requests in common/Store 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
703c3442da update: forum/components/UserCard 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
e0d3a8c733 update: common/helpers/listItems: fix isSeparator 2020-09-18 22:18:32 +02:00
Alexander Skvortsov
527748ff66 update: forum/components/TerminalPost 2020-09-18 22:18:31 +02:00
Alexander Skvortsov
eb440bb9b6 update: forum/components/DiscussionList 2020-09-18 22:18:31 +02:00
Matthew Kilgore
d71f77d592 update: common/components/Placeholder.js 2020-09-18 22:18:31 +02:00
Matthew Kilgore
8b891abb2b update: forum/components/IndexPage.js 2020-09-18 22:18:30 +02:00
Matthew Kilgore
5f0dcc71ba update: common/components.Page.js 2020-09-18 22:18:30 +02:00
Matthew Kilgore
4fcafe3b2f Set application drawer 2020-09-18 22:18:30 +02:00
Matthew Kilgore
29065e1ee9 infrastructure: provide component classes instead of instances to routes.js 2020-09-18 22:18:29 +02:00
Matthew Kilgore
2b39bd7a0d Fix classList usage in listItems 2020-09-18 22:18:29 +02:00
Matthew Kilgore
94fe3236d7 infrastructure: Add routing to common/Application 2020-09-18 22:18:29 +02:00
Matthew Kilgore
3b6e5a0caf infrastructure: Add routes, history, search to ForumApplication 2020-09-18 22:18:29 +02:00
Matthew Kilgore
9ac8f1543a update: forum/utils/History.js 2020-09-18 22:18:28 +02:00
Matthew Kilgore
439e3a5a9a update: common/components/LinkButton 2020-09-18 22:18:28 +02:00
Matthew Kilgore
883e1a9d6a infrastruture: include the class name of the component where children is provided 2020-09-18 22:18:28 +02:00
Matthew Kilgore
2ac2edbbad Replace mithril-stream with mithril/stream, set it as a global via m.stream 2020-09-18 22:18:27 +02:00
Matthew Kilgore
18148141c3 update: common/components/RequestErrorModal 2020-09-18 22:18:27 +02:00
Matthew Kilgore
10f4223028 Add error warning if children attr is ever used 2020-09-18 22:18:27 +02:00
Matthew Kilgore
e10220ae47 update: Fix Alert on modals 2020-09-18 22:18:26 +02:00
Matthew Kilgore
dcd14821c2 infrastructure: revert to using this.attrs 2020-09-18 22:18:26 +02:00
Alexander Skvortsov
edeaa5855c update: SignUpModal 2020-09-18 22:18:26 +02:00
Alexander Skvortsov
cc91244f1a update: LoginModal 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
5faab8bdbd update: ForgotPasswordModal 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
99d79e7571 update: Modal 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
5606eae0f1 update: ModalManager 2020-09-18 22:18:25 +02:00
Alexander Skvortsov
62cb71d4e1 update: ModalManagerState 2020-09-18 22:18:24 +02:00
Alexander Skvortsov
e28ba4acff infrastructure: fix broken translation of strings with html tags (vnodes) passed into them
Under the old system, we would set the "childen" property of the vnode in the translation to an array containing the text between the tag as a raw string. Mithril 2 expects everything to be a vnode, and as such, errors when processing this raw string. This commit runs that "children" array of raw strings (and possibly other vnodes) through m.fragment to convert everything to vnodes.
2020-09-18 22:18:24 +02:00
Alexander Skvortsov
5b3914535d infrastructure: Replace m.deferred in Application with native promise functionality 2020-09-18 22:18:24 +02:00
Alexander Skvortsov
44975bc606 update: common/Session.js
- Use body instead of data
- Use template literal instead of concatenation
2020-09-18 22:18:23 +02:00
Alexander Skvortsov
f7931e8a30 infrastructure: uncomment modal manager mount in application mount 2020-09-18 22:18:23 +02:00
Alexander Skvortsov
3ba655b2f9 infrastructure: install mithril streams package 2020-09-18 22:18:23 +02:00
Alexander Skvortsov
735ecab446 infrastructure: Run npm audit fix 2020-09-18 22:18:22 +02:00
Matthew Kilgore
64d4eb8c4c update: forum/components/HeaderSecondary (no search, session, notifs) 2020-09-18 22:18:22 +02:00
Matthew Kilgore
e4f1a397d6 update: forum/components/HeaderPrimary 2020-09-18 22:18:22 +02:00
Matthew Kilgore
5a244dcfd2 update: common/components/SelectDropdown 2020-09-18 22:18:21 +02:00
Matthew Kilgore
b60309284c update: common/components/Dropdown 2020-09-18 22:18:21 +02:00
Matthew Kilgore
392fe98c02 comment routes and components and states in forum application 2020-09-18 22:18:21 +02:00
Matthew Kilgore
873f489fec update: common/helpers/listItems 2020-09-18 22:18:21 +02:00
Matthew Kilgore
9d8374208f update: common/components/Button 2020-09-18 22:18:20 +02:00
Matthew Kilgore
07551b4890 update: common/components/Button 2020-09-18 22:18:20 +02:00
Matthew Kilgore
b2c147c147 update: common/components/LoadingIndicator 2020-09-18 22:18:20 +02:00
Matthew Kilgore
5bf5bd36ee update: common/utils/patchMithril (remove unnecessary hacks)
Removed m.prop AND m.withAttr, no alternatives yet (m.prop is mithril/stream import)
2020-09-18 22:18:19 +02:00
Matthew Kilgore
4435ff193a update: common/Component (& converted a tad) 2020-09-18 22:18:19 +02:00
Matthew Kilgore
5c30f8fa67 Setup Mithril 2 2020-09-18 22:18:19 +02:00
190 changed files with 1023 additions and 3956 deletions

2
.gitattributes vendored
View File

@@ -11,5 +11,3 @@ phpunit.xml export-ignore
tests export-ignore
js/dist/* -diff
* text=auto eol=lf

View File

@@ -1,102 +1,5 @@
# Changelog
## [0.1.0-beta.14](https://github.com/flarum/core/compare/v0.1.0-beta.13...v0.1.0-beta.14)
### Added
- Check dependencies before enabling / disabling extensions (https://github.com/flarum/core/pull/2188)
- Set up temporary infrastructure for TypeScript in core (https://github.com/flarum/core/pull/2206)
- Better UI for request error modals (https://github.com/flarum/core/pull/1929)
- Display name extender, tests, frontend UI (https://github.com/flarum/core/pull/2174)
- Scroll to post or show alert when editing a post from another page (https://github.com/flarum/core/pull/2108)
- Feature to test email config by sending an email to the current user (https://github.com/flarum/core/pull/2023)
- Allow searching users by group ID using the group gambit (https://github.com/flarum/core/pull/2192)
- Use `liveHumanTimes` helper to update times without reload/rerender (https://github.com/flarum/core/pull/2208)
- View extender, tests (https://github.com/flarum/core/pull/2134)
- User extender to replace `PrepareUserGroups` (https://github.com/flarum/core/pull/2110)
- Increase extensibility of skeleton PHP (https://github.com/flarum/core/pull/2308, https://github.com/flarum/core/pull/2318)
- Pass a translator instance to `getEmailSubject` in `MailableInterface` (https://github.com/flarum/core/pull/2244)
- Force LF line endings on windows (https://github.com/flarum/core/pull/2321)
- Add a `Link` component for internal and external links (https://github.com/flarum/core/pull/2315)
- `ConfirmDocumentUnload` component
- Error handler middleware can now be manipulated by the middleware extender
### Changed
- Update to Mithril 2 (https://github.com/flarum/core/pull/2255)
- Stop storing component instances (https://github.com/flarum/core/issues/1821, https://github.com/flarum/core/issues/2144)
- Update to Laravel 6.x (https://github.com/flarum/core/issues/2055)
- `Flarum\Foundation\Application` no longer implements `Illuminate\Contracts\Foundation\Application` (#2142)
- `Flarum\Foundation\Application` no longer inherits `Illuminate\Container\Container` (#2142)
- `paths` have been split off from `Flarum\Foundation\Application` into `Flarum\Foundation\Paths`, which can be injected where needed (#2142)
- `Flarum\User\Gate` no longer implements `Illuminate\Contracts\Auth\Access\Gate` (https://github.com/flarum/core/pull/2181)
- Improve Group Gambit performance (https://github.com/flarum/core/pull/2192)
- Switch to `dayjs` from `momentjs` (https://github.com/flarum/core/pull/2219)
- Don't create a `bio` column in `users` for new installations (https://github.com/flarum/core/pull/2215)
- Start converting core JS to TypeScript (https://github.com/flarum/core/pull/2207)
- Make Carbon an explicit dependency (https://github.com/flarum/core/commit/3b39c212e0fef7522e7d541a9214ff3817138d5d)
- Use Symfony's translator interface instead of Laravel's (https://github.com/flarum/core/pull/2243)
- Use newer versions of fontawesome (https://github.com/flarum/core/pull/2274)
- Use URL generator instead of `app()->url()` where possible (https://github.com/flarum/core/pull/2302)
- Move config from `config.php` into an injectable helper class (https://github.com/flarum/core/pull/2271)
- Use reserved TLD for bogus and test urls (https://github.com/flarum/core/commit/6860b24b70bd04544dde90e537ce021a5fc5a689)
- Replace `m.stream` with `flarum/utils/Stream` (https://github.com/flarum/core/pull/2316)
- Replace `affixedSidebar` util with `AffixedSidebar` component
- Replace `m.withAttr` with `flarum/utils/withAttr`
- Scroll Listener is now passive, performance improvement (https://github.com/flarum/core/pull/2387)
### Fixed
- `generate:migration` command for extensions (https://github.com/flarum/core/commit/443949f7b9d7558dbc1e0994cb898cbac59bec87)
- Container config for `UninstalledSite` (https://github.com/flarum/core/commit/ecdce44d555dd36a365fd472b2916e677ef173cf)
- Tooltip glitch on page chang (https://github.com/flarum/core/issues/2118)
- Using multiple extenders in tests (https://github.com/flarum/core/commit/c4f4f218bf4b175a30880b807f9ccb1a37a25330)
- Header glitch when opening modals (https://github.com/flarum/core/pull/2131)
- Ensure `SameSite` is explicitly set for cookies (https://github.com/flarum/core/pull/2159)
- Ensure `Flarum\User\Event\AvatarChanged` event is properly dispatched (https://github.com/flarum/core/pull/2197)
- Show correct error message on wrong password when changing email (https://github.com/flarum/core/pull/2171)
- Discussion unreadCount could be higher than commentCount if posts deleted (https://github.com/flarum/core/pull/2195)
- Don't show page title on the default route (https://github.com/flarum/core/pull/2047)
- Add page title to `All Discussions` page when it isn't the default route (https://github.com/flarum/core/pull/2047)
- Accept `'0'` as `false` for `flarum/components/Checkbox` (https://github.com/flarum/core/pull/2210)
- Fix PostStreamScrubber background (https://github.com/flarum/core/pull/2222)
- Test port on BaseUrl tests (https://github.com/flarum/core/pull/2226)
- `UrlGenerator` can now generate urls with optional parameters (https://github.com/flarum/core/pull/2246)
- Allow `less` to be compiled independently of Flarum (https://github.com/flarum/core/pull/2252)
- Use correct number abbreviation (https://github.com/flarum/core/pull/2261)
- Ensure avatar html uses alt tags for accessibility (https://github.com/flarum/core/pull/2269)
- Escape regex when searching (https://github.com/flarum/core/pull/2273)
- Remove unneeded semicolons inserted during JS compilation (https://github.com/flarum/core/pull/2280)
- Don't require a username/password for SMTP (https://github.com/flarum/core/pull/2287)
- Allow uppercase entries for SMTP encryption validation (https://github.com/flarum/core/pull/2289)
- Ensure that the right number of posts is returned from list posts API (https://github.com/flarum/core/pull/2291)
- Fix a variety of PostStream bugs (https://github.com/flarum/core/pull/2160, https://github.com/flarum/core/pull/2160)
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2324)
- Sliding discussion button in wrong place (https://github.com/flarum/core/pull/2330, https://github.com/flarum/core/pull/2383)
- Sliding discussion glitch on mobile (https://github.com/flarum/core/pull/2381)
- Fix PostStream for posts with top margins, and scrubber position when scrolling below posts (https://github.com/flarum/core/pull/2369)
### Removed
- `Flarum\Event\AbstractConfigureRoutes` event class
- `Flarum\Event\ConfigureApiRoutes` event class
- `Flarum\Event\ConfigureForumRoutes` event class
- `Flarum\Console\Event\Configuring` event class
- `Flarum\Event\ConfigureModelDates` event class
- `Flarum\Event\ConfigureLocales` event class
- `Flarum\Event\ConfigureModelDefaultAttributes` event class
- `Flarum\Event\GetModelRelationship` event class
- `Flarum\User\Event\BioChanged` event class
- `Flarum\Database\MigrationServiceProvider` moved into `Flarum\Database\DatabaseServiceProvider`
- Unused `admin/components/Widget` component (`admin/component/DashboardWidget` should be used instead)
- Mandrill mail driver (https://github.com/flarum/core/commit/bca833d3f1c34d45d95bf905902368a2753b8908)
### Deprecated
- `Flarum\User\Event\GetDisplayName` event class
- Global path helpers, `Flarum\Foundation\Application` path methods (https://github.com/flarum/core/pull/2155)
- `Flarum\User\AssertPermissionTrait` (https://github.com/flarum/core/pull/2044)
## [0.1.0-beta.13](https://github.com/flarum/core/compare/v0.1.0-beta.12...v0.1.0-beta.13)
### Added

View File

@@ -1,14 +1,12 @@
<p align="center"><img src="https://flarum.org/assets/img/logo.png"></p>
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
<p align="center">
<a href="https://github.com/flarum/core/actions?query=workflow%3ATests"><img src="https://github.com/flarum/core/workflows/Tests/badge.svg" alt="PHP Tests"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/dt/flarum/core" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/github/v/release/flarum/core?sort=semver" alt="Latest Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://img.shields.io/packagist/l/flarum/core" alt="License"></a>
<a href="https://github.styleci.io/repos/28257573"><img src="https://github.styleci.io/repos/28257573/shield?style=flat" alt="StyleCI"></a>
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
</p>
## About Flarum
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
@@ -34,3 +32,4 @@ If you discover a security vulnerability within Flarum, please send an e-mail to
## License
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).

View File

@@ -10,7 +10,7 @@
"email": "franz@develophp.org"
},
{
"name": "Daniël Klabbers",
"name": "Daniel Klabbers",
"email": "daniel@klabbers.email",
"homepage": "https://luceos.com"
},
@@ -27,10 +27,6 @@
{
"name": "Matthew Kilgore",
"email": "matthew@kilgore.dev"
},
{
"name": "Alexander (Sasha) Skvortsov",
"email": "askvortsov@flarum.org"
}
],
"support": {
@@ -76,11 +72,11 @@
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^2.3.6",
"symfony/config": "^4.3.4",
"symfony/console": "^4.3.4",
"symfony/event-dispatcher": "^4.3.4",
"symfony/translation": "^4.3.4",
"symfony/yaml": "^4.3.4",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0",
"wikimedia/less.php": "^3.0"
},

14
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

16
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

27
js/package-lock.json generated
View File

@@ -3556,9 +3556,9 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
"jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg=="
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
},
"jquery.hotkeys": {
"version": "0.1.0",
@@ -4546,6 +4546,11 @@
"integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
"dev": true
},
"serialize-javascript": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
"integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ=="
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -4895,29 +4900,21 @@
}
},
"terser-webpack-plugin": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
"integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
"serialize-javascript": "^4.0.0",
"serialize-javascript": "^2.1.2",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
},
"dependencies": {
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"requires": {
"randombytes": "^2.1.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View File

@@ -10,7 +10,7 @@
"dayjs": "^1.8.28",
"expose-loader": "^0.7.5",
"flarum-webpack-config": "0.1.0-beta.10",
"jquery": "^3.5.1",
"jquery": "^3.4.1",
"jquery.hotkeys": "^0.1.0",
"lodash-es": "^4.17.14",
"m.attrs.bidi": "github:tobscure/m.attrs.bidi",

20
js/shims.d.ts vendored
View File

@@ -1,5 +1,6 @@
// Mithril
import Mithril from 'mithril';
import * as Mithril from 'mithril';
import Stream from 'mithril/stream';
// Other third-party libs
import * as _dayjs from 'dayjs';
@@ -8,6 +9,21 @@ import * as _$ from 'jquery';
// Globals from flarum/core
import Application from './src/common/Application';
/**
* Helpers that flarum/core patches into Mithril
*/
interface m extends Mithril.Static {
prop: typeof Stream;
}
/**
* Export Mithril typings globally.
*
* This lets us use these typings without an extra import everywhere we use
* Mithril in a TypeScript file.
*/
export as namespace Mithril;
/**
* flarum/core exposes several extensions globally:
*
@@ -20,7 +36,7 @@ import Application from './src/common/Application';
*/
declare global {
const $: typeof _$;
const m: Mithril.Static;
const m: m;
const dayjs: typeof _dayjs;
}

View File

@@ -27,19 +27,20 @@ export default class AdminApplication extends Application {
* @inheritdoc
*/
mount() {
// Mithril does not render the home route on https://example.com/admin, so
// we need to go to https://example.com/admin#/ explicitly.
if (!document.location.hash) document.location.hash = '#/';
m.route.prefix = '#';
super.mount();
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
m.mount(document.getElementById('header-navigation'), Navigation);
m.mount(document.getElementById('header-primary'), HeaderPrimary);
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
m.mount(document.getElementById('admin-navigation'), AdminNav);
// Mithril does not render the home route on https://example.com/admin, so
// we need to go to https://example.com/admin#/ explicitly.
if (!document.location.hash) document.location.hash = '#/';
m.route.prefix = '#';
super.mount();
// If an extension has just been enabled, then we will run its settings
// callback.
const enabled = localStorage.getItem('enabledExtension');

View File

@@ -1,21 +1,21 @@
import Page from '../../common/components/Page';
import Button from '../../common/components/Button';
import Switch from '../../common/components/Switch';
import Stream from '../../common/utils/Stream';
import EditCustomCssModal from './EditCustomCssModal';
import EditCustomHeaderModal from './EditCustomHeaderModal';
import EditCustomFooterModal from './EditCustomFooterModal';
import UploadImageButton from './UploadImageButton';
import saveSettings from '../utils/saveSettings';
import withAttr from '../../common/utils/withAttr';
export default class AppearancePage extends Page {
oninit(vnode) {
super.oninit(vnode);
this.primaryColor = Stream(app.data.settings.theme_primary_color);
this.secondaryColor = Stream(app.data.settings.theme_secondary_color);
this.darkMode = Stream(app.data.settings.theme_dark_mode);
this.coloredHeader = Stream(app.data.settings.theme_colored_header);
this.primaryColor = m.stream(app.data.settings.theme_primary_color);
this.secondaryColor = m.stream(app.data.settings.theme_secondary_color);
this.darkMode = m.stream(app.data.settings.theme_dark_mode);
this.coloredHeader = m.stream(app.data.settings.theme_colored_header);
}
view() {

View File

@@ -5,7 +5,6 @@ import Button from '../../common/components/Button';
import saveSettings from '../utils/saveSettings';
import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch';
import Stream from '../../common/utils/Stream';
import withAttr from '../../common/utils/withAttr';
export default class BasicsPage extends Page {
@@ -27,7 +26,7 @@ export default class BasicsPage extends Page {
this.values = {};
const settings = app.data.settings;
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
this.localeOptions = {};
const locales = app.data.locales;
@@ -194,7 +193,9 @@ export default class BasicsPage extends Page {
saveSettings(settings)
.then(() => {
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
type: 'success',
});
})
.catch(() => {})
.then(() => {

View File

@@ -4,7 +4,7 @@ import Badge from '../../common/components/Badge';
import Group from '../../common/models/Group';
import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch';
import Stream from '../../common/utils/Stream';
import withAttr from '../../common/utils/withAttr';
/**
* The `EditGroupModal` component shows a modal dialog which allows the user
@@ -16,11 +16,11 @@ export default class EditGroupModal extends Modal {
this.group = this.attrs.group || app.store.createRecord('groups');
this.nameSingular = Stream(this.group.nameSingular() || '');
this.namePlural = Stream(this.group.namePlural() || '');
this.icon = Stream(this.group.icon() || '');
this.color = Stream(this.group.color() || '');
this.isHidden = Stream(this.group.isHidden() || false);
this.nameSingular = m.stream(this.group.nameSingular() || '');
this.namePlural = m.stream(this.group.namePlural() || '');
this.icon = m.stream(this.group.icon() || '');
this.color = m.stream(this.group.color() || '');
this.isHidden = m.stream(this.group.isHidden() || false);
}
className() {

View File

@@ -123,7 +123,6 @@ export default class ExtensionsPage extends Page {
url: app.forum.attribute('apiUrl') + '/extensions/' + id,
method: 'PATCH',
body: { enabled: !enabled },
errorHandler: this.onerror.bind(this),
})
.then(() => {
if (!enabled) localStorage.setItem('enabledExtension', id);
@@ -132,27 +131,4 @@ export default class ExtensionsPage extends Page {
app.modal.show(LoadingModal);
}
onerror(e) {
// We need to give the modal animation time to start; if we close the modal too early,
// it breaks the bootstrap modal library.
// TODO: This workaround should be removed when we move away from bootstrap JS for modals.
setTimeout(() => {
app.modal.close();
}, 300); // Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
if (e.status !== 409) {
throw e;
}
const error = e.response.errors[0];
app.alerts.show(
{ type: 'error' },
app.translator.trans(`core.lib.error.${error.code}_message`, {
extension: error.extension,
extensions: error.extensions.join(', '),
})
);
}
}

View File

@@ -5,7 +5,7 @@ import Alert from '../../common/components/Alert';
import Select from '../../common/components/Select';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import saveSettings from '../utils/saveSettings';
import Stream from '../../common/utils/Stream';
import withAttr from '../../common/utils/withAttr';
export default class MailPage extends Page {
oninit(vnode) {
@@ -25,7 +25,7 @@ export default class MailPage extends Page {
this.status = { sending: false, errors: {} };
const settings = app.data.settings;
this.fields.forEach((key) => (this.values[key] = Stream(settings[key])));
this.fields.forEach((key) => (this.values[key] = m.stream(settings[key])));
app
.request({
@@ -40,7 +40,7 @@ export default class MailPage extends Page {
for (const driver in this.driverFields) {
for (const field in this.driverFields[driver]) {
this.fields.push(field);
this.values[field] = Stream(settings[field]);
this.values[field] = m.stream(settings[field]);
}
}
@@ -194,7 +194,9 @@ export default class MailPage extends Page {
})
.then((response) => {
this.sendingTest = false;
this.testEmailSuccessAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.email.send_test_mail_success'));
this.testEmailSuccessAlert = app.alerts.show(app.translator.trans('core.admin.email.send_test_mail_success'), {
type: 'success',
});
})
.catch((error) => {
this.sendingTest = false;
@@ -217,7 +219,9 @@ export default class MailPage extends Page {
saveSettings(settings)
.then(() => {
this.successAlert = app.alerts.show({ type: 'success' }, app.translator.trans('core.admin.basics.saved_message'));
this.successAlert = app.alerts.show(app.translator.trans('core.admin.basics.saved_message'), {
type: 'success',
});
})
.catch(() => {})
.then(() => {

View File

@@ -1,6 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Stream from '../../common/utils/Stream';
import saveSettings from '../utils/saveSettings';
export default class SettingsModal extends Modal {
@@ -36,7 +35,7 @@ export default class SettingsModal extends Modal {
}
setting(key, fallback = '') {
this.settings[key] = this.settings[key] || Stream(app.data.settings[key] || fallback);
this.settings[key] = this.settings[key] || m.stream(app.data.settings[key] || fallback);
return this.settings[key];
}

View File

@@ -198,19 +198,13 @@ export default class Application {
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
// Add a class to the body which indicates that the page has been scrolled
// down. When this happens, we'll add classes to the header and app body
// which will set the navbar's position to fixed. We don't want to always
// have it fixed, as that could overlap with custom headers.
const scrollListener = new ScrollListener((top) => {
// down.
new ScrollListener((top) => {
const $app = $('#app');
const offset = $app.offset().top;
$app.toggleClass('affix', top >= offset).toggleClass('scrolled', top > offset);
$('.App-header').toggleClass('navbar-fixed-top', top >= offset);
});
scrollListener.start();
scrollListener.update();
}).start();
$(() => {
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
@@ -278,7 +272,7 @@ export default class Application {
/**
* Make an AJAX request, handling any low-level errors that may occur.
*
* @see https://mithril.js.org/request.html
* @see https://lhorie.github.io/mithril/mithril.request.html
* @param {Object} options
* @return {Promise}
* @public
@@ -410,7 +404,7 @@ export default class Application {
console.groupEnd();
}
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
this.requestErrorAlert = this.alerts.show(error.alert.content, error.alert);
}
return Promise.reject(error);

View File

@@ -1,9 +1,10 @@
import * as Mithril from 'mithril';
let deprecatedPropsWarned = false;
let deprecatedInitPropsWarned = false;
export type ComponentAttrs = {
className?: string;
export interface ComponentAttrs extends Mithril.Attributes {}
[key: string]: any;
};
/**
* The `Component` class defines a user interface 'building block'. A component
@@ -32,7 +33,7 @@ export interface ComponentAttrs extends Mithril.Attributes {}
*
* @see https://mithril.js.org/components.html
*/
export default abstract class Component<T extends ComponentAttrs = ComponentAttrs> implements Mithril.ClassComponent<T> {
export default abstract class Component<T extends ComponentAttrs = any> implements Mithril.ClassComponent<T> {
/**
* The root DOM element for the component.
*/
@@ -131,38 +132,5 @@ export default abstract class Component<T extends ComponentAttrs = ComponentAttr
*
* This can be used to assign default values for missing, optional attrs.
*/
protected static initAttrs<T>(attrs: T): void {
// Deprecated, part of Mithril 2 BC layer
if ('initProps' in this && !deprecatedInitPropsWarned) {
deprecatedInitPropsWarned = true;
console.warn('initProps is deprecated, please use initAttrs instead.');
(this as any).initProps(attrs);
}
}
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
/**
* The attributes passed into the component.
*
* @see https://mithril.js.org/components.html#passing-data-to-components
*
* @deprecated, use attrs instead.
*/
get props() {
if (!deprecatedPropsWarned) {
deprecatedPropsWarned = true;
console.warn('this.props is deprecated, please use this.attrs instead.');
}
return this.attrs;
}
set props(props) {
if (!deprecatedPropsWarned) {
deprecatedPropsWarned = true;
console.warn('this.props is deprecated, please use this.attrs instead.');
}
this.attrs = props;
}
// END DEPRECATED MITHRIL 2 BC LAYER
protected static initAttrs<T>(attrs: T): void {}
}

View File

@@ -15,7 +15,7 @@ import * as Mithril from 'mithril';
* This should only be used when necessary, and only with `m.render`. If you are unsure whether you need
* this or `Component, you probably need `Component`.
*/
export default abstract class Fragment {
export default abstract class Fragment implements Mithril.ClassComponent {
/**
* The root DOM element for the fragment.
*/
@@ -52,7 +52,7 @@ export default abstract class Fragment {
*
* @final
*/
public render(): Mithril.Vnode<Mithril.Attributes, this> {
public render(): Mithril.Vnode {
const vdom = this.view();
vdom.attrs = vdom.attrs || {};
@@ -61,14 +61,15 @@ export default abstract class Fragment {
vdom.attrs.oncreate = (vnode) => {
this.element = vnode.dom;
if (originalOnCreate) originalOnCreate.apply(this, [vnode]);
if (this.oncreate) this.oncreate.apply(this, vnode);
if (originalOnCreate) originalOnCreate.apply(this, vnode);
};
return vdom;
}
/**
* Creates a view out of virtual elements.
* @inheritdoc
*/
abstract view(): Mithril.Vnode<Mithril.Attributes, this>;
abstract view();
}

View File

@@ -83,7 +83,7 @@ export default class Store {
*/
find(type, id, query = {}, options = {}) {
let params = query;
let url = app.forum.attribute('apiUrl') + (query.search ? '/search/' : '/') + type;
let url = app.forum.attribute('apiUrl') + '/' + type;
if (id instanceof Array) {
url += '?filter[id]=' + id.join(',');

View File

@@ -12,14 +12,12 @@ import anchorScroll from './utils/anchorScroll';
import RequestError from './utils/RequestError';
import abbreviateNumber from './utils/abbreviateNumber';
import * as string from './utils/string';
import Stream from './utils/Stream';
import SubtreeRetainer from './utils/SubtreeRetainer';
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
import extract from './utils/extract';
import ScrollListener from './utils/ScrollListener';
import stringToColor from './utils/stringToColor';
import subclassOf from './utils/subclassOf';
import SuperTextarea from './utils/SuperTextarea';
import patchMithril from './utils/patchMithril';
import classList from './utils/classList';
import extractText from './utils/extractText';
@@ -48,7 +46,6 @@ import FieldSet from './components/FieldSet';
import Select from './components/Select';
import Navigation from './components/Navigation';
import Alert from './components/Alert';
import Link from './components/Link';
import LinkButton from './components/LinkButton';
import Checkbox from './components/Checkbox';
import SelectDropdown from './components/SelectDropdown';
@@ -68,7 +65,6 @@ import username from './helpers/username';
import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems';
import Fragment from './Fragment';
import DefaultResolver from './resolvers/DefaultResolver';
export default {
extend: extend,
@@ -89,9 +85,7 @@ export default {
'utils/extract': extract,
'utils/ScrollListener': ScrollListener,
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/SuperTextarea': SuperTextarea,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/classList': classList,
@@ -122,7 +116,6 @@ export default {
'components/Select': Select,
'components/Navigation': Navigation,
'components/Alert': Alert,
'components/Link': Link,
'components/LinkButton': LinkButton,
'components/Checkbox': Checkbox,
'components/SelectDropdown': SelectDropdown,
@@ -141,5 +134,4 @@ export default {
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems,
'resolvers/DefaultResolver': DefaultResolver,
};

View File

@@ -1,33 +1,31 @@
import Component, { ComponentAttrs } from '../Component';
import Component from '../Component';
import Button from './Button';
import listItems from '../helpers/listItems';
import extract from '../utils/extract';
import Mithril from 'mithril';
export interface AlertAttrs extends ComponentAttrs {
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
type?: string;
/** An array of controls to show in the alert. */
controls?: Mithril.Children;
/** Whether or not the alert can be dismissed. */
dismissible?: boolean;
/** A callback to run when the alert is dismissed */
ondismiss?: Function;
}
/**
* The `Alert` component represents an alert box, which contains a message,
* some controls, and may be dismissible.
*
* ### Attrs
*
* - `type` The type of alert this is. Will be used to give the alert a class
* name of `Alert--{type}`.
* - `controls` An array of controls to show in the alert.
* - `dismissible` Whether or not the alert can be dismissed.
* - `ondismiss` A callback to run when the alert is dismissed.
*
* All other attrs will be assigned as attributes on the DOM element.
*/
export default class Alert<T extends AlertAttrs = AlertAttrs> extends Component<T> {
view(vnode: Mithril.Vnode) {
export default class Alert extends Component {
view(vnode) {
const attrs = Object.assign({}, this.attrs);
const type = extract(attrs, 'type');
attrs.className = 'Alert Alert--' + type + ' ' + (attrs.className || '');
const content = extract(attrs, 'content') || vnode.children;
const controls = (extract(attrs, 'controls') || []) as Mithril.ChildArray;
const controls = extract(attrs, 'controls') || [];
// If the alert is meant to be dismissible (which is the case by default),
// then we will create a dismiss button to append as the final control in

View File

@@ -1,47 +0,0 @@
import Component from '../Component';
import extract from '../utils/extract';
/**
* The link component enables both internal and external links.
* It will return a regular HTML link for any links to external sites,
* and it will use Mithril's m.route.Link for any internal links.
*
* Links will default to internal; the 'external' attr must be set to
* `true` for the link to be external.
*/
export default class Link extends Component {
view(vnode) {
let { options = {}, ...attrs } = vnode.attrs;
attrs.href = attrs.href || '';
// For some reason, m.route.Link does not like vnode.text, so if present, we
// need to convert it to text vnodes and store it in children.
const children = vnode.children || { tag: '#', children: vnode.text };
if (attrs.external) {
return <a {...attrs}>{children}</a>;
}
// If the href URL of the link is the same as the current page path
// we will not add a new entry to the browser history.
// This allows us to still refresh the Page component
// without adding endless history entries.
if (attrs.href === m.route.get()) {
if (!('replace' in options)) options.replace = true;
}
// Mithril 2 does not completely rerender the page if a route change leads to the same route
// (or the same component handling a different route).
// Here, the `force` parameter will use Mithril's key system to force a full rerender
// see https://mithril.js.org/route.html#key-parameter
if (extract(attrs, 'force')) {
if (!('state' in options)) options.state = {};
if (!('key' in options.state)) options.state.key = Date.now();
}
attrs.options = options;
return <m.route.Link {...attrs}>{children}</m.route.Link>;
}
}

View File

@@ -1,5 +1,4 @@
import Button from './Button';
import Link from './Link';
/**
* The `LinkButton` component defines a `Button` which links to a route.
@@ -12,20 +11,18 @@ import Link from './Link';
* active.
* - `href` The URL to link to. If the current URL `m.route()` matches this,
* the `active` prop will automatically be set to true.
* - `force` Whether the page should be fully rerendered. Defaults to `true`.
*/
export default class LinkButton extends Button {
static initAttrs(attrs) {
super.initAttrs(attrs);
attrs.active = this.isActive(attrs);
if (attrs.force === undefined) attrs.force = true;
}
view(vnode) {
const vdom = super.view(vnode);
vdom.tag = Link;
vdom.tag = m.route.Link;
vdom.attrs.active = String(vdom.attrs.active);
return vdom;

View File

@@ -24,20 +24,11 @@ export default class Modal extends Component {
oncreate(vnode) {
super.oncreate(vnode);
this.attrs.animateShow(() => this.onready());
this.attrs.onshow(() => this.onready());
}
onbeforeremove() {
// If the global modal state currently contains a modal,
// we've just opened up a new one, and accordingly,
// we don't need to show a hide animation.
if (!this.attrs.state.modal) {
this.attrs.animateHide();
// Here, we ensure that the animation has time to complete.
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
return new Promise((resolve) => setTimeout(resolve, 300));
}
onremove() {
this.attrs.onhide();
}
view() {
@@ -112,11 +103,13 @@ export default class Modal extends Component {
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {}
/**
* Hide the modal.
*/
hide() {
this.attrs.state.close();
this.attrs.onhide();
}
/**

View File

@@ -11,14 +11,7 @@ export default class ModalManager extends Component {
return (
<div className="ModalManager modal fade">
{modal
? modal.componentClass.component({
...modal.attrs,
animateShow: this.animateShow.bind(this),
animateHide: this.animateHide.bind(this),
state: this.attrs.state,
})
: ''}
{modal ? modal.componentClass.component({ ...modal.attrs, onshow: this.animateShow.bind(this), onhide: this.animateHide.bind(this) }) : ''}
</div>
);
}
@@ -35,14 +28,6 @@ export default class ModalManager extends Component {
animateShow(readyCallback) {
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
// If we are opening this modal while another modal is already open,
// the shown event will not run, because the modal is already open.
// So, we need to manually trigger the readyCallback.
if (this.$().hasClass('in')) {
readyCallback();
return;
}
this.$()
.one('shown.bs.modal', readyCallback)
.modal({

View File

@@ -11,7 +11,9 @@ export default class Page extends Component {
super.oninit(vnode);
app.previous = app.current;
app.current = new PageState(this.constructor, { routeName: this.attrs.routeName });
app.current = new PageState(this.constructor);
this.onNewRoute();
app.drawer.hide();
app.modal.close();
@@ -22,13 +24,16 @@ export default class Page extends Component {
* @type {String}
*/
this.bodyClass = '';
}
/**
* Whether we should scroll to the top of the page when its rendered.
*
* @type {Boolean}
*/
this.scrollTopOnCreate = true;
/**
* A collections of actions to run when the route changes.
* This is extracted here, and not hardcoded in oninit, as oninit is not called
* when a different route is handled by the same component, but we still need to
* adjust the current route name.
*/
onNewRoute() {
app.current.set('routeName', this.attrs.routeName);
}
oncreate(vnode) {
@@ -37,10 +42,6 @@ export default class Page extends Component {
if (this.bodyClass) {
$('#app').addClass(this.bodyClass);
}
if (this.scrollTopOnCreate) {
$(window).scrollTop(0);
}
}
onremove() {

View File

@@ -1,11 +1,11 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
/**
* The `fullTime` helper displays a formatted time string wrapped in a <time>
* tag.
*
* @param {Date} time
* @return {Object}
*/
export default function fullTime(time: Date): Mithril.Vnode {
export default function fullTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -1,13 +1,14 @@
import dayjs from 'dayjs';
import * as Mithril from 'mithril';
import humanTimeUtil from '../utils/humanTime';
/**
* The `humanTime` helper displays a time in a human-friendly time-ago format
* (e.g. '12 days ago'), wrapped in a <time> tag with other information about
* the time.
*
* @param {Date} time
* @return {Object}
*/
export default function humanTime(time: Date): Mithril.Vnode {
export default function humanTime(time) {
const d = dayjs(time);
const datetime = d.format();

View File

@@ -0,0 +1,12 @@
/**
* The `icon` helper displays an icon.
*
* @param {String} fontClass The full icon class, prefix and the icons name.
* @param {Object} attrs Any other attributes to apply.
* @return {Object}
*/
export default function icon(fontClass, attrs = {}) {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
return <i {...attrs} />;
}

View File

@@ -1,13 +0,0 @@
import * as Mithril from 'mithril';
/**
* The `icon` helper displays an icon.
*
* @param fontClass The full icon class, prefix and the icons name.
* @param attrs Any other attributes to apply.
*/
export default function icon(fontClass: string, attrs: Mithril.Attributes = {}): Mithril.Vnode {
attrs.className = 'icon ' + fontClass + ' ' + (attrs.className || '');
return <i {...attrs} />;
}

View File

@@ -51,6 +51,8 @@ export default function listItems(items) {
</li>
);
node.state = node.state || {};
return node;
});
}

View File

@@ -1,41 +0,0 @@
import Mithril from 'mithril';
/**
* Generates a route resolver for a given component.
* In addition to regular route resolver functionality:
* - It provide the current route name as an attr
* - It sets a key on the component so a rerender will be triggered on route change.
*/
export default class DefaultResolver {
component: Mithril.Component;
routeName: string;
constructor(component, routeName) {
this.component = component;
this.routeName = routeName;
}
/**
* When a route change results in a changed key, a full page
* rerender occurs. This method can be overriden in subclasses
* to prevent rerenders on some route changes.
*/
makeKey() {
return this.routeName + JSON.stringify(m.route.param());
}
makeAttrs(vnode) {
return {
...vnode.attrs,
routeName: this.routeName,
};
}
onmatch(args, requestedPath, route) {
return this.component;
}
render(vnode) {
return [{ ...vnode, attrs: this.makeAttrs(vnode), key: this.makeKey() }];
}
}

View File

@@ -0,0 +1,50 @@
import Alert from '../components/Alert';
export default class AlertManagerState {
constructor() {
this.activeAlerts = {};
this.alertId = 0;
}
getActiveAlerts() {
return this.activeAlerts;
}
/**
* Show an Alert in the alerts area.
*/
show(children, attrs, componentClass = Alert) {
// Breaking Change Compliance Warning, Remove in Beta 15.
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
if (attrs === Alert || attrs instanceof Alert) {
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
}
// End Change Compliance Warning, Remove in Beta 15
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
m.redraw();
return this.alertId;
}
/**
* Dismiss an alert.
*/
dismiss(key) {
if (!key || !(key in this.activeAlerts)) return;
delete this.activeAlerts[key];
m.redraw();
}
/**
* Clear all alerts.
*
* @public
*/
clear() {
this.activeAlerts = {};
m.redraw();
}
}

View File

@@ -1,80 +0,0 @@
import Mithril from 'mithril';
import Alert, { AlertAttrs } from '../components/Alert';
/**
* Returned by `AlertManagerState.show`. Used to dismiss alerts.
*/
export type AlertIdentifier = number;
export interface AlertState {
componentClass: typeof Alert;
attrs: AlertAttrs;
children: Mithril.Children;
}
export default class AlertManagerState {
protected activeAlerts: { [id: number]: AlertState } = {};
protected alertId = 0;
getActiveAlerts() {
return this.activeAlerts;
}
/**
* Show an Alert in the alerts area.
*
* @returns The alert's ID, which can be used to dismiss the alert.
*/
show(children: Mithril.Children): AlertIdentifier;
show(attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
show(componentClass: Alert, attrs: AlertAttrs, children: Mithril.Children): AlertIdentifier;
show(arg1: any, arg2?: any, arg3?: any) {
// Assigns variables as per the above signatures
let componentClass = Alert;
let attrs: AlertAttrs = {};
let children: Mithril.Children;
if (arguments.length == 1) {
children = arg1 as Mithril.Children;
} else if (arguments.length == 2) {
attrs = arg1 as AlertAttrs;
children = arg2 as Mithril.Children;
} else if (arguments.length == 3) {
componentClass = arg1 as typeof Alert;
attrs = arg2 as AlertAttrs;
children = arg3;
}
// Breaking Change Compliance Warning, Remove in Beta 15.
// This is applied to the first argument (attrs) because previously, the alert was passed as the first argument.
if (attrs === Alert || attrs instanceof Alert) {
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
console.error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
throw new Error('The AlertManager can only show Alerts. Whichever extension triggered this alert should be updated to comply with beta 14.');
}
// End Change Compliance Warning, Remove in Beta 15
this.activeAlerts[++this.alertId] = { children, attrs, componentClass };
m.redraw();
return this.alertId;
}
/**
* Dismiss an alert.
*/
dismiss(key: AlertIdentifier): void {
if (!key || !(key in this.activeAlerts)) return;
delete this.activeAlerts[key];
m.redraw();
}
/**
* Clear all alerts.
*/
clear(): void {
this.activeAlerts = {};
m.redraw();
}
}

View File

@@ -58,7 +58,7 @@ export default class ScrollListener {
*/
start() {
if (!this.active) {
window.addEventListener('scroll', (this.active = this.loop.bind(this)), { passive: true });
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
}
}

View File

@@ -1,3 +0,0 @@
import Stream from 'mithril/stream';
export default Stream;

View File

@@ -28,9 +28,6 @@ export default class SubtreeRetainer {
constructor(...callbacks) {
this.callbacks = callbacks;
this.data = {};
// Build the initial data, so it is available when calling
// needsRebuild from the onbeforeupdate hook.
this.needsRebuild();
}
/**
@@ -63,8 +60,6 @@ export default class SubtreeRetainer {
*/
check(...callbacks) {
this.callbacks = this.callbacks.concat(callbacks);
// Update the data cache when new checks are added.
this.needsRebuild();
}
/**

View File

@@ -22,8 +22,6 @@ export default class SuperTextarea {
*/
setValue(value) {
this.$.val(value).trigger('input');
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
}
/**
@@ -51,6 +49,8 @@ export default class SuperTextarea {
*/
insertAtCursor(text) {
this.insertAt(this.el.selectionStart, text);
this.el.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
}
/**

View File

@@ -1,6 +1,3 @@
import dayjs from 'dayjs';
import 'dayjs/plugin/relativeTime';
/**
* The `humanTime` utility converts a date to a localized, human-readable time-
* ago string.

View File

@@ -1,9 +1,6 @@
import DefaultResolver from '../resolvers/DefaultResolver';
/**
* The `mapRoutes` utility converts a map of named application routes into a
* format that can be understood by Mithril, and wraps them in route resolvers
* to provide each route with the current route name.
* format that can be understood by Mithril.
*
* @see https://mithril.js.org/route.html#signature
* @param {Object} routes
@@ -13,17 +10,14 @@ import DefaultResolver from '../resolvers/DefaultResolver';
export default function mapRoutes(routes, basePath = '') {
const map = {};
for (const routeName in routes) {
const route = routes[routeName];
for (const key in routes) {
const route = routes[key];
if ('resolver' in route) {
map[basePath + route.path] = route.resolver;
} else if ('component' in route) {
const resolverClass = 'resolverClass' in route ? route.resolverClass : DefaultResolver;
map[basePath + route.path] = new resolverClass(route.component, routeName);
} else {
throw new Error(`Either a resolver or a component must be provided for the route [${routeName}]`);
}
map[basePath + route.path] = {
render() {
return m(route.component, { routeName: key });
},
};
}
return map;

View File

@@ -1,12 +1,39 @@
import withAttr from './withAttr';
import Stream from './Stream';
let deprecatedMPropWarned = false;
let deprecatedMWithAttrWarned = false;
import Stream from 'mithril/stream';
import extract from './extract';
export default function patchMithril(global) {
const defaultMithril = global.m;
/**
* If the href URL of the link is the same as the current page path
* we will not add a new entry to the browser history.
*
* This allows us to still refresh the Page component
* without adding endless history entries.
*
* We also add the `force` attribute that adds a custom state key
* for when you want to force a complete refresh of the Page
*/
const defaultLinkView = defaultMithril.route.Link.view;
const modifiedLink = {
view: function (vnode) {
let { href, options = {} } = vnode.attrs;
if (href === m.route.get()) {
if (!('replace' in options)) options.replace = true;
}
if (extract(vnode.attrs, 'force')) {
if (!('state' in options)) options.state = {};
if (!('key' in options.state)) options.state.key = Date.now();
}
vnode.attrs.options = options;
return defaultLinkView(vnode);
},
};
const modifiedMithril = function (comp, ...args) {
const node = defaultMithril.apply(this, arguments);
@@ -17,28 +44,29 @@ export default function patchMithril(global) {
modifiedMithril.bidi(node, node.attrs.bidi);
}
// Allows us to use a "route" attr on links, which will automatically convert the link to one which
// supports linking to other pages in the SPA without refreshing the document.
if (node.attrs.route) {
node.attrs.href = node.attrs.route;
node.tag = modifiedLink;
// For some reason, m.route.Link does not like vnode.text, so if present, we
// need to convert it to text vnodes and store it in children.
if (node.text) {
node.children = { tag: '#', children: node.text };
}
delete node.attrs.route;
}
return node;
};
Object.keys(defaultMithril).forEach((key) => (modifiedMithril[key] = defaultMithril[key]));
// BEGIN DEPRECATED MITHRIL 2 BC LAYER
modifiedMithril.prop = function (...args) {
if (!deprecatedMPropWarned) {
deprecatedMPropWarned = true;
console.warn('m.prop() is deprecated, please use the Stream util (flarum/utils/Streams) instead.');
}
return Stream.bind(this)(...args);
};
modifiedMithril.stream = Stream;
modifiedMithril.withAttr = function (...args) {
if (!deprecatedMWithAttrWarned) {
deprecatedMWithAttrWarned = true;
console.warn("m.withAttr() is deprecated, please use flarum's withAttr util (flarum/utils/withAttr) instead.");
}
return withAttr.bind(this)(...args);
};
// END DEPRECATED MITHRIL 2 BC LAYER
modifiedMithril.route.Link = modifiedLink;
global.m = modifiedMithril;
}

View File

@@ -115,19 +115,17 @@ export default class ForumApplication extends Application {
this.routes[defaultAction].path = '/';
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
this.pane = new Pane(document.getElementById('app'));
m.route.prefix = '';
super.mount(this.forum.attribute('basePath'));
// We mount navigation and header components after the page, so components
// like the back button can access the updated state when rendering.
m.mount(document.getElementById('app-navigation'), { view: () => Navigation.component({ className: 'App-backControl', drawer: true }) });
m.mount(document.getElementById('header-navigation'), Navigation);
m.mount(document.getElementById('header-primary'), HeaderPrimary);
m.mount(document.getElementById('header-secondary'), HeaderSecondary);
m.mount(document.getElementById('composer'), { view: () => Composer.component({ state: this.composer }) });
this.pane = new Pane(document.getElementById('app'));
m.route.prefix = '';
super.mount(this.forum.attribute('basePath'));
alertEmailConfirmation(this);
// Route the home link back home when clicked. We do not want it to register

View File

@@ -71,7 +71,6 @@ import Search from './components/Search';
import DiscussionListItem from './components/DiscussionListItem';
import LoadingPost from './components/LoadingPost';
import PostsUserPage from './components/PostsUserPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
import routes from './routes';
import ForumApplication from './ForumApplication';
@@ -147,7 +146,6 @@ export default Object.assign(compat, {
'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage,
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
routes: routes,
ForumApplication: ForumApplication,
});

View File

@@ -1,6 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Stream from '../../common/utils/Stream';
/**
* The `ChangeEmailModal` component shows a modal dialog which allows the user
@@ -22,14 +21,14 @@ export default class ChangeEmailModal extends Modal {
*
* @type {function}
*/
this.email = Stream(app.session.user.email());
this.email = m.stream(app.session.user.email());
/**
* The value of the password input.
*
* @type {function}
*/
this.password = Stream('');
this.password = m.stream('');
}
className() {

View File

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

View File

@@ -82,7 +82,7 @@ export default class Composer extends Component {
});
// When the escape key is pressed on any inputs, close the composer.
this.$().on('keydown', ':input', 'esc', () => this.state.close());
this.$().on('keydown', ':input', 'esc', () => this.close());
this.handlers = {};

View File

@@ -1,6 +1,5 @@
import ComposerBody from './ComposerBody';
import extractText from '../../common/utils/extractText';
import Stream from '../../common/utils/Stream';
/**
* The `DiscussionComposer` component displays the composer content for starting
@@ -27,7 +26,7 @@ export default class DiscussionComposer extends ComposerBody {
oninit(vnode) {
super.oninit(vnode);
this.composer.fields.title = this.composer.fields.title || Stream('');
this.composer.fields.title = this.composer.fields.title || m.stream('');
/**
* The value of the title input.

View File

@@ -1,5 +1,4 @@
import Component 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';
@@ -51,7 +50,6 @@ export default class DiscussionListItem extends Component {
'DiscussionListItem',
this.active() ? 'active' : '',
this.attrs.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
'ontouchstart' in window ? 'Slidable' : '',
]),
};
}
@@ -91,16 +89,16 @@ export default class DiscussionListItem extends Component {
)
: ''}
<span
<a
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
onclick={this.markAsRead.bind(this)}
>
{icon('fas fa-check')}
</span>
</a>
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
<Link
href={user ? app.route.user(user) : '#'}
<a
route={user ? app.route.user(user) : '#'}
className="DiscussionListItem-author"
title={extractText(
app.translator.trans('core.forum.discussion_list.started_text', { user: user, ago: humanTime(discussion.createdAt()) })
@@ -110,14 +108,14 @@ export default class DiscussionListItem extends Component {
}}
>
{avatar(user, { title: '' })}
</Link>
</a>
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
<Link href={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
<a route={app.route.discussion(discussion, jumpTo)} className="DiscussionListItem-main">
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
</Link>
</a>
<span
className="DiscussionListItem-count"
@@ -138,7 +136,7 @@ export default class DiscussionListItem extends Component {
// This allows the user to drag the row to either side of the screen to
// reveal controls.
if ('ontouchstart' in window) {
const slidableInstance = slidable(this.$());
const slidableInstance = slidable(this.$().addClass('Slidable'));
this.$('.DiscussionListItem-controls').on('hidden.bs.dropdown', () => slidableInstance.reset());
}

View File

@@ -47,10 +47,11 @@ export default class DiscussionPage extends Page {
app.history.push('discussion');
this.bodyClass = 'App--discussion';
this.prevRoute = m.route.get();
}
onremove() {
super.onremove();
// If we are indeed navigating away from this discussion, then disable the
// discussion list pane. Also, if we're composing a reply to this
// discussion, minimize the composer unless it's empty, in which case
@@ -82,6 +83,7 @@ export default class DiscussionPage extends Page {
{PostStream.component({
discussion,
stream: this.stream,
targetPost: this.stream.targetPost,
onPositionChange: this.positionChanged.bind(this),
})}
</div>
@@ -93,6 +95,33 @@ export default class DiscussionPage extends Page {
);
}
onbeforeupdate(vnode) {
super.onbeforeupdate(vnode);
if (m.route.get() !== this.prevRoute) {
this.prevRoute = m.route.get();
// If we have routed to the same discussion as we were viewing previously,
// cancel the unloading of this controller and instead prompt the post
// stream to jump to the new 'near' param.
if (this.discussion) {
const idParam = m.route.param('id');
if (idParam && idParam.split('-')[0] === this.discussion.id()) {
const near = m.route.param('near') || '1';
if (near !== String(this.near)) {
this.stream.goToNumber(near);
}
this.near = near;
} else {
this.oninit(vnode);
}
}
}
}
/**
* Load the discussion from the API or use the preloaded one.
*/
@@ -131,8 +160,10 @@ export default class DiscussionPage extends Page {
* @param {Discussion} discussion
*/
show(discussion) {
this.discussion = discussion;
app.history.push('discussion', discussion.title());
app.setTitle(discussion.title());
app.setTitle(this.discussion.title());
app.setTitleCount(0);
// When the API responds with a discussion, it will also include a number of
@@ -155,7 +186,7 @@ export default class DiscussionPage extends Page {
record.relationships.discussion.data.id === discussionId
)
.map((record) => app.store.getById('posts', record.id))
.sort((a, b) => a.createdAt() - b.createdAt())
.sort((a, b) => a.id() - b.id())
.slice(0, 20);
}
@@ -163,12 +194,10 @@ export default class DiscussionPage extends Page {
// posts we want to display. Tell the stream to scroll down and highlight
// the specific post that was routed to.
this.stream = new PostStreamState(discussion, includedPosts);
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true).then(() => {
this.discussion = discussion;
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
app.current.set('discussion', discussion);
app.current.set('stream', this.stream);
});
app.current.set('discussion', discussion);
app.current.set('stream', this.stream);
}
/**
@@ -217,7 +246,10 @@ export default class DiscussionPage extends Page {
// replace it into the window's history and our own history stack.
const url = app.route.discussion(discussion, (this.near = startNumber));
this.prevRoute = url;
m.route.set(url, null, { replace: true });
window.history.replaceState(null, document.title, url);
app.history.push('discussion', discussion.title());
// If the user hasn't read past here before, then we'll update their read

View File

@@ -1,6 +1,5 @@
import highlight from '../../common/helpers/highlight';
import LinkButton from '../../common/components/LinkButton';
import Link from '../../common/components/Link';
/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
@@ -24,7 +23,7 @@ export default class DiscussionsSearchSource {
include: 'mostRelevantPost',
};
return app.store.find('discussions', params, { search: query }).then((results) => (this.results[query] = results));
return app.store.find('discussions', params).then((results) => (this.results[query] = results));
}
view(query) {
@@ -48,10 +47,10 @@ export default class DiscussionsSearchSource {
return (
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
<a route={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
</Link>
</a>
</li>
);
}),

View File

@@ -1,6 +1,5 @@
import ComposerBody from './ComposerBody';
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import icon from '../../common/helpers/icon';
function minimizeComposerIfFullScreen(e) {
@@ -40,9 +39,9 @@ export default class EditPostComposer extends ComposerBody {
'title',
<h3>
{icon('fas fa-pencil-alt')}{' '}
<Link href={app.route.discussion(post.discussion(), post.number())} onclick={minimizeComposerIfFullScreen}>
<a route={app.route.discussion(post.discussion(), post.number())} onclick={minimizeComposerIfFullScreen}>
{app.translator.trans('core.forum.composer_edit.post_link', { number: post.number(), discussion: post.discussion().title() })}
</Link>
</a>
</h3>
);
@@ -96,13 +95,10 @@ export default class EditPostComposer extends ComposerBody {
},
app.translator.trans('core.forum.composer_edit.view_button')
);
alert = app.alerts.show(
{
type: 'success',
controls: [viewButton],
},
app.translator.trans('core.forum.composer_edit.edited_message')
);
alert = app.alerts.show(app.translator.trans('core.forum.composer_edit.edited_message'), {
type: 'success',
controls: [viewButton],
});
}
this.composer.hide();

View File

@@ -4,7 +4,6 @@ import GroupBadge from '../../common/components/GroupBadge';
import Group from '../../common/models/Group';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
/**
* The `EditUserModal` component displays a modal dialog with a login form.
@@ -15,17 +14,17 @@ export default class EditUserModal extends Modal {
const user = this.attrs.user;
this.username = Stream(user.username() || '');
this.email = Stream(user.email() || '');
this.isEmailConfirmed = Stream(user.isEmailConfirmed() || false);
this.setPassword = Stream(false);
this.password = Stream(user.password() || '');
this.username = m.stream(user.username() || '');
this.email = m.stream(user.email() || '');
this.isEmailConfirmed = m.stream(user.isEmailConfirmed() || false);
this.setPassword = m.stream(false);
this.password = m.stream(user.password() || '');
this.groups = {};
app.store
.all('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.forEach((group) => (this.groups[group.id()] = Stream(user.groups().indexOf(group) !== -1)));
.forEach((group) => (this.groups[group.id()] = m.stream(user.groups().indexOf(group) !== -1)));
}
className() {

View File

@@ -2,7 +2,6 @@ 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';
/**
* The `EventPost` component displays a post which indicating a discussion
@@ -30,9 +29,9 @@ export default class EventPost extends Post {
const data = Object.assign(this.descriptionData(), {
user,
username: user ? (
<Link className="EventPost-user" href={app.route.user(user)}>
<a className="EventPost-user" route={app.route.user(user)}>
{username}
</Link>
</a>
) : (
username
),

View File

@@ -1,7 +1,6 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import extractText from '../../common/utils/extractText';
import Stream from '../../common/utils/Stream';
/**
* The `ForgotPasswordModal` component displays a modal which allows the user to
@@ -20,7 +19,7 @@ export default class ForgotPasswordModal extends Modal {
*
* @type {Function}
*/
this.email = Stream(this.attrs.email || '');
this.email = m.stream(this.attrs.email || '');
/**
* Whether or not the password reset email was sent successfully.

View File

@@ -42,7 +42,26 @@ export default class IndexPage extends Page {
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
this.bodyClass = 'App--index';
this.scrollTopOnCreate = false;
this.currentPath = m.route.get();
}
onbeforeupdate(vnode) {
super.onbeforeupdate(vnode);
const curPath = m.route.get();
if (this.currentPath !== curPath) {
this.onNewRoute();
app.discussions.clear();
app.discussions.refreshParams(app.search.params());
this.currentPath = curPath;
this.setTitle();
}
}
view() {
@@ -86,22 +105,18 @@ export default class IndexPage extends Page {
$('#app').css('min-height', $(window).height() + heroHeight);
// Let browser handle scrolling on page reload.
if (app.previous.type == null) return;
// When on mobile, only retain scroll if we're coming from a discussion page.
// Otherwise, we've just changed the filter, so we should go to the top of the page.
if (app.screen() == 'desktop' || app.screen() == 'desktop-hd' || this.lastDiscussion) {
$(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
} else {
$(window).scrollTop(0);
}
// Scroll to the remembered position. We do this after a short delay so that
// it happens after the browser has done its own "back button" scrolling,
// which isn't right. https://github.com/flarum/core/issues/835
const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
scroll();
setTimeout(scroll, 1);
// If we've just returned from a discussion page, then the constructor will
// have set the `lastDiscussion` property. If this is the case, we want to
// scroll down to that discussion so that it's in view.
if (this.lastDiscussion) {
const $discussion = this.$(`li[data-id="${this.lastDiscussion.id()}"] .DiscussionListItem`);
const $discussion = this.$(`.DiscussionListItem[data-id="${this.lastDiscussion.id()}"]`);
if ($discussion.length) {
const indexTop = $('#header').outerHeight();
@@ -116,16 +131,14 @@ export default class IndexPage extends Page {
}
}
onbeforeremove() {
// Save the scroll position so we can restore it when we return to the
// discussion list.
app.cache.scrollTop = $(window).scrollTop();
}
onremove() {
super.onremove();
$('#app').css('min-height', '');
// Save the scroll position so we can restore it when we return to the
// discussion list.
app.cache.scrollTop = $(window).scrollTop();
}
/**

View File

@@ -5,7 +5,6 @@ import Button from '../../common/components/Button';
import LogInButtons from './LogInButtons';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
/**
* The `LogInModal` component displays a modal dialog with a login form.
@@ -24,21 +23,21 @@ export default class LogInModal extends Modal {
*
* @type {Function}
*/
this.identification = Stream(this.attrs.identification || '');
this.identification = m.stream(this.attrs.identification || '');
/**
* The value of the password input.
*
* @type {Function}
*/
this.password = Stream(this.attrs.password || '');
this.password = m.stream(this.attrs.password || '');
/**
* The value of the remember me input.
*
* @type {Function}
*/
this.remember = Stream(!!this.attrs.remember);
this.remember = m.stream(!!this.attrs.remember);
}
className() {

View File

@@ -3,7 +3,6 @@ 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';
/**
* The `Notification` component abstract displays a single notification.
@@ -20,11 +19,13 @@ export default class Notification extends Component {
const notification = this.attrs.notification;
const href = this.href();
const linkAttrs = {};
linkAttrs[href.indexOf('://') === -1 ? 'route' : 'href'] = href;
return (
<Link
<a
className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
href={href}
external={href.includes('://')}
{...linkAttrs}
onclick={this.markAsRead.bind(this)}
>
{!notification.isRead() &&
@@ -44,7 +45,7 @@ export default class Notification extends Component {
<span className="Notification-content">{this.content()}</span>
{humanTime(notification.createdAt())}
<div className="Notification-excerpt">{this.excerpt()}</div>
</Link>
</a>
);
}

View File

@@ -1,7 +1,6 @@
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';
@@ -64,10 +63,10 @@ export default class NotificationList extends Component {
return (
<div className="NotificationGroup">
{group.discussion ? (
<Link className="NotificationGroup-header" href={app.route.discussion(group.discussion)}>
<a className="NotificationGroup-header" route={app.route.discussion(group.discussion)}>
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
{group.discussion.title()}
</Link>
</a>
) : (
<div className="NotificationGroup-header">{app.forum.attribute('title')}</div>
)}

View File

@@ -1,5 +1,4 @@
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';
@@ -19,12 +18,12 @@ export default class PostPreview extends Component {
const excerpt = highlight(post.contentPlain(), this.attrs.highlight, 300);
return (
<Link className="PostPreview" href={app.route.post(post)} onclick={this.attrs.onclick}>
<a className="PostPreview" route={app.route.post(post)} onclick={this.attrs.onclick}>
<span className="PostPreview-content">
{avatar(user)}
{username(user)} <span className="PostPreview-excerpt">{excerpt}</span>
</span>
</Link>
</a>
);
}
}

View File

@@ -26,19 +26,17 @@ export default class PostStream extends Component {
}
view() {
function fadeIn(element, isInitialized, context) {
if (!context.fadedIn) $(element).hide().fadeIn();
context.fadedIn = true;
}
let lastTime;
const viewingEnd = this.stream.viewingEnd();
const posts = this.stream.posts();
const postIds = this.discussion.postIds();
const postFadeIn = (vnode) => {
$(vnode.dom).addClass('fadeIn');
// 500 is the duration of the fadeIn CSS animation + 100ms,
// so the animation has time to complete
setTimeout(() => $(vnode.dom).removeClass('fadeIn'), 500);
};
const items = posts.map((post, i) => {
let content;
const attrs = { 'data-index': this.stream.visibleStart + i };
@@ -49,13 +47,13 @@ export default class PostStream extends Component {
content = PostComponent ? PostComponent.component({ post }) : '';
attrs.key = 'post' + post.id();
attrs.oncreate = postFadeIn;
attrs.config = fadeIn;
attrs['data-time'] = time.toISOString();
attrs['data-number'] = post.number();
attrs['data-id'] = post.id();
attrs['data-type'] = post.contentType();
// If the post before this one was more than 4 days ago, we will
// If the post before this one was more than 4 hours ago, we will
// display a 'time gap' indicating how long it has been in between
// the posts.
const dt = time - lastTime;
@@ -97,7 +95,7 @@ export default class PostStream extends Component {
// is not already doing so, then show a 'write a reply' placeholder.
if (viewingEnd && (!app.session.user || this.discussion.canReply())) {
items.push(
<div className="PostStream-item" key="reply" data-index={this.stream.count()} oncreate={postFadeIn}>
<div className="PostStream-item" key="reply">
{ReplyPlaceholder.component({ discussion: this.discussion })}
</div>
);
@@ -129,16 +127,24 @@ export default class PostStream extends Component {
* Start scrolling, if appropriate, to a newly-targeted post.
*/
triggerScroll() {
if (!this.stream.needsScroll) return;
if (!this.attrs.targetPost) return;
const target = this.stream.targetPost;
this.stream.needsScroll = false;
const oldTarget = this.prevTarget;
const newTarget = this.attrs.targetPost;
if ('number' in target) {
this.scrollToNumber(target.number, this.stream.animateScroll);
} else if ('index' in target) {
this.scrollToIndex(target.index, this.stream.animateScroll, target.reply);
if (oldTarget) {
if ('number' in oldTarget && oldTarget.number === newTarget.number) return;
if ('index' in oldTarget && oldTarget.index === newTarget.index) return;
}
if ('number' in newTarget) {
this.scrollToNumber(newTarget.number, this.stream.noAnimationScroll);
} else if ('index' in newTarget) {
const backwards = newTarget.index === this.stream.count() - 1;
this.scrollToIndex(newTarget.index, this.stream.noAnimationScroll, backwards);
}
this.prevTarget = newTarget;
}
/**
@@ -188,9 +194,9 @@ export default class PostStream extends Component {
// seen if the browser were scrolled right up to the top of the page,
// and the viewport had a height of 0.
const $items = this.$('.PostStream-item[data-index]');
let index = $items.first().data('index') || 0;
let visible = 0;
let period = '';
let indexFromViewPort = null;
// Now loop through each of the items in the discussion. An 'item' is
// either a single post or a 'gap' of one or more posts that haven't
@@ -216,10 +222,8 @@ export default class PostStream extends Component {
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
const visiblePost = visibleBottom - visibleTop;
// We take the index of the first item that passed the previous checks.
// It is the item that is first visible in the viewport.
if (indexFromViewPort === null) {
indexFromViewPort = parseFloat($this.data('index')) + visibleTop / height;
if (top <= viewportTop) {
index = parseFloat($this.data('index')) + visibleTop / height;
}
if (visiblePost > 0) {
@@ -232,10 +236,7 @@ export default class PostStream extends Component {
if (time) period = time;
});
// If indexFromViewPort is null, it means no posts are visible in the
// viewport. This can happen, when drafting a long reply post. In that case
// set the index to the last post.
this.stream.index = indexFromViewPort !== null ? indexFromViewPort + 1 : this.stream.count();
this.stream.index = index + 1;
this.stream.visible = visible;
if (period) this.stream.description = dayjs(period).format('MMMM YYYY');
}
@@ -308,17 +309,18 @@ export default class PostStream extends Component {
*
* @param {Integer} index
* @param {Boolean} animate
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
* at the given index, instead of the top of it.
* @return {jQuery.Deferred}
*/
scrollToIndex(index, animate, reply) {
const $item = reply ? $('.PostStream-item:last-child') : this.$(`.PostStream-item[data-index=${index}]`);
scrollToIndex(index, animate, bottom) {
const $item = this.$(`.PostStream-item[data-index=${index}]`);
this.scrollToItem($item, animate, true, reply);
if (reply) {
this.flashItem($item);
}
return this.scrollToItem($item, animate, true, bottom).then(() => {
if (index == this.stream.count() - 1) {
this.flashItem(this.$('.PostStream-item:last-child'));
}
});
}
/**
@@ -328,12 +330,12 @@ export default class PostStream extends Component {
* @param {Boolean} animate
* @param {Boolean} force Whether or not to force scrolling to the item, even
* if it is already in the viewport.
* @param {Boolean} reply Whether or not to scroll to the reply placeholder.
* @param {Boolean} bottom Whether or not to scroll to the bottom of the post
* at the given index, instead of the top of it.
* @return {jQuery.Deferred}
*/
scrollToItem($item, animate, force, reply) {
scrollToItem($item, animate, force, bottom) {
const $container = $('html, body').stop(true);
const index = $item.data('index');
if ($item.length) {
const itemTop = $item.offset().top - this.getMarginTop();
@@ -342,10 +344,10 @@ export default class PostStream extends Component {
const scrollBottom = scrollTop + $(window).height();
// If the item is already in the viewport, we may not need to scroll.
// If we're scrolling to the reply placeholder, we'll make sure its
// If we're scrolling to the bottom of an item, then we'll make sure the
// bottom will line up with the top of the composer.
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
const top = reply ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
const top = bottom ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
if (!animate) {
$container.scrollTop(top);
@@ -355,43 +357,12 @@ export default class PostStream extends Component {
}
}
const updateScrubberHeight = () => {
// We manually set the index because we want to display the index of the
// exact post we've scrolled to, not just that of the first post within viewport.
this.updateScrubber();
if (index !== undefined) this.stream.index = index + 1;
};
// If we don't update this before the scroll, the scrubber will start
// at the top, and animate down, which can be confusing
updateScrubberHeight();
this.stream.forceUpdateScrubber = true;
return Promise.all([$container.promise(), this.stream.loadPromise]).then(() => {
this.updateScrubber();
const index = $item.data('index');
m.redraw.sync();
// Rendering post contents will probably throw off our position.
// To counter this, we'll scroll either:
// - To the reply placeholder (aligned with composer top)
// - To the top of the page if we're on the first post
// - To the top of a post (if that post exists)
// If the post does not currently exist, it's probably
// outside of the range we loaded in, so we won't adjust anything,
// as it will soon be rendered by the "load more" system.
let itemOffset;
if (reply) {
const $placeholder = $('.PostStream-item:last-child');
$(window).scrollTop($placeholder.offset().top + $placeholder.height() - $(window).height() + app.composer.computedHeight());
} else if (index === 0) {
$(window).scrollTop(0);
} else if ((itemOffset = $(`.PostStream-item[data-index=${index}]`).offset())) {
$(window).scrollTop(itemOffset.top - this.getMarginTop());
}
// We want to adjust this again after posts have been loaded in
// and position adjusted so that the scrubber's height is accurate.
updateScrubberHeight();
const scroll = index == 0 ? 0 : $(`.PostStream-item[data-index=${$item.data('index')}]`).offset().top - this.getMarginTop();
$(window).scrollTop(scroll);
this.calculatePosition();
this.stream.paused = false;
});
@@ -403,11 +374,6 @@ export default class PostStream extends Component {
* @param {jQuery} $item
*/
flashItem($item) {
// This might execute before the fadeIn class has been removed in PostStreamItem's
// oncreate, so we remove it just to be safe and avoid a double animation.
$item.removeClass('fadeIn');
$item.addClass('flash').on('animationend webkitAnimationEnd', (e) => {
$item.removeClass('flash');
});
$item.addClass('flash').one('animationend webkitAnimationEnd', () => $item.removeClass('flash'));
}
}

View File

@@ -90,10 +90,7 @@ export default class PostStreamScrubber extends Component {
}
onupdate() {
if (this.stream.forceUpdateScrubber) {
this.stream.forceUpdateScrubber = false;
this.stream.loadPromise.then(() => this.updateScrubberValues({ animate: true, forceHeightChange: true }));
}
this.stream.loadPromise.then(() => this.updateScrubberValues({ animate: true, forceHeightChange: true }));
}
oncreate(vnode) {
@@ -140,7 +137,7 @@ export default class PostStreamScrubber extends Component {
setTimeout(() => this.scrollListener.start());
this.stream.loadPromise.then(() => this.updateScrubberValues({ animate: false, forceHeightChange: true }));
this.updateScrubberValues({ animate: true, forceHeightChange: true });
}
onremove() {

View File

@@ -1,5 +1,4 @@
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';
@@ -41,11 +40,11 @@ export default class PostUser extends Component {
return (
<div className="PostUser">
<h3>
<Link href={app.route.user(user)}>
<a route={app.route.user(user)}>
{avatar(user, { className: 'PostUser-avatar' })}
{userOnline(user)}
{username(user)}
</Link>
</a>
</h3>
<ul className="PostUser-badges badges">{listItems(user.badges().toArray())}</ul>
{card}

View File

@@ -1,7 +1,6 @@
import UserPage from './UserPage';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Button from '../../common/components/Button';
import Link from '../../common/components/Link';
import Placeholder from '../../common/components/Placeholder';
import CommentPost from './CommentPost';
@@ -74,7 +73,7 @@ export default class PostsUserPage extends UserPage {
<li>
<div className="PostsUserPage-discussion">
{app.translator.trans('core.forum.user.in_discussion_text', {
discussion: <Link href={app.route.post(post)}>{post.discussion().title()}</Link>,
discussion: <a route={app.route.post(post)}>{post.discussion().title()}</a>,
})}
</div>

View File

@@ -1,6 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Stream from '../../common/utils/Stream';
/**
* The 'RenameDiscussionModal' displays a modal dialog with an input to rename a discussion
@@ -11,7 +10,7 @@ export default class RenameDiscussionModal extends Modal {
this.discussion = this.attrs.discussion;
this.currentTitle = this.attrs.currentTitle;
this.newTitle = Stream(this.currentTitle);
this.newTitle = m.stream(this.currentTitle);
}
className() {

View File

@@ -1,6 +1,5 @@
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';
@@ -37,9 +36,9 @@ export default class ReplyComposer extends ComposerBody {
'title',
<h3>
{icon('fas fa-reply')}{' '}
<Link href={app.route.discussion(discussion)} onclick={minimizeComposerIfFullScreen}>
<a route={app.route.discussion(discussion)} onclick={minimizeComposerIfFullScreen}>
{discussion.title()}
</Link>
</a>
</h3>
);
@@ -99,13 +98,10 @@ export default class ReplyComposer extends ComposerBody {
},
app.translator.trans('core.forum.composer_reply.view_button')
);
alert = app.alerts.show(
{
type: 'success',
controls: [viewButton],
},
app.translator.trans('core.forum.composer_reply.posted_message')
);
alert = app.alerts.show(app.translator.trans('core.forum.composer_reply.posted_message'), {
type: 'success',
controls: [viewButton],
});
}
this.composer.hide();

View File

@@ -33,7 +33,7 @@ export default class ReplyPlaceholder extends Component {
}
const reply = () => {
DiscussionControls.replyAction.call(this.attrs.discussion, true).catch(() => {});
DiscussionControls.replyAction.call(this.attrs.discussion, true);
};
return (

View File

@@ -4,7 +4,6 @@ import Button from '../../common/components/Button';
import LogInButtons from './LogInButtons';
import extractText from '../../common/utils/extractText';
import ItemList from '../../common/utils/ItemList';
import Stream from '../../common/utils/Stream';
/**
* The `SignUpModal` component displays a modal dialog with a singup form.
@@ -25,21 +24,21 @@ export default class SignUpModal extends Modal {
*
* @type {Function}
*/
this.username = Stream(this.attrs.username || '');
this.username = m.stream(this.attrs.username || '');
/**
* The value of the email input.
*
* @type {Function}
*/
this.email = Stream(this.attrs.email || '');
this.email = m.stream(this.attrs.email || '');
/**
* The value of the password input.
*
* @type {Function}
*/
this.password = Stream(this.attrs.password || '');
this.password = m.stream(this.attrs.password || '');
}
className() {
@@ -175,7 +174,7 @@ export default class SignUpModal extends Modal {
* Get the data that should be submitted in the sign-up request.
*
* @return {Object}
* @protected
* @public
*/
submitData() {
const data = {

View File

@@ -6,7 +6,6 @@ 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';
@@ -51,10 +50,10 @@ export default class UserCard extends Component {
{this.attrs.editable ? (
[AvatarEditor.component({ user, className: 'UserCard-avatar' }), username(user)]
) : (
<Link href={app.route.user(user)}>
<a route={app.route.user(user)}>
<div className="UserCard-avatar">{avatar(user)}</div>
{username(user)}
</Link>
</a>
)}
</h2>

View File

@@ -27,6 +27,17 @@ export default class UserPage extends Page {
this.user = null;
this.bodyClass = 'App--user';
this.prevUsername = m.route.param('username');
}
onbeforeupdate() {
const currUsername = m.route.param('username');
if (currUsername !== this.prevUsername) {
this.prevUsername = currUsername;
this.loadUser(currUsername);
}
}
view() {
@@ -135,7 +146,7 @@ export default class UserPage extends Page {
items.add(
'posts',
<LinkButton href={app.route('user.posts', { username: user.username() })} icon="far fa-comment">
<LinkButton href={app.route('user.posts', { username: user.username() })} force icon="far fa-comment">
{app.translator.trans('core.forum.user.posts_link')} <span className="Button-badge">{user.commentCount()}</span>
</LinkButton>,
100
@@ -143,7 +154,7 @@ export default class UserPage extends Page {
items.add(
'discussions',
<LinkButton href={app.route('user.discussions', { username: user.username() })} icon="fas fa-bars">
<LinkButton href={app.route('user.discussions', { username: user.username() })} force icon="fas fa-bars">
{app.translator.trans('core.forum.user.discussions_link')} <span className="Button-badge">{user.discussionCount()}</span>
</LinkButton>,
90

View File

@@ -1,7 +1,6 @@
import highlight from '../../common/helpers/highlight';
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Link from '../../common/components/Link';
/**
* The `UsersSearchSource` finds and displays user search results in the search
@@ -16,14 +15,10 @@ export default class UsersSearchResults {
search(query) {
return app.store
.find(
'users',
{
filter: { q: query },
page: { limit: 5 },
},
{ search: query }
)
.find('users', {
filter: { q: query },
page: { limit: 5 },
})
.then((results) => {
this.results[query] = results;
m.redraw();
@@ -53,10 +48,10 @@ export default class UsersSearchResults {
return (
<li className="UserSearchResult" data-index={'users' + user.id()}>
<Link href={app.route.user(user)}>
<a route={app.route.user(user)}>
{avatar(user)}
{{ ...name, text: undefined, children }}
</Link>
</a>
</li>
);
}),

View File

@@ -1,49 +0,0 @@
import DefaultResolver from '../../common/resolvers/DefaultResolver';
import DiscussionPage from '../components/DiscussionPage';
/**
* This isn't exported as it is a temporary measure.
* A more robust system will be implemented alongside UTF-8 support in beta 15.
*/
function getDiscussionIdFromSlug(slug: string | undefined) {
if (!slug) return;
return slug.split('-')[0];
}
/**
* A custom route resolver for DiscussionPage that generates the same key to all posts
* on the same discussion. It triggers a scroll when going from one post to another
* in the same discussion.
*/
export default class DiscussionPageResolver extends DefaultResolver {
static scrollToPostNumber: string | null = null;
makeKey() {
const params = { ...m.route.param() };
if ('near' in params) {
delete params.near;
}
params.id = getDiscussionIdFromSlug(params.id);
return this.routeName.replace('.near', '') + JSON.stringify(params);
}
onmatch(args, requestedPath, route) {
if (app.current.matches(DiscussionPage) && getDiscussionIdFromSlug(args.id) === getDiscussionIdFromSlug(m.route.param('id'))) {
// By default, the first post number of any discussion is 1
DiscussionPageResolver.scrollToPostNumber = args.near || '1';
}
return super.onmatch(args, requestedPath, route);
}
render(vnode) {
if (DiscussionPageResolver.scrollToPostNumber !== null) {
const number = DiscussionPageResolver.scrollToPostNumber;
// Scroll after a timeout to avoid clashes with the render.
setTimeout(() => app.current.get('stream').goToNumber(number));
DiscussionPageResolver.scrollToPostNumber = null;
}
return super.render(vnode);
}
}

View File

@@ -4,7 +4,6 @@ import PostsUserPage from './components/PostsUserPage';
import DiscussionsUserPage from './components/DiscussionsUserPage';
import SettingsPage from './components/SettingsPage';
import NotificationsPage from './components/NotificationsPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
/**
* The `routes` initializer defines the forum app's routes.
@@ -15,8 +14,8 @@ export default function (app) {
app.routes = {
index: { path: '/all', component: IndexPage },
discussion: { path: '/d/:id', component: DiscussionPage, resolverClass: DiscussionPageResolver },
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage, resolverClass: DiscussionPageResolver },
discussion: { path: '/d/:id', component: DiscussionPage },
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage },
user: { path: '/u/:username', component: PostsUserPage },
'user.posts': { path: '/u/:username', component: PostsUserPage },

View File

@@ -1,5 +1,4 @@
import subclassOf from '../../common/utils/subclassOf';
import Stream from '../../common/utils/Stream';
import ReplyComposer from '../components/ReplyComposer';
class ComposerState {
@@ -75,7 +74,7 @@ class ComposerState {
this.onExit = null;
this.fields = {
content: Stream(''),
content: m.stream(''),
};
/**

View File

@@ -77,23 +77,17 @@ export default class DiscussionListState {
}
/**
* Clear and reload the discussion list. Passing the option `deferClear: true`
* will clear discussions only after new data has been received.
* This can be used to refresh discussions without loading animations.
* Clear and reload the discussion list.
*/
refresh({ deferClear = false } = {}) {
refresh({ clear = true } = {}) {
this.loading = true;
if (!deferClear) {
if (clear) {
this.clear();
}
return this.loadResults().then(
(results) => {
// This ensures that any changes made while waiting on this request
// are ignored. Otherwise, we could get duplicate discussions.
// We don't use `this.clear()` to avoid an unnecessary redraw.
this.discussions = [];
this.parseResults(results);
},
() => {
@@ -119,7 +113,7 @@ export default class DiscussionListState {
params.page = { offset };
params.include = params.include.join(',');
return this.app.store.find('discussions', params, { search: params.filter.q });
return this.app.store.find('discussions', params);
}
/**

View File

@@ -2,8 +2,9 @@ import setRouteWithForcedRefresh from '../../common/utils/setRouteWithForcedRefr
import SearchState from './SearchState';
export default class GlobalSearchState extends SearchState {
constructor(cachedSearches = []) {
constructor(cachedSearches = [], searchRoute = 'index') {
super(cachedSearches);
this.searchRoute = searchRoute;
}
getValue() {
@@ -90,6 +91,6 @@ export default class GlobalSearchState extends SearchState {
const params = this.params();
delete params.q;
setRouteWithForcedRefresh(app.route(app.current.get('routeName'), params));
setRouteWithForcedRefresh(app.route(this.searchRoute, params));
}
}

View File

@@ -37,18 +37,6 @@ class PostStreamState {
*/
this.description = '';
/**
* When the page is scrolled, goToIndex is called, or the page is loaded,
* various listeners result in the scrubber being updated with a new
* position and values. However, if goToNumber is called, the scrubber
* will not be updated. Accordingly, we add logic to the scrubber's
* onupdate to update itself, but only when needed, as indicated by this
* property.
*
* @type {Boolean}
*/
this.forceUpdateScrubber = false;
this.show(includedPosts);
}
@@ -96,18 +84,15 @@ class PostStreamState {
// If we want to go to the reply preview, then we will go to the end of the
// discussion and then scroll to the very bottom of the page.
if (number === 'reply') {
const resultPromise = this.goToLast();
this.targetPost.reply = true;
return resultPromise;
return this.goToLast();
}
this.paused = true;
this.loadPromise = this.loadNearNumber(number);
this.needsScroll = true;
this.targetPost = { number };
this.animateScroll = !noAnimation;
this.noAnimationScroll = noAnimation;
this.number = number;
// In this case, the redraw is only called after the response has been loaded
@@ -130,9 +115,8 @@ class PostStreamState {
this.loadPromise = this.loadNearIndex(index);
this.needsScroll = true;
this.targetPost = { index };
this.animateScroll = !noAnimation;
this.noAnimationScroll = noAnimation;
this.index = index;
m.redraw();
@@ -282,13 +266,7 @@ class PostStreamState {
}
});
if (loadIds.length) {
return app.store.find('posts', loadIds).then((newPosts) => {
return loaded.concat(newPosts).sort((a, b) => a.createdAt() - b.createdAt());
});
}
return Promise.resolve(loaded);
return loadIds.length ? app.store.find('posts', loadIds) : Promise.resolve(loaded);
}
/**
@@ -354,12 +332,7 @@ class PostStreamState {
* @return {boolean}
*/
viewingEnd() {
// In some cases, such as if we've stickied a post, an event post
// may have been added / removed. This means that `this.visibleEnd`
// and`this.count()` will be out of sync by 1 post, but we are still
// "viewing the end" of the post stream, so we should still reload
// all posts up until the last one.
return Math.abs(this.count() - this.visibleEnd) <= 1;
return this.visibleEnd === this.count();
}
/**

View File

@@ -61,9 +61,7 @@ export default {
onclick: () => {
// 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.replyAction
.bind(discussion)(true, false)
.catch(() => {});
return this.replyAction(discussion, true, false).catch(() => {});
},
},
app.translator.trans(

View File

@@ -129,7 +129,9 @@ export default {
error: 'core.forum.user_controls.delete_error_message',
}[type];
app.alerts.show({ type }, app.translator.trans(message, { username, email }));
app.alerts.show(app.translator.trans(message, { username, email }), {
type,
});
},
/**

View File

@@ -52,18 +52,13 @@ export default function alertEmailConfirmation(app) {
}
}
class ContainedAlert extends Alert {
view(vnode) {
const vdom = super.view(vnode);
return { ...vdom, children: [<div className="container">{vdom.children}</div>] };
}
}
m.mount($('<div/>').insertBefore('#content')[0], {
view: () => (
<ContainedAlert dismissible={false} controls={[<ResendButton />]}>
{app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: <strong>{user.email()}</strong> })}
</ContainedAlert>
<Alert dismissible={false} controls={[<ResendButton />]}>
<div className="container">
{app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: <strong>{user.email()}</strong> })}
</div>
</Alert>
),
});
}

View File

@@ -236,16 +236,12 @@
.App-header {
padding: 8px;
height: @header-height;
position: absolute;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: @zindex-header;
.affix & {
position: fixed;
}
& when (@config-colored-header = true) {
.light-contents(@header-color, @header-control-bg, @header-control-color);
}

View File

@@ -105,10 +105,6 @@
text-align: left;
}
}
.off.Checkbox--switch .Checkbox-display {
background: @muted-more-color;
}
}
.Modal-footer {
border: 0;

View File

@@ -271,8 +271,6 @@
}
}
.Post-footer {
display: inline-block;
height: 0;
margin-top: 5px;
margin-bottom: 20px;
@@ -290,6 +288,7 @@
margin-top: -5px;
float: right;
position: relative;
z-index: 1;
.transition(opacity 0.2s);

View File

@@ -6,7 +6,18 @@
margin-top: 10px;
}
}
@-webkit-keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
@keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
.PostStream-item {
.animation(fadeIn 0.6s ease-in-out);
&:not(:last-child) {
border-bottom: 1px solid @control-bg;
@@ -93,16 +104,3 @@
.animation(pulsate 0.2s ease-in-out);
.animation-iteration-count(1);
}
@-webkit-keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
@keyframes fadeIn {
0% {opacity: 0}
100% {opacity: 1}
}
.fadeIn {
.animation(fadeIn 0.4s ease-in-out);
.animation-iteration-count(1);
}

View File

@@ -17,7 +17,7 @@
top: 0;
bottom: 0;
left: 0;
width: 100%;
width: auto;
height: auto;
z-index: 0;
color: #fff !important;
@@ -36,15 +36,12 @@
.Slidable-underneath--left {
text-align: left;
}
.Slidable-underneath--right {
left: unset;
}
.Slidable-content {
.transition(~"box-shadow 0.2s, border-radius 0.2s");
.sliding& {
position: relative;
background: @control-bg;
background: #fff;
z-index: 2;
border-radius: 2px;
.box-shadow(0 2px 6px @shadow-color);

View File

@@ -9,8 +9,6 @@
namespace Flarum\Admin;
use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ErrorHandling\Registry;
use Flarum\Foundation\ErrorHandling\Reporter;
@@ -54,25 +52,20 @@ class AdminServiceProvider extends AbstractServiceProvider
HttpMiddleware\StartSession::class,
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\SetLocale::class,
'flarum.admin.route_resolver',
HttpMiddleware\CheckCsrfToken::class,
Middleware\RequireAdministrateAbility::class
HttpMiddleware\SetLocale::class,
Middleware\RequireAdministrateAbility::class,
];
});
$this->app->bind('flarum.admin.error_handler', function () {
return new HttpMiddleware\HandleErrors(
$this->app->make(Registry::class),
$this->app['flarum.config']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
$this->app['flarum']->inDebugMode() ? $this->app->make(WhoopsFormatter::class) : $this->app->make(ViewFormatter::class),
$this->app->tagged(Reporter::class)
);
});
$this->app->bind('flarum.admin.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.admin.routes'));
});
$this->app->singleton('flarum.admin.handler', function () {
$pipe = new MiddlewarePipe;
@@ -80,7 +73,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.admin.routes')));
return $pipe;
});
@@ -123,7 +116,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$events = $this->app->make('events');
$events->listen(
[Enabled::class, Disabled::class, ClearingCache::class],
ClearingCache::class,
function () {
$recompile = new RecompileFrontendAssets(
$this->app->make('flarum.assets.admin'),

View File

@@ -51,24 +51,19 @@ class ApiServiceProvider extends AbstractServiceProvider
HttpMiddleware\RememberFromCookie::class,
HttpMiddleware\AuthenticateWithSession::class,
HttpMiddleware\AuthenticateWithHeader::class,
HttpMiddleware\CheckCsrfToken::class,
HttpMiddleware\SetLocale::class,
'flarum.api.route_resolver',
HttpMiddleware\CheckCsrfToken::class
];
});
$this->app->bind('flarum.api.error_handler', function () {
return new HttpMiddleware\HandleErrors(
$this->app->make(Registry::class),
new JsonApiFormatter($this->app['flarum.config']->inDebugMode()),
new JsonApiFormatter($this->app['flarum']->inDebugMode()),
$this->app->tagged(Reporter::class)
);
});
$this->app->bind('flarum.api.route_resolver', function () {
return new HttpMiddleware\ResolveRoute($this->app->make('flarum.api.routes'));
});
$this->app->singleton('flarum.api.handler', function () {
$pipe = new MiddlewarePipe;
@@ -76,16 +71,10 @@ class ApiServiceProvider extends AbstractServiceProvider
$pipe->pipe($this->app->make($middleware));
}
$pipe->pipe(new HttpMiddleware\ExecuteRoute());
$pipe->pipe(new HttpMiddleware\DispatchRoute($this->app->make('flarum.api.routes')));
return $pipe;
});
$this->app->singleton('flarum.api.notification_serializers', function () {
return [
'discussionRenamed' => BasicDiscussionSerializer::class
];
});
}
/**
@@ -93,7 +82,7 @@ class ApiServiceProvider extends AbstractServiceProvider
*/
public function boot()
{
$this->setNotificationSerializers();
$this->registerNotificationSerializers();
AbstractSerializeController::setContainer($this->app);
AbstractSerializeController::setEventDispatcher($events = $this->app->make('events'));
@@ -105,12 +94,13 @@ class ApiServiceProvider extends AbstractServiceProvider
/**
* Register notification serializers.
*/
protected function setNotificationSerializers()
protected function registerNotificationSerializers()
{
$blueprints = [];
$serializers = $this->app->make('flarum.api.notification_serializers');
$serializers = [
'discussionRenamed' => BasicDiscussionSerializer::class
];
// Deprecated in beta 15, remove in beta 16
$this->app->make('events')->dispatch(
new ConfigureNotificationTypes($blueprints, $serializers)
);

View File

@@ -11,10 +11,10 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -49,14 +49,9 @@ class ListDiscussionsController extends AbstractListController
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionRepository
* @var DiscussionSearcher
*/
protected $discussions;
/**
* @var Filterer
*/
protected $filterer;
protected $searcher;
/**
* @var UrlGenerator
@@ -67,10 +62,9 @@ class ListDiscussionsController extends AbstractListController
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionRepository $discussions, Filterer $filterer, UrlGenerator $url)
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
{
$this->discussions = $discussions;
$this->filterer = $filterer;
$this->searcher = $searcher;
$this->url = $url;
}
@@ -80,16 +74,16 @@ class ListDiscussionsController extends AbstractListController
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$filters = $this->extractFilter($request);
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$query = $this->discussions->query();
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$results = $this->searcher->search($criteria, $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),

View File

@@ -10,9 +10,10 @@
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Filter\Filterer;
use Flarum\Http\UrlGenerator;
use Flarum\User\UserRepository;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -40,9 +41,9 @@ class ListUsersController extends AbstractListController
];
/**
* @var Filterer
* @var UserSearcher
*/
protected $filterer;
protected $searcher;
/**
* @var UrlGenerator
@@ -50,20 +51,13 @@ class ListUsersController extends AbstractListController
protected $url;
/**
* @var UserRepository
*/
protected $users;
/**
* @param Filterer $filterer
* @param UserSearcher $searcher
* @param UrlGenerator $url
* @param UserRepository $users
*/
public function __construct(Filterer $filterer, UrlGenerator $url, UserRepository $users)
public function __construct(UserSearcher $searcher, UrlGenerator $url)
{
$this->filterer = $filterer;
$this->searcher = $searcher;
$this->url = $url;
$this->users = $users;
}
/**
@@ -75,16 +69,16 @@ class ListUsersController extends AbstractListController
$actor->assertCan('viewUserList');
$query = $this->users->query();
$filters = $this->extractFilter($request);
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->filterer->filter($actor, $query, $filters, $sort, $limit, $offset, $load);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),

View File

@@ -1,112 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchDiscussionsController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = DiscussionSerializer::class;
/**
* {@inheritdoc}
*/
public $include = [
'user',
'lastPostedUser',
'mostRelevantPost',
'mostRelevantPost.user'
];
/**
* {@inheritdoc}
*/
public $optionalInclude = [
'firstPost',
'lastPost'
];
/**
* {@inheritdoc}
*/
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
/**
* @var DiscussionSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = array_merge($this->extractInclude($request), ['state']);
$results = $this->searcher->search($criteria, $limit, $offset);
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
Discussion::setStateUser($actor);
$results = $results->getResults()->load($load);
if ($relations = array_intersect($load, ['firstPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
}
}

View File

@@ -1,93 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class SearchUsersController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = UserSerializer::class;
/**
* {@inheritdoc}
*/
public $include = ['groups'];
/**
* {@inheritdoc}
*/
public $sortFields = [
'username',
'commentCount',
'discussionCount',
'lastSeenAt',
'joinedAt'
];
/**
* @var UserSearcher
*/
protected $searcher;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @param UserSearcher $searcher
* @param UrlGenerator $url
*/
public function __construct(UserSearcher $searcher, UrlGenerator $url)
{
$this->searcher = $searcher;
$this->url = $url;
}
/**
* {@inheritdoc}
*/
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
$actor->assertCan('viewUserList');
$query = Arr::get($this->extractFilter($request), 'q');
$sort = $this->extractSort($request);
$criteria = new SearchCriteria($actor, $query, $sort);
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$load = $this->extractInclude($request);
$results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),
$request->getQueryParams(),
$offset,
$limit,
$results->areMoreResults() ? null : 0
);
return $results->getResults();
}
}

View File

@@ -10,7 +10,6 @@
namespace Flarum\Api\Serializer;
use Flarum\Foundation\Application;
use Flarum\Foundation\Config;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
@@ -22,9 +21,9 @@ class ForumSerializer extends AbstractSerializer
protected $type = 'forums';
/**
* @var Config
* @var Application
*/
protected $config;
protected $app;
/**
* @var SettingsRepositoryInterface
@@ -37,13 +36,13 @@ class ForumSerializer extends AbstractSerializer
protected $url;
/**
* @param Config $config
* @param Application $app
* @param SettingsRepositoryInterface $settings
* @param UrlGenerator $url
*/
public function __construct(Config $config, SettingsRepositoryInterface $settings, UrlGenerator $url)
public function __construct(Application $app, SettingsRepositoryInterface $settings, UrlGenerator $url)
{
$this->config = $config;
$this->app = $app;
$this->settings = $settings;
$this->url = $url;
}
@@ -67,7 +66,7 @@ class ForumSerializer extends AbstractSerializer
'showLanguageSelector' => (bool) $this->settings->get('show_language_selector', true),
'baseUrl' => $url = $this->url->to('forum')->base(),
'basePath' => parse_url($url, PHP_URL_PATH) ?: '',
'debug' => $this->config->inDebugMode(),
'debug' => $this->app->inDebugMode(),
'apiUrl' => $this->url->to('api')->base(),
'welcomeTitle' => $this->settings->get('welcome_title'),
'welcomeMessage' => $this->settings->get('welcome_message'),

View File

@@ -95,13 +95,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\SendConfirmationEmailController::class)
);
// List users
$map->get(
'/search/users',
'users.search',
$route->toController(Controller\SearchUsersController::class)
);
/*
|--------------------------------------------------------------------------
| Notifications
@@ -170,13 +163,6 @@ return function (RouteCollection $map, RouteHandlerFactory $route) {
$route->toController(Controller\DeleteDiscussionController::class)
);
// Search discussions
$map->get(
'/search/discussions',
'discussions.search',
$route->toController(Controller\SearchDiscussionsController::class)
);
/*
|--------------------------------------------------------------------------
| Posts

View File

@@ -14,7 +14,6 @@ use Flarum\Database\Console\MigrateCommand;
use Flarum\Database\Console\ResetCommand;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\Console\CacheClearCommand;
use Flarum\Foundation\Console\InfoCommand;
class ConsoleServiceProvider extends AbstractServiceProvider
{
@@ -27,7 +26,6 @@ class ConsoleServiceProvider extends AbstractServiceProvider
return [
CacheClearCommand::class,
GenerateMigrationCommand::class,
InfoCommand::class,
MigrateCommand::class,
ResetCommand::class,
];

View File

@@ -82,7 +82,7 @@ abstract class AbstractModel extends Eloquent
}
$this->attributes = array_map(function ($item) {
return is_callable($item) ? $item($this) : $item;
return is_callable($item) ? $item() : $item;
}, $this->attributes);
parent::__construct($attributes);

View File

@@ -13,9 +13,6 @@ use Flarum\Notification\Blueprint\BlueprintInterface;
use InvalidArgumentException;
use ReflectionClass;
/**
* @deprecated in beta 15, removed in beta 16
*/
class ConfigureNotificationTypes
{
/**

View File

@@ -9,9 +9,6 @@
namespace Flarum\Event;
/**
* @deprecated in beta 15, remove in beta 16. Use the Post extender instead.
*/
class ConfigurePostTypes
{
private $models;

View File

@@ -14,28 +14,11 @@ use Illuminate\Contracts\Container\Container;
class Csrf implements ExtenderInterface
{
protected $csrfExemptRoutes = [];
protected $csrfExemptPaths = [];
/**
* Exempt a named route from CSRF checks.
*
* @param string $routeName
*/
public function exemptRoute(string $routeName)
{
$this->csrfExemptRoutes[] = $routeName;
return $this;
}
/**
* Exempt a path from csrf checks. Wildcards are supported.
*
* @deprecated beta 15, remove beta 16. Exempt routes should be used instead.
*/
public function exemptPath(string $path)
{
$this->csrfExemptRoutes[] = $path;
$this->csrfExemptPaths[] = $path;
return $this;
}
@@ -43,7 +26,7 @@ class Csrf implements ExtenderInterface
public function extend(Container $container, Extension $extension = null)
{
$container->extend('flarum.http.csrfExemptPaths', function ($existingExemptPaths) {
return array_merge($existingExemptPaths, $this->csrfExemptRoutes);
return array_merge($existingExemptPaths, $this->csrfExemptPaths);
});
}
}

Some files were not shown because too many files have changed in this diff Show More