Skip to content

Commit a635fc2

Browse files
committed
feat: messages anchor link
1 parent 863d652 commit a635fc2

File tree

14 files changed

+230
-29
lines changed

14 files changed

+230
-29
lines changed

extensions/messages/extend.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
->css(__DIR__.'/less/forum.less')
2525
->jsDirectory(__DIR__.'/js/dist/forum')
2626
->route('/messages', 'messages')
27-
->route('/messages/dialog/{id:\d+}', 'messages.dialog'),
27+
->route('/messages/dialog/{id:\d+}[/{near:\d+}]', 'messages.dialog'),
2828

2929
(new Extend\Frontend('admin'))
3030
->js(__DIR__.'/js/dist/admin.js')

extensions/messages/js/@types/shims.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import DialogListState from '../forum/states/DialogListState';
33

44
declare module 'flarum/forum/routes' {
55
export interface ForumRoutes {
6-
dialog: (tag: Dialog) => string;
6+
dialog: (dialog: Dialog, near?: number) => string;
77
}
88
}
99

extensions/messages/js/src/admin/extend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export default [
1313
allowGuest: false,
1414
}),
1515
'start',
16-
98
16+
95
1717
),
1818
];

extensions/messages/js/src/common/models/DialogMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import type Dialog from './Dialog';
55
import type User from 'flarum/common/models/User';
66

77
export default class DialogMessage extends Model {
8+
number() {
9+
return Model.attribute<number>('number').call(this);
10+
}
811
content() {
912
return Model.attribute<string | null | undefined>('content').call(this);
1013
}

extensions/messages/js/src/forum/components/DialogSection.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,27 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
2424
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
2525
super.oninit(vnode);
2626

27-
this.messages = new MessageStreamState({
27+
this.messages = new MessageStreamState(this.requestParams());
28+
29+
this.messages.refresh();
30+
}
31+
32+
requestParams(): any {
33+
const params: any = {
2834
filter: {
2935
dialog: this.attrs.dialog.id(),
3036
},
31-
sort: '-createdAt',
32-
});
37+
sort: '-number',
38+
};
3339

34-
this.messages.refresh();
40+
const near = m.route.param('near');
41+
42+
if (near) {
43+
params.page = params.page || {};
44+
params.page.near = parseInt(near);
45+
}
46+
47+
return params;
3548
}
3649

3750
view() {

extensions/messages/js/src/forum/components/Message.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,21 @@ export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessa
105105
const message = this.attrs.message;
106106

107107
items.add('user', <PostUser post={message} />, 100);
108-
items.add('meta', <PostMeta post={message} />);
108+
items.add(
109+
'meta',
110+
<PostMeta
111+
post={message}
112+
permalink={() => {
113+
const dialog = message.dialog();
114+
115+
if (!dialog) {
116+
return null;
117+
}
118+
119+
return app.forum.attribute('baseOrigin') + app.route.dialog(dialog, message.number());
120+
}}
121+
/>
122+
);
109123

110124
return items;
111125
}

extensions/messages/js/src/forum/components/MessageStream.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
135135

136136
messageItem(message: DialogMessage, index: number) {
137137
return (
138-
<div className="MessageStream-item" key={index} data-id={message.id()}>
138+
<div className="MessageStream-item" key={index} data-id={message.id()} data-number={message.number()}>
139139
{this.timeGap(message)}
140140
<Message message={message} />
141141
</div>
@@ -186,7 +186,22 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
186186
}
187187

188188
scrollToBottom() {
189-
this.element.scrollTop = this.element.scrollHeight;
189+
const near = m.route.param('near');
190+
191+
if (near) {
192+
const $message = this.element.querySelector(`.MessageStream-item[data-number="${near}"]`);
193+
194+
if ($message) {
195+
this.element.scrollTop = $message.getBoundingClientRect().top - this.element.getBoundingClientRect().top;
196+
197+
// pulsate the message
198+
$message.classList.add('flash');
199+
} else {
200+
this.element.scrollTop = this.element.scrollHeight;
201+
}
202+
} else {
203+
this.element.scrollTop = this.element.scrollHeight;
204+
}
190205
}
191206

192207
whileMaintainingScroll(callback: () => null | Promise<void>) {

extensions/messages/js/src/forum/extend.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export default [
99
new Extend.Routes() //
1010
.add('messages', '/messages', () => import('./components/MessagesPage'))
1111
.add('dialog', '/messages/dialog/:id', () => import('./components/MessagesPage'))
12-
.helper('dialog', (dialog: Dialog) => app.route('dialog', { id: dialog.id() })),
12+
.add('dialog', '/messages/dialog/:id/:near', () => import('./components/MessagesPage'))
13+
.helper('dialog', (dialog: Dialog, near?: number) => app.route('dialog', { id: dialog.id(), near: near })),
1314
];
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Flarum.
5+
*
6+
* For detailed copyright and license information, please view the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
use Illuminate\Database\Schema\Blueprint;
11+
use Illuminate\Database\Schema\Builder;
12+
13+
return [
14+
'up' => function (Builder $schema) {
15+
$schema->table('dialog_messages', function (Blueprint $table) {
16+
$table->unsignedBigInteger('number')->nullable()->after('content');
17+
});
18+
19+
$numbers = [];
20+
21+
$schema->getConnection()
22+
->table('dialogs')
23+
->orderBy('id')
24+
->each(function (object $dialog) use ($schema, &$numbers) {
25+
$numbers[$dialog->id] = 0;
26+
27+
$schema->getConnection()
28+
->table('dialog_messages')
29+
->where('dialog_id', $dialog->id)
30+
->orderBy('id')
31+
->each(function (object $message) use ($schema, &$numbers) {
32+
$schema->getConnection()
33+
->table('dialog_messages')
34+
->where('id', $message->id)
35+
->update(['number' => ++$numbers[$message->dialog_id]]);
36+
});
37+
38+
unset($numbers[$dialog->id]);
39+
});
40+
41+
$schema->table('dialog_messages', function (Blueprint $table) {
42+
$table->unsignedBigInteger('number')->nullable(false)->change();
43+
});
44+
},
45+
'down' => function (Builder $schema) {
46+
$schema->table('dialog_messages', function (Blueprint $table) {
47+
$table->dropColumn('number');
48+
});
49+
}
50+
];

extensions/messages/src/Api/Resource/DialogMessageResource.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
use Flarum\Messages\Dialog;
2525
use Flarum\Messages\DialogMessage;
2626
use Illuminate\Database\Eloquent\Builder;
27+
use Illuminate\Database\Query\Builder as QueryBuilder;
2728
use Illuminate\Support\Arr;
2829
use Illuminate\Support\Carbon;
2930
use Tobyz\JsonApiServer\Context as OriginalContext;
31+
use Tobyz\JsonApiServer\Exception\BadRequestException;
3032

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

9496
return [];
9597
})
98+
->extractOffset(function (Context $context, array $defaultExtracts): int {
99+
$queryParams = $context->request->getQueryParams();
100+
$near = intval(Arr::get($queryParams, 'page.near'));
101+
102+
if ($near > 1) {
103+
$filter = $defaultExtracts['filter'];
104+
$dialogId = $filter['dialog'] ?? null;
105+
106+
if (count($filter) > 1 || ! $dialogId || ($context->queryParam('sort') && $context->queryParam('sort') !== '-number')) {
107+
throw new BadRequestException(
108+
'You can only use page[near] with filter[dialog] and the default sort order'
109+
);
110+
}
111+
112+
$limit = $defaultExtracts['limit'];
113+
114+
// Change the offset to the one nearest to the message number.
115+
$index = DialogMessage::query()
116+
->select('row_index')
117+
->fromSub(function (QueryBuilder $query) use ($dialogId) {
118+
$query->select('number')
119+
->selectRaw('ROW_NUMBER() OVER (ORDER BY number DESC) AS row_index')
120+
->from('dialog_messages')
121+
->where('dialog_id', $dialogId);
122+
}, 'dialog_messages')
123+
->where('number', '<=', $near)
124+
->value('row_index');
125+
126+
return max(0, $index - $limit / 2);
127+
}
128+
129+
return $defaultExtracts['offset'];
130+
})
96131
->paginate(),
97132
];
98133
}
@@ -101,6 +136,7 @@ public function fields(): array
101136
{
102137
return [
103138

139+
Schema\Number::make('number'),
104140
Schema\Str::make('content')
105141
->requiredOnCreate()
106142
->writableOnCreate()
@@ -161,7 +197,7 @@ public function fields(): array
161197
public function sorts(): array
162198
{
163199
return [
164-
SortColumn::make('createdAt'),
200+
SortColumn::make('number'),
165201
];
166202
}
167203

extensions/messages/src/DialogMessage.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
use Flarum\User\User;
2222
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2323
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
24+
use Illuminate\Database\Query\Expression;
2425

2526
/**
2627
* @property int $id
2728
* @property int $dialog_id
2829
* @property int|null $user_id
2930
* @property string $content
31+
* @property int $number
3032
* @property \Carbon\Carbon $created_at
3133
* @property \Carbon\Carbon $updated_at
3234
* @property-read Dialog $dialog
@@ -48,6 +50,28 @@ class DialogMessage extends AbstractModel implements Formattable
4850

4951
protected $guarded = [];
5052

53+
protected $casts = [
54+
'dialog_id' => 'integer',
55+
'user_id' => 'integer',
56+
'number' => 'integer',
57+
];
58+
59+
public static function boot()
60+
{
61+
parent::boot();
62+
63+
static::creating(function (self $message) {
64+
$db = static::getConnectionResolver()->connection();
65+
66+
$message->number = new Expression('('.
67+
$db->table('dialog_messages', 'dm')
68+
->whereRaw($db->getTablePrefix().'dm.dialog_id = '.intval($message->dialog_id))
69+
->selectRaw('COALESCE(MAX('.$db->getTablePrefix().'dm.number), 0) + 1')
70+
->toSql()
71+
.')');
72+
});
73+
}
74+
5175
public function dialog(): BelongsTo
5276
{
5377
return $this->belongsTo(Dialog::class);

extensions/messages/tests/integration/api/ListTest.php

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ protected function setUp(): void
3939
['id' => 104, 'type' => 'direct'],
4040
],
4141
DialogMessage::class => [
42-
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!'],
43-
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!'],
44-
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!'],
45-
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!'],
46-
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
47-
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!'],
42+
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!', 'number' => 1],
43+
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!', 'number' => 2],
44+
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!', 'number' => 1],
45+
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!', 'number' => 2],
46+
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!', 'number' => 1],
47+
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!', 'number' => 2],
4848
],
4949
'dialog_user' => [
5050
['dialog_id' => 102, 'user_id' => 3, 'joined_at' => Carbon::now()],
@@ -125,4 +125,49 @@ public static function dialogMessagesAccessProvider(): array
125125
'Karlach can see messages in dialogs with Astarion and Gale' => [5, [104, 105, 106, 107]],
126126
];
127127
}
128+
129+
public function test_can_list_near_accessible_dialog_messages(): void
130+
{
131+
$messages = [];
132+
133+
for ($i = 1; $i <= 40; $i++) {
134+
$messages[] = ['id' => 200 + $i, 'dialog_id' => 200, 'user_id' => $i % 2 === 0 ? 3 : 4, 'content' => '<t>Hello, Gale!</t>', 'number' => $i];
135+
}
136+
137+
$this->prepareDatabase([
138+
Dialog::class => [
139+
['id' => 200, 'type' => 'direct'],
140+
],
141+
DialogMessage::class => $messages,
142+
'dialog_user' => [
143+
['dialog_id' => 200, 'user_id' => 3, 'joined_at' => Carbon::now()],
144+
['dialog_id' => 200, 'user_id' => 4, 'joined_at' => Carbon::now()],
145+
],
146+
]);
147+
148+
$this->database()->table('dialogs')->where('id', '!=', 200)->delete();
149+
$this->database()->table('dialog_messages')->where('dialog_id', '!=', 200)->delete();
150+
151+
$response = $this->send(
152+
$this->request('GET', '/api/dialog-messages', [
153+
'authenticatedAs' => 3,
154+
])->withQueryParams([
155+
'include' => 'dialog',
156+
'page' => ['near' => 10],
157+
'filter' => ['dialog' => 200],
158+
]),
159+
);
160+
161+
$json = $response->getBody()->getContents();
162+
$prettyJson = json_encode($json, JSON_PRETTY_PRINT);
163+
164+
$this->assertEquals(200, $response->getStatusCode(), $prettyJson);
165+
$this->assertJson($json);
166+
167+
$data = json_decode($json, true)['data'];
168+
$prettyJson = json_encode(json_decode($json), JSON_PRETTY_PRINT);
169+
170+
$this->assertEquals(40, $this->database()->table('dialog_messages')->count());
171+
$this->assertCount(19, $data, $prettyJson);
172+
}
128173
}

extensions/messages/tests/integration/api/dialog_messages/CreateTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ protected function setUp(): void
3636
['id' => 102, 'type' => 'direct'],
3737
],
3838
DialogMessage::class => [
39-
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
39+
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!', 'number' => 1],
4040
],
4141
'dialog_user' => [
4242
['dialog_id' => 102, 'user_id' => 4, 'joined_at' => Carbon::now()],

0 commit comments

Comments
 (0)