Skip to content

Commit

Permalink
feat: messages anchor link
Browse files Browse the repository at this point in the history
  • Loading branch information
SychO9 committed Jan 31, 2025
1 parent 863d652 commit a635fc2
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 29 deletions.
2 changes: 1 addition & 1 deletion extensions/messages/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
->css(__DIR__.'/less/forum.less')
->jsDirectory(__DIR__.'/js/dist/forum')
->route('/messages', 'messages')
->route('/messages/dialog/{id:\d+}', 'messages.dialog'),
->route('/messages/dialog/{id:\d+}[/{near:\d+}]', 'messages.dialog'),

(new Extend\Frontend('admin'))
->js(__DIR__.'/js/dist/admin.js')
Expand Down
2 changes: 1 addition & 1 deletion extensions/messages/js/@types/shims.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DialogListState from '../forum/states/DialogListState';

declare module 'flarum/forum/routes' {
export interface ForumRoutes {
dialog: (tag: Dialog) => string;
dialog: (dialog: Dialog, near?: number) => string;
}
}

Expand Down
2 changes: 1 addition & 1 deletion extensions/messages/js/src/admin/extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ export default [
allowGuest: false,
}),
'start',
98
95
),
];
3 changes: 3 additions & 0 deletions extensions/messages/js/src/common/models/DialogMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type Dialog from './Dialog';
import type User from 'flarum/common/models/User';

export default class DialogMessage extends Model {
number() {
return Model.attribute<number>('number').call(this);
}
content() {
return Model.attribute<string | null | undefined>('content').call(this);
}
Expand Down
21 changes: 17 additions & 4 deletions extensions/messages/js/src/forum/components/DialogSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,27 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
super.oninit(vnode);

this.messages = new MessageStreamState({
this.messages = new MessageStreamState(this.requestParams());

this.messages.refresh();
}

requestParams(): any {
const params: any = {
filter: {
dialog: this.attrs.dialog.id(),
},
sort: '-createdAt',
});
sort: '-number',
};

this.messages.refresh();
const near = m.route.param('near');

if (near) {
params.page = params.page || {};
params.page.near = parseInt(near);
}

return params;
}

view() {
Expand Down
16 changes: 15 additions & 1 deletion extensions/messages/js/src/forum/components/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,21 @@ export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessa
const message = this.attrs.message;

items.add('user', <PostUser post={message} />, 100);
items.add('meta', <PostMeta post={message} />);
items.add(
'meta',
<PostMeta
post={message}
permalink={() => {
const dialog = message.dialog();

if (!dialog) {
return null;
}

return app.forum.attribute('baseOrigin') + app.route.dialog(dialog, message.number());
}}
/>
);

return items;
}
Expand Down
19 changes: 17 additions & 2 deletions extensions/messages/js/src/forum/components/MessageStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia

messageItem(message: DialogMessage, index: number) {
return (
<div className="MessageStream-item" key={index} data-id={message.id()}>
<div className="MessageStream-item" key={index} data-id={message.id()} data-number={message.number()}>
{this.timeGap(message)}
<Message message={message} />
</div>
Expand Down Expand Up @@ -186,7 +186,22 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
}

scrollToBottom() {
this.element.scrollTop = this.element.scrollHeight;
const near = m.route.param('near');

if (near) {
const $message = this.element.querySelector(`.MessageStream-item[data-number="${near}"]`);

if ($message) {
this.element.scrollTop = $message.getBoundingClientRect().top - this.element.getBoundingClientRect().top;

// pulsate the message
$message.classList.add('flash');
} else {
this.element.scrollTop = this.element.scrollHeight;
}
} else {
this.element.scrollTop = this.element.scrollHeight;
}
}

whileMaintainingScroll(callback: () => null | Promise<void>) {
Expand Down
3 changes: 2 additions & 1 deletion extensions/messages/js/src/forum/extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ export default [
new Extend.Routes() //
.add('messages', '/messages', () => import('./components/MessagesPage'))
.add('dialog', '/messages/dialog/:id', () => import('./components/MessagesPage'))
.helper('dialog', (dialog: Dialog) => app.route('dialog', { id: dialog.id() })),
.add('dialog', '/messages/dialog/:id/:near', () => import('./components/MessagesPage'))
.helper('dialog', (dialog: Dialog, near?: number) => app.route('dialog', { id: dialog.id(), near: near })),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?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.
*/

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;

return [
'up' => function (Builder $schema) {
$schema->table('dialog_messages', function (Blueprint $table) {
$table->unsignedBigInteger('number')->nullable()->after('content');
});

$numbers = [];

$schema->getConnection()
->table('dialogs')
->orderBy('id')
->each(function (object $dialog) use ($schema, &$numbers) {
$numbers[$dialog->id] = 0;

$schema->getConnection()
->table('dialog_messages')
->where('dialog_id', $dialog->id)
->orderBy('id')
->each(function (object $message) use ($schema, &$numbers) {
$schema->getConnection()
->table('dialog_messages')
->where('id', $message->id)
->update(['number' => ++$numbers[$message->dialog_id]]);
});

unset($numbers[$dialog->id]);
});

$schema->table('dialog_messages', function (Blueprint $table) {
$table->unsignedBigInteger('number')->nullable(false)->change();
});
},
'down' => function (Builder $schema) {
$schema->table('dialog_messages', function (Blueprint $table) {
$table->dropColumn('number');
});
}
];
38 changes: 37 additions & 1 deletion extensions/messages/src/Api/Resource/DialogMessageResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
use Flarum\Messages\Dialog;
use Flarum\Messages\DialogMessage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Tobyz\JsonApiServer\Context as OriginalContext;
use Tobyz\JsonApiServer\Exception\BadRequestException;

/**
* @extends Resource\AbstractDatabaseResource<DialogMessage>
Expand Down Expand Up @@ -93,6 +95,39 @@ public function endpoints(): array

return [];
})
->extractOffset(function (Context $context, array $defaultExtracts): int {
$queryParams = $context->request->getQueryParams();
$near = intval(Arr::get($queryParams, 'page.near'));

if ($near > 1) {
$filter = $defaultExtracts['filter'];
$dialogId = $filter['dialog'] ?? null;

if (count($filter) > 1 || ! $dialogId || ($context->queryParam('sort') && $context->queryParam('sort') !== '-number')) {
throw new BadRequestException(
'You can only use page[near] with filter[dialog] and the default sort order'
);
}

$limit = $defaultExtracts['limit'];

// Change the offset to the one nearest to the message number.
$index = DialogMessage::query()
->select('row_index')
->fromSub(function (QueryBuilder $query) use ($dialogId) {
$query->select('number')
->selectRaw('ROW_NUMBER() OVER (ORDER BY number DESC) AS row_index')
->from('dialog_messages')
->where('dialog_id', $dialogId);
}, 'dialog_messages')
->where('number', '<=', $near)
->value('row_index');

return max(0, $index - $limit / 2);
}

return $defaultExtracts['offset'];
})
->paginate(),
];
}
Expand All @@ -101,6 +136,7 @@ public function fields(): array
{
return [

Schema\Number::make('number'),
Schema\Str::make('content')
->requiredOnCreate()
->writableOnCreate()
Expand Down Expand Up @@ -161,7 +197,7 @@ public function fields(): array
public function sorts(): array
{
return [
SortColumn::make('createdAt'),
SortColumn::make('number'),
];
}

Expand Down
24 changes: 24 additions & 0 deletions extensions/messages/src/DialogMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
use Flarum\User\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Query\Expression;

/**
* @property int $id
* @property int $dialog_id
* @property int|null $user_id
* @property string $content
* @property int $number
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read Dialog $dialog
Expand All @@ -48,6 +50,28 @@ class DialogMessage extends AbstractModel implements Formattable

protected $guarded = [];

protected $casts = [
'dialog_id' => 'integer',
'user_id' => 'integer',
'number' => 'integer',
];

public static function boot()
{
parent::boot();

static::creating(function (self $message) {
$db = static::getConnectionResolver()->connection();

$message->number = new Expression('('.

Check failure on line 66 in extensions/messages/src/DialogMessage.php

View workflow job for this annotation

GitHub Actions / run / PHPStan PHP 8.2

Property Flarum\Messages\DialogMessage::$number (int) does not accept Illuminate\Database\Query\Expression<string>.

Check failure on line 66 in extensions/messages/src/DialogMessage.php

View workflow job for this annotation

GitHub Actions / run / PHPStan PHP 8.3

Property Flarum\Messages\DialogMessage::$number (int) does not accept Illuminate\Database\Query\Expression<string>.

Check failure on line 66 in extensions/messages/src/DialogMessage.php

View workflow job for this annotation

GitHub Actions / run / PHPStan PHP 8.4

Property Flarum\Messages\DialogMessage::$number (int) does not accept Illuminate\Database\Query\Expression<string>.
$db->table('dialog_messages', 'dm')
->whereRaw($db->getTablePrefix().'dm.dialog_id = '.intval($message->dialog_id))
->selectRaw('COALESCE(MAX('.$db->getTablePrefix().'dm.number), 0) + 1')
->toSql()
.')');
});
}

public function dialog(): BelongsTo
{
return $this->belongsTo(Dialog::class);
Expand Down
57 changes: 51 additions & 6 deletions extensions/messages/tests/integration/api/ListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ protected function setUp(): void
['id' => 104, 'type' => 'direct'],
],
DialogMessage::class => [
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!'],
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!'],
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!'],
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!'],
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!'],
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!', 'number' => 1],
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!', 'number' => 2],
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!', 'number' => 1],
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!', 'number' => 2],
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!', 'number' => 1],
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!', 'number' => 2],
],
'dialog_user' => [
['dialog_id' => 102, 'user_id' => 3, 'joined_at' => Carbon::now()],
Expand Down Expand Up @@ -125,4 +125,49 @@ public static function dialogMessagesAccessProvider(): array
'Karlach can see messages in dialogs with Astarion and Gale' => [5, [104, 105, 106, 107]],
];
}

public function test_can_list_near_accessible_dialog_messages(): void
{
$messages = [];

for ($i = 1; $i <= 40; $i++) {
$messages[] = ['id' => 200 + $i, 'dialog_id' => 200, 'user_id' => $i % 2 === 0 ? 3 : 4, 'content' => '<t>Hello, Gale!</t>', 'number' => $i];
}

$this->prepareDatabase([
Dialog::class => [
['id' => 200, 'type' => 'direct'],
],
DialogMessage::class => $messages,
'dialog_user' => [
['dialog_id' => 200, 'user_id' => 3, 'joined_at' => Carbon::now()],
['dialog_id' => 200, 'user_id' => 4, 'joined_at' => Carbon::now()],
],
]);

$this->database()->table('dialogs')->where('id', '!=', 200)->delete();
$this->database()->table('dialog_messages')->where('dialog_id', '!=', 200)->delete();

$response = $this->send(
$this->request('GET', '/api/dialog-messages', [
'authenticatedAs' => 3,
])->withQueryParams([
'include' => 'dialog',
'page' => ['near' => 10],
'filter' => ['dialog' => 200],
]),
);

$json = $response->getBody()->getContents();
$prettyJson = json_encode($json, JSON_PRETTY_PRINT);

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

$data = json_decode($json, true)['data'];
$prettyJson = json_encode(json_decode($json), JSON_PRETTY_PRINT);

$this->assertEquals(40, $this->database()->table('dialog_messages')->count());
$this->assertCount(19, $data, $prettyJson);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protected function setUp(): void
['id' => 102, 'type' => 'direct'],
],
DialogMessage::class => [
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!', 'number' => 1],
],
'dialog_user' => [
['dialog_id' => 102, 'user_id' => 4, 'joined_at' => Carbon::now()],
Expand Down
Loading

0 comments on commit a635fc2

Please sign in to comment.