Skip to content
Merged
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
9 changes: 6 additions & 3 deletions extensions/lock/js/src/forum/addLockControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ export default function addLockControl() {
DiscussionControls.lockAction = function () {
this.save({ isLocked: !this.isLocked() }).then(() => {
if (app.current.matches(DiscussionPage)) {
app.current.get('stream').update();
app.current
.get('stream')
.update()
.then(() => m.redraw());
} else {
m.redraw();
}

m.redraw();
});
};
}
12 changes: 12 additions & 0 deletions extensions/sticky/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Flarum\Discussion\Discussion;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Extend;
use Flarum\Realtime\Extend\Realtime as RealtimeExtend;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\Sticky\Api\DiscussionResourceFields;
use Flarum\Sticky\Event\DiscussionWasStickied;
Expand Down Expand Up @@ -58,4 +59,15 @@
(new Extend\Settings())
->default('flarum-sticky.only_sticky_unread_discussions', true)
->serializeToForum('onlyStickyUnreadDiscussions', 'flarum-sticky.only_sticky_unread_discussions', 'boolval'),

(new Extend\Conditional())
->whenExtensionEnabled('flarum-realtime', fn () => [
(new RealtimeExtend())
->broadcastModelEvent(
[DiscussionWasStickied::class, DiscussionWasUnstickied::class],
fn ($event) => $event->discussion,
fn ($event) => $event->user,
'stickiedEvent'
),
]),
];
9 changes: 6 additions & 3 deletions extensions/sticky/js/src/forum/addStickyControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ export default function addStickyControl() {
DiscussionControls.stickyAction = function () {
this.save({ isSticky: !this.isSticky() }).then(() => {
if (app.current.matches(DiscussionPage)) {
app.current.get('stream').update();
app.current
.get('stream')
.update()
.then(() => m.redraw());
} else {
m.redraw();
}

m.redraw();
});
};
}
6 changes: 6 additions & 0 deletions extensions/sticky/js/src/forum/extendRealtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import app from 'flarum/forum/app';
import RealtimeExtend from 'ext:flarum/realtime/forum/extenders/Realtime';

export default function extendRealtime() {
new RealtimeExtend().onDiscussionStreamEvent('stickiedEvent').extend(app, { name: 'flarum-sticky', exports: {} });
}
7 changes: 7 additions & 0 deletions extensions/sticky/js/src/forum/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import addStickyBadge from './addStickyBadge';
import addStickyControl from './addStickyControl';
import addStickyExcerpt from './addStickyExcerpt';
import addStickyClass from './addStickyClass';
import extendRealtime from './extendRealtime';

export { default as extend } from './extend';

Expand All @@ -12,4 +13,10 @@ app.initializers.add('flarum-sticky', () => {
addStickyControl();
addStickyExcerpt();
addStickyClass();

// Register a discussion stream update event with flarum/realtime when enabled.
// Stickying or unstickying a discussion will trigger a DiscussionPage stream reload for other users.
if ('flarum-realtime' in flarum.extensions) {
extendRealtime();
}
});
5 changes: 3 additions & 2 deletions extensions/sticky/js/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
// and also tells your Typescript server to read core's global typings for
// access to `dayjs` and `$` in the global namespace.
"include": ["src/**/*", "../../../framework/core/js/dist-typings/@types/**/*", "@types/**/*"],
"include": ["src/**/*", "../../../framework/core/js/dist-typings/@types/**/*", "@types/**/*", "../../realtime/js/dist-typings/@types/**/*"],
"compilerOptions": {
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"ext:flarum/realtime/*": ["../../realtime/js/dist-typings/*"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,54 @@ public static function stickyDataProvider(): array
[3, false, false],
];
}

#[Test]
public function sticky_response_exposes_new_event_post_in_linkage()
{
// Discussion 2 starts un-stickied with one comment post (id 2). Stickying
// creates a `discussionStickied` event post via mergePost(). The PATCH
// response must surface that new post in the discussion's `posts`
// relationship linkage so the client can refresh its post stream without
// a full reload. See flarum/framework#TBD.
$response = $this->send(
$this->request('PATCH', '/api/discussions/2', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'isSticky' => true,
],
],
],
])
);

$body = $response->getBody()->getContents();
$json = json_decode($body, true);

$this->assertEquals(200, $response->getStatusCode(), $body);

$linkage = $json['data']['relationships']['posts']['data'] ?? null;
$this->assertIsArray($linkage, 'PATCH response must include the discussion posts relationship linkage');

$linkageIds = array_map(fn (array $entry) => $entry['id'], $linkage);

// The original comment post is still there.
$this->assertContains('2', $linkageIds, 'Linkage should still contain the original comment post id');

// And the newly-created event post is too — its id is whatever the next
// available id is after the fixture posts (1–4). The discussion gained
// exactly one post; assert the linkage grew by one.
$this->assertCount(2, $linkageIds, 'Linkage should contain the comment post and the new event post');

// Identify the new post id (the one that isn't '2') and assert it
// corresponds to a `discussionStickied` post in the included array.
$newPostId = array_values(array_diff($linkageIds, ['2']))[0] ?? null;
$this->assertNotNull($newPostId);

$stickiedRow = Post::query()->find($newPostId);
$this->assertNotNull($stickiedRow, 'New post in linkage should exist');
$this->assertEquals('discussionStickied', $stickiedRow->type);
$this->assertEquals(2, $stickiedRow->discussion_id);
}
}
12 changes: 12 additions & 0 deletions extensions/tags/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Flarum\Flags\Api\Resource\FlagResource;
use Flarum\Post\Filter\PostSearcher;
use Flarum\Post\Post;
use Flarum\Realtime\Extend\Realtime as RealtimeExtend;
use Flarum\Search\Database\DatabaseSearchDriver;
use Flarum\Tags\Access;
use Flarum\Tags\Api;
Expand Down Expand Up @@ -178,4 +179,15 @@ function (Endpoint\Index|Endpoint\Show|Endpoint\Create $endpoint) {
return $endpoint
->addDefaultInclude(['eventPostMentionsTags']);
}),

(new Extend\Conditional())
->whenExtensionEnabled('flarum-realtime', fn () => [
(new RealtimeExtend())
->broadcastModelEvent(
[DiscussionWasTagged::class],
fn ($event) => $event->discussion,
fn ($event) => $event->actor,
'taggedEvent'
),
]),
];
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@ export default class TagDiscussionModal extends TagSelectionModal<TagDiscussionM
if (discussion) {
discussion.save({ relationships: { tags } }).then(() => {
if (app.current.matches(DiscussionPage)) {
app.current.get('stream').update();
app.current
.get('stream')
.update()
.then(() => m.redraw());
} else {
m.redraw();
}

m.redraw();
});
}

Expand Down
6 changes: 6 additions & 0 deletions extensions/tags/js/src/forum/extendRealtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import app from 'flarum/forum/app';
import RealtimeExtend from 'ext:flarum/realtime/forum/extenders/Realtime';

export default function extendRealtime() {
new RealtimeExtend().onDiscussionStreamEvent('taggedEvent').extend(app, { name: 'flarum-tags', exports: {} });
}
7 changes: 7 additions & 0 deletions extensions/tags/js/src/forum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import addTagFilter from './addTagFilter';
import addTagLabels from './addTagLabels';
import addTagControl from './addTagControl';
import addTagComposer from './addTagComposer';
import extendRealtime from './extendRealtime';

export { default as extend } from './extend';

Expand All @@ -18,6 +19,12 @@ app.initializers.add('flarum-tags', () => {
addTagLabels();
addTagControl();
addTagComposer();

// Register a discussion stream update event with flarum/realtime when enabled.
// Tag changes will trigger a DiscussionPage stream reload for other users.
if ('flarum-realtime' in flarum.extensions) {
extendRealtime();
}
});

import './forum';
5 changes: 3 additions & 2 deletions extensions/tags/js/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
// and also tells your Typescript server to read core's global typings for
// access to `dayjs` and `$` in the global namespace.
"include": ["src/**/*", "../../../framework/core/js/dist-typings/@types/**/*", "@types/**/*"],
"include": ["src/**/*", "../../../framework/core/js/dist-typings/@types/**/*", "@types/**/*", "../../realtime/js/dist-typings/@types/**/*"],
"compilerOptions": {
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"ext:flarum/realtime/*": ["../../realtime/js/dist-typings/*"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,13 @@ export default class RenameDiscussionModal<
.save({ title })
.then(() => {
if (app.viewingDiscussion(this.discussion)) {
app.current.get('stream').update();
app.current
.get('stream')
.update()
.then(() => m.redraw());
} else {
m.redraw();
}
m.redraw();
this.hide();
})
.catch(() => {
Expand Down
12 changes: 12 additions & 0 deletions framework/core/src/Api/Resource/DiscussionResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ public function fields(): array
return $context->creating()
|| $context->getActor()->can('rename', $discussion);
})
->set(function (Discussion $discussion, string $value, Context $context) {
// On update, route through `rename()` so that `Renamed`
// fires — the listener creates the `discussionRenamed`
// event post and dispatches notifications. The default
// attribute setter would silently skip both.
if ($context->updating()) {
$discussion->rename($value);
} else {
$discussion->title = $value;
}
})
->minLength(3)
->maxLength(80),
Schema\Str::make('content')
Expand Down Expand Up @@ -207,6 +218,7 @@ public function fields(): array
->withLinkage(function (Context $context) {
return $context->showing(self::class)
|| $context->creating(self::class)
|| $context->updating(self::class)
|| $context->creating(PostResource::class);
})
->get(function (Discussion $discussion, Context $context) {
Expand Down
105 changes: 105 additions & 0 deletions framework/core/tests/integration/api/discussions/UpdateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?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\Tests\integration\api\discussions;

use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Post\Post;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Flarum\User\User;
use PHPUnit\Framework\Attributes\Test;

class UpdateTest extends TestCase
{
use RetrievesAuthorizedUsers;

protected function setUp(): void
{
parent::setUp();

$this->prepareDatabase([
User::class => [
$this->normalUser(),
],
Discussion::class => [
['id' => 1, 'title' => 'Original title', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1, 'last_post_number' => 1],
],
Post::class => [
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>first post</p></t>', 'number' => 1],
],
]);
}

#[Test]
public function renaming_creates_discussion_renamed_event_post(): void
{
$response = $this->send(
$this->request('PATCH', '/api/discussions/1', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'title' => 'Renamed title',
],
],
],
])
);

$body = $response->getBody()->getContents();
$this->assertEquals(200, $response->getStatusCode(), $body);

// Server-side: a `discussionRenamed` post row was inserted.
$renamedPost = Post::query()
->where('discussion_id', 1)
->where('type', 'discussionRenamed')
->first();

$this->assertNotNull($renamedPost, 'A discussionRenamed event post should be created when the title changes');

// Client-visible: the PATCH response carries the refreshed `posts`
// linkage with the new event post's id. Without this, frontend
// `stream.update()` can't surface the renamed event post.
$json = json_decode($body, true);

$linkage = $json['data']['relationships']['posts']['data'] ?? null;
$this->assertIsArray($linkage, 'PATCH response must include the discussion posts relationship linkage');

$linkageIds = array_map(fn (array $entry) => $entry['id'], $linkage);

$this->assertContains('1', $linkageIds, 'Linkage should still contain the original comment post id');
$this->assertContains((string) $renamedPost->id, $linkageIds, 'Linkage should contain the new event post id');
}

#[Test]
public function setting_title_to_same_value_creates_no_event_post(): void
{
$response = $this->send(
$this->request('PATCH', '/api/discussions/1', [
'authenticatedAs' => 1,
'json' => [
'data' => [
'attributes' => [
'title' => 'Original title',
],
],
],
])
);

$this->assertEquals(200, $response->getStatusCode());

$this->assertNull(
Post::query()->where('discussion_id', 1)->where('type', 'discussionRenamed')->first(),
'No discussionRenamed event post should be created when the title is unchanged'
);
}
}
Loading