diff --git a/package.json b/package.json index 396e8d3a..a679982e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "license": "See license in LICENSE", "workspaces": [ "packages/*", - "sample-apps/*/*" + "sample-apps/**" ], "scripts": { "build:all": "yarn workspaces foreach -v --topological-dev run build", diff --git a/packages/feeds-client/__integration-tests__/docs-snippets/comments.test.ts b/packages/feeds-client/__integration-tests__/docs-snippets/comments.test.ts index c8972109..736a1536 100644 --- a/packages/feeds-client/__integration-tests__/docs-snippets/comments.test.ts +++ b/packages/feeds-client/__integration-tests__/docs-snippets/comments.test.ts @@ -65,7 +65,6 @@ describe('Comments page', () => { }); it(`Reading comments`, async () => { - await feed.getOrCreate(); // Supported values for sort: first, last, top, controversial, best @@ -116,6 +115,8 @@ describe('Comments page', () => { await client.addCommentReaction({ id: comment.id, type: 'like', + // When set to true, and there is an existing reaction from the user, the reaction will be updated instead of creating a new one + enforce_unique: true, }); await client.deleteCommentReaction({ diff --git a/packages/feeds-client/__integration-tests__/docs-snippets/reactions.test.ts b/packages/feeds-client/__integration-tests__/docs-snippets/reactions.test.ts index f134357c..2fd50306 100644 --- a/packages/feeds-client/__integration-tests__/docs-snippets/reactions.test.ts +++ b/packages/feeds-client/__integration-tests__/docs-snippets/reactions.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, it } from 'vitest'; import { createTestClient, createTestTokenGenerator, @@ -6,13 +6,18 @@ import { } from '../utils'; import type { FeedsClient } from '../../src/feeds-client'; import type { Feed } from '../../src/feed'; -import type { ActivityResponse, UserRequest } from '../../src/gen/models'; +import type { + ActivityResponse, + CommentResponse, + UserRequest, +} from '../../src/gen/models'; describe('Reactions page', () => { let client: FeedsClient; const user: UserRequest = getTestUser(); let feed: Feed; let activity: ActivityResponse; + let comment: CommentResponse; beforeAll(async () => { client = createTestClient(); @@ -25,35 +30,63 @@ describe('Reactions page', () => { text: 'Hello, world!', }) ).activity; + comment = ( + await client.addComment({ + object_id: activity.id, + object_type: 'activity', + comment: 'Great post!', + }) + ).comment; }); it(`Reactions`, async () => { + // Add a reaction to an activity const addResponse = await client.addActivityReaction({ - activity_id: activity.id, + activity_id: 'activity_123', type: 'like', custom: { emoji: '❤️', }, + // Optionally override existing reaction + enforce_unique: true, }); - expect(addResponse.reaction).toBeDefined(); + console.log(addResponse.reaction); + // Adding a reaction without triggering push notifications + await client.addActivityReaction({ + activity_id: 'activity_123', + type: 'like', + custom: { + emoji: '❤️', + }, + skip_push: true, + }); + + // Add a reaction to a comment + await client.addCommentReaction({ + id: comment.id, + type: 'like', + custom: { + emoji: '👍', + }, + // Optionally override existing reaction + enforce_unique: true, + }); + // Adding a comment reaction without triggering push notifications + await client.addCommentReaction({ + id: comment.id, + type: 'like', + custom: { + emoji: '👍', + }, + skip_push: true, + }); const deleteResponse = await client.deleteActivityReaction({ activity_id: activity.id, type: 'like', }); - - expect(deleteResponse.reaction).toBeDefined(); - - expect( - feed.state.getLatestValue().activities?.[0].own_reactions, - ).toBeDefined(); - expect( - feed.state.getLatestValue().activities?.[0].latest_reactions, - ).toBeDefined(); - expect( - feed.state.getLatestValue().activities?.[0].reaction_groups, - ).toBeDefined(); + console.log(deleteResponse.reaction); }); afterAll(async () => { diff --git a/packages/feeds-client/src/feed/event-handlers/activity/handle-activity-reaction-updated.test.ts b/packages/feeds-client/src/feed/event-handlers/activity/handle-activity-reaction-updated.test.ts new file mode 100644 index 00000000..0477488d --- /dev/null +++ b/packages/feeds-client/src/feed/event-handlers/activity/handle-activity-reaction-updated.test.ts @@ -0,0 +1,282 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Feed, handleActivityReactionUpdated } from '../../../feed'; +import { FeedsClient } from '../../../feeds-client'; +import { + generateActivityPinResponse, + generateActivityResponse, + generateFeedResponse, + generateOwnUser, + getHumanId, + generateActivityReactionUpdatedEvent, + generateFeedReactionResponse, +} from '../../../test-utils'; +import { shouldUpdateState } from '../../../utils'; +import type { EventPayload } from '../../../types-internal'; + +describe(handleActivityReactionUpdated.name, () => { + let feed: Feed; + let client: FeedsClient; + let currentUserId: string; + let activityId: string; + + beforeEach(() => { + client = new FeedsClient('mock-api-key'); + currentUserId = getHumanId(); + activityId = getHumanId(); + client.state.partialNext({ + connected_user: generateOwnUser({ id: currentUserId }), + }); + const feedResponse = generateFeedResponse({ + id: 'main', + group_id: 'user', + created_by: { id: currentUserId }, + }); + feed = new Feed( + client, + feedResponse.group_id, + feedResponse.id, + feedResponse, + ); + }); + + it('updates the reaction in the correct activity for current user & updates activities with event.activity', () => { + const event = generateActivityReactionUpdatedEvent({ + reaction: { + user: { id: currentUserId }, + type: 'downvote', + activity_id: activityId, + }, + activity: { + reaction_count: 1, + latest_reactions: [], + reaction_groups: {}, + }, + }); + + const existingReaction = generateFeedReactionResponse({ + user: { id: currentUserId }, + type: 'like', + activity_id: activityId, + }); + + const activity = generateActivityResponse({ + id: event.activity.id, + reaction_count: 1, + own_reactions: [existingReaction], + latest_reactions: [], + reaction_groups: {}, + }); + const activityPin = generateActivityPinResponse({ + activity: { ...activity }, + }); + feed.state.partialNext({ + activities: [activity], + pinned_activities: [activityPin], + }); + + const stateBefore = feed.currentState; + + expect(stateBefore.activities![0].reaction_count).toBe(1); + expect(stateBefore.pinned_activities![0].activity.reaction_count).toBe(1); + + handleActivityReactionUpdated.call(feed, event); + + const stateAfter = feed.currentState; + + expect(stateAfter.activities![0].own_reactions).toContain(event.reaction); + expect(stateAfter.pinned_activities![0].activity.own_reactions).toContain( + event.reaction, + ); + expect(stateAfter.activities![0].own_bookmarks).toBe( + stateBefore.activities![0].own_bookmarks, + ); + expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toBe( + stateBefore.pinned_activities![0].activity.own_bookmarks, + ); + expect(stateAfter.activities![0].reaction_count).toBe(1); + expect(stateAfter.pinned_activities![0].activity.reaction_count).toBe(1); + }); + + it('does not update own_reactions if the reaction is from another user but still updates activity', () => { + const event = generateActivityReactionUpdatedEvent({ + reaction: { + user: { id: 'other-user-id' }, + type: 'downvote', + activity_id: activityId, + }, + activity: { + reaction_count: 2, + }, + }); + + const existingReaction = generateFeedReactionResponse({ + user: { id: currentUserId }, + type: 'like', + activity_id: activityId, + }); + + const activity = generateActivityResponse({ + id: event.activity.id, + reaction_count: 1, + own_reactions: [existingReaction], + latest_reactions: [], + reaction_groups: {}, + }); + const activityPin = generateActivityPinResponse({ + activity: { ...activity }, + }); + feed.state.partialNext({ + activities: [activity], + pinned_activities: [activityPin], + }); + + const stateBefore = feed.currentState; + + expect(stateBefore.activities![0].reaction_count).toBe(1); + expect(stateBefore.pinned_activities![0].activity.reaction_count).toBe(1); + + handleActivityReactionUpdated.call(feed, event); + + const stateAfter = feed.currentState; + + expect(stateAfter.activities![0].own_reactions).toHaveLength(1); + expect( + stateAfter.pinned_activities![0].activity.own_reactions, + ).toHaveLength(1); + expect(stateAfter.activities![0].reaction_count).toBe(2); + expect(stateAfter.pinned_activities![0].activity.reaction_count).toBe(2); + expect(stateAfter.activities![0].own_bookmarks).toBe( + stateBefore.activities![0].own_bookmarks, + ); + expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toBe( + stateBefore.pinned_activities![0].activity.own_bookmarks, + ); + expect(stateAfter.activities![0].own_reactions).toBe( + stateBefore.activities![0].own_reactions, + ); + expect(stateAfter.pinned_activities![0].activity.own_reactions).toBe( + stateBefore.pinned_activities![0].activity.own_reactions, + ); + }); + + it('does nothing if activity is not found', () => { + const event = generateActivityReactionUpdatedEvent({ + reaction: { user: { id: currentUserId } }, + }); + const activity = generateActivityResponse({ + id: 'unrelated', + }); + const activityPin = generateActivityPinResponse({ + activity: { ...activity }, + }); + feed.state.partialNext({ + activities: [activity], + pinned_activities: [activityPin], + }); + + const stateBefore = feed.currentState; + + handleActivityReactionUpdated.call(feed, event); + + const stateAfter = feed.currentState; + + expect(stateAfter).toBe(stateBefore); + }); + + describe(`Activity reaction updated ${shouldUpdateState.name} integration`, () => { + let currentUserPayload: EventPayload<'feeds.activity.reaction.updated'>; + + beforeEach(() => { + currentUserPayload = generateActivityReactionUpdatedEvent({ + reaction: { user: { id: currentUserId }, activity_id: activityId }, + activity: { id: activityId }, + }); + + feed.state.partialNext({ activities: [currentUserPayload.activity] }); + feed.state.partialNext({ watch: true }); + }); + + it(`skips update if ${shouldUpdateState.name} returns false`, () => { + // 1. HTTP and then WS + + handleActivityReactionUpdated.call(feed, currentUserPayload, false); + + let stateBefore = feed.currentState; + + handleActivityReactionUpdated.call(feed, currentUserPayload); + + let stateAfter = feed.currentState; + + expect(stateAfter).toBe(stateBefore); + // @ts-expect-error Using Feed internals for tests only + expect(feed.stateUpdateQueue.size).toEqual(0); + + // 2. WS and the HTTP + + handleActivityReactionUpdated.call(feed, currentUserPayload); + + stateBefore = feed.currentState; + + handleActivityReactionUpdated.call(feed, currentUserPayload, false); + + stateAfter = feed.currentState; + + expect(stateAfter).toBe(stateBefore); + // @ts-expect-error Using Feed internals for tests only + expect(feed.stateUpdateQueue.size).toEqual(0); + }); + + it('allows update again from WS after clearing the stateUpdateQueue', () => { + handleActivityReactionUpdated.call(feed, currentUserPayload); + + // Clear the queue + (feed as any).stateUpdateQueue.clear(); + + // Now update should be allowed from another WS event + handleActivityReactionUpdated.call(feed, currentUserPayload); + + const activities = feed.currentState.activities!; + const activity = activities.find((a) => a.id === activityId); + const [latestReaction] = activity?.own_reactions ?? []; + + expect(activity?.own_reactions.length).toEqual(1); + expect(latestReaction).toMatchObject(currentUserPayload.reaction); + }); + + it('allows update again from HTTP response after clearing the stateUpdateQueue', () => { + handleActivityReactionUpdated.call(feed, currentUserPayload, false); + + // Clear the queue + (feed as any).stateUpdateQueue.clear(); + + // Now update should be allowed from another HTTP response + handleActivityReactionUpdated.call(feed, currentUserPayload, false); + + const activities = feed.currentState.activities!; + const activity = activities.find((a) => a.id === activityId); + const [latestReaction] = activity?.own_reactions ?? []; + + expect(activity?.own_reactions.length).toEqual(1); + expect(latestReaction).toMatchObject(currentUserPayload.reaction); + }); + + it('should not insert anything into the stateUpdateQueue if the connected_user did not trigger the reaction', () => { + const otherUserPayload = generateActivityReactionUpdatedEvent({ + reaction: { user: { id: getHumanId() }, activity_id: activityId }, + activity: { id: activityId }, + }); + + handleActivityReactionUpdated.call(feed, otherUserPayload); + + expect((feed as any).stateUpdateQueue).toEqual(new Set()); + + handleActivityReactionUpdated.call(feed, otherUserPayload); + + const activities = feed.currentState.activities!; + const activity = activities.find((a) => a.id === activityId); + + expect((feed as any).stateUpdateQueue).toEqual(new Set()); + expect(activity?.own_reactions.length).toEqual(0); + }); + }); +}); diff --git a/packages/feeds-client/src/feed/event-handlers/activity/handle-activity-reaction-updated.ts b/packages/feeds-client/src/feed/event-handlers/activity/handle-activity-reaction-updated.ts new file mode 100644 index 00000000..da2669ad --- /dev/null +++ b/packages/feeds-client/src/feed/event-handlers/activity/handle-activity-reaction-updated.ts @@ -0,0 +1,140 @@ +import type { Feed } from '../../feed'; +import type { + ActivityPinResponse, + ActivityResponse, +} from '../../../gen/models'; +import type { EventPayload, PartializeAllBut } from '../../../types-internal'; +import { + getStateUpdateQueueId, + shouldUpdateState, + updateEntityInArray, +} from '../../../utils'; + +export type ActivityReactionUpdatedPayload = PartializeAllBut< + EventPayload<'feeds.activity.reaction.updated'>, + 'activity' | 'reaction' +>; + +// shared function to update the activity with the new reaction +const sharedUpdateActivity = ({ + payload, + currentActivity, + eventBelongsToCurrentUser, +}: { + payload: ActivityReactionUpdatedPayload; + currentActivity: ActivityResponse; + eventBelongsToCurrentUser: boolean; +}) => { + const { activity: newActivity, reaction: newReaction } = payload; + let ownReactions = currentActivity.own_reactions; + + if (eventBelongsToCurrentUser) { + ownReactions = [newReaction]; + } + + return { + ...currentActivity, + latest_reactions: newActivity.latest_reactions, + reaction_groups: newActivity.reaction_groups, + reaction_count: newActivity.reaction_count, + own_reactions: ownReactions, + }; +}; + +export const updateReactionInActivities = ( + payload: ActivityReactionUpdatedPayload, + activities: ActivityResponse[] | undefined, + eventBelongsToCurrentUser: boolean, +) => + updateEntityInArray({ + entities: activities, + matcher: (activity) => activity.id === payload.activity.id, + updater: (matchedActivity) => + sharedUpdateActivity({ + payload, + currentActivity: matchedActivity, + eventBelongsToCurrentUser, + }), + }); + +export const updateReactionInPinnedActivities = ( + payload: ActivityReactionUpdatedPayload, + pinnedActivities: ActivityPinResponse[] | undefined, + eventBelongsToCurrentUser: boolean, +) => + updateEntityInArray({ + entities: pinnedActivities, + matcher: (pinnedActivity) => + pinnedActivity.activity.id === payload.activity.id, + updater: (matchedPinnedActivity) => { + const updatedActivity = sharedUpdateActivity({ + payload, + currentActivity: matchedPinnedActivity.activity, + eventBelongsToCurrentUser, + }); + + // this should never happen, but just in case + if (updatedActivity === matchedPinnedActivity.activity) { + return matchedPinnedActivity; + } + + return { + ...matchedPinnedActivity, + activity: updatedActivity, + }; + }, + }); + +export function handleActivityReactionUpdated( + this: Feed, + payload: ActivityReactionUpdatedPayload, + fromWs?: boolean, +) { + const connectedUser = this.client.state.getLatestValue().connected_user; + + const eventBelongsToCurrentUser = + typeof connectedUser !== 'undefined' && + payload.reaction.user.id === connectedUser.id; + + if ( + !shouldUpdateState({ + stateUpdateQueueId: getStateUpdateQueueId( + payload, + 'activity-reaction-updated', + ), + stateUpdateQueue: this.stateUpdateQueue, + watch: this.currentState.watch, + fromWs, + isTriggeredByConnectedUser: eventBelongsToCurrentUser, + }) + ) { + return; + } + + const { + activities: currentActivities, + pinned_activities: currentPinnedActivities, + } = this.currentState; + + const [result1, result2] = [ + this.hasActivity(payload.activity.id) + ? updateReactionInActivities( + payload, + currentActivities, + eventBelongsToCurrentUser, + ) + : undefined, + updateReactionInPinnedActivities( + payload, + currentPinnedActivities, + eventBelongsToCurrentUser, + ), + ]; + + if (result1?.changed || result2.changed) { + this.state.partialNext({ + ...(result1 ? { activities: result1.entities } : {}), + pinned_activities: result2.entities, + }); + } +} diff --git a/packages/feeds-client/src/feed/event-handlers/activity/index.ts b/packages/feeds-client/src/feed/event-handlers/activity/index.ts index 1b674810..3286e60c 100644 --- a/packages/feeds-client/src/feed/event-handlers/activity/index.ts +++ b/packages/feeds-client/src/feed/event-handlers/activity/index.ts @@ -4,4 +4,5 @@ export * from './handle-activity-removed-from-feed'; export * from './handle-activity-updated'; export * from './handle-activity-reaction-added'; export * from './handle-activity-reaction-deleted'; +export * from './handle-activity-reaction-updated'; export * from './handle-activity-marked'; diff --git a/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-added.ts b/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-added.ts index aca1aa10..554c4faa 100644 --- a/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-added.ts +++ b/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-added.ts @@ -1,5 +1,5 @@ import type { Feed } from '../../feed'; -import type { EventPayload} from '../../../types-internal'; +import type { EventPayload } from '../../../types-internal'; import { type PartializeAllBut } from '../../../types-internal'; import { getStateUpdateQueueId, shouldUpdateState } from '../../../utils'; @@ -53,7 +53,6 @@ export function handleCommentReactionAdded( newComments[commentIndex] = { ...newComments[commentIndex], reaction_count: comment.reaction_count ?? 0, - // TODO: FIXME this should be handled by the backend latest_reactions: comment.latest_reactions ?? [], reaction_groups: comment.reaction_groups ?? {}, own_reactions: ownReactions, diff --git a/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-updated.test.ts b/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-updated.test.ts new file mode 100644 index 00000000..b1b525b4 --- /dev/null +++ b/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-updated.test.ts @@ -0,0 +1,350 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { Feed, handleCommentReactionUpdated } from '../../../feed'; +import { FeedsClient } from '../../../feeds-client'; +import { + generateCommentResponse, + generateFeedResponse, + generateOwnUser, + getHumanId, + generateFeedReactionResponse, + generateCommentReactionUpdatedEvent, +} from '../../../test-utils'; +import type { + CommentResponse, + FeedsReactionResponse, +} from '../../../gen/models'; +import { shouldUpdateState } from '../../../utils'; +import type { EventPayload } from '../../../types-internal'; + +describe(handleCommentReactionUpdated.name, () => { + let feed: Feed; + let client: FeedsClient; + let currentUserId: string; + let activityId: string; + + beforeEach(() => { + client = new FeedsClient('mock-api-key'); + currentUserId = getHumanId(); + client.state.partialNext({ + connected_user: generateOwnUser({ id: currentUserId }), + }); + const feedResponse = generateFeedResponse({ + id: 'main', + group_id: 'user', + created_by: { id: currentUserId }, + }); + feed = new Feed( + client, + feedResponse.group_id, + feedResponse.id, + feedResponse, + ); + activityId = `activity-${getHumanId()}`; + }); + + it('updates own_reactions for current user and updates comment fields from the event', () => { + const existingCommentId = `comment-${getHumanId()}`; + const existingReaction = generateFeedReactionResponse({ + type: 'like', + user: { id: currentUserId }, + activity_id: activityId, + comment_id: existingCommentId, + }); + const existingComment = generateCommentResponse({ + id: existingCommentId, + object_id: activityId, + latest_reactions: [], + reaction_groups: {}, + own_reactions: [existingReaction], + }); + feed.state.partialNext({ + comments_by_entity_id: { + [activityId]: { + comments: [existingComment], + pagination: { sort: 'first' }, + }, + }, + }); + + const event = generateCommentReactionUpdatedEvent({ + comment: { + id: existingComment.id, + object_id: activityId, + latest_reactions: [], + reaction_groups: {}, + }, + reaction: { + type: 'downvote', + user: { id: currentUserId }, + activity_id: activityId, + comment_id: existingCommentId, + }, + }); + + const stateBefore = feed.currentState; + const [initialReaction] = (stateBefore.comments_by_entity_id[activityId] + ?.comments ?? [])[0].own_reactions; + expect( + stateBefore.comments_by_entity_id[activityId]?.comments?.[0] + ?.own_reactions, + ).toHaveLength(1); + expect(initialReaction).toBe(existingReaction); + + handleCommentReactionUpdated.call(feed, event); + + const stateAfter = feed.currentState; + const [updated] = stateAfter.comments_by_entity_id[activityId]!.comments!; + expect(updated.own_reactions).toHaveLength(1); + expect(updated.own_reactions[0]).toBe(event.reaction); + // ensure we used event's latest_reactions & reaction_groups (Object.is check) + expect(updated.latest_reactions).toBe(event.comment.latest_reactions); + expect(updated.reaction_groups).toBe(event.comment.reaction_groups); + }); + + it('does modify own_reactions if the target reaction belongs to another user', () => { + const existingComment = generateCommentResponse({ + object_id: activityId, + latest_reactions: [], + reaction_groups: {}, + own_reactions: [], + }); + feed.state.partialNext({ + comments_by_entity_id: { + [activityId]: { + comments: [existingComment], + pagination: { sort: 'first' }, + }, + }, + }); + + const event = generateCommentReactionUpdatedEvent({ + comment: { + id: existingComment.id, + object_id: activityId, + latest_reactions: [], + reaction_groups: {}, + }, + reaction: { + type: 'laugh', + user: { id: 'other-user' }, + activity_id: activityId, + comment_id: existingComment.id, + }, + }); + + handleCommentReactionUpdated.call(feed, event); + const stateAfter = feed.currentState; + const [updated] = stateAfter.comments_by_entity_id[activityId]!.comments!; + expect(updated.own_reactions).toHaveLength(0); + expect(updated.latest_reactions).toBe(event.comment.latest_reactions); + expect(updated.reaction_groups).toBe(event.comment.reaction_groups); + }); + + it('updates the proper entity state (prefers parent_id)', () => { + const parentId = `comment-${getHumanId()}`; + const existingCommentId = `comment-${getHumanId()}`; + const existingReaction = generateFeedReactionResponse({ + type: 'like', + user: { id: currentUserId }, + activity_id: activityId, + comment_id: existingCommentId, + }); + const existingComment = generateCommentResponse({ + id: existingCommentId, + object_id: activityId, + parent_id: parentId, + latest_reactions: [], + reaction_groups: {}, + own_reactions: [existingReaction], + }); + feed.state.partialNext({ + comments_by_entity_id: { + [parentId]: { + comments: [existingComment], + pagination: { sort: 'first' }, + }, + }, + }); + + const addedEvent = generateCommentReactionUpdatedEvent({ + comment: { + id: existingComment.id, + object_id: activityId, + parent_id: parentId, + latest_reactions: [], + reaction_groups: {}, + }, + reaction: { + type: 'downvote', + user: { id: currentUserId }, + activity_id: activityId, + comment_id: existingComment.id, + }, + }); + + const stateBefore = feed.currentState; + expect( + stateBefore.comments_by_entity_id[parentId]?.comments?.[0]?.own_reactions, + ).toHaveLength(1); + const [initialReaction] = (stateBefore.comments_by_entity_id[parentId] + ?.comments ?? [])[0].own_reactions; + expect(initialReaction).toBe(existingReaction); + + handleCommentReactionUpdated.call(feed, addedEvent); + const stateAfter1 = feed.currentState; + const [updated1] = stateAfter1.comments_by_entity_id[parentId]!.comments!; + expect(updated1.own_reactions).toHaveLength(1); + expect(updated1.own_reactions[0]).toBe(addedEvent.reaction); + expect(updated1.latest_reactions).toBe(addedEvent.comment.latest_reactions); + expect(updated1.reaction_groups).toBe(addedEvent.comment.reaction_groups); + }); + + it('does nothing if comment is not found in state', () => { + const addedEvent = generateCommentReactionUpdatedEvent({ + comment: { object_id: activityId }, + reaction: { user: { id: currentUserId } }, + }); + const stateBefore = feed.currentState; + + handleCommentReactionUpdated.call(feed, addedEvent); + const stateAfter = feed.currentState; + expect(stateAfter).toBe(stateBefore); + }); + + describe(`Comment reaction updated ${shouldUpdateState.name} integration`, () => { + let currentUserPayload: EventPayload<'feeds.comment.reaction.updated'>; + let existingComment: CommentResponse; + let existingReaction: FeedsReactionResponse; + let newReaction: FeedsReactionResponse; + let commentId: string; + + beforeEach(() => { + commentId = `comment-${getHumanId()}`; + existingReaction = generateFeedReactionResponse({ + type: 'heart', + user: { id: currentUserId }, + activity_id: activityId, + comment_id: commentId, + }); + newReaction = generateFeedReactionResponse({ + type: 'like', + user: { id: currentUserId }, + activity_id: activityId, + comment_id: commentId, + }); + existingComment = generateCommentResponse({ + id: commentId, + object_id: activityId, + own_reactions: [existingReaction], + }); + + currentUserPayload = generateCommentReactionUpdatedEvent({ + comment: existingComment, + reaction: newReaction, + }); + + feed.state.partialNext({ + comments_by_entity_id: { + [activityId]: { + comments: [existingComment], + pagination: { sort: 'first' }, + }, + }, + }); + feed.state.partialNext({ watch: true }); + }); + + it(`skips update if ${shouldUpdateState.name} returns false`, () => { + // 1. HTTP and then WS + + handleCommentReactionUpdated.call(feed, currentUserPayload, false); + + let stateBefore = feed.currentState; + + handleCommentReactionUpdated.call(feed, currentUserPayload); + + let stateAfter = feed.currentState; + + expect(stateAfter).toBe(stateBefore); + // @ts-expect-error Using Feed internals for tests only + expect(feed.stateUpdateQueue.size).toEqual(0); + + // 2. WS and the HTTP + + handleCommentReactionUpdated.call(feed, currentUserPayload); + + stateBefore = feed.currentState; + + handleCommentReactionUpdated.call(feed, currentUserPayload, false); + + stateAfter = feed.currentState; + + expect(stateAfter).toBe(stateBefore); + // @ts-expect-error Using Feed internals for tests only + expect(feed.stateUpdateQueue.size).toEqual(0); + }); + + it('allows update again from WS after clearing the stateUpdateQueue', () => { + handleCommentReactionUpdated.call(feed, currentUserPayload); + + // Clear the queue + (feed as any).stateUpdateQueue.clear(); + + // Now update should be allowed from another WS event + handleCommentReactionUpdated.call(feed, currentUserPayload); + + const comments = + feed.currentState.comments_by_entity_id[activityId]?.comments; + const comment = comments?.find((a) => a.id === commentId); + const [latestReaction] = (comment?.own_reactions ?? []).toReversed(); + + expect(comment?.own_reactions.length).toEqual(1); + expect(latestReaction).toMatchObject(newReaction); + }); + + it('allows update again from HTTP response after clearing the stateUpdateQueue', () => { + handleCommentReactionUpdated.call(feed, currentUserPayload, false); + + // Clear the queue + (feed as any).stateUpdateQueue.clear(); + + // Now update should be allowed from another HTTP response + handleCommentReactionUpdated.call(feed, currentUserPayload, false); + + const comments = + feed.currentState.comments_by_entity_id[activityId]?.comments; + const comment = comments?.find((a) => a.id === commentId); + const [latestReaction] = (comment?.own_reactions ?? []).toReversed(); + + expect(comment?.own_reactions.length).toEqual(1); + expect(latestReaction).toMatchObject(newReaction); + }); + + it('should not insert anything into the stateUpdateQueue if the connected_user did not trigger the comment reaction deletion', () => { + const otherUserPayload = generateCommentReactionUpdatedEvent({ + comment: existingComment, + reaction: { + ...existingReaction, + user: { + id: getHumanId(), + }, + }, + }); + + handleCommentReactionUpdated.call(feed, otherUserPayload); + + expect((feed as any).stateUpdateQueue).toEqual(new Set()); + + handleCommentReactionUpdated.call(feed, otherUserPayload); + + const comments = + feed.currentState.comments_by_entity_id[activityId]?.comments; + const comment = comments?.find((a) => a.id === commentId); + const [latestReaction] = comment?.own_reactions ?? []; + + expect((feed as any).stateUpdateQueue).toEqual(new Set()); + expect(comment?.own_reactions.length).toEqual(1); + expect(latestReaction).toMatchObject(existingReaction); + }); + }); +}); diff --git a/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-updated.ts b/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-updated.ts new file mode 100644 index 00000000..b8531138 --- /dev/null +++ b/packages/feeds-client/src/feed/event-handlers/comment/handle-comment-reaction-updated.ts @@ -0,0 +1,72 @@ +import type { Feed } from '../../feed'; +import type { EventPayload } from '../../../types-internal'; +import { type PartializeAllBut } from '../../../types-internal'; +import { getStateUpdateQueueId, shouldUpdateState } from '../../../utils'; + +export type CommentReactionUpdatedPayload = PartializeAllBut< + EventPayload<'feeds.comment.reaction.updated'>, + 'comment' | 'reaction' +>; + +export function handleCommentReactionUpdated( + this: Feed, + payload: CommentReactionUpdatedPayload, + fromWs?: boolean, +) { + const { comment, reaction } = payload; + const connectedUser = this.client.state.getLatestValue().connected_user; + + const isOwnReaction = reaction.user.id === connectedUser?.id; + + if ( + !shouldUpdateState({ + stateUpdateQueueId: getStateUpdateQueueId( + payload, + 'comment-reaction-updated', + ), + stateUpdateQueue: this.stateUpdateQueue, + watch: this.currentState.watch, + fromWs, + isTriggeredByConnectedUser: isOwnReaction, + }) + ) { + return; + } + + this.state.next((currentState) => { + const commentIndex = this.getCommentIndex(comment, currentState); + + if (commentIndex === -1) return currentState; + + const forId = comment.parent_id ?? comment.object_id; + + const entityState = currentState.comments_by_entity_id[forId]; + + const newComments = entityState?.comments?.concat([]) ?? []; + + let ownReactions = newComments[commentIndex].own_reactions; + + if (isOwnReaction) { + ownReactions = [reaction]; + } + + newComments[commentIndex] = { + ...newComments[commentIndex], + reaction_count: comment.reaction_count ?? 0, + latest_reactions: comment.latest_reactions ?? [], + reaction_groups: comment.reaction_groups ?? {}, + own_reactions: ownReactions, + }; + + return { + ...currentState, + comments_by_entity_id: { + ...currentState.comments_by_entity_id, + [forId]: { + ...entityState, + comments: newComments, + }, + }, + }; + }); +} diff --git a/packages/feeds-client/src/feed/event-handlers/comment/index.ts b/packages/feeds-client/src/feed/event-handlers/comment/index.ts index 65134d85..f7f9453f 100644 --- a/packages/feeds-client/src/feed/event-handlers/comment/index.ts +++ b/packages/feeds-client/src/feed/event-handlers/comment/index.ts @@ -3,4 +3,4 @@ export * from './handle-comment-deleted'; export * from './handle-comment-updated'; export * from './handle-comment-reaction-added'; export * from './handle-comment-reaction-deleted'; -// export * from './handle-comment-reaction'; +export * from './handle-comment-reaction-updated'; diff --git a/packages/feeds-client/src/feed/feed.ts b/packages/feeds-client/src/feed/feed.ts index d9c30046..8feaa754 100644 --- a/packages/feeds-client/src/feed/feed.ts +++ b/packages/feeds-client/src/feed/feed.ts @@ -42,8 +42,10 @@ import { handleActivityMarked, handleActivityReactionAdded, handleActivityReactionDeleted, + handleActivityReactionUpdated, handleCommentReactionAdded, handleCommentReactionDeleted, + handleCommentReactionUpdated, addAggregatedActivitiesToState, updateNotificationStatus, handleStoriesFeedUpdated, @@ -151,7 +153,7 @@ export class Feed extends FeedApi { 'feeds.activity.deleted': handleActivityDeleted.bind(this), 'feeds.activity.reaction.added': handleActivityReactionAdded.bind(this), 'feeds.activity.reaction.deleted': handleActivityReactionDeleted.bind(this), - 'feeds.activity.reaction.updated': Feed.noop, + 'feeds.activity.reaction.updated': handleActivityReactionUpdated.bind(this), 'feeds.activity.removed_from_feed': handleActivityRemovedFromFeed.bind(this), 'feeds.activity.updated': handleActivityUpdated.bind(this), @@ -173,7 +175,7 @@ export class Feed extends FeedApi { 'feeds.follow.updated': handleFollowUpdated.bind(this), 'feeds.comment.reaction.added': handleCommentReactionAdded.bind(this), 'feeds.comment.reaction.deleted': handleCommentReactionDeleted.bind(this), - 'feeds.comment.reaction.updated': Feed.noop, + 'feeds.comment.reaction.updated': handleCommentReactionUpdated.bind(this), 'feeds.feed_member.added': handleFeedMemberAdded.bind(this), 'feeds.feed_member.removed': handleFeedMemberRemoved.bind(this), 'feeds.feed_member.updated': handleFeedMemberUpdated.bind(this), diff --git a/packages/feeds-client/src/feeds-client/feeds-client.ts b/packages/feeds-client/src/feeds-client/feeds-client.ts index 05207c61..e69fd17d 100644 --- a/packages/feeds-client/src/feeds-client/feeds-client.ts +++ b/packages/feeds-client/src/feeds-client/feeds-client.ts @@ -51,6 +51,7 @@ import { Feed, handleActivityReactionAdded, handleActivityReactionDeleted, + handleActivityReactionUpdated, handleActivityUpdated, handleCommentAdded, handleCommentDeleted, @@ -68,7 +69,8 @@ import { handleUserUpdated } from './event-handlers'; import type { SyncFailure } from '../common/real-time/event-models'; import { UnhandledErrorType } from '../common/real-time/event-models'; import { updateCommentCount } from '../feed/event-handlers/comment/utils'; -import { configureLoggers } from '../utils/logger'; +import { configureLoggers } from '../utils'; +import { handleCommentReactionUpdated } from '../feed/event-handlers/comment/handle-comment-reaction-updated'; export type FeedsClientState = { connected_user: OwnUser | undefined; @@ -417,9 +419,14 @@ export class FeedsClient extends FeedsApi { activity_id: string; }, ) => { + const shouldEnforceUnique = request.enforce_unique; const response = await super.addActivityReaction(request); for (const feed of Object.values(this.activeFeeds)) { - handleActivityReactionAdded.bind(feed)(response, false); + if (shouldEnforceUnique) { + handleActivityReactionUpdated.bind(feed)(response, false); + } else { + handleActivityReactionAdded.bind(feed)(response, false); + } } return response; }; @@ -449,9 +456,14 @@ export class FeedsClient extends FeedsApi { addCommentReaction = async ( request: AddCommentReactionRequest & { id: string }, ): Promise> => { + const shouldEnforceUnique = request.enforce_unique; const response = await super.addCommentReaction(request); for (const feed of Object.values(this.activeFeeds)) { - handleCommentReactionAdded.bind(feed)(response, false); + if (shouldEnforceUnique) { + handleCommentReactionUpdated.bind(feed)(response, false); + } else { + handleCommentReactionAdded.bind(feed)(response, false); + } } return response; }; diff --git a/packages/feeds-client/src/test-utils/response-generators.ts b/packages/feeds-client/src/test-utils/response-generators.ts index 9cb6e6cf..71cb9dfd 100644 --- a/packages/feeds-client/src/test-utils/response-generators.ts +++ b/packages/feeds-client/src/test-utils/response-generators.ts @@ -283,6 +283,32 @@ export function generateActivityReactionAddedEvent( }; } +export function generateActivityReactionUpdatedEvent( + overrides: Omit< + Partial>, + 'activity' | 'type' | 'reaction' | 'user' + > & { + activity?: Parameters[0]; + reaction?: Parameters[0]; + user?: Parameters[0]; + } = {}, +): EventPayload<'feeds.activity.reaction.updated'> { + const activity = generateActivityResponse(overrides.activity); + const reaction = generateFeedReactionResponse(overrides.reaction); + const user = generateUserResponse(overrides.user); + + return { + type: 'feeds.activity.reaction.updated', + created_at: new Date(), + fid: '', + custom: {}, + ...overrides, + user, + reaction, + activity, + }; +} + export function generateActivityReactionDeletedEvent( overrides: Omit< Partial>, @@ -532,6 +558,32 @@ export function generateCommentReactionDeletedEvent( }; } +export function generateCommentReactionUpdatedEvent( + overrides: Omit< + Partial>, + 'comment' | 'reaction' | 'activity' | 'type' + > & { + comment?: Parameters[0]; + reaction?: Parameters[0]; + activity?: Parameters[0]; + } = {}, +): EventPayload<'feeds.comment.reaction.updated'> { + const comment = generateCommentResponse(overrides.comment); + const reaction = generateFeedReactionResponse(overrides.reaction); + const activity = generateActivityResponse(overrides.activity); + + return { + type: 'feeds.comment.reaction.updated', + created_at: new Date(), + fid: '', + custom: {}, + ...overrides, + comment, + reaction, + activity, + }; +} + export function generateFeedMemberAddedEvent( overrides: Omit< Partial>, diff --git a/packages/feeds-client/src/utils/state-update-queue.ts b/packages/feeds-client/src/utils/state-update-queue.ts index 4b10ef3c..e70ffbdc 100644 --- a/packages/feeds-client/src/utils/state-update-queue.ts +++ b/packages/feeds-client/src/utils/state-update-queue.ts @@ -12,13 +12,21 @@ import type { CommentUpdatedPayload, } from '../feed'; import { ensureExhausted } from './ensure-exhausted'; +import type { + CommentReactionUpdatedPayload +} from '../feed/event-handlers/comment/handle-comment-reaction-updated'; +import type { + ActivityReactionUpdatedPayload +} from '../feed/event-handlers/activity/handle-activity-reaction-updated'; export type StateUpdateQueuePrefix = | 'activity-updated' | 'activity-reaction-created' | 'activity-reaction-deleted' + | 'activity-reaction-updated' | 'comment-reaction-created' | 'comment-reaction-deleted' + | 'comment-reaction-updated' | 'follow-created' | 'follow-deleted' | 'follow-updated' @@ -30,8 +38,10 @@ type StateUpdateQueuePayloadByPrefix = { 'activity-updated': ActivityUpdatedPayload; 'activity-reaction-created': ActivityReactionAddedPayload; 'activity-reaction-deleted': ActivityReactionDeletedPayload; + 'activity-reaction-updated': ActivityReactionUpdatedPayload; 'comment-reaction-created': CommentReactionAddedPayload; 'comment-reaction-deleted': CommentReactionDeletedPayload; + 'comment-reaction-updated': CommentReactionUpdatedPayload; 'follow-created': FollowResponse; 'follow-deleted': FollowResponse; 'follow-updated': FollowResponse; @@ -163,7 +173,8 @@ export function getStateUpdateQueueId( return toJoin.concat([data.activity.id]).join('-') } case 'activity-reaction-created': - case 'activity-reaction-deleted': { + case 'activity-reaction-deleted': + case 'activity-reaction-updated': { return toJoin .concat([ data.activity.id, @@ -172,7 +183,8 @@ export function getStateUpdateQueueId( .join('-'); } case 'comment-reaction-created': - case 'comment-reaction-deleted': { + case 'comment-reaction-deleted': + case 'comment-reaction-updated': { return toJoin .concat([ data.comment.id, diff --git a/sample-apps/react-native/ExpoTikTokApp/components/common/Reaction.tsx b/sample-apps/react-native/ExpoTikTokApp/components/common/Reaction.tsx index 00e8a88f..a6cc8ce1 100644 --- a/sample-apps/react-native/ExpoTikTokApp/components/common/Reaction.tsx +++ b/sample-apps/react-native/ExpoTikTokApp/components/common/Reaction.tsx @@ -1,6 +1,7 @@ -import { +import type { ActivityResponse, - CommentResponse, + CommentResponse} from '@stream-io/feeds-react-native-sdk'; +import { useOwnCapabilities, isCommentResponse, useFeedsClient, @@ -65,11 +66,13 @@ export const Reaction = ({ id: entity.id, type, create_notification_activity: true, + enforce_unique: true, }) : client?.addActivityReaction({ activity_id: entity.id, type, create_notification_activity: true, + enforce_unique: true, })); }); diff --git a/sample-apps/react-native/ExpoTikTokApp/constants/stream.js b/sample-apps/react-native/ExpoTikTokApp/constants/stream.js index 874b1622..5887bd91 100644 --- a/sample-apps/react-native/ExpoTikTokApp/constants/stream.js +++ b/sample-apps/react-native/ExpoTikTokApp/constants/stream.js @@ -1,5 +1,6 @@ export const apiKey = 'm3mpnb9j9jb3'; -export const tokenCreationUrl = 'https://pronto.getstream.io/api/auth/create-token?environment=feeds-react-native-sample-app'; +export const tokenCreationUrl = + 'https://pronto.getstream.io/api/auth/create-token?environment=feeds-react-native-sample-app'; export const placesApiKey = process.env.EXPO_PUBLIC_PLACES_API_KEY; export const mapApiKey = process.env.EXPO_PUBLIC_MAPS_API_KEY; diff --git a/sample-apps/react-native/ExpoTikTokApp/hooks/useCreateClient.ts b/sample-apps/react-native/ExpoTikTokApp/hooks/useCreateClient.ts index 9993ca0b..0849d1cc 100644 --- a/sample-apps/react-native/ExpoTikTokApp/hooks/useCreateClient.ts +++ b/sample-apps/react-native/ExpoTikTokApp/hooks/useCreateClient.ts @@ -1,25 +1,32 @@ import { useCallback } from 'react'; import { useCreateFeedsClient } from '@stream-io/feeds-react-native-sdk'; import { apiKey, tokenCreationUrl } from '@/constants/stream'; -import { LocalUser } from '@/contexts/UserContext'; +import type { LocalUser } from '@/contexts/UserContext'; const tokenProviderFactory = (userId: string) => async () => { if (!tokenCreationUrl) { throw new Error('Token creation url is missing'); } - const tokenGeneratorUrl = new URL(tokenCreationUrl); - tokenGeneratorUrl.searchParams.set('api_key', apiKey); - tokenGeneratorUrl.searchParams.set('user_id', userId); - const response = await fetch(tokenGeneratorUrl.toString()); - if (!response.ok) { - throw new Error(`Failed to get token: ${response.status}`); + + try { + const tokenGeneratorUrl = new URL(tokenCreationUrl); + tokenGeneratorUrl.searchParams.set('api_key', apiKey); + tokenGeneratorUrl.searchParams.set('user_id', userId); + const response = await fetch(tokenGeneratorUrl.toString()); + if (!response.ok) { + throw new Error(`Failed to get token: ${response.status}`); + } + const data = await response.json(); + return data.token; + } catch (error) { + console.error('An error has occurred while generating a token: ', error); } - const data = await response.json(); - return data.token; }; const CLIENT_OPTIONS = { configure_loggers_options: { default: { level: 'debug' } }, + base_url: 'http://localhost:3030', + timeout: 10000 }; export const useCreateClient = (user: LocalUser) => { diff --git a/sample-apps/react-native/ExpoTikTokApp/setup-env.js b/sample-apps/react-native/ExpoTikTokApp/setup-env.js index efd66dca..8fa63fc3 100644 --- a/sample-apps/react-native/ExpoTikTokApp/setup-env.js +++ b/sample-apps/react-native/ExpoTikTokApp/setup-env.js @@ -12,7 +12,7 @@ require('dotenv').config(); const users = JSON.parse( await fs.readFile(path.resolve('users.json'), 'utf-8'), ); - const nodeClient = new StreamClient(key, secret); + const nodeClient = new StreamClient(key, secret, { basePath: 'http://localhost:3030' }); console.log('Creating users...'); await nodeClient.upsertUsers(users); diff --git a/sample-apps/react-sample-app/app/api/create-token/route.ts b/sample-apps/react-sample-app/app/api/create-token/route.ts index 65e5bdcf..c80ba652 100644 --- a/sample-apps/react-sample-app/app/api/create-token/route.ts +++ b/sample-apps/react-sample-app/app/api/create-token/route.ts @@ -1,12 +1,26 @@ -import { cookies } from 'next/headers'; import { streamServerClient } from '../client'; +import { cookies } from 'next/headers'; + +export async function GET( + req: Request, + ctx: { params?: Record } = {}, +) { + const cookieId = (await cookies()).get('user_id')?.value; + + // 1) query string: ?user_id=... or ?userId=... + const url = new URL(req.url); + const queryUserId = + url.searchParams.get('user_id') ?? url.searchParams.get('userId'); + + // 2) dynamic route param: /api/token/[userId] + const routeUserId = ctx.params?.userId; -export async function GET() { - const userId = (await cookies()).get('user_id')?.value; + const userId = cookieId ?? queryUserId ?? routeUserId; if (!userId) { - return new Response(undefined, { + return new Response(JSON.stringify({ error: 'missing_user_id' }), { status: 401, + headers: { 'Content-Type': 'application/json' }, }); } @@ -14,8 +28,6 @@ export async function GET() { return new Response(JSON.stringify({ token }), { status: 200, - headers: { - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); } diff --git a/yarn.lock b/yarn.lock index 3fca035a..ab21796f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,6 +24,13 @@ __metadata: languageName: node linkType: hard +"@alloc/quick-lru@npm:^5.2.0": + version: 5.2.0 + resolution: "@alloc/quick-lru@npm:5.2.0" + checksum: bdc35758b552bcf045733ac047fb7f9a07c4678b944c641adfbd41f798b4b91fffd0fdc0df2578d9b0afc7b4d636aa6e110ead5d6281a2adc1ab90efd7f057f8 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -1746,6 +1753,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.2.0": + version: 1.5.0 + resolution: "@emnapi/runtime@npm:1.5.0" + dependencies: + tslib: ^2.4.0 + checksum: 03b23bdc0bb72bce4d8967ca29d623c2599af18977975c10532577db2ec89a57d97d2c76c5c4bde856c7c29302b9f7af357e921c42bd952bdda206972185819a + languageName: node + linkType: hard + "@emnapi/wasi-threads@npm:1.0.2": version: 1.0.2 resolution: "@emnapi/wasi-threads@npm:1.0.2" @@ -3300,6 +3316,181 @@ __metadata: languageName: node linkType: hard +"@img/sharp-darwin-arm64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-darwin-arm64@npm:0.33.5" + dependencies: + "@img/sharp-libvips-darwin-arm64": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-darwin-x64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-darwin-x64@npm:0.33.5" + dependencies: + "@img/sharp-libvips-darwin-x64": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-arm64@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@img/sharp-libvips-darwin-x64@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm64@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-arm@npm:1.0.5": + version: 1.0.5 + resolution: "@img/sharp-libvips-linux-arm@npm:1.0.5" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-s390x@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.4" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linux-x64@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-linux-x64@npm:1.0.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-libvips-linuxmusl-x64@npm:1.0.4": + version: 1.0.4 + resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linux-arm64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-linux-arm64@npm:0.33.5" + dependencies: + "@img/sharp-libvips-linux-arm64": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-arm@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-linux-arm@npm:0.33.5" + dependencies: + "@img/sharp-libvips-linux-arm": 1.0.5 + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-s390x@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-linux-s390x@npm:0.33.5" + dependencies: + "@img/sharp-libvips-linux-s390x": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linux-x64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-linux-x64@npm:0.33.5" + dependencies: + "@img/sharp-libvips-linux-x64": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-arm64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.5" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-linuxmusl-x64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-linuxmusl-x64@npm:0.33.5" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": 1.0.4 + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@img/sharp-wasm32@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-wasm32@npm:0.33.5" + dependencies: + "@emnapi/runtime": ^1.2.0 + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@img/sharp-win32-ia32@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-win32-ia32@npm:0.33.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@img/sharp-win32-x64@npm:0.33.5": + version: 0.33.5 + resolution: "@img/sharp-win32-x64@npm:0.33.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -3987,6 +4178,69 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:15.0.3": + version: 15.0.3 + resolution: "@next/env@npm:15.0.3" + checksum: 8a805f594f4da85f5070c1c0def8946e5c32620ac401b72f7cb5710db7a7fd1a085d23b40f3ea6c6ef7ef437d91c600786c7361c98ad83771e39d3a460aa30d4 + languageName: node + linkType: hard + +"@next/swc-darwin-arm64@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-darwin-arm64@npm:15.0.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-darwin-x64@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-darwin-x64@npm:15.0.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-arm64-gnu@npm:15.0.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-arm64-musl@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-arm64-musl@npm:15.0.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-linux-x64-gnu@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-x64-gnu@npm:15.0.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@next/swc-linux-x64-musl@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-linux-x64-musl@npm:15.0.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@next/swc-win32-arm64-msvc@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-win32-arm64-msvc@npm:15.0.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:15.0.3": + version: 15.0.3 + resolution: "@next/swc-win32-x64-msvc@npm:15.0.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": version: 5.1.1-v1 resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" @@ -5674,7 +5928,7 @@ __metadata: languageName: unknown linkType: soft -"@stream-io/feeds-react-sdk@workspace:packages/react-sdk": +"@stream-io/feeds-react-sdk@workspace:*, @stream-io/feeds-react-sdk@workspace:packages/react-sdk": version: 0.0.0-use.local resolution: "@stream-io/feeds-react-sdk@workspace:packages/react-sdk" dependencies: @@ -5744,6 +5998,22 @@ __metadata: languageName: node linkType: hard +"@swc/counter@npm:0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: df8f9cfba9904d3d60f511664c70d23bb323b3a0803ec9890f60133954173047ba9bdeabce28cd70ba89ccd3fd6c71c7b0bd58be85f611e1ffbe5d5c18616598 + languageName: node + linkType: hard + +"@swc/helpers@npm:0.5.13": + version: 0.5.13 + resolution: "@swc/helpers@npm:0.5.13" + dependencies: + tslib: ^2.4.0 + checksum: d50c2c10da6ef940af423c6b03ad9c3c94cf9de59314b1e921a7d1bcc081a6074481c9d67b655fc8fe66a73288f98b25950743792a63882bfb5793b362494fc0 + languageName: node + linkType: hard + "@testing-library/jest-native@npm:^5.4.3": version: 5.4.3 resolution: "@testing-library/jest-native@npm:5.4.3" @@ -6004,6 +6274,22 @@ __metadata: languageName: node linkType: hard +"@types/lodash.uniqby@npm:^4": + version: 4.7.9 + resolution: "@types/lodash.uniqby@npm:4.7.9" + dependencies: + "@types/lodash": "*" + checksum: 24cc8af36e0d4c52b7294c7ba7d814c89ce2c8118d94350bbed21031fef850fa1a280bfd2b30a47e0b5f7aa6ac649a36a5089aa76bc23787963a5ee6443f631e + languageName: node + linkType: hard + +"@types/lodash@npm:*": + version: 4.17.20 + resolution: "@types/lodash@npm:4.17.20" + checksum: dc7bb4653514dd91117a4c4cec2c37e2b5a163d7643445e4757d76a360fabe064422ec7a42dde7450c5e7e0e7e678d5e6eae6d2a919abcddf581d81e63e63839 + languageName: node + linkType: hard + "@types/ms@npm:*": version: 2.1.0 resolution: "@types/ms@npm:2.1.0" @@ -6045,6 +6331,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20": + version: 20.19.18 + resolution: "@types/node@npm:20.19.18" + dependencies: + undici-types: ~6.21.0 + checksum: ac56e19820a50e57c7db05fd88ff87e7f4d1f67b19dd407cdf4c3099885e97c8a17cd0e165d818f94f45b05883acd540c1e5a9aa4c3adf847a63d935f5bf4f45 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.1": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" @@ -6059,6 +6354,22 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.15 + resolution: "@types/prop-types@npm:15.7.15" + checksum: 31aa2f59b28f24da6fb4f1d70807dae2aedfce090ec63eaf9ea01727a9533ef6eaf017de5bff99fbccad7d1c9e644f52c6c2ba30869465dd22b1a7221c29f356 + languageName: node + linkType: hard + +"@types/react-dom@npm:^18": + version: 18.3.7 + resolution: "@types/react-dom@npm:18.3.7" + peerDependencies: + "@types/react": ^18.0.0 + checksum: c8b63ec944d2a68992b4dba474003fe55ee1d949c4b9c8fe97eecb2290de23f76acfb670b2f7ceb46a5fc8e46808d1745369b03edda48a7a0cf730eff4c5d315 + languageName: node + linkType: hard + "@types/react-test-renderer@npm:19.0.0": version: 19.0.0 resolution: "@types/react-test-renderer@npm:19.0.0" @@ -6077,6 +6388,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18": + version: 18.3.25 + resolution: "@types/react@npm:18.3.25" + dependencies: + "@types/prop-types": "*" + csstype: ^3.0.2 + checksum: 33bb754ac0201cb0f4eed7f42c6c71e6fe4f00bc1b028a5a69d2383c77bfa33c4b99827299d525a0582dab697de4e9fda9e23b283dec1083f96704aea2157edd + languageName: node + linkType: hard + "@types/semver@npm:^7.3.12": version: 7.7.0 resolution: "@types/semver@npm:7.7.0" @@ -7392,7 +7713,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.1": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.1, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -8183,6 +8504,15 @@ __metadata: languageName: node linkType: hard +"busboy@npm:1.6.0": + version: 1.6.0 + resolution: "busboy@npm:1.6.0" + dependencies: + streamsearch: ^1.1.0 + checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e + languageName: node + linkType: hard + "bytes@npm:3.1.0": version: 3.1.0 resolution: "bytes@npm:3.1.0" @@ -8294,6 +8624,13 @@ __metadata: languageName: node linkType: hard +"camelcase-css@npm:^2.0.1": + version: 2.0.1 + resolution: "camelcase-css@npm:2.0.1" + checksum: 1cec2b3b3dcb5026688a470b00299a8db7d904c4802845c353dbd12d9d248d3346949a814d83bfd988d4d2e5b9904c07efe76fecd195a1d4f05b543e7c0b56b1 + languageName: node + linkType: hard + "camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" @@ -8308,6 +8645,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001579": + version: 1.0.30001746 + resolution: "caniuse-lite@npm:1.0.30001746" + checksum: 3d2a310b49bc414d87184a73ca7c3b1aea0a1547cc9e3bfd8a6ee45129d269917a94b8e773e7b2821cfc3b9e1256759a34e4b7242c56e3f70f575213baa6c880 + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001726": version: 1.0.30001726 resolution: "caniuse-lite@npm:1.0.30001726" @@ -8406,6 +8750,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.6.0": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: ~3.1.2 + braces: ~3.0.2 + fsevents: ~2.3.2 + glob-parent: ~5.1.2 + is-binary-path: ~2.1.0 + is-glob: ~4.0.1 + normalize-path: ~3.0.0 + readdirp: ~3.6.0 + dependenciesMeta: + fsevents: + optional: true + checksum: d2f29f499705dcd4f6f3bbed79a9ce2388cf530460122eed3b9c48efeab7a4e28739c6551fd15bec9245c6b9eeca7a32baa64694d64d9b6faeb74ddb8c4a413d + languageName: node + linkType: hard + "chownr@npm:^1.1.4": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -8543,7 +8906,7 @@ __metadata: languageName: node linkType: hard -"client-only@npm:^0.0.1": +"client-only@npm:0.0.1, client-only@npm:^0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" checksum: 0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 @@ -8568,6 +8931,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -9055,6 +9425,15 @@ __metadata: languageName: node linkType: hard +"cssesc@npm:^3.0.0": + version: 3.0.0 + resolution: "cssesc@npm:3.0.0" + bin: + cssesc: bin/cssesc + checksum: f8c4ababffbc5e2ddf2fa9957dda1ee4af6048e22aeda1869d0d00843223c1b13ad3f5d88b51caa46c994225eacb636b764eb807a8883e2fb6f99b4f4e8c48b2 + languageName: node + linkType: hard + "cssom@npm:^0.5.0": version: 0.5.0 resolution: "cssom@npm:0.5.0" @@ -9378,6 +9757,13 @@ __metadata: languageName: node linkType: hard +"didyoumean@npm:^1.2.2": + version: 1.2.2 + resolution: "didyoumean@npm:1.2.2" + checksum: d5d98719d58b3c2fa59663c4c42ba9716f1fd01245c31d5fce31915bd3aa26e6aac149788e007358f778ebbd68a2256eb5973e8ca6f221df221ba060115acf2e + languageName: node + linkType: hard + "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -9401,6 +9787,13 @@ __metadata: languageName: node linkType: hard +"dlv@npm:^1.1.3": + version: 1.1.3 + resolution: "dlv@npm:1.1.3" + checksum: d7381bca22ed11933a1ccf376db7a94bee2c57aa61e490f680124fa2d1cd27e94eba641d9f45be57caab4f9a6579de0983466f620a2cd6230d7ec93312105ae7 + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -11360,6 +11753,30 @@ __metadata: languageName: node linkType: hard +"facebook-clone@workspace:sample-apps/react-sample-app": + version: 0.0.0-use.local + resolution: "facebook-clone@workspace:sample-apps/react-sample-app" + dependencies: + "@stream-io/feeds-react-sdk": "workspace:*" + "@stream-io/node-sdk": 0.6.0 + "@types/lodash.uniqby": ^4 + "@types/node": ^20 + "@types/react": ^18 + "@types/react-dom": ^18 + clsx: ^2.1.1 + dotenv: ^16.4.5 + lodash.uniqby: ^4.7.0 + material-symbols: ^0.27.1 + next: 15.0.3 + postcss: ^8 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106 + tailwindcss: ^3.4.1 + typescript: ^5 + uuid: ^11.0.3 + languageName: unknown + linkType: soft + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -12041,7 +12458,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.0, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -14226,6 +14643,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^1.21.6": + version: 1.21.7 + resolution: "jiti@npm:1.21.7" + bin: + jiti: bin/jiti.js + checksum: 9cd20dabf82e3a4cceecb746a69381da7acda93d34eed0cdb9c9bdff3bce07e4f2f4a016ca89924392c935297d9aedc58ff9f7d3281bc5293319ad244926e0b7 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -14692,6 +15118,13 @@ __metadata: languageName: node linkType: hard +"lilconfig@npm:^3.0.0, lilconfig@npm:^3.1.3": + version: 3.1.3 + resolution: "lilconfig@npm:3.1.3" + checksum: 644eb10830350f9cdc88610f71a921f510574ed02424b57b0b3abb66ea725d7a082559552524a842f4e0272c196b88dfe1ff7d35ffcc6f45736777185cd67c9a + languageName: node + linkType: hard + "lines-and-columns@npm:2.0.3": version: 2.0.3 resolution: "lines-and-columns@npm:2.0.3" @@ -14817,6 +15250,13 @@ __metadata: languageName: node linkType: hard +"lodash.uniqby@npm:^4.7.0": + version: 4.7.0 + resolution: "lodash.uniqby@npm:4.7.0" + checksum: 659264545a95726d1493123345aad8cbf56e17810fa9a0b029852c6d42bc80517696af09d99b23bef1845d10d95e01b8b4a1da578f22aeba7a30d3e0022a4938 + languageName: node + linkType: hard + "lodash@npm:^4.17.19, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -14992,6 +15432,13 @@ __metadata: languageName: node linkType: hard +"material-symbols@npm:^0.27.1": + version: 0.27.2 + resolution: "material-symbols@npm:0.27.2" + checksum: c1572d86aa69b355dd273ba1a9bea06f09310451a3384e846f6282240097215e8713bc3918a08e0d786be1c0d8405ca15d2bfc0817385fc2ce6f6d747e041344 + languageName: node + linkType: hard + "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -16024,7 +16471,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.11, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7, nanoid@npm:^3.3.8": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -16084,6 +16531,67 @@ __metadata: languageName: node linkType: hard +"next@npm:15.0.3": + version: 15.0.3 + resolution: "next@npm:15.0.3" + dependencies: + "@next/env": 15.0.3 + "@next/swc-darwin-arm64": 15.0.3 + "@next/swc-darwin-x64": 15.0.3 + "@next/swc-linux-arm64-gnu": 15.0.3 + "@next/swc-linux-arm64-musl": 15.0.3 + "@next/swc-linux-x64-gnu": 15.0.3 + "@next/swc-linux-x64-musl": 15.0.3 + "@next/swc-win32-arm64-msvc": 15.0.3 + "@next/swc-win32-x64-msvc": 15.0.3 + "@swc/counter": 0.1.3 + "@swc/helpers": 0.5.13 + busboy: 1.6.0 + caniuse-lite: ^1.0.30001579 + postcss: 8.4.31 + sharp: ^0.33.5 + styled-jsx: 5.1.6 + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.41.2 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + sharp: + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 7c37c0fd05c4044fa89dabdac62a81de5300b387411cc9862341f45bd68da180cba7125ef6d2c3961e3409cbf8afd03b6c835cb4a6142c8988b5fc3741e72871 + languageName: node + linkType: hard + "node-fetch@npm:2.6.7": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" @@ -16497,6 +17005,13 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:^3.0.0": + version: 3.0.0 + resolution: "object-hash@npm:3.0.0" + checksum: 80b4904bb3857c52cc1bfd0b52c0352532ca12ed3b8a6ff06a90cd209dfda1b95cee059a7625eb9da29537027f68ac4619363491eedb2f5d3dddbba97494fd6c + languageName: node + linkType: hard + "object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" @@ -17069,7 +17584,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.0.7, picomatch@npm:^2.2.2, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.0.7, picomatch@npm:^2.2.1, picomatch@npm:^2.2.2, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf @@ -17090,6 +17605,13 @@ __metadata: languageName: node linkType: hard +"pify@npm:^2.3.0": + version: 2.3.0 + resolution: "pify@npm:2.3.0" + checksum: 9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.7": version: 4.0.7 resolution: "pirates@npm:4.0.7" @@ -17131,14 +17653,88 @@ __metadata: languageName: node linkType: hard -"postcss-value-parser@npm:^4.2.0": +"postcss-import@npm:^15.1.0": + version: 15.1.0 + resolution: "postcss-import@npm:15.1.0" + dependencies: + postcss-value-parser: ^4.0.0 + read-cache: ^1.0.0 + resolve: ^1.1.7 + peerDependencies: + postcss: ^8.0.0 + checksum: 7bd04bd8f0235429009d0022cbf00faebc885de1d017f6d12ccb1b021265882efc9302006ba700af6cab24c46bfa2f3bc590be3f9aee89d064944f171b04e2a3 + languageName: node + linkType: hard + +"postcss-js@npm:^4.0.1": + version: 4.1.0 + resolution: "postcss-js@npm:4.1.0" + dependencies: + camelcase-css: ^2.0.1 + peerDependencies: + postcss: ^8.4.21 + checksum: 1fe3d51770f66d301e63103c15830d26875b1ae9bbe3ba6bf61256860edde3d9c0de5aa0c3e34d34b80c099f5d95b589cfcc92dac718253c8351aa8e05a8d80a + languageName: node + linkType: hard + +"postcss-load-config@npm:^4.0.2": + version: 4.0.2 + resolution: "postcss-load-config@npm:4.0.2" + dependencies: + lilconfig: ^3.0.0 + yaml: ^2.3.4 + peerDependencies: + postcss: ">=8.0.9" + ts-node: ">=9.0.0" + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + checksum: 7c27dd3801db4eae207a5116fed2db6b1ebb780b40c3dd62a3e57e087093a8e6a14ee17ada729fee903152d6ef4826c6339eb135bee6208e0f3140d7e8090185 + languageName: node + linkType: hard + +"postcss-nested@npm:^6.2.0": + version: 6.2.0 + resolution: "postcss-nested@npm:6.2.0" + dependencies: + postcss-selector-parser: ^6.1.1 + peerDependencies: + postcss: ^8.2.14 + checksum: 2c86ecf2d0ce68f27c87c7e24ae22dc6dd5515a89fcaf372b2627906e11f5c1f36e4a09e4c15c20fd4a23d628b3d945c35839f44496fbee9a25866258006671b + languageName: node + linkType: hard + +"postcss-selector-parser@npm:^6.1.1, postcss-selector-parser@npm:^6.1.2": + version: 6.1.2 + resolution: "postcss-selector-parser@npm:6.1.2" + dependencies: + cssesc: ^3.0.0 + util-deprecate: ^1.0.2 + checksum: ce9440fc42a5419d103f4c7c1847cb75488f3ac9cbe81093b408ee9701193a509f664b4d10a2b4d82c694ee7495e022f8f482d254f92b7ffd9ed9dea696c6f84 + languageName: node + linkType: hard + +"postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f languageName: node linkType: hard -"postcss@npm:^8.4.43, postcss@npm:^8.5.6": +"postcss@npm:8.4.31": + version: 8.4.31 + resolution: "postcss@npm:8.4.31" + dependencies: + nanoid: ^3.3.6 + picocolors: ^1.0.0 + source-map-js: ^1.0.2 + checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea + languageName: node + linkType: hard + +"postcss@npm:^8, postcss@npm:^8.4.43, postcss@npm:^8.4.47, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -17461,6 +18057,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:19.0.0-rc-66855b96-20241106": + version: 19.0.0-rc-66855b96-20241106 + resolution: "react-dom@npm:19.0.0-rc-66855b96-20241106" + dependencies: + scheduler: 0.25.0-rc-66855b96-20241106 + peerDependencies: + react: 19.0.0-rc-66855b96-20241106 + checksum: 25ea89c2f5cf13bfb410aac8a95d0323925992c27c1f1419504dbf875e56f1293de05cce932c75fc4971e8077208fc24b6840eaa891b8ab9a89b7acbfde42de5 + languageName: node + linkType: hard + "react-dom@npm:19.1.0": version: 19.1.0 resolution: "react-dom@npm:19.1.0" @@ -17905,6 +18512,13 @@ __metadata: languageName: node linkType: hard +"react@npm:19.0.0-rc-66855b96-20241106": + version: 19.0.0-rc-66855b96-20241106 + resolution: "react@npm:19.0.0-rc-66855b96-20241106" + checksum: 13ef936f9988035e59eb3b25266108d0d045034c67c4328fc9014afbcfc1ccc1cf197604e4929c225314ef76946b695f514440a44fdb512ed38bf3624efae0f4 + languageName: node + linkType: hard + "react@npm:19.1.0": version: 19.1.0 resolution: "react@npm:19.1.0" @@ -17912,6 +18526,15 @@ __metadata: languageName: node linkType: hard +"read-cache@npm:^1.0.0": + version: 1.0.0 + resolution: "read-cache@npm:1.0.0" + dependencies: + pify: ^2.3.0 + checksum: cffc728b9ede1e0667399903f9ecaf3789888b041c46ca53382fa3a06303e5132774dc0a96d0c16aa702dbac1ea0833d5a868d414f5ab2af1e1438e19e6657c6 + languageName: node + linkType: hard + "read-pkg-up@npm:^10.0.0": version: 10.1.0 resolution: "read-pkg-up@npm:10.1.0" @@ -17965,6 +18588,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: ^2.2.1 + checksum: 1ced032e6e45670b6d7352d71d21ce7edf7b9b928494dcaba6f11fba63180d9da6cd7061ebc34175ffda6ff529f481818c962952004d273178acd70f7059b320 + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -18152,7 +18784,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.10, resolve@npm:^1.22.2, resolve@npm:^1.22.4": +"resolve@npm:^1.1.7, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.10, resolve@npm:^1.22.2, resolve@npm:^1.22.4, resolve@npm:^1.22.8": version: 1.22.10 resolution: "resolve@npm:1.22.10" dependencies: @@ -18187,7 +18819,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.10#~builtin, resolve@patch:resolve@^1.22.2#~builtin, resolve@patch:resolve@^1.22.4#~builtin": +"resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.10#~builtin, resolve@patch:resolve@^1.22.2#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin": version: 1.22.10 resolution: "resolve@patch:resolve@npm%3A1.22.10#~builtin::version=1.22.10&hash=c3c19d" dependencies: @@ -18473,6 +19105,13 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:0.25.0-rc-66855b96-20241106": + version: 0.25.0-rc-66855b96-20241106 + resolution: "scheduler@npm:0.25.0-rc-66855b96-20241106" + checksum: 950c3714393c370c5628e3e2e581c2e5d3cb503260e9a1e00984b0631d3e18500fb57f80a9c45a034c54852670620c3c526a35d40073cb97b6f5f43f56d8c938 + languageName: node + linkType: hard + "scheduler@npm:0.26.0, scheduler@npm:^0.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" @@ -18665,6 +19304,75 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.33.5": + version: 0.33.5 + resolution: "sharp@npm:0.33.5" + dependencies: + "@img/sharp-darwin-arm64": 0.33.5 + "@img/sharp-darwin-x64": 0.33.5 + "@img/sharp-libvips-darwin-arm64": 1.0.4 + "@img/sharp-libvips-darwin-x64": 1.0.4 + "@img/sharp-libvips-linux-arm": 1.0.5 + "@img/sharp-libvips-linux-arm64": 1.0.4 + "@img/sharp-libvips-linux-s390x": 1.0.4 + "@img/sharp-libvips-linux-x64": 1.0.4 + "@img/sharp-libvips-linuxmusl-arm64": 1.0.4 + "@img/sharp-libvips-linuxmusl-x64": 1.0.4 + "@img/sharp-linux-arm": 0.33.5 + "@img/sharp-linux-arm64": 0.33.5 + "@img/sharp-linux-s390x": 0.33.5 + "@img/sharp-linux-x64": 0.33.5 + "@img/sharp-linuxmusl-arm64": 0.33.5 + "@img/sharp-linuxmusl-x64": 0.33.5 + "@img/sharp-wasm32": 0.33.5 + "@img/sharp-win32-ia32": 0.33.5 + "@img/sharp-win32-x64": 0.33.5 + color: ^4.2.3 + detect-libc: ^2.0.3 + semver: ^7.6.3 + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 04beae89910ac65c5f145f88de162e8466bec67705f497ace128de849c24d168993e016f33a343a1f3c30b25d2a90c3e62b017a9a0d25452371556f6cd2471e4 + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -18847,7 +19555,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b @@ -19103,6 +19811,13 @@ __metadata: languageName: node linkType: hard +"streamsearch@npm:^1.1.0": + version: 1.1.0 + resolution: "streamsearch@npm:1.1.0" + checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942 + languageName: node + linkType: hard + "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" @@ -19324,6 +20039,22 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.1.6": + version: 5.1.6 + resolution: "styled-jsx@npm:5.1.6" + dependencies: + client-only: 0.0.1 + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 879ad68e3e81adcf4373038aaafe55f968294955593660e173fbf679204aff158c59966716a60b29af72dc88795cfb2c479b6d2c3c87b2b2d282f3e27cc66461 + languageName: node + linkType: hard + "styleq@npm:^0.1.3": version: 0.1.3 resolution: "styleq@npm:0.1.3" @@ -19331,7 +20062,7 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:3.35.0": +"sucrase@npm:3.35.0, sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" dependencies: @@ -19409,6 +20140,39 @@ __metadata: languageName: node linkType: hard +"tailwindcss@npm:^3.4.1": + version: 3.4.17 + resolution: "tailwindcss@npm:3.4.17" + dependencies: + "@alloc/quick-lru": ^5.2.0 + arg: ^5.0.2 + chokidar: ^3.6.0 + didyoumean: ^1.2.2 + dlv: ^1.1.3 + fast-glob: ^3.3.2 + glob-parent: ^6.0.2 + is-glob: ^4.0.3 + jiti: ^1.21.6 + lilconfig: ^3.1.3 + micromatch: ^4.0.8 + normalize-path: ^3.0.0 + object-hash: ^3.0.0 + picocolors: ^1.1.1 + postcss: ^8.4.47 + postcss-import: ^15.1.0 + postcss-js: ^4.0.1 + postcss-load-config: ^4.0.2 + postcss-nested: ^6.2.0 + postcss-selector-parser: ^6.1.2 + resolve: ^1.22.8 + sucrase: ^3.35.0 + bin: + tailwind: lib/cli.js + tailwindcss: lib/cli.js + checksum: bda962f30e9a2f0567e2ee936ec863d5178958078e577ced13da60b3af779062a53a7e95f2f32b5c558f12a7477dea3ce071441a7362c6d7bf50bc9e166728a4 + languageName: node + linkType: hard + "tapable@npm:^2.2.0": version: 2.2.1 resolution: "tapable@npm:2.2.1" @@ -19973,7 +20737,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.8.3, typescript@npm:~5.9.2": +"typescript@npm:^5, typescript@npm:^5.8.3, typescript@npm:~5.9.2": version: 5.9.2 resolution: "typescript@npm:5.9.2" bin: @@ -20003,7 +20767,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^5.8.3#~builtin, typescript@patch:typescript@~5.9.2#~builtin": +"typescript@patch:typescript@^5#~builtin, typescript@patch:typescript@^5.8.3#~builtin, typescript@patch:typescript@~5.9.2#~builtin": version: 5.9.2 resolution: "typescript@patch:typescript@npm%3A5.9.2#~builtin::version=5.9.2&hash=85af82" bin: @@ -20064,6 +20828,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 46331c7d6016bf85b3e8f20c159d62f5ae471aba1eb3dc52fff35a0259d58dcc7d592d4cc4f00c5f9243fa738a11cfa48bd20203040d4a9e6bc25e807fab7ab3 + languageName: node + linkType: hard + "undici-types@npm:~7.10.0": version: 7.10.0 resolution: "undici-types@npm:7.10.0" @@ -20329,7 +21100,7 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 @@ -20352,6 +21123,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^11.0.3": + version: 11.1.0 + resolution: "uuid@npm:11.1.0" + bin: + uuid: dist/esm/bin/uuid + checksum: 840f19758543c4631e58a29439e51b5b669d5f34b4dd2700b6a1d15c5708c7a6e0c3e2c8c4a2eae761a3a7caa7e9884d00c86c02622ba91137bd3deade6b4b4a + languageName: node + linkType: hard + "uuid@npm:^7.0.3": version: 7.0.3 resolution: "uuid@npm:7.0.3" @@ -21087,7 +21867,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.6.1": +"yaml@npm:^2.3.4, yaml@npm:^2.6.1": version: 2.8.1 resolution: "yaml@npm:2.8.1" bin: