Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions extensions/subscriptions/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Flarum\Post\Event\Restored;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\Subscriptions\Api\UserResourceFields;
use Flarum\Subscriptions\Extend\Subscription;
use Flarum\Subscriptions\Filter\SubscriptionFilter;
use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage;
use Flarum\Subscriptions\Listener;
Expand Down Expand Up @@ -64,6 +65,12 @@
->listen(Posted::class, Listener\FollowAfterReply::class)
->listen(Started::class, Listener\FollowAfterCreate::class),

// Register the built-in subscription types. Third-party extensions can add
// their own types via (new Flarum\Subscriptions\Extend\Subscription())->addSubscriptionType().
(new Subscription())
->addSubscriptionType('follow', ['follow', 'following', 'followed'])
->addSubscriptionType('ignore', ['ignore', 'ignoring', 'ignored']),

(new Extend\SearchDriver(DatabaseSearchDriver::class))
->addFilter(DiscussionSearcher::class, SubscriptionFilter::class)
->addMutator(DiscussionSearcher::class, HideIgnoredFromAllDiscussionsPage::class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,26 @@ export default class SubscriptionGambit extends BooleanGambit {
];
}

canonicalKey(): string[] {
return ['following', 'ignoring'];
}

toFilter(matches: string[], negate: boolean): Record<string, any> {
const key = (negate ? '-' : '') + this.filterKey();
const filterKey = (negate ? '-' : '') + this.filterKey();

// Map the matched surface keyword to the canonical internal DB value.
// This ensures SubscriptionFilter always receives 'follow' or 'ignore'
// regardless of which locale keyword was used.
const allFollowKeys = [
'following',
'followed',
app.translator.trans('flarum-subscriptions.lib.gambits.discussions.subscription.following_key', {}, true),
];

const value = allFollowKeys.includes(matches[1]) ? 'follow' : 'ignore';

return {
[key]: matches[1],
[filterKey]: value,
};
}

Expand All @@ -22,7 +37,12 @@ export default class SubscriptionGambit extends BooleanGambit {
}

fromFilter(value: string, negate: boolean): string {
return `${negate ? '-' : ''}is:${value}`;
const key = this.key();
// value is the canonical DB value ('follow' or 'ignore'); map back to
// the locale-appropriate surface keyword for display in the search bar.
const keyword = value === 'follow' ? key[0] : key[1];

return `${negate ? '-' : ''}is:${keyword}`;
}

enabled(): boolean {
Expand Down
61 changes: 61 additions & 0 deletions extensions/subscriptions/src/Extend/Subscription.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Subscriptions\Extend;

use Flarum\Extend\ExtenderInterface;
use Flarum\Extension\Extension;
use Illuminate\Contracts\Container\Container;

class Subscription implements ExtenderInterface
{
private array $types = [];

/**
* Register additional subscription type aliases accepted by the subscription filter.
*
* The canonical value is the string stored in the `discussion_user.subscription`
* column. The aliases are the surface-level values the frontend may send as the
* `filter[subscription]` parameter — typically the canonical value itself plus
* any locale-neutral keywords you want to accept (e.g. verb forms).
*
* Example — adding a "lurk" subscription type:
*
* ```php
* (new Extend\Subscription())
* ->addSubscriptionType('lurk', ['lurk', 'lurking', 'lurked'])
* ```
*
* @param string $canonicalValue The value stored in the database.
* @param string|string[] $aliases One or more filter values that map to this canonical value.
*/
public function addSubscriptionType(string $canonicalValue, string|array $aliases): self
{
$this->types[$canonicalValue] = array_merge(
$this->types[$canonicalValue] ?? [],
(array) $aliases,
);

return $this;
}

public function extend(Container $container, ?Extension $extension = null): void
{
// Ensure the registry exists before extending it.
$container->bindIf('flarum-subscriptions.subscription_types', fn () => []);

$container->extend('flarum-subscriptions.subscription_types', function (array $types) {
foreach ($this->types as $canonical => $aliases) {
$types[$canonical] = array_unique(array_merge($types[$canonical] ?? [], $aliases));
}

return $types;
});
}
}
41 changes: 36 additions & 5 deletions extensions/subscriptions/src/Filter/SubscriptionFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Flarum\Search\SearchState;
use Flarum\Search\ValidateFilterTrait;
use Flarum\User\User;
use Illuminate\Contracts\Container\Container;
use Illuminate\Database\Eloquent\Builder;

/**
Expand All @@ -23,6 +24,11 @@ class SubscriptionFilter implements FilterInterface
{
use ValidateFilterTrait;

public function __construct(
private readonly Container $container,
) {
}

public function getFilterKey(): string
{
return 'subscription';
Expand All @@ -32,19 +38,44 @@ public function filter(SearchState $state, string|array $value, bool $negate): v
{
$value = $this->asString($value);

preg_match('/^(follow|ignor)(?:ing|ed)$/i', $value, $matches);
$canonicalValue = $this->resolveCanonicalValue($value);

if ($canonicalValue === null) {
// Unrecognised value — match nothing rather than everything.
$state->getQuery()->whereRaw('0 = 1');

return;
}

$this->constrain($state->getQuery(), $state->getActor(), $canonicalValue, $negate);
}

/**
* Resolve a filter value to its canonical database value using the
* registered subscription type registry. Returns null if unrecognised.
*/
protected function resolveCanonicalValue(string $value): ?string
{
/** @var array<string, string[]> $types */
$types = $this->container->make('flarum-subscriptions.subscription_types');

foreach ($types as $canonical => $aliases) {
if (in_array($value, $aliases, true)) {
return $canonical;
}
}

$this->constrain($state->getQuery(), $state->getActor(), $matches[1], $negate);
return null;
}

protected function constrain(Builder $query, User $actor, string $subscriptionType, bool $negate): void
{
$method = $negate ? 'whereNotIn' : 'whereIn';
$query->$method('discussions.id', function ($query) use ($actor, $subscriptionType) {
$query->select('discussion_id')
->from('discussion_user')
->where('user_id', $actor->id)
->where('subscription', $subscriptionType === 'follow' ? 'follow' : 'ignore');
->from('discussion_user')
->where('user_id', $actor->id)
->where('subscription', $subscriptionType);
});
}
}
Loading
Loading