├── .editorconfig ├── .github ├── CODEOWNERS └── workflows │ ├── backend.yml │ └── frontend.yml ├── LICENSE.md ├── README.md ├── composer.json ├── extend.php ├── js ├── admin.ts ├── dist │ ├── admin.js │ ├── admin.js.map │ ├── forum.js │ └── forum.js.map ├── forum.ts ├── package.json ├── src │ ├── admin │ │ ├── extend.ts │ │ └── index.ts │ ├── common │ │ └── utils │ │ │ └── SortMap.js │ └── forum │ │ ├── components │ │ ├── CheckableButton.js │ │ ├── SearchField.js │ │ ├── SmallUserCard.js │ │ ├── UserDirectoryHero.tsx │ │ ├── UserDirectoryList.js │ │ ├── UserDirectoryListItem.js │ │ ├── UserDirectoryPage.js │ │ └── UserDirectoryUserCard.js │ │ ├── extend.ts │ │ ├── extenders │ │ ├── extendCommentPost.tsx │ │ ├── extendIndexPage.tsx │ │ └── extendUsersSearchSource.tsx │ │ ├── index.ts │ │ ├── models │ │ └── Text.ts │ │ ├── searchTypes │ │ ├── AbstractType.js │ │ ├── GroupFilter.js │ │ └── TextFilter.js │ │ └── states │ │ └── UserDirectoryState.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── migrations └── 2019_06_10_000000_rename_permissions.php ├── phpstan.neon ├── resources ├── less │ ├── components │ │ ├── SearchFiled.less │ │ ├── UserDirectoryHero.less │ │ ├── list.less │ │ ├── smallCards.less │ │ └── toolbar.less │ └── forum.less ├── locale │ └── en.yml └── views │ └── index.blade.php └── src ├── Access └── UserPolicy.php ├── Api └── PermissionBasedForumSettings.php └── Content └── UserDirectory.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = lf 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | [*.md] 10 | indent_size = 2 11 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @FriendsOfFlarum/maintainers 2 | -------------------------------------------------------------------------------- /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: FoF User Directory PHP 2 | 3 | on: [workflow_dispatch, push, pull_request] 4 | 5 | jobs: 6 | run: 7 | uses: flarum/framework/.github/workflows/REUSABLE_backend.yml@2.x 8 | with: 9 | enable_backend_testing: true 10 | enable_phpstan: true 11 | 12 | backend_directory: . 13 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: FoF User Directory JS 2 | 3 | on: [workflow_dispatch, push, pull_request] 4 | 5 | jobs: 6 | run: 7 | uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@2.x 8 | with: 9 | enable_bundlewatch: false 10 | enable_prettier: true 11 | enable_typescript: true 12 | 13 | frontend_directory: ./js 14 | backend_directory: . 15 | js_package_manager: yarn 16 | main_git_branch: 2.x 17 | 18 | secrets: 19 | bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2020 FriendsOfFlarum 4 | Copyright (c) 2017-2019 Flagrow 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # User directory by FriendsOfFlarum 2 | 3 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Latest Stable Version](https://img.shields.io/packagist/v/fof/user-directory.svg)](https://packagist.org/packages/fof/user-directory) 4 | 5 | An extension that generates a new url `/users` that provides a list of users, with the ability to sort. You can search through the users from the global 6 | search input field, a new item shows up "Search all users for ...". 7 | 8 | You can protect access to the list with a permission. If the user has no access, they will see a 404 not found page. 9 | 10 | A setting allows you to show a link to the directory from the homepage sidebar, or you can use the [links extension](https://discuss.flarum.org/d/18335-friendsofflarum-links) if you prefer a link in the header. 11 | 12 | ### Installation 13 | 14 | ```sh 15 | composer require fof/user-directory:"*" 16 | ``` 17 | 18 | ### Updating 19 | 20 | To the next minor version: 21 | 22 | ```sh 23 | composer update fof/user-directory 24 | ``` 25 | 26 | To the latest compatible version: 27 | 28 | ```sh 29 | composer require fof/user-directory:"*" 30 | ``` 31 | 32 | ### Links 33 | 34 | - [Flarum Discuss post](https://discuss.flarum.org/d/5682) 35 | - [Source code on GitHub](https://github.com/FriendsOfFlarum/user-directory) 36 | - [Report an issue](https://github.com/FriendsOfFlarum/user-directory/issues) 37 | - [Download via Packagist](https://packagist.org/packages/fof/user-directory) 38 | 39 | An extension by [FriendsOfFlarum](https://github.com/FriendsOfFlarum) 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fof/user-directory", 3 | "description": "The permission based public user directory extension for your Flarum forum.", 4 | "keywords": [ 5 | "extension", 6 | "flarum", 7 | "user list", 8 | "member list", 9 | "user directory" 10 | ], 11 | "type": "flarum-extension", 12 | "license": "MIT", 13 | "support": { 14 | "issues": "https://github.com/FriendsOfFlarum/user-directory/issues", 15 | "source": "https://github.com/FriendsOfFlarum/user-directory", 16 | "forum": "https://discuss.flarum.org/d/5682" 17 | }, 18 | "homepage": "https://friendsofflarum.org", 19 | "funding": [ 20 | { 21 | "type": "website", 22 | "url": "https://opencollective.com/fof/donate" 23 | } 24 | ], 25 | "authors": [ 26 | { 27 | "name": "FriendsOfFlarum", 28 | "homepage": "https://friendsofflarum.org" 29 | } 30 | ], 31 | "require": { 32 | "flarum/core": "2.x-dev", 33 | "ext-json": "*" 34 | }, 35 | "replace": { 36 | "flagrow/user-directory": "*" 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "2.x": "2.x-dev" 41 | }, 42 | "flarum-extension": { 43 | "title": "FoF User Directory", 44 | "category": "feature", 45 | "icon": { 46 | "name": "far fa-address-book", 47 | "backgroundColor": "#e74c3c", 48 | "color": "#fff" 49 | }, 50 | "optional-dependencies": [ 51 | "flarum/suspend" 52 | ] 53 | }, 54 | "flagrow": { 55 | "discuss": "https://discuss.flarum.org/d/5682" 56 | }, 57 | "flarum-cli": { 58 | "modules": { 59 | "githubActions": true, 60 | "backendTesting": true 61 | } 62 | } 63 | }, 64 | "autoload": { 65 | "psr-4": { 66 | "FoF\\UserDirectory\\": "src/" 67 | } 68 | }, 69 | "autoload-dev": { 70 | "psr-4": { 71 | "FoF\\UserDirectory\\Tests\\": "tests/" 72 | } 73 | }, 74 | "scripts": { 75 | "test": [ 76 | "@test:unit", 77 | "@test:integration" 78 | ], 79 | "test:unit": "phpunit -c tests/phpunit.unit.xml", 80 | "test:integration": "phpunit -c tests/phpunit.integration.xml", 81 | "test:setup": "@php tests/integration/setup.php", 82 | "analyse:phpstan": "phpstan analyse", 83 | "clear-cache:phpstan": "phpstan clear-result-cache" 84 | }, 85 | "scripts-descriptions": { 86 | "test": "Runs all tests.", 87 | "test:unit": "Runs all unit tests.", 88 | "test:integration": "Runs all integration tests.", 89 | "test:setup": "Sets up a database for use with integration tests. Execute this only once.", 90 | "analyse:phpstan": "Run static analysis" 91 | }, 92 | "require-dev": { 93 | "flarum/testing": "2.x-dev", 94 | "flarum/phpstan": "2.x-dev", 95 | "flarum/suspend": "*" 96 | }, 97 | "minimum-stability": "dev" 98 | } 99 | -------------------------------------------------------------------------------- /extend.php: -------------------------------------------------------------------------------- 1 | js(__DIR__.'/js/dist/admin.js'), 20 | 21 | (new Extend\Frontend('forum')) 22 | ->js(__DIR__.'/js/dist/forum.js') 23 | ->css(__DIR__.'/resources/less/forum.less') 24 | ->route('/users', 'fof_user_directory', Content\UserDirectory::class), 25 | 26 | new Extend\Locales(__DIR__.'/resources/locale'), 27 | 28 | (new Extend\ApiResource(Resource\ForumResource::class)) 29 | ->fields(Api\PermissionBasedForumSettings::class), 30 | 31 | (new Extend\Policy()) 32 | ->globalPolicy(Access\UserPolicy::class), 33 | 34 | (new Extend\View()) 35 | ->namespace('fof.user-directory', __DIR__.'/resources/views'), 36 | 37 | (new Extend\Settings()) 38 | ->default('fof-user-directory.admin.settings.link', false) 39 | ->default('fof-user-directory.use-small-cards', false) 40 | ->default('fof-user-directory.disable-global-search-source', false) 41 | ->default('fof-user-directory.default-sort', 'default') 42 | ->default('fof-user-directory.link-group-mentions', true) 43 | ->serializeToForum('userDirectorySmallCards', 'fof-user-directory.use-small-cards', 'boolVal') 44 | ->serializeToForum('userDirectoryDisableGlobalSearchSource', 'fof-user-directory.disable-global-search-source', 'boolVal') 45 | ->serializeToForum('userDirectoryLinkGroupMentions', 'fof-user-directory.link-group-mentions', 'boolVal'), 46 | ]; 47 | -------------------------------------------------------------------------------- /js/admin.ts: -------------------------------------------------------------------------------- 1 | export * from './src/admin'; 2 | -------------------------------------------------------------------------------- /js/dist/admin.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={n:r=>{var t=r&&r.__esModule?()=>r.default:()=>r;return e.d(t,{a:t}),t},d:(r,t)=>{for(var s in t)e.o(t,s)&&!e.o(r,s)&&Object.defineProperty(r,s,{enumerable:!0,get:t[s]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r),e.d(r,{SortMap:()=>o,extend:()=>a});const t=flarum.reg.get("core","admin/app");var s=e.n(t);class o{sortMap(){return{username_az:"username",username_za:"-username",newest:"-joinedAt",oldest:"joinedAt",most_discussions:"-discussionCount",least_discussions:"discussionCount"}}}flarum.reg.add("fof-user-directory","common/utils/SortMap",o);const n=flarum.reg.get("core","common/extenders"),a=[(new(e.n(n)().Admin)).setting((()=>({setting:"fof-user-directory-link",label:s().translator.trans("fof-user-directory.admin.settings.link"),type:"boolean"}))).setting((()=>({setting:"fof-user-directory.use-small-cards",label:s().translator.trans("fof-user-directory.admin.settings.use-small-cards"),type:"boolean"}))).setting((()=>({setting:"fof-user-directory.disable-global-search-source",label:s().translator.trans("fof-user-directory.admin.settings.disable-global-search-source"),type:"boolean"}))).setting((()=>({setting:"fof-user-directory.link-group-mentions",label:s().translator.trans("fof-user-directory.admin.settings.link-group-mentions"),type:"boolean"}))).setting((()=>{const e={"":s().translator.trans("fof-user-directory.lib.sort.not_specified")};return Object.keys((new o).sortMap()).forEach((r=>{e[r]=s().translator.trans("fof-user-directory.lib.sort."+r)})),{setting:"fof-user-directory.default-sort",label:s().translator.trans("fof-user-directory.admin.settings.default-sort"),options:e,type:"select",default:""}})).permission((()=>({icon:"far fa-address-book",label:s().translator.trans("fof-user-directory.admin.permissions.view_user_directory"),permission:"fof.user-directory.view",allowGuest:!0})),"view")];s().initializers.add("fof-user-directory",(()=>{}))})(),module.exports=r})(); 2 | //# sourceMappingURL=admin.js.map -------------------------------------------------------------------------------- /js/dist/admin.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"admin.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,mECL9D,MAAM,EAA+BC,OAAOC,IAAIV,IAAI,OAAQ,a,aCI7C,MAAMW,EACnB,OAAAC,GACE,MAAO,CACLC,YAAa,WACbC,YAAa,YACbC,OAAQ,YACRC,OAAQ,WACRC,iBAAkB,mBAClBC,kBAAmB,kBAEvB,EAEFT,OAAOC,IAAIS,IAAI,qBAAsB,uBAAwBR,GChB7D,MAAM,EAA+BF,OAAOC,IAAIV,IAAI,OAAQ,oBCG5D,IAAgB,I,MAAI,WAAeoB,SAAQ,KAAM,CAC/CA,QAAS,0BACTC,MAAO,eAAeC,MAAM,0CAC5BC,KAAM,cACJH,SAAQ,KAAM,CAChBA,QAAS,qCACTC,MAAO,eAAeC,MAAM,qDAC5BC,KAAM,cACJH,SAAQ,KAAM,CAChBA,QAAS,kDACTC,MAAO,eAAeC,MAAM,kEAC5BC,KAAM,cACJH,SAAQ,KAAM,CAChBA,QAAS,yCACTC,MAAO,eAAeC,MAAM,yDAC5BC,KAAM,cACJH,SAAQ,KACV,MAAMI,EAAc,CAClB,GAAI,eAAeF,MAAM,8CAK3B,OAHAzB,OAAO4B,MAAK,IAAId,GAAUC,WAAWc,SAAQC,IAC3CH,EAAYG,GAAQ,eAAeL,MAAM,+BAAiCK,EAAK,IAE1E,CACLP,QAAS,kCACTC,MAAO,eAAeC,MAAM,kDAC5BM,QAASJ,EACTD,KAAM,SACNM,QAAS,GACV,IACAC,YAAW,KAAM,CAClBC,KAAM,sBACNV,MAAO,eAAeC,MAAM,4DAC5BQ,WAAY,0BACZE,YAAY,KACV,SClCJ,iBAAiBb,IAAI,sBAAsB,Q","sources":["webpack://@fof/user-directory/webpack/bootstrap","webpack://@fof/user-directory/webpack/runtime/compat get default export","webpack://@fof/user-directory/webpack/runtime/define property getters","webpack://@fof/user-directory/webpack/runtime/hasOwnProperty shorthand","webpack://@fof/user-directory/webpack/runtime/make namespace object","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'admin/app')\"","webpack://@fof/user-directory/./src/common/utils/SortMap.js","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/extenders')\"","webpack://@fof/user-directory/./src/admin/extend.ts","webpack://@fof/user-directory/./src/admin/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.reg.get('core', 'admin/app');","/**\n * The sort options.\n * We use a class and not just a POJO/function because we want extensions to be able to extend it\n */\nexport default class SortMap {\n sortMap() {\n return {\n username_az: 'username',\n username_za: '-username',\n newest: '-joinedAt',\n oldest: 'joinedAt',\n most_discussions: '-discussionCount',\n least_discussions: 'discussionCount'\n };\n }\n}\nflarum.reg.add('fof-user-directory', 'common/utils/SortMap', SortMap);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extenders');","import Extend from 'flarum/common/extenders';\nimport app from 'flarum/admin/app';\nimport SortMap from '../common/utils/SortMap';\nexport default [new Extend.Admin().setting(() => ({\n setting: 'fof-user-directory-link',\n label: app.translator.trans('fof-user-directory.admin.settings.link'),\n type: 'boolean'\n})).setting(() => ({\n setting: 'fof-user-directory.use-small-cards',\n label: app.translator.trans('fof-user-directory.admin.settings.use-small-cards'),\n type: 'boolean'\n})).setting(() => ({\n setting: 'fof-user-directory.disable-global-search-source',\n label: app.translator.trans('fof-user-directory.admin.settings.disable-global-search-source'),\n type: 'boolean'\n})).setting(() => ({\n setting: 'fof-user-directory.link-group-mentions',\n label: app.translator.trans('fof-user-directory.admin.settings.link-group-mentions'),\n type: 'boolean'\n})).setting(() => {\n const sortOptions = {\n '': app.translator.trans('fof-user-directory.lib.sort.not_specified')\n };\n Object.keys(new SortMap().sortMap()).forEach(sort => {\n sortOptions[sort] = app.translator.trans('fof-user-directory.lib.sort.' + sort);\n });\n return {\n setting: 'fof-user-directory.default-sort',\n label: app.translator.trans('fof-user-directory.admin.settings.default-sort'),\n options: sortOptions,\n type: 'select',\n default: ''\n };\n}).permission(() => ({\n icon: 'far fa-address-book',\n label: app.translator.trans('fof-user-directory.admin.permissions.view_user_directory'),\n permission: 'fof.user-directory.view',\n allowGuest: true\n}), 'view')];","import app from 'flarum/admin/app';\nimport SortMap from '../common/utils/SortMap';\nexport { SortMap };\nexport { default as extend } from './extend';\napp.initializers.add('fof-user-directory', () => {\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","reg","SortMap","sortMap","username_az","username_za","newest","oldest","most_discussions","least_discussions","add","setting","label","trans","type","sortOptions","keys","forEach","sort","options","default","permission","icon","allowGuest"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/dist/forum.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={n:r=>{var t=r&&r.__esModule?()=>r.default:()=>r;return e.d(t,{a:t}),t},d:(r,t)=>{for(var s in t)e.o(t,s)&&!e.o(r,s)&&Object.defineProperty(r,s,{enumerable:!0,get:t[s]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};(()=>{"use strict";e.r(r),e.d(r,{extend:()=>Pe});const t=flarum.reg.get("core","forum/app");var s=e.n(t);const o=flarum.reg.get("core","common/extend"),a=flarum.reg.get("core","forum/components/CommentPost");var n=e.n(a);const i=function(){s().forum.attribute("canSeeUserDirectoryLink")&&s().forum.attribute("userDirectoryLinkGroupMentions")&&this.$(".GroupMention").each((function(){if($(this).hasClass("GroupMention--linked"))return;const e=$(this).find(".GroupMention-name").text(),r=s().store.getBy("groups","namePlural",e.slice(1));if(r){const e=$(``);e.on("click",(function(e){m.route.set(this.getAttribute("href")),e.preventDefault()})),$(this).addClass("GroupMention--linked").wrap(e)}}))};function u(){(0,o.extend)(n().prototype,"oncreate",i),(0,o.extend)(n().prototype,"onupdate",i)}flarum.reg.add("fof-user-directory","forum/extenders/extendCommentPost",u);const c=flarum.reg.get("core","forum/components/UsersSearchSource");var l=e.n(c);const d=flarum.reg.get("core","common/components/LinkButton");var f=e.n(d);function p(){(0,o.extend)(l().prototype,"view",(function(e,r){e&&s().forum.attribute("canSeeUserDirectoryLink")&&!s().forum.attribute("userDirectoryDisableGlobalSearchSource")&&(r=r.toLowerCase(),e.splice(1,0,m("li",null,m(f(),{className:"Button Button--link",href:s().route("fof_user_directory",{q:r}),icon:"fas fa-search"},s().translator.trans("fof-user-directory.forum.search.users_heading",{query:r})))))}))}flarum.reg.add("fof-user-directory","forum/extenders/extendUsersSearchSource",p);const h=flarum.reg.get("core","forum/components/IndexSidebar");var g=e.n(h);function y(){(0,o.extend)(g().prototype,"navItems",(e=>{s().forum.attribute("canSeeUserDirectoryLink")&&s().forum.attribute("canSearchUsers")&&e.add("fof-user-directory",m(f(),{href:s().route("fof_user_directory"),icon:"far fa-address-book"},s().translator.trans("fof-user-directory.forum.page.nav")),85)}))}flarum.reg.add("fof-user-directory","forum/extenders/extendIndexPage",y);const b=flarum.reg.get("core","common/extenders");var v=e.n(b);const S=flarum.reg.get("core","forum/components/PageStructure");var x=e.n(S);const w=flarum.reg.get("core","common/components/Page");var F=e.n(w);const P=flarum.reg.get("core","common/utils/ItemList");var q=e.n(P);const D=flarum.reg.get("core","common/helpers/listItems");var U=e.n(D);const I=flarum.reg.get("core","common/components/Select");var k=e.n(I);const N=flarum.reg.get("core","common/components/Button");var C=e.n(N);const _=flarum.reg.get("core","common/components/Dropdown");var G=e.n(_);const L=flarum.reg.get("core","common/utils/extractText");var B=e.n(L);const R=flarum.reg.get("core","common/Component");var T=e.n(R);const j=flarum.reg.get("core","common/components/LoadingIndicator");var M=e.n(j);const A=flarum.reg.get("core","common/components/Placeholder");var O=e.n(A);const E=flarum.reg.get("core","forum/components/UserCard");var z=e.n(E);const K=flarum.reg.get("core","common/utils/humanTime");var H=e.n(K);class J extends(z()){infoItems(){const e=new(q()),r=this.attrs.user;return e.add("joined",app.translator.trans("core.forum.user.joined_date_text",{ago:H()(r.joinTime())})),e}}flarum.reg.add("fof-user-directory","forum/components/SmallUserCard",J);const Q=flarum.reg.get("core","common/components/Icon");var V=e.n(Q);class W extends(z()){infoItems(){const e=super.infoItems(),r=this.attrs.user;return e.has("lastSeen")&&e.setPriority("lastSeen",100),e.has("joined")&&e.setPriority("joined",95),e.has("points")&&e.setPriority("points",60),e.has("best-answer-count")&&e.setPriority("best-answer-count",68),e.has("masquerade-bio")&&e.setPriority("masquerade-bio",50),e.add("discussion-count",m("div",{className:"userStat"},m(V(),{name:"fas fa-comment"}),s().translator.trans("fof-user-directory.forum.page.usercard.discussion-count",{count:r.discussionCount()})),70),e.add("comment-count",m("div",{className:"userStat"},m(V(),{name:"fas fa-comments"}),s().translator.trans("fof-user-directory.forum.page.usercard.post-count",{count:r.commentCount()})),69),e}}flarum.reg.add("fof-user-directory","forum/components/UserDirectoryUserCard",W);class X extends(T()){view(e){const{user:r,useSmallCards:t}=this.attrs,s={user:r,className:"UserCard--directory"+(t?" UserCard--small":""),controlsButtonClassName:"Button Button--icon Button--flat"};return m("div",{className:"User"},t?J.component(s):W.component(s))}}flarum.reg.add("fof-user-directory","forum/components/UserDirectoryListItem",X);class Y extends(T()){view(){const{state:e}=this.attrs,r=e.getParams(),t=s().forum.attribute("userDirectorySmallCards");let o;if(e.isLoading()?o=M().component():e.moreResults&&(o=C().component({className:"Button",onclick:e.loadMore.bind(e)},s().translator.trans("fof-user-directory.forum.page.load_more_button"))),e.empty()){const e=s().translator.trans("fof-user-directory.forum.page.empty_text");return m("div",{className:"DiscussionList"},O().component({text:e}))}return m("div",{className:"UserDirectoryList"+(e.isSearchResults()?" UserDirectoryList--searchResults":"")+(t?" UserDirectoryList--small-cards":"")},m("ul",{className:"UserDirectoryList-users"},e.users.map((e=>m("li",{key:e.id(),"data-id":e.id()},X.component({user:e,params:r,useSmallCards:t}))))),m("div",{className:"UserDirectoryList-loadMore"},o))}}flarum.reg.add("fof-user-directory","forum/components/UserDirectoryList",Y);class Z{sortMap(){return{username_az:"username",username_za:"-username",newest:"-joinedAt",oldest:"joinedAt",most_discussions:"-discussionCount",least_discussions:"discussionCount"}}}flarum.reg.add("fof-user-directory","common/utils/SortMap",Z);class ee{constructor(e,r){void 0===e&&(e={}),void 0===r&&(r=window.app),this.params=e,this.app=r,this.users=[],this.moreResults=!1,this.loading=!1,this.qBuilder={}}requestParams(){const e={include:["groups"],filter:{}},r=this.params.sort||app.forum.attribute("userDirectoryDefaultSort");return e.sort=this.sortMap()[r],this.params.q&&(e.filter.q=this.params.q),e}sortMap(){return{default:"",...(new Z).sortMap()}}getParams(){return this.params}clear(){this.users=[],m.redraw()}refreshParams(e){this.hasUsers()&&!Object.keys(e).some((r=>this.getParams()[r]!==e[r]))||(this.params=e,e.qBuilder&&(Object.assign(this.qBuilder,e.qBuilder||{}),this.params.q=Object.values(this.qBuilder).join(" ").trim()),this.params.q?"string"!=typeof this.params.q&&(this.params.q=String(this.params.q)):this.params.q="",this.refresh())}refresh(){return this.loading=!0,this.clear(),this.loadResults().then((e=>{this.users=[],this.parseResults(e)}),(()=>{this.loading=!1,m.redraw()}))}loadResults(e){const r=this.app.preloadedApiDocument();if(r)return Promise.resolve(r);const t=this.requestParams();return t.page={offset:e},t.include=t.include.join(","),this.app.store.find("users",t)}loadMore(){this.loading=!0,this.loadResults(this.users.length).then(this.parseResults.bind(this))}parseResults(e){return this.users.push(...e),this.loading=!1,this.moreResults=!!e.payload.links&&!!e.payload.links.next,m.redraw(),e}hasUsers(){return this.users.length>0}isLoading(){return this.loading}isSearchResults(){return!!this.params.q}empty(){return!this.hasUsers()&&!this.isLoading()}}flarum.reg.add("fof-user-directory","forum/states/UserDirectoryState",ee);class re extends(C()){getButtonContent(e){const r=super.getButtonContent(e);return this.attrs.checked&&r.push(m(V(),{name:"fas fa-check",className:"Button-icon ButtonCheck"})),r}}flarum.reg.add("fof-user-directory","forum/components/CheckableButton",re);const te=flarum.reg.get("core","common/utils/withAttr");var se=e.n(te);const oe=flarum.reg.get("core","common/utils/KeyboardNavigatable");var ae=e.n(oe);class ne{constructor(){this.suggestions=[],this.loading=!1}resourceType(){}search(e){}renderKind(e){}renderLabel(e){}applyFilter(e,r){}initializeFromParams(e){}}flarum.reg.add("fof-user-directory","forum/searchTypes/AbstractType",ne);class ie extends ne{resourceType(){return"fof-user-directory-text"}search(e){this.suggestions=e?[s().store.createRecord("fof-user-directory-text",{attributes:{text:e}})]:[]}renderKind(){return s().translator.trans("fof-user-directory.forum.search.kinds.text")}renderLabel(e){return m(".UserDirectorySearchLabel",e.text())}applyFilter(e,r){e.q=e.q?e.q+" ":"",e.q+=r.text()}initializeFromParams(e){return e.q?Promise.resolve(e.q.split(" ").filter((e=>-1===e.indexOf(":"))).map((e=>s().store.createRecord("fof-user-directory-text",{attributes:{text:e}})))):Promise.resolve([])}}flarum.reg.add("fof-user-directory","forum/searchTypes/TextFilter",ie);const ue=flarum.reg.get("core","common/models/Group");var ce=e.n(ue);class le extends ne{resourceType(){return"groups"}search(e){this.suggestions=[],e&&(e=e.toLowerCase(),s().store.all("groups").forEach((r=>{r.id()!==ce().GUEST_ID&&(-1===r.nameSingular().toLowerCase().indexOf(e)&&-1===r.namePlural().toLowerCase().indexOf(e)||this.suggestions.push(r))})))}renderKind(){return s().translator.trans("fof-user-directory.forum.search.kinds.group")}renderLabel(e){return m(".UserDirectorySearchLabel",e.color()?{className:"colored",style:{backgroundColor:e.color()}}:{},[e.icon()?[V().component({name:e.icon()})," "]:null,e.namePlural()])}applyFilter(e,r){e.q=e.q?e.q+" ":"",e.q+="group:"+r.id()}initializeFromParams(e){if(!e.q)return Promise.resolve([]);const r=[],t=e.q.match(/\bgroup:(\d+)\b/g);if(!t||!t.length)return Promise.resolve([]);const o=[];t.forEach((e=>{const r=e.replace("group:","");o.push(r)}));const a=[...new Set(o)].map((e=>s().store.find("groups",e).then((e=>(e&&r.push(e),e))).catch((r=>(console.error("Error loading group:",e,r),null)))));return Promise.all(a).then((()=>r))}}flarum.reg.add("fof-user-directory","forum/searchTypes/GroupFilter",le);class me extends(T()){oninit(e){super.oninit(e),this.searchIndex=0,this.navigator=new(ae()),this.navigator.when((e=>"Tab"!==e.key||!!this.filter)).onUp((()=>{this.searchIndex>0&&(this.searchIndex--,m.redraw())})).onDown((()=>{this.searchIndex{this.filter?(this.selectResult(this.allSuggestions()[this.searchIndex]),m.redraw()):this.applyFiltering()})).onRemove((()=>{this.appliedFilters.pop()})),this.availableFilters=this.filterTypes().toArray(),this.appliedFilters=[],this.filter="",this.focused=!1,this.availableFilters.forEach((e=>{e.initializeFromParams({sort:m.route.param("sort"),q:m.route.param("q")}).then((e=>{this.appliedFilters.push(...e),m.redraw()}))}))}view(){const e=this.allSuggestions(),r=this.availableFilters.some((e=>e.loading));return m("div",{className:"Form-group Usersearchbox"},m("label",{className:"UserDirectorySearchInput FormControl "+(this.focused?"focus":"")},m("span",{className:"UserDirectorySearchInput-selected"},this.appliedFilters.map(((e,r)=>m("span",{className:"UserDirectorySearchInput-filter",onclick:()=>{this.appliedFilters.splice(r,1),this.applyFiltering()},title:this.searchResultKind(e)},this.recipientLabel(e))))),m("input",{className:"FormControl",placeholder:s().translator.trans("fof-user-directory.forum.search.field.placeholder"),value:this.filter,oninput:se()("value",(e=>{this.filter=e,this.performNewSearch()})),onkeydown:this.navigator.navigate.bind(this.navigator),onfocus:()=>{this.focused=!0},onblur:()=>{this.focused=!1}}),r&&m(M(),null),!!e.length&&m("ul",{className:"Dropdown-menu"},e.map(((e,r)=>m("li",{className:this.searchIndex===r?"active":"",onclick:()=>{this.selectResult(e),this.applyFiltering()}},m("button",{type:"button"},m("span",{className:"UserDirectorySearchKind"},this.searchResultKind(e)),this.recipientLabel(e))))))))}filterTypes(){const e=new(q());return e.add("text",new ie,10),e.add("group",new le,20),e}filterForResource(e){return this.availableFilters.find((r=>r.resourceType()===e.data.type))}recipientLabel(e){const r=this.filterForResource(e);return r?r.renderLabel(e):"[unknown]"}searchResultKind(e){const r=this.filterForResource(e);return r?r.renderKind(e):"[unknown]"}selectResult(e){e&&(this.appliedFilters.push(e),this.clearSuggestions())}clearSuggestions(){this.filter="",this.availableFilters.forEach((e=>{e.search("")}))}allSuggestions(){return[].concat(...this.availableFilters.map((e=>e.suggestions)))}performNewSearch(){this.searchIndex=0,this.availableFilters.forEach((e=>{e.search(this.filter)})),this.attrs.state.refreshParams({...this.attrs.state.getParams(),qBuilder:this.qBuilder()})}qBuilder(e){return void 0===e&&(e={}),this.appliedFilters.forEach((r=>{const t=this.filterForResource(r);t?t.applyFilter(e,r):console.warn("Cannot find filter class for resource",r)})),{filter:`${this.filter} ${e.q||""}`}}applyFiltering(){const e={sort:m.route.param("sort")};this.qBuilder(e),m.route.set(s().route("fof_user_directory",e))}}flarum.reg.add("fof-user-directory","forum/components/SearchField",me);const de=flarum.reg.get("core","common/components/Separator");var fe=e.n(de);const pe=flarum.reg.get("core","common/helpers/textContrastClass");var he=e.n(pe);const ge=flarum.reg.get("core","common/utils/classList");var ye=e.n(ge);class be extends(T()){view(){const e=this.heroColor();return m("header",{className:ye()("Hero","UserDirectoryHero",{"UserDirectoryHero--colored":e,[he()(e)]:e}),style:e?{"--hero-bg":e}:void 0},m("div",{className:"container"},this.viewItems().toArray()))}viewItems(){const e=new(q());return e.add("content",m("div",{className:"containerNarrow"},this.contentItems().toArray()),80),e}contentItems(){const e=new(q());return e.add("user-directory-title",m("h1",{className:"Hero-title"},m(V(),{name:this.heroIcon()})," ",s().translator.trans("fof-user-directory.forum.hero.title")),100),e}heroColor(){return null}heroIcon(){return"far fa-address-book"}}flarum.reg.add("fof-user-directory","forum/components/UserDirectoryHero",be);class ve extends(F()){oninit(e){super.oninit(e),this.state=new ee({}),this.enabledGroupFilters=[],this.enabledSpecialGroupFilters={};const r=s().preloadedApiDocument(),t=r&&r.payload&&r.payload.fofUserDirectory,o=t?t.q:m.route.param("q")||"";if(o){const e=o.match(/\bgroup:(\d+)\b/g);e&&(this.enabledGroupFilters=e.map((e=>e.replace("group:","")))),s().initializers.has("flarum-suspend")&&s().forum.attribute("hasSuspendPermission")&&o.includes("is:suspended")&&(this.enabledSpecialGroupFilters["flarum-suspend"]="is:suspended")}const a={q:o,sort:t?t.sort:m.route.param("sort")};this.state.refreshParams(a),this.bodyClass="User--directory",s().history.push("users",s().translator.trans("fof-user-directory.forum.header.back_to_user_directory_tooltip"))}oncreate(e){super.oncreate(e),s().setTitle(B()(s().translator.trans("fof-user-directory.forum.page.nav")))}view(){return m(x(),{className:"UserDirectoryPage",hero:()=>m(be,null),sidebar:()=>m(g(),null)},m("div",{className:"IndexPage-toolbar"},m("ul",{className:"IndexPage-toolbar-view"},U()(this.viewItems().toArray())),m("ul",{className:"IndexPage-toolbar-action"},U()(this.actionItems().toArray()))),m(Y,{state:this.state}))}viewItems(){const e=new(q()),r=this.state.sortMap(),t={};for(const e in r)t[e]=s().translator.trans("fof-user-directory.lib.sort."+e);return e.add("sort",k().component({options:t,value:this.state.getParams().sort||s().forum.attribute("userDirectoryDefaultSort"),onchange:this.changeParams.bind(this)}),100),e.add("filterGroups",G().component({caretIcon:"fas fa-filter",label:s().translator.trans("fof-user-directory.forum.page.filter_button"),buttonClassName:"Button",className:"GroupFilterDropdown"},this.groupItems().toArray()),80),e.add("search",me.component({state:this.state}),60),e}groupItems(){const e=new(q());return s().store.all("groups").filter((e=>"2"!==e.id()&&"3"!==e.id())).forEach((r=>{e.add(r.namePlural(),re.component({className:"GroupFilterButton",icon:r.icon(),checked:this.enabledGroupFilters.includes(r.id()),onclick:()=>{const e=r.id();this.enabledGroupFilters.includes(e)?this.enabledGroupFilters=this.enabledGroupFilters.filter((r=>r!=e)):(this.enabledGroupFilters.push(e),this.enabledSpecialGroupFilters=[]),this.changeParams(this.params().sort)}},r.namePlural()))})),s().initializers.has("flarum-suspend")&&s().forum.attribute("hasSuspendPermission")&&(e.add("suspend",re.component({className:"GroupFilterButton",icon:"fas fa-ban",checked:"is:suspended"===this.enabledSpecialGroupFilters["flarum-suspend"],onclick:()=>{const e="flarum-suspend";"is:suspended"===this.enabledSpecialGroupFilters[e]?this.enabledSpecialGroupFilters[e]="":(this.enabledSpecialGroupFilters[e]="is:suspended",this.enabledGroupFilters=[]),this.changeParams(this.params().sort)}},s().translator.trans("flarum-suspend.forum.user_badge.suspended_tooltip")),90),e.add("seperator",fe().component(),50)),e}actionItems(){const e=new(q());return e.add("refresh",C().component({title:s().translator.trans("fof-user-directory.forum.page.refresh_tooltip"),icon:"fas fa-sync",className:"Button Button--icon",onclick:()=>{this.state.refresh(),s().session.user&&(s().store.find("users",s().session.user.id()),m.redraw())}})),e}changeParams(e){const r=this.params();e===s().forum.attribute("userDirectoryDefaultSort")?delete r.sort:r.sort=e;let t="";for(const e in this.enabledSpecialGroupFilters)this.enabledSpecialGroupFilters[e]&&(t+=this.enabledSpecialGroupFilters[e]+" ");this.enabledGroupFilters.length>0&&this.enabledGroupFilters.forEach((e=>{t+=`group:${e} `})),r.q=t.trim(),delete r.qBuilder,this.state.refreshParams(r),m.route.set(s().route("fof_user_directory",r))}stickyParams(){return{sort:m.route.param("sort"),q:m.route.param("q")}}params(){return this.stickyParams()}}function Se(e){return Se="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Se(e)}flarum.reg.add("fof-user-directory","forum/components/UserDirectoryPage",ve);const xe=flarum.reg.get("core","common/Model");var we=e.n(xe);class Fe extends(we()){constructor(){var e,r,t;super(...arguments),e=this,r="text",t=we().attribute("text"),(r=function(e){var r=function(e){if("object"!=Se(e)||!e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var t=r.call(e,"string");if("object"!=Se(t))return t;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==Se(r)?r:r+""}(r))in e?Object.defineProperty(e,r,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[r]=t}}flarum.reg.add("fof-user-directory","forum/models/Text",Fe);const Pe=[(new(v().Routes)).add("fof_user_directory","/users",ve),(new(v().Store)).add("fof-user-directory-text",Fe)];s().initializers.add("fof-user-directory",(function(){u(),p(),y()}))})(),module.exports=r})(); 2 | //# sourceMappingURL=forum.js.map -------------------------------------------------------------------------------- /js/dist/forum.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"forum.js","mappings":"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","sources":["webpack://@fof/user-directory/webpack/bootstrap","webpack://@fof/user-directory/webpack/runtime/compat get default export","webpack://@fof/user-directory/webpack/runtime/define property getters","webpack://@fof/user-directory/webpack/runtime/hasOwnProperty shorthand","webpack://@fof/user-directory/webpack/runtime/make namespace object","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'forum/app')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/extend')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'forum/components/CommentPost')\"","webpack://@fof/user-directory/./src/forum/extenders/extendCommentPost.tsx","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'forum/components/UsersSearchSource')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/LinkButton')\"","webpack://@fof/user-directory/./src/forum/extenders/extendUsersSearchSource.tsx","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'forum/components/IndexSidebar')\"","webpack://@fof/user-directory/./src/forum/extenders/extendIndexPage.tsx","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/extenders')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'forum/components/PageStructure')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Page')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/utils/ItemList')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/helpers/listItems')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Select')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Button')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Dropdown')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/utils/extractText')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/Component')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/LoadingIndicator')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Placeholder')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'forum/components/UserCard')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/utils/humanTime')\"","webpack://@fof/user-directory/./src/forum/components/SmallUserCard.js","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Icon')\"","webpack://@fof/user-directory/./src/forum/components/UserDirectoryUserCard.js","webpack://@fof/user-directory/./src/forum/components/UserDirectoryListItem.js","webpack://@fof/user-directory/./src/forum/components/UserDirectoryList.js","webpack://@fof/user-directory/./src/common/utils/SortMap.js","webpack://@fof/user-directory/./src/forum/states/UserDirectoryState.js","webpack://@fof/user-directory/./src/forum/components/CheckableButton.js","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/utils/withAttr')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/utils/KeyboardNavigatable')\"","webpack://@fof/user-directory/./src/forum/searchTypes/AbstractType.js","webpack://@fof/user-directory/./src/forum/searchTypes/TextFilter.js","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/models/Group')\"","webpack://@fof/user-directory/./src/forum/searchTypes/GroupFilter.js","webpack://@fof/user-directory/./src/forum/components/SearchField.js","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/components/Separator')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/helpers/textContrastClass')\"","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/utils/classList')\"","webpack://@fof/user-directory/./src/forum/components/UserDirectoryHero.tsx","webpack://@fof/user-directory/./src/forum/components/UserDirectoryPage.js","webpack://@fof/user-directory/./node_modules/@babel/runtime/helpers/esm/typeof.js","webpack://@fof/user-directory/external root \"flarum.reg.get('core', 'common/Model')\"","webpack://@fof/user-directory/./src/forum/models/Text.ts","webpack://@fof/user-directory/./node_modules/@babel/runtime/helpers/esm/defineProperty.js","webpack://@fof/user-directory/./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js","webpack://@fof/user-directory/./node_modules/@babel/runtime/helpers/esm/toPrimitive.js","webpack://@fof/user-directory/./src/forum/extend.ts","webpack://@fof/user-directory/./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.reg.get('core', 'forum/app');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extend');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/components/CommentPost');","import app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport CommentPost from 'flarum/forum/components/CommentPost';\nexport const linkGroupMentions = function () {\n if (app.forum.attribute('canSeeUserDirectoryLink') && app.forum.attribute('userDirectoryLinkGroupMentions')) {\n // @ts-ignore\n this.$('.GroupMention').each(function () {\n // @ts-ignore\n if ($(this).hasClass('GroupMention--linked')) return;\n\n // @ts-ignore\n const name = $(this).find('.GroupMention-name').text();\n const group = app.store.getBy('groups', 'namePlural', name.slice(1));\n if (group) {\n const link = $(``);\n link.on('click', function (e) {\n // @ts-ignore\n m.route.set(this.getAttribute('href'));\n e.preventDefault();\n });\n\n // @ts-ignore\n $(this).addClass('GroupMention--linked').wrap(link);\n }\n });\n }\n};\nexport default function extendCommentPost() {\n extend(CommentPost.prototype, 'oncreate', linkGroupMentions);\n extend(CommentPost.prototype, 'onupdate', linkGroupMentions);\n}\nflarum.reg.add('fof-user-directory', 'forum/extenders/extendCommentPost', extendCommentPost);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/components/UsersSearchSource');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/LinkButton');","import app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport UsersSearchSource from 'flarum/forum/components/UsersSearchSource';\nimport LinkButton from 'flarum/common/components/LinkButton';\nexport default function extendUsersSearchSource() {\n extend(UsersSearchSource.prototype, 'view', function (view, query) {\n if (!view || !app.forum.attribute('canSeeUserDirectoryLink') || app.forum.attribute('userDirectoryDisableGlobalSearchSource')) {\n return;\n }\n query = query.toLowerCase();\n view.splice(1, 0, m(\"li\", null, m(LinkButton, {\n className: \"Button Button--link\",\n href: app.route('fof_user_directory', {\n q: query\n }),\n icon: \"fas fa-search\"\n }, app.translator.trans('fof-user-directory.forum.search.users_heading', {\n query\n }))));\n });\n}\nflarum.reg.add('fof-user-directory', 'forum/extenders/extendUsersSearchSource', extendUsersSearchSource);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/components/IndexSidebar');","import IndexSidebar from 'flarum/forum/components/IndexSidebar';\nimport app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport LinkButton from 'flarum/common/components/LinkButton';\nexport default function extendIndexPage() {\n extend(IndexSidebar.prototype, 'navItems', items => {\n if (app.forum.attribute('canSeeUserDirectoryLink') && app.forum.attribute('canSearchUsers')) {\n items.add('fof-user-directory', m(LinkButton, {\n href: app.route('fof_user_directory'),\n icon: \"far fa-address-book\"\n }, app.translator.trans('fof-user-directory.forum.page.nav')), 85);\n }\n });\n}\nflarum.reg.add('fof-user-directory', 'forum/extenders/extendIndexPage', extendIndexPage);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/extenders');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/components/PageStructure');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Page');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/ItemList');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/helpers/listItems');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Select');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Button');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Dropdown');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/extractText');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/Component');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/LoadingIndicator');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Placeholder');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'forum/components/UserCard');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/humanTime');","import UserCard from 'flarum/forum/components/UserCard';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport humanTime from 'flarum/common/utils/humanTime';\nexport default class SmallUserCard extends UserCard {\n //Overriding infoItems so that other extensions can separately add items to small cards\n infoItems() {\n const items = new ItemList();\n const user = this.attrs.user;\n items.add('joined', app.translator.trans('core.forum.user.joined_date_text', {\n ago: humanTime(user.joinTime())\n }));\n return items;\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/SmallUserCard', SmallUserCard);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Icon');","import UserCard from 'flarum/forum/components/UserCard';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport humanTime from 'flarum/common/utils/humanTime';\nimport Icon from 'flarum/common/components/Icon';\nimport app from 'flarum/forum/app';\nexport default class UserDirectoryUserCard extends UserCard {\n /**\n * Allowing to add additonal items unique to the user directory.\n *\n * @return {ItemList}\n */\n infoItems() {\n const items = super.infoItems();\n const user = this.attrs.user;\n if (items.has('lastSeen')) items.setPriority('lastSeen', 100);\n if (items.has('joined')) items.setPriority('joined', 95);\n if (items.has('points')) items.setPriority('points', 60);\n if (items.has('best-answer-count')) items.setPriority('best-answer-count', 68);\n if (items.has('masquerade-bio')) items.setPriority('masquerade-bio', 50);\n items.add('discussion-count', m(\"div\", {\n className: \"userStat\"\n }, m(Icon, {\n name: \"fas fa-comment\"\n }), app.translator.trans('fof-user-directory.forum.page.usercard.discussion-count', {\n count: user.discussionCount()\n })), 70);\n items.add('comment-count', m(\"div\", {\n className: \"userStat\"\n }, m(Icon, {\n name: \"fas fa-comments\"\n }), app.translator.trans('fof-user-directory.forum.page.usercard.post-count', {\n count: user.commentCount()\n })), 69);\n return items;\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/UserDirectoryUserCard', UserDirectoryUserCard);","import Component from 'flarum/common/Component';\nimport UserCard from 'flarum/forum/components/UserCard';\nimport SmallUserCard from './SmallUserCard';\nimport UserDirectoryUserCard from './UserDirectoryUserCard';\nexport default class UserDirectoryListItem extends Component {\n view(vnode) {\n const {\n user,\n useSmallCards\n } = this.attrs;\n const attributes = {\n user,\n className: `UserCard--directory${useSmallCards ? ' UserCard--small' : ''}`,\n controlsButtonClassName: 'Button Button--icon Button--flat'\n };\n return m(\"div\", {\n className: \"User\"\n }, useSmallCards ? SmallUserCard.component(attributes) : UserDirectoryUserCard.component(attributes));\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/UserDirectoryListItem', UserDirectoryListItem);","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport Button from 'flarum/common/components/Button';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport Placeholder from 'flarum/common/components/Placeholder';\nimport UserDirectoryListItem from './UserDirectoryListItem';\n\n/**\n * Based on Flarum's DiscussionList\n */\nexport default class UserDirectoryList extends Component {\n view() {\n const {\n state\n } = this.attrs;\n const params = state.getParams();\n const useSmallCards = app.forum.attribute('userDirectorySmallCards');\n let loading;\n if (state.isLoading()) {\n loading = LoadingIndicator.component();\n } else if (state.moreResults) {\n loading = Button.component({\n className: 'Button',\n onclick: state.loadMore.bind(state)\n }, app.translator.trans('fof-user-directory.forum.page.load_more_button'));\n }\n if (state.empty()) {\n const text = app.translator.trans('fof-user-directory.forum.page.empty_text');\n return m(\"div\", {\n className: \"DiscussionList\"\n }, Placeholder.component({\n text\n }));\n }\n return m(\"div\", {\n className: 'UserDirectoryList' + (state.isSearchResults() ? ' UserDirectoryList--searchResults' : '') + (useSmallCards ? ' UserDirectoryList--small-cards' : '')\n }, m(\"ul\", {\n className: \"UserDirectoryList-users\"\n }, state.users.map(user => {\n return m(\"li\", {\n key: user.id(),\n \"data-id\": user.id()\n }, UserDirectoryListItem.component({\n user,\n params,\n useSmallCards\n }));\n })), m(\"div\", {\n className: \"UserDirectoryList-loadMore\"\n }, loading));\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/UserDirectoryList', UserDirectoryList);","/**\n * The sort options.\n * We use a class and not just a POJO/function because we want extensions to be able to extend it\n */\nexport default class SortMap {\n sortMap() {\n return {\n username_az: 'username',\n username_za: '-username',\n newest: '-joinedAt',\n oldest: 'joinedAt',\n most_discussions: '-discussionCount',\n least_discussions: 'discussionCount'\n };\n }\n}\nflarum.reg.add('fof-user-directory', 'common/utils/SortMap', SortMap);","/**\n * Based on Flarum's DiscussionListState\n */\nimport SortMap from '../../common/utils/SortMap';\nexport default class UserDirectoryState {\n constructor(params, app) {\n if (params === void 0) {\n params = {};\n }\n if (app === void 0) {\n app = window.app;\n }\n this.params = params;\n this.app = app;\n this.users = [];\n this.moreResults = false;\n this.loading = false;\n this.qBuilder = {};\n }\n requestParams() {\n const params = {\n include: ['groups'],\n filter: {}\n };\n const sortKey = this.params.sort || app.forum.attribute('userDirectoryDefaultSort');\n\n // sort might be set to null if no sort params has been passed\n params.sort = this.sortMap()[sortKey];\n if (this.params.q) {\n params.filter.q = this.params.q;\n }\n return params;\n }\n sortMap() {\n return {\n default: '',\n ...new SortMap().sortMap()\n };\n }\n getParams() {\n return this.params;\n }\n clear() {\n this.users = [];\n m.redraw();\n }\n refreshParams(newParams) {\n if (!this.hasUsers() || Object.keys(newParams).some(key => this.getParams()[key] !== newParams[key])) {\n this.params = newParams;\n\n // If we have a qBuilder, use it to build the query\n if (newParams.qBuilder) {\n Object.assign(this.qBuilder, newParams.qBuilder || {});\n this.params.q = Object.values(this.qBuilder).join(' ').trim();\n }\n\n // Make sure we have a query parameter\n if (!this.params.q) {\n this.params.q = '';\n } else if (typeof this.params.q !== 'string') {\n // Ensure q is a string\n this.params.q = String(this.params.q);\n }\n this.refresh();\n }\n }\n refresh() {\n this.loading = true;\n this.clear();\n return this.loadResults().then(results => {\n this.users = [];\n this.parseResults(results);\n }, () => {\n this.loading = false;\n m.redraw();\n });\n }\n loadResults(offset) {\n const preloadedUsers = this.app.preloadedApiDocument();\n if (preloadedUsers) {\n return Promise.resolve(preloadedUsers);\n }\n const params = this.requestParams();\n params.page = {\n offset\n };\n params.include = params.include.join(',');\n return this.app.store.find('users', params);\n }\n loadMore() {\n this.loading = true;\n this.loadResults(this.users.length).then(this.parseResults.bind(this));\n }\n parseResults(results) {\n this.users.push(...results);\n this.loading = false;\n this.moreResults = !!results.payload.links && !!results.payload.links.next;\n m.redraw();\n return results;\n }\n hasUsers() {\n return this.users.length > 0;\n }\n isLoading() {\n return this.loading;\n }\n isSearchResults() {\n return !!this.params.q;\n }\n empty() {\n return !this.hasUsers() && !this.isLoading();\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/states/UserDirectoryState', UserDirectoryState);","import Button from 'flarum/common/components/Button';\nimport Icon from 'flarum/common/components/Icon';\nexport default class CheckableButton extends Button {\n /**\n * Get the template for the button's content.\n *\n * @return {*}\n * @protected\n */\n getButtonContent(children) {\n const prev = super.getButtonContent(children);\n if (this.attrs.checked) prev.push(m(Icon, {\n name: \"fas fa-check\",\n className: \"Button-icon ButtonCheck\"\n }));\n return prev;\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/CheckableButton', CheckableButton);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/withAttr');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/KeyboardNavigatable');","/**\n * @abstract\n */\nexport default class AbstractType {\n constructor() {\n this.suggestions = [];\n this.loading = false;\n }\n\n /**\n * The `type` property of the Models used in suggestions and applied filters for this type\n * @return {String}\n */\n resourceType() {\n //\n }\n\n /**\n * Executed when the search query changes\n * The method should update this.suggestions with the new results\n * If asynchronous loading is used, this.loading should be set to true during the process\n * @param {String} query\n */\n search(query) {\n //\n }\n\n /**\n * Renders the \"kind\" label next to the value indicating what kind of information that result is\n * Should probably just be a translated text\n * @param {Model} resource\n * @return {vnode}\n */\n renderKind(resource) {\n //\n }\n\n /**\n * Renders the Label containing the suggestion's value\n * Should be a vdom template using the .UserDirectorySearchLabel class or similar\n * @param {Model} resource\n * @return {vnode}\n */\n renderLabel(resource) {\n //\n }\n\n /**\n * Applies a filter on a params object to use in the page request\n * @param {Object} params Object. Might or might not contain a `q` property or `sort` property. In the future, `filters` object might be supported\n * @param {Model} resource\n */\n applyFilter(params, resource) {\n //\n }\n\n /**\n * Used to populate the search field on page load with values from the querystring\n * A promise must be returned, and the UI will auto-update once the promise returns\n * @param {Object} params Object with a `q` and `sort` property. `filters` might be supported in the future\n * @return {Promise}\n */\n initializeFromParams(params) {\n //\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/searchTypes/AbstractType', AbstractType);","import app from 'flarum/forum/app';\nimport AbstractType from './AbstractType';\n\n/* global m */\n\nexport default class TextFilter extends AbstractType {\n resourceType() {\n return 'fof-user-directory-text';\n }\n search(query) {\n if (!query) {\n this.suggestions = [];\n return;\n }\n this.suggestions = [app.store.createRecord('fof-user-directory-text', {\n attributes: {\n text: query\n }\n })];\n }\n renderKind() {\n return app.translator.trans('fof-user-directory.forum.search.kinds.text');\n }\n renderLabel(resource) {\n return m('.UserDirectorySearchLabel', resource.text());\n }\n applyFilter(params, resource) {\n params.q = params.q ? params.q + ' ' : '';\n params.q += resource.text();\n }\n initializeFromParams(params) {\n if (!params.q) {\n return Promise.resolve([]);\n }\n return Promise.resolve(params.q.split(' ')\n // Words with : are gambits and we will ignore them\n .filter(word => word.indexOf(':') === -1).map(word => app.store.createRecord('fof-user-directory-text', {\n attributes: {\n text: word\n }\n })));\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/searchTypes/TextFilter', TextFilter);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/models/Group');","import app from 'flarum/forum/app';\nimport Group from 'flarum/common/models/Group';\nimport Icon from 'flarum/common/components/Icon';\nimport AbstractType from './AbstractType';\n\n/* global m */\n\nexport default class GroupFilter extends AbstractType {\n resourceType() {\n return 'groups';\n }\n search(query) {\n this.suggestions = [];\n if (!query) {\n return;\n }\n query = query.toLowerCase();\n app.store.all('groups').forEach(group => {\n // Do not allow Guest group as it wouldn't do anything\n if (group.id() === Group.GUEST_ID) {\n return;\n }\n if (group.nameSingular().toLowerCase().indexOf(query) !== -1 || group.namePlural().toLowerCase().indexOf(query) !== -1) {\n this.suggestions.push(group);\n }\n });\n }\n renderKind() {\n return app.translator.trans('fof-user-directory.forum.search.kinds.group');\n }\n renderLabel(group) {\n return m('.UserDirectorySearchLabel', group.color() ? {\n className: 'colored',\n style: {\n backgroundColor: group.color()\n }\n } : {}, [group.icon() ? [Icon.component({\n name: group.icon()\n }), ' '] : null, group.namePlural()]);\n }\n applyFilter(params, group) {\n params.q = params.q ? params.q + ' ' : '';\n params.q += 'group:' + group.id();\n }\n initializeFromParams(params) {\n if (!params.q) {\n return Promise.resolve([]);\n }\n const groups = [];\n\n // Extract all group: parameters from the query string\n const groupMatches = params.q.match(/\\bgroup:(\\d+)\\b/g);\n if (!groupMatches || !groupMatches.length) {\n return Promise.resolve([]);\n }\n\n // Get all unique group IDs from all group: parameters\n const allGroupIds = [];\n groupMatches.forEach(match => {\n const id = match.replace('group:', '');\n allGroupIds.push(id);\n });\n\n // Deduplicate group IDs\n const uniqueGroupIds = [...new Set(allGroupIds)];\n\n // Load all group models\n const promises = uniqueGroupIds.map(id => {\n return app.store.find('groups', id).then(group => {\n if (group) groups.push(group);\n return group;\n }).catch(error => {\n console.error('Error loading group:', id, error);\n return null;\n });\n });\n return Promise.all(promises).then(() => groups);\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/searchTypes/GroupFilter', GroupFilter);","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\nimport withAttr from 'flarum/common/utils/withAttr';\nimport KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport TextFilter from '../searchTypes/TextFilter';\nimport GroupFilter from '../searchTypes/GroupFilter';\nexport default class SearchField extends Component {\n oninit(vnode) {\n super.oninit(vnode);\n this.searchIndex = 0;\n this.navigator = new KeyboardNavigatable();\n this.navigator.when(event => {\n // Do not handle keyboard when TAB is pressed and there's nothing in field\n // Without this it's impossible to TAB out of the field\n return event.key !== 'Tab' || !!this.filter;\n }).onUp(() => {\n if (this.searchIndex > 0) {\n this.searchIndex--;\n m.redraw();\n }\n }).onDown(() => {\n if (this.searchIndex < this.allSuggestions().length - 1) {\n this.searchIndex++;\n m.redraw();\n }\n }).onSelect(() => {\n if (this.filter) {\n this.selectResult(this.allSuggestions()[this.searchIndex]);\n m.redraw();\n } else {\n this.applyFiltering();\n }\n }).onRemove(() => {\n this.appliedFilters.pop();\n });\n this.availableFilters = this.filterTypes().toArray();\n this.appliedFilters = [];\n this.filter = '';\n this.focused = false;\n\n // When the page loads, initialize UI with filters from the parameters\n this.availableFilters.forEach(filter => {\n filter.initializeFromParams({\n sort: m.route.param('sort'),\n q: m.route.param('q')\n }).then(resources => {\n this.appliedFilters.push(...resources);\n m.redraw();\n });\n });\n }\n view() {\n const suggestions = this.allSuggestions();\n const loading = this.availableFilters.some(filter => filter.loading);\n return m(\"div\", {\n className: \"Form-group Usersearchbox\"\n }, m(\"label\", {\n className: `UserDirectorySearchInput FormControl ${this.focused ? 'focus' : ''}`\n }, m(\"span\", {\n className: \"UserDirectorySearchInput-selected\"\n }, this.appliedFilters.map((recipient, index) => m(\"span\", {\n className: \"UserDirectorySearchInput-filter\",\n onclick: () => {\n this.appliedFilters.splice(index, 1);\n this.applyFiltering();\n },\n title: this.searchResultKind(recipient)\n }, this.recipientLabel(recipient)))), m(\"input\", {\n className: \"FormControl\",\n placeholder: app.translator.trans('fof-user-directory.forum.search.field.placeholder'),\n value: this.filter,\n oninput: withAttr('value', value => {\n this.filter = value;\n this.performNewSearch();\n }),\n onkeydown: this.navigator.navigate.bind(this.navigator),\n onfocus: () => {\n this.focused = true;\n },\n onblur: () => {\n this.focused = false;\n }\n }), loading && m(LoadingIndicator, null), !!suggestions.length && m(\"ul\", {\n className: \"Dropdown-menu\"\n }, suggestions.map((result, index) => m(\"li\", {\n className: this.searchIndex === index ? 'active' : '',\n onclick: () => {\n this.selectResult(result);\n this.applyFiltering();\n }\n }, m(\"button\", {\n type: \"button\"\n }, m(\"span\", {\n className: \"UserDirectorySearchKind\"\n }, this.searchResultKind(result)), this.recipientLabel(result)))))));\n }\n filterTypes() {\n const items = new ItemList();\n items.add('text', new TextFilter(), 10);\n items.add('group', new GroupFilter(), 20);\n return items;\n }\n filterForResource(resource) {\n return this.availableFilters.find(f => f.resourceType() === resource.data.type);\n }\n recipientLabel(resource) {\n const filter = this.filterForResource(resource);\n if (filter) {\n return filter.renderLabel(resource);\n }\n return '[unknown]';\n }\n searchResultKind(resource) {\n const filter = this.filterForResource(resource);\n if (filter) {\n return filter.renderKind(resource);\n }\n return '[unknown]';\n }\n selectResult(result) {\n if (!result) {\n return;\n }\n this.appliedFilters.push(result);\n this.clearSuggestions();\n }\n clearSuggestions() {\n this.filter = '';\n this.availableFilters.forEach(filter => {\n filter.search('');\n });\n }\n allSuggestions() {\n return [].concat(...this.availableFilters.map(filter => filter.suggestions));\n }\n performNewSearch() {\n this.searchIndex = 0;\n this.availableFilters.forEach(filter => {\n filter.search(this.filter);\n });\n this.attrs.state.refreshParams({\n ...this.attrs.state.getParams(),\n qBuilder: this.qBuilder()\n });\n }\n qBuilder(params) {\n if (params === void 0) {\n params = {};\n }\n this.appliedFilters.forEach(resource => {\n const filter = this.filterForResource(resource);\n if (filter) {\n filter.applyFilter(params, resource);\n } else {\n console.warn('Cannot find filter class for resource', resource);\n }\n });\n return {\n filter: `${this.filter} ${params.q || ''}`\n };\n }\n applyFiltering() {\n const params = {\n sort: m.route.param('sort')\n };\n this.qBuilder(params);\n m.route.set(app.route('fof_user_directory', params));\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/SearchField', SearchField);","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/components/Separator');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/helpers/textContrastClass');","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/utils/classList');","import app from 'flarum/forum/app';\nimport Component from 'flarum/common/Component';\nimport Icon from 'flarum/common/components/Icon';\nimport textContrastClass from 'flarum/common/helpers/textContrastClass';\nimport classList from 'flarum/common/utils/classList';\nimport ItemList from 'flarum/common/utils/ItemList';\nexport default class UserDirectoryHero extends Component {\n view() {\n const color = this.heroColor();\n return m(\"header\", {\n className: classList('Hero', 'UserDirectoryHero', {\n 'UserDirectoryHero--colored': color,\n [textContrastClass(color)]: color\n }),\n style: color ? {\n '--hero-bg': color\n } : undefined\n }, m(\"div\", {\n className: \"container\"\n }, this.viewItems().toArray()));\n }\n viewItems() {\n const items = new ItemList();\n items.add('content', m(\"div\", {\n className: \"containerNarrow\"\n }, this.contentItems().toArray()), 80);\n return items;\n }\n contentItems() {\n const items = new ItemList();\n items.add('user-directory-title', m(\"h1\", {\n className: \"Hero-title\"\n }, m(Icon, {\n name: this.heroIcon()\n }), \" \", app.translator.trans('fof-user-directory.forum.hero.title')), 100);\n return items;\n }\n heroColor() {\n // Example return a color string to display a colored hero\n //return app.forum.attribute('themeSecondaryColor');\n return null;\n }\n heroIcon() {\n return 'far fa-address-book';\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/UserDirectoryHero', UserDirectoryHero);","import IndexSidebar from 'flarum/forum/components/IndexSidebar';\nimport PageStructure from 'flarum/forum/components/PageStructure';\nimport app from 'flarum/forum/app';\nimport Page from 'flarum/common/components/Page';\nimport ItemList from 'flarum/common/utils/ItemList';\nimport listItems from 'flarum/common/helpers/listItems';\nimport Select from 'flarum/common/components/Select';\nimport Button from 'flarum/common/components/Button';\nimport Dropdown from 'flarum/common/components/Dropdown';\nimport extractText from 'flarum/common/utils/extractText';\nimport UserDirectoryList from './UserDirectoryList';\nimport UserDirectoryState from '../states/UserDirectoryState';\nimport CheckableButton from './CheckableButton';\nimport SearchField from './SearchField';\nimport Separator from 'flarum/common/components/Separator';\nimport UserDirectoryHero from './UserDirectoryHero';\n\n/**\n * This page re-uses Flarum's IndexPage CSS classes\n */\nexport default class UserDirectoryPage extends Page {\n oninit(vnode) {\n super.oninit(vnode);\n this.state = new UserDirectoryState({});\n\n // Initialize the group filters before refreshing params\n this.enabledGroupFilters = [];\n this.enabledSpecialGroupFilters = {};\n\n // Extract group IDs from the query parameter\n // First check if we have preloaded data from the server\n const preloadedApiDocument = app.preloadedApiDocument();\n const preloadedData = preloadedApiDocument && preloadedApiDocument.payload && preloadedApiDocument.payload.fofUserDirectory;\n\n // Get query from preloaded data or URL parameter\n const q = preloadedData ? preloadedData.q : m.route.param('q') || '';\n if (q) {\n // Extract group filters\n const groupMatches = q.match(/\\bgroup:(\\d+)\\b/g);\n if (groupMatches) {\n this.enabledGroupFilters = groupMatches.map(match => match.replace('group:', ''));\n }\n\n // Extract special group filters\n if (app.initializers.has('flarum-suspend') && app.forum.attribute('hasSuspendPermission')) {\n if (q.includes('is:suspended')) {\n this.enabledSpecialGroupFilters['flarum-suspend'] = 'is:suspended';\n }\n }\n }\n\n // Now refresh params with the current URL parameters or preloaded data\n const params = {\n q: q,\n sort: preloadedData ? preloadedData.sort : m.route.param('sort')\n };\n this.state.refreshParams(params);\n this.bodyClass = 'User--directory';\n app.history.push('users', app.translator.trans('fof-user-directory.forum.header.back_to_user_directory_tooltip'));\n }\n oncreate(vnode) {\n super.oncreate(vnode);\n app.setTitle(extractText(app.translator.trans('fof-user-directory.forum.page.nav')));\n }\n view() {\n return m(PageStructure, {\n className: \"UserDirectoryPage\",\n hero: () => m(UserDirectoryHero, null),\n sidebar: () => m(IndexSidebar, null)\n }, m(\"div\", {\n className: \"IndexPage-toolbar\"\n }, m(\"ul\", {\n className: \"IndexPage-toolbar-view\"\n }, listItems(this.viewItems().toArray())), m(\"ul\", {\n className: \"IndexPage-toolbar-action\"\n }, listItems(this.actionItems().toArray()))), m(UserDirectoryList, {\n state: this.state\n }));\n }\n viewItems() {\n const items = new ItemList();\n const sortMap = this.state.sortMap();\n const sortOptions = {};\n for (const i in sortMap) {\n sortOptions[i] = app.translator.trans('fof-user-directory.lib.sort.' + i);\n }\n items.add('sort', Select.component({\n options: sortOptions,\n value: this.state.getParams().sort || app.forum.attribute('userDirectoryDefaultSort'),\n onchange: this.changeParams.bind(this)\n }), 100);\n items.add('filterGroups', Dropdown.component({\n caretIcon: 'fas fa-filter',\n label: app.translator.trans('fof-user-directory.forum.page.filter_button'),\n buttonClassName: 'Button',\n className: 'GroupFilterDropdown'\n }, this.groupItems().toArray()), 80);\n items.add('search', SearchField.component({\n state: this.state\n }), 60);\n return items;\n }\n groupItems() {\n const items = new ItemList();\n app.store.all('groups').filter(group => group.id() !== '2' && group.id() !== '3').forEach(group => {\n items.add(group.namePlural(), CheckableButton.component({\n className: 'GroupFilterButton',\n icon: group.icon(),\n checked: this.enabledGroupFilters.includes(group.id()),\n onclick: () => {\n const id = group.id();\n if (this.enabledGroupFilters.includes(id)) {\n this.enabledGroupFilters = this.enabledGroupFilters.filter(e => e != id);\n } else {\n this.enabledGroupFilters.push(id);\n // Empty the special group filters\n this.enabledSpecialGroupFilters = [];\n }\n this.changeParams(this.params().sort);\n }\n }, group.namePlural()));\n });\n if (app.initializers.has('flarum-suspend') && app.forum.attribute('hasSuspendPermission')) {\n items.add('suspend', CheckableButton.component({\n className: 'GroupFilterButton',\n icon: 'fas fa-ban',\n checked: this.enabledSpecialGroupFilters['flarum-suspend'] === 'is:suspended',\n onclick: () => {\n const id = 'flarum-suspend';\n if (this.enabledSpecialGroupFilters[id] === 'is:suspended') {\n this.enabledSpecialGroupFilters[id] = '';\n } else {\n this.enabledSpecialGroupFilters[id] = 'is:suspended';\n // Empty the group filters\n this.enabledGroupFilters = [];\n }\n this.changeParams(this.params().sort);\n }\n }, app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip')), 90);\n items.add('seperator', Separator.component(), 50);\n }\n return items;\n }\n actionItems() {\n const items = new ItemList();\n items.add('refresh', Button.component({\n title: app.translator.trans('fof-user-directory.forum.page.refresh_tooltip'),\n icon: 'fas fa-sync',\n className: 'Button Button--icon',\n onclick: () => {\n this.state.refresh();\n if (app.session.user) {\n app.store.find('users', app.session.user.id());\n m.redraw();\n }\n }\n }));\n return items;\n }\n\n /**\n * Redirect to the index page using the given sort parameter.\n *\n * @param {String} sort\n */\n changeParams(sort) {\n const params = this.params();\n if (sort === app.forum.attribute('userDirectoryDefaultSort')) {\n delete params.sort;\n } else {\n params.sort = sort;\n }\n\n // Build the query parameter\n let q = '';\n\n // Add special group filters\n for (const filter in this.enabledSpecialGroupFilters) {\n if (this.enabledSpecialGroupFilters[filter]) {\n q += this.enabledSpecialGroupFilters[filter] + ' ';\n }\n }\n\n // Add group filters\n if (this.enabledGroupFilters.length > 0) {\n this.enabledGroupFilters.forEach(groupId => {\n q += `group:${groupId} `;\n });\n }\n\n // Set the query parameter\n params.q = q.trim();\n\n // Remove qBuilder to avoid confusion\n delete params.qBuilder;\n\n // Update the state\n this.state.refreshParams(params);\n\n // Update the URL\n m.route.set(app.route('fof_user_directory', params));\n }\n stickyParams() {\n return {\n sort: m.route.param('sort'),\n q: m.route.param('q')\n };\n }\n params() {\n return this.stickyParams();\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/components/UserDirectoryPage', UserDirectoryPage);","function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.reg.get('core', 'common/Model');","import _defineProperty from \"@babel/runtime/helpers/esm/defineProperty\";\nimport Model from 'flarum/common/Model';\n\n/**\n * Special model used only client-side to hold a free text search value in the search field\n */\nexport default class Text extends Model {\n constructor() {\n super(...arguments);\n _defineProperty(this, \"text\", Model.attribute('text'));\n }\n}\nflarum.reg.add('fof-user-directory', 'forum/models/Text', Text);","import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };","import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };","import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };","import Extend from 'flarum/common/extenders';\nimport UserDirectoryPage from './components/UserDirectoryPage';\nimport Text from './models/Text';\nexport default [new Extend.Routes() //\n.add('fof_user_directory', '/users', UserDirectoryPage), new Extend.Store() //\n.add('fof-user-directory-text', Text)];","import app from 'flarum/forum/app';\nimport extendCommentPost from './extenders/extendCommentPost';\nimport extendUsersSearchSource from './extenders/extendUsersSearchSource';\nimport extendIndexPage from './extenders/extendIndexPage';\nexport { default as extend } from './extend';\napp.initializers.add('fof-user-directory', function () {\n extendCommentPost();\n extendUsersSearchSource();\n extendIndexPage();\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","reg","linkGroupMentions","attribute","this","$","each","hasClass","name","find","text","group","getBy","slice","link","q","id","on","e","m","route","set","getAttribute","preventDefault","addClass","wrap","extendCommentPost","extend","add","extendUsersSearchSource","view","query","toLowerCase","splice","className","href","icon","trans","extendIndexPage","items","SmallUserCard","infoItems","user","attrs","app","translator","ago","joinTime","UserDirectoryUserCard","super","has","setPriority","count","discussionCount","commentCount","UserDirectoryListItem","vnode","useSmallCards","attributes","controlsButtonClassName","component","UserDirectoryList","state","params","getParams","loading","isLoading","moreResults","onclick","loadMore","bind","empty","isSearchResults","users","map","SortMap","sortMap","username_az","username_za","newest","oldest","most_discussions","least_discussions","UserDirectoryState","constructor","window","qBuilder","requestParams","include","filter","sortKey","sort","forum","default","clear","redraw","refreshParams","newParams","hasUsers","keys","some","assign","values","join","trim","String","refresh","loadResults","then","results","parseResults","offset","preloadedUsers","preloadedApiDocument","Promise","resolve","page","store","length","push","payload","links","next","CheckableButton","getButtonContent","children","prev","checked","AbstractType","suggestions","resourceType","search","renderKind","resource","renderLabel","applyFilter","initializeFromParams","TextFilter","createRecord","split","word","indexOf","GroupFilter","all","forEach","nameSingular","namePlural","color","style","backgroundColor","groups","groupMatches","match","allGroupIds","replace","promises","Set","catch","error","console","SearchField","oninit","searchIndex","navigator","when","event","onUp","onDown","allSuggestions","onSelect","selectResult","applyFiltering","onRemove","appliedFilters","pop","availableFilters","filterTypes","toArray","focused","param","resources","recipient","index","title","searchResultKind","recipientLabel","placeholder","oninput","performNewSearch","onkeydown","navigate","onfocus","onblur","result","type","filterForResource","f","data","clearSuggestions","concat","warn","UserDirectoryHero","heroColor","undefined","viewItems","contentItems","heroIcon","UserDirectoryPage","enabledGroupFilters","enabledSpecialGroupFilters","preloadedData","fofUserDirectory","includes","bodyClass","oncreate","hero","sidebar","actionItems","sortOptions","i","options","onchange","changeParams","caretIcon","label","buttonClassName","groupItems","groupId","stickyParams","_typeof","iterator","Text","r","t","arguments","toPrimitive","TypeError","toPropertyKey","configurable","writable"],"sourceRoot":""} -------------------------------------------------------------------------------- /js/forum.ts: -------------------------------------------------------------------------------- 1 | export * from './src/forum'; 2 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@fof/user-directory", 4 | "prettier": "@flarum/prettier-config", 5 | "dependencies": { 6 | "@flarum/prettier-config": "^1.0.0", 7 | "flarum-tsconfig": "^2.0.0", 8 | "flarum-webpack-config": "^3.0.0", 9 | "webpack": "^5.65.0", 10 | "webpack-cli": "^5.0" 11 | }, 12 | "devDependencies": { 13 | "prettier": "^3.0.2" 14 | }, 15 | "scripts": { 16 | "dev": "webpack --mode development --watch", 17 | "build": "webpack --mode production", 18 | "format": "prettier --write src", 19 | "format-check": "prettier --check src" 20 | } 21 | } -------------------------------------------------------------------------------- /js/src/admin/extend.ts: -------------------------------------------------------------------------------- 1 | import Extend from 'flarum/common/extenders'; 2 | import app from 'flarum/admin/app'; 3 | import SortMap from '../common/utils/SortMap'; 4 | 5 | export default [ 6 | new Extend.Admin() 7 | .setting(() => ({ 8 | setting: 'fof-user-directory-link', 9 | label: app.translator.trans('fof-user-directory.admin.settings.link'), 10 | type: 'boolean', 11 | })) 12 | .setting(() => ({ 13 | setting: 'fof-user-directory.use-small-cards', 14 | label: app.translator.trans('fof-user-directory.admin.settings.use-small-cards'), 15 | type: 'boolean', 16 | })) 17 | .setting(() => ({ 18 | setting: 'fof-user-directory.disable-global-search-source', 19 | label: app.translator.trans('fof-user-directory.admin.settings.disable-global-search-source'), 20 | type: 'boolean', 21 | })) 22 | .setting(() => ({ 23 | setting: 'fof-user-directory.link-group-mentions', 24 | label: app.translator.trans('fof-user-directory.admin.settings.link-group-mentions'), 25 | type: 'boolean', 26 | })) 27 | .setting(() => { 28 | const sortOptions = { 29 | '': app.translator.trans('fof-user-directory.lib.sort.not_specified'), 30 | }; 31 | 32 | Object.keys(new SortMap().sortMap()).forEach((sort) => { 33 | sortOptions[sort] = app.translator.trans('fof-user-directory.lib.sort.' + sort); 34 | }); 35 | 36 | return { 37 | setting: 'fof-user-directory.default-sort', 38 | label: app.translator.trans('fof-user-directory.admin.settings.default-sort'), 39 | options: sortOptions, 40 | type: 'select', 41 | default: '', 42 | }; 43 | }) 44 | .permission( 45 | () => ({ 46 | icon: 'far fa-address-book', 47 | label: app.translator.trans('fof-user-directory.admin.permissions.view_user_directory'), 48 | permission: 'fof.user-directory.view', 49 | allowGuest: true, 50 | }), 51 | 'view' 52 | ), 53 | ]; 54 | -------------------------------------------------------------------------------- /js/src/admin/index.ts: -------------------------------------------------------------------------------- 1 | import app from 'flarum/admin/app'; 2 | import SortMap from '../common/utils/SortMap'; 3 | 4 | export { SortMap }; 5 | 6 | export { default as extend } from './extend'; 7 | 8 | app.initializers.add('fof-user-directory', () => { 9 | // 10 | }); 11 | -------------------------------------------------------------------------------- /js/src/common/utils/SortMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The sort options. 3 | * We use a class and not just a POJO/function because we want extensions to be able to extend it 4 | */ 5 | export default class SortMap { 6 | sortMap() { 7 | return { 8 | username_az: 'username', 9 | username_za: '-username', 10 | newest: '-joinedAt', 11 | oldest: 'joinedAt', 12 | most_discussions: '-discussionCount', 13 | least_discussions: 'discussionCount', 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /js/src/forum/components/CheckableButton.js: -------------------------------------------------------------------------------- 1 | import Button from 'flarum/common/components/Button'; 2 | import Icon from 'flarum/common/components/Icon'; 3 | 4 | export default class CheckableButton extends Button { 5 | /** 6 | * Get the template for the button's content. 7 | * 8 | * @return {*} 9 | * @protected 10 | */ 11 | getButtonContent(children) { 12 | const prev = super.getButtonContent(children); 13 | 14 | if (this.attrs.checked) prev.push(); 15 | 16 | return prev; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /js/src/forum/components/SearchField.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import Component from 'flarum/common/Component'; 3 | import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; 4 | import withAttr from 'flarum/common/utils/withAttr'; 5 | import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable'; 6 | import ItemList from 'flarum/common/utils/ItemList'; 7 | import TextFilter from '../searchTypes/TextFilter'; 8 | import GroupFilter from '../searchTypes/GroupFilter'; 9 | 10 | export default class SearchField extends Component { 11 | oninit(vnode) { 12 | super.oninit(vnode); 13 | 14 | this.searchIndex = 0; 15 | this.navigator = new KeyboardNavigatable(); 16 | this.navigator 17 | .when((event) => { 18 | // Do not handle keyboard when TAB is pressed and there's nothing in field 19 | // Without this it's impossible to TAB out of the field 20 | return event.key !== 'Tab' || !!this.filter; 21 | }) 22 | .onUp(() => { 23 | if (this.searchIndex > 0) { 24 | this.searchIndex--; 25 | m.redraw(); 26 | } 27 | }) 28 | .onDown(() => { 29 | if (this.searchIndex < this.allSuggestions().length - 1) { 30 | this.searchIndex++; 31 | m.redraw(); 32 | } 33 | }) 34 | .onSelect(() => { 35 | if (this.filter) { 36 | this.selectResult(this.allSuggestions()[this.searchIndex]); 37 | m.redraw(); 38 | } else { 39 | this.applyFiltering(); 40 | } 41 | }) 42 | .onRemove(() => { 43 | this.appliedFilters.pop(); 44 | }); 45 | 46 | this.availableFilters = this.filterTypes().toArray(); 47 | this.appliedFilters = []; 48 | 49 | this.filter = ''; 50 | this.focused = false; 51 | 52 | // When the page loads, initialize UI with filters from the parameters 53 | this.availableFilters.forEach((filter) => { 54 | filter 55 | .initializeFromParams({ 56 | sort: m.route.param('sort'), 57 | q: m.route.param('q'), 58 | }) 59 | .then((resources) => { 60 | this.appliedFilters.push(...resources); 61 | m.redraw(); 62 | }); 63 | }); 64 | } 65 | 66 | view() { 67 | const suggestions = this.allSuggestions(); 68 | 69 | const loading = this.availableFilters.some((filter) => filter.loading); 70 | 71 | return ( 72 |
73 | 124 |
125 | ); 126 | } 127 | 128 | filterTypes() { 129 | const items = new ItemList(); 130 | 131 | items.add('text', new TextFilter(), 10); 132 | items.add('group', new GroupFilter(), 20); 133 | 134 | return items; 135 | } 136 | 137 | filterForResource(resource) { 138 | return this.availableFilters.find((f) => f.resourceType() === resource.data.type); 139 | } 140 | 141 | recipientLabel(resource) { 142 | const filter = this.filterForResource(resource); 143 | 144 | if (filter) { 145 | return filter.renderLabel(resource); 146 | } 147 | 148 | return '[unknown]'; 149 | } 150 | 151 | searchResultKind(resource) { 152 | const filter = this.filterForResource(resource); 153 | 154 | if (filter) { 155 | return filter.renderKind(resource); 156 | } 157 | 158 | return '[unknown]'; 159 | } 160 | 161 | selectResult(result) { 162 | if (!result) { 163 | return; 164 | } 165 | 166 | this.appliedFilters.push(result); 167 | this.clearSuggestions(); 168 | } 169 | 170 | clearSuggestions() { 171 | this.filter = ''; 172 | this.availableFilters.forEach((filter) => { 173 | filter.search(''); 174 | }); 175 | } 176 | 177 | allSuggestions() { 178 | return [].concat(...this.availableFilters.map((filter) => filter.suggestions)); 179 | } 180 | 181 | performNewSearch() { 182 | this.searchIndex = 0; 183 | 184 | this.availableFilters.forEach((filter) => { 185 | filter.search(this.filter); 186 | }); 187 | 188 | this.attrs.state.refreshParams({ ...this.attrs.state.getParams(), qBuilder: this.qBuilder() }); 189 | } 190 | 191 | qBuilder(params = {}) { 192 | this.appliedFilters.forEach((resource) => { 193 | const filter = this.filterForResource(resource); 194 | 195 | if (filter) { 196 | filter.applyFilter(params, resource); 197 | } else { 198 | console.warn('Cannot find filter class for resource', resource); 199 | } 200 | }); 201 | return { filter: `${this.filter} ${params.q || ''}` }; 202 | } 203 | 204 | applyFiltering() { 205 | const params = { 206 | sort: m.route.param('sort'), 207 | }; 208 | 209 | this.qBuilder(params); 210 | 211 | m.route.set(app.route('fof_user_directory', params)); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /js/src/forum/components/SmallUserCard.js: -------------------------------------------------------------------------------- 1 | import UserCard from 'flarum/forum/components/UserCard'; 2 | import ItemList from 'flarum/common/utils/ItemList'; 3 | import humanTime from 'flarum/common/utils/humanTime'; 4 | 5 | export default class SmallUserCard extends UserCard { 6 | //Overriding infoItems so that other extensions can separately add items to small cards 7 | infoItems() { 8 | const items = new ItemList(); 9 | const user = this.attrs.user; 10 | 11 | items.add('joined', app.translator.trans('core.forum.user.joined_date_text', { ago: humanTime(user.joinTime()) })); 12 | 13 | return items; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /js/src/forum/components/UserDirectoryHero.tsx: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import Component from 'flarum/common/Component'; 3 | import Icon from 'flarum/common/components/Icon'; 4 | import textContrastClass from 'flarum/common/helpers/textContrastClass'; 5 | import classList from 'flarum/common/utils/classList'; 6 | import ItemList from 'flarum/common/utils/ItemList'; 7 | import type Mithril from 'mithril'; 8 | 9 | export default class UserDirectoryHero extends Component { 10 | view() { 11 | const color = this.heroColor(); 12 | 13 | return ( 14 |
18 |
{this.viewItems().toArray()}
19 |
20 | ); 21 | } 22 | 23 | viewItems(): ItemList { 24 | const items = new ItemList(); 25 | 26 | items.add('content',
{this.contentItems().toArray()}
, 80); 27 | 28 | return items; 29 | } 30 | 31 | contentItems(): ItemList { 32 | const items = new ItemList(); 33 | 34 | items.add( 35 | 'user-directory-title', 36 |

37 | {app.translator.trans('fof-user-directory.forum.hero.title')} 38 |

, 39 | 100 40 | ); 41 | 42 | return items; 43 | } 44 | 45 | heroColor(): string | null { 46 | // Example return a color string to display a colored hero 47 | //return app.forum.attribute('themeSecondaryColor'); 48 | return null; 49 | } 50 | 51 | heroIcon(): string { 52 | return 'far fa-address-book'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /js/src/forum/components/UserDirectoryList.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import Component from 'flarum/common/Component'; 3 | import Button from 'flarum/common/components/Button'; 4 | import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; 5 | import Placeholder from 'flarum/common/components/Placeholder'; 6 | import UserDirectoryListItem from './UserDirectoryListItem'; 7 | 8 | /** 9 | * Based on Flarum's DiscussionList 10 | */ 11 | export default class UserDirectoryList extends Component { 12 | view() { 13 | const { state } = this.attrs; 14 | 15 | const params = state.getParams(); 16 | const useSmallCards = app.forum.attribute('userDirectorySmallCards'); 17 | let loading; 18 | 19 | if (state.isLoading()) { 20 | loading = LoadingIndicator.component(); 21 | } else if (state.moreResults) { 22 | loading = Button.component( 23 | { 24 | className: 'Button', 25 | onclick: state.loadMore.bind(state), 26 | }, 27 | app.translator.trans('fof-user-directory.forum.page.load_more_button') 28 | ); 29 | } 30 | 31 | if (state.empty()) { 32 | const text = app.translator.trans('fof-user-directory.forum.page.empty_text'); 33 | return
{Placeholder.component({ text })}
; 34 | } 35 | 36 | return ( 37 |
44 |
    45 | {state.users.map((user) => { 46 | return ( 47 |
  • 48 | {UserDirectoryListItem.component({ user, params, useSmallCards })} 49 |
  • 50 | ); 51 | })} 52 |
53 |
{loading}
54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /js/src/forum/components/UserDirectoryListItem.js: -------------------------------------------------------------------------------- 1 | import Component from 'flarum/common/Component'; 2 | import UserCard from 'flarum/forum/components/UserCard'; 3 | import SmallUserCard from './SmallUserCard'; 4 | import UserDirectoryUserCard from './UserDirectoryUserCard'; 5 | 6 | export default class UserDirectoryListItem extends Component { 7 | view(vnode) { 8 | const { user, useSmallCards } = this.attrs; 9 | 10 | const attributes = { 11 | user, 12 | className: `UserCard--directory${useSmallCards ? ' UserCard--small' : ''}`, 13 | controlsButtonClassName: 'Button Button--icon Button--flat', 14 | }; 15 | 16 | return
{useSmallCards ? SmallUserCard.component(attributes) : UserDirectoryUserCard.component(attributes)}
; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /js/src/forum/components/UserDirectoryPage.js: -------------------------------------------------------------------------------- 1 | import IndexSidebar from 'flarum/forum/components/IndexSidebar'; 2 | import PageStructure from 'flarum/forum/components/PageStructure'; 3 | import app from 'flarum/forum/app'; 4 | import Page from 'flarum/common/components/Page'; 5 | import ItemList from 'flarum/common/utils/ItemList'; 6 | import listItems from 'flarum/common/helpers/listItems'; 7 | import Select from 'flarum/common/components/Select'; 8 | import Button from 'flarum/common/components/Button'; 9 | import Dropdown from 'flarum/common/components/Dropdown'; 10 | import extractText from 'flarum/common/utils/extractText'; 11 | import UserDirectoryList from './UserDirectoryList'; 12 | import UserDirectoryState from '../states/UserDirectoryState'; 13 | import CheckableButton from './CheckableButton'; 14 | import SearchField from './SearchField'; 15 | import Separator from 'flarum/common/components/Separator'; 16 | import UserDirectoryHero from './UserDirectoryHero'; 17 | 18 | /** 19 | * This page re-uses Flarum's IndexPage CSS classes 20 | */ 21 | export default class UserDirectoryPage extends Page { 22 | oninit(vnode) { 23 | super.oninit(vnode); 24 | 25 | this.state = new UserDirectoryState({}); 26 | 27 | // Initialize the group filters before refreshing params 28 | this.enabledGroupFilters = []; 29 | this.enabledSpecialGroupFilters = {}; 30 | 31 | // Extract group IDs from the query parameter 32 | // First check if we have preloaded data from the server 33 | const preloadedApiDocument = app.preloadedApiDocument(); 34 | const preloadedData = preloadedApiDocument && preloadedApiDocument.payload && preloadedApiDocument.payload.fofUserDirectory; 35 | 36 | // Get query from preloaded data or URL parameter 37 | const q = preloadedData ? preloadedData.q : m.route.param('q') || ''; 38 | 39 | if (q) { 40 | // Extract group filters 41 | const groupMatches = q.match(/\bgroup:(\d+)\b/g); 42 | if (groupMatches) { 43 | this.enabledGroupFilters = groupMatches.map((match) => match.replace('group:', '')); 44 | } 45 | 46 | // Extract special group filters 47 | if (app.initializers.has('flarum-suspend') && app.forum.attribute('hasSuspendPermission')) { 48 | if (q.includes('is:suspended')) { 49 | this.enabledSpecialGroupFilters['flarum-suspend'] = 'is:suspended'; 50 | } 51 | } 52 | } 53 | 54 | // Now refresh params with the current URL parameters or preloaded data 55 | const params = { 56 | q: q, 57 | sort: preloadedData ? preloadedData.sort : m.route.param('sort'), 58 | }; 59 | 60 | this.state.refreshParams(params); 61 | 62 | this.bodyClass = 'User--directory'; 63 | 64 | app.history.push('users', app.translator.trans('fof-user-directory.forum.header.back_to_user_directory_tooltip')); 65 | } 66 | 67 | oncreate(vnode) { 68 | super.oncreate(vnode); 69 | 70 | app.setTitle(extractText(app.translator.trans('fof-user-directory.forum.page.nav'))); 71 | } 72 | 73 | view() { 74 | return ( 75 | } sidebar={() => }> 76 |
77 |
    {listItems(this.viewItems().toArray())}
78 |
    {listItems(this.actionItems().toArray())}
79 |
80 | 81 |
82 | ); 83 | } 84 | 85 | viewItems() { 86 | const items = new ItemList(); 87 | const sortMap = this.state.sortMap(); 88 | 89 | const sortOptions = {}; 90 | for (const i in sortMap) { 91 | sortOptions[i] = app.translator.trans('fof-user-directory.lib.sort.' + i); 92 | } 93 | 94 | items.add( 95 | 'sort', 96 | Select.component({ 97 | options: sortOptions, 98 | value: this.state.getParams().sort || app.forum.attribute('userDirectoryDefaultSort'), 99 | onchange: this.changeParams.bind(this), 100 | }), 101 | 100 102 | ); 103 | 104 | items.add( 105 | 'filterGroups', 106 | Dropdown.component( 107 | { 108 | caretIcon: 'fas fa-filter', 109 | label: app.translator.trans('fof-user-directory.forum.page.filter_button'), 110 | buttonClassName: 'Button', 111 | className: 'GroupFilterDropdown', 112 | }, 113 | this.groupItems().toArray() 114 | ), 115 | 80 116 | ); 117 | 118 | items.add( 119 | 'search', 120 | SearchField.component({ 121 | state: this.state, 122 | }), 123 | 60 124 | ); 125 | 126 | return items; 127 | } 128 | 129 | groupItems() { 130 | const items = new ItemList(); 131 | 132 | app.store 133 | .all('groups') 134 | .filter((group) => group.id() !== '2' && group.id() !== '3') 135 | .forEach((group) => { 136 | items.add( 137 | group.namePlural(), 138 | CheckableButton.component( 139 | { 140 | className: 'GroupFilterButton', 141 | icon: group.icon(), 142 | checked: this.enabledGroupFilters.includes(group.id()), 143 | onclick: () => { 144 | const id = group.id(); 145 | if (this.enabledGroupFilters.includes(id)) { 146 | this.enabledGroupFilters = this.enabledGroupFilters.filter((e) => e != id); 147 | } else { 148 | this.enabledGroupFilters.push(id); 149 | // Empty the special group filters 150 | this.enabledSpecialGroupFilters = []; 151 | } 152 | 153 | this.changeParams(this.params().sort); 154 | }, 155 | }, 156 | group.namePlural() 157 | ) 158 | ); 159 | }); 160 | 161 | if (app.initializers.has('flarum-suspend') && app.forum.attribute('hasSuspendPermission')) { 162 | items.add( 163 | 'suspend', 164 | CheckableButton.component( 165 | { 166 | className: 'GroupFilterButton', 167 | icon: 'fas fa-ban', 168 | checked: this.enabledSpecialGroupFilters['flarum-suspend'] === 'is:suspended', 169 | onclick: () => { 170 | const id = 'flarum-suspend'; 171 | if (this.enabledSpecialGroupFilters[id] === 'is:suspended') { 172 | this.enabledSpecialGroupFilters[id] = ''; 173 | } else { 174 | this.enabledSpecialGroupFilters[id] = 'is:suspended'; 175 | // Empty the group filters 176 | this.enabledGroupFilters = []; 177 | } 178 | 179 | this.changeParams(this.params().sort); 180 | }, 181 | }, 182 | app.translator.trans('flarum-suspend.forum.user_badge.suspended_tooltip') 183 | ), 184 | 90 185 | ); 186 | 187 | items.add('seperator', Separator.component(), 50); 188 | } 189 | 190 | return items; 191 | } 192 | 193 | actionItems() { 194 | const items = new ItemList(); 195 | 196 | items.add( 197 | 'refresh', 198 | Button.component({ 199 | title: app.translator.trans('fof-user-directory.forum.page.refresh_tooltip'), 200 | icon: 'fas fa-sync', 201 | className: 'Button Button--icon', 202 | onclick: () => { 203 | this.state.refresh(); 204 | if (app.session.user) { 205 | app.store.find('users', app.session.user.id()); 206 | m.redraw(); 207 | } 208 | }, 209 | }) 210 | ); 211 | 212 | return items; 213 | } 214 | 215 | /** 216 | * Redirect to the index page using the given sort parameter. 217 | * 218 | * @param {String} sort 219 | */ 220 | changeParams(sort) { 221 | const params = this.params(); 222 | 223 | if (sort === app.forum.attribute('userDirectoryDefaultSort')) { 224 | delete params.sort; 225 | } else { 226 | params.sort = sort; 227 | } 228 | 229 | // Build the query parameter 230 | let q = ''; 231 | 232 | // Add special group filters 233 | for (const filter in this.enabledSpecialGroupFilters) { 234 | if (this.enabledSpecialGroupFilters[filter]) { 235 | q += this.enabledSpecialGroupFilters[filter] + ' '; 236 | } 237 | } 238 | 239 | // Add group filters 240 | if (this.enabledGroupFilters.length > 0) { 241 | this.enabledGroupFilters.forEach((groupId) => { 242 | q += `group:${groupId} `; 243 | }); 244 | } 245 | 246 | // Set the query parameter 247 | params.q = q.trim(); 248 | 249 | // Remove qBuilder to avoid confusion 250 | delete params.qBuilder; 251 | 252 | // Update the state 253 | this.state.refreshParams(params); 254 | 255 | // Update the URL 256 | m.route.set(app.route('fof_user_directory', params)); 257 | } 258 | 259 | stickyParams() { 260 | return { 261 | sort: m.route.param('sort'), 262 | q: m.route.param('q'), 263 | }; 264 | } 265 | 266 | params() { 267 | return this.stickyParams(); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /js/src/forum/components/UserDirectoryUserCard.js: -------------------------------------------------------------------------------- 1 | import UserCard from 'flarum/forum/components/UserCard'; 2 | import ItemList from 'flarum/common/utils/ItemList'; 3 | import humanTime from 'flarum/common/utils/humanTime'; 4 | import Icon from 'flarum/common/components/Icon'; 5 | import app from 'flarum/forum/app'; 6 | 7 | export default class UserDirectoryUserCard extends UserCard { 8 | /** 9 | * Allowing to add additonal items unique to the user directory. 10 | * 11 | * @return {ItemList} 12 | */ 13 | infoItems() { 14 | const items = super.infoItems(); 15 | const user = this.attrs.user; 16 | 17 | if (items.has('lastSeen')) items.setPriority('lastSeen', 100); 18 | if (items.has('joined')) items.setPriority('joined', 95); 19 | if (items.has('points')) items.setPriority('points', 60); 20 | if (items.has('best-answer-count')) items.setPriority('best-answer-count', 68); 21 | if (items.has('masquerade-bio')) items.setPriority('masquerade-bio', 50); 22 | 23 | items.add( 24 | 'discussion-count', 25 |
26 | 27 | {app.translator.trans('fof-user-directory.forum.page.usercard.discussion-count', { 28 | count: user.discussionCount(), 29 | })} 30 |
, 31 | 70 32 | ); 33 | 34 | items.add( 35 | 'comment-count', 36 |
37 | 38 | {app.translator.trans('fof-user-directory.forum.page.usercard.post-count', { 39 | count: user.commentCount(), 40 | })} 41 |
, 42 | 69 43 | ); 44 | 45 | return items; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /js/src/forum/extend.ts: -------------------------------------------------------------------------------- 1 | import Extend from 'flarum/common/extenders'; 2 | import UserDirectoryPage from './components/UserDirectoryPage'; 3 | import Text from './models/Text'; 4 | 5 | export default [ 6 | new Extend.Routes() // 7 | .add('fof_user_directory', '/users', UserDirectoryPage), 8 | 9 | new Extend.Store() // 10 | .add('fof-user-directory-text', Text), 11 | ]; 12 | -------------------------------------------------------------------------------- /js/src/forum/extenders/extendCommentPost.tsx: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import { extend } from 'flarum/common/extend'; 3 | import CommentPost from 'flarum/forum/components/CommentPost'; 4 | import Group from 'flarum/common/models/Group'; 5 | 6 | export const linkGroupMentions = function () { 7 | if (app.forum.attribute('canSeeUserDirectoryLink') && app.forum.attribute('userDirectoryLinkGroupMentions')) { 8 | // @ts-ignore 9 | this.$('.GroupMention').each(function () { 10 | // @ts-ignore 11 | if ($(this).hasClass('GroupMention--linked')) return; 12 | 13 | // @ts-ignore 14 | const name = $(this).find('.GroupMention-name').text(); 15 | const group = app.store.getBy('groups', 'namePlural', name.slice(1)); 16 | 17 | if (group) { 18 | const link = $(``); 19 | 20 | link.on('click', function (e) { 21 | // @ts-ignore 22 | m.route.set(this.getAttribute('href')); 23 | e.preventDefault(); 24 | }); 25 | 26 | // @ts-ignore 27 | $(this).addClass('GroupMention--linked').wrap(link); 28 | } 29 | }); 30 | } 31 | }; 32 | 33 | export default function extendCommentPost() { 34 | extend(CommentPost.prototype, 'oncreate', linkGroupMentions); 35 | extend(CommentPost.prototype, 'onupdate', linkGroupMentions); 36 | } 37 | -------------------------------------------------------------------------------- /js/src/forum/extenders/extendIndexPage.tsx: -------------------------------------------------------------------------------- 1 | import IndexSidebar from 'flarum/forum/components/IndexSidebar'; 2 | import app from 'flarum/forum/app'; 3 | import { extend } from 'flarum/common/extend'; 4 | import LinkButton from 'flarum/common/components/LinkButton'; 5 | 6 | export default function extendIndexPage() { 7 | extend(IndexSidebar.prototype, 'navItems', (items) => { 8 | if (app.forum.attribute('canSeeUserDirectoryLink') && app.forum.attribute('canSearchUsers')) { 9 | items.add( 10 | 'fof-user-directory', 11 | 12 | {app.translator.trans('fof-user-directory.forum.page.nav')} 13 | , 14 | 85 15 | ); 16 | } 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /js/src/forum/extenders/extendUsersSearchSource.tsx: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import { extend } from 'flarum/common/extend'; 3 | import UsersSearchSource from 'flarum/forum/components/UsersSearchSource'; 4 | import LinkButton from 'flarum/common/components/LinkButton'; 5 | 6 | export default function extendUsersSearchSource() { 7 | extend(UsersSearchSource.prototype, 'view', function (view, query: string) { 8 | if (!view || !app.forum.attribute('canSeeUserDirectoryLink') || app.forum.attribute('userDirectoryDisableGlobalSearchSource')) { 9 | return; 10 | } 11 | 12 | query = query.toLowerCase(); 13 | 14 | view.splice( 15 | 1, 16 | 0, 17 |
  • 18 | 19 | {app.translator.trans('fof-user-directory.forum.search.users_heading', { query })} 20 | 21 |
  • 22 | ); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /js/src/forum/index.ts: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import extendCommentPost from './extenders/extendCommentPost'; 3 | import extendUsersSearchSource from './extenders/extendUsersSearchSource'; 4 | import extendIndexPage from './extenders/extendIndexPage'; 5 | 6 | export { default as extend } from './extend'; 7 | 8 | app.initializers.add('fof-user-directory', function () { 9 | extendCommentPost(); 10 | extendUsersSearchSource(); 11 | extendIndexPage(); 12 | }); 13 | -------------------------------------------------------------------------------- /js/src/forum/models/Text.ts: -------------------------------------------------------------------------------- 1 | import Model from 'flarum/common/Model'; 2 | 3 | /** 4 | * Special model used only client-side to hold a free text search value in the search field 5 | */ 6 | export default class Text extends Model { 7 | text = Model.attribute('text'); 8 | } 9 | -------------------------------------------------------------------------------- /js/src/forum/searchTypes/AbstractType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @abstract 3 | */ 4 | export default class AbstractType { 5 | constructor() { 6 | this.suggestions = []; 7 | this.loading = false; 8 | } 9 | 10 | /** 11 | * The `type` property of the Models used in suggestions and applied filters for this type 12 | * @return {String} 13 | */ 14 | resourceType() { 15 | // 16 | } 17 | 18 | /** 19 | * Executed when the search query changes 20 | * The method should update this.suggestions with the new results 21 | * If asynchronous loading is used, this.loading should be set to true during the process 22 | * @param {String} query 23 | */ 24 | search(query) { 25 | // 26 | } 27 | 28 | /** 29 | * Renders the "kind" label next to the value indicating what kind of information that result is 30 | * Should probably just be a translated text 31 | * @param {Model} resource 32 | * @return {vnode} 33 | */ 34 | renderKind(resource) { 35 | // 36 | } 37 | 38 | /** 39 | * Renders the Label containing the suggestion's value 40 | * Should be a vdom template using the .UserDirectorySearchLabel class or similar 41 | * @param {Model} resource 42 | * @return {vnode} 43 | */ 44 | renderLabel(resource) { 45 | // 46 | } 47 | 48 | /** 49 | * Applies a filter on a params object to use in the page request 50 | * @param {Object} params Object. Might or might not contain a `q` property or `sort` property. In the future, `filters` object might be supported 51 | * @param {Model} resource 52 | */ 53 | applyFilter(params, resource) { 54 | // 55 | } 56 | 57 | /** 58 | * Used to populate the search field on page load with values from the querystring 59 | * A promise must be returned, and the UI will auto-update once the promise returns 60 | * @param {Object} params Object with a `q` and `sort` property. `filters` might be supported in the future 61 | * @return {Promise} 62 | */ 63 | initializeFromParams(params) { 64 | // 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /js/src/forum/searchTypes/GroupFilter.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import Group from 'flarum/common/models/Group'; 3 | import Icon from 'flarum/common/components/Icon'; 4 | import AbstractType from './AbstractType'; 5 | 6 | /* global m */ 7 | 8 | export default class GroupFilter extends AbstractType { 9 | resourceType() { 10 | return 'groups'; 11 | } 12 | 13 | search(query) { 14 | this.suggestions = []; 15 | 16 | if (!query) { 17 | return; 18 | } 19 | 20 | query = query.toLowerCase(); 21 | 22 | app.store.all('groups').forEach((group) => { 23 | // Do not allow Guest group as it wouldn't do anything 24 | if (group.id() === Group.GUEST_ID) { 25 | return; 26 | } 27 | 28 | if (group.nameSingular().toLowerCase().indexOf(query) !== -1 || group.namePlural().toLowerCase().indexOf(query) !== -1) { 29 | this.suggestions.push(group); 30 | } 31 | }); 32 | } 33 | 34 | renderKind() { 35 | return app.translator.trans('fof-user-directory.forum.search.kinds.group'); 36 | } 37 | 38 | renderLabel(group) { 39 | return m( 40 | '.UserDirectorySearchLabel', 41 | group.color() 42 | ? { 43 | className: 'colored', 44 | style: { 45 | backgroundColor: group.color(), 46 | }, 47 | } 48 | : {}, 49 | [ 50 | group.icon() 51 | ? [ 52 | Icon.component({ 53 | name: group.icon(), 54 | }), 55 | ' ', 56 | ] 57 | : null, 58 | group.namePlural(), 59 | ] 60 | ); 61 | } 62 | 63 | applyFilter(params, group) { 64 | params.q = params.q ? params.q + ' ' : ''; 65 | params.q += 'group:' + group.id(); 66 | } 67 | 68 | initializeFromParams(params) { 69 | if (!params.q) { 70 | return Promise.resolve([]); 71 | } 72 | 73 | const groups = []; 74 | 75 | // Extract all group: parameters from the query string 76 | const groupMatches = params.q.match(/\bgroup:(\d+)\b/g); 77 | 78 | if (!groupMatches || !groupMatches.length) { 79 | return Promise.resolve([]); 80 | } 81 | 82 | // Get all unique group IDs from all group: parameters 83 | const allGroupIds = []; 84 | groupMatches.forEach((match) => { 85 | const id = match.replace('group:', ''); 86 | allGroupIds.push(id); 87 | }); 88 | 89 | // Deduplicate group IDs 90 | const uniqueGroupIds = [...new Set(allGroupIds)]; 91 | 92 | // Load all group models 93 | const promises = uniqueGroupIds.map((id) => { 94 | return app.store 95 | .find('groups', id) 96 | .then((group) => { 97 | if (group) groups.push(group); 98 | return group; 99 | }) 100 | .catch((error) => { 101 | console.error('Error loading group:', id, error); 102 | return null; 103 | }); 104 | }); 105 | 106 | return Promise.all(promises).then(() => groups); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /js/src/forum/searchTypes/TextFilter.js: -------------------------------------------------------------------------------- 1 | import app from 'flarum/forum/app'; 2 | import AbstractType from './AbstractType'; 3 | 4 | /* global m */ 5 | 6 | export default class TextFilter extends AbstractType { 7 | resourceType() { 8 | return 'fof-user-directory-text'; 9 | } 10 | 11 | search(query) { 12 | if (!query) { 13 | this.suggestions = []; 14 | return; 15 | } 16 | 17 | this.suggestions = [ 18 | app.store.createRecord('fof-user-directory-text', { 19 | attributes: { 20 | text: query, 21 | }, 22 | }), 23 | ]; 24 | } 25 | 26 | renderKind() { 27 | return app.translator.trans('fof-user-directory.forum.search.kinds.text'); 28 | } 29 | 30 | renderLabel(resource) { 31 | return m('.UserDirectorySearchLabel', resource.text()); 32 | } 33 | 34 | applyFilter(params, resource) { 35 | params.q = params.q ? params.q + ' ' : ''; 36 | params.q += resource.text(); 37 | } 38 | 39 | initializeFromParams(params) { 40 | if (!params.q) { 41 | return Promise.resolve([]); 42 | } 43 | 44 | return Promise.resolve( 45 | params.q 46 | .split(' ') 47 | // Words with : are gambits and we will ignore them 48 | .filter((word) => word.indexOf(':') === -1) 49 | .map((word) => 50 | app.store.createRecord('fof-user-directory-text', { 51 | attributes: { 52 | text: word, 53 | }, 54 | }) 55 | ) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /js/src/forum/states/UserDirectoryState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on Flarum's DiscussionListState 3 | */ 4 | import SortMap from '../../common/utils/SortMap'; 5 | 6 | export default class UserDirectoryState { 7 | constructor(params = {}, app = window.app) { 8 | this.params = params; 9 | 10 | this.app = app; 11 | 12 | this.users = []; 13 | 14 | this.moreResults = false; 15 | 16 | this.loading = false; 17 | 18 | this.qBuilder = {}; 19 | } 20 | 21 | requestParams() { 22 | const params = { include: ['groups'], filter: {} }; 23 | 24 | const sortKey = this.params.sort || app.forum.attribute('userDirectoryDefaultSort'); 25 | 26 | // sort might be set to null if no sort params has been passed 27 | params.sort = this.sortMap()[sortKey]; 28 | 29 | if (this.params.q) { 30 | params.filter.q = this.params.q; 31 | } 32 | 33 | return params; 34 | } 35 | 36 | sortMap() { 37 | return { 38 | default: '', 39 | ...new SortMap().sortMap(), 40 | }; 41 | } 42 | 43 | getParams() { 44 | return this.params; 45 | } 46 | 47 | clear() { 48 | this.users = []; 49 | m.redraw(); 50 | } 51 | 52 | refreshParams(newParams) { 53 | if (!this.hasUsers() || Object.keys(newParams).some((key) => this.getParams()[key] !== newParams[key])) { 54 | this.params = newParams; 55 | 56 | // If we have a qBuilder, use it to build the query 57 | if (newParams.qBuilder) { 58 | Object.assign(this.qBuilder, newParams.qBuilder || {}); 59 | this.params.q = Object.values(this.qBuilder).join(' ').trim(); 60 | } 61 | 62 | // Make sure we have a query parameter 63 | if (!this.params.q) { 64 | this.params.q = ''; 65 | } else if (typeof this.params.q !== 'string') { 66 | // Ensure q is a string 67 | this.params.q = String(this.params.q); 68 | } 69 | 70 | this.refresh(); 71 | } 72 | } 73 | 74 | refresh() { 75 | this.loading = true; 76 | 77 | this.clear(); 78 | 79 | return this.loadResults().then( 80 | (results) => { 81 | this.users = []; 82 | this.parseResults(results); 83 | }, 84 | () => { 85 | this.loading = false; 86 | m.redraw(); 87 | } 88 | ); 89 | } 90 | 91 | loadResults(offset) { 92 | const preloadedUsers = this.app.preloadedApiDocument(); 93 | 94 | if (preloadedUsers) { 95 | return Promise.resolve(preloadedUsers); 96 | } 97 | 98 | const params = this.requestParams(); 99 | params.page = { offset }; 100 | params.include = params.include.join(','); 101 | 102 | return this.app.store.find('users', params); 103 | } 104 | 105 | loadMore() { 106 | this.loading = true; 107 | 108 | this.loadResults(this.users.length).then(this.parseResults.bind(this)); 109 | } 110 | 111 | parseResults(results) { 112 | this.users.push(...results); 113 | 114 | this.loading = false; 115 | this.moreResults = !!results.payload.links && !!results.payload.links.next; 116 | 117 | m.redraw(); 118 | 119 | return results; 120 | } 121 | 122 | hasUsers() { 123 | return this.users.length > 0; 124 | } 125 | 126 | isLoading() { 127 | return this.loading; 128 | } 129 | 130 | isSearchResults() { 131 | return !!this.params.q; 132 | } 133 | 134 | empty() { 135 | return !this.hasUsers() && !this.isLoading(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use Flarum's tsconfig as a starting point 3 | "extends": "flarum-tsconfig", 4 | // This will match all .ts, .tsx, .d.ts, .js, .jsx files 5 | "include": ["src/**/*"], 6 | "compilerOptions": { 7 | // This will output typings to `dist-typings` 8 | "declarationDir": "./dist-typings", 9 | "baseUrl": ".", 10 | "paths": { 11 | "flarum/*": ["../vendor/flarum/core/js/dist-typings/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('flarum-webpack-config')(); 2 | -------------------------------------------------------------------------------- /migrations/2019_06_10_000000_rename_permissions.php: -------------------------------------------------------------------------------- 1 | function (Builder $schema) { 17 | Permission::query() 18 | ->where('permission', 'flagrow.user-directory.view') 19 | ->update(['permission' => 'fof.user-directory.view']); 20 | }, 21 | ]; 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/flarum/phpstan/extension.neon 3 | 4 | parameters: 5 | # The level will be increased in Flarum 2.0 6 | level: 5 7 | paths: 8 | - extend.php 9 | - src 10 | excludePaths: 11 | - *.blade.php 12 | databaseMigrationsPath: ['migrations'] 13 | -------------------------------------------------------------------------------- /resources/less/components/SearchFiled.less: -------------------------------------------------------------------------------- 1 | // Style based on TagDiscussionModal.less from flarum/tags 2 | .UserDirectorySearchInput { 3 | padding-top: 0; 4 | padding-bottom: 0; 5 | margin-bottom: 0 !important; 6 | height: auto; 7 | min-height: 36px; // Same as .FormControl 8 | position: relative; 9 | 10 | display: grid !important; 11 | grid-template-columns: max-content 1fr; 12 | column-gap: 10px; 13 | 14 | @media @phone { 15 | margin-left: 0; 16 | } 17 | 18 | // Same background as .focus to give it better visibility 19 | background-color: var(--body-bg); 20 | color: var(--text-color); 21 | border-color: var(--control-bg); 22 | 23 | &:focus-within { 24 | border-color: var(--primary-color); 25 | } 26 | 27 | input { 28 | display: inline; 29 | outline: none; 30 | margin-top: -2px; 31 | margin-bottom: -2px; 32 | border: 0 !important; 33 | padding: 0; 34 | width: 100%; 35 | margin-right: -100%; 36 | background: transparent !important; 37 | } 38 | 39 | .LoadingIndicator { 40 | float: right; 41 | pointer-events: none; 42 | } 43 | 44 | .Dropdown-menu { 45 | position: absolute; 46 | top: 100%; 47 | left: 0; 48 | right: 0; 49 | display: block; 50 | } 51 | } 52 | 53 | .UserDirectorySearchInput-selected { 54 | display: flex; 55 | align-items: center; 56 | column-gap: 5px; 57 | 58 | &:empty { 59 | display: none; 60 | } 61 | 62 | .UserDirectorySearchInput-filter { 63 | cursor: not-allowed; 64 | } 65 | } 66 | 67 | .UserDirectorySearchLabel { 68 | font-size: 85%; 69 | font-weight: 600; 70 | display: inline-block; 71 | padding: 0.1em 0.5em; 72 | border-radius: var(--border-radius); 73 | background: var(--control-bg); 74 | color: var(--control-color); 75 | text-transform: none; 76 | 77 | &.colored { 78 | color: var(--body-bg) !important; 79 | } 80 | } 81 | 82 | .UserDirectorySearchKind { 83 | display: inline-block; 84 | min-width: 5em; 85 | color: #aaa; 86 | font-style: italic; 87 | } 88 | -------------------------------------------------------------------------------- /resources/less/components/UserDirectoryHero.less: -------------------------------------------------------------------------------- 1 | @media @tablet-up { 2 | .Hero.UserDirectoryHero { 3 | .container { 4 | padding-bottom: 40px; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /resources/less/components/list.less: -------------------------------------------------------------------------------- 1 | .UserDirectoryList { 2 | .UserDirectoryList-users { 3 | list-style: none; 4 | padding: 0; 5 | clear: both; 6 | } 7 | .User { 8 | margin-bottom: 1px; 9 | } 10 | 11 | .UserCard--directory { 12 | width: 100%; 13 | box-shadow: 0 2px 6px var(--shadow-color); 14 | margin-bottom: 10px; 15 | 16 | &, 17 | .darkenBackground { 18 | border-radius: var(--border-radius); 19 | } 20 | .container { 21 | width: auto !important; 22 | padding: 20px !important; 23 | } 24 | .UserCard-identity { 25 | font-size: 22px; 26 | } 27 | // Workaround for https://github.com/flarum/core/issues/1792 28 | .UserCard-controls { 29 | position: relative !important; 30 | z-index: auto; 31 | } 32 | .Button { 33 | @media @phone { 34 | color: #fff !important; 35 | } 36 | } 37 | 38 | @media @tablet-up { 39 | .Dropdown-menu { 40 | // for not overlapping header 41 | z-index: calc(~"var(--zindex-header) - 1"); 42 | } 43 | } 44 | } 45 | 46 | &-loadMore { 47 | text-align: center; 48 | } 49 | } 50 | 51 | @import "./smallCards"; 52 | -------------------------------------------------------------------------------- /resources/less/components/smallCards.less: -------------------------------------------------------------------------------- 1 | .UserDirectoryList { 2 | .UserCard--small { 3 | .container { 4 | padding: 8px !important; 5 | position: relative; 6 | } 7 | 8 | .UserCard-profile { 9 | padding-left: 0; 10 | grid-template-columns: 30px 1fr; 11 | gap: 10px; 12 | } 13 | 14 | .UserCard-identity { 15 | font-size: 14px; 16 | } 17 | 18 | .UserCard-avatar { 19 | margin-left: 0; 20 | margin-right: 10px; 21 | 22 | .Avatar { 23 | @avatar-size: 26px; 24 | width: @avatar-size; 25 | height: @avatar-size; 26 | line-height: @avatar-size; 27 | border-width: 2px; 28 | font-size: 14px; 29 | } 30 | } 31 | 32 | .UserCard-info { 33 | margin-left: 5px; 34 | 35 | & > li { 36 | display: block; 37 | } 38 | } 39 | 40 | @media @phone { 41 | .UserCard-avatar { 42 | margin: 0 auto 15px auto; 43 | 44 | .Avatar { 45 | @avatar-size: 36px; 46 | width: @avatar-size; 47 | height: @avatar-size; 48 | line-height: @avatar-size; 49 | font-size: 20px; 50 | } 51 | } 52 | 53 | .username { 54 | font-size: 16px; 55 | } 56 | 57 | .UserCard-badges { 58 | margin-left: 5px; 59 | margin-right: 0; 60 | } 61 | } 62 | 63 | @media (min-width: @screen-phone-max) { 64 | .UserCard-avatar { 65 | display: inline-block; 66 | } 67 | 68 | //Long usernames 69 | a { 70 | display: flex; 71 | white-space: nowrap; 72 | } 73 | 74 | .username { 75 | font-size: 18px; 76 | text-overflow: ellipsis; 77 | overflow: hidden; 78 | } 79 | } 80 | 81 | @media @tablet-up { 82 | .UserCard-badges { 83 | position: absolute; 84 | top: 0; 85 | left: 8px; 86 | 87 | .Badge { 88 | width: 18px; 89 | height: 18px; 90 | line-height: 16px; 91 | 92 | &-icon { 93 | font-size: 9px; 94 | } 95 | } 96 | } 97 | 98 | .UserCard-info { 99 | margin: 0; 100 | } 101 | } 102 | } 103 | 104 | &--small-cards { 105 | .UserDirectoryList-users { 106 | display: grid; 107 | //Equal Width Columns 108 | grid-template-columns: repeat(4, minmax(0, 1fr)); 109 | grid-column-gap: 10px; 110 | 111 | @media (max-width: @screen-tablet-max) { 112 | grid-template-columns: repeat(3, minmax(0, 1fr)); 113 | } 114 | 115 | @media @phone { 116 | grid-template-columns: repeat(2, minmax(0, 1fr)); 117 | } 118 | 119 | @media (max-width: 400px) { 120 | grid-template-columns: 1fr; 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /resources/less/components/toolbar.less: -------------------------------------------------------------------------------- 1 | .User--directory { 2 | .IndexPage-toolbar { 3 | display: flex; 4 | row-gap: 10px; 5 | 6 | .IndexPage-toolbar-view { 7 | display: flex; 8 | column-gap: 10px; 9 | row-gap: 5px; 10 | flex-grow: 1; 11 | 12 | > li { 13 | vertical-align: middle; 14 | width: fit-content; 15 | } 16 | } 17 | 18 | @media @phone { 19 | flex-direction: column; 20 | 21 | .item-search { 22 | flex: 0 1 100%; 23 | } 24 | 25 | .IndexPage-toolbar-view, 26 | .IndexPage-toolbar-action { 27 | display: flex; 28 | flex-wrap: wrap; 29 | } 30 | .IndexPage-toolbar-view { 31 | flex-wrap: wrap-reverse; 32 | justify-content: space-between; 33 | 34 | & > li { 35 | margin-right: 0; 36 | } 37 | } 38 | 39 | .IndexPage-toolbar-action { 40 | flex-shrink: 1; 41 | float: none; 42 | 43 | & > li { 44 | margin-left: 0; 45 | } 46 | li.item-refresh { 47 | margin-left: auto; 48 | } 49 | } 50 | } 51 | @media screen and (max-width: 370px) { 52 | .IndexPage-toolbar-view { 53 | & > li { 54 | width: 100%; 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | .item-filterGroups { 62 | .ButtonGroup { 63 | button { 64 | @media @phone { 65 | padding-top: 4px; 66 | } 67 | } 68 | } 69 | 70 | .GroupFilterButton { 71 | display: flex; 72 | 73 | .Button-label { 74 | width: 100%; 75 | } 76 | .ButtonCheck { 77 | margin-left: 10px; 78 | } 79 | } 80 | } 81 | 82 | @media @tablet-up { 83 | .IndexPage-toolbar-action { 84 | .item-clarkwinkelmann-mailing { 85 | .Button { 86 | & > .Button-icon { 87 | margin-right: 0; 88 | } 89 | & > .Button-label { 90 | display: none; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /resources/less/forum.less: -------------------------------------------------------------------------------- 1 | @import "components/toolbar"; 2 | @import "components/list"; 3 | @import "components/SearchFiled"; 4 | @import "components/UserDirectoryHero.less"; 5 | 6 | .UserCard-info { 7 | .item-discussion-stats { 8 | display: block; 9 | margin-top: 20px; 10 | } 11 | } 12 | 13 | .UserCard { 14 | .userStat { 15 | .icon { 16 | margin-right: 5px; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /resources/locale/en.yml: -------------------------------------------------------------------------------- 1 | fof-user-directory: 2 | forum: 3 | header: 4 | back_to_user_directory_tooltip: Back to User Directory 5 | 6 | hero: 7 | title: => fof-user-directory.forum.page.nav 8 | 9 | search: 10 | users_heading: 'Search all users for "{query}"' 11 | field: 12 | placeholder: Search all users 13 | kinds: 14 | group: Group 15 | text: Free text 16 | page: 17 | nav: User Directory 18 | refresh_tooltip: => core.forum.index.refresh_tooltip 19 | load_more_button: => core.ref.load_more 20 | empty_text: We could not find any user matching your search. 21 | filter_button: Filter Groups 22 | usercard: 23 | discussion-count: "{count, plural, one { {count} discussion} other {{count} discussions}}" 24 | post-count: "{count, plural, one { {count} post} other {{count} posts}}" 25 | 26 | admin: 27 | permissions: 28 | view_user_directory: View user directory 29 | settings: 30 | link: Add link on homepage for users able to see the directory 31 | default-sort: Default sort 32 | use-small-cards: Use small user cards 33 | disable-global-search-source: Do not add User Directory to the Flarum global search field 34 | link-group-mentions: Link group mentions in posts to the User Directory 35 | 36 | lib: 37 | sort: 38 | not_specified: Use Flarum default 39 | default: Default 40 | username_az: Username (a-z) 41 | username_za: Username (z-a) 42 | newest: Newest 43 | oldest: Oldest 44 | most_posts: Most posts 45 | least_posts: Least posts 46 | most_discussions: Most discussions 47 | least_discussions: Least discussions 48 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 4 |
    5 |

    {{ $translator->trans('fof-user-directory.forum.page.nav') }}

    6 | 7 |
      8 | @foreach ($apiDocument->data as $user) 9 |
    • 10 | {{ $user->attributes->username }} 11 |
    • 12 | @endforeach 13 |
    14 | 15 | {{ $translator->trans('core.views.index.next_page_button') }} » 16 |
    17 | -------------------------------------------------------------------------------- /src/Access/UserPolicy.php: -------------------------------------------------------------------------------- 1 | hasPermission('fof.user-directory.view') && $actor->hasPermission('searchUsers'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Api/PermissionBasedForumSettings.php: -------------------------------------------------------------------------------- 1 | get(fn ($forum, Context $context) => $context->getActor()->can('seeUserList') && $this->settings->get('fof-user-directory-link')), 31 | Schema\Str::make('userDirectoryDefaultSort') 32 | ->get(fn () => $this->settings->get('fof-user-directory.default-sort') ?: 'default'), 33 | Schema\Boolean::make('hasSuspendPermission') 34 | // Only serialize if the actor has permission 35 | ->visible(fn ($forum, Context $context) => $context->getActor()->hasPermission('user.suspend')) 36 | ->get(fn ($forum, Context $context) => $context->getActor()->hasPermission('user.suspend')), 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Content/UserDirectory.php: -------------------------------------------------------------------------------- 1 | 'username', 33 | 'username_za' => '-username', 34 | 'newest' => '-joinedAt', 35 | 'oldest' => 'joinedAt', 36 | 'most_discussions' => '-discussionCount', 37 | 'least_discussions' => 'discussionCount', 38 | ]; 39 | 40 | public function __construct( 41 | protected Client $api, 42 | protected Factory $view, 43 | protected SettingsRepositoryInterface $settings 44 | ) { 45 | } 46 | 47 | private function getDocument(User $actor, array $params, Request $request) 48 | { 49 | $actor->assertCan('seeUserList'); 50 | 51 | // Make sure groups are included in the API request 52 | if (!isset($params['include'])) { 53 | $params['include'] = 'groups'; 54 | } elseif (is_array($params['include'])) { 55 | if (!in_array('groups', $params['include'])) { 56 | $params['include'][] = 'groups'; 57 | } 58 | $params['include'] = implode(',', $params['include']); 59 | } elseif (is_string($params['include']) && !str_contains($params['include'], 'groups')) { 60 | $params['include'] .= ',groups'; 61 | } 62 | 63 | return json_decode($this->api->withQueryParams($params)->withParentRequest($request)->get('/users')->getBody()); 64 | } 65 | 66 | /** 67 | * @throws PermissionDeniedException 68 | */ 69 | public function __invoke(Document $document, Request $request): Document 70 | { 71 | $queryParams = $request->getQueryParams(); 72 | $actor = RequestUtil::getActor($request); 73 | 74 | $sort = Arr::pull($queryParams, 'sort') ?: $this->settings->get('fof-user-directory.default-sort'); 75 | $q = Arr::pull($queryParams, 'q'); 76 | $page = Arr::pull($queryParams, 'page', 1); 77 | 78 | // Ensure the query parameter is properly formatted 79 | if ($q) { 80 | // Make sure it's a string 81 | $q = (string) $q; 82 | } 83 | 84 | $params = [ 85 | // ?? used to prevent null values. null would result in the whole sortMap array being sent in the params 86 | 'sort' => Arr::get($this->sortMap, $sort ?? '', ''), 87 | 'filter' => compact('q'), 88 | 'page' => ['offset' => ($page - 1) * 20, 'limit' => 20], 89 | ]; 90 | 91 | $apiDocument = $this->getDocument($actor, $params, $request); 92 | 93 | $document->content = $this->view->make('fof.user-directory::index', compact('page', 'apiDocument')); 94 | 95 | $document->payload['apiDocument'] = $apiDocument; 96 | 97 | // Add query parameters to the payload so the frontend can initialize filters 98 | $document->payload['fofUserDirectory'] = [ 99 | 'q' => $q, 100 | 'sort' => $sort, 101 | ]; 102 | 103 | return $document; 104 | } 105 | } 106 | --------------------------------------------------------------------------------