Skip to content

Commit ea089c1

Browse files
committed
chore: improvements
1 parent 9733081 commit ea089c1

File tree

7 files changed

+102
-44
lines changed

7 files changed

+102
-44
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
2929
this.messages.refresh();
3030
}
3131

32-
requestParams(): any {
32+
requestParams(forgetNear = false): any {
3333
const params: any = {
3434
filter: {
3535
dialog: this.attrs.dialog.id(),
@@ -39,7 +39,7 @@ export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDia
3939

4040
const near = m.route.param('near');
4141

42-
if (near) {
42+
if (near && !forgetNear) {
4343
params.page = params.page || {};
4444
params.page.near = parseInt(near);
4545
}

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

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,29 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
7777
content() {
7878
const items: Mithril.Children[] = [];
7979

80-
const messages = this.attrs.state.getAllItems().sort((a, b) => a.createdAt().getTime() - b.createdAt().getTime());
80+
const messages = Array.from(new Map(this.attrs.state.getAllItems().map((msg) => [msg.id(), msg])).values()).sort(
81+
(a, b) => a.number() - b.number()
82+
);
8183

8284
const ReplyPlaceholder = this.replyPlaceholderComponent();
8385
const LoadingPost = this.loadingPostComponent();
8486

8587
if (messages[0].id() !== (this.attrs.dialog.data.relationships?.firstMessage.data as ModelIdentifier).id) {
8688
items.push(
87-
<div className="MessageStream-item" key="loadPrevious">
89+
<div className="MessageStream-item" key="loadNext">
8890
<Button
8991
onclick={() => this.whileMaintainingScroll(() => this.attrs.state.loadNext())}
9092
type="button"
91-
className="Button Button--block MessageStream-loadPrev"
93+
className="Button Button--block MessageStream-loadNext"
9294
>
93-
{app.translator.trans('flarum-messages.forum.messages_page.stream.load_previous_button')}
95+
{app.translator.trans('flarum-messages.forum.messages_page.stream.load_next_button')}
9496
</Button>
9597
</div>
9698
);
9799

98100
if (LoadingPost) {
99101
items.push(
100-
<div className="MessageStream-item" key="loading-prev">
102+
<div className="MessageStream-item" key="loading-next">
101103
<LoadingPost />
102104
</div>
103105
);
@@ -106,6 +108,28 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
106108

107109
messages.forEach((message, index) => items.push(this.messageItem(message, index)));
108110

111+
if (messages[messages.length - 1].id() !== (this.attrs.dialog.data.relationships?.lastMessage.data as ModelIdentifier).id) {
112+
if (LoadingPost) {
113+
items.push(
114+
<div className="MessageStream-item" key="loading-prev">
115+
<LoadingPost />
116+
</div>
117+
);
118+
}
119+
120+
items.push(
121+
<div className="MessageStream-item" key="loadPrev">
122+
<Button
123+
onclick={() => this.whileMaintainingScroll(() => this.attrs.state.loadPrev())}
124+
type="button"
125+
className="Button Button--block MessageStream-loadPrev"
126+
>
127+
{app.translator.trans('flarum-messages.forum.messages_page.stream.load_previous_button')}
128+
</Button>
129+
</div>
130+
);
131+
}
132+
109133
if (app.session.user!.canSendAnyMessage() && ReplyPlaceholder) {
110134
items.push(
111135
<div className="MessageStream-item" key="reply">
@@ -177,7 +201,7 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
177201
return this.attrs.state.loadNext();
178202
}
179203

180-
if (this.element.scrollTop + this.element.clientHeight === this.element.scrollHeight && this.attrs.state.hasPrev()) {
204+
if (this.element.scrollTop + this.element.clientHeight >= this.element.scrollHeight && this.attrs.state.hasPrev()) {
181205
return this.attrs.state.loadPrev();
182206
}
183207

@@ -193,9 +217,10 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
193217

194218
if ($message) {
195219
this.element.scrollTop = $message.getBoundingClientRect().top - this.element.getBoundingClientRect().top;
196-
197-
// pulsate the message
198220
$message.classList.add('flash');
221+
222+
// forget near
223+
window.history.replaceState(null, '', app.route.dialog(this.attrs.dialog));
199224
} else {
200225
this.element.scrollTop = this.element.scrollHeight;
201226
}
@@ -208,9 +233,11 @@ export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDia
208233
const scrollTop = this.element.scrollTop;
209234
const scrollHeight = this.element.scrollHeight;
210235

236+
const closerToBottomThanTop = scrollTop > (scrollHeight - this.element.clientHeight) / 2;
237+
211238
const result = callback();
212239

213-
if (result instanceof Promise) {
240+
if (result instanceof Promise && !closerToBottomThanTop) {
214241
result.then(() => {
215242
requestAnimationFrame(() => {
216243
this.element.scrollTop = this.element.scrollHeight - scrollHeight + scrollTop;

extensions/messages/js/src/forum/states/MessageStreamState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PaginatedListState, { PaginatedListParams } from 'flarum/common/states/PaginatedListState';
22
import DialogMessage from '../../common/models/DialogMessage';
3+
import { ApiQueryParamsPlural } from 'flarum/common/Store';
34

45
export interface MessageStreamParams extends PaginatedListParams {
56
//

extensions/messages/locale/en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ flarum-messages:
5353
send_message_button: Send a Message
5454
stream:
5555
load_previous_button: Load previous messages
56+
load_next_button: Load next messages
5657
start_of_the_conversation: Start of the conversation
5758
time_lapsed_text: => core.forum.post_stream.time_lapsed_text
5859
title: Messages

framework/core/js/src/common/states/PaginatedListState.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import app from '../../common/app';
2-
import Model from '../Model';
3-
import { ApiQueryParamsPlural, ApiResponsePlural } from '../Store';
2+
import type Model from '../Model';
3+
import type { ApiQueryParamsPlural, ApiResponsePlural } from '../Store';
44
import type Mithril from 'mithril';
5-
import setRouteWithForcedRefresh from '../utils/setRouteWithForcedRefresh';
65

76
export type SortMapItem =
87
| string
@@ -15,9 +14,9 @@ export type SortMap = {
1514
[key: string]: SortMapItem;
1615
};
1716

18-
export interface Page<TModel> {
17+
export interface Page<TModel extends Model> {
1918
number: number;
20-
items: TModel[];
19+
items: ApiResponsePlural<TModel> | TModel[];
2120

2221
hasPrev?: boolean;
2322
hasNext?: boolean;
@@ -73,7 +72,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
7372
}
7473

7574
public loadPrev(): Promise<void> {
76-
if (this.loadingPrev || this.getLocation().page === 1) return Promise.resolve();
75+
if (this.loadingPrev || !this.hasPrev()) return Promise.resolve();
7776

7877
this.loadingPrev = true;
7978

@@ -140,7 +139,7 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
140139
delete params.include;
141140
}
142141

143-
return app.store.find<T[]>(this.type, params).then((results) => {
142+
return app.store.find<T[]>(this.type, this.mutateRequestParams(params, page)).then((results) => {
144143
const usedPerPage = results.payload?.meta?.perPage;
145144
const usedTotal = results.payload?.meta?.page?.total;
146145

@@ -160,6 +159,35 @@ export default abstract class PaginatedListState<T extends Model, P extends Pagi
160159
});
161160
}
162161

162+
protected mutateRequestParams(params: ApiQueryParamsPlural, page: number): ApiQueryParamsPlural {
163+
/*
164+
* Support use of page[near]=
165+
*/
166+
if (params.page?.near && this.hasItems()) {
167+
delete params.page?.near;
168+
169+
const nextPage = this.location.page < page;
170+
171+
const offsets = this.getPages().map((page) => {
172+
if ('payload' in page.items) {
173+
return page.items.payload.meta?.page?.offset || 0;
174+
}
175+
176+
return 0;
177+
});
178+
179+
const minOffset = Math.min(...offsets);
180+
const maxOffset = Math.max(...offsets);
181+
182+
const limit = this.pageSize || PaginatedListState.DEFAULT_PAGE_SIZE;
183+
184+
params.page ||= {};
185+
params.page.offset = nextPage ? maxOffset + limit : Math.max(minOffset - limit, 0);
186+
}
187+
188+
return params;
189+
}
190+
163191
/**
164192
* Get the parameters that should be passed in the API request.
165193
* Do not include page offset unless subclass overrides loadPage.

framework/core/src/Api/Endpoint/Index.php

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function query(?Closure $query): static
6969
protected function setUp(): void
7070
{
7171
$this->route('GET', '/')
72-
->query(function ($query, ?Pagination $pagination, Context $context): Context {
72+
->query(function ($query, ?Pagination $pagination, Context $context, array $filters, ?array $sort, int $offset, ?int $limit): Context {
7373
$collection = $context->collection;
7474

7575
// This model has a searcher API, so we'll use that instead of the default.
@@ -81,13 +81,6 @@ protected function setUp(): void
8181
if ($query instanceof Builder && $search->searchable($modelClass)) {
8282
$actor = $context->getActor();
8383

84-
$extracts = $this->defaultExtracts($context);
85-
86-
$filters = $this->extractFilterValue($context, $extracts);
87-
$sort = $this->extractSortValue($context, $extracts);
88-
$limit = $this->extractLimitValue($context, $extracts);
89-
$offset = $this->extractOffsetValue($context, $extracts);
90-
9184
$sortIsDefault = ! $context->queryParam('sort');
9285

9386
$results = $search->query(
@@ -101,8 +94,8 @@ protected function setUp(): void
10194
else {
10295
$context = $context->withQuery($query);
10396

104-
$this->applySorts($query, $context);
105-
$this->applyFilters($query, $context);
97+
$this->applySorts($query, $context, $sort);
98+
$this->applyFilters($query, $context, $filters);
10699

107100
if ($pagination && method_exists($pagination, 'apply')) {
108101
$pagination->apply($query);
@@ -130,8 +123,20 @@ protected function setUp(): void
130123

131124
$pagination = ($this->paginationResolver)($context);
132125

126+
$extracts = $this->defaultExtracts($context);
127+
128+
$filters = $this->extractFilterValue($context, $extracts);
129+
$sort = $this->extractSortValue($context, $extracts);
130+
$limit = $this->extractLimitValue($context, $extracts);
131+
$offset = $this->extractOffsetValue($context, $extracts);
132+
133+
if ($pagination instanceof OffsetPagination) {
134+
$pagination->offset = $offset;
135+
$pagination->limit = $limit;
136+
}
137+
133138
if ($this->query) {
134-
$context = ($this->query)($query, $pagination, $context);
139+
$context = ($this->query)($query, $pagination, $context, $filters, $sort, $offset, $limit);
135140

136141
if (! $context instanceof Context) {
137142
throw new RuntimeException('The Index endpoint query closure must return a Context instance.');
@@ -140,8 +145,8 @@ protected function setUp(): void
140145
/** @var Context $context */
141146
$context = $context->withQuery($query);
142147

143-
$this->applySorts($query, $context);
144-
$this->applyFilters($query, $context);
148+
$this->applySorts($query, $context, $sort);
149+
$this->applyFilters($query, $context, $filters);
145150

146151
if ($pagination) {
147152
$pagination->apply($query);
@@ -206,9 +211,9 @@ public function defaultSort(?string $defaultSort): static
206211
return $this;
207212
}
208213

209-
final protected function applySorts($query, Context $context): void
214+
final protected function applySorts($query, Context $context, ?array $sort): void
210215
{
211-
if (! ($sortString = $context->queryParam('sort', $this->defaultSort))) {
216+
if (! $sort) {
212217
return;
213218
}
214219

@@ -220,7 +225,7 @@ final protected function applySorts($query, Context $context): void
220225

221226
$sorts = $collection->resolveSorts();
222227

223-
foreach (parse_sort_string($sortString) as [$name, $direction]) {
228+
foreach ($sort as $name => $direction) {
224229
foreach ($sorts as $field) {
225230
if ($field->name === $name && $field->isVisible($context)) {
226231
$field->apply($query, $direction, $context);
@@ -234,18 +239,12 @@ final protected function applySorts($query, Context $context): void
234239
}
235240
}
236241

237-
final protected function applyFilters($query, Context $context): void
242+
final protected function applyFilters($query, Context $context, array $filters): void
238243
{
239-
if (! ($filters = $context->queryParam('filter'))) {
244+
if (empty($filters)) {
240245
return;
241246
}
242247

243-
if (! is_array($filters)) {
244-
throw (new BadRequestException('filter must be an array'))->setSource([
245-
'parameter' => 'filter',
246-
]);
247-
}
248-
249248
$collection = $context->collection;
250249

251250
if (! $collection instanceof Listable) {

framework/core/tests/integration/api/groups/ListTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ public function shows_limited_index_for_guest()
4040
$this->request('GET', '/api/groups')
4141
);
4242

43-
$this->assertEquals(200, $response->getStatusCode());
44-
$data = json_decode($response->getBody()->getContents(), true);
43+
$body = $response->getBody()->getContents();
44+
45+
$this->assertEquals(200, $response->getStatusCode(), $body);
46+
$data = json_decode($body, true);
4547

4648
// The four default groups created by the installer
4749
$this->assertEquals(['1', '2', '3', '4'], Arr::pluck($data['data'], 'id'));

0 commit comments

Comments
 (0)