From 1d8148072af1d899955a3d4b1e9b1957322961ed Mon Sep 17 00:00:00 2001 From: Vishal Narkhede Date: Fri, 12 Apr 2024 11:39:23 +0200 Subject: [PATCH 1/6] feat: polls feature endpoints (#1269) Co-authored-by: Lennart Kuijs Co-authored-by: kanat --- src/channel.ts | 40 ++++++ src/channel_state.ts | 92 ++++++++++++++ src/client.ts | 206 ++++++++++++++++++++++++++++++ src/events.ts | 5 + src/thread.ts | 30 +++-- src/types.ts | 295 ++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 657 insertions(+), 11 deletions(-) diff --git a/src/channel.ts b/src/channel.ts index 63b64ccdd..ca9cffab3 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -53,6 +53,7 @@ import { UserFilters, UserResponse, QueryChannelAPIResponse, + PollVoteData, SendMessageOptions, } from './types'; import { Role } from './permissions'; @@ -1158,6 +1159,20 @@ export class Channel(this._channelURL() + '/call', options); } + /** + * Cast or cancel one or more votes on a poll + * @param pollId string The poll id + * @param votes PollVoteData[] The votes that will be casted (or canceled in case of an empty array) + * @returns {APIResponse & PollVoteResponse} The poll votes + */ + async vote(messageId: string, pollId: string, vote: PollVoteData) { + return await this.getClient().castPollVote(messageId, pollId, vote); + } + + async removeVote(messageId: string, pollId: string, voteId: string) { + return await this.getClient().removePollVote(messageId, pollId, voteId); + } + /** * on - Listen to events on this channel. * @@ -1401,6 +1416,31 @@ export class Channel, + poll: PollResponse, + messageId: string, + ) => { + const message = this.findMessage(messageId); + if (!message) return; + + if (message.poll_id !== pollVote.poll_id) return; + + const updatedPoll = { ...poll }; + let ownVotes = [...(message.poll.own_votes || [])]; + + if (pollVote.user_id === this._channel.getClient().userID) { + if (pollVote.option_id && poll.enforce_unique_vote) { + // remove all previous votes where option_id is not empty + ownVotes = ownVotes.filter((vote) => !vote.option_id); + } else if (pollVote.answer_text) { + // remove all previous votes where option_id is empty + ownVotes = ownVotes.filter((vote) => vote.answer_text); + } + + ownVotes.push(pollVote); + } + + updatedPoll.own_votes = ownVotes as PollVote[]; + const newMessage = { ...message, poll: updatedPoll }; + + this.addMessageSorted((newMessage as unknown) as MessageResponse, false, false); + }; + + addPollVote = (pollVote: PollVote, poll: PollResponse, messageId: string) => { + const message = this.findMessage(messageId); + if (!message) return; + + if (message.poll_id !== pollVote.poll_id) return; + + const updatedPoll = { ...poll }; + const ownVotes = [...(message.poll.own_votes || [])]; + + if (pollVote.user_id === this._channel.getClient().userID) { + ownVotes.push(pollVote); + } + + updatedPoll.own_votes = ownVotes as PollVote[]; + const newMessage = { ...message, poll: updatedPoll }; + + this.addMessageSorted((newMessage as unknown) as MessageResponse, false, false); + }; + + removePollVote = ( + pollVote: PollVote, + poll: PollResponse, + messageId: string, + ) => { + const message = this.findMessage(messageId); + if (!message) return; + + if (message.poll_id !== pollVote.poll_id) return; + + const updatedPoll = { ...poll }; + const ownVotes = [...(message.poll.own_votes || [])]; + if (pollVote.user_id === this._channel.getClient().userID) { + const index = ownVotes.findIndex((vote) => vote.option_id === pollVote.option_id); + if (index > -1) { + ownVotes.splice(index, 1); + } + } + + updatedPoll.own_votes = ownVotes as PollVote[]; + + const newMessage = { ...message, poll: updatedPoll }; + this.addMessageSorted((newMessage as unknown) as MessageResponse, false, false); + }; + + updatePoll = (poll: PollResponse, messageId: string) => { + const message = this.findMessage(messageId); + if (!message) return; + + const updatedPoll = { + ...poll, + own_votes: [...(message.poll?.own_votes || [])], + }; + + const newMessage = { ...message, poll: updatedPoll }; + + this.addMessageSorted((newMessage as unknown) as MessageResponse, false, false); + }; + /** * Updates the message.user property with updated user object, for messages. * diff --git a/src/client.ts b/src/client.ts index c194298c3..67e5a7218 100644 --- a/src/client.ts +++ b/src/client.ts @@ -117,9 +117,14 @@ import { OGAttachment, OwnUserResponse, PartialMessageUpdate, + PartialPollUpdate, PartialUserUpdate, PermissionAPIResponse, PermissionsAPIResponse, + PollData, + PollOptionData, + PollVoteData, + PollVotesAPIResponse, PushProvider, PushProviderConfig, PushProviderID, @@ -127,6 +132,7 @@ import { PushProviderUpsertResponse, QueryChannelsAPIResponse, QuerySegmentsOptions, + QueryPollsResponse, ReactionResponse, ReactivateUserOptions, ReactivateUsersOptions, @@ -173,6 +179,20 @@ import { QuerySegmentTargetsFilter, SortParam, GetMessageOptions, + QueryVotesFilters, + VoteSort, + CreatePollAPIResponse, + GetPollAPIResponse, + UpdatePollAPIResponse, + CreatePollOptionAPIResponse, + GetPollOptionAPIResponse, + UpdatePollOptionAPIResponse, + PollVote, + CastVoteAPIResponse, + QueryPollsFilters, + PollSort, + QueryPollsOptions, + QueryVotesOptions, } from './types'; import { InsightMetrics, postInsights } from './insights'; import { Thread } from './thread'; @@ -3406,4 +3426,190 @@ export class StreamChat(this.baseURL + `/messages/${id}/commit`); } + + /** + * Creates a poll + * @param params PollData The poll that will be created + * @returns {APIResponse & CreatePollAPIResponse} The poll + */ + async createPoll(poll: PollData) { + return await this.post(this.baseURL + `/polls`, poll); + } + + /** + * Retrieves a poll + * @param id string The poll id + * @returns {APIResponse & GetPollAPIResponse} The poll + */ + async getPoll(id: string, userId?: string): Promise { + return await this.get(this.baseURL + `/polls/${id}`, { + ...(userId ? { user_id: userId } : {}), + }); + } + + /** + * Updates a poll + * @param poll PollData The poll that will be updated + * @returns {APIResponse & PollResponse} The poll + */ + async updatePoll(poll: PollData) { + return await this.put(this.baseURL + `/polls`, poll); + } + + /** + * Partially updates a poll + * @param id string The poll id + * @param {PartialPollUpdate} partialPollObject which should contain id and any of "set" or "unset" params; + * example: {id: "44f26af5-f2be-4fa7-9dac-71cf893781de", set:{field: value}, unset:["field2"]} + * @returns {APIResponse & UpdatePollAPIResponse} The poll + */ + async partialUpdatePoll( + id: string, + partialPollObject: PartialPollUpdate, + ): Promise { + return await this.patch(this.baseURL + `/polls/${id}`, partialPollObject); + } + + /** + * Delete a poll + * @param id string The poll id + * @param userId string The user id (only serverside) + * @returns + */ + async deletePoll(id: string, userId?: string): Promise { + return await this.delete(this.baseURL + `/polls/${id}`, { + ...(userId ? { user_id: userId } : {}), + }); + } + + /** + * Close a poll + * @param id string The poll id + * @returns {APIResponse & UpdatePollAPIResponse} The poll + */ + async closePoll(id: string): Promise { + return this.partialUpdatePoll(id, { + set: { + is_closed: true, + }, + }); + } + + /** + * Creates a poll option + * @param pollId string The poll id + * @param option PollOptionData The poll option that will be created + * @returns {APIResponse & PollOptionResponse} The poll option + */ + async createPollOption(pollId: string, option: PollOptionData) { + return await this.post( + this.baseURL + `/polls/${pollId}/options`, + option, + ); + } + + /** + * Retrieves a poll option + * @param pollId string The poll id + * @param optionId string The poll option id + * @returns {APIResponse & PollOptionResponse} The poll option + */ + async getPollOption(pollId: string, optionId: string) { + return await this.get( + this.baseURL + `/polls/${pollId}/options/${optionId}`, + ); + } + + /** + * Updates a poll option + * @param pollId string The poll id + * @param option PollOptionData The poll option that will be updated + * @returns + */ + async updatePollOption(pollId: string, option: PollOptionData) { + return await this.put(this.baseURL + `/polls/${pollId}/options`, option); + } + + /** + * Delete a poll option + * @param pollId string The poll id + * @param optionId string The poll option id + * @returns {APIResponse} The poll option + */ + async deletePollOption(pollId: string, optionId: string) { + return await this.delete(this.baseURL + `/polls/${pollId}/options/${optionId}`); + } + + /** + * Cast vote on a poll + * @param messageId string The message id + * @param pollId string The poll id + * @param vote PollVoteData The vote that will be casted + * @returns {APIResponse & CastVoteAPIResponse} The poll vote + */ + async castPollVote(messageId: string, pollId: string, vote: PollVoteData, options = {}) { + return await this.post( + this.baseURL + `/messages/${messageId}/polls/${pollId}/vote`, + { vote, ...options }, + ); + } + + /** + * Add a poll answer + * @param messageId string The message id + * @param pollId string The poll id + * @param answerText string The answer text + */ + async addPollAnswer(messageId: string, pollId: string, answerText: string) { + return this.castPollVote(messageId, pollId, { + answer_text: answerText, + }); + } + + async removePollVote(messageId: string, pollId: string, voteId: string) { + return await this.delete( + this.baseURL + `/messages/${messageId}/polls/${pollId}/vote/${voteId}`, + ); + } + + /** + * Queries polls + * @param filter + * @param sort + * @param options Option object, {limit: 10, offset:0} + * @returns {APIResponse & QueryPollsResponse} The polls + */ + async queryPolls( + filter: QueryPollsFilters = {}, + sort: PollSort = [], + options: QueryPollsOptions = {}, + ): Promise { + return await this.post(this.baseURL + '/polls/query', { + filter, + sort: normalizeQuerySort(sort), + ...options, + }); + } + + /** + * Queries poll votes + * @param pollId + * @param filter + * @param sort + * @param options Option object, {limit: 10, offset:0} + + * @returns {APIResponse & PollVotesAPIResponse} The poll votes + */ + async queryPollVotes( + pollId: string, + filter: QueryVotesFilters = {}, + sort: VoteSort = [], + options: QueryVotesOptions = {}, + ): Promise { + return await this.post(this.baseURL + `/polls/${pollId}/votes`, { + filter, + sort: normalizeQuerySort(sort), + ...options, + }); + } } diff --git a/src/events.ts b/src/events.ts index c0a762dee..2bb1f5482 100644 --- a/src/events.ts +++ b/src/events.ts @@ -30,6 +30,11 @@ export const EVENT_MAP = { 'notification.mutes_updated': true, 'notification.removed_from_channel': true, 'notification.thread_message_new': true, + 'poll.closed': true, + 'poll.updated': true, + 'poll.vote_casted': true, + 'poll.vote_changed': true, + 'poll.vote_removed': true, 'reaction.deleted': true, 'reaction.new': true, 'reaction.updated': true, diff --git a/src/thread.ts b/src/thread.ts index 788651357..1cc402093 100644 --- a/src/thread.ts +++ b/src/thread.ts @@ -31,24 +31,38 @@ export class Thread; read: ThreadReadStatus = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: Record = {}; constructor(client: StreamChat, t: ThreadResponse) { - this.id = t.parent_message.id; - this.message = formatMessage(t.parent_message); - this.latestReplies = t.latest_replies.map(formatMessage); - this.participants = t.thread_participants; - this.replyCount = t.reply_count; - this.channel = t.channel; + const { + parent_message_id, + parent_message, + latest_replies, + thread_participants, + reply_count, + channel, + read, + ...data + } = t; + + this.id = parent_message_id; + this.message = formatMessage(parent_message); + this.latestReplies = latest_replies.map(formatMessage); + this.participants = thread_participants; + this.replyCount = reply_count; + this.channel = channel; this._channel = client.channel(t.channel.type, t.channel.id); this._client = client; - if (t.read) { - for (const r of t.read) { + if (read) { + for (const r of read) { this.read[r.user.id] = { ...r, last_read: new Date(r.last_read), }; } } + this.data = data; } getClient(): StreamChat { diff --git a/src/types.ts b/src/types.ts index 8ad93215e..f391fcd96 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,8 @@ export type DefaultGenerics = { commandType: LiteralStringForUnion; eventType: UR; messageType: UR; + pollOptionType: UR; + pollType: UR; reactionType: UR; userType: UR; }; @@ -51,6 +53,8 @@ export type ExtendableGenerics = { commandType: string; eventType: UR; messageType: UR; + pollOptionType: UR; + pollType: UR; reactionType: UR; userType: UR; }; @@ -93,6 +97,7 @@ export type AppSettingsAPIResponse, @@ -614,6 +621,7 @@ export type MessageResponse< export type MessageResponseBase< StreamChatGenerics extends ExtendableGenerics = DefaultGenerics > = MessageBase & { + poll: PollResponse; type: MessageLabel; args?: string; before_message_send_failed?: boolean; @@ -890,6 +898,7 @@ export type CreateChannelOptions; + poll_vote?: PollVote; queriedChannels?: { channels: ChannelAPIResponse[]; isLatestMessageSet?: boolean; @@ -1365,6 +1376,8 @@ export type ChannelFilters, @@ -1378,6 +1391,8 @@ export type ChannelFilters[Key] @@ -1390,6 +1405,8 @@ export type ChannelFilters[Key] @@ -1397,6 +1414,94 @@ export type ChannelFilters; +export type QueryPollsOptions = Pager; + +export type VotesFiltersOptions = { + is_answer?: boolean; + option_id?: string; + user_id?: string; +}; + +export type QueryVotesOptions = Pager; + +export type QueryPollsFilters = QueryFilters< + { + id?: RequireOnlyOne, '$eq' | '$in'>> | PrimitiveFilter; + } & { + user_id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + is_closed?: + | RequireOnlyOne, '$eq'>> + | PrimitiveFilter; + } & { + max_votes_allowed?: + | RequireOnlyOne< + Pick, '$eq' | '$ne' | '$gt' | '$lt' | '$gte' | '$lte'> + > + | PrimitiveFilter; + } & { + allow_answers?: + | RequireOnlyOne, '$eq'>> + | PrimitiveFilter; + } & { + allow_user_suggested_options?: + | RequireOnlyOne, '$eq'>> + | PrimitiveFilter; + } & { + voting_visibility?: + | RequireOnlyOne, '$eq'>> + | PrimitiveFilter; + } & { + created_at?: + | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | PrimitiveFilter; + } & { + created_by_id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + updated_at?: + | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | PrimitiveFilter; + } & { + name?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } +>; + +export type QueryVotesFilters = QueryFilters< + { + id?: RequireOnlyOne, '$eq' | '$in'>> | PrimitiveFilter; + } & { + option_id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + is_answer?: + | RequireOnlyOne, '$eq'>> + | PrimitiveFilter; + } & { + user_id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + created_at?: + | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | PrimitiveFilter; + } & { + created_by_id?: + | RequireOnlyOne, '$eq' | '$in'>> + | PrimitiveFilter; + } & { + updated_at?: + | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | PrimitiveFilter; + } +>; + export type ContainsOperator = { [Key in keyof CustomType]?: CustomType[Key] extends (infer ContainType)[] ? @@ -1429,6 +1534,8 @@ export type MessageFilters, @@ -1442,6 +1549,8 @@ export type MessageFilters[Key] @@ -1454,6 +1563,8 @@ export type MessageFilters[Key] @@ -1532,6 +1643,8 @@ export type UserFilters, @@ -1545,6 +1658,8 @@ export type UserFilters[Key] @@ -1557,6 +1672,8 @@ export type UserFilters[Key] @@ -1631,6 +1748,26 @@ export type QuerySort | UserSort; +export type PollSort = PollSortBase | Array; + +export type PollSortBase = { + created_at?: AscDesc; + id?: AscDesc; + is_closed?: AscDesc; + name?: AscDesc; + updated_at?: AscDesc; +}; + +export type VoteSort = VoteSortBase | Array; + +export type VoteSortBase = { + created_at?: AscDesc; + id?: AscDesc; + is_closed?: AscDesc; + name?: AscDesc; + updated_at?: AscDesc; +}; + /** * Base Types */ @@ -1820,6 +1957,7 @@ export type ChannelConfigFields = { message_retention?: string; mutes?: boolean; name?: string; + polls?: boolean; push_notifications?: boolean; quotes?: boolean; reactions?: boolean; @@ -2103,7 +2241,8 @@ export type EndpointName = | 'ListImports' | 'UpsertPushProvider' | 'DeletePushProvider' - | 'ListPushProviders'; + | 'ListPushProviders' + | 'CreatePoll'; export type ExportChannelRequest = { id: string; @@ -2203,6 +2342,7 @@ export type MessageBase< pin_expires?: string | null; pinned?: boolean; pinned_at?: string | null; + poll_id?: string; quoted_message_id?: string; show_in_channel?: boolean; silent?: boolean; @@ -2479,8 +2619,8 @@ export type DeleteType = 'soft' | 'hard' | 'pruning'; DeleteUserOptions specifies a collection of one or more `user_ids` to be deleted. `user`: - - soft: marks user as deleted and retains all user data - - pruning: marks user as deleted and nullifies user information + - soft: marks user as deleted and retains all user data + - pruning: marks user as deleted and nullifies user information - hard: deletes user completely - this requires hard option for messages and conversation as well `conversations`: - soft: marks all conversation channels as deleted (same effect as Delete Channels with 'hard' option disabled) @@ -2743,3 +2883,152 @@ export class ErrorFromResponse extends Error { response?: AxiosResponse; status?: number; } + +export type QueryPollsResponse = { + polls: PollResponse[]; + next?: string; +}; + +export type CreatePollAPIResponse = { + poll: PollResponse; +}; + +export type GetPollAPIResponse = { + poll: PollResponse; +}; + +export type UpdatePollAPIResponse = { + poll: PollResponse; +}; + +export type PollResponse< + StreamChatGenerics extends ExtendableGenerics = DefaultGenerics +> = StreamChatGenerics['pollType'] & { + answers_count: number; + created_at: string; + created_by: UserResponse | null; + created_by_id: string; + enforce_unique_vote: boolean; + id: string; + latest_answers: PollVote[]; + latest_votes_by_option: Record[]>; + max_votes_allowed: number; + name: string; + options: PollOption[]; + updated_at: string; + vote_count: number; + vote_counts_by_option: Record; + allow_answers?: boolean; + allow_user_suggested_options?: boolean; + channel?: ChannelAPIResponse | null; + cid?: string; + description?: string; + is_closed?: boolean; + own_votes?: PollVote[]; + voting_visibility?: VotingVisibility; +}; + +export type PollOption = { + created_at: string; + id: string; + poll_id: string; + text: string; + updated_at: string; + vote_count: number; + votes?: PollVote[]; +}; + +export enum VotingVisibility { + anonymous = 'anonymous', + public = 'public', +} + +export type PollData< + StreamChatGenerics extends ExtendableGenerics = DefaultGenerics +> = StreamChatGenerics['pollType'] & { + name: string; + allow_answers?: boolean; + allow_user_suggested_options?: boolean; + description?: string; + enforce_unique_vote?: boolean; + id?: string; + is_closed?: boolean; + max_votes_allowed?: number; + options?: PollOptionData[]; + user_id?: string; + voting_visibility?: VotingVisibility; +}; + +export type PartialPollUpdate = { + // id: string; + set?: Partial>; + unset?: Array>; +}; + +export type PollOptionData< + StreamChatGenerics extends ExtendableGenerics = DefaultGenerics +> = StreamChatGenerics['pollType'] & { + text: string; + id?: string; + position?: number; +}; + +export type PartialPollOptionUpdate = { + set?: Partial>; + unset?: Array>; +}; + +export type PollVoteData = { + answer_text?: string; + is_answer?: boolean; + option_id?: string; +}; + +export type PollPaginationOptions = { + limit?: number; + next?: string; +}; + +export type CreatePollOptionAPIResponse = { + poll_option: PollOptionResponse; +}; + +export type GetPollOptionAPIResponse< + StreamChatGenerics extends ExtendableGenerics = DefaultGenerics +> = CreatePollOptionAPIResponse; +export type UpdatePollOptionAPIResponse< + StreamChatGenerics extends ExtendableGenerics = DefaultGenerics +> = CreatePollOptionAPIResponse; + +export type PollOptionResponse< + StreamChatGenerics extends ExtendableGenerics = DefaultGenerics +> = StreamChatGenerics['pollType'] & { + created_at: string; + id: string; + poll_id: string; + position: number; + text: string; + updated_at: string; + vote_count: number; + votes?: PollVote[]; +}; + +export type PollVote = { + created_at: string; + id: string; + is_answer: boolean; + poll_id: string; + user_id: string; + answer_text?: string; + option_id?: string; + user?: UserResponse; +}; + +export type PollVotesAPIResponse = { + votes: PollVote[]; + next?: string; +}; + +export type CastVoteAPIResponse = { + vote: PollVote; +}; From f549449ec6b6d22824d10949c2dc2cd5cc62c00e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:41:24 +0200 Subject: [PATCH 2/6] chore: release v8.26.0 (#1276) Co-authored-by: github-actions --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d5154d8..a81453042 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [8.26.0](https://github.com/GetStream/stream-chat-js/compare/v8.25.1...v8.26.0) (2024-04-12) + + +### Features + +* polls feature endpoints ([#1269](https://github.com/GetStream/stream-chat-js/issues/1269)) ([1d81480](https://github.com/GetStream/stream-chat-js/commit/1d8148072af1d899955a3d4b1e9b1957322961ed)) + ### [8.25.1](https://github.com/GetStream/stream-chat-js/compare/v8.25.0...v8.25.1) (2024-03-28) diff --git a/package.json b/package.json index 23de57d3c..ea4316eaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stream-chat", - "version": "8.25.1", + "version": "8.26.0", "description": "JS SDK for the Stream Chat API", "author": "GetStream", "homepage": "https://getstream.io/chat/", From ef21c1042ab9982600c946bb3a965fde3bdaf0da Mon Sep 17 00:00:00 2001 From: Federico Guerinoni <41150432+guerinoni@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:03:54 +0200 Subject: [PATCH 3/6] feat: implement queryReactions (#1279) --- src/client.ts | 35 +++++++++++++++++++++++++++++++++++ src/types.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/client.ts b/src/client.ts index 67e5a7218..e663068c5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -193,6 +193,10 @@ import { PollSort, QueryPollsOptions, QueryVotesOptions, + ReactionFilters, + ReactionSort, + QueryReactionsAPIResponse, + QueryReactionsOptions, } from './types'; import { InsightMetrics, postInsights } from './insights'; import { Thread } from './thread'; @@ -1590,6 +1594,37 @@ export class StreamChat} filter object MongoDB style filters + * @param {ReactionSort} [sort] Sort options, for instance {created_at: -1}. + * @param {QueryReactionsOptions} [options] Pagination object + * + * @return {Promise<{ QueryReactionsAPIResponse } search channels response + */ + async queryReactions( + messageID: string, + filter: ReactionFilters, + sort: ReactionSort = [], + options: QueryReactionsOptions = {}, + ) { + // Make sure we wait for the connect promise if there is a pending one + await this.wsPromise; + + // Return a list of channels + const payload = { + filter, + sort: normalizeQuerySort(sort), + ...options, + }; + + return await this.post>( + this.baseURL + '/messages/' + messageID + '/reactions', + payload, + ); + } + hydrateActiveChannels( channelsFromApi: ChannelAPIResponse[] = [], stateOptions: ChannelStateOptions = {}, diff --git a/src/types.ts b/src/types.ts index f391fcd96..59b17d378 100644 --- a/src/types.ts +++ b/src/types.ts @@ -293,6 +293,12 @@ export type ChannelResponse< updated_at?: string; }; +export type QueryReactionsOptions = Pager; + +export type QueryReactionsAPIResponse = APIResponse & { + reactions: ReactionResponse[]; +}; + export type QueryChannelsAPIResponse = APIResponse & { channels: Omit, keyof APIResponse>[]; }; @@ -1354,6 +1360,26 @@ export type BannedUsersFilters = QueryFilters< } >; +export type ReactionFilters = QueryFilters< + { + user_id?: + | RequireOnlyOne['user_id']>, '$eq' | '$in'>> + | PrimitiveFilter['user_id']>; + } & { + type?: + | RequireOnlyOne['type']>, '$eq'>> + | PrimitiveFilter['type']>; + } & { + created_at?: + | RequireOnlyOne, '$eq' | '$gt' | '$lt' | '$gte' | '$lte'>> + | PrimitiveFilter; + } & { + [Key in keyof Omit, 'user_id' | 'type' | 'created_at'>]: RequireOnlyOne< + QueryFilter[Key]> + >; + } +>; + export type ChannelFilters = QueryFilters< ContainsOperator & { members?: @@ -1689,6 +1715,16 @@ export type BannedUsersSort = BannedUsersSortBase | Array; export type BannedUsersSortBase = { created_at?: AscDesc }; +export type ReactionSort = + | ReactionSortBase + | Array>; + +export type ReactionSortBase = Sort< + StreamChatGenerics['reactionType'] +> & { + created_at?: AscDesc; +}; + export type ChannelSort = | ChannelSortBase | Array>; From cf311e09364535766b4c485492ed933b36470def Mon Sep 17 00:00:00 2001 From: Vishal Narkhede Date: Wed, 24 Apr 2024 15:25:00 +0200 Subject: [PATCH 4/6] fix: make poll type optional on message response (#1281) --- src/channel_state.ts | 6 +++--- src/types.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/channel_state.ts b/src/channel_state.ts index 0e573986c..34023b9d1 100644 --- a/src/channel_state.ts +++ b/src/channel_state.ts @@ -500,7 +500,7 @@ export class ChannelState vote.option_id === pollVote.option_id); if (index > -1) { diff --git a/src/types.ts b/src/types.ts index 59b17d378..9be9a093d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -627,7 +627,6 @@ export type MessageResponse< export type MessageResponseBase< StreamChatGenerics extends ExtendableGenerics = DefaultGenerics > = MessageBase & { - poll: PollResponse; type: MessageLabel; args?: string; before_message_send_failed?: boolean; @@ -649,6 +648,7 @@ export type MessageResponseBase< pin_expires?: string | null; pinned_at?: string | null; pinned_by?: UserResponse | null; + poll?: PollResponse; reaction_counts?: { [key: string]: number } | null; reaction_scores?: { [key: string]: number } | null; reply_count?: number; From 63facdd487e671c05786b030557d973e6c5101e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:27:31 +0200 Subject: [PATCH 5/6] chore: release v8.27.0 (#1282) Co-authored-by: github-actions Co-authored-by: Vishal Narkhede --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a81453042..5b0354f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [8.27.0](https://github.com/GetStream/stream-chat-js/compare/v8.26.0...v8.27.0) (2024-04-24) + + +### Features + +* implement queryReactions ([#1279](https://github.com/GetStream/stream-chat-js/issues/1279)) ([ef21c10](https://github.com/GetStream/stream-chat-js/commit/ef21c1042ab9982600c946bb3a965fde3bdaf0da)) + ## [8.26.0](https://github.com/GetStream/stream-chat-js/compare/v8.25.1...v8.26.0) (2024-04-12) diff --git a/package.json b/package.json index ea4316eaa..7cf15afa4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stream-chat", - "version": "8.26.0", + "version": "8.27.0", "description": "JS SDK for the Stream Chat API", "author": "GetStream", "homepage": "https://getstream.io/chat/", From 0d5f87fe29dc946044dedd1ad6df0e8780a04e8f Mon Sep 17 00:00:00 2001 From: Federico Guerinoni <41150432+guerinoni@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:29:29 +0200 Subject: [PATCH 6/6] feat: add reactiongroups in `MessageResponse` (#1278) Signed-off-by: Federico Guerinoni --- src/types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/types.ts b/src/types.ts index 9be9a093d..2db5673b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -627,6 +627,7 @@ export type MessageResponse< export type MessageResponseBase< StreamChatGenerics extends ExtendableGenerics = DefaultGenerics > = MessageBase & { + reaction_groups: Record; type: MessageLabel; args?: string; before_message_send_failed?: boolean; @@ -658,6 +659,13 @@ export type MessageResponseBase< updated_at?: string; }; +export type ReactionGroupResponse = { + count: number; + sum_scores: number; + first_reaction_at?: string; + last_reaction_at?: string; +}; + export type ModerationDetailsResponse = { action: 'MESSAGE_RESPONSE_ACTION_BOUNCE' | (string & {}); error_msg: string;