import { deepClone, SimpleUUID } from "/js/utils.js"; import { pinPostInFeed, unpinPostInFeed } from "/js/dataHelpers.js"; import { Signal, SignalMap, ComputedMap, ReactiveStore } from "/js/signals.js"; // The store saves patch data for optimistic updates. export class PatchStore extends ReactiveStore { constructor(dataStore) { super("patchStore"); this.dataStore = dataStore; this.$postPatches = new SignalMap(); this.$patchedPosts = new ComputedMap((postURI) => { const post = this.dataStore.$posts.get(postURI); if (!post) return null; const patches = this.$postPatches.get(postURI) || []; return this.applyPostPatches(post, patches); }); this.$profilePatches = new SignalMap(); this.$patchedProfiles = new ComputedMap((did) => { const profile = this.dataStore.$profiles.get(did); if (!profile) return null; const patches = this.$profilePatches.get(did) || []; return this.applyProfilePatches(profile, patches); }); this.$patchedDetailedProfiles = new ComputedMap((did) => { const profile = this.dataStore.$detailedProfiles.get(did); if (!profile) return null; const patches = this.$profilePatches.get(did) || []; return this.applyProfilePatches(profile, patches); }); this.$messagePatches = new SignalMap(); this.$patchedMessages = new ComputedMap((messageId) => { const message = this.dataStore.$messages.get(messageId); if (!message) return null; const patches = this.$messagePatches.get(messageId) || []; let patchedMessage = message; for (const patch of patches) { patchedMessage = this.applyMessagePatch(patchedMessage, patch.body); } return patchedMessage; }); this.$preferencePatches = new Signal.State([]); this.$currentUserPatches = new Signal.State([]); this.$authorFeedPatches = new SignalMap(); this.uuid = new SimpleUUID(); } /* Post Patches */ _getPostPatches(postURI) { return this.$postPatches.get(postURI) || []; } addPostPatch(postURI, patchBody) { const patchId = this.uuid.create(); this.$postPatches.set(postURI, [ ...this._getPostPatches(postURI), { id: patchId, body: patchBody }, ]); return patchId; } removePostPatch(postURI, patchId) { this.$postPatches.set( postURI, this._getPostPatches(postURI).filter(({ id }) => id !== patchId), ); } applyPostPatches(post, patches) { let patchedPost = deepClone(post); for (const patch of patches) { patchedPost = this.applyPostPatch(patchedPost, patch.body); } if (patchedPost.author) { patchedPost = { ...patchedPost, author: this.applyProfilePatches(patchedPost.author), }; } return patchedPost; } applyPostPatch(post, patchBody) { switch (patchBody.type) { case "createRepost": return { ...post, viewer: { ...post.viewer, repost: "fake repost", }, repostCount: post.repostCount + 1, }; case "deleteRepost": return { ...post, viewer: { ...post.viewer, repost: null, }, repostCount: post.repostCount - 1, }; case "addLike": return { ...post, viewer: { ...post.viewer, like: "fake like", }, likeCount: post.likeCount + 1, }; case "removeLike": return { ...post, viewer: { ...post.viewer, like: null, }, likeCount: post.likeCount - 1, }; case "addBookmark": return { ...post, viewer: { ...post.viewer, bookmarked: true, }, bookmarkCount: post.bookmarkCount + 1, }; case "removeBookmark": return { ...post, viewer: { ...post.viewer, bookmarked: false, }, bookmarkCount: post.bookmarkCount - 1, }; case "hidePost": return { ...post, viewer: { ...post.viewer, isHidden: true, }, }; default: throw new Error("Unknown patch type", patchBody.type); } } /* Profile Patches */ _getProfilePatches(profileURI) { return this.$profilePatches.get(profileURI) || []; } addProfilePatch(profileURI, patchBody) { const patchId = this.uuid.create(); this.$profilePatches.set(profileURI, [ ...this._getProfilePatches(profileURI), { id: patchId, body: patchBody }, ]); return patchId; } removeProfilePatch(profileURI, patchId) { this.$profilePatches.set( profileURI, this._getProfilePatches(profileURI).filter(({ id }) => id !== patchId), ); } applyProfilePatches(profile, patches) { const profilePatches = patches ?? this._getProfilePatches(profile.did); let patchedProfile = deepClone(profile); for (const patch of profilePatches) { patchedProfile = this.applyProfilePatch(patchedProfile, patch.body); } return patchedProfile; } applyProfilePatch(profile, patchBody) { switch (patchBody.type) { case "followProfile": return { ...profile, followersCount: profile.followersCount + 1, viewer: { ...profile.viewer, following: "fake following", }, }; case "unfollowProfile": return { ...profile, followersCount: profile.followersCount - 1, viewer: { ...profile.viewer, following: null, }, }; case "muteProfile": return { ...profile, viewer: { ...profile.viewer, muted: true, }, }; case "unmuteProfile": return { ...profile, viewer: { ...profile.viewer, muted: false, }, }; case "blockProfile": return { ...profile, viewer: { ...profile.viewer, blocking: "fake blocking", }, }; case "unblockProfile": return { ...profile, viewer: { ...profile.viewer, blocking: null, }, }; case "updatePostNotificationSubscription": return { ...profile, viewer: { ...profile.viewer, activitySubscription: patchBody.activitySubscription, }, }; default: throw new Error("Unknown patch type", patchBody.type); } } /* Message Patches */ _getMessagePatches(messageId) { return this.$messagePatches.get(messageId) || []; } addMessagePatch(messageId, patchBody) { const patchId = this.uuid.create(); this.$messagePatches.set(messageId, [ ...this._getMessagePatches(messageId), { id: patchId, body: patchBody }, ]); return patchId; } removeMessagePatch(messageId, patchId) { this.$messagePatches.set( messageId, this._getMessagePatches(messageId).filter(({ id }) => id !== patchId), ); } applyMessagePatches(message) { const messagePatches = this._getMessagePatches(message.id); let patchedMessage = deepClone(message); for (const patch of messagePatches) { patchedMessage = this.applyMessagePatch(patchedMessage, patch.body); } return patchedMessage; } applyMessagePatch(message, patchBody) { switch (patchBody.type) { case "addReaction": return { ...message, reactions: [...message.reactions, patchBody.reaction], }; case "removeReaction": const { currentUserDid, value } = patchBody; return { ...message, reactions: message.reactions.filter( (reaction) => reaction.sender.did !== currentUserDid && reaction.value !== value, ), }; default: throw new Error("Unknown patch type", patchBody.type); } } /* Preference Patches */ addPreferencePatch(patchBody) { const patchId = this.uuid.create(); const patches = this.$preferencePatches.get(); this.$preferencePatches.set([...patches, { id: patchId, body: patchBody }]); return patchId; } removePreferencePatch(patchId) { const patches = this.$preferencePatches.get(); this.$preferencePatches.set(patches.filter(({ id }) => id !== patchId)); } applyPreferencePatches(preferences, patches) { const preferencePatches = patches ?? this.$preferencePatches.get(); let patchedPreferences = preferences.clone(); for (const patch of preferencePatches) { patchedPreferences = this.applyPreferencePatch( patchedPreferences, patch.body, ); } return patchedPreferences; } applyPreferencePatch(preferences, patchBody) { switch (patchBody.type) { case "pinFeed": return preferences.pinFeed(patchBody.feedUri, patchBody.entryType); case "unpinFeed": return preferences.unpinFeed(patchBody.feedUri); case "subscribeLabeler": return preferences.subscribeLabeler( patchBody.did, patchBody.labelerInfo, ); case "unsubscribeLabeler": return preferences.unsubscribeLabeler(patchBody.did); case "setContentLabelPref": return preferences.setContentLabelPref({ label: patchBody.label, visibility: patchBody.visibility, labelerDid: patchBody.labelerDid, }); default: throw new Error("Unknown patch type", patchBody.type); } } /* Current User Patches */ addCurrentUserPatch(patchBody) { const patchId = this.uuid.create(); const patches = this.$currentUserPatches.get(); this.$currentUserPatches.set([ ...patches, { id: patchId, body: patchBody }, ]); return patchId; } removeCurrentUserPatch(patchId) { const patches = this.$currentUserPatches.get(); this.$currentUserPatches.set(patches.filter(({ id }) => id !== patchId)); } applyCurrentUserPatches(user, patches) { if (!user) return user; const currentUserPatches = patches ?? this.$currentUserPatches.get(); let patched = deepClone(user); for (const patch of currentUserPatches) { patched = this.applyCurrentUserPatch(patched, patch.body); } return patched; } applyCurrentUserPatch(user, patchBody) { switch (patchBody.type) { case "setPinnedPost": return { ...user, pinnedPost: patchBody.pinnedPost }; case "clearPinnedPost": { const { pinnedPost: _, ...rest } = user; return rest; } default: throw new Error("Unknown patch type", patchBody.type); } } /* Author Feed Patches */ _getAuthorFeedPatches(feedURI) { return this.$authorFeedPatches.get(feedURI) || []; } addAuthorFeedPatch(feedURI, patchBody) { const patchId = this.uuid.create(); this.$authorFeedPatches.set(feedURI, [ ...this._getAuthorFeedPatches(feedURI), { id: patchId, body: patchBody }, ]); return patchId; } removeAuthorFeedPatch(feedURI, patchId) { this.$authorFeedPatches.set( feedURI, this._getAuthorFeedPatches(feedURI).filter(({ id }) => id !== patchId), ); } applyAuthorFeedPatches(feedURI, feed) { if (!feed) return feed; const patches = this._getAuthorFeedPatches(feedURI); let patched = { feed: [...feed.feed], cursor: feed.cursor }; for (const patch of patches) { patched = this.applyAuthorFeedPatch(patched, patch.body); } return patched; } applyAuthorFeedPatch(feed, patchBody) { switch (patchBody.type) { case "pinPost": return { ...feed, feed: pinPostInFeed(feed.feed, patchBody.post) }; case "unpinPost": return { ...feed, feed: unpinPostInFeed(feed.feed, patchBody.post) }; default: throw new Error("Unknown patch type", patchBody.type); } } }