Skip to content

Commit 3a7f732

Browse files
feat: [CHA-375] add draft messages api (#1490)
## CLA - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [ ] Code changes are tested ## Description of the changes, What, Why and How? ## Changelog - Add Draft Messages API (thanks for the types @MartinCupela) --------- Co-authored-by: MartinCupela <32706194+MartinCupela@users.noreply.github.com> Co-authored-by: martincupela <martin.cupela@gmail.com>
1 parent 9413433 commit 3a7f732

File tree

4 files changed

+273
-0
lines changed

4 files changed

+273
-0
lines changed

src/channel.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ import {
6262
AIState,
6363
MessageOptions,
6464
PushPreference,
65+
CreateDraftResponse,
66+
GetDraftResponse,
67+
DraftMessagePayload,
6568
} from './types';
6669
import { Role } from './permissions';
6770
import { DEFAULT_QUERY_CHANNEL_MESSAGE_LIST_PAGE_SIZE } from './constants';
@@ -1351,6 +1354,47 @@ export class Channel<StreamChatGenerics extends ExtendableGenerics = DefaultGene
13511354
return await this.getClient().removePollVote(messageId, pollId, voteId);
13521355
}
13531356

1357+
/**
1358+
* createDraft - Creates or updates a draft message in a channel
1359+
*
1360+
* @param {string} channelType The channel type
1361+
* @param {string} channelID The channel ID
1362+
* @param {DraftMessagePayload<StreamChatGenerics>} message The draft message to create or update
1363+
*
1364+
* @return {Promise<CreateDraftResponse<StreamChatGenerics>>} Response containing the created draft
1365+
*/
1366+
async createDraft(message: DraftMessagePayload<StreamChatGenerics>) {
1367+
return await this.getClient().post<CreateDraftResponse<StreamChatGenerics>>(this._channelURL() + '/draft', {
1368+
message,
1369+
});
1370+
}
1371+
1372+
/**
1373+
* deleteDraft - Deletes a draft message from a channel
1374+
*
1375+
* @param {Object} options
1376+
* @param {string} options.parent_id Optional parent message ID for drafts in threads
1377+
*
1378+
* @return {Promise<APIResponse>} API response
1379+
*/
1380+
async deleteDraft({ parent_id }: { parent_id?: string } = {}) {
1381+
return await this.getClient().delete<APIResponse>(this._channelURL() + '/draft', { parent_id });
1382+
}
1383+
1384+
/**
1385+
* getDraft - Retrieves a draft message from a channel
1386+
*
1387+
* @param {Object} options
1388+
* @param {string} options.parent_id Optional parent message ID for drafts in threads
1389+
*
1390+
* @return {Promise<GetDraftResponse<StreamChatGenerics>>} Response containing the draft
1391+
*/
1392+
async getDraft({ parent_id }: { parent_id?: string } = {}) {
1393+
return await this.getClient().get<GetDraftResponse<StreamChatGenerics>>(this._channelURL() + '/draft', {
1394+
parent_id,
1395+
});
1396+
}
1397+
13541398
/**
13551399
* on - Listen to events on this channel.
13561400
*

src/client.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ import {
214214
UserResponse,
215215
UserSort,
216216
VoteSort,
217+
QueryDraftsResponse,
218+
DraftFilters,
219+
DraftSort,
220+
Pager,
217221
} from './types';
218222
import { InsightMetrics, postInsights } from './insights';
219223
import { Thread } from './thread';
@@ -4046,4 +4050,27 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
40464050
...options,
40474051
});
40484052
}
4053+
4054+
/**
4055+
* queryDrafts - Queries drafts for the current user
4056+
*
4057+
* @param {object} [options] Query options
4058+
* @param {object} [options.filter] Filters for the query
4059+
* @param {number} [options.sort] Sort parameters
4060+
* @param {number} [options.limit] Limit the number of results
4061+
* @param {string} [options.next] Pagination parameter
4062+
* @param {string} [options.prev] Pagination parameter
4063+
* @param {string} [options.user_id] Has to be provided when called server-side
4064+
*
4065+
* @return {Promise<APIResponse & { drafts: DraftResponse<StreamChatGenerics>[]; next?: string }>} Response containing the drafts
4066+
*/
4067+
async queryDrafts(
4068+
options: Pager & {
4069+
filter?: DraftFilters<StreamChatGenerics>;
4070+
sort?: DraftSort;
4071+
user_id?: string;
4072+
} = {},
4073+
) {
4074+
return await this.post<QueryDraftsResponse<StreamChatGenerics>>(this.baseURL + '/drafts/query', options);
4075+
}
40494076
}

src/types.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,18 @@ export type ChannelFilters<StreamChatGenerics extends ExtendableGenerics = Defau
15971597
}
15981598
>;
15991599

1600+
export type DraftFilters<SCG extends ExtendableGenerics = DefaultGenerics> = {
1601+
channel_cid?:
1602+
| RequireOnlyOne<Pick<QueryFilter<DraftResponse<SCG>['channel_cid']>, '$in' | '$eq'>>
1603+
| PrimitiveFilter<DraftResponse<SCG>['channel_cid']>;
1604+
created_at?:
1605+
| RequireOnlyOne<Pick<QueryFilter<DraftResponse<SCG>['created_at']>, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>>
1606+
| PrimitiveFilter<DraftResponse<SCG>['created_at']>;
1607+
parent_id?:
1608+
| RequireOnlyOne<Pick<QueryFilter<DraftResponse<SCG>['created_at']>, '$in' | '$eq' | '$exists'>>
1609+
| PrimitiveFilter<DraftResponse<SCG>['parent_id']>;
1610+
};
1611+
16001612
export type QueryPollsParams = {
16011613
filter?: QueryPollsFilters;
16021614
options?: QueryPollsOptions;
@@ -2046,6 +2058,12 @@ export type SearchMessageSort<StreamChatGenerics extends ExtendableGenerics = De
20462058
| SearchMessageSortBase<StreamChatGenerics>
20472059
| Array<SearchMessageSortBase<StreamChatGenerics>>;
20482060

2061+
export type DraftSortBase = {
2062+
created_at?: AscDesc;
2063+
};
2064+
2065+
export type DraftSort = DraftSortBase | Array<DraftSortBase>;
2066+
20492067
export type QuerySort<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> =
20502068
| BannedUsersSort
20512069
| ChannelSort<StreamChatGenerics>
@@ -3833,3 +3851,51 @@ export type SdkIdentifier = { name: 'react' | 'react-native' | 'expo' | 'angular
38333851
* available. Is used by the react-native SDKs to enrich the user agent further.
38343852
*/
38353853
export type DeviceIdentifier = { os: string; model?: string };
3854+
3855+
export declare type DraftResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
3856+
channel_cid: string;
3857+
created_at: string;
3858+
message: DraftMessage<StreamChatGenerics>;
3859+
channel?: ChannelResponse<StreamChatGenerics>;
3860+
parent_id?: string;
3861+
parent_message?: MessageResponseBase<StreamChatGenerics>;
3862+
quoted_message?: MessageResponseBase<StreamChatGenerics>;
3863+
};
3864+
export declare type CreateDraftResponse<
3865+
StreamChatGenerics extends ExtendableGenerics = DefaultGenerics
3866+
> = APIResponse & {
3867+
draft: DraftResponse<StreamChatGenerics>;
3868+
};
3869+
3870+
export declare type GetDraftResponse<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = APIResponse & {
3871+
draft: DraftResponse<StreamChatGenerics>;
3872+
};
3873+
3874+
export declare type QueryDraftsResponse<
3875+
StreamChatGenerics extends ExtendableGenerics = DefaultGenerics
3876+
> = APIResponse & {
3877+
drafts: DraftResponse<StreamChatGenerics>[];
3878+
next?: string;
3879+
};
3880+
3881+
export declare type DraftMessagePayload<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = Omit<
3882+
DraftMessage<StreamChatGenerics>,
3883+
'id'
3884+
> &
3885+
Partial<Pick<DraftMessage<StreamChatGenerics>, 'id'>>;
3886+
3887+
export declare type DraftMessage<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
3888+
id: string;
3889+
text: string;
3890+
attachments?: Attachment<StreamChatGenerics>[];
3891+
custom?: {};
3892+
html?: string;
3893+
mentioned_users?: string[];
3894+
mml?: string;
3895+
parent_id?: string;
3896+
poll_id?: string;
3897+
quoted_message_id?: string;
3898+
show_in_channel?: boolean;
3899+
silent?: boolean;
3900+
type?: MessageLabel;
3901+
};

test/unit/draft.test.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { expect } from 'chai';
2+
import sinon from 'sinon';
3+
import { StreamChat } from '../../src';
4+
import { generateChannel } from './test-utils/generateChannel';
5+
import { v4 as uuidv4 } from 'uuid';
6+
7+
describe('Draft Messages', () => {
8+
let client;
9+
let channel;
10+
const apiKey = 'test-api-key';
11+
const channelType = 'messaging';
12+
const channelID = 'test-channel';
13+
const userID = 'test-user';
14+
const parentID = 'parent-message-id';
15+
16+
const draftMessage = {
17+
text: 'Draft message text',
18+
attachments: [{ type: 'image', url: 'https://example.com/image.jpg' }],
19+
mentioned_users: ['user1', 'user2'],
20+
};
21+
22+
const draftWithParent = {
23+
text: 'Draft message text',
24+
attachments: [{ type: 'image', url: 'https://example.com/image.jpg' }],
25+
mentioned_users: ['user1', 'user2'],
26+
parent_id: parentID,
27+
};
28+
29+
const draftResponse = {
30+
draft: {
31+
channel_cid: `${channelType}:${channelID}`,
32+
created_at: '2023-01-01T00:00:00Z',
33+
message: {
34+
id: 'draft-id',
35+
...draftMessage,
36+
},
37+
parent_id: parentID,
38+
},
39+
};
40+
41+
beforeEach(() => {
42+
client = new StreamChat(apiKey);
43+
client.userID = userID;
44+
let channelResponse = generateChannel({
45+
channel: { id: channelID, name: 'Test channel', members: [] },
46+
}).channel;
47+
channel = client.channel(channelResponse.type, channelResponse.id);
48+
49+
// Mock the methods
50+
sinon.stub(client, 'queryDrafts').resolves(draftResponse);
51+
sinon.stub(channel, 'createDraft').resolves(draftResponse);
52+
sinon.stub(channel, 'getDraft').resolves(draftResponse);
53+
sinon.stub(channel, 'deleteDraft').resolves({ duration: '0.01ms' });
54+
});
55+
56+
afterEach(() => {
57+
sinon.restore();
58+
});
59+
60+
it('should create a draft message', async () => {
61+
const response = await channel.createDraft(draftMessage);
62+
63+
expect(channel.createDraft.calledOnce).to.be.true;
64+
expect(channel.createDraft.firstCall.args[0]).to.deep.equal(draftMessage);
65+
expect(response).to.deep.equal(draftResponse);
66+
});
67+
68+
it('should create a draft message with parent ID', async () => {
69+
const response = await channel.createDraft(draftWithParent);
70+
71+
expect(channel.createDraft.calledOnce).to.be.true;
72+
expect(channel.createDraft.firstCall.args[0]).to.deep.equal(draftWithParent);
73+
expect(response).to.deep.equal(draftResponse);
74+
});
75+
76+
it('should get a draft message', async () => {
77+
const response = await channel.getDraft(parentID);
78+
79+
expect(channel.getDraft.calledOnce).to.be.true;
80+
expect(channel.getDraft.firstCall.args[0]).to.deep.equal(parentID);
81+
expect(response).to.deep.equal(draftResponse);
82+
});
83+
84+
it('should get a draft message with parent ID', async () => {
85+
const response = await channel.getDraft(parentID);
86+
87+
expect(channel.getDraft.calledOnce).to.be.true;
88+
expect(channel.getDraft.firstCall.args[0]).to.deep.equal(parentID);
89+
expect(response).to.deep.equal(draftResponse);
90+
});
91+
92+
it('should delete a draft message', async () => {
93+
await channel.deleteDraft();
94+
95+
expect(channel.deleteDraft.calledOnce).to.be.true;
96+
expect(channel.deleteDraft.firstCall.args[0]).to.be.undefined;
97+
});
98+
99+
it('should delete a draft message with parent ID', async () => {
100+
await channel.deleteDraft(parentID);
101+
102+
expect(channel.deleteDraft.calledOnce).to.be.true;
103+
expect(channel.deleteDraft.firstCall.args[0]).to.deep.equal(parentID);
104+
});
105+
106+
it('should query drafts', async () => {
107+
const queryOptions = {
108+
filter: { created_at: { $gt: '2023-01-01T00:00:00Z' } },
109+
limit: 10,
110+
};
111+
112+
const queryResponse = {
113+
drafts: [draftResponse.draft, { ...draftResponse.draft, channel_cid: 'messaging:other-channel' }],
114+
next: 'next-page-token',
115+
};
116+
client.queryDrafts.resolves(queryResponse);
117+
118+
const response = await client.queryDrafts(queryOptions);
119+
120+
expect(client.queryDrafts.calledOnce).to.be.true;
121+
expect(client.queryDrafts.firstCall.args[0]).to.deep.equal(queryOptions);
122+
expect(response).to.deep.equal(queryResponse);
123+
});
124+
125+
it('should query drafts with default options', async () => {
126+
const queryResponse = {
127+
drafts: [draftResponse.draft],
128+
};
129+
client.queryDrafts.resolves(queryResponse);
130+
131+
const response = await client.queryDrafts();
132+
expect(client.queryDrafts.calledOnce).to.be.true;
133+
expect(client.queryDrafts.firstCall.args[0]).to.be.undefined;
134+
expect(response).to.deep.equal(queryResponse);
135+
});
136+
});

0 commit comments

Comments
 (0)