mirror of
https://github.com/flarum/core.git
synced 2025-08-13 20:04:24 +02:00
Compare commits
88 Commits
sm/fulltex
...
dk/DISCUSS
Author | SHA1 | Date | |
---|---|---|---|
|
dff35c1046 | ||
|
f6c9bbb427 | ||
|
feb968780a | ||
|
5b89d3e91a | ||
|
ba7599e6fe | ||
|
80b34d1164 | ||
|
3accdc322c | ||
|
4247e54c64 | ||
|
ef35faaded | ||
|
715b8c39ae | ||
|
232618aba6 | ||
|
96e1411b7d | ||
|
21b483625e | ||
|
9363682e1c | ||
|
c766881e1f | ||
|
e63e161be6 | ||
|
3264455068 | ||
|
d7fcd8a9e5 | ||
|
b4f3f0558e | ||
|
919c3bb770 | ||
|
7298ccb301 | ||
|
cfdd6910eb | ||
|
7ebeb9c0a5 | ||
|
af3f91ca5b | ||
|
4784307e26 | ||
|
105b22976e | ||
|
fea31a8290 | ||
|
accdfde6e1 | ||
|
7684a1086a | ||
|
f8577c8078 | ||
|
e55844f3db | ||
|
1d20f4d4aa | ||
|
803f0cd0f4 | ||
|
8576df1a43 | ||
|
1792e22639 | ||
|
5e281136f6 | ||
|
b868c3d763 | ||
|
297a2d8c5c | ||
|
c0af41c305 | ||
|
d0669b08aa | ||
|
6b8e9ce1db | ||
|
fbbece4bda | ||
|
13e655aca5 | ||
|
c00e8706e1 | ||
|
1b5da13e8a | ||
|
ecfbcd1c30 | ||
|
818a100625 | ||
|
176b5540d8 | ||
|
2e76a8ecb5 | ||
|
11aa7bbb35 | ||
|
3a26c29935 | ||
|
94e92cf24e | ||
|
aa33cfd1f8 | ||
|
4901c586ce | ||
|
7a6d477550 | ||
|
b89a01c010 | ||
|
8b11fef3ee | ||
|
8a114cd826 | ||
|
62c93b4a05 | ||
|
fab71f2d01 | ||
|
e8c867dcac | ||
|
1247a7f1dd | ||
|
b0aad1a2d6 | ||
|
bddc9d96f2 | ||
|
d684248492 | ||
|
85b63681ae | ||
|
8372363cc2 | ||
|
a6a067ad48 | ||
|
241eba4d0c | ||
|
a6b12826c3 | ||
|
dd868ab44e | ||
|
5f3e0d6a09 | ||
|
661b9d7d9a | ||
|
b7498d6cb1 | ||
|
e7c55532a0 | ||
|
cce6b74fce | ||
|
da651c722b | ||
|
abc9670659 | ||
|
b66fe5dd5f | ||
|
7d79895ae0 | ||
|
3ab4529232 | ||
|
360a2ba1d8 | ||
|
eaa4063fef | ||
|
72d277bd45 | ||
|
28e3ccfde6 | ||
|
3f864bafc8 | ||
|
3af0481f30 | ||
|
1761660c98 |
2
.github/workflows/REUSABLE_frontend.yml
vendored
2
.github/workflows/REUSABLE_frontend.yml
vendored
@@ -118,7 +118,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.0'
|
||||
php-version: '8.2'
|
||||
extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip
|
||||
tools: composer:v2
|
||||
|
||||
|
67
CHANGELOG.md
67
CHANGELOG.md
@@ -1,6 +1,71 @@
|
||||
# Changelog
|
||||
|
||||
# [v1.6.3](https://github.com/flarum/framework/compare/v1.6.2...v1.6.3)
|
||||
## [v1.7.1](https://github.com/flarum/framework/compare/v1.7.0...v1.7.1)
|
||||
### Fixed
|
||||
- (tags) composer tag selection modal using wrong primary max & min numbers (abc9670659426b765274376945b818b70d84848c)
|
||||
- missing parameter names in token title translation. (#3752)
|
||||
- hardcoded language strings in StatusWidget (#3754)
|
||||
- hide developer tokens section in if there is nothing to display or create (#3753)
|
||||
- improve sessions user UI on mobile (dd868ab44e11e892d020e3b9412553c6a789e68d)
|
||||
|
||||
## [v1.7.0](https://github.com/flarum/framework/compare/v1.6.3...v1.7.0)
|
||||
### Added
|
||||
- (actions) allow running JS tests in GH actions [#3730]
|
||||
- (core) PHP 8.2 Support [#3709]
|
||||
- (jest) create jest config package for unit testing [#3678]
|
||||
- (jest) mithril component testing [#3679]
|
||||
- (phpstan) foundation for usage in extensions [#3666]
|
||||
- (seo) Do not use h3 header for poster author in posts stream [#3732]
|
||||
- (seo) Use h2 header for discussions on discussions list [#3731]
|
||||
- (seo) shift h1 tag from logo to discussion title [#3724]
|
||||
- (tags) admin tag selection component (reusable tag selection modal) [#3686]
|
||||
- Admin User Search [#3712]
|
||||
- access tokens user management UI [#3587]
|
||||
- add display name column to admin users list [#3740]
|
||||
- allow push additional items to the end of the poststream [#3691]
|
||||
- allow using utf8 characters in tag slugs [#3588]
|
||||
- expose queue driver, schedule status [#3593]
|
||||
- expose {time} to eventPost data, fix renamed tooltip [#3698]
|
||||
- frontend `Model` extender [#3646]
|
||||
- global logout to clear all sessions, access tokens, email tokens and password tokens [#3605]
|
||||
- improved page navigation for users list [#3741]
|
||||
- introduce frontend extenders [#3645]
|
||||
### Fixed
|
||||
- (mentions) correctly convert a 3 char. hex color to a 6 char. one [#3694]
|
||||
- (mentions) post reply mention missing notification on approval [#3738]
|
||||
- (phpstan) adapt phpstan package for extension use [#3727]
|
||||
- (tags) clickable tag labels have underline [#3737]
|
||||
- (tags) tag text color contrast [#3653]
|
||||
- 3 digit hex color value in color input not supported [#3706]
|
||||
- column `id` can be ambiguous in group filter with extensions [#3696]
|
||||
- disallow certain dangerous LESS features ([1761660](1761660c98ea5a3e9665fb8e6041d1f2ee62a444))
|
||||
- evaluated page title content [#3684]
|
||||
- invalid translation key for scheduler dashboard [#3736]
|
||||
- load actor.groups on showforumcontroller [#3716]
|
||||
- make go-to-page input number-like [#3743]
|
||||
- normal logout affects all sessions [#3571]
|
||||
- permissions table on mobile is unusable [#3722]
|
||||
- post dropdown opens all dropdowns in `.Post-actions` [#3675]
|
||||
- typo in Formatter extender docblock [#3676]
|
||||
- undefined showing in dropdown active title [#3700]
|
||||
### Changed
|
||||
- (phpstan) enable phpstan in bundled extensions [#3667]
|
||||
- Add missing states exports to `compat.ts` [#3683]
|
||||
- Indicate cross-origin request in generic error message [#3669]
|
||||
- Merge branch 'release/v1.6.2' ([e0b9dcf](e0b9dcfbcd7db175368dbc98255f9223da8df17d))
|
||||
- The negate field doesn't get used, which means you cant exclude tags [#3713]
|
||||
- Update forum.less to fix the misalignment of the choose tags button [#3726]
|
||||
- `yarn audit-fix` ([8ddb0fe](8ddb0feb097dad06c5763107d7a7f7b5a55562c4))
|
||||
- `yarn` ([ee1e04c](ee1e04cdc26b3e63057a58899f32f482901a95fd))
|
||||
- convert `Dropdown` components to TS [#3608]
|
||||
- fix php 8.1 on preg_match 2nd argument being null, which also optimizes slightly ([d7b9a03](d7b9a03f31847c39631ba495df8f515509774610))
|
||||
- improve group mentions parsing [#3723]
|
||||
- prepare `@flarum/jest-config` for release ([748cca6](748cca6d12f8b1744a6017c09395725bdbb4a118))
|
||||
- remove use of deprecated phpunit assertion ([3af0481](3af0481f304277f5380fac9c9b169a7fa651f53b))
|
||||
- set flarum version to 1.7.0 for dev ([2517bc0](2517bc0f70b0f0e3d3ea3f6ae06af8604d89b25d))
|
||||
- update JS dependencies [#3695]
|
||||
|
||||
## [v1.6.3](https://github.com/flarum/framework/compare/v1.6.2...v1.6.3)
|
||||
### Fixed
|
||||
* Post mentions can be used to read any post on the forum without access control (ab1c868b978e8b0d09a5d682c54665dae17d0985).
|
||||
* Notifications can leak restricted content (d0a2b95dca57d3dae9a0d77b610b1cb1d0b1766a).
|
||||
|
@@ -40,6 +40,7 @@
|
||||
"Flarum\\": "framework/core/src",
|
||||
"Flarum\\Akismet\\": "extensions/akismet/src",
|
||||
"Flarum\\Approval\\": "extensions/approval/src",
|
||||
"Flarum\\BBCode\\": "extensions/bbcode/src",
|
||||
"Flarum\\Flags\\": "extensions/flags/src",
|
||||
"Flarum\\Likes\\": "extensions/likes/src",
|
||||
"Flarum\\Lock\\": "extensions/lock/src",
|
||||
@@ -127,6 +128,7 @@
|
||||
"psr/http-server-middleware": "^1.0",
|
||||
"pusher/pusher-php-server": "^2.2",
|
||||
"s9e/text-formatter": "^2.3.6",
|
||||
"staudenmeir/eloquent-eager-limit": "^1.0",
|
||||
"sycho/json-api": "^0.5.0",
|
||||
"sycho/sourcemap": "^2.0.0",
|
||||
"symfony/config": "^5.2.2",
|
||||
|
@@ -19,8 +19,8 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6",
|
||||
"flarum/approval": "^1.2"
|
||||
"flarum/core": "^1.7",
|
||||
"flarum/approval": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
2
extensions/akismet/js/dist/forum.js
generated
vendored
2
extensions/akismet/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const o=flarum.core.compat["common/extend"],r=flarum.core.compat["forum/app"];var a=t.n(r);const n=flarum.core.compat["forum/utils/PostControls"];var m=t.n(n);const s=flarum.core.compat["forum/components/CommentPost"];var l=t.n(s);a().initializers.add("flarum-akismet",(function(){(0,o.extend)(m(),"destructiveControls",(function(t,e){if(t.has("approve")){var o=e.flags();if(o&&o.some((function(t){return"akismet"===(null==t?void 0:t.type())}))){var r=t.get("approve");r&&"object"==typeof r&&"children"in r&&(r.children=a().translator.trans("flarum-akismet.forum.post.not_spam_button"))}}})),(0,o.override)(l().prototype,"flagReason",(function(t,e){return"akismet"===e.type()?a().translator.trans("flarum-akismet.forum.post.akismet_flagged_text"):t(e)}))}))})(),module.exports=e})();
|
||||
(()=>{var t={n:e=>{var o=e&&e.__esModule?()=>e.default:()=>e;return t.d(o,{a:o}),o},d:(e,o)=>{for(var r in o)t.o(o,r)&&!t.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:o[r]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};(()=>{"use strict";t.r(e);const o=flarum.core.compat["common/extend"],r=flarum.core.compat["forum/app"];var a=t.n(r);const n=flarum.core.compat["forum/utils/PostControls"];var s=t.n(n);const l=flarum.core.compat["forum/components/Post"];var u=t.n(l);a().initializers.add("flarum-akismet",(function(){(0,o.extend)(s(),"destructiveControls",(function(t,e){if(t.has("approve")){var o=e.flags();if(o&&o.some((function(t){return"akismet"===(null==t?void 0:t.type())}))){var r=t.get("approve");r&&"object"==typeof r&&"children"in r&&(r.children=a().translator.trans("flarum-akismet.forum.post.not_spam_button"))}}})),(0,o.override)(u().prototype,"flagReason",(function(t,e){return"akismet"===e.type()?a().translator.trans("flarum-akismet.forum.post.akismet_flagged_text"):t(e)}))}))})(),module.exports=e})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/akismet/js/dist/forum.js.map
generated
vendored
2
extensions/akismet/js/dist/forum.js.map
generated
vendored
@@ -1 +1 @@
|
||||
{"version":3,"file":"forum.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,KAAKC,OAAO,iBCAlD,EAA+BF,OAAOC,KAAKC,OAAO,a,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,4B,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,gC,aCSxDC,IAAAA,aAAAA,IAAqB,kBAAkB,YACrCC,EAAAA,EAAAA,QAAOC,IAAc,uBAAuB,SAAUC,EAAmCC,GACvF,GAAID,EAAME,IAAI,WAAY,CACxB,IAAMC,EAAQF,EAAKE,QAEnB,GAAIA,GAASA,EAAMC,MAAK,SAACC,GAAI,MAAsB,aAAb,MAAJA,OAAI,EAAJA,EAAMC,OAAoB,IAAG,CAC7D,IAAMC,EAAcP,EAAMf,IAAI,WAC1BsB,GAAsC,iBAAhBA,GAA4B,aAAcA,IAClEA,EAAYC,SAAWX,IAAAA,WAAAA,MAAqB,6CAEhD,CACF,CACF,KAEAY,EAAAA,EAAAA,UAASC,IAAAA,UAAuB,cAAc,SAAUC,EAAUN,GAChE,MAAoB,YAAhBA,EAAKC,OACAT,IAAAA,WAAAA,MAAqB,kDAGvBc,EAASN,EAClB,GACF,G","sources":["webpack://@flarum/akismet/webpack/bootstrap","webpack://@flarum/akismet/webpack/runtime/compat get default export","webpack://@flarum/akismet/webpack/runtime/define property getters","webpack://@flarum/akismet/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/akismet/webpack/runtime/make namespace object","webpack://@flarum/akismet/external root \"flarum.core.compat['common/extend']\"","webpack://@flarum/akismet/external root \"flarum.core.compat['forum/app']\"","webpack://@flarum/akismet/external root \"flarum.core.compat['forum/utils/PostControls']\"","webpack://@flarum/akismet/external root \"flarum.core.compat['forum/components/CommentPost']\"","webpack://@flarum/akismet/./src/forum/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/utils/PostControls'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/CommentPost'];","import { extend, override } from 'flarum/common/extend';\nimport app from 'flarum/forum/app';\nimport type Post from 'flarum/common/models/Post';\nimport type ItemList from 'flarum/common/utils/ItemList';\n\nimport PostControls from 'flarum/forum/utils/PostControls';\nimport CommentPost from 'flarum/forum/components/CommentPost';\nimport type Mithril from 'mithril';\n\napp.initializers.add('flarum-akismet', () => {\n extend(PostControls, 'destructiveControls', function (items: ItemList<Mithril.Children>, post: Post) {\n if (items.has('approve')) {\n const flags = post.flags();\n\n if (flags && flags.some((flag) => flag?.type() === 'akismet')) {\n const approveItem = items.get('approve');\n if (approveItem && typeof approveItem === 'object' && 'children' in approveItem) {\n approveItem.children = app.translator.trans('flarum-akismet.forum.post.not_spam_button');\n }\n }\n }\n });\n\n override(CommentPost.prototype, 'flagReason', function (original, flag) {\n if (flag.type() === 'akismet') {\n return app.translator.trans('flarum-akismet.forum.post.akismet_flagged_text');\n }\n\n return original(flag);\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","app","extend","PostControls","items","post","has","flags","some","flag","type","approveItem","children","override","CommentPost","original"],"sourceRoot":""}
|
||||
{"version":3,"file":"forum.js","mappings":"MACA,IAAIA,EAAsB,CCA1BA,EAAyBC,IACxB,IAAIC,EAASD,GAAUA,EAAOE,WAC7B,IAAOF,EAAiB,QACxB,IAAM,EAEP,OADAD,EAAoBI,EAAEF,EAAQ,CAAEG,EAAGH,IAC5BA,CAAM,ECLdF,EAAwB,CAACM,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXP,EAAoBS,EAAEF,EAAYC,KAASR,EAAoBS,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDR,EAAwB,CAACc,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFf,EAAyBM,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,G,+BCL9D,MAAM,EAA+BC,OAAOC,KAAKC,OAAO,iBCAlD,EAA+BF,OAAOC,KAAKC,OAAO,a,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,4B,aCAxD,MAAM,EAA+BF,OAAOC,KAAKC,OAAO,yB,aCSxDC,IAAAA,aAAAA,IAAqB,kBAAkB,YACrCC,EAAAA,EAAAA,QAAOC,IAAc,uBAAuB,SAAUC,EAAmCC,GACvF,GAAID,EAAME,IAAI,WAAY,CACxB,IAAMC,EAAQF,EAAKE,QAEnB,GAAIA,GAASA,EAAMC,MAAK,SAACC,GAAI,MAAsB,aAAb,MAAJA,OAAI,EAAJA,EAAMC,OAAoB,IAAG,CAC7D,IAAMC,EAAcP,EAAMf,IAAI,WAC1BsB,GAAsC,iBAAhBA,GAA4B,aAAcA,IAClEA,EAAYC,SAAWX,IAAAA,WAAAA,MAAqB,6CAEhD,CACF,CACF,KAEAY,EAAAA,EAAAA,UAASC,IAAAA,UAAyB,cAAc,SAAUC,EAAUN,GAClE,MAAoB,YAAhBA,EAAKC,OACAT,IAAAA,WAAAA,MAAqB,kDAGvBc,EAASN,EAClB,GACF,G","sources":["webpack://@flarum/akismet/webpack/bootstrap","webpack://@flarum/akismet/webpack/runtime/compat get default export","webpack://@flarum/akismet/webpack/runtime/define property getters","webpack://@flarum/akismet/webpack/runtime/hasOwnProperty shorthand","webpack://@flarum/akismet/webpack/runtime/make namespace object","webpack://@flarum/akismet/external root \"flarum.core.compat['common/extend']\"","webpack://@flarum/akismet/external root \"flarum.core.compat['forum/app']\"","webpack://@flarum/akismet/external root \"flarum.core.compat['forum/utils/PostControls']\"","webpack://@flarum/akismet/external root \"flarum.core.compat['forum/components/Post']\"","webpack://@flarum/akismet/./src/forum/index.ts"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extend'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/app'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/utils/PostControls'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/Post'];","import { extend, override } from 'flarum/common/extend';\nimport app from 'flarum/forum/app';\nimport type Post from 'flarum/common/models/Post';\nimport type ItemList from 'flarum/common/utils/ItemList';\n\nimport PostControls from 'flarum/forum/utils/PostControls';\nimport PostComponent from 'flarum/forum/components/Post';\nimport type Mithril from 'mithril';\n\napp.initializers.add('flarum-akismet', () => {\n extend(PostControls, 'destructiveControls', function (items: ItemList<Mithril.Children>, post: Post) {\n if (items.has('approve')) {\n const flags = post.flags();\n\n if (flags && flags.some((flag) => flag?.type() === 'akismet')) {\n const approveItem = items.get('approve');\n if (approveItem && typeof approveItem === 'object' && 'children' in approveItem) {\n approveItem.children = app.translator.trans('flarum-akismet.forum.post.not_spam_button');\n }\n }\n }\n });\n\n override(PostComponent.prototype, 'flagReason', function (original, flag) {\n if (flag.type() === 'akismet') {\n return app.translator.trans('flarum-akismet.forum.post.akismet_flagged_text');\n }\n\n return original(flag);\n });\n});\n"],"names":["__webpack_require__","module","getter","__esModule","d","a","exports","definition","key","o","Object","defineProperty","enumerable","get","obj","prop","prototype","hasOwnProperty","call","Symbol","toStringTag","value","flarum","core","compat","app","extend","PostControls","items","post","has","flags","some","flag","type","approveItem","children","override","PostComponent","original"],"sourceRoot":""}
|
@@ -20,7 +20,7 @@
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript-coverage-report": "^0.6.1"
|
||||
|
@@ -4,7 +4,7 @@ import type Post from 'flarum/common/models/Post';
|
||||
import type ItemList from 'flarum/common/utils/ItemList';
|
||||
|
||||
import PostControls from 'flarum/forum/utils/PostControls';
|
||||
import CommentPost from 'flarum/forum/components/CommentPost';
|
||||
import PostComponent from 'flarum/forum/components/Post';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
app.initializers.add('flarum-akismet', () => {
|
||||
@@ -21,7 +21,7 @@ app.initializers.add('flarum-akismet', () => {
|
||||
}
|
||||
});
|
||||
|
||||
override(CommentPost.prototype, 'flagReason', function (original, flag) {
|
||||
override(PostComponent.prototype, 'flagReason', function (original, flag) {
|
||||
if (flag.type() === 'akismet') {
|
||||
return app.translator.trans('flarum-akismet.forum.post.akismet_flagged_text');
|
||||
}
|
||||
|
@@ -19,8 +19,8 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6",
|
||||
"flarum/flags": "^1.2"
|
||||
"flarum/core": "^1.7",
|
||||
"flarum/flags": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0"
|
||||
},
|
||||
|
@@ -36,5 +36,10 @@ class UpdateDiscussionAfterPostApproval
|
||||
$user->refreshCommentCount();
|
||||
$user->save();
|
||||
}
|
||||
|
||||
if ($post->user) {
|
||||
$post->user->refreshCommentCount();
|
||||
$post->user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,12 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Flarum\\BBCode\\": "src"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
@@ -7,24 +7,14 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Extend;
|
||||
use s9e\TextFormatter\Configurator;
|
||||
namespace Flarum\BBCode;
|
||||
|
||||
return (new Extend\Formatter)
|
||||
->configure(function (Configurator $config) {
|
||||
$config->BBCodes->addFromRepository('B');
|
||||
$config->BBCodes->addFromRepository('I');
|
||||
$config->BBCodes->addFromRepository('U');
|
||||
$config->BBCodes->addFromRepository('S');
|
||||
$config->BBCodes->addFromRepository('URL');
|
||||
$config->BBCodes->addFromRepository('IMG');
|
||||
$config->BBCodes->addFromRepository('EMAIL');
|
||||
$config->BBCodes->addFromRepository('CODE');
|
||||
$config->BBCodes->addFromRepository('QUOTE');
|
||||
$config->BBCodes->addFromRepository('LIST');
|
||||
$config->BBCodes->addFromRepository('DEL');
|
||||
$config->BBCodes->addFromRepository('COLOR');
|
||||
$config->BBCodes->addFromRepository('CENTER');
|
||||
$config->BBCodes->addFromRepository('SIZE');
|
||||
$config->BBCodes->addFromRepository('*');
|
||||
});
|
||||
use Flarum\Extend;
|
||||
|
||||
return [
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\Formatter)
|
||||
->render(Render::class)
|
||||
->configure(Configure::class),
|
||||
];
|
||||
|
10
extensions/bbcode/locale/en.yml
Normal file
10
extensions/bbcode/locale/en.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
flarum-bbcode:
|
||||
|
||||
##
|
||||
# UNIQUE KEYS - The following keys are used in only one location each.
|
||||
##
|
||||
|
||||
# Translations in this namespace are used by the forum user interface.
|
||||
forum:
|
||||
quote:
|
||||
wrote: wrote
|
59
extensions/bbcode/src/Configure.php
Normal file
59
extensions/bbcode/src/Configure.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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\BBCode;
|
||||
|
||||
use s9e\TextFormatter\Configurator;
|
||||
|
||||
class Configure
|
||||
{
|
||||
public function __invoke(Configurator $config)
|
||||
{
|
||||
$this->addTagsFromRepositories($config);
|
||||
$this->adaptHighlightJs($config);
|
||||
}
|
||||
|
||||
protected function addTagsFromRepositories(Configurator $config): void
|
||||
{
|
||||
$config->BBCodes->addFromRepository('B');
|
||||
$config->BBCodes->addFromRepository('I');
|
||||
$config->BBCodes->addFromRepository('U');
|
||||
$config->BBCodes->addFromRepository('S');
|
||||
$config->BBCodes->addFromRepository('URL');
|
||||
$config->BBCodes->addFromRepository('IMG');
|
||||
$config->BBCodes->addFromRepository('EMAIL');
|
||||
$config->BBCodes->addFromRepository('CODE');
|
||||
$config->BBCodes->addFromRepository('QUOTE', 'default', [
|
||||
'authorStr' => '<xsl:value-of select="@author"/> <xsl:value-of select="$L_WROTE"/>'
|
||||
]);
|
||||
$config->BBCodes->addFromRepository('LIST');
|
||||
$config->BBCodes->addFromRepository('DEL');
|
||||
$config->BBCodes->addFromRepository('COLOR');
|
||||
$config->BBCodes->addFromRepository('CENTER');
|
||||
$config->BBCodes->addFromRepository('SIZE');
|
||||
$config->BBCodes->addFromRepository('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix for highlight JS not working after changing post content.
|
||||
*
|
||||
* @link https://github.com/flarum/framework/issues/3794
|
||||
*/
|
||||
protected function adaptHighlightJs(Configurator $config): void
|
||||
{
|
||||
$codeTag = $config->tags->get('CODE');
|
||||
$script = '
|
||||
<script>
|
||||
if(window.hljsLoader && !document.currentScript.parentNode.hasAttribute(\'data-s9e-livepreview-onupdate\')) {
|
||||
window.hljsLoader.highlightBlocks(document.currentScript.parentNode);
|
||||
}
|
||||
</script>';
|
||||
$codeTag->template = str_replace('</pre>', $script.'</pre>', $codeTag->template);
|
||||
}
|
||||
}
|
33
extensions/bbcode/src/Render.php
Normal file
33
extensions/bbcode/src/Render.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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\BBCode;
|
||||
|
||||
use s9e\TextFormatter\Renderer;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class Render
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
protected $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function __invoke(Renderer $renderer, $context, string $xml): string
|
||||
{
|
||||
$renderer->setParameter('L_WROTE', $this->translator->trans('flarum-bbcode.forum.quote.wrote'));
|
||||
|
||||
return $xml;
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0"
|
||||
},
|
||||
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
2
extensions/emoji/js/dist/forum.js
generated
vendored
2
extensions/emoji/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/emoji/js/dist/forum.js.map
generated
vendored
2
extensions/emoji/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@
|
||||
"prettier": "^2.5.1",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript-coverage-report": "^0.6.1",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
@@ -80,7 +80,7 @@ export default function addComposerAutocomplete() {
|
||||
dropdown.setIndex($(this).parent().index() - 1);
|
||||
}}
|
||||
>
|
||||
<img alt={emoji} class="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} />
|
||||
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} />
|
||||
{name}
|
||||
</button>
|
||||
);
|
||||
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
6
extensions/flags/js/dist-typings/forum/components/FlagsDropdown.d.ts
generated
vendored
6
extensions/flags/js/dist-typings/forum/components/FlagsDropdown.d.ts
generated
vendored
@@ -1,7 +1,7 @@
|
||||
export default class FlagsDropdown {
|
||||
export default class FlagsDropdown extends NotificationsDropdown<import("flarum/common/components/Dropdown").IDropdownAttrs> {
|
||||
static initAttrs(attrs: any): void;
|
||||
getMenu(): JSX.Element;
|
||||
goToRoute(): void;
|
||||
constructor();
|
||||
getUnreadCount(): any;
|
||||
getNewCount(): unknown;
|
||||
}
|
||||
import NotificationsDropdown from "flarum/forum/components/NotificationsDropdown";
|
||||
|
2
extensions/flags/js/dist/forum.js
generated
vendored
2
extensions/flags/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/flags/js/dist/forum.js.map
generated
vendored
2
extensions/flags/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@
|
||||
"@types/mithril": "^2.0.8",
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0",
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
|
@@ -108,7 +108,7 @@ export default function () {
|
||||
user,
|
||||
reason,
|
||||
}),
|
||||
detail ? <span className="Post-flagged-detail">{detail}</span> : '',
|
||||
!!detail && <span className="Post-flagged-detail">{detail}</span>,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
@@ -55,7 +55,7 @@ export default class FlagList extends Component {
|
||||
) : !this.state.loading ? (
|
||||
<div className="NotificationList-empty">{app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}</div>
|
||||
) : (
|
||||
LoadingIndicator.component({ className: 'LoadingIndicator--block' })
|
||||
<LoadingIndicator className="LoadingIndicator--block" />
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
@@ -67,15 +67,13 @@ export default class FlagPostModal extends Modal {
|
||||
<input type="radio" name="reason" checked={this.reason() === 'off_topic'} value="off_topic" onclick={withAttr('value', this.reason)} />
|
||||
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_label')}</strong>
|
||||
{app.translator.trans('flarum-flags.forum.flag_post.reason_off_topic_text')}
|
||||
{this.reason() === 'off_topic' ? (
|
||||
{this.reason() === 'off_topic' && (
|
||||
<textarea
|
||||
className="FormControl"
|
||||
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
|
||||
value={this.reasonDetail()}
|
||||
oninput={withAttr('value', this.reasonDetail)}
|
||||
></textarea>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</label>,
|
||||
70
|
||||
@@ -95,15 +93,13 @@ export default class FlagPostModal extends Modal {
|
||||
{app.translator.trans('flarum-flags.forum.flag_post.reason_inappropriate_text', {
|
||||
a: guidelinesUrl ? <a href={guidelinesUrl} target="_blank" /> : undefined,
|
||||
})}
|
||||
{this.reason() === 'inappropriate' ? (
|
||||
{this.reason() === 'inappropriate' && (
|
||||
<textarea
|
||||
className="FormControl"
|
||||
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
|
||||
value={this.reasonDetail()}
|
||||
oninput={withAttr('value', this.reasonDetail)}
|
||||
></textarea>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</label>,
|
||||
60
|
||||
@@ -115,15 +111,13 @@ export default class FlagPostModal extends Modal {
|
||||
<input type="radio" name="reason" checked={this.reason() === 'spam'} value="spam" onclick={withAttr('value', this.reason)} />
|
||||
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_label')}</strong>
|
||||
{app.translator.trans('flarum-flags.forum.flag_post.reason_spam_text')}
|
||||
{this.reason() === 'spam' ? (
|
||||
{this.reason() === 'spam' && (
|
||||
<textarea
|
||||
className="FormControl"
|
||||
placeholder={app.translator.trans('flarum-flags.forum.flag_post.reason_details_placeholder')}
|
||||
value={this.reasonDetail()}
|
||||
oninput={withAttr('value', this.reasonDetail)}
|
||||
></textarea>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</label>,
|
||||
50
|
||||
@@ -134,10 +128,8 @@ export default class FlagPostModal extends Modal {
|
||||
<label className="checkbox">
|
||||
<input type="radio" name="reason" checked={this.reason() === 'other'} value="other" onclick={withAttr('value', this.reason)} />
|
||||
<strong>{app.translator.trans('flarum-flags.forum.flag_post.reason_other_label')}</strong>
|
||||
{this.reason() === 'other' ? (
|
||||
{this.reason() === 'other' && (
|
||||
<textarea className="FormControl" value={this.reasonDetail()} oninput={withAttr('value', this.reasonDetail)}></textarea>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</label>,
|
||||
10
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import NotificationsDropdown from 'flarum/components/NotificationsDropdown';
|
||||
import NotificationsDropdown from 'flarum/forum/components/NotificationsDropdown';
|
||||
|
||||
import FlagList from './FlagList';
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class FlagsDropdown extends NotificationsDropdown {
|
||||
getMenu() {
|
||||
return (
|
||||
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? FlagList.component({ state: this.attrs.state }) : ''}
|
||||
{this.showing && <FlagList state={this.attrs.state} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@@ -13,12 +13,15 @@ use Flarum\Api\Controller;
|
||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
||||
use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Likes\Notification\PostLikedBlueprint;
|
||||
use Flarum\Likes\Query\LikedByFilter;
|
||||
use Flarum\Likes\Query\LikedFilter;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\Filter\UserFilterer;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -41,19 +44,32 @@ return [
|
||||
->hasMany('likes', BasicUserSerializer::class)
|
||||
->attribute('canLike', function (PostSerializer $serializer, $model) {
|
||||
return (bool) $serializer->getActor()->can('like', $model);
|
||||
})
|
||||
->attribute('likesCount', function (PostSerializer $serializer, $model) {
|
||||
return $model->getAttribute('likes_count') ?: 0;
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude('posts.likes'),
|
||||
->addInclude('posts.likes')
|
||||
->loadWhere('posts.likes', [LoadLikesRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude('likes'),
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude('likes'),
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
|
||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
||||
->addInclude('likes'),
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
|
||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
||||
->addInclude('likes'),
|
||||
->addInclude('likes')
|
||||
->loadWhere('likes', [LoadLikesRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadLikesRelationship::class, 'countRelation']),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
|
||||
@@ -63,6 +79,9 @@ return [
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(LikedByFilter::class),
|
||||
|
||||
(new Extend\Filter(UserFilterer::class))
|
||||
->addFilter(LikedFilter::class),
|
||||
|
||||
(new Extend\Settings())
|
||||
->default('flarum-likes.like_own_post', true),
|
||||
|
||||
|
2
extensions/likes/js/dist/forum.js
generated
vendored
2
extensions/likes/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/likes/js/dist/forum.js.map
generated
vendored
2
extensions/likes/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0"
|
||||
},
|
||||
|
9
extensions/likes/js/src/@types/shims.d.ts
vendored
Normal file
9
extensions/likes/js/src/@types/shims.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import Post from 'flarum/common/models/Post';
|
||||
import User from 'flarum/common/models/User';
|
||||
|
||||
declare module 'flarum/common/models/Post' {
|
||||
export default interface Post {
|
||||
likes(): User[];
|
||||
likesCount(): number;
|
||||
}
|
||||
}
|
@@ -15,32 +15,31 @@ export default function () {
|
||||
|
||||
items.add(
|
||||
'like',
|
||||
Button.component(
|
||||
{
|
||||
className: 'Button Button--link',
|
||||
onclick: () => {
|
||||
isLiked = !isLiked;
|
||||
<Button
|
||||
className="Button Button--link"
|
||||
onclick={() => {
|
||||
isLiked = !isLiked;
|
||||
|
||||
post.save({ isLiked });
|
||||
post.save({ isLiked });
|
||||
|
||||
// We've saved the fact that we do or don't like the post, but in order
|
||||
// to provide instantaneous feedback to the user, we'll need to add or
|
||||
// remove the like from the relationship data manually.
|
||||
const data = post.data.relationships.likes.data;
|
||||
data.some((like, i) => {
|
||||
if (like.id === app.session.user.id()) {
|
||||
data.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (isLiked) {
|
||||
data.unshift({ type: 'users', id: app.session.user.id() });
|
||||
// We've saved the fact that we do or don't like the post, but in order
|
||||
// to provide instantaneous feedback to the user, we'll need to add or
|
||||
// remove the like from the relationship data manually.
|
||||
const data = post.data.relationships.likes.data;
|
||||
data.some((like, i) => {
|
||||
if (like.id === app.session.user.id()) {
|
||||
data.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
app.translator.trans(isLiked ? 'flarum-likes.forum.post.unlike_link' : 'flarum-likes.forum.post.like_link')
|
||||
)
|
||||
});
|
||||
|
||||
if (isLiked) {
|
||||
data.unshift({ type: 'users', id: app.session.user.id() });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{app.translator.trans(isLiked ? 'flarum-likes.forum.post.unlike_link' : 'flarum-likes.forum.post.like_link')}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import Link from 'flarum/common/components/Link';
|
||||
import punctuateSeries from 'flarum/common/helpers/punctuateSeries';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
import PostLikesModal from './components/PostLikesModal';
|
||||
|
||||
@@ -15,7 +16,7 @@ export default function () {
|
||||
|
||||
if (likes && likes.length) {
|
||||
const limit = 4;
|
||||
const overLimit = likes.length > limit;
|
||||
const overLimit = post.likesCount() > limit;
|
||||
|
||||
// Construct a list of names of users who have liked this post. Make sure the
|
||||
// current user is first in the list, and cap a maximum of 4 items.
|
||||
@@ -34,26 +35,31 @@ export default function () {
|
||||
// others" name to the end of the list. Clicking on it will display a modal
|
||||
// with a full list of names.
|
||||
if (overLimit) {
|
||||
const count = likes.length - names.length;
|
||||
const count = post.likesCount() - names.length;
|
||||
const label = app.translator.trans('flarum-likes.forum.post.others_link', { count });
|
||||
|
||||
names.push(
|
||||
<a
|
||||
href="#"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
app.modal.show(PostLikesModal, { post });
|
||||
}}
|
||||
>
|
||||
{app.translator.trans('flarum-likes.forum.post.others_link', { count })}
|
||||
</a>
|
||||
);
|
||||
if (app.forum.attribute('canSearchUsers')) {
|
||||
names.push(
|
||||
<Button
|
||||
className="Button Button--ua-reset Button--text"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
app.modal.show(PostLikesModal, { post });
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
names.push(<span>{label}</span>);
|
||||
}
|
||||
}
|
||||
|
||||
items.add(
|
||||
'liked',
|
||||
<div className="Post-likedBy">
|
||||
{icon('far fa-thumbs-up')}
|
||||
{app.translator.trans('flarum-likes.forum.post.liked_by' + (likes[0] === app.session.user ? '_self' : '') + '_text', {
|
||||
{app.translator.trans(`flarum-likes.forum.post.liked_by${likes[0] === app.session.user ? '_self' : ''}_text`, {
|
||||
count: names.length,
|
||||
users: punctuateSeries(names),
|
||||
})}
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
|
||||
export default class PostLikesModal extends Modal {
|
||||
className() {
|
||||
return 'PostLikesModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('flarum-likes.forum.post_likes.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<ul className="PostLikesModal-list">
|
||||
{this.attrs.post.likes().map((user) => (
|
||||
<li>
|
||||
<Link href={app.route.user(user)}>
|
||||
{avatar(user)} {username(user)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
72
extensions/likes/js/src/forum/components/PostLikesModal.tsx
Normal file
72
extensions/likes/js/src/forum/components/PostLikesModal.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import type { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
import type Mithril from 'mithril';
|
||||
import PostLikesModalState from '../states/PostLikesModalState';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
|
||||
export interface IPostLikesModalAttrs extends IInternalModalAttrs {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
export default class PostLikesModal<CustomAttrs extends IPostLikesModalAttrs = IPostLikesModalAttrs> extends Modal<CustomAttrs, PostLikesModalState> {
|
||||
oninit(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.state = new PostLikesModalState({
|
||||
filter: {
|
||||
liked: this.attrs.post.id()!,
|
||||
},
|
||||
});
|
||||
|
||||
this.state.refresh();
|
||||
}
|
||||
|
||||
className() {
|
||||
return 'PostLikesModal Modal--small';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('flarum-likes.forum.post_likes.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return (
|
||||
<>
|
||||
<div className="Modal-body">
|
||||
{this.state.isInitialLoading() ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<ul className="PostLikesModal-list">
|
||||
{this.state.getPages().map((page) =>
|
||||
page.items.map((user) => (
|
||||
<li>
|
||||
<Link href={app.route.user(user)}>
|
||||
{avatar(user)} {username(user)}
|
||||
</Link>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{this.state.hasNext() ? (
|
||||
<div className="Modal-footer">
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--block" onclick={() => this.state.loadNext()} loading={this.state.isLoadingNext()}>
|
||||
{app.translator.trans('flarum-likes.forum.post_likes.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@@ -9,5 +9,6 @@ export default [
|
||||
|
||||
new Extend.Model(Post) //
|
||||
.hasMany<User>('likes')
|
||||
.attribute<number>('likesCount')
|
||||
.attribute<boolean>('canLike'),
|
||||
];
|
||||
|
26
extensions/likes/js/src/forum/states/PostLikesModalState.ts
Normal file
26
extensions/likes/js/src/forum/states/PostLikesModalState.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import PaginatedListState, { PaginatedListParams } from 'flarum/common/states/PaginatedListState';
|
||||
import User from 'flarum/common/models/User';
|
||||
|
||||
export interface PostLikesModalListParams extends PaginatedListParams {
|
||||
filter: {
|
||||
liked: string;
|
||||
};
|
||||
page?: {
|
||||
offset?: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default class PostLikesModalState<P extends PostLikesModalListParams = PostLikesModalListParams> extends PaginatedListState<User, P> {
|
||||
constructor(params: P, page: number = 1) {
|
||||
const limit = 10;
|
||||
|
||||
params.page = { ...(params.page || {}), limit };
|
||||
|
||||
super(params, page, limit);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'users';
|
||||
}
|
||||
}
|
@@ -35,6 +35,7 @@ flarum-likes:
|
||||
# These translations are used by the Users Who Like This modal dialog.
|
||||
post_likes:
|
||||
title: Users Who Like This
|
||||
load_more_button: => core.ref.load_more
|
||||
|
||||
# These translations are used in the Settings page.
|
||||
settings:
|
||||
|
61
extensions/likes/src/Api/LoadLikesRelationship.php
Normal file
61
extensions/likes/src/Api/LoadLikesRelationship.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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\Likes\Api;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class LoadLikesRelationship
|
||||
{
|
||||
public static $maxLikes = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): BelongsToMany
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
return $query
|
||||
// So that we can tell if the current user has liked the post.
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxLikes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation($controller, $data): void
|
||||
{
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// @phpstan-ignore-next-line
|
||||
$loadable = $data->newCollection($data->posts)->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount('likes');
|
||||
}
|
||||
}
|
||||
}
|
@@ -11,17 +11,20 @@ namespace Flarum\Likes\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
use Flarum\Filter\ValidateFilterTrait;
|
||||
|
||||
class LikedByFilter implements FilterInterface
|
||||
{
|
||||
use ValidateFilterTrait;
|
||||
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'likedBy';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string $filterValue, bool $negate)
|
||||
public function filter(FilterState $filterState, $filterValue, bool $negate)
|
||||
{
|
||||
$likedId = trim($filterValue, '"');
|
||||
$likedId = $this->asInt($filterValue);
|
||||
|
||||
$filterState
|
||||
->getQuery()
|
||||
|
34
extensions/likes/src/Query/LikedFilter.php
Normal file
34
extensions/likes/src/Query/LikedFilter.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Likes\Query;
|
||||
|
||||
use Flarum\Filter\FilterInterface;
|
||||
use Flarum\Filter\FilterState;
|
||||
|
||||
class LikedFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'liked';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string $filterValue, bool $negate)
|
||||
{
|
||||
$likedId = trim($filterValue, '"');
|
||||
|
||||
$filterState
|
||||
->getQuery()
|
||||
->whereIn('id', function ($query) use ($likedId) {
|
||||
$query->select('user_id')
|
||||
->from('post_likes')
|
||||
->where('post_id', $likedId);
|
||||
}, 'and', $negate);
|
||||
}
|
||||
}
|
210
extensions/likes/tests/integration/api/ListPostsTest.php
Normal file
210
extensions/likes/tests/integration/api/ListPostsTest.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?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\Likes\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListPostsTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-likes');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'discussions' => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
],
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
['id' => 102, 'username' => 'user102', 'email' => '102@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 103, 'username' => 'user103', 'email' => '103@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 104, 'username' => 'user104', 'email' => '104@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 105, 'username' => 'user105', 'email' => '105@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 106, 'username' => 'user106', 'email' => '106@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 107, 'username' => 'user107', 'email' => '107@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 108, 'username' => 'user108', 'email' => '108@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 109, 'username' => 'user109', 'email' => '109@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 110, 'username' => 'user110', 'email' => '110@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 111, 'username' => 'user111', 'email' => '111@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 112, 'username' => 'user112', 'email' => '112@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'post_likes' => [
|
||||
['user_id' => 102, 'post_id' => 101],
|
||||
['user_id' => 104, 'post_id' => 101],
|
||||
['user_id' => 105, 'post_id' => 101],
|
||||
['user_id' => 106, 'post_id' => 101],
|
||||
['user_id' => 107, 'post_id' => 101],
|
||||
['user_id' => 108, 'post_id' => 101],
|
||||
['user_id' => 109, 'post_id' => 101],
|
||||
['user_id' => 110, 'post_id' => 101],
|
||||
['user_id' => 2, 'post_id' => 101],
|
||||
['user_id' => 111, 'post_id' => 101],
|
||||
['user_id' => 112, 'post_id' => 101],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::GUEST_ID, 'permission' => 'searchUsers'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function liked_filter_works()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users')
|
||||
->withQueryParams([
|
||||
'filter' => ['liked' => 101],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
// Order-independent comparison
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing([
|
||||
102, 104, 105, 106, 107, 108, 109, 110, 2, 111, 112
|
||||
], $ids, 'IDs do not match');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function liked_filter_works_negated()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/users')
|
||||
->withQueryParams([
|
||||
'filter' => ['-liked' => 101],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
// Order-independent comparison
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing([1, 103], $ids, 'IDs do not match');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_post_endpoint()
|
||||
{
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts/101', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'include' => 'likes',
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$likes = $data['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_list_posts_endpoint()
|
||||
{
|
||||
// List posts endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/posts', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'filter' => ['discussion' => 100],
|
||||
'include' => 'likes',
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$likes = $data[0]['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider likesIncludeProvider
|
||||
* @test
|
||||
*/
|
||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
||||
{
|
||||
// Show discussion endpoint
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/discussions/100', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withQueryParams([
|
||||
'include' => $include,
|
||||
])
|
||||
);
|
||||
|
||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
||||
|
||||
$likes = collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['relationships']['likes']['data'];
|
||||
|
||||
// Only displays a limited amount of likes
|
||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
||||
// Displays the correct count of likes
|
||||
$this->assertEquals(11, collect($included)
|
||||
->where('type', 'posts')
|
||||
->where('id', 101)
|
||||
->first()['attributes']['likesCount']);
|
||||
// Of the limited amount of likes, the actor always appears
|
||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
||||
}
|
||||
|
||||
public function likesIncludeProvider(): array
|
||||
{
|
||||
return [
|
||||
['posts,posts.likes'],
|
||||
['posts.likes'],
|
||||
[''],
|
||||
];
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
2
extensions/lock/js/dist/forum.js
generated
vendored
2
extensions/lock/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var o={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return o.d(n,{a:n}),n},d:(t,n)=>{for(var e in n)o.o(n,e)&&!o.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:n[e]})},o:(o,t)=>Object.prototype.hasOwnProperty.call(o,t),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},t={};(()=>{"use strict";o.r(t),o.d(t,{extend:()=>j});const n=flarum.core.compat["common/extend"],e=flarum.core.compat["forum/app"];var c=o.n(e);const r=flarum.core.compat["forum/components/NotificationGrid"];var s=o.n(r);function a(o,t){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(o,t){return o.__proto__=t,o},a(o,t)}function i(o,t){o.prototype=Object.create(t.prototype),o.prototype.constructor=o,a(o,t)}const u=flarum.core.compat["forum/components/Notification"];var l=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return"fas fa-lock"},n.href=function(){var o=this.attrs.notification;return c().route.discussion(o.subject(),o.content().postNumber)},n.content=function(){return c().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})},t}(o.n(u)());const f=flarum.core.compat["common/models/Discussion"];var d=o.n(f);const p=flarum.core.compat["common/components/Badge"];var k=o.n(p);const y=flarum.core.compat["forum/utils/DiscussionControls"];var b=o.n(y);const _=flarum.core.compat["forum/components/DiscussionPage"];var v=o.n(_);const h=flarum.core.compat["common/components/Button"];var g=o.n(h);const L=flarum.core.compat["common/extenders"];var x=o.n(L);const O=flarum.core.compat["forum/components/EventPost"];var P=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"},n.descriptionKey=function(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"},t}(o.n(O)());const j=[(new(x().PostTypes)).add("discussionLocked",P),new(x().Model)(d()).attribute("isLocked").attribute("canLock")];c().initializers.add("flarum-lock",(function(){c().notificationComponents.discussionLocked=l,(0,n.extend)(d().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",k().component({type:"locked",label:c().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,n.extend)(b(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",g().component({icon:"fas fa-lock",onclick:this.lockAction.bind(t)},c().translator.trans(t.isLocked()?"flarum-lock.forum.discussion_controls.unlock_button":"flarum-lock.forum.discussion_controls.lock_button")))})),b().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((function(){c().current.matches(v())&&c().current.get("stream").update(),m.redraw()}))},(0,n.extend)(s().prototype,"notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:c().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=t})();
|
||||
(()=>{var o={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return o.d(n,{a:n}),n},d:(t,n)=>{for(var e in n)o.o(n,e)&&!o.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:n[e]})},o:(o,t)=>Object.prototype.hasOwnProperty.call(o,t),r:o=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})}},t={};(()=>{"use strict";o.r(t),o.d(t,{extend:()=>j});const n=flarum.core.compat["common/extend"],e=flarum.core.compat["forum/app"];var c=o.n(e);const r=flarum.core.compat["forum/components/NotificationGrid"];var s=o.n(r);function a(o,t){return a=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(o,t){return o.__proto__=t,o},a(o,t)}function i(o,t){o.prototype=Object.create(t.prototype),o.prototype.constructor=o,a(o,t)}const u=flarum.core.compat["forum/components/Notification"];var f=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return"fas fa-lock"},n.href=function(){var o=this.attrs.notification;return c().route.discussion(o.subject(),o.content().postNumber)},n.content=function(){return c().translator.trans("flarum-lock.forum.notifications.discussion_locked_text",{user:this.attrs.notification.fromUser()})},t}(o.n(u)());const l=flarum.core.compat["common/models/Discussion"];var d=o.n(l);const p=flarum.core.compat["common/components/Badge"];var k=o.n(p);const y=flarum.core.compat["forum/utils/DiscussionControls"];var b=o.n(y);const _=flarum.core.compat["forum/components/DiscussionPage"];var v=o.n(_);const h=flarum.core.compat["common/components/Button"];var g=o.n(h);const L=flarum.core.compat["common/extenders"];var x=o.n(L);const O=flarum.core.compat["forum/components/EventPost"];var P=function(o){function t(){return o.apply(this,arguments)||this}i(t,o);var n=t.prototype;return n.icon=function(){return this.attrs.post.content().locked?"fas fa-lock":"fas fa-unlock"},n.descriptionKey=function(){return this.attrs.post.content().locked?"flarum-lock.forum.post_stream.discussion_locked_text":"flarum-lock.forum.post_stream.discussion_unlocked_text"},t}(o.n(O)());const j=[(new(x().PostTypes)).add("discussionLocked",P),new(x().Model)(d()).attribute("isLocked").attribute("canLock")];c().initializers.add("flarum-lock",(function(){c().notificationComponents.discussionLocked=f,(0,n.extend)(d().prototype,"badges",(function(o){this.isLocked()&&o.add("locked",m(k(),{type:"locked",label:c().translator.trans("flarum-lock.forum.badge.locked_tooltip"),icon:"fas fa-lock"}))})),(0,n.extend)(b(),"moderationControls",(function(o,t){t.canLock()&&o.add("lock",m(g(),{icon:"fas fa-lock",onclick:this.lockAction.bind(t)},c().translator.trans("flarum-lock.forum.discussion_controls."+(t.isLocked()?"unlock":"lock")+"_button")))})),b().lockAction=function(){this.save({isLocked:!this.isLocked()}).then((function(){c().current.matches(v())&&c().current.get("stream").update(),m.redraw()}))},(0,n.extend)(s().prototype,"notificationTypes",(function(o){o.add("discussionLocked",{name:"discussionLocked",icon:"fas fa-lock",label:c().translator.trans("flarum-lock.forum.settings.notify_discussion_locked_label")})}))}))})(),module.exports=t})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/lock/js/dist/forum.js.map
generated
vendored
2
extensions/lock/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0",
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
|
@@ -6,14 +6,7 @@ import Badge from 'flarum/common/components/Badge';
|
||||
export default function addLockBadge() {
|
||||
extend(Discussion.prototype, 'badges', function (badges) {
|
||||
if (this.isLocked()) {
|
||||
badges.add(
|
||||
'locked',
|
||||
Badge.component({
|
||||
type: 'locked',
|
||||
label: app.translator.trans('flarum-lock.forum.badge.locked_tooltip'),
|
||||
icon: 'fas fa-lock',
|
||||
})
|
||||
);
|
||||
badges.add('locked', <Badge type="locked" label={app.translator.trans('flarum-lock.forum.badge.locked_tooltip')} icon="fas fa-lock" />);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -9,15 +9,9 @@ export default function addLockControl() {
|
||||
if (discussion.canLock()) {
|
||||
items.add(
|
||||
'lock',
|
||||
Button.component(
|
||||
{
|
||||
icon: 'fas fa-lock',
|
||||
onclick: this.lockAction.bind(discussion),
|
||||
},
|
||||
app.translator.trans(
|
||||
discussion.isLocked() ? 'flarum-lock.forum.discussion_controls.unlock_button' : 'flarum-lock.forum.discussion_controls.lock_button'
|
||||
)
|
||||
)
|
||||
<Button icon="fas fa-lock" onclick={this.lockAction.bind(discussion)}>
|
||||
{app.translator.trans(`flarum-lock.forum.discussion_controls.${discussion.isLocked() ? 'unlock' : 'lock'}_button`)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -32,7 +32,7 @@ class LockedFilterGambit extends AbstractRegexGambit implements FilterInterface
|
||||
return 'locked';
|
||||
}
|
||||
|
||||
public function filter(FilterState $filterState, string $filterValue, bool $negate)
|
||||
public function filter(FilterState $filterState, $filterValue, bool $negate)
|
||||
{
|
||||
$this->constrain($filterState->getQuery(), $negate);
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
2
extensions/markdown/js/dist/admin.js
generated
vendored
2
extensions/markdown/js/dist/admin.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var t={n:o=>{var e=o&&o.__esModule?()=>o.default:()=>o;return t.d(e,{a:e}),e},d:(o,e)=>{for(var r in e)t.o(e,r)&&!t.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:e[r]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},o={};(()=>{"use strict";t.r(o);const e=flarum.core.compat["admin/app"];var r=t.n(e);const i=flarum.core.compat["common/app"];var n=t.n(i);const a=flarum.core.compat["common/extend"],c=flarum.core.compat["common/components/TextEditor"];var l=t.n(c);const s=flarum.core.compat["common/utils/BasicEditorDriver"];var d=t.n(s);const u=flarum.core.compat["common/utils/styleSelectedText"];var f=t.n(u);function p(t,o){return p=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,o){return t.__proto__=o,t},p(t,o)}function h(t,o){t.prototype=Object.create(o.prototype),t.prototype.constructor=t,p(t,o)}const k=flarum.core.compat["common/Component"];var y=t.n(k),b=function(t){function o(){return t.apply(this,arguments)||this}return h(o,t),o.prototype.view=function(t){return m("div",{class:"MarkdownToolbar"},t.children)},o}(y());const x=flarum.core.compat["common/helpers/icon"];var v=t.n(x);const g=flarum.core.compat["common/components/Tooltip"];var _=t.n(g),w=function(t){function o(){return t.apply(this,arguments)||this}h(o,t);var e=o.prototype;return e.oncreate=function(o){t.prototype.oncreate.call(this,o)},e.view=function(){var t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},v()(this.attrs.icon));return this.attrs.title?m(_(),{text:this.attrs.title},t):t},e.keydown=function(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())},o}(y());const O=flarum.core.compat["common/utils/ItemList"];var T=t.n(O),P=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",S={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},j=function(t,o){f()(o.el,S[t])};function I(t,o,e){return function(r){r.key===o&&(r.metaKey&&"⌘"===P||r.ctrlKey&&"ctrl"===P)&&(r.preventDefault(),j(t,e))}}function F(t){var o=this,e="function"==typeof t?t():new(T());function r(t,o){return n().translator.trans("flarum-markdown.lib.composer."+t+"_tooltip")+(o?" <"+P+"-"+o+">":"")}var i=function(t){return function(){return j(t,o.attrs.composer.editor)}};return e.add("header",m(w,{title:r("header"),icon:"fas fa-heading",onclick:i("header")}),1e3),e.add("bold",m(w,{title:r("bold","b"),icon:"fas fa-bold",onclick:i("bold")}),900),e.add("italic",m(w,{title:r("italic","i"),icon:"fas fa-italic",onclick:i("italic")}),800),e.add("strikethrough",m(w,{title:r("strikethrough"),icon:"fas fa-strikethrough",onclick:i("strikethrough")}),700),e.add("quote",m(w,{title:r("quote"),icon:"fas fa-quote-left",onclick:i("quote")}),600),e.add("spoiler",m(w,{title:r("spoiler"),icon:"fas fa-exclamation-triangle",onclick:i("spoiler")}),500),e.add("code",m(w,{title:r("code"),icon:"fas fa-code",onclick:i("code")}),400),e.add("link",m(w,{title:r("link"),icon:"fas fa-link",onclick:i("link")}),300),e.add("image",m(w,{title:r("image"),icon:"fas fa-image",onclick:i("image")}),200),e.add("unordered_list",m(w,{title:r("unordered_list"),icon:"fas fa-list-ul",onclick:i("unordered_list")}),100),e.add("ordered_list",m(w,{title:r("ordered_list"),icon:"fas fa-list-ol",onclick:i("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(d().prototype,"keyHandlers",(function(t){t.add("bold",I("bold","b",this)),t.add("italic",I("italic","i",this))})),l().prototype.markdownToolbarItems?(0,a.override)(l().prototype,"markdownToolbarItems",F):l().prototype.markdownToolbarItems=F,(0,a.extend)(l().prototype,"toolbarItems",(function(t){t.add("markdown",m(b,{for:this.textareaId,setShortcutHandler:function(t){return shortcutHandler=t}},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=o})();
|
||||
(()=>{var t={n:o=>{var e=o&&o.__esModule?()=>o.default:()=>o;return t.d(e,{a:e}),e},d:(o,e)=>{for(var r in e)t.o(e,r)&&!t.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:e[r]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},o={};(()=>{"use strict";t.r(o);const e=flarum.core.compat["admin/app"];var r=t.n(e);const i=flarum.core.compat["common/app"];var n=t.n(i);const a=flarum.core.compat["common/extend"],c=flarum.core.compat["common/components/TextEditor"];var l=t.n(c);const s=flarum.core.compat["common/utils/BasicEditorDriver"];var d=t.n(s);const u=flarum.core.compat["common/utils/styleSelectedText"];var f=t.n(u);function p(t,o){return p=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,o){return t.__proto__=o,t},p(t,o)}function h(t,o){t.prototype=Object.create(o.prototype),t.prototype.constructor=t,p(t,o)}const k=flarum.core.compat["common/Component"];var y=t.n(k),b=function(t){function o(){return t.apply(this,arguments)||this}return h(o,t),o.prototype.view=function(t){return m("div",{className:"MarkdownToolbar"},t.children)},o}(y());const x=flarum.core.compat["common/helpers/icon"];var v=t.n(x);const g=flarum.core.compat["common/components/Tooltip"];var _=t.n(g),w=function(t){function o(){return t.apply(this,arguments)||this}h(o,t);var e=o.prototype;return e.oncreate=function(o){t.prototype.oncreate.call(this,o)},e.view=function(){var t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},v()(this.attrs.icon));return this.attrs.title?m(_(),{text:this.attrs.title},t):t},e.keydown=function(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())},o}(y());const O=flarum.core.compat["common/utils/ItemList"];var T=t.n(O),P=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",S={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},j=function(t,o){f()(o.el,S[t])};function I(t,o,e){return function(r){r.key===o&&(r.metaKey&&"⌘"===P||r.ctrlKey&&"ctrl"===P)&&(r.preventDefault(),j(t,e))}}function F(t){var o=this,e="function"==typeof t?t():new(T());function r(t,o){return n().translator.trans("flarum-markdown.lib.composer."+t+"_tooltip")+(o?" <"+P+"-"+o+">":"")}var i=function(t){return function(){return j(t,o.attrs.composer.editor)}};return e.add("header",m(w,{title:r("header"),icon:"fas fa-heading",onclick:i("header")}),1e3),e.add("bold",m(w,{title:r("bold","b"),icon:"fas fa-bold",onclick:i("bold")}),900),e.add("italic",m(w,{title:r("italic","i"),icon:"fas fa-italic",onclick:i("italic")}),800),e.add("strikethrough",m(w,{title:r("strikethrough"),icon:"fas fa-strikethrough",onclick:i("strikethrough")}),700),e.add("quote",m(w,{title:r("quote"),icon:"fas fa-quote-left",onclick:i("quote")}),600),e.add("spoiler",m(w,{title:r("spoiler"),icon:"fas fa-exclamation-triangle",onclick:i("spoiler")}),500),e.add("code",m(w,{title:r("code"),icon:"fas fa-code",onclick:i("code")}),400),e.add("link",m(w,{title:r("link"),icon:"fas fa-link",onclick:i("link")}),300),e.add("image",m(w,{title:r("image"),icon:"fas fa-image",onclick:i("image")}),200),e.add("unordered_list",m(w,{title:r("unordered_list"),icon:"fas fa-list-ul",onclick:i("unordered_list")}),100),e.add("ordered_list",m(w,{title:r("ordered_list"),icon:"fas fa-list-ol",onclick:i("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(d().prototype,"keyHandlers",(function(t){t.add("bold",I("bold","b",this)),t.add("italic",I("italic","i",this))})),l().prototype.markdownToolbarItems?(0,a.override)(l().prototype,"markdownToolbarItems",F):l().prototype.markdownToolbarItems=F,(0,a.extend)(l().prototype,"toolbarItems",(function(t){t.add("markdown",m(b,{for:this.textareaId,setShortcutHandler:function(t){return shortcutHandler=t}},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=o})();
|
||||
//# sourceMappingURL=admin.js.map
|
2
extensions/markdown/js/dist/admin.js.map
generated
vendored
2
extensions/markdown/js/dist/admin.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2
extensions/markdown/js/dist/forum.js
generated
vendored
2
extensions/markdown/js/dist/forum.js
generated
vendored
@@ -1,2 +1,2 @@
|
||||
(()=>{var t={n:o=>{var e=o&&o.__esModule?()=>o.default:()=>o;return t.d(e,{a:e}),e},d:(o,e)=>{for(var r in e)t.o(e,r)&&!t.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:e[r]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},o={};(()=>{"use strict";t.r(o);const e=flarum.core.compat["forum/app"];var r=t.n(e);const i=flarum.core.compat["common/app"];var n=t.n(i);const a=flarum.core.compat["common/extend"],c=flarum.core.compat["common/components/TextEditor"];var l=t.n(c);const s=flarum.core.compat["common/utils/BasicEditorDriver"];var d=t.n(s);const u=flarum.core.compat["common/utils/styleSelectedText"];var f=t.n(u);function p(t,o){return p=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,o){return t.__proto__=o,t},p(t,o)}function h(t,o){t.prototype=Object.create(o.prototype),t.prototype.constructor=t,p(t,o)}const k=flarum.core.compat["common/Component"];var y=t.n(k),b=function(t){function o(){return t.apply(this,arguments)||this}return h(o,t),o.prototype.view=function(t){return m("div",{class:"MarkdownToolbar"},t.children)},o}(y());const x=flarum.core.compat["common/helpers/icon"];var v=t.n(x);const g=flarum.core.compat["common/components/Tooltip"];var _=t.n(g),w=function(t){function o(){return t.apply(this,arguments)||this}h(o,t);var e=o.prototype;return e.oncreate=function(o){t.prototype.oncreate.call(this,o)},e.view=function(){var t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},v()(this.attrs.icon));return this.attrs.title?m(_(),{text:this.attrs.title},t):t},e.keydown=function(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())},o}(y());const O=flarum.core.compat["common/utils/ItemList"];var T=t.n(O),P=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",S={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},j=function(t,o){f()(o.el,S[t])};function I(t,o,e){return function(r){r.key===o&&(r.metaKey&&"⌘"===P||r.ctrlKey&&"ctrl"===P)&&(r.preventDefault(),j(t,e))}}function F(t){var o=this,e="function"==typeof t?t():new(T());function r(t,o){return n().translator.trans("flarum-markdown.lib.composer."+t+"_tooltip")+(o?" <"+P+"-"+o+">":"")}var i=function(t){return function(){return j(t,o.attrs.composer.editor)}};return e.add("header",m(w,{title:r("header"),icon:"fas fa-heading",onclick:i("header")}),1e3),e.add("bold",m(w,{title:r("bold","b"),icon:"fas fa-bold",onclick:i("bold")}),900),e.add("italic",m(w,{title:r("italic","i"),icon:"fas fa-italic",onclick:i("italic")}),800),e.add("strikethrough",m(w,{title:r("strikethrough"),icon:"fas fa-strikethrough",onclick:i("strikethrough")}),700),e.add("quote",m(w,{title:r("quote"),icon:"fas fa-quote-left",onclick:i("quote")}),600),e.add("spoiler",m(w,{title:r("spoiler"),icon:"fas fa-exclamation-triangle",onclick:i("spoiler")}),500),e.add("code",m(w,{title:r("code"),icon:"fas fa-code",onclick:i("code")}),400),e.add("link",m(w,{title:r("link"),icon:"fas fa-link",onclick:i("link")}),300),e.add("image",m(w,{title:r("image"),icon:"fas fa-image",onclick:i("image")}),200),e.add("unordered_list",m(w,{title:r("unordered_list"),icon:"fas fa-list-ul",onclick:i("unordered_list")}),100),e.add("ordered_list",m(w,{title:r("ordered_list"),icon:"fas fa-list-ol",onclick:i("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(d().prototype,"keyHandlers",(function(t){t.add("bold",I("bold","b",this)),t.add("italic",I("italic","i",this))})),l().prototype.markdownToolbarItems?(0,a.override)(l().prototype,"markdownToolbarItems",F):l().prototype.markdownToolbarItems=F,(0,a.extend)(l().prototype,"toolbarItems",(function(t){t.add("markdown",m(b,{for:this.textareaId,setShortcutHandler:function(t){return shortcutHandler=t}},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=o})();
|
||||
(()=>{var t={n:o=>{var e=o&&o.__esModule?()=>o.default:()=>o;return t.d(e,{a:e}),e},d:(o,e)=>{for(var r in e)t.o(e,r)&&!t.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:e[r]})},o:(t,o)=>Object.prototype.hasOwnProperty.call(t,o),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},o={};(()=>{"use strict";t.r(o);const e=flarum.core.compat["forum/app"];var r=t.n(e);const i=flarum.core.compat["common/app"];var n=t.n(i);const a=flarum.core.compat["common/extend"],c=flarum.core.compat["common/components/TextEditor"];var l=t.n(c);const s=flarum.core.compat["common/utils/BasicEditorDriver"];var d=t.n(s);const u=flarum.core.compat["common/utils/styleSelectedText"];var f=t.n(u);function p(t,o){return p=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,o){return t.__proto__=o,t},p(t,o)}function h(t,o){t.prototype=Object.create(o.prototype),t.prototype.constructor=t,p(t,o)}const k=flarum.core.compat["common/Component"];var y=t.n(k),b=function(t){function o(){return t.apply(this,arguments)||this}return h(o,t),o.prototype.view=function(t){return m("div",{className:"MarkdownToolbar"},t.children)},o}(y());const x=flarum.core.compat["common/helpers/icon"];var v=t.n(x);const g=flarum.core.compat["common/components/Tooltip"];var _=t.n(g),w=function(t){function o(){return t.apply(this,arguments)||this}h(o,t);var e=o.prototype;return e.oncreate=function(o){t.prototype.oncreate.call(this,o)},e.view=function(){var t=m("button",{className:"Button Button--icon Button--link",type:"button","data-hotkey":this.attrs.hotkey,onkeydown:this.keydown.bind(this),onclick:this.attrs.onclick},v()(this.attrs.icon));return this.attrs.title?m(_(),{text:this.attrs.title},t):t},e.keydown=function(t){" "!==t.key&&"Enter"!==t.key||(t.preventDefault(),this.element.click())},o}(y());const O=flarum.core.compat["common/utils/ItemList"];var T=t.n(O),P=navigator.userAgent.match(/Macintosh/)?"⌘":"ctrl",S={header:{prefix:"### "},bold:{prefix:"**",suffix:"**",trimFirst:!0},italic:{prefix:"_",suffix:"_",trimFirst:!0},strikethrough:{prefix:"~~",suffix:"~~",trimFirst:!0},quote:{prefix:"> ",multiline:!0,surroundWithNewlines:!0},code:{prefix:"`",suffix:"`",blockPrefix:"```",blockSuffix:"```"},link:{prefix:"[",suffix:"](https://)",replaceNext:"https://",scanFor:"https?://"},image:{prefix:"",replaceNext:"https://",scanFor:"https?://"},unordered_list:{prefix:"- ",multiline:!0,surroundWithNewlines:!0},ordered_list:{prefix:"1. ",multiline:!0,orderedList:!0},spoiler:{prefix:">!",suffix:"!<",blockPrefix:">! ",multiline:!0,trimFirst:!0}},j=function(t,o){f()(o.el,S[t])};function I(t,o,e){return function(r){r.key===o&&(r.metaKey&&"⌘"===P||r.ctrlKey&&"ctrl"===P)&&(r.preventDefault(),j(t,e))}}function F(t){var o=this,e="function"==typeof t?t():new(T());function r(t,o){return n().translator.trans("flarum-markdown.lib.composer."+t+"_tooltip")+(o?" <"+P+"-"+o+">":"")}var i=function(t){return function(){return j(t,o.attrs.composer.editor)}};return e.add("header",m(w,{title:r("header"),icon:"fas fa-heading",onclick:i("header")}),1e3),e.add("bold",m(w,{title:r("bold","b"),icon:"fas fa-bold",onclick:i("bold")}),900),e.add("italic",m(w,{title:r("italic","i"),icon:"fas fa-italic",onclick:i("italic")}),800),e.add("strikethrough",m(w,{title:r("strikethrough"),icon:"fas fa-strikethrough",onclick:i("strikethrough")}),700),e.add("quote",m(w,{title:r("quote"),icon:"fas fa-quote-left",onclick:i("quote")}),600),e.add("spoiler",m(w,{title:r("spoiler"),icon:"fas fa-exclamation-triangle",onclick:i("spoiler")}),500),e.add("code",m(w,{title:r("code"),icon:"fas fa-code",onclick:i("code")}),400),e.add("link",m(w,{title:r("link"),icon:"fas fa-link",onclick:i("link")}),300),e.add("image",m(w,{title:r("image"),icon:"fas fa-image",onclick:i("image")}),200),e.add("unordered_list",m(w,{title:r("unordered_list"),icon:"fas fa-list-ul",onclick:i("unordered_list")}),100),e.add("ordered_list",m(w,{title:r("ordered_list"),icon:"fas fa-list-ol",onclick:i("ordered_list")}),0),e}r().initializers.add("flarum-markdown",(function(t){(0,a.extend)(d().prototype,"keyHandlers",(function(t){t.add("bold",I("bold","b",this)),t.add("italic",I("italic","i",this))})),l().prototype.markdownToolbarItems?(0,a.override)(l().prototype,"markdownToolbarItems",F):l().prototype.markdownToolbarItems=F,(0,a.extend)(l().prototype,"toolbarItems",(function(t){t.add("markdown",m(b,{for:this.textareaId,setShortcutHandler:function(t){return shortcutHandler=t}},this.markdownToolbarItems().toArray()),100)}))}))})(),module.exports=o})();
|
||||
//# sourceMappingURL=forum.js.map
|
2
extensions/markdown/js/dist/forum.js.map
generated
vendored
2
extensions/markdown/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"dependencies": {
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -20,7 +20,7 @@
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1"
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,6 @@ import Component from 'flarum/common/Component';
|
||||
|
||||
export default class MarkdownToolbar extends Component {
|
||||
view(vnode) {
|
||||
return <div class="MarkdownToolbar">{vnode.children}</div>;
|
||||
return <div className="MarkdownToolbar">{vnode.children}</div>;
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"flarum/core": "^1.6.3"
|
||||
"flarum/core": "^1.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -33,6 +33,9 @@
|
||||
"flarum-extension": {
|
||||
"title": "Mentions",
|
||||
"category": "feature",
|
||||
"optional-dependencies": [
|
||||
"flarum/tags"
|
||||
],
|
||||
"icon": {
|
||||
"name": "fas fa-at",
|
||||
"backgroundColor": "#539EC1",
|
||||
@@ -74,6 +77,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"flarum/core": "*@dev",
|
||||
"flarum/tags": "*@dev",
|
||||
"flarum/testing": "^1.0.0"
|
||||
},
|
||||
"repositories": [
|
||||
|
@@ -18,6 +18,7 @@ use Flarum\Api\Serializer\PostSerializer;
|
||||
use Flarum\Approval\Event\PostWasApproved;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Mentions\Api\LoadMentionedByRelationship;
|
||||
use Flarum\Post\Event\Deleted;
|
||||
use Flarum\Post\Event\Hidden;
|
||||
use Flarum\Post\Event\Posted;
|
||||
@@ -25,6 +26,8 @@ use Flarum\Post\Event\Restored;
|
||||
use Flarum\Post\Event\Revised;
|
||||
use Flarum\Post\Filter\PostFilterer;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Api\Serializer\TagSerializer;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
@@ -37,18 +40,19 @@ return [
|
||||
|
||||
(new Extend\Formatter)
|
||||
->configure(ConfigureMentions::class)
|
||||
->parse(Formatter\EagerLoadMentionedModels::class)
|
||||
->render(Formatter\FormatPostMentions::class)
|
||||
->render(Formatter\FormatUserMentions::class)
|
||||
->render(Formatter\FormatGroupMentions::class)
|
||||
->unparse(Formatter\UnparsePostMentions::class)
|
||||
->unparse(Formatter\UnparseUserMentions::class)
|
||||
->parse(Formatter\CheckPermissions::class),
|
||||
->unparse(Formatter\UnparseUserMentions::class),
|
||||
|
||||
(new Extend\Model(Post::class))
|
||||
->belongsToMany('mentionedBy', Post::class, 'post_mentions_post', 'mentions_post_id', 'post_id')
|
||||
->belongsToMany('mentionsPosts', Post::class, 'post_mentions_post', 'post_id', 'mentions_post_id')
|
||||
->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id')
|
||||
->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id'),
|
||||
->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id')
|
||||
->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id'),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
@@ -64,41 +68,39 @@ return [
|
||||
->hasMany('mentionedBy', BasicPostSerializer::class)
|
||||
->hasMany('mentionsPosts', BasicPostSerializer::class)
|
||||
->hasMany('mentionsUsers', BasicUserSerializer::class)
|
||||
->hasMany('mentionsGroups', GroupSerializer::class),
|
||||
->hasMany('mentionsGroups', GroupSerializer::class)
|
||||
->attribute('mentionedByCount', function (BasicPostSerializer $serializer, Post $post) {
|
||||
// Only if it was eager loaded.
|
||||
return $post->getAttribute('mentioned_by_count') ?? 0;
|
||||
}),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
|
||||
->load([
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user', 'posts.mentionedBy',
|
||||
'posts.mentionedBy.mentionsPosts', 'posts.mentionedBy.mentionsPosts.user', 'posts.mentionedBy.mentionsUsers',
|
||||
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user',
|
||||
'posts.mentionsGroups'
|
||||
]),
|
||||
])
|
||||
->loadWhere('posts.mentionedBy', [LoadMentionedByRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadMentionedByRelationship::class, 'countRelation']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsUsers', 'firstPost.mentionsPosts', 'firstPost.mentionsPosts.user', 'firstPost.mentionsGroups',
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user', 'lastPost.mentionsGroups'
|
||||
'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user', 'lastPost.mentionsGroups',
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion']),
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
// We wouldn't normally need to eager load on a single model,
|
||||
// but we do so here for visibility scoping.
|
||||
->loadWhere('mentionedBy', [LoadMentionedByRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadMentionedByRelationship::class, 'countRelation']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->addInclude(['mentionedBy', 'mentionedBy.user', 'mentionedBy.discussion'])
|
||||
->load([
|
||||
'mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionedBy',
|
||||
'mentionedBy.mentionsPosts', 'mentionedBy.mentionsPosts.user', 'mentionedBy.mentionsUsers',
|
||||
'mentionsGroups'
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
||||
->addOptionalInclude('mentionsGroups'),
|
||||
|
||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
||||
->addOptionalInclude('mentionsGroups'),
|
||||
|
||||
(new Extend\ApiController(Controller\AbstractSerializeController::class))
|
||||
->prepareDataForSerialization(FilterVisiblePosts::class),
|
||||
->load(['mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionsGroups'])
|
||||
->loadWhere('mentionedBy', [LoadMentionedByRelationship::class, 'mutateRelation'])
|
||||
->prepareDataForSerialization([LoadMentionedByRelationship::class, 'countRelation']),
|
||||
|
||||
(new Extend\Settings)
|
||||
->serializeToForum('allowUsernameMentionFormat', 'flarum-mentions.allow_username_format', 'boolval'),
|
||||
@@ -112,10 +114,33 @@ return [
|
||||
->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class),
|
||||
|
||||
(new Extend\Filter(PostFilterer::class))
|
||||
->addFilter(Filter\MentionedFilter::class),
|
||||
->addFilter(Filter\MentionedFilter::class)
|
||||
->addFilter(Filter\MentionedPostFilter::class),
|
||||
|
||||
(new Extend\ApiSerializer(CurrentUserSerializer::class))
|
||||
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user, array $attributes): bool {
|
||||
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool {
|
||||
return $user->can('mentionGroups');
|
||||
})
|
||||
}),
|
||||
|
||||
// Tag mentions
|
||||
(new Extend\Conditional())
|
||||
->whenExtensionEnabled('flarum-tags', [
|
||||
(new Extend\Formatter)
|
||||
->render(Formatter\FormatTagMentions::class)
|
||||
->unparse(Formatter\UnparseTagMentions::class),
|
||||
|
||||
(new Extend\ApiSerializer(BasicPostSerializer::class))
|
||||
->hasMany('mentionsTags', TagSerializer::class),
|
||||
|
||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
||||
->load(['posts.mentionsTags']),
|
||||
|
||||
(new Extend\ApiController(Controller\ListDiscussionsController::class))
|
||||
->load([
|
||||
'firstPost.mentionsTags', 'lastPost.mentionsTags',
|
||||
]),
|
||||
|
||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
||||
->load(['mentionsTags']),
|
||||
]),
|
||||
];
|
||||
|
3
extensions/mentions/js/dist/forum.js
generated
vendored
3
extensions/mentions/js/dist/forum.js
generated
vendored
File diff suppressed because one or more lines are too long
1
extensions/mentions/js/dist/forum.js.LICENSE.txt
generated
vendored
Normal file
1
extensions/mentions/js/dist/forum.js.LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
2
extensions/mentions/js/dist/forum.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1",
|
||||
"flarum-webpack-config": "^2.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack": "^5.76.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"@flarum/prettier-config": "^1.0.0"
|
||||
},
|
||||
|
21
extensions/mentions/js/src/@types/shims.d.ts
vendored
Normal file
21
extensions/mentions/js/src/@types/shims.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import MentionFormats from '../forum/mentionables/formats/MentionFormats';
|
||||
import type BasePost from 'flarum/common/models/Post';
|
||||
|
||||
declare module 'flarum/forum/ForumApplication' {
|
||||
export default interface ForumApplication {
|
||||
mentionFormats: MentionFormats;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'flarum/common/models/User' {
|
||||
export default interface User {
|
||||
canMentionGroups(): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'flarum/common/models/Post' {
|
||||
export default interface Post {
|
||||
mentionedBy(): BasePost[] | undefined | null;
|
||||
mentionedByCount(): number;
|
||||
}
|
||||
}
|
@@ -2,42 +2,15 @@ import app from 'flarum/forum/app';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import TextEditor from 'flarum/common/components/TextEditor';
|
||||
import TextEditorButton from 'flarum/common/components/TextEditorButton';
|
||||
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
|
||||
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import usernameHelper from 'flarum/common/helpers/username';
|
||||
import highlight from 'flarum/common/helpers/highlight';
|
||||
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
|
||||
import { truncate } from 'flarum/common/utils/string';
|
||||
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||
import Badge from 'flarum/common/components/Badge';
|
||||
import Group from 'flarum/common/models/Group';
|
||||
|
||||
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
|
||||
import getMentionText from './utils/getMentionText';
|
||||
|
||||
const throttledSearch = throttle(
|
||||
250, // 250ms timeout
|
||||
function (typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions) {
|
||||
const typedLower = typed.toLowerCase();
|
||||
if (!searched.includes(typedLower)) {
|
||||
app.store.find('users', { filter: { q: typed }, page: { limit: 5 } }).then((results) => {
|
||||
results.forEach((u) => {
|
||||
if (!returnedUserIds.has(u.id())) {
|
||||
returnedUserIds.add(u.id());
|
||||
returnedUsers.push(u);
|
||||
}
|
||||
});
|
||||
|
||||
buildSuggestions();
|
||||
});
|
||||
|
||||
searched.push(typedLower);
|
||||
}
|
||||
}
|
||||
);
|
||||
import MentionFormats from './mentionables/formats/MentionFormats';
|
||||
import MentionableModels from './mentionables/MentionableModels';
|
||||
|
||||
export default function addComposerAutocomplete() {
|
||||
app.mentionFormats = new MentionFormats();
|
||||
|
||||
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
|
||||
const dropdown = new AutocompleteDropdown();
|
||||
|
||||
@@ -57,47 +30,42 @@ export default function addComposerAutocomplete() {
|
||||
});
|
||||
|
||||
extend(TextEditor.prototype, 'buildEditorParams', function (params) {
|
||||
const searched = [];
|
||||
let relMentionStart;
|
||||
let absMentionStart;
|
||||
let typed;
|
||||
let matchTyped;
|
||||
|
||||
// We store users returned from an API here to preserve order in which they are returned
|
||||
// This prevents the user list jumping around while users are returned.
|
||||
// We also use a hashset for user IDs to provide O(1) lookup for the users already in the list.
|
||||
const returnedUsers = Array.from(app.store.all('users'));
|
||||
const returnedUserIds = new Set(returnedUsers.map((u) => u.id()));
|
||||
let mentionables = new MentionableModels({
|
||||
onmouseenter: function () {
|
||||
dropdown.setIndex($(this).parent().index());
|
||||
},
|
||||
onclick: (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
|
||||
|
||||
// Store groups, but exclude the two virtual groups - 'Guest' and 'Member'.
|
||||
const returnedGroups = Array.from(
|
||||
app.store.all('groups').filter((group) => {
|
||||
return group.id() != Group.GUEST_ID && group.id() != Group.MEMBER_ID;
|
||||
})
|
||||
);
|
||||
dropdown.hide();
|
||||
},
|
||||
});
|
||||
|
||||
const applySuggestion = (replacement) => {
|
||||
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');
|
||||
|
||||
dropdown.hide();
|
||||
};
|
||||
|
||||
params.inputListeners.push(() => {
|
||||
const suggestionsInputListener = () => {
|
||||
const selection = this.attrs.composer.editor.getSelectionRange();
|
||||
|
||||
const cursor = selection[0];
|
||||
|
||||
if (selection[1] - cursor > 0) return;
|
||||
|
||||
// Search backwards from the cursor for an '@' symbol. If we find one,
|
||||
// we will want to show the autocomplete dropdown!
|
||||
// Search backwards from the cursor for a mention triggering symbol. If we find one,
|
||||
// we will want to show the correct autocomplete dropdown!
|
||||
// Check classes implementing the IMentionableModel interface to see triggering symbols.
|
||||
const lastChunk = this.attrs.composer.editor.getLastNChars(30);
|
||||
absMentionStart = 0;
|
||||
let activeFormat = null;
|
||||
for (let i = lastChunk.length - 1; i >= 0; i--) {
|
||||
const character = lastChunk.substr(i, 1);
|
||||
if (character === '@' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
|
||||
activeFormat = app.mentionFormats.get(character);
|
||||
|
||||
if (activeFormat && (i === 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
|
||||
relMentionStart = i + 1;
|
||||
absMentionStart = cursor - lastChunk.length + i + 1;
|
||||
mentionables.init(activeFormat.makeMentionables());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -106,132 +74,17 @@ export default function addComposerAutocomplete() {
|
||||
dropdown.active = false;
|
||||
|
||||
if (absMentionStart) {
|
||||
typed = lastChunk.substring(relMentionStart).toLowerCase();
|
||||
matchTyped = typed.match(/^["|“]((?:(?!"#).)+)$/);
|
||||
typed = (matchTyped && matchTyped[1]) || typed;
|
||||
const typed = lastChunk.substring(relMentionStart).toLowerCase();
|
||||
matchTyped = activeFormat.queryFromTyped(typed);
|
||||
|
||||
const makeSuggestion = function (user, replacement, content, className = '') {
|
||||
const username = usernameHelper(user);
|
||||
if (!matchTyped) return;
|
||||
|
||||
if (typed) {
|
||||
username.children = [highlight(username.text, typed)];
|
||||
delete username.text;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={'PostPreview ' + className}
|
||||
onclick={() => applySuggestion(replacement)}
|
||||
onmouseenter={function () {
|
||||
dropdown.setIndex($(this).parent().index());
|
||||
}}
|
||||
>
|
||||
<span className="PostPreview-content">
|
||||
{avatar(user)}
|
||||
{username} {content}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const makeGroupSuggestion = function (group, replacement, content, className = '') {
|
||||
let groupName = group.namePlural().toLowerCase();
|
||||
|
||||
if (typed) {
|
||||
groupName = highlight(groupName, typed);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={'PostPreview ' + className}
|
||||
onclick={() => applySuggestion(replacement)}
|
||||
onmouseenter={function () {
|
||||
dropdown.setIndex($(this).parent().index());
|
||||
}}
|
||||
>
|
||||
<span className="PostPreview-content">
|
||||
<Badge class={`Avatar Badge Badge--group--${group.id()} Badge-icon `} color={group.color()} type="group" icon={group.icon()} />
|
||||
<span className="username">{groupName}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const userMatches = function (user) {
|
||||
const names = [user.username(), user.displayName()];
|
||||
|
||||
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
|
||||
};
|
||||
|
||||
const groupMatches = function (group) {
|
||||
const names = [group.nameSingular(), group.namePlural()];
|
||||
|
||||
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
|
||||
};
|
||||
mentionables.typed = matchTyped;
|
||||
|
||||
const buildSuggestions = () => {
|
||||
const suggestions = [];
|
||||
|
||||
// If the user has started to type a username, then suggest users
|
||||
// matching that username.
|
||||
if (typed) {
|
||||
returnedUsers.forEach((user) => {
|
||||
if (!userMatches(user)) return;
|
||||
|
||||
suggestions.push(makeSuggestion(user, getMentionText(user), '', 'MentionsDropdown-user'));
|
||||
});
|
||||
|
||||
// ... or groups.
|
||||
if (app.session?.user?.canMentionGroups()) {
|
||||
returnedGroups.forEach((group) => {
|
||||
if (!groupMatches(group)) return;
|
||||
|
||||
suggestions.push(makeGroupSuggestion(group, getMentionText(undefined, undefined, group), '', 'MentionsDropdown-group'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is replying to a discussion, or if they are editing a
|
||||
// post, then we can suggest other posts in the discussion to mention.
|
||||
// We will add the 5 most recent comments in the discussion which
|
||||
// match any username characters that have been typed.
|
||||
if (this.attrs.composer.bodyMatches(ReplyComposer) || this.attrs.composer.bodyMatches(EditPostComposer)) {
|
||||
const composerAttrs = this.attrs.composer.body.attrs;
|
||||
const composerPost = composerAttrs.post;
|
||||
const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;
|
||||
|
||||
if (discussion) {
|
||||
discussion
|
||||
.posts()
|
||||
// Filter to only comment posts, and replies before this message
|
||||
.filter((post) => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
|
||||
// Sort by new to old
|
||||
.sort((a, b) => b.createdAt() - a.createdAt())
|
||||
// Filter to where the user matches what is being typed
|
||||
.filter((post) => {
|
||||
const user = post.user();
|
||||
return user && userMatches(user);
|
||||
})
|
||||
// Get the first 5
|
||||
.splice(0, 5)
|
||||
// Make the suggestions
|
||||
.forEach((post) => {
|
||||
const user = post.user();
|
||||
suggestions.push(
|
||||
makeSuggestion(
|
||||
user,
|
||||
getMentionText(user, post.id()),
|
||||
[
|
||||
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }),
|
||||
' — ',
|
||||
truncate(post.contentPlain(), 200),
|
||||
],
|
||||
'MentionsDropdown-post'
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
// If the user has started to type a mention,
|
||||
// then suggest models matching.
|
||||
const suggestions = mentionables.buildSuggestions();
|
||||
|
||||
if (suggestions.length) {
|
||||
dropdown.items = suggestions;
|
||||
@@ -271,13 +124,11 @@ export default function addComposerAutocomplete() {
|
||||
dropdown.setIndex(0);
|
||||
dropdown.$().scrollTop(0);
|
||||
|
||||
// Don't send API calls searching for users until at least 2 characters have been typed.
|
||||
// This focuses the mention results on users and posts in the discussion.
|
||||
if (typed.length > 1 && app.forum.attribute('canSearchUsers')) {
|
||||
throttledSearch(typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions);
|
||||
}
|
||||
mentionables.search()?.then(buildSuggestions);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
params.inputListeners.push(suggestionsInputListener);
|
||||
});
|
||||
|
||||
extend(TextEditor.prototype, 'toolbarItems', function (items) {
|
||||
|
@@ -6,6 +6,8 @@ import PostPreview from 'flarum/forum/components/PostPreview';
|
||||
import punctuateSeries from 'flarum/common/helpers/punctuateSeries';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import icon from 'flarum/common/helpers/icon';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import MentionedByModal from './components/MentionedByModal';
|
||||
|
||||
export default function addMentionedByList() {
|
||||
function hidePreview() {
|
||||
@@ -36,14 +38,31 @@ export default function addMentionedByList() {
|
||||
// popup.
|
||||
m.render(
|
||||
$preview[0],
|
||||
replies.map((reply) => (
|
||||
<li data-number={reply.number()}>
|
||||
{PostPreview.component({
|
||||
post: reply,
|
||||
onclick: hidePreview.bind(this),
|
||||
})}
|
||||
</li>
|
||||
))
|
||||
<>
|
||||
{replies.map((reply) => (
|
||||
<li data-number={reply.number()}>
|
||||
<PostPreview post={reply} onclick={hidePreview.bind(this)} />
|
||||
</li>
|
||||
))}
|
||||
{replies.length < post.mentionedByCount() && (
|
||||
<li className="Post-mentionedBy-preview-more">
|
||||
<Button
|
||||
className="PostPreview Button"
|
||||
onclick={() => {
|
||||
hidePreview.call(this);
|
||||
app.modal.show(MentionedByModal, { post });
|
||||
}}
|
||||
>
|
||||
<span className="PostPreview-content">
|
||||
<span className="PostPreview-badge Avatar">{icon('fas fa-reply-all')}</span>
|
||||
<span>
|
||||
{app.translator.trans('flarum-mentions.forum.post.mentioned_by_more_text', { count: post.mentionedByCount() - replies.length })}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
$preview
|
||||
@@ -127,7 +146,7 @@ export default function addMentionedByList() {
|
||||
<div className="Post-mentionedBy">
|
||||
<span className="Post-mentionedBy-summary">
|
||||
{icon('fas fa-reply')}
|
||||
{app.translator.trans('flarum-mentions.forum.post.mentioned_by' + (repliers[0].user() === app.session.user ? '_self' : '') + '_text', {
|
||||
{app.translator.trans(`flarum-mentions.forum.post.mentioned_by${repliers[0].user() === app.session.user ? '_self' : ''}_text`, {
|
||||
count: names.length,
|
||||
users: punctuateSeries(names),
|
||||
})}
|
||||
|
@@ -14,10 +14,14 @@ export default function addPostMentionPreviews() {
|
||||
const parentPost = this.attrs.post;
|
||||
const $parentPost = this.$();
|
||||
|
||||
this.$().on('click', '.UserMention:not(.UserMention--deleted), .PostMention:not(.PostMention--deleted)', function (e) {
|
||||
m.route.set(this.getAttribute('href'));
|
||||
e.preventDefault();
|
||||
});
|
||||
this.$().on(
|
||||
'click',
|
||||
'.UserMention:not(.UserMention--deleted), .PostMention:not(.PostMention--deleted), .TagMention:not(.TagMention--deleted)',
|
||||
function (e) {
|
||||
m.route.set(this.getAttribute('href'));
|
||||
e.preventDefault();
|
||||
}
|
||||
);
|
||||
|
||||
this.$('.PostMention:not(.PostMention--deleted)').each(function () {
|
||||
const $this = $(this);
|
||||
@@ -76,14 +80,14 @@ export default function addPostMentionPreviews() {
|
||||
const discussion = post.discussion();
|
||||
|
||||
m.render($preview[0], [
|
||||
discussion !== parentPost.discussion() ? (
|
||||
discussion !== parentPost.discussion() && (
|
||||
<li>
|
||||
<span className="PostMention-preview-discussion">{discussion.title()}</span>
|
||||
</li>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
<li>{PostPreview.component({ post })}</li>,
|
||||
<li>
|
||||
<PostPreview post={post} />
|
||||
</li>,
|
||||
]);
|
||||
positionPreview();
|
||||
};
|
||||
@@ -92,7 +96,7 @@ export default function addPostMentionPreviews() {
|
||||
if (post && post.discussion()) {
|
||||
showPost(post);
|
||||
} else {
|
||||
m.render($preview[0], LoadingIndicator.component());
|
||||
m.render($preview[0], <LoadingIndicator />);
|
||||
app.store.find('posts', id).then(showPost);
|
||||
positionPreview();
|
||||
}
|
||||
|
@@ -9,6 +9,9 @@ import getMentionText from './utils/getMentionText';
|
||||
import * as reply from './utils/reply';
|
||||
import selectedText from './utils/selectedText';
|
||||
import * as textFormatter from './utils/textFormatter';
|
||||
import MentionableModel from './mentionables/MentionableModel';
|
||||
import MentionFormat from './mentionables/formats/MentionFormat';
|
||||
import Mentionables from './extenders/Mentionables';
|
||||
|
||||
export default {
|
||||
'mentions/components/MentionsUserPage': MentionsUserPage,
|
||||
@@ -22,4 +25,7 @@ export default {
|
||||
'mentions/utils/reply': reply,
|
||||
'mentions/utils/selectedText': selectedText,
|
||||
'mentions/utils/textFormatter': textFormatter,
|
||||
'mentions/mentionables/MentionableModel': MentionableModel,
|
||||
'mentions/mentionables/formats/MentionFormat': MentionFormat,
|
||||
'mentions/extenders/Mentionables': Mentionables,
|
||||
};
|
||||
|
@@ -0,0 +1,73 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import PostPreview from 'flarum/forum/components/PostPreview';
|
||||
import Modal, { IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import type Mithril from 'mithril';
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import MentionedByModalState from '../state/MentionedByModalState';
|
||||
|
||||
export interface IMentionedByModalAttrs extends IInternalModalAttrs {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
export default class MentionedByModal<CustomAttrs extends IMentionedByModalAttrs = IMentionedByModalAttrs> extends Modal<
|
||||
CustomAttrs,
|
||||
MentionedByModalState
|
||||
> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.state = new MentionedByModalState({
|
||||
filter: {
|
||||
mentionedPost: this.attrs.post.id()!,
|
||||
},
|
||||
sort: 'number',
|
||||
});
|
||||
|
||||
this.state.refresh();
|
||||
}
|
||||
|
||||
className(): string {
|
||||
return 'MentionedByModal';
|
||||
}
|
||||
|
||||
title(): Mithril.Children {
|
||||
return app.translator.trans('flarum-mentions.forum.mentioned_by.title');
|
||||
}
|
||||
|
||||
content(): Mithril.Children {
|
||||
return (
|
||||
<>
|
||||
<div className="Modal-body">
|
||||
{this.state.isInitialLoading() ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<>
|
||||
<ul className="MentionedByModal-list Dropdown-menu Dropdown-menu--inline Post-mentionedBy-preview">
|
||||
{this.state.getPages().map((page) =>
|
||||
page.items.map((reply) => (
|
||||
<li data-number={reply.number()}>
|
||||
<PostPreview post={reply} onclick={() => app.modal.close()} />
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{this.state.hasNext() && (
|
||||
<div className="Modal-footer">
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--block" onclick={() => this.state.loadNext()} loading={this.state.isLoadingNext()}>
|
||||
{app.translator.trans('flarum-mentions.forum.mentioned_by.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
import Component from 'flarum/common/Component';
|
||||
import type { ComponentAttrs } from 'flarum/common/Component';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
import type MentionableModel from '../mentionables/MentionableModel';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface IMentionsDropdownItemAttrs extends ComponentAttrs {
|
||||
mentionable: MentionableModel;
|
||||
onclick: () => void;
|
||||
onmouseenter: () => void;
|
||||
}
|
||||
|
||||
export default class MentionsDropdownItem<CustomAttrs extends IMentionsDropdownItemAttrs> extends Component<CustomAttrs> {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs>): Mithril.Children {
|
||||
const { mentionable, ...attrs } = this.attrs;
|
||||
|
||||
const className = classList('MentionsDropdownItem', 'PostPreview', `MentionsDropdown-${mentionable.type()}`);
|
||||
|
||||
return (
|
||||
<button className={className} {...attrs}>
|
||||
<span className="PostPreview-content">{vnode.children}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
@@ -8,7 +8,8 @@ export default [
|
||||
.add('user.mentions', '/u/:username/mentions', MentionsUserPage),
|
||||
|
||||
new Extend.Model(Post) //
|
||||
.hasMany<Post>('mentionedBy'),
|
||||
.hasMany<Post>('mentionedBy')
|
||||
.attribute<number>('mentionedByCount'),
|
||||
|
||||
new Extend.Model(User) //
|
||||
.attribute<boolean>('canMentionGroups'),
|
||||
|
54
extensions/mentions/js/src/forum/extenders/Mentionables.ts
Normal file
54
extensions/mentions/js/src/forum/extenders/Mentionables.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type ForumApplication from 'flarum/forum/ForumApplication';
|
||||
import type IExtender from 'flarum/common/extenders/IExtender';
|
||||
import type MentionableModel from '../mentionables/MentionableModel';
|
||||
import type MentionFormat from '../mentionables/formats/MentionFormat';
|
||||
|
||||
export default class Mentionables implements IExtender<ForumApplication> {
|
||||
protected formats: (new () => MentionFormat)[] = [];
|
||||
protected mentionables: Record<string, (new () => MentionableModel)[]> = {};
|
||||
|
||||
/**
|
||||
* Register a new mention format.
|
||||
* Must extend MentionFormat and have a unique unused trigger symbol.
|
||||
*/
|
||||
format(format: new () => MentionFormat): this {
|
||||
this.formats.push(format);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new mentionable model to a mention format.
|
||||
* Only works if the format has already been registered,
|
||||
* and the format allows using multiple mentionables.
|
||||
*
|
||||
* @param symbol The trigger symbol of the format to extend (ex: @).
|
||||
* @param mentionable The mentionable instance to register.
|
||||
* Must extend MentionableModel.
|
||||
*/
|
||||
mentionable(symbol: string, mentionable: new () => MentionableModel): this {
|
||||
if (!this.mentionables[symbol]) {
|
||||
this.mentionables[symbol] = [];
|
||||
}
|
||||
|
||||
this.mentionables[symbol].push(mentionable);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(app: ForumApplication): void {
|
||||
for (const format of this.formats) {
|
||||
app.mentionFormats.extend(format);
|
||||
}
|
||||
|
||||
for (const symbol in this.mentionables) {
|
||||
const format = app.mentionFormats.get(symbol);
|
||||
|
||||
if (!format) continue;
|
||||
|
||||
for (const mentionable of this.mentionables[symbol]) {
|
||||
format.extend(mentionable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,7 +14,7 @@ export default class PostQuoteButton extends Fragment {
|
||||
view() {
|
||||
return (
|
||||
<button
|
||||
class="Button PostQuoteButton"
|
||||
className="Button PostQuoteButton"
|
||||
onclick={() => {
|
||||
reply(this.post, this.content);
|
||||
}}
|
||||
|
@@ -70,14 +70,9 @@ app.initializers.add('flarum-mentions', function () {
|
||||
const user = this.user;
|
||||
items.add(
|
||||
'mentions',
|
||||
LinkButton.component(
|
||||
{
|
||||
href: app.route('user.mentions', { username: user.slug() }),
|
||||
name: 'mentions',
|
||||
icon: 'fas fa-at',
|
||||
},
|
||||
app.translator.trans('flarum-mentions.forum.user.mentions_link')
|
||||
),
|
||||
<LinkButton href={app.route('user.mentions', { username: user.slug() })} name="mentions" icon="fas fa-at">
|
||||
{app.translator.trans('flarum-mentions.forum.user.mentions_link')}
|
||||
</LinkButton>,
|
||||
80
|
||||
);
|
||||
});
|
||||
@@ -87,8 +82,8 @@ app.initializers.add('flarum-mentions', function () {
|
||||
|
||||
// Apply color contrast fix on group mentions.
|
||||
extend(Post.prototype, 'oncreate', function () {
|
||||
this.$('.GroupMention--colored').each(function () {
|
||||
this.classList.add(textContrastClass(getComputedStyle(this).getPropertyValue('--group-color')));
|
||||
this.$('.GroupMention--colored, .TagMention--colored').each(function () {
|
||||
this.classList.add(textContrastClass(getComputedStyle(this).getPropertyValue('--color')));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -0,0 +1,72 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Group from 'flarum/common/models/Group';
|
||||
import MentionableModel from './MentionableModel';
|
||||
import type Mithril from 'mithril';
|
||||
import Badge from 'flarum/common/components/Badge';
|
||||
import highlight from 'flarum/common/helpers/highlight';
|
||||
import type AtMentionFormat from './formats/AtMentionFormat';
|
||||
|
||||
export default class GroupMention extends MentionableModel<Group, AtMentionFormat> {
|
||||
type(): string {
|
||||
return 'group';
|
||||
}
|
||||
|
||||
initialResults(): Group[] {
|
||||
return Array.from(
|
||||
app.store.all<Group>('groups').filter((g: Group) => {
|
||||
return g.id() !== Group.GUEST_ID && g.id() !== Group.MEMBER_ID;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the mention syntax for a group mention.
|
||||
*
|
||||
* @"Name Plural"#gGroupID
|
||||
*
|
||||
* @example <caption>Group mention</caption>
|
||||
* // '@"Mods"#g4'
|
||||
* forGroup(group) // Group display name is 'Mods', group ID is 4
|
||||
*/
|
||||
public replacement(group: Group): string {
|
||||
return this.format.format(group.namePlural(), 'g', group.id());
|
||||
}
|
||||
|
||||
suggestion(model: Group, typed: string): Mithril.Children {
|
||||
let groupName: Mithril.Children = model.namePlural();
|
||||
|
||||
if (typed) {
|
||||
groupName = highlight(groupName, typed);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Badge className={`Avatar Badge Badge--group--${model.id()} Badge-icon`} color={model.color()} type="group" icon={model.icon()} />
|
||||
<span className="username">{groupName}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
matches(model: Group, typed: string): boolean {
|
||||
if (!typed) return false;
|
||||
|
||||
const names = [model.namePlural().toLowerCase(), model.nameSingular().toLowerCase()];
|
||||
|
||||
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
|
||||
}
|
||||
|
||||
maxStoreMatchedResults(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* All groups are already loaded, so we don't need to search for them.
|
||||
*/
|
||||
search(typed: string): Promise<Group[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
enabled(): boolean {
|
||||
return app.session?.user?.canMentionGroups() ?? false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
import type Mithril from 'mithril';
|
||||
import type Model from 'flarum/common/Model';
|
||||
import type MentionFormat from './formats/MentionFormat';
|
||||
|
||||
export default abstract class MentionableModel<M extends Model = Model, Format extends MentionFormat = MentionFormat> {
|
||||
public format: Format;
|
||||
|
||||
public constructor(format: Format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
abstract type(): string;
|
||||
abstract initialResults(): M[];
|
||||
abstract search(typed: string): Promise<M[]>;
|
||||
abstract replacement(model: M): string;
|
||||
abstract suggestion(model: M, typed: string): Mithril.Children;
|
||||
abstract matches(model: M, typed: string): boolean;
|
||||
abstract maxStoreMatchedResults(): number | null;
|
||||
abstract enabled(): boolean;
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
import type MentionableModel from './MentionableModel';
|
||||
import type Model from 'flarum/common/Model';
|
||||
import type Mithril from 'mithril';
|
||||
import MentionsDropdownItem from '../components/MentionsDropdownItem';
|
||||
import { throttle } from 'flarum/common/utils/throttleDebounce';
|
||||
|
||||
export default class MentionableModels {
|
||||
protected mentionables?: MentionableModel[];
|
||||
/**
|
||||
* We store models returned from an API here to preserve order in which they are returned
|
||||
* This prevents the list jumping around while models are returned.
|
||||
* We also use a hashmap for model IDs to provide O(1) lookup for the users already in the list.
|
||||
*/
|
||||
private results: Record<string, Map<string, Model>> = {};
|
||||
public typed: string | null = null;
|
||||
private searched: string[] = [];
|
||||
private dropdownItemAttrs: Record<string, any> = {};
|
||||
|
||||
constructor(dropdownItemAttrs: Record<string, any>) {
|
||||
this.dropdownItemAttrs = dropdownItemAttrs;
|
||||
}
|
||||
|
||||
public init(mentionables: MentionableModel[]): void {
|
||||
this.typed = null;
|
||||
this.mentionables = mentionables;
|
||||
|
||||
for (const mentionable of this.mentionables) {
|
||||
this.results[mentionable.type()] = new Map(mentionable.initialResults().map((result) => [result.id() as string, result]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't send API calls searching for models until at least 2 characters have been typed.
|
||||
* This focuses the mention results on models already loaded.
|
||||
*/
|
||||
public readonly search = throttle(250, async (): Promise<void> => {
|
||||
if (!this.typed || this.typed.length <= 1) return;
|
||||
|
||||
const typedLower = this.typed.toLowerCase();
|
||||
|
||||
if (this.searched.includes(typedLower)) return;
|
||||
|
||||
for (const mentionable of this.mentionables!) {
|
||||
for (const model of await mentionable.search(typedLower)) {
|
||||
if (!this.results[mentionable.type()].has(model.id() as string)) {
|
||||
this.results[mentionable.type()].set(model.id() as string, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.searched.push(typedLower);
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
public matches(mentionable: MentionableModel, model: Model): boolean {
|
||||
return mentionable.matches(model, this.typed?.toLowerCase() || '');
|
||||
}
|
||||
|
||||
public makeSuggestion(mentionable: MentionableModel, model: Model): Mithril.Children {
|
||||
const content = mentionable.suggestion(model, this.typed!);
|
||||
const replacement = mentionable.replacement(model);
|
||||
|
||||
const { onclick, ...attrs } = this.dropdownItemAttrs;
|
||||
|
||||
return (
|
||||
<MentionsDropdownItem mentionable={mentionable} onclick={() => onclick(replacement)} {...attrs}>
|
||||
{content}
|
||||
</MentionsDropdownItem>
|
||||
);
|
||||
}
|
||||
|
||||
public buildSuggestions(): Mithril.Children {
|
||||
const suggestions: Mithril.Children = [];
|
||||
|
||||
for (const mentionable of this.mentionables!) {
|
||||
if (!mentionable.enabled()) continue;
|
||||
|
||||
let matches = Array.from(this.results[mentionable.type()].values()).filter((model) => this.matches(mentionable, model));
|
||||
|
||||
const max = mentionable.maxStoreMatchedResults();
|
||||
if (max) matches = matches.splice(0, max);
|
||||
|
||||
for (const model of matches) {
|
||||
const dropdownItem = this.makeSuggestion(mentionable, model);
|
||||
suggestions.push(dropdownItem);
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
102
extensions/mentions/js/src/forum/mentionables/PostMention.tsx
Normal file
102
extensions/mentions/js/src/forum/mentionables/PostMention.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import MentionableModel from './MentionableModel';
|
||||
import type Post from 'flarum/common/models/Post';
|
||||
import type Mithril from 'mithril';
|
||||
import usernameHelper from 'flarum/common/helpers/username';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import highlight from 'flarum/common/helpers/highlight';
|
||||
import { truncate } from 'flarum/common/utils/string';
|
||||
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
|
||||
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
|
||||
import getCleanDisplayName from '../utils/getCleanDisplayName';
|
||||
import type AtMentionFormat from './formats/AtMentionFormat';
|
||||
|
||||
export default class PostMention extends MentionableModel<Post, AtMentionFormat> {
|
||||
type(): string {
|
||||
return 'post';
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is replying to a discussion, or if they are editing a
|
||||
* post, then we can suggest other posts in the discussion to mention.
|
||||
* We will add the 5 most recent comments in the discussion which
|
||||
* match any username characters that have been typed.
|
||||
*/
|
||||
initialResults(): Post[] {
|
||||
if (!app.composer.bodyMatches(ReplyComposer) && !app.composer.bodyMatches(EditPostComposer)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const composerAttrs = app.composer.body.attrs;
|
||||
const composerPost = composerAttrs.post;
|
||||
const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;
|
||||
|
||||
return (
|
||||
discussion
|
||||
.posts()
|
||||
// Filter to only comment posts, and replies before this message
|
||||
.filter((post: Post) => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
|
||||
// Sort by new to old
|
||||
.sort((a: Post, b: Post) => b.createdAt().getTime() - a.createdAt().getTime())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the syntax for mentioning of a post. Also cleans up the display name.
|
||||
*
|
||||
* @example <caption>Post mention</caption>
|
||||
* // '@"User"#p13'
|
||||
* // @"Display name"#pPostID
|
||||
* forPostMention(user, 13) // User display name is 'User', post ID is 13
|
||||
*/
|
||||
public replacement(post: Post): string {
|
||||
const user = post.user();
|
||||
const cleanText = getCleanDisplayName(user);
|
||||
return this.format.format(cleanText, 'p', post.id());
|
||||
}
|
||||
|
||||
suggestion(model: Post, typed: string): Mithril.Children {
|
||||
const user = model.user() || null;
|
||||
const username = usernameHelper(user);
|
||||
|
||||
if (typed) {
|
||||
username.children = [highlight((username.text ?? '') as string, typed)];
|
||||
delete username.text;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{avatar(user)}
|
||||
{username}
|
||||
{[
|
||||
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: model.number() }),
|
||||
' — ',
|
||||
truncate(model.contentPlain() ?? '', 200),
|
||||
]}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
matches(model: Post, typed: string): boolean {
|
||||
const user = model.user();
|
||||
const userMentionable = app.mentionFormats.mentionable('user')!;
|
||||
|
||||
return !typed || (user && userMentionable.matches(user, typed));
|
||||
}
|
||||
|
||||
maxStoreMatchedResults(): number {
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post mention suggestions are only offered from current discussion posts.
|
||||
*/
|
||||
search(typed: string): Promise<Post[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
enabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
65
extensions/mentions/js/src/forum/mentionables/TagMention.tsx
Normal file
65
extensions/mentions/js/src/forum/mentionables/TagMention.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import Badge from 'flarum/common/components/Badge';
|
||||
import highlight from 'flarum/common/helpers/highlight';
|
||||
import type Tag from 'flarum/tags/common/models/Tag';
|
||||
import type Mithril from 'mithril';
|
||||
import MentionableModel from './MentionableModel';
|
||||
import type HashMentionFormat from './formats/HashMentionFormat';
|
||||
|
||||
export default class TagMention extends MentionableModel<Tag, HashMentionFormat> {
|
||||
type(): string {
|
||||
return 'tag';
|
||||
}
|
||||
|
||||
initialResults(): Tag[] {
|
||||
return Array.from(app.store.all<Tag>('tags'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the mention syntax for a tag mention.
|
||||
*
|
||||
* ~tagSlug
|
||||
*
|
||||
* @example <caption>Tag mention</caption>
|
||||
* // ~general
|
||||
* forTag(tag) // Tag display name is 'Tag', tag ID is 5
|
||||
*/
|
||||
public replacement(tag: Tag): string {
|
||||
return this.format.format(tag.slug());
|
||||
}
|
||||
|
||||
matches(model: Tag, typed: string): boolean {
|
||||
if (!typed) return false;
|
||||
|
||||
const names = [model.name().toLowerCase()];
|
||||
|
||||
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
|
||||
}
|
||||
|
||||
maxStoreMatchedResults(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async search(typed: string): Promise<Tag[]> {
|
||||
return await app.store.find<Tag[]>('tags', { filter: { q: typed }, page: { limit: 5 } });
|
||||
}
|
||||
|
||||
suggestion(model: Tag, typed: string): Mithril.Children {
|
||||
let tagName: Mithril.Children = model.name();
|
||||
|
||||
if (typed) {
|
||||
tagName = highlight(tagName, typed);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Badge className="Avatar" icon={model.icon()} color={model.color()} type="tag" />
|
||||
<span className="username">{tagName}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
enabled(): boolean {
|
||||
return 'flarum-tags' in flarum.extensions;
|
||||
}
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import type Mithril from 'mithril';
|
||||
import type User from 'flarum/common/models/User';
|
||||
import usernameHelper from 'flarum/common/helpers/username';
|
||||
import avatar from 'flarum/common/helpers/avatar';
|
||||
import highlight from 'flarum/common/helpers/highlight';
|
||||
import MentionableModel from './MentionableModel';
|
||||
import getCleanDisplayName, { shouldUseOldFormat } from '../utils/getCleanDisplayName';
|
||||
import AtMentionFormat from './formats/AtMentionFormat';
|
||||
|
||||
export default class UserMention extends MentionableModel<User, AtMentionFormat> {
|
||||
type(): string {
|
||||
return 'user';
|
||||
}
|
||||
|
||||
initialResults(): User[] {
|
||||
return Array.from(app.store.all<User>('users'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically determines which mention syntax to be used based on the option in the
|
||||
* admin dashboard. Also performs display name clean-up automatically.
|
||||
*
|
||||
* @"Display name"#UserID or `@username`
|
||||
*
|
||||
* @example <caption>New display name syntax</caption>
|
||||
* // '@"user"#1'
|
||||
* forUser(User) // User is ID 1, display name is 'User'
|
||||
*
|
||||
* @example <caption>Using old syntax</caption>
|
||||
* // '@username'
|
||||
* forUser(user) // User's username is 'username'
|
||||
*/
|
||||
public replacement(user: User): string {
|
||||
if (shouldUseOldFormat()) {
|
||||
const cleanText = getCleanDisplayName(user, false);
|
||||
return this.format.format(cleanText);
|
||||
}
|
||||
|
||||
const cleanText = getCleanDisplayName(user);
|
||||
return this.format.format(cleanText, '', user.id());
|
||||
}
|
||||
|
||||
suggestion(model: User, typed: string): Mithril.Children {
|
||||
const username = usernameHelper(model);
|
||||
|
||||
if (typed) {
|
||||
username.children = [highlight((username.text ?? '') as string, typed)];
|
||||
delete username.text;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{avatar(model)}
|
||||
{username}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
matches(model: User, typed: string): boolean {
|
||||
if (!typed) return false;
|
||||
|
||||
const names = [model.username(), model.displayName()];
|
||||
|
||||
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
|
||||
}
|
||||
|
||||
maxStoreMatchedResults(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async search(typed: string): Promise<User[]> {
|
||||
return await app.store.find<User[]>('users', { filter: { q: typed }, page: { limit: 5 } });
|
||||
}
|
||||
|
||||
enabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import MentionFormat from './MentionFormat';
|
||||
import type MentionableModel from '../MentionableModel';
|
||||
import UserMention from '../UserMention';
|
||||
import PostMention from '../PostMention';
|
||||
import GroupMention from '../GroupMention';
|
||||
|
||||
export default class AtMentionFormat extends MentionFormat {
|
||||
public mentionables: (new (...args: any[]) => MentionableModel)[] = [UserMention, PostMention, GroupMention];
|
||||
protected extendable: boolean = true;
|
||||
|
||||
public trigger(): string {
|
||||
return '@';
|
||||
}
|
||||
|
||||
public queryFromTyped(typed: string): string | null {
|
||||
const matchTyped = typed.match(/^["“]?((?:(?!"#).)+)$/);
|
||||
|
||||
return matchTyped ? matchTyped[1] : null;
|
||||
}
|
||||
|
||||
public format(name: string, char: string | null = '', id: string | null = null): string {
|
||||
return {
|
||||
simple: `@${name}`,
|
||||
safe: `@"${name}"#${char}${id}`,
|
||||
}[id ? 'safe' : 'simple'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import MentionFormat from './MentionFormat';
|
||||
import MentionableModel from '../MentionableModel';
|
||||
import TagMention from '../TagMention';
|
||||
|
||||
export default class HashMentionFormat extends MentionFormat {
|
||||
public mentionables: (new (...args: any[]) => MentionableModel)[] = [TagMention];
|
||||
protected extendable: boolean = false;
|
||||
|
||||
public trigger(): string {
|
||||
return '#';
|
||||
}
|
||||
|
||||
public queryFromTyped(typed: string): string | null {
|
||||
const matchTyped = typed.match(/^[-_\p{L}\p{N}\p{M}]+$/giu);
|
||||
|
||||
return matchTyped ? matchTyped[0] : null;
|
||||
}
|
||||
|
||||
public format(slug: string): string {
|
||||
return `#${slug}`;
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import type MentionableModel from '../MentionableModel';
|
||||
import type Model from 'flarum/common/Model';
|
||||
|
||||
export default abstract class MentionFormat {
|
||||
protected instances?: MentionableModel[];
|
||||
|
||||
public makeMentionables(): MentionableModel[] {
|
||||
return this.instances ?? (this.instances = this.mentionables.map((Mentionable) => new Mentionable(this)));
|
||||
}
|
||||
|
||||
public getMentionable(type: string): MentionableModel | null {
|
||||
return this.makeMentionables().find((mentionable) => mentionable.type() === type) ?? null;
|
||||
}
|
||||
|
||||
public extend(mentionable: new (...args: any[]) => MentionableModel): void {
|
||||
if (!this.extendable) throw new Error('This mention format does not allow extending.');
|
||||
|
||||
this.mentionables.push(mentionable);
|
||||
}
|
||||
|
||||
abstract mentionables: (new (...args: any[]) => MentionableModel)[];
|
||||
|
||||
protected abstract extendable: boolean;
|
||||
|
||||
abstract trigger(): string;
|
||||
|
||||
/**
|
||||
* Picks the term to search in the API from the typed text.
|
||||
* @example:
|
||||
* * Full text = `Hello @"John D`
|
||||
* * Typed text = `"John D`
|
||||
* * Query = `John D`
|
||||
*/
|
||||
abstract queryFromTyped(typed: string): string | null;
|
||||
|
||||
abstract format(...args: any): string;
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import AtMentionFormat from './AtMentionFormat';
|
||||
import HashMentionFormat from './HashMentionFormat';
|
||||
import type MentionFormat from './MentionFormat';
|
||||
import MentionableModel from '../MentionableModel';
|
||||
|
||||
export default class MentionFormats {
|
||||
protected formats: MentionFormat[] = [new AtMentionFormat(), new HashMentionFormat()];
|
||||
|
||||
public get(symbol: string): MentionFormat | null {
|
||||
return this.formats.find((f) => f.trigger() === symbol) ?? null;
|
||||
}
|
||||
|
||||
public mentionable(type: string): MentionableModel | null {
|
||||
for (const format of this.formats) {
|
||||
const mentionable = format.getMentionable(type);
|
||||
|
||||
if (mentionable) return mentionable;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public extend(format: new () => MentionFormat) {
|
||||
this.formats.push(new format());
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import PaginatedListState, { PaginatedListParams } from 'flarum/common/states/PaginatedListState';
|
||||
import Post from 'flarum/common/models/Post';
|
||||
|
||||
export interface MentionedByModalListParams extends PaginatedListParams {
|
||||
filter: {
|
||||
mentionedPost: string;
|
||||
};
|
||||
sort?: string;
|
||||
page?: {
|
||||
offset?: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default class MentionedByModalState<P extends MentionedByModalListParams = MentionedByModalListParams> extends PaginatedListState<Post, P> {
|
||||
constructor(params: P, page: number = 1) {
|
||||
const limit = 10;
|
||||
|
||||
params.page = { ...(params.page || {}), limit };
|
||||
|
||||
super(params, page, limit);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'posts';
|
||||
}
|
||||
}
|
@@ -1,45 +1,21 @@
|
||||
import getCleanDisplayName, { shouldUseOldFormat } from './getCleanDisplayName';
|
||||
import app from 'flarum/forum/app';
|
||||
|
||||
/**
|
||||
* Fetches the mention text for a specified user (and optionally a post ID for replies, or group).
|
||||
* Fetches the mention text for a specified user (and optionally a post ID for replies or group).
|
||||
*
|
||||
* Automatically determines which mention syntax to be used based on the option in the
|
||||
* admin dashboard. Also performs display name clean-up automatically.
|
||||
*
|
||||
* @example <caption>New display name syntax</caption>
|
||||
* // '@"User"#1'
|
||||
* getMentionText(User) // User is ID 1, display name is 'User'
|
||||
*
|
||||
* @example <caption>Replying</caption>
|
||||
* // '@"User"#p13'
|
||||
* getMentionText(User, 13) // User display name is 'User', post ID is 13
|
||||
*
|
||||
* @example <caption>Using old syntax</caption>
|
||||
* // '@username'
|
||||
* getMentionText(User) // User's username is 'username'
|
||||
*
|
||||
* @example <caption>Group mention</caption>
|
||||
* // '@"Mods"#g4'
|
||||
* getMentionText(undefined, undefined, group) // Group display name is 'Mods', group ID is 4
|
||||
* @deprecated Use `app.mentionables.get('user').replacement(user)` instead. Will be removed in 2.0.
|
||||
*/
|
||||
export default function getMentionText(user, postId, group) {
|
||||
if (user !== undefined && postId === undefined) {
|
||||
if (shouldUseOldFormat()) {
|
||||
// Plain @username
|
||||
const cleanText = getCleanDisplayName(user, false);
|
||||
return `@${cleanText}`;
|
||||
}
|
||||
// @"Display name"#UserID
|
||||
const cleanText = getCleanDisplayName(user);
|
||||
return `@"${cleanText}"#${user.id()}`;
|
||||
return app.mentionables.get('user').replacement(user);
|
||||
} else if (user !== undefined && postId !== undefined) {
|
||||
// @"Display name"#pPostID
|
||||
const cleanText = getCleanDisplayName(user);
|
||||
return `@"${cleanText}"#p${postId}`;
|
||||
return app.mentionables.get('post').replacement(app.store.getById('posts', postId));
|
||||
} else if (group !== undefined) {
|
||||
// @"Name Plural"#gGroupID
|
||||
return `@"${group.namePlural()}"#g${group.id()}`;
|
||||
} else {
|
||||
throw 'No parameters were passed';
|
||||
return app.mentionables.get('group').replacement(group);
|
||||
}
|
||||
|
||||
throw 'No parameters were passed';
|
||||
}
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import DiscussionControls from 'flarum/forum/utils/DiscussionControls';
|
||||
import EditPostComposer from 'flarum/forum/components/EditPostComposer';
|
||||
import getMentionText from './getMentionText';
|
||||
|
||||
export function insertMention(post, composer, quote) {
|
||||
return new Promise((resolve) => {
|
||||
const user = post.user();
|
||||
const mention = getMentionText(user, post.id()) + ' ';
|
||||
const mention = app.mentionFormats.mentionable('post').replacement(post) + ' ';
|
||||
|
||||
// If the composer is empty, then assume we're starting a new reply.
|
||||
// In which case we don't want the user to have to confirm if they
|
||||
|
@@ -20,6 +20,10 @@ export function filterUserMentions(tag) {
|
||||
tag.invalidate();
|
||||
}
|
||||
|
||||
export function postFilterUserMentions(tag) {
|
||||
tag.setAttribute('deleted', false);
|
||||
}
|
||||
|
||||
export function filterPostMentions(tag) {
|
||||
const post = app.store.getById('posts', tag.getAttribute('id'));
|
||||
|
||||
@@ -32,14 +36,16 @@ export function filterPostMentions(tag) {
|
||||
}
|
||||
}
|
||||
|
||||
export function postFilterPostMentions(tag) {
|
||||
tag.setAttribute('deleted', false);
|
||||
}
|
||||
|
||||
export function filterGroupMentions(tag) {
|
||||
if (app.session?.user?.canMentionGroups()) {
|
||||
const group = app.store.getById('groups', tag.getAttribute('id'));
|
||||
|
||||
if (group) {
|
||||
tag.setAttribute('groupname', extractText(group.namePlural()));
|
||||
tag.setAttribute('icon', group.icon());
|
||||
tag.setAttribute('color', group.color());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -47,3 +53,38 @@ export function filterGroupMentions(tag) {
|
||||
|
||||
tag.invalidate();
|
||||
}
|
||||
|
||||
export function postFilterGroupMentions(tag) {
|
||||
if (app.session?.user?.canMentionGroups()) {
|
||||
const group = app.store.getById('groups', tag.getAttribute('id'));
|
||||
|
||||
tag.setAttribute('color', group.color());
|
||||
tag.setAttribute('icon', group.icon());
|
||||
tag.setAttribute('deleted', false);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterTagMentions(tag) {
|
||||
if ('flarum-tags' in flarum.extensions) {
|
||||
const model = app.store.getBy('tags', 'slug', tag.getAttribute('slug'));
|
||||
|
||||
if (model) {
|
||||
tag.setAttribute('id', model.id());
|
||||
tag.setAttribute('tagname', model.name());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
tag.invalidate();
|
||||
}
|
||||
|
||||
export function postFilterTagMentions(tag) {
|
||||
if ('flarum-tags' in flarum.extensions) {
|
||||
const model = app.store.getBy('tags', 'slug', tag.getAttribute('slug'));
|
||||
|
||||
tag.setAttribute('icon', model.icon());
|
||||
tag.setAttribute('color', model.color());
|
||||
tag.setAttribute('deleted', false);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@
|
||||
"declarationDir": "./dist-typings",
|
||||
"paths": {
|
||||
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
|
||||
"flarum/tags/*": ["../../tags/js/dist-typings/*"],
|
||||
// TODO: remove after export registry system implemented
|
||||
// Without this, the old-style `@flarum/core` import is resolved to
|
||||
// source code in flarum/core instead of the dist typings.
|
||||
|
@@ -2,8 +2,6 @@
|
||||
background: var(--control-bg);
|
||||
color: var(--control-color);
|
||||
border-radius: @border-radius;
|
||||
padding: 2px 5px;
|
||||
border: 0 !important;
|
||||
font-weight: 600;
|
||||
|
||||
blockquote & {
|
||||
@@ -14,7 +12,12 @@
|
||||
color: var(--link-color);
|
||||
}
|
||||
}
|
||||
.UserMention, .PostMention, .GroupMention {
|
||||
.UserMention, .PostMention, .GroupMention, .TagMention {
|
||||
padding: 2px 5px;
|
||||
vertical-align: middle;
|
||||
border: 0 !important;
|
||||
white-space: nowrap;
|
||||
|
||||
&--deleted {
|
||||
opacity: 0.8;
|
||||
filter: grayscale(1);
|
||||
@@ -27,12 +30,38 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
// @TODO: 2.0 use an icon in the XSLT template.
|
||||
&:before {
|
||||
.fas();
|
||||
content: @fa-var-reply;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.GroupMention {
|
||||
background-color: var(--color, var(--control-bg));
|
||||
color: var(--control-color);
|
||||
--link-color: currentColor;
|
||||
|
||||
&--colored {
|
||||
--control-color: var(--contrast-color, var(--body-bg));
|
||||
--link-color: var(--control-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
& when (is-extension-enabled('flarum-tags')) {
|
||||
.TagMention {
|
||||
--tag-bg: var(--color, var(--control-bg));
|
||||
.tag-label();
|
||||
margin: 0 2px;
|
||||
|
||||
.icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ComposerBody-mentionsWrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -50,6 +79,7 @@
|
||||
}
|
||||
}
|
||||
.MentionsDropdown, .PostMention-preview, .Post-mentionedBy-preview {
|
||||
// @TODO: Rename to .MentionsDropdownItem, along with child classes. 2.0
|
||||
.PostPreview {
|
||||
color: @muted-color;
|
||||
|
||||
@@ -97,24 +127,9 @@
|
||||
position: absolute;
|
||||
.Button--color(@tooltip-color, @tooltip-bg);
|
||||
}
|
||||
.GroupMention {
|
||||
background-color: var(--group-color, var(--control-bg));
|
||||
color: var(--control-color);
|
||||
--link-color: currentColor;
|
||||
|
||||
&--colored {
|
||||
--control-color: var(--contrast-color, var(--body-bg));
|
||||
--link-color: var(--control-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
.MentionsDropdown .Badge {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media @phone {
|
||||
.MentionsDropdown {
|
||||
max-width: 100%;
|
||||
|
@@ -25,6 +25,11 @@ flarum-mentions:
|
||||
mention_tooltip: Mention a user, group or post
|
||||
reply_to_post_text: "Reply to #{number}"
|
||||
|
||||
# These translations are used by the mentioned by modal dialog.
|
||||
mentioned_by:
|
||||
title: Replies to this post
|
||||
load_more_button: => core.ref.load_more
|
||||
|
||||
# These translations are used by the Notifications dropdown, a.k.a. "the bell".
|
||||
notifications:
|
||||
others_text: => core.ref.some_others
|
||||
@@ -34,6 +39,7 @@ flarum-mentions:
|
||||
|
||||
# These translations are displayed beneath individual posts.
|
||||
post:
|
||||
mentioned_by_more_text: "{count} more replies."
|
||||
mentioned_by_self_text: "{users} replied to this." # Can be pluralized to agree with the number of users!
|
||||
mentioned_by_text: "{users} replied to this." # Can be pluralized to agree with the number of users!
|
||||
others_text: => core.ref.some_others
|
||||
|
68
extensions/mentions/src/Api/LoadMentionedByRelationship.php
Normal file
68
extensions/mentions/src/Api/LoadMentionedByRelationship.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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\Mentions\Api;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Apply visibility permissions to API data's mentionedBy relationship.
|
||||
* And limit mentionedBy to 3 posts only for performance reasons.
|
||||
*/
|
||||
class LoadMentionedByRelationship
|
||||
{
|
||||
public static $maxMentionedBy = 4;
|
||||
|
||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request)
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
return $query
|
||||
->with(['mentionsPosts', 'mentionsPosts.user', 'mentionsUsers'])
|
||||
->whereVisibleTo($actor)
|
||||
->oldest()
|
||||
// Limiting a relationship results is only possible because
|
||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
||||
// trait.
|
||||
->limit(self::$maxMentionedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
||||
*/
|
||||
public static function countRelation($controller, $data, ServerRequestInterface $request): void
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
$loadable = null;
|
||||
|
||||
if ($data instanceof Discussion) {
|
||||
// @phpstan-ignore-next-line
|
||||
$loadable = $data->newCollection((array) $data->posts)->filter(function ($post) {
|
||||
return $post instanceof Post;
|
||||
});
|
||||
} elseif ($data instanceof Collection) {
|
||||
$loadable = $data;
|
||||
} elseif ($data instanceof Post) {
|
||||
$loadable = $data->newCollection([$data]);
|
||||
}
|
||||
|
||||
if ($loadable) {
|
||||
$loadable->loadCount([
|
||||
'mentionedBy' => function ($query) use ($actor) {
|
||||
return $query->whereVisibleTo($actor);
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,27 +9,41 @@
|
||||
|
||||
namespace Flarum\Mentions;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Post\PostRepository;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use s9e\TextFormatter\Configurator;
|
||||
use s9e\TextFormatter\Parser\Tag;
|
||||
use s9e\TextFormatter\Parser\Tag as FormatterTag;
|
||||
|
||||
/**
|
||||
* @TODO: refactor this lump of code into a mentionable models polymorphic system (for v2.0).
|
||||
*/
|
||||
class ConfigureMentions
|
||||
{
|
||||
public const USER_MENTION_WITH_DISPLAY_NAME_REGEX = '/\B@["“](?<displayname>((?!"#[a-z]{0,3}[0-9]+).)+)["”]#(?<id>[0-9]+)\b/';
|
||||
public const USER_MENTION_WITH_USERNAME_REGEX = '/\B@(?<username>[a-z0-9_-]+)(?!#)/i';
|
||||
public const POST_MENTION_WITH_DISPLAY_NAME_REGEX = '/\B@["“](?<displayname>((?!"#[a-z]{0,3}[0-9]+).)+)["”]#p(?<id>[0-9]+)\b/';
|
||||
public const GROUP_MENTION_WITH_NAME_REGEX = '/\B@["“](?<groupname>((?!"#[a-z]{0,3}[0-9]+).)+)["|”]#g(?<id>[0-9]+)\b/';
|
||||
public const TAG_MENTION_WITH_SLUG_REGEX = '/(?:[^“"]|^)\B#(?<slug>[-_\p{L}\p{N}\p{M}]+)\b/ui';
|
||||
|
||||
/**
|
||||
* @var UrlGenerator
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @param UrlGenerator $url
|
||||
* @var ExtensionManager
|
||||
*/
|
||||
public function __construct(UrlGenerator $url)
|
||||
protected $extensions;
|
||||
|
||||
public function __construct(UrlGenerator $url, ExtensionManager $extensions)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->extensions = $extensions;
|
||||
}
|
||||
|
||||
public function __invoke(Configurator $config)
|
||||
@@ -37,6 +51,10 @@ class ConfigureMentions
|
||||
$this->configureUserMentions($config);
|
||||
$this->configurePostMentions($config);
|
||||
$this->configureGroupMentions($config);
|
||||
|
||||
if ($this->extensions->isEnabled('flarum-tags')) {
|
||||
$this->configureTagMentions($config);
|
||||
}
|
||||
}
|
||||
|
||||
private function configureUserMentions(Configurator $config): void
|
||||
@@ -58,25 +76,31 @@ class ConfigureMentions
|
||||
<span class="UserMention UserMention--deleted">@<xsl:value-of select="@displayname"/></span>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>';
|
||||
$tag->filterChain->prepend([static::class, 'addUserId'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterUserMentions(tag); }');
|
||||
|
||||
$config->Preg->match('/\B@["|“](?<displayname>((?!"#[a-z]{0,3}[0-9]+).)+)["|”]#(?<id>[0-9]+)\b/', $tagName);
|
||||
$config->Preg->match('/\B@(?<username>[a-z0-9_-]+)(?!#)/i', $tagName);
|
||||
$tag->filterChain->prepend([static::class, 'addUserId'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterUserMentions(tag); }')
|
||||
->addParameterByName('mentions');
|
||||
|
||||
$tag->filterChain->append([static::class, 'dummyFilter'])
|
||||
->setJs('function(tag) { return flarum.extensions["flarum-mentions"].postFilterUserMentions(tag); }');
|
||||
|
||||
$config->Preg->match(self::USER_MENTION_WITH_DISPLAY_NAME_REGEX, $tagName);
|
||||
$config->Preg->match(self::USER_MENTION_WITH_USERNAME_REGEX, $tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @param FormatterTag $tag
|
||||
* @param array<string, Collection> $mentions
|
||||
* @return bool|void
|
||||
*/
|
||||
public static function addUserId($tag)
|
||||
public static function addUserId($tag, array $mentions)
|
||||
{
|
||||
$allow_username_format = (bool) resolve(SettingsRepositoryInterface::class)->get('flarum-mentions.allow_username_format');
|
||||
|
||||
if ($tag->hasAttribute('username') && $allow_username_format) {
|
||||
$user = User::where('username', $tag->getAttribute('username'))->first();
|
||||
$user = $mentions['users']->where('username', $tag->getAttribute('username'))->first();
|
||||
} elseif ($tag->hasAttribute('id')) {
|
||||
$user = User::find($tag->getAttribute('id'));
|
||||
$user = $mentions['users']->where('id', $tag->getAttribute('id'))->first();
|
||||
}
|
||||
|
||||
if (isset($user)) {
|
||||
@@ -115,20 +139,22 @@ class ConfigureMentions
|
||||
$tag->filterChain
|
||||
->prepend([static::class, 'addPostId'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterPostMentions(tag); }')
|
||||
->addParameterByName('actor');
|
||||
->addParameterByName('mentions');
|
||||
|
||||
$config->Preg->match('/\B@["|“](?<displayname>((?!"#[a-z]{0,3}[0-9]+).)+)["|”]#p(?<id>[0-9]+)\b/', $tagName);
|
||||
$tag->filterChain->append([static::class, 'dummyFilter'])
|
||||
->setJs('function(tag) { return flarum.extensions["flarum-mentions"].postFilterPostMentions(tag); }');
|
||||
|
||||
$config->Preg->match(self::POST_MENTION_WITH_DISPLAY_NAME_REGEX, $tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @param FormatterTag $tag
|
||||
* @param array<string, Collection> $mentions
|
||||
* @return bool|void
|
||||
*/
|
||||
public static function addPostId($tag, User $actor)
|
||||
public static function addPostId($tag, array $mentions)
|
||||
{
|
||||
$post = resolve(PostRepository::class)
|
||||
->queryVisibleTo($actor)
|
||||
->find($tag->getAttribute('id'));
|
||||
$post = $mentions['posts']->where('id', $tag->getAttribute('id'))->first();
|
||||
|
||||
if ($post) {
|
||||
$tag->setAttribute('discussionid', (string) $post->discussion_id);
|
||||
@@ -148,8 +174,6 @@ class ConfigureMentions
|
||||
|
||||
$tag = $config->tags->add($tagName);
|
||||
$tag->attributes->add('groupname');
|
||||
$tag->attributes->add('icon');
|
||||
$tag->attributes->add('color');
|
||||
$tag->attributes->add('id')->filterChain->append('#uint');
|
||||
|
||||
$tag->template = '
|
||||
@@ -157,7 +181,7 @@ class ConfigureMentions
|
||||
<xsl:when test="@deleted != 1">
|
||||
<xsl:choose>
|
||||
<xsl:when test="string(@color) != \'\'">
|
||||
<span class="GroupMention GroupMention--colored" style="--group-color:{@color};">
|
||||
<span class="GroupMention GroupMention--colored" style="--color:{@color};">
|
||||
<span class="GroupMention-name">@<xsl:value-of select="@groupname"/></span>
|
||||
<xsl:if test="string(@icon) != \'\'">
|
||||
<i class="icon {@icon}"></i>
|
||||
@@ -183,29 +207,132 @@ class ConfigureMentions
|
||||
</span>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>';
|
||||
$tag->filterChain->prepend([static::class, 'addGroupId'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterGroupMentions(tag); }');
|
||||
|
||||
$config->Preg->match('/\B@["|“](?<groupname>((?!"#[a-z]{0,3}[0-9]+).)+)["|”]#g(?<id>[0-9]+)\b/', $tagName);
|
||||
$tag->filterChain->prepend([static::class, 'addGroupId'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterGroupMentions(tag); }')
|
||||
->addParameterByName('actor')
|
||||
->addParameterByName('mentions');
|
||||
|
||||
$tag->filterChain->append([static::class, 'dummyFilter'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].postFilterGroupMentions(tag); }');
|
||||
|
||||
$config->Preg->match(self::GROUP_MENTION_WITH_NAME_REGEX, $tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tag
|
||||
* @param FormatterTag $tag
|
||||
* @param User $actor
|
||||
* @param array<string, Collection> $mentions
|
||||
* @return bool|void
|
||||
*/
|
||||
public static function addGroupId($tag)
|
||||
public static function addGroupId(FormatterTag $tag, User $actor, array $mentions)
|
||||
{
|
||||
$group = Group::find($tag->getAttribute('id'));
|
||||
$id = $tag->getAttribute('id');
|
||||
|
||||
if (isset($group) && ! in_array($group->id, [Group::GUEST_ID, Group::MEMBER_ID])) {
|
||||
if ($actor->cannot('mentionGroups') || in_array($id, [Group::GUEST_ID, Group::MEMBER_ID])) {
|
||||
$tag->invalidate();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$group = $mentions['groups']->where('id', $id)->first();
|
||||
|
||||
if ($group) {
|
||||
$tag->setAttribute('id', $group->id);
|
||||
$tag->setAttribute('groupname', $group->name_plural);
|
||||
$tag->setAttribute('icon', $group->icon ?? 'fas fa-at');
|
||||
$tag->setAttribute('color', $group->color);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$tag->invalidate();
|
||||
}
|
||||
|
||||
private function configureTagMentions(Configurator $config)
|
||||
{
|
||||
$config->rendering->parameters['TAG_URL'] = $this->url->to('forum')->route('tag', ['slug' => '']);
|
||||
|
||||
$tagName = 'TAGMENTION';
|
||||
|
||||
$tag = $config->tags->add($tagName);
|
||||
$tag->attributes->add('tagname');
|
||||
$tag->attributes->add('slug');
|
||||
$tag->attributes->add('id')->filterChain->append('#uint');
|
||||
|
||||
$tag->template = '
|
||||
<xsl:choose>
|
||||
<xsl:when test="@deleted != 1">
|
||||
<a href="{$TAG_URL}{@slug}" data-id="{@id}">
|
||||
<xsl:attribute name="class">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@color != \'\'">
|
||||
<xsl:text>TagMention TagMention--colored</xsl:text>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<xsl:text>TagMention</xsl:text>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:attribute>
|
||||
<xsl:attribute name="style">
|
||||
<xsl:choose>
|
||||
<xsl:when test="@color != \'\'">
|
||||
<xsl:text>--color:</xsl:text>
|
||||
<xsl:value-of select="@color"/>
|
||||
</xsl:when>
|
||||
</xsl:choose>
|
||||
</xsl:attribute>
|
||||
<span class="TagMention-text">
|
||||
<xsl:if test="@icon != \'\'">
|
||||
<i class="icon {@icon}"></i>
|
||||
</xsl:if>
|
||||
<xsl:value-of select="@tagname"/>
|
||||
</span>
|
||||
</a>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<span class="TagMention TagMention--deleted" data-id="{@id}">
|
||||
<span class="TagMention-text">
|
||||
<xsl:value-of select="@tagname"/>
|
||||
</span>
|
||||
</span>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>';
|
||||
|
||||
$tag->filterChain
|
||||
->prepend([static::class, 'addTagId'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].filterTagMentions(tag); }')
|
||||
->addParameterByName('mentions');
|
||||
|
||||
$tag->filterChain
|
||||
->append([static::class, 'dummyFilter'])
|
||||
->setJS('function(tag) { return flarum.extensions["flarum-mentions"].postFilterTagMentions(tag); }');
|
||||
|
||||
$config->Preg->match(self::TAG_MENTION_WITH_SLUG_REGEX, $tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormatterTag $tag
|
||||
* @param array<string, Collection> $mentions
|
||||
* @return true|void
|
||||
*/
|
||||
public static function addTagId(FormatterTag $tag, array $mentions)
|
||||
{
|
||||
/** @var Tag|null $model */
|
||||
$model = $mentions['tags']->where('slug', $tag->getAttribute('slug'))->first();
|
||||
|
||||
if ($model) {
|
||||
$tag->setAttribute('id', (string) $model->id);
|
||||
$tag->setAttribute('tagname', $model->name);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when only an append JS filter is needed,
|
||||
* to add post tag validation attributes.
|
||||
*/
|
||||
public static function dummyFilter(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user