diff --git a/frontend/openchat-agent/src/services/community/candid/idl.js b/frontend/openchat-agent/src/services/community/candid/idl.js index 2217cd1a10..34b01915dd 100644 --- a/frontend/openchat-agent/src/services/community/candid/idl.js +++ b/frontend/openchat-agent/src/services/community/candid/idl.js @@ -58,49 +58,17 @@ export const idlFactory = ({ IDL }) => { 'added_by_name' : IDL.Text, 'added_by_display_name' : IDL.Opt(IDL.Text), }); - const ICRC2_TransferFromError = IDL.Variant({ - 'GenericError' : IDL.Record({ - 'message' : IDL.Text, - 'error_code' : IDL.Nat, - }), - 'TemporarilyUnavailable' : IDL.Null, - 'InsufficientAllowance' : IDL.Record({ 'allowance' : IDL.Nat }), - 'BadBurn' : IDL.Record({ 'min_burn_amount' : IDL.Nat }), - 'Duplicate' : IDL.Record({ 'duplicate_of' : IDL.Nat }), - 'BadFee' : IDL.Record({ 'expected_fee' : IDL.Nat }), - 'CreatedInFuture' : IDL.Record({ 'ledger_time' : IDL.Nat64 }), - 'TooOld' : IDL.Null, - 'InsufficientFunds' : IDL.Record({ 'balance' : IDL.Nat }), - }); - const GateCheckFailedReason = IDL.Variant({ - 'NotLifetimeDiamondMember' : IDL.Null, - 'NotDiamondMember' : IDL.Null, - 'PaymentFailed' : ICRC2_TransferFromError, - 'InsufficientBalance' : IDL.Nat, - 'NoSnsNeuronsFound' : IDL.Null, - 'NoSnsNeuronsWithRequiredDissolveDelayFound' : IDL.Null, - 'Locked' : IDL.Null, - 'NoUniquePersonProof' : IDL.Null, - 'FailedVerifiedCredentialCheck' : IDL.Text, - 'NoSnsNeuronsWithRequiredStakeFound' : IDL.Null, - }); - const UserFailedGateCheck = IDL.Record({ - 'user_id' : UserId, - 'reason' : GateCheckFailedReason, - }); const UserFailedError = IDL.Record({ 'user_id' : UserId, 'error' : IDL.Text, }); const AddMembersToChannelFailed = IDL.Record({ 'users_limit_reached' : IDL.Vec(UserId), - 'users_failed_gate_check' : IDL.Vec(UserFailedGateCheck), 'users_already_in_channel' : IDL.Vec(UserId), 'users_failed_with_error' : IDL.Vec(UserFailedError), }); const AddMembersToChannelPartialSuccess = IDL.Record({ 'users_limit_reached' : IDL.Vec(UserId), - 'users_failed_gate_check' : IDL.Vec(UserFailedGateCheck), 'users_already_in_channel' : IDL.Vec(UserId), 'users_failed_with_error' : IDL.Vec(UserFailedError), 'users_added' : IDL.Vec(UserId), @@ -108,6 +76,7 @@ export const idlFactory = ({ IDL }) => { const AddMembersToChannelResponse = IDL.Variant({ 'Failed' : AddMembersToChannelFailed, 'UserNotInChannel' : IDL.Null, + 'CommunityPublic' : IDL.Null, 'PartialSuccess' : AddMembersToChannelPartialSuccess, 'ChannelNotFound' : IDL.Null, 'UserLimitReached' : IDL.Nat32, diff --git a/frontend/openchat-agent/src/services/community/candid/types.d.ts b/frontend/openchat-agent/src/services/community/candid/types.d.ts index bee52518fb..a749bc1b7e 100644 --- a/frontend/openchat-agent/src/services/community/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/community/candid/types.d.ts @@ -65,6 +65,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | @@ -119,13 +120,11 @@ export interface AddMembersToChannelArgs { } export interface AddMembersToChannelFailed { 'users_limit_reached' : Array, - 'users_failed_gate_check' : Array, 'users_already_in_channel' : Array, 'users_failed_with_error' : Array, } export interface AddMembersToChannelPartialSuccess { 'users_limit_reached' : Array, - 'users_failed_gate_check' : Array, 'users_already_in_channel' : Array, 'users_failed_with_error' : Array, 'users_added' : Array, @@ -134,6 +133,7 @@ export type AddMembersToChannelResponse = { 'Failed' : AddMembersToChannelFailed } | { 'UserNotInChannel' : null } | + { 'CommunityPublic' : null } | { 'PartialSuccess' : AddMembersToChannelPartialSuccess } | { 'ChannelNotFound' : null } | { 'UserLimitReached' : number } | @@ -2355,10 +2355,6 @@ export interface UpdatedRules { } export interface User { 'username' : string, 'user_id' : UserId } export interface UserFailedError { 'user_id' : UserId, 'error' : string } -export interface UserFailedGateCheck { - 'user_id' : UserId, - 'reason' : GateCheckFailedReason, -} export interface UserGroup { 'members' : number, 'name' : string, diff --git a/frontend/openchat-agent/src/services/community/mappers.ts b/frontend/openchat-agent/src/services/community/mappers.ts index 1d28fd3fd2..3289004b9b 100644 --- a/frontend/openchat-agent/src/services/community/mappers.ts +++ b/frontend/openchat-agent/src/services/community/mappers.ts @@ -32,7 +32,6 @@ import type { UpdatedEvent, UpdateUserGroupResponse, UserFailedError, - UserFailedGateCheck, UserGroupDetails, } from "openchat-shared"; import { CommonResponses, UnsupportedValueError } from "openchat-shared"; @@ -54,7 +53,6 @@ import type { ApiOptionalCommunityPermissions, ApiAddMembersToChannelFailed, ApiAddMembersToChannelPartialSuccess, - ApiUserFailedGateCheck, ApiUserFailedError, ApiMessageMatch, ApiCommunityCanisterCommunitySummaryUpdates, @@ -83,7 +81,6 @@ import { communityChannelSummary, communityPermissions, communitySummary, - gateCheckFailedReason, groupPermissions, groupSubtype, memberRole, @@ -136,6 +133,9 @@ export function addMembersToChannelResponse( if ("InternalError" in candid) { return CommonResponses.internalError(); } + if ("CommunityPublic" in candid) { + return CommonResponses.communityPublic(); + } throw new UnsupportedValueError( "Unexpected ApiAddMembersToChannelResponse type received", candid, @@ -147,7 +147,6 @@ function addToChannelFailed(candid: ApiAddMembersToChannelFailed): AddMembersToC kind: "add_to_channel_failed", usersLimitReached: candid.users_limit_reached.map((u) => u.toString()), usersAlreadyInChannel: candid.users_already_in_channel.map((u) => u.toString()), - usersFailedGateCheck: candid.users_failed_gate_check.map(userFailedGateCheck), usersFailedWithError: candid.users_failed_with_error.map(userFailedWithError), }; } @@ -159,13 +158,6 @@ function userFailedWithError(candid: ApiUserFailedError): UserFailedError { }; } -function userFailedGateCheck(candid: ApiUserFailedGateCheck): UserFailedGateCheck { - return { - userId: candid.user_id.toString(), - reason: gateCheckFailedReason(candid.reason), - }; -} - function addToChannelPartialSuccess( candid: ApiAddMembersToChannelPartialSuccess, ): AddMembersToChannelResponse { @@ -173,7 +165,6 @@ function addToChannelPartialSuccess( kind: "add_to_channel_partial_success", usersLimitReached: candid.users_limit_reached.map((u) => u.toString()), usersAlreadyInChannel: candid.users_already_in_channel.map((u) => u.toString()), - usersFailedGateCheck: candid.users_failed_gate_check.map(userFailedGateCheck), usersFailedWithError: candid.users_failed_with_error.map(userFailedWithError), usersAdded: candid.users_added.map((u) => u.toString()), }; diff --git a/frontend/openchat-agent/src/services/group/candid/types.d.ts b/frontend/openchat-agent/src/services/group/candid/types.d.ts index f083edab31..9014f10279 100644 --- a/frontend/openchat-agent/src/services/group/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/group/candid/types.d.ts @@ -62,6 +62,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts index a345be8cf2..2f56676689 100644 --- a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts index 15f1e0e8fd..d078d3d4d0 100644 --- a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts @@ -51,6 +51,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts index 075ebea974..789655e823 100644 --- a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/online/candid/types.d.ts b/frontend/openchat-agent/src/services/online/candid/types.d.ts index eec406bef8..086a10d5ae 100644 --- a/frontend/openchat-agent/src/services/online/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/online/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts index 90e46aab7c..76dc5c4e27 100644 --- a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/registry/candid/types.d.ts b/frontend/openchat-agent/src/services/registry/candid/types.d.ts index 1ac2669ef6..2b85e8b2c6 100644 --- a/frontend/openchat-agent/src/services/registry/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/registry/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts index ade672ec4a..6dc4a85013 100644 --- a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts index 8f8904b0ee..6cf1a8125c 100644 --- a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/translations/candid/types.d.ts b/frontend/openchat-agent/src/services/translations/candid/types.d.ts index 946fe8165e..962ef8cbd4 100644 --- a/frontend/openchat-agent/src/services/translations/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/translations/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/user/candid/idl.js b/frontend/openchat-agent/src/services/user/candid/idl.js index 30f6a18b95..bbe27cebc5 100644 --- a/frontend/openchat-agent/src/services/user/candid/idl.js +++ b/frontend/openchat-agent/src/services/user/candid/idl.js @@ -174,6 +174,7 @@ export const idlFactory = ({ IDL }) => { 'StartedCall' : IDL.Null, 'ChosenAsGroupOwner' : IDL.Null, 'TippedMessage' : IDL.Null, + 'Streak365' : IDL.Null, 'SentGiphy' : IDL.Null, 'SetCommunityAccessGate' : IDL.Null, 'Streak14' : IDL.Null, diff --git a/frontend/openchat-agent/src/services/user/candid/types.d.ts b/frontend/openchat-agent/src/services/user/candid/types.d.ts index 07a8042183..29b43e64c1 100644 --- a/frontend/openchat-agent/src/services/user/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/user/candid/types.d.ts @@ -62,6 +62,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | diff --git a/frontend/openchat-agent/src/services/user/mappers.ts b/frontend/openchat-agent/src/services/user/mappers.ts index 7d8b87b29f..09e1390657 100644 --- a/frontend/openchat-agent/src/services/user/mappers.ts +++ b/frontend/openchat-agent/src/services/user/mappers.ts @@ -393,6 +393,9 @@ export function achievementType(candid: ApiAchievement): Achievement { if ("Streak7" in candid) { return "streak_7"; } + if ("Streak365" in candid) { + return "streak_365"; + } if ("UpgradedToGoldDiamond" in candid) { return "upgrade_to_gold_diamond"; } diff --git a/frontend/openchat-agent/src/services/userIndex/candid/idl.js b/frontend/openchat-agent/src/services/userIndex/candid/idl.js index 622b86f674..170170c8e8 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/idl.js +++ b/frontend/openchat-agent/src/services/userIndex/candid/idl.js @@ -389,6 +389,7 @@ export const idlFactory = ({ IDL }) => { }); const UsersResponse = IDL.Variant({ 'Success' : IDL.Record({ + 'deleted' : IDL.Vec(UserId), 'timestamp' : TimestampMillis, 'users' : IDL.Vec(UserSummaryV2), 'current_user' : IDL.Opt(CurrentUserSummary), diff --git a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts index af5b63eb50..703478636c 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts @@ -44,6 +44,7 @@ export type Achievement = { 'AppointedGroupModerator' : null } | { 'StartedCall' : null } | { 'ChosenAsGroupOwner' : null } | { 'TippedMessage' : null } | + { 'Streak365' : null } | { 'SentGiphy' : null } | { 'SetCommunityAccessGate' : null } | { 'Streak14' : null } | @@ -1859,6 +1860,7 @@ export interface UsersInvited { } export type UsersResponse = { 'Success' : { + 'deleted' : Array, 'timestamp' : TimestampMillis, 'users' : Array, 'current_user' : [] | [CurrentUserSummary], diff --git a/frontend/openchat-agent/src/services/userIndex/mappers.ts b/frontend/openchat-agent/src/services/userIndex/mappers.ts index eb3a311127..cbf5ba5012 100644 --- a/frontend/openchat-agent/src/services/userIndex/mappers.ts +++ b/frontend/openchat-agent/src/services/userIndex/mappers.ts @@ -68,6 +68,7 @@ export function usersApiResponse(candid: ApiUsersResponse): UsersApiResponse { return { serverTimestamp: timestamp, users: candid.Success.users.map(userSummaryUpdate), + deletedUserIds: new Set(candid.Success.deleted.map((d) => d.toString())), currentUser: optional(candid.Success.current_user, (u) => currentUserSummary(u, timestamp), ), diff --git a/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts b/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts index ab7ff3094e..743a0475b2 100644 --- a/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts +++ b/frontend/openchat-agent/src/services/userIndex/userIndex.client.ts @@ -52,7 +52,9 @@ import { apiOptional, apiToken } from "../common/chatMappers"; import type { AgentConfig } from "../../config"; import { getCachedUsers, + getCachedDeletedUserIds, getSuspendedUsersSyncedUpTo, + setCachedDeletedUserIds, setCachedUsers, setDisplayNameInCache, setSuspendedUsersSyncedUpTo, @@ -144,16 +146,17 @@ export class UserIndexClient extends CandidService { const allUsers = users.userGroups.flatMap((g) => g.users); const fromCache = await getCachedUsers(allUsers); + const cachedDeletedUserIds = await getCachedDeletedUserIds(); const suspendedUsersSyncedTo = await getSuspendedUsersSyncedUpTo(); // We throw away all of the updatedSince values passed in and instead use the values from the cache, this // ensures the cache is always correct and doesn't miss any updates - const args = this.buildGetUsersArgs(allUsers, fromCache, allowStale); - - const apiResponse = await this.getUsersFromBackend(args, suspendedUsersSyncedTo); + const args = this.buildGetUsersArgs(allUsers, fromCache, allowStale, cachedDeletedUserIds); const requestedFromServer = new Set([...args.userGroups.flatMap((g) => g.users)]); + const apiResponse = await this.getUsersFromBackend(args, suspendedUsersSyncedTo); + // We return the fully hydrated users so that it is not possible for the Svelte store to miss any updates const mergedResponse = this.mergeGetUsersResponse( chitState, @@ -163,6 +166,8 @@ export class UserIndexClient extends CandidService { fromCache, ); + setCachedDeletedUserIds(apiResponse.deletedUserIds); + setCachedUsers(mergedResponse.users).catch((err) => console.error("Failed to save users to the cache", err), ); @@ -188,6 +193,7 @@ export class UserIndexClient extends CandidService { return Promise.resolve({ serverTimestamp: 0n, users: [], + deletedUserIds: new Set(), }); const userGroups = users.userGroups.filter((g) => g.users.length > 0); @@ -211,6 +217,7 @@ export class UserIndexClient extends CandidService { users: string[], fromCache: UserSummary[], allowStale: boolean, + cachedDeletedUserIds: Set, ): UsersArgs { const fromCacheGrouped = groupBy(fromCache, (u) => u.updated); const fromCacheSet = new Set(fromCache.map((u) => u.userId)); @@ -220,7 +227,9 @@ export class UserIndexClient extends CandidService { }; // Add the users not found in the cache and ask for all updates - const notFoundInCache = users.filter((u) => !fromCacheSet.has(u)); + const notFoundInCache = users.filter( + (u) => !fromCacheSet.has(u) && !cachedDeletedUserIds.has(u), + ); if (notFoundInCache.length > 0) { args.userGroups.push({ users: notFoundInCache, @@ -232,7 +241,9 @@ export class UserIndexClient extends CandidService { // Add the users found in the cache but only ask for updates since the date they were last updated in the cache for (const [updatedSince, users] of fromCacheGrouped) { args.userGroups.push({ - users: users.map((u) => u.userId), + users: users + .filter((u) => !cachedDeletedUserIds.has(u.userId)) + .map((u) => u.userId), updatedSince, }); } @@ -312,6 +323,7 @@ export class UserIndexClient extends CandidService { serverTimestamp: apiResponse.serverTimestamp, users, currentUser: apiResponse.currentUser, + deletedUserIds: apiResponse.deletedUserIds, }; } diff --git a/frontend/openchat-agent/src/utils/userCache.ts b/frontend/openchat-agent/src/utils/userCache.ts index 277d0f73c5..97ceffde02 100644 --- a/frontend/openchat-agent/src/utils/userCache.ts +++ b/frontend/openchat-agent/src/utils/userCache.ts @@ -1,7 +1,7 @@ import { openDB, type DBSchema, type IDBPDatabase } from "idb"; -import type { DiamondMembershipStatus, UserSummary } from "openchat-shared"; +import { deletedUser, type DiamondMembershipStatus, type UserSummary } from "openchat-shared"; -const CACHE_VERSION = 9; +const CACHE_VERSION = 10; let db: UserDatabase | undefined; @@ -17,6 +17,11 @@ export interface UserSchema extends DBSchema { key: "value"; value: bigint; }; + + deletedUserIds: { + key: string; + value: string; + }; } export function lazyOpenUserCache(): UserDatabase { @@ -35,8 +40,12 @@ function openUserCache(): UserDatabase { if (db.objectStoreNames.contains("suspendedUsersSyncedUpTo")) { db.deleteObjectStore("suspendedUsersSyncedUpTo"); } + if (db.objectStoreNames.contains("deletedUserIds")) { + db.deleteObjectStore("deletedUserIds"); + } db.createObjectStore("users"); db.createObjectStore("suspendedUsersSyncedUpTo"); + db.createObjectStore("deletedUserIds"); }, }); } @@ -53,7 +62,17 @@ export async function getCachedUsers(userIds: string[]): Promise } export async function getAllUsers(): Promise { - return (await lazyOpenUserCache()).getAll("users"); + const users = await (await lazyOpenUserCache()).getAll("users"); + const deleted = await getDeletedUserIdsList(); + return [...users, ...deleted.map(deletedUser)]; +} + +async function getDeletedUserIdsList(): Promise { + return (await lazyOpenUserCache()).getAll("deletedUserIds"); +} + +export async function getCachedDeletedUserIds(): Promise> { + return getDeletedUserIdsList().then((list) => new Set(list)); } export async function setCachedUsers(users: UserSummary[]): Promise { @@ -61,6 +80,15 @@ export async function setCachedUsers(users: UserSummary[]): Promise { writeCachedUsersToDatabase(lazyOpenUserCache(), users); } +export async function setCachedDeletedUserIds(deletedUserIds: Set): Promise { + if (deletedUserIds.size === 0) return; + const db = await lazyOpenUserCache(); + const tx = (await db).transaction("deletedUserIds", "readwrite", { durability: "relaxed" }); + const store = tx.objectStore("deletedUserIds"); + Promise.all([...deletedUserIds].map((d) => store.put(d, d))); + await tx.done; +} + export async function writeCachedUsersToDatabase( db: UserDatabase, users: UserSummary[], diff --git a/frontend/openchat-client/src/openchat.ts b/frontend/openchat-client/src/openchat.ts index 8e7bb66b15..d0b35410d0 100644 --- a/frontend/openchat-client/src/openchat.ts +++ b/frontend/openchat-client/src/openchat.ts @@ -470,6 +470,7 @@ import { LARGE_GROUP_THRESHOLD, isCompositeGate, shouldPreprocessGate, + deletedUser, } from "openchat-shared"; import { failedMessagesStore } from "./stores/failedMessages"; import { @@ -4928,6 +4929,7 @@ export class OpenChat extends OpenChatAgentWorker { if (userGroups.length === 0) { return Promise.resolve({ users: [], + deletedUserIds: new Set(), }); } @@ -4938,7 +4940,8 @@ export class OpenChat extends OpenChatAgentWorker { allowStale, }) .then((resp) => { - userStore.addMany(resp.users); + const deletedUsers = [...resp.deletedUserIds].map(deletedUser); + userStore.addMany([...resp.users, ...deletedUsers]); if (resp.serverTimestamp !== undefined) { // If we went to the server, all users not returned are still up to date, so we mark them as such const usersReturned = new Set(resp.users.map((u) => u.userId)); @@ -4954,7 +4957,7 @@ export class OpenChat extends OpenChatAgentWorker { } return resp; }) - .catch(() => ({ users: [] })); + .catch(() => ({ users: [], deletedUserIds: new Set() })); } getUser(userId: string, allowStale = false): Promise { diff --git a/frontend/openchat-client/src/stores/user.ts b/frontend/openchat-client/src/stores/user.ts index 38df4a1ebb..ec217059bc 100644 --- a/frontend/openchat-client/src/stores/user.ts +++ b/frontend/openchat-client/src/stores/user.ts @@ -140,13 +140,15 @@ export const userStore = { } }, addMany: (newUsers: UserSummary[]): void => { - normalUsers.update((users) => { - const clone = new Map(users); - return newUsers.reduce((lookup, user) => overwriteUser(lookup, user), clone); - }); - const [suspended, notSuspended] = partitionSuspendedUsers(newUsers); - suspendedUsers.addMany(suspended); - suspendedUsers.deleteMany(notSuspended); + if (newUsers.length > 0) { + normalUsers.update((users) => { + const clone = new Map(users); + return newUsers.reduce((lookup, user) => overwriteUser(lookup, user), clone); + }); + const [suspended, notSuspended] = partitionSuspendedUsers(newUsers); + suspendedUsers.addMany(suspended); + suspendedUsers.deleteMany(notSuspended); + } }, setUpdated: (userIds: string[], timestamp: bigint): void => { normalUsers.update((users) => { diff --git a/frontend/openchat-shared/src/domain/chit.ts b/frontend/openchat-shared/src/domain/chit.ts index b29951d113..777d9cf930 100644 --- a/frontend/openchat-shared/src/domain/chit.ts +++ b/frontend/openchat-shared/src/domain/chit.ts @@ -57,6 +57,7 @@ export const achievements = [ "streak_7", "streak_14", "streak_30", + "streak_365", "sent_reminder", "proved_unique_personhood", "pinned_message", diff --git a/frontend/openchat-shared/src/domain/community/index.ts b/frontend/openchat-shared/src/domain/community/index.ts index 19084c3dc0..9f062ac236 100644 --- a/frontend/openchat-shared/src/domain/community/index.ts +++ b/frontend/openchat-shared/src/domain/community/index.ts @@ -27,6 +27,7 @@ import type { import type { ChatNotFound, CommunityFrozen, + CommunityPublic, Failure, InternalError, NotAuthorised, @@ -104,14 +105,12 @@ export interface UserFailedError { export type AddMembersToChannelFailed = { kind: "add_to_channel_failed"; usersLimitReached: string[]; - usersFailedGateCheck: UserFailedGateCheck[]; usersAlreadyInChannel: string[]; usersFailedWithError: UserFailedError[]; }; export interface AddMembersToChannelPartialSuccess { kind: "add_to_channel_partial_success"; usersLimitReached: string[]; - usersFailedGateCheck: UserFailedGateCheck[]; usersAlreadyInChannel: string[]; usersFailedWithError: UserFailedError[]; usersAdded: string[]; @@ -128,7 +127,8 @@ export type AddMembersToChannelResponse = | UserSuspended | CommunityFrozen | InternalError - | Offline; + | Offline + | CommunityPublic; export type BlockCommunityUserResponse = Success | Failure | Offline; diff --git a/frontend/openchat-shared/src/domain/response.ts b/frontend/openchat-shared/src/domain/response.ts index db65f89818..0c8636970e 100644 --- a/frontend/openchat-shared/src/domain/response.ts +++ b/frontend/openchat-shared/src/domain/response.ts @@ -10,6 +10,7 @@ export type UserNotInCommunity = { kind: "user_not_in_community" }; export type CommunityFrozen = { kind: "community_frozen" }; export type ChatFrozen = { kind: "chat_frozen" }; export type CommunityNotPublic = { kind: "community_not_public" }; +export type CommunityPublic = { kind: "community_public" }; export type MessageNotFound = { kind: "message_not_found"; }; @@ -77,6 +78,7 @@ export const CommonResponses = { offline: (): Offline => ({ kind: "offline" }) as Offline, blocked: (): Blocked => ({ kind: "blocked" }) as Blocked, userNotFound: (): UserNotFound => ({ kind: "unknown_user" }), + communityPublic: (): CommunityPublic => ({ kind: "community_public" }), }; export type Blocked = { diff --git a/frontend/openchat-shared/src/domain/user/user.ts b/frontend/openchat-shared/src/domain/user/user.ts index 7290e4b64a..1121b946a9 100644 --- a/frontend/openchat-shared/src/domain/user/user.ts +++ b/frontend/openchat-shared/src/domain/user/user.ts @@ -27,6 +27,22 @@ export type UserSummary = DataContent & { isUniquePerson: boolean; }; +export function deletedUser(userId: string): UserSummary { + return { + kind: "user", + userId, + username: "Deleted User", + displayName: undefined, + updated: BigInt(Number.MAX_VALUE), // we want to *never* request updates for a deleted user + suspended: false, + diamondStatus: "inactive", + chitBalance: 0, + streak: 0, + isUniquePerson: false, + totalChitEarned: 0, + }; +} + // Note this *has* to return UserSummary | undefined because of the types, but we would not expect it to ever do so in practice export function mergeUserSummaryWithUpdates( cached: UserSummary | undefined, @@ -163,6 +179,7 @@ export type UsersArgs = { export type UsersResponse = { serverTimestamp?: bigint; users: UserSummary[]; + deletedUserIds: Set; currentUser?: CurrentUserSummary; }; @@ -189,6 +206,7 @@ export type UserSummaryUpdate = { export type UsersApiResponse = { serverTimestamp: bigint; + deletedUserIds: Set; users: UserSummaryUpdate[]; currentUser?: CurrentUserSummary; };