Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants';
import type {
AIState,
APIResponse,
AscDesc,
BanUserOptions,
ChannelAPIResponse,
ChannelData,
Expand Down Expand Up @@ -66,6 +65,7 @@ import type {
SendMessageAPIResponse,
SendMessageOptions,
SendReactionOptions,
Sort,
StaticLocationPayload,
TruncateChannelAPIResponse,
TruncateOptions,
Expand Down Expand Up @@ -1305,7 +1305,7 @@ export class Channel {
async getReplies(
parent_id: string,
options: MessagePaginationOptions & { user?: UserResponse; user_id?: string },
sort?: { created_at: AscDesc }[],
sort?: Required<Sort<'created_at'>>[],
) {
const normalizedSort = sort ? normalizeQuerySort(sort) : undefined;
const data = await this.getClient().get<GetRepliesAPIResponse>(
Expand Down
6 changes: 3 additions & 3 deletions src/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
throttle,
} from './utils';
import type {
AscDesc,
EventTypes,
LocalMessage,
MessagePaginationOptions,
MessageResponse,
ReadResponse,
Sort,
ThreadResponse,
UserResponse,
} from './types';
Expand All @@ -22,7 +22,7 @@ import { MessageComposer } from './messageComposer';
import { WithSubscriptions } from './utils/WithSubscriptions';

type QueryRepliesOptions = {
sort?: { created_at: AscDesc }[];
sort?: Required<Sort<'created_at'>>[];
} & MessagePaginationOptions & { user?: UserResponse; user_id?: string };

export type ThreadState = {
Expand Down Expand Up @@ -68,7 +68,7 @@ export type ThreadUserReadState = {
export type ThreadReadState = Record<string, ThreadUserReadState | undefined>;

const DEFAULT_PAGE_LIMIT = 50;
const DEFAULT_SORT: { created_at: AscDesc }[] = [{ created_at: -1 }];
const DEFAULT_SORT: QueryRepliesOptions['sort'] = [{ created_at: -1 }];
const MARK_AS_READ_THROTTLE_TIMEOUT = 1000;
// TODO: remove this once we move to API v2
export const THREAD_RESPONSE_RESERVED_KEYS: Record<keyof ThreadResponse, true> = {
Expand Down
119 changes: 56 additions & 63 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2168,32 +2168,37 @@ export type MemberFilters = QueryFilters<

export type BannedUsersSort = BannedUsersSortBase | Array<BannedUsersSortBase>;

export type BannedUsersSortBase = { created_at?: AscDesc };
export type BannedUsersSortBase = Sort<'created_at'>;

export type ReactionSort = ReactionSortBase | Array<ReactionSortBase>;

export type ReactionSortBase = Sort<CustomReactionData> & {
created_at?: AscDesc;
};
export type ReactionSortBase = Sort<CustomReactionData> & Sort<'created_at'>;

export type ChannelSort = ChannelSortBase | Array<ChannelSortBase>;

export type ChannelSortBase = Sort<CustomChannelData> & {
created_at?: AscDesc;
has_unread?: AscDesc;
last_message_at?: AscDesc;
last_updated?: AscDesc;
member_count?: AscDesc;
pinned_at?: AscDesc;
unread_count?: AscDesc;
updated_at?: AscDesc;
};
export type ChannelSortBase = Sort<CustomChannelData> &
Sort<
| 'created_at'
| 'has_unread'
| 'last_message_at'
| 'last_updated'
| 'member_count'
| 'pinned_at'
| 'unread_count'
| 'updated_at'
>;

export type PinnedMessagesSort = PinnedMessagesSortBase | Array<PinnedMessagesSortBase>;
export type PinnedMessagesSortBase = { pinned_at?: AscDesc };

export type Sort<T> = {
[P in keyof T]?: AscDesc;
export type PinnedMessagesSortBase = Sort<'pinned_at'>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Sort<T extends string | Record<string, any>> = {
[P in T extends string ? T : keyof T]?:
| AscDesc
| {
direction: AscDesc;
type?: 'string' | 'number';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kanat what types will be supported?

};
};

export type UserSort = Sort<UserResponse> | Array<Sort<UserResponse>>;
Expand All @@ -2212,51 +2217,40 @@ export type MemberSort =
>
>;

export type SearchMessageSortBase = Sort<CustomMessageData> & {
attachments?: AscDesc;
'attachments.type'?: AscDesc;
created_at?: AscDesc;
id?: AscDesc;
'mentioned_users.id'?: AscDesc;
parent_id?: AscDesc;
pinned?: AscDesc;
relevance?: AscDesc;
reply_count?: AscDesc;
text?: AscDesc;
type?: AscDesc;
updated_at?: AscDesc;
'user.id'?: AscDesc;
};
export type SearchMessageSortBase = Sort<CustomMessageData> &
Sort<
| 'attachments'
| 'attachments.type'
| 'created_at'
| 'id'
| 'mentioned_users.id'
| 'parent_id'
| 'pinned'
| 'relevance'
| 'reply_count'
| 'text'
| 'type'
| 'updated_at'
| 'user.id'
>;

export type SearchMessageSort = SearchMessageSortBase | Array<SearchMessageSortBase>;

export type QuerySort = BannedUsersSort | ChannelSort | SearchMessageSort | UserSort;

export type DraftSortBase = {
created_at?: AscDesc;
};
export type DraftSortBase = Sort<'created_ats'>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export type DraftSortBase = Sort<'created_ats'>;
export type DraftSortBase = Sort<'created_at'>;


export type DraftSort = DraftSortBase | Array<DraftSortBase>;

export type PollSort = PollSortBase | Array<PollSortBase>;

export type PollSortBase = {
created_at?: AscDesc;
id?: AscDesc;
is_closed?: AscDesc;
name?: AscDesc;
updated_at?: AscDesc;
};
export type PollSortBase = Sort<
'created_at' | 'id' | 'is_closed' | 'name' | 'updated_at'
>;

export type VoteSort = VoteSortBase | Array<VoteSortBase>;

export type VoteSortBase = {
created_at?: AscDesc;
id?: AscDesc;
is_closed?: AscDesc;
name?: AscDesc;
updated_at?: AscDesc;
};
export type VoteSortBase = PollSortBase;

/**
* Base Types
Expand Down Expand Up @@ -3569,10 +3563,9 @@ export type QueryMessageHistorySort =
| QueryMessageHistorySortBase
| Array<QueryMessageHistorySortBase>;

export type QueryMessageHistorySortBase = {
message_updated_at?: AscDesc;
message_updated_by_id?: AscDesc;
};
export type QueryMessageHistorySortBase = Sort<
'message_updated_at' | 'message_updated_by_id'
>;

export type QueryMessageHistoryOptions = Pager;

Expand Down Expand Up @@ -4321,15 +4314,15 @@ export type LiveLocationPayload = {

export type ThreadSort = ThreadSortBase | Array<ThreadSortBase>;

export type ThreadSortBase = {
active_participant_count?: AscDesc;
created_at?: AscDesc;
last_message_at?: AscDesc;
parent_message_id?: AscDesc;
participant_count?: AscDesc;
reply_count?: AscDesc;
updated_at?: AscDesc;
};
export type ThreadSortBase = Sort<
| 'active_participant_count'
| 'created_at'
| 'last_message_at'
| 'parent_message_id'
| 'participant_count'
| 'reply_count'
| 'updated_at'
>;

export type ThreadFilters = QueryFilters<
{
Expand Down
35 changes: 21 additions & 14 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
PromoteChannelParams,
QueryChannelAPIResponse,
ReactionGroupResponse,
Sort,
UpdatedMessage,
UserResponse,
} from './types';
Expand Down Expand Up @@ -131,20 +132,26 @@ export function addFileToFormData(

return data;
}
export function normalizeQuerySort<T extends Record<string, AscDesc | undefined>>(
sort: T | T[],
) {
const sortFields: Array<{ direction: AscDesc; field: keyof T }> = [];
const sortArr = Array.isArray(sort) ? sort : [sort];
for (const item of sortArr) {
const entries = Object.entries(item) as [keyof T, AscDesc][];
if (entries.length > 1) {
console.warn(
"client._buildSort() - multiple fields in a single sort object detected. Object's field order is not guaranteed",
);
}
for (const [field, direction] of entries) {
sortFields.push({ field, direction });

export function normalizeQuerySort<T extends Sort<string>>(sort: T | T[]) {
const sortFields: Array<{ direction: AscDesc; field: keyof T; type: string | null }> =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const sortFields: Array<{ direction: AscDesc; field: keyof T; type: string | null }> =
const sortFields: Array<{ direction: AscDesc; field: keyof T; type: string | number | null }> =

Not sure, but basing it on this:

https://github.com/GetStream/stream-chat-js/pull/1673/changes#r2664806559

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type is just a string in this case as it's supposed to be sent to the BE, TS should not remove this during compile time. In this case it's just generalized to string so I don't have to do "string" | "number" | "some-other-type-once-added" since we don't really care about specificity at that point.

[];
const sortArray = Array.isArray(sort) ? sort : [sort];
for (const item of sortArray) {
const entries = Object.entries(item);

for (const [field, directionOrObject] of entries) {
if (!directionOrObject) continue;

if (typeof directionOrObject === 'number') {
sortFields.push({ field, direction: directionOrObject, type: null });
} else {
sortFields.push({
field,
direction: directionOrObject.direction,
type: directionOrObject.type ?? null,
});
}
}
}
return sortFields;
Expand Down
24 changes: 24 additions & 0 deletions test/unit/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,36 @@ describe('test if sort is deterministic', () => {
expect(sort).to.have.length(4);
expect(sort[0].field).to.be.equal('created_at');
expect(sort[0].direction).to.be.equal(1);
expect(sort[0].type).toBe(null);
expect(sort[1].field).to.be.equal('has_unread');
expect(sort[1].direction).to.be.equal(-1);
expect(sort[1].type).toBe(null);
expect(sort[2].field).to.be.equal('last_active');
expect(sort[2].direction).to.be.equal(1);
expect(sort[2].type).toBe(null);
expect(sort[3].field).to.be.equal('deleted_at');
expect(sort[3].direction).to.be.equal(-1);
expect(sort[3].type).toBe(null);
});
it('test sort array with typed fields', () => {
let sort = normalizeQuerySort([
{ created_at: { direction: -1, type: 'string' }, has_unread: -1 },
{ last_active: 1, deleted_at: -1 },
]);

expect(sort).to.have.length(4);
expect(sort[0].field).to.be.equal('created_at');
expect(sort[0].direction).to.be.equal(-1);
expect(sort[0].type).toBe('string');
expect(sort[1].field).to.be.equal('has_unread');
expect(sort[1].direction).to.be.equal(-1);
expect(sort[1].type).toBe(null);
expect(sort[2].field).to.be.equal('last_active');
expect(sort[2].direction).to.be.equal(1);
expect(sort[2].type).toBe(null);
expect(sort[3].field).to.be.equal('deleted_at');
expect(sort[3].direction).to.be.equal(-1);
expect(sort[3].type).toBe(null);
});
});

Expand Down
Loading