diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index f0f5877..12544c3 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -4,10 +4,9 @@ on: [workflow_dispatch, push, pull_request] jobs: run: - uses: flarum/framework/.github/workflows/REUSABLE_backend.yml@1.x + uses: flarum/framework/.github/workflows/REUSABLE_backend.yml@2.x with: enable_backend_testing: true enable_phpstan: true - php_versions: '["8.0", "8.1", "8.2", "8.3", "8.4]' backend_directory: . diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index fc3d8e5..0cb769e 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch, push, pull_request] jobs: run: - uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@1.x + uses: flarum/framework/.github/workflows/REUSABLE_frontend.yml@2.x with: enable_bundlewatch: false enable_prettier: true @@ -13,7 +13,7 @@ jobs: frontend_directory: ./js backend_directory: . js_package_manager: yarn - main_git_branch: 1.x + main_git_branch: 2.x secrets: bundlewatch_github_token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} diff --git a/composer.json b/composer.json index c075b8c..f8b1ea8 100755 --- a/composer.json +++ b/composer.json @@ -20,8 +20,8 @@ } ], "require": { - "flarum/core": "^1.8.3", - "fof/follow-tags": "^1.1.4" + "flarum/core": "^2.0.0", + "fof/follow-tags": "^2.0.0" }, "authors": [ { @@ -60,11 +60,11 @@ } }, "require-dev": { - "flarum/phpstan": "*", - "flarum/testing": "*", - "flarum/approval": "*", - "fof/user-directory": "*", - "flarum/gdpr": "dev-main" + "flarum/phpstan": "^2.0.0", + "flarum/testing": "^2.0.0", + "flarum/approval": "^2.0.0", + "fof/user-directory": "^2.0.0", + "flarum/gdpr": "^2.0.0" }, "autoload-dev": { "psr-4": { @@ -88,5 +88,7 @@ "test:integration": "Runs all integration tests.", "test:setup": "Sets up a database for use with integration tests. Execute this only once.", "analyse:phpstan": "Run static analysis" - } + }, + "minimum-stability": "beta", + "prefer-stable": true } diff --git a/extend.php b/extend.php index 255b628..679b91e 100755 --- a/extend.php +++ b/extend.php @@ -12,20 +12,11 @@ namespace IanM\FollowUsers; -use Flarum\Api\Controller\ListUsersController; -use Flarum\Api\Controller\ShowForumController; -use Flarum\Api\Controller\ShowUserController; -use Flarum\Api\Serializer\BasicUserSerializer; -use Flarum\Api\Serializer\CurrentUserSerializer; -use Flarum\Api\Serializer\DiscussionSerializer; -use Flarum\Api\Serializer\UserSerializer; +use Flarum\Api\Endpoint; +use Flarum\Api\Resource; use Flarum\Discussion\Event as DiscussionEvent; -use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Extend; use Flarum\Gdpr\Extend\UserData; -use Flarum\Http\RequestUtil; -use Flarum\User\Event\Saving; -use Flarum\User\Filter\UserFilterer; use Flarum\User\Search\UserSearcher; use Flarum\User\User; @@ -47,57 +38,37 @@ ->namespace('ianm-follow-users', __DIR__.'/resources/views'), (new Extend\Notification()) - ->type(Notifications\NewFollowerBlueprint::class, BasicUserSerializer::class, ['alert']) - ->type(Notifications\NewUnfollowerBlueprint::class, BasicUserSerializer::class, ['alert']) - ->type(Notifications\NewDiscussionBlueprint::class, DiscussionSerializer::class, ['alert', 'email']) - ->type(Notifications\NewPostByUserBlueprint::class, DiscussionSerializer::class, ['alert', 'email']), + ->type(Notifications\NewFollowerBlueprint::class, ['alert']) + ->type(Notifications\NewUnfollowerBlueprint::class, ['alert']) + ->type(Notifications\NewDiscussionBlueprint::class, ['alert', 'email']) + ->type(Notifications\NewPostByUserBlueprint::class, ['alert', 'email']), (new Extend\Event()) - ->listen(Saving::class, Listeners\SaveFollowedToDatabase::class) ->listen(DiscussionEvent\Deleted::class, Listeners\DeleteNotificationWhenDiscussionIsHiddenOrDeleted::class) ->listen(DiscussionEvent\Hidden::class, Listeners\DeleteNotificationWhenDiscussionIsHiddenOrDeleted::class) ->listen(DiscussionEvent\Restored::class, Listeners\RestoreNotificationWhenDiscussionIsRestored::class) ->subscribe(Listeners\QueueNotificationJobs::class), - (new Extend\Filter(DiscussionFilterer::class)) - ->addFilter(Query\FollowUsersDiscussionFilter::class), - - (new Extend\Filter(UserFilterer::class)) - ->addFilter(Query\FollowedUsersFilterGambit::class), - - (new Extend\SimpleFlarumSearch(UserSearcher::class)) - ->addGambit(Query\FollowedUsersFilterGambit::class), - (new Extend\User()) ->registerPreference('blocksFollow', 'boolval', false), (new Extend\Policy()) ->modelPolicy(User::class, Access\UserPolicy::class), - (new Extend\ApiSerializer(CurrentUserSerializer::class)) - ->hasMany('followedUsers', UserSerializer::class), - - (new Extend\ApiSerializer(BasicUserSerializer::class)) - ->attributes(Api\AddBasicUserAttributes::class), - - (new Extend\ApiSerializer(UserSerializer::class)) - ->attributes(Api\AddUserAttributes::class), - - (new Extend\ApiController(ListUsersController::class)) - ->prepareDataForSerialization(function (ListUsersController $controller, $data, $request) { - $actor = RequestUtil::getActor($request); - $actor->load('followedUsers'); - - return $data; + // API Resource extensions (Flarum 2.x) + (new Extend\ApiResource(Resource\UserResource::class)) + ->fields(Api\UserResourceFields::class) + ->endpoint(Endpoint\Index::class, function (Endpoint\Index $endpoint) { + return $endpoint->addDefaultInclude(['followedUsers', 'followedBy']); }) - ->addInclude(['followedUsers', 'followedBy']), + ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) { + return $endpoint->addDefaultInclude(['followedUsers', 'followedBy']); + }), - (new Extend\ApiController(ShowUserController::class)) - ->prepareDataForSerialization(Api\LoadRelations::class) - ->addInclude(['followedUsers', 'followedBy']), - - (new Extend\ApiController(ShowForumController::class)) - ->addInclude('actor.followedUsers'), + (new Extend\ApiResource(Resource\ForumResource::class)) + ->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint) { + return $endpoint->addDefaultInclude(['actor.followedUsers']); + }), (new Extend\Settings()) ->default('ianm-follow-users.button-on-profile', false) @@ -110,4 +81,8 @@ (new UserData()) ->addType(Data\FollowUser::class), ]), + + (new Extend\SearchDriver(\Flarum\Search\Database\DatabaseSearchDriver::class)) + ->addFilter(\Flarum\Discussion\Search\DiscussionSearcher::class, Query\FollowUsersDiscussionFilter::class) + ->addFilter(UserSearcher::class, Query\FollowedUsersFilter::class), ]; diff --git a/js/package.json b/js/package.json index 0369f9c..27291f2 100755 --- a/js/package.json +++ b/js/package.json @@ -4,20 +4,27 @@ "private": true, "prettier": "@flarum/prettier-config", "dependencies": { - "flarum-webpack-config": "^2.0.0", + "css-what": "^5.1.0" + }, + "devDependencies": { + "prettier": "^3.0.2", "@flarum/prettier-config": "^1.0.0", - "flarum-tsconfig": "^1.0.2", - "css-what": "^5.1.0", - "webpack": "^5.89.0", - "webpack-cli": "^5.1.4" + "flarum-tsconfig": "^2.0.0", + "flarum-webpack-config": "^3.0.0", + "webpack": "^5.65.0", + "webpack-cli": "^5.0", + "typescript-coverage-report": "^0.6.1" }, "scripts": { "dev": "webpack --mode development --watch", "build": "webpack --mode production", + "analyze": "cross-env ANALYZER=true npm run build", "format": "prettier --write src", - "format-check": "prettier --check src" - }, - "devDependencies": { - "prettier": "^3.2.4" + "format-check": "prettier --check src", + "clean-typings": "npx rimraf dist-typings && mkdir dist-typings", + "build-typings": "npm run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && npm run post-build-typings", + "post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'", + "check-typings": "tsc --noEmit --emitDeclarationOnly false", + "check-typings-coverage": "typescript-coverage-report" } } diff --git a/js/src/@types/shims.d.ts b/js/src/@types/shims.d.ts index fe87424..6ae68ec 100644 --- a/js/src/@types/shims.d.ts +++ b/js/src/@types/shims.d.ts @@ -1,3 +1,5 @@ +import User from 'flarum/common/models/User'; + declare module 'flarum/common/models/User' { export default interface User { followed(): boolean; diff --git a/js/src/admin/extend.ts b/js/src/admin/extend.ts new file mode 100644 index 0000000..ade170e --- /dev/null +++ b/js/src/admin/extend.ts @@ -0,0 +1,28 @@ +import app from 'flarum/admin/app'; +import Extend from 'flarum/common/extenders'; +import commonExtend from '../common/extend'; + +export default [ + ...commonExtend, + + new Extend.Admin() // + .permission( + () => ({ + icon: 'fas fa-user-friends', + label: app.translator.trans('ianm-follow-users.admin.permissions.be_followed_label'), + permission: 'user.beFollowed', + }), + 'reply', + 95 + ) + .setting(() => ({ + label: app.translator.trans('ianm-follow-users.admin.settings.button-on-profile-label'), + type: 'bool', + setting: 'ianm-follow-users.button-on-profile', + })) + .setting(() => ({ + label: app.translator.trans('ianm-follow-users.admin.settings.stats-on-profile-label'), + type: 'bool', + setting: 'ianm-follow-users.stats-on-profile', + })), +]; diff --git a/js/src/admin/index.js b/js/src/admin/index.js deleted file mode 100755 index d79fbf8..0000000 --- a/js/src/admin/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import app from 'flarum/admin/app'; -import * as follow_tags from '@fof-follow-tags'; -import followingPageOptions from '../common/helpers/followingPageOptions'; - -app.initializers.add('ianm-follow-users', () => { - app.extensionData - .for('ianm-follow-users') - .registerPermission( - { - icon: 'fas fa-user-friends', - label: app.translator.trans('ianm-follow-users.admin.permissions.be_followed_label'), - permission: 'user.beFollowed', - }, - 'reply', - 95 - ) - .registerSetting({ - label: app.translator.trans('ianm-follow-users.admin.settings.button-on-profile-label'), - type: 'bool', - setting: 'ianm-follow-users.button-on-profile', - }) - .registerSetting({ - label: app.translator.trans('ianm-follow-users.admin.settings.stats-on-profile-label'), - type: 'bool', - setting: 'ianm-follow-users.stats-on-profile', - }); - - if (app.initializers.has('fof/follow-tags')) { - // Replace the original function with our customized version - follow_tags.utils.followingPageOptions = followingPageOptions; - // Execute the customized helper to cache the returned list of options - follow_tags.utils.followingPageOptions('admin.settings'); - } -}); diff --git a/js/src/admin/index.ts b/js/src/admin/index.ts new file mode 100755 index 0000000..fce6642 --- /dev/null +++ b/js/src/admin/index.ts @@ -0,0 +1,17 @@ +import app from 'flarum/admin/app'; +import addFollowingPageOption from 'ext:fof/follow-tags/common/utils/addFollowingPageOption'; + +export { default as extend } from './extend'; + +app.initializers.add( + 'ianm-follow-users', + () => { + if ('fof-follow-tags' in flarum.extensions) { + // Register our "users" option with fof-follow-tags + addFollowingPageOption(() => ({ + users: app.translator.trans('ianm-follow-users.lib.following_link'), + })); + } + }, + -10 // Run before fof-follow-tags so our option is registered before the cache is populated +); diff --git a/js/src/common/FollowLevels.js b/js/src/common/FollowLevels.js deleted file mode 100644 index b4bcbae..0000000 --- a/js/src/common/FollowLevels.js +++ /dev/null @@ -1,21 +0,0 @@ -import app from 'flarum/common/app'; - -const trans = (key) => (opts) => app.translator.trans(`ianm-follow-users.lib.follow_levels.${key}`, opts); - -export const FollowLevels = Object.freeze([ - { - value: 'unfollow', - name: trans('unfollow.name'), - description: trans('unfollow.description'), - }, - { - value: 'follow', - name: trans('follow.name'), - description: trans('follow.description'), - }, - { - value: 'lurk', - name: trans('lurk.name'), - description: trans('lurk.description'), - }, -]); diff --git a/js/src/common/FollowLevels.ts b/js/src/common/FollowLevels.ts new file mode 100644 index 0000000..3642749 --- /dev/null +++ b/js/src/common/FollowLevels.ts @@ -0,0 +1,33 @@ +import app from 'flarum/common/app'; + +type TranslatorParameters = Record; +type TranslationFunction = (opts?: TranslatorParameters) => any; + +const trans = + (key: string): TranslationFunction => + (opts) => + app.translator.trans(`ianm-follow-users.lib.follow_levels.${key}`, opts ?? {}); + +export interface FollowLevel { + value: string; + name: TranslationFunction; + description: TranslationFunction; +} + +export const FollowLevels: readonly FollowLevel[] = Object.freeze([ + { + value: 'unfollow', + name: trans('unfollow.name'), + description: trans('unfollow.description'), + }, + { + value: 'follow', + name: trans('follow.name'), + description: trans('follow.description'), + }, + { + value: 'lurk', + name: trans('lurk.name'), + description: trans('lurk.description'), + }, +]); diff --git a/js/src/common/extend.ts b/js/src/common/extend.ts new file mode 100644 index 0000000..54f5c29 --- /dev/null +++ b/js/src/common/extend.ts @@ -0,0 +1,7 @@ +import Extend from 'flarum/common/extenders'; +import FollowedUsersGambit from './gambits/FollowedUsersGambit'; + +export default [ + new Extend.Search() // + .gambit('users', FollowedUsersGambit), +]; diff --git a/js/src/common/gambits/FollowedUsersGambit.ts b/js/src/common/gambits/FollowedUsersGambit.ts new file mode 100644 index 0000000..2bdae86 --- /dev/null +++ b/js/src/common/gambits/FollowedUsersGambit.ts @@ -0,0 +1,12 @@ +import app from 'flarum/common/app'; +import { BooleanGambit } from 'flarum/common/query/IGambit'; + +export default class FollowedUsersGambit extends BooleanGambit { + key() { + return app.translator.trans('ianm-follow-users.lib.gambits.followeduser.key', {}, true); + } + + filterKey() { + return 'followeduser'; + } +} diff --git a/js/src/common/helpers/followingPageOptions.js b/js/src/common/helpers/followingPageOptions.ts similarity index 52% rename from js/src/common/helpers/followingPageOptions.js rename to js/src/common/helpers/followingPageOptions.ts index a24a8e9..78342fe 100644 --- a/js/src/common/helpers/followingPageOptions.js +++ b/js/src/common/helpers/followingPageOptions.ts @@ -1,22 +1,23 @@ import app from 'flarum/common/app'; -import * as follow_tags from '@fof-follow-tags'; +import followingPageOptionsOriginal from 'ext:fof/follow-tags/common/utils/followingPageOptions'; + +type FollowingPageOptions = { + [key: string]: string | any[]; +}; // We need to add options to the list of options available on the following page -// As `follow_tags.utils.followingPageOptions` is a function, we cannot really +// As `followingPageOptions` is a function, we cannot really // extend or override it with the Flarum helpers. // As the result of this function is cached after its first execution, // we can use the below version and execute this one to cache the desired options. -// Save the reference to the original function, as it will be overriden -const original = follow_tags.utils.followingPageOptions; - -// Customized version of the helper with addition options for followed users -export default (section) => { +// Customized version of the helper with additional options for followed users +export default function followingPageOptions(section: string): FollowingPageOptions { // Get the original options - const options = original(section); + const options = followingPageOptionsOriginal(section); options.users = app.translator.trans('ianm-follow-users.lib.following_link'); // Return the mutated options list return options; -}; +} diff --git a/js/src/forum/addFollowBadge.js b/js/src/forum/addFollowBadge.js deleted file mode 100644 index f3904f8..0000000 --- a/js/src/forum/addFollowBadge.js +++ /dev/null @@ -1,29 +0,0 @@ -import app from 'flarum/forum/app'; -import { extend } from 'flarum/common/extend'; -import Discussion from 'flarum/common/models/Discussion'; -import User from 'flarum/common/models/User'; -import Badge from 'flarum/common/components/Badge'; - -export default function addFollowBadge() { - extend(Discussion.prototype, 'badges', function (badges) { - if (this.user()?.followed?.()) { - badges.add( - 'user-following', - - ); - } - }); - - extend(User.prototype, 'badges', function (badges) { - if (this.followed()) { - badges.add( - 'user-following', - - ); - } - }); -} diff --git a/js/src/forum/addFollowBadge.tsx b/js/src/forum/addFollowBadge.tsx new file mode 100644 index 0000000..937fb43 --- /dev/null +++ b/js/src/forum/addFollowBadge.tsx @@ -0,0 +1,32 @@ +import app from 'flarum/forum/app'; +import { extend } from 'flarum/common/extend'; +import Discussion from 'flarum/common/models/Discussion'; +import User from 'flarum/common/models/User'; +import Badge from 'flarum/common/components/Badge'; +import ItemList from 'flarum/common/utils/ItemList'; +import type Mithril from 'mithril'; + +export default function addFollowBadge() { + extend(Discussion.prototype, 'badges', function (items: ItemList) { + const user = this.user(); + if (user && user instanceof User) { + const followedStatus = user.followed?.(); + if (followedStatus) { + items.add( + 'user-following', + + ); + } + } + }); + + extend(User.prototype, 'badges', function (items: ItemList) { + const followedStatus = this.followed?.(); + if (followedStatus) { + items.add( + 'user-following', + + ); + } + }); +} diff --git a/js/src/forum/addFollowControls.js b/js/src/forum/addFollowControls.tsx similarity index 66% rename from js/src/forum/addFollowControls.js rename to js/src/forum/addFollowControls.tsx index fdf82ee..d961d90 100644 --- a/js/src/forum/addFollowControls.js +++ b/js/src/forum/addFollowControls.tsx @@ -6,21 +6,23 @@ import { SelectFollowUserTypeModal } from './components/SelectFollowLevelModal'; import User from 'flarum/common/models/User'; import UserCard from 'flarum/forum/components/UserCard'; import { findFirstVdomChild } from './util/findVdomChild'; +import ItemList from 'flarum/common/utils/ItemList'; +import type Mithril from 'mithril'; /** * Opens the SelectFollowLevelModal with the provided user. - * - * @param {User} user */ -function openFollowLevelModal(user) { +function openFollowLevelModal(user: User) { if (!(user instanceof User)) return; - app.modal.show(SelectFollowUserTypeModal, { user }); + app.modal.show(SelectFollowUserTypeModal as any, { user }); } export default function addFollowControls() { - extend(UserControls, 'userControls', function (items, user) { - const followingBlockingUser = !user.canBeFollowed() && user.followed(); + // @ts-expect-error - extend typing doesn't handle static method signatures with multiple parameters well + extend(UserControls, 'userControls', function (items: ItemList, user: any, _isContextControls?: boolean) { + const typedUser = user as User; + const followingBlockingUser = !typedUser.canBeFollowed() && typedUser.followed(); const icon = 'fas fa-user-friends'; if (followingBlockingUser) { @@ -29,7 +31,7 @@ export default function addFollowControls() { ); }); - extend(UserCard.prototype, 'view', function (view) { + extend(UserCard.prototype, 'view', function (this: UserCard & { attrs: { user?: User } }, view: Mithril.Vnode) { const user = this.attrs.user; + if (!user) return; if ( !app.forum.attribute('ianm-follow-users.button-on-profile') || !app.session.user || app.session.user === user || !user.canBeFollowed() || - view.attrs.className.includes('UserCard--small') + (view.attrs as any)?.className?.includes('UserCard--small') ) { return; } @@ -78,7 +81,9 @@ export default function addFollowControls() { ); findFirstVdomChild(view, '.UserCard-profile', (vdom) => { - vdom.children.splice(2, 0, followButton); + if (Array.isArray(vdom.children)) { + vdom.children.splice(2, 0, followButton); + } }); }); } diff --git a/js/src/forum/addFollowingUsers.js b/js/src/forum/addFollowingUsers.js deleted file mode 100644 index 148ed9e..0000000 --- a/js/src/forum/addFollowingUsers.js +++ /dev/null @@ -1,61 +0,0 @@ -import app from 'flarum/forum/app'; -import { extend } from 'flarum/common/extend'; -import * as follow_tags from '@fof-follow-tags'; -import * as user_directory from '@fof-user-directory'; -import DiscussionListState from 'flarum/forum/states/DiscussionListState'; -import followingPageOptions from '../common/helpers/followingPageOptions'; -import Separator from 'flarum/common/components/Separator'; - -export default function () { - if (app.initializers.has('fof/follow-tags')) { - // Replace the original function with our customized version - follow_tags.utils.followingPageOptions = followingPageOptions; - // Execute the customized helper to cache the returned list of options - follow_tags.utils.followingPageOptions('forum.index.following'); - - extend(DiscussionListState.prototype, 'requestParams', function (params) { - if (!follow_tags.utils.isFollowingPage() || !app.session.user) return; - - if (!this.followTags) { - this.followTags = follow_tags.utils.getDefaultFollowingFiltering(); - } - - const followTags = this.followTags; - - if (app.current.get('routeName') === 'following' && followTags === 'users') { - params.filter['following-users'] = true; - - delete params.filter.subscription; - } - }); - } - - if (app.initializers.has('fof-user-directory')) { - extend(user_directory.UserDirectoryPage.prototype, 'groupItems', function (items) { - items.add( - 'follow-users', - user_directory.CheckableButton.component( - { - className: 'GroupFilterButton', - icon: 'fas fa-user-friends', - checked: this.enabledSpecialGroupFilters['ianm-follow-users'] === 'is:followeduser', - onclick: () => { - const id = 'ianm-follow-users'; - if (this.enabledSpecialGroupFilters[id] === 'is:followeduser') { - this.enabledSpecialGroupFilters[id] = ''; - } else { - this.enabledSpecialGroupFilters[id] = 'is:followeduser'; - } - - this.changeParams(this.params().sort); - }, - }, - app.translator.trans('ianm-follow-users.forum.filter.following') - ), - 65 - ); - - items.add('separator', , 50); - }); - } -} diff --git a/js/src/forum/addFollowingUsers.tsx b/js/src/forum/addFollowingUsers.tsx new file mode 100644 index 0000000..4eb7529 --- /dev/null +++ b/js/src/forum/addFollowingUsers.tsx @@ -0,0 +1,85 @@ +import app from 'flarum/forum/app'; +import { extend } from 'flarum/common/extend'; +import DiscussionListState from 'flarum/forum/states/DiscussionListState'; +import Separator from 'flarum/common/components/Separator'; +import ItemList from 'flarum/common/utils/ItemList'; +import type Mithril from 'mithril'; +import UserDirectoryPage from 'ext:fof/user-directory/forum/components/UserDirectoryPage'; +import CheckableButton from 'ext:fof/user-directory/forum/components/CheckableButton'; +import addFollowingPageOption from 'ext:fof/follow-tags/common/utils/addFollowingPageOption'; +import { getDefaultFollowingFiltering } from 'ext:fof/follow-tags/forum/utils/getDefaultFollowingFiltering'; + +export default function () { + // Register our "users" option with fof-follow-tags + addFollowingPageOption(() => ({ + users: app.translator.trans('ianm-follow-users.lib.following_link'), + })); + + extend( + DiscussionListState.prototype, + 'requestParams', + function (this: DiscussionListState & { followTags?: string | {} }, params: Record) { + // Check if we're on the following page (inline to avoid import issues) + const isFollowingPage = 'flarum-subscriptions' in flarum.extensions && m.route.get().includes(app.route('following')); + + if (!isFollowingPage || !app.session.user) return; + + if (!this.followTags) { + this.followTags = getDefaultFollowingFiltering(); + } + + const followTags = this.followTags; + + if (app.current.get('routeName') === 'following' && followTags === 'users') { + params.filter['following-users'] = true; + + delete params.filter.subscription; + } + } + ); + + if ('fof-user-directory' in flarum.extensions) { + // Initialize the filter state from URL parameters + extend(UserDirectoryPage.prototype, 'oninit', function (this: any) { + const q = m.route.param('q') || ''; + if (q.includes('is:followeduser')) { + if (!this.enabledSpecialGroupFilters) this.enabledSpecialGroupFilters = {}; + this.enabledSpecialGroupFilters['ianm-follow-users'] = 'is:followeduser'; + } + }); + + extend( + UserDirectoryPage.prototype, + 'groupItems', + function ( + this: typeof UserDirectoryPage.prototype & { enabledSpecialGroupFilters?: Record }, + items: ItemList + ) { + items.add( + 'follow-users', + { + const id = 'ianm-follow-users'; + if (!this.enabledSpecialGroupFilters) this.enabledSpecialGroupFilters = {}; + if (this.enabledSpecialGroupFilters[id] === 'is:followeduser') { + this.enabledSpecialGroupFilters[id] = ''; + } else { + this.enabledSpecialGroupFilters[id] = 'is:followeduser'; + } + + this.changeParams(this.params().sort); + }} + > + {app.translator.trans('ianm-follow-users.forum.filter.following')} + , + 65 + ); + + items.add('separator', , 50); + } + ); + } +} diff --git a/js/src/forum/addNotifictionSettings.ts b/js/src/forum/addNotifictionSettings.ts new file mode 100644 index 0000000..8052798 --- /dev/null +++ b/js/src/forum/addNotifictionSettings.ts @@ -0,0 +1,26 @@ +import { extend } from 'flarum/common/extend'; + +export default function addNotificationSettings() { + extend('flarum/forum/components/NotificationGrid', 'notificationTypes', function (items) { + items.add('newFollower', { + name: 'newFollower', + icon: 'fas fa-user-plus', + label: app.translator.trans('ianm-follow-users.forum.settings.notify_new_follower_label'), + }); + items.add('newUnfollower', { + name: 'newUnfollower', + icon: 'fas fa-user-minus', + label: app.translator.trans('ianm-follow-users.forum.settings.notify_new_unfollower_label'), + }); + items.add('newDiscussionByUser', { + name: 'newDiscussionByUser', + icon: 'fas fa-user-friends', + label: app.translator.trans('ianm-follow-users.forum.settings.notify_new_discussion_label'), + }); + items.add('newPostByUser', { + name: 'newPostByUser', + icon: 'fas fa-user-friends', + label: app.translator.trans('ianm-follow-users.forum.settings.notify_new_post_label'), + }); + }); +} diff --git a/js/src/forum/addPrivacySetting.js b/js/src/forum/addPrivacySetting.tsx similarity index 79% rename from js/src/forum/addPrivacySetting.js rename to js/src/forum/addPrivacySetting.tsx index 1cca7fd..18b2d13 100755 --- a/js/src/forum/addPrivacySetting.js +++ b/js/src/forum/addPrivacySetting.tsx @@ -1,15 +1,14 @@ import app from 'flarum/forum/app'; import { extend } from 'flarum/common/extend'; -import SettingsPage from 'flarum/forum/components/SettingsPage'; import Switch from 'flarum/common/components/Switch'; export default function () { - extend(SettingsPage.prototype, 'privacyItems', function (items) { + extend('flarum/forum/components/SettingsPage', 'privacyItems', function (items) { items.add( 'follow-users-block', { + onchange={(value: boolean) => { this.blocksFollowLoading = true; this.user.savePreferences({ blocksFollow: value }).then(() => { diff --git a/js/src/forum/addProfilePage.js b/js/src/forum/addProfilePage.tsx similarity index 100% rename from js/src/forum/addProfilePage.js rename to js/src/forum/addProfilePage.tsx diff --git a/js/src/forum/addUserCardStats.tsx b/js/src/forum/addUserCardStats.tsx index 9d887b2..48ad9d3 100644 --- a/js/src/forum/addUserCardStats.tsx +++ b/js/src/forum/addUserCardStats.tsx @@ -1,12 +1,13 @@ import { extend } from 'flarum/common/extend'; -import icon from 'flarum/common/helpers/icon'; +import Icon from 'flarum/common/components/Icon'; import ItemList from 'flarum/common/utils/ItemList'; import app from 'flarum/forum/app'; import UserCard from 'flarum/forum/components/UserCard'; import type Mithril from 'mithril'; +import type User from 'flarum/common/models/User'; export default function addUserCardStats() { - extend(UserCard.prototype, 'infoItems', function (items: ItemList) { + extend(UserCard.prototype, 'infoItems', function (this: UserCard & { attrs: { user?: User } }, items: ItemList) { if (!this.attrs.user || !app.forum.attribute('ianm-follow-users.stats-on-profile')) return; const user = this.attrs.user; @@ -17,7 +18,7 @@ export default function addUserCardStats() { 'followers',
- {icon('fas fa-user-friends')} + {followedUsersCount} {' '} {app.translator.trans('ianm-follow-users.forum.followed', { count: followedUsersCount })} {followersUsersCount}{' '} diff --git a/js/src/forum/components/FollowedUserListItem.js b/js/src/forum/components/FollowedUserListItem.tsx similarity index 56% rename from js/src/forum/components/FollowedUserListItem.js rename to js/src/forum/components/FollowedUserListItem.tsx index 4fd6ad9..f4adac6 100644 --- a/js/src/forum/components/FollowedUserListItem.js +++ b/js/src/forum/components/FollowedUserListItem.tsx @@ -1,8 +1,14 @@ import Component from 'flarum/common/Component'; import UserCard from 'flarum/forum/components/UserCard'; +import type User from 'flarum/common/models/User'; +import type Mithril from 'mithril'; -export default class FollowedUserListItem extends Component { - view() { +interface FollowedUserListItemAttrs { + user: User; +} + +export default class FollowedUserListItem extends Component { + view(): Mithril.Children { const { user } = this.attrs; return ( diff --git a/js/src/forum/components/NewDiscussionNotification.js b/js/src/forum/components/NewDiscussionNotification.tsx old mode 100755 new mode 100644 similarity index 59% rename from js/src/forum/components/NewDiscussionNotification.js rename to js/src/forum/components/NewDiscussionNotification.tsx index b520f2c..f64bb4a --- a/js/src/forum/components/NewDiscussionNotification.js +++ b/js/src/forum/components/NewDiscussionNotification.tsx @@ -1,26 +1,28 @@ import app from 'flarum/forum/app'; import Notification from 'flarum/forum/components/Notification'; +import type Mithril from 'mithril'; export default class NewDiscussionNotification extends Notification { - icon() { + icon(): string { return 'fas fa-user-friends'; } - href() { + href(): string { const notification = this.attrs.notification; const discussion = notification.subject(); - return app.route.discussion(discussion); + return app.route.discussion(discussion as any); } - content() { + content(): Mithril.Children { + const subject = this.attrs.notification.subject(); return app.translator.trans('ianm-follow-users.forum.notifications.new_discussion_text', { user: this.attrs.notification.fromUser(), - title: this.attrs.notification.subject().title(), + title: subject && typeof (subject as any).title === 'function' ? (subject as any).title() : '', }); } - excerpt() { + excerpt(): null { return null; } } diff --git a/js/src/forum/components/NewFollowerNotification.js b/js/src/forum/components/NewFollowerNotification.tsx old mode 100755 new mode 100644 similarity index 74% rename from js/src/forum/components/NewFollowerNotification.js rename to js/src/forum/components/NewFollowerNotification.tsx index 736428e..6d052af --- a/js/src/forum/components/NewFollowerNotification.js +++ b/js/src/forum/components/NewFollowerNotification.tsx @@ -1,25 +1,26 @@ import app from 'flarum/forum/app'; import Notification from 'flarum/forum/components/Notification'; +import type Mithril from 'mithril'; export default class NewFollowerNotification extends Notification { - icon() { + icon(): string { return 'fas fa-user-plus'; } - href() { + href(): string { const notification = this.attrs.notification; const user = notification.subject(); - return app.route.user(user); + return app.route.user(user as any); } - content() { + content(): Mithril.Children { return app.translator.trans('ianm-follow-users.forum.notifications.new_follower_text', { user: this.attrs.notification.fromUser(), }); } - excerpt() { + excerpt(): null { return null; } } diff --git a/js/src/forum/components/NewPostNotification.js b/js/src/forum/components/NewPostNotification.tsx old mode 100755 new mode 100644 similarity index 71% rename from js/src/forum/components/NewPostNotification.js rename to js/src/forum/components/NewPostNotification.tsx index 89f697c..1ed24d9 --- a/js/src/forum/components/NewPostNotification.js +++ b/js/src/forum/components/NewPostNotification.tsx @@ -1,26 +1,27 @@ import app from 'flarum/forum/app'; import Notification from 'flarum/forum/components/Notification'; +import type Mithril from 'mithril'; export default class NewPostNotification extends Notification { - icon() { + icon(): string { return 'fas fa-user-friends'; } - href() { + href(): string { const notification = this.attrs.notification; const discussion = notification.subject(); const content = notification.content() || {}; - return app.route.discussion(discussion, content.postNumber); + return app.route.discussion(discussion as any, (content as any).postNumber); } - content() { + content(): Mithril.Children { return app.translator.trans('ianm-follow-users.forum.notifications.new_post_text', { user: this.attrs.notification.fromUser(), }); } - excerpt() { + excerpt(): null { return null; } } diff --git a/js/src/forum/components/NewUnfollowerNotification.js b/js/src/forum/components/NewUnfollowerNotification.tsx old mode 100755 new mode 100644 similarity index 74% rename from js/src/forum/components/NewUnfollowerNotification.js rename to js/src/forum/components/NewUnfollowerNotification.tsx index 6f285b8..39b897d --- a/js/src/forum/components/NewUnfollowerNotification.js +++ b/js/src/forum/components/NewUnfollowerNotification.tsx @@ -1,25 +1,26 @@ import app from 'flarum/forum/app'; import Notification from 'flarum/forum/components/Notification'; +import type Mithril from 'mithril'; export default class NewUnfollowerNotification extends Notification { - icon() { + icon(): string { return 'fas fa-user-minus'; } - href() { + href(): string { const notification = this.attrs.notification; const user = notification.subject(); - return app.route.user(user); + return app.route.user(user as any); } - content() { + content(): Mithril.Children { return app.translator.trans('ianm-follow-users.forum.notifications.new_unfollower_text', { user: this.attrs.notification.fromUser(), }); } - excerpt() { + excerpt(): null { return null; } } diff --git a/js/src/forum/components/ProfilePage.js b/js/src/forum/components/ProfilePage.tsx old mode 100755 new mode 100644 similarity index 74% rename from js/src/forum/components/ProfilePage.js rename to js/src/forum/components/ProfilePage.tsx index f965e3e..12c608a --- a/js/src/forum/components/ProfilePage.js +++ b/js/src/forum/components/ProfilePage.tsx @@ -4,28 +4,33 @@ import { SelectFollowUserTypeModal } from './SelectFollowLevelModal'; import Placeholder from 'flarum/common/components/Placeholder'; import FollowedUserListItem from './FollowedUserListItem'; import LoadingIndicator from 'flarum/common/components/LoadingIndicator'; +import type User from 'flarum/common/models/User'; +import type Mithril from 'mithril'; export default class ProfilePage extends UserPage { - oninit(vnode) { + loading!: boolean; + followedUsers!: User[]; + + oninit(vnode: Mithril.Vnode) { super.oninit(vnode); this.refresh(); } - refresh() { + refresh(): void { this.loading = true; - this.loadUser(app.session.user.username()); - this.followedUsers = app.session.user.followedUsers(); + this.loadUser(app.session.user!.username()); + this.followedUsers = (app.session.user as any).followedUsers(); this.loading = false; m.redraw(); } - changeUserFollowOptions(user) { - app.modal.show(SelectFollowUserTypeModal, { user }); + changeUserFollowOptions(user: User): void { + app.modal.show(SelectFollowUserTypeModal as any, { user }); } - content() { + content(): Mithril.Children { if (this.loading) { return (
@@ -57,7 +62,7 @@ export default class ProfilePage extends UserPage { ); } - show() { + show(): void { this.user = app.session.user; m.redraw(); diff --git a/js/src/forum/components/SelectFollowLevelModal.js b/js/src/forum/components/SelectFollowLevelModal.tsx similarity index 67% rename from js/src/forum/components/SelectFollowLevelModal.js rename to js/src/forum/components/SelectFollowLevelModal.tsx index 10178d7..9630d56 100644 --- a/js/src/forum/components/SelectFollowLevelModal.js +++ b/js/src/forum/components/SelectFollowLevelModal.tsx @@ -1,50 +1,59 @@ import app from 'flarum/forum/app'; -import Modal from 'flarum/common/components/Modal'; +import FormModal from 'flarum/common/components/FormModal'; import User from 'flarum/common/models/User'; import Button from 'flarum/common/components/Button'; import Select from 'flarum/common/components/Select'; import { FollowLevels } from '../../common/FollowLevels'; +import type Mithril from 'mithril'; -export class SelectFollowUserTypeModal extends Modal { - state = { +interface SelectFollowUserTypeModalAttrs { + user: User; +} + +interface SelectFollowUserTypeModalState { + user: User | null; + saving: boolean; + followState: 'lurk' | 'follow' | 'unfollow' | undefined; +} + +// @ts-expect-error - FormModal attrs constraint issue +export class SelectFollowUserTypeModal extends FormModal { + // @ts-expect-error - Custom state structure + state: SelectFollowUserTypeModalState = { /** * User being followed - * - * @type User | null */ user: null, /** * Is the modal currently saving? - * - * @type boolean */ saving: false, /** * Currently selected follow level. * - * @type "lurk" | "follow" | "unfollow" * @example "lurk" */ followState: undefined, }; - oninit(vnode) { + oninit(vnode: Mithril.Vnode) { super.oninit(vnode); this.state.user = this.attrs.user; - this.state.followState = this.state.user.followed() || 'unfollow'; + const followedStatus = this.state.user.followed(); + this.state.followState = (followedStatus === true ? 'follow' : followedStatus) || 'unfollow'; } - className = () => 'iam_follow_users-selectFollowLevelModal'; + className = (): string => 'iam_follow_users-selectFollowLevelModal'; - title() { + title(): Mithril.Children { return this.trans('title', { username: {this.state.user?.displayName?.()} }); } - content() { + content(): Mithril.Children { // If `this.user` isn't a valid User, exit quickly to prevent complete forum errors. if (!(this.state.user instanceof User)) { // Show a more detailed error if this happens when the forum is in debug mode. @@ -57,9 +66,13 @@ export class SelectFollowUserTypeModal extends Modal { const user = this.state.user; - const availableLevelOptions = FollowLevels.reduce((acc, curr) => ({ ...acc, [curr.value]: curr.name() }), {}); + const availableLevelOptions = FollowLevels.reduce((acc, curr) => ({ ...acc, [curr.value]: curr.name() }), {} as Record); const selectedLevel = FollowLevels.find((l) => l.value === this.state.followState); + if (!selectedLevel) { + return null; + } + return (