From 3f0c4cd97f4c16b1ccd2fbb13f426f91cb813f09 Mon Sep 17 00:00:00 2001 From: Julian Jelfs Date: Fri, 24 Nov 2023 13:00:12 +0000 Subject: [PATCH] refine offline tracking (#4825) --- frontend/app/src/components/home/Home.svelte | 6 +- .../src/components/home/LinkPreview.svelte | 4 +- .../home/communities/explore/Explore.svelte | 4 +- frontend/openchat-agent/src/constants.ts | 6 - .../src/services/candidService.ts | 4 +- .../services/community/community.client.ts | 22 ++- .../src/services/group/group.client.ts | 12 +- .../src/services/openchatAgent.ts | 157 +++++++++--------- .../src/services/user/user.client.ts | 17 +- frontend/openchat-agent/src/utils/caching.ts | 3 +- frontend/openchat-agent/src/utils/chat.ts | 14 +- frontend/openchat-client/src/liveState.ts | 6 +- frontend/openchat-client/src/openchat.ts | 44 ++--- .../openchat-client/src/stores/markRead.ts | 6 +- .../openchat-client/src/stores/network.ts | 45 +++-- frontend/openchat-client/src/types.d.ts | 14 ++ frontend/openchat-client/src/utils/poller.ts | 17 +- frontend/openchat-shared/src/constants.ts | 9 + frontend/openchat-shared/src/index.ts | 3 +- frontend/openchat-shared/src/types.d.ts | 12 ++ frontend/openchat-shared/src/utils/index.ts | 1 + frontend/openchat-shared/src/utils/logging.ts | 3 +- frontend/openchat-shared/src/utils/network.ts | 13 ++ 23 files changed, 250 insertions(+), 172 deletions(-) delete mode 100644 frontend/openchat-agent/src/constants.ts create mode 100644 frontend/openchat-client/src/types.d.ts create mode 100644 frontend/openchat-shared/src/types.d.ts create mode 100644 frontend/openchat-shared/src/utils/network.ts diff --git a/frontend/app/src/components/home/Home.svelte b/frontend/app/src/components/home/Home.svelte index 6d79c1199a..54fff88aa2 100644 --- a/frontend/app/src/components/home/Home.svelte +++ b/frontend/app/src/components/home/Home.svelte @@ -195,7 +195,7 @@ ? selectedMultiUserChat.subtype?.governanceCanisterId : undefined; $: nervousSystem = client.tryGetNervousSystem(governanceCanisterId); - $: networkStatus = client.networkStatus; + $: offlineStore = client.offlineStore; $: { if ($identityState.kind === "registering") { @@ -1058,7 +1058,7 @@ on:close={() => (showProfileCard = undefined)} /> {/if} -
+
{#if $layoutStore.showNav} {/if} -{#if $networkStatus === "offline"} +{#if $offlineStore} {/if} diff --git a/frontend/app/src/components/home/LinkPreview.svelte b/frontend/app/src/components/home/LinkPreview.svelte index 7ec1292891..83f121fa95 100644 --- a/frontend/app/src/components/home/LinkPreview.svelte +++ b/frontend/app/src/components/home/LinkPreview.svelte @@ -23,7 +23,7 @@ $: youtubeMatch = text.match(client.youtubeRegex()); $: twitterLinkMatch = text.match(client.twitterLinkRegex()); - $: networkStatus = client.networkStatus; + $: offlineStore = client.offlineStore; function closestAncestor( el: HTMLElement | null | undefined, @@ -55,7 +55,7 @@ intersecting && !$eventListScrolling && !rendered && - $networkStatus === "online" + !$offlineStore ) { // make sure we only actually *load* the preview(s) once previewsPromise = previewsPromise ?? loadPreviews(links); diff --git a/frontend/app/src/components/home/communities/explore/Explore.svelte b/frontend/app/src/components/home/communities/explore/Explore.svelte index 0287fb6a7a..ebb5708251 100644 --- a/frontend/app/src/components/home/communities/explore/Explore.svelte +++ b/frontend/app/src/components/home/communities/explore/Explore.svelte @@ -36,7 +36,7 @@ $: more = total > searchResults.length; $: isDiamond = client.isDiamond; $: loading = searching && searchResults.length === 0; - $: networkStatus = client.networkStatus; + $: offlineStore = client.offlineStore; let filters = derived( [communityFiltersStore, client.moderationFlags], @@ -167,7 +167,7 @@ {:else if searchResults.length === 0} - {#if $networkStatus === "offline"} + {#if $offlineStore}

{$_("offlineError")}

diff --git a/frontend/openchat-agent/src/constants.ts b/frontend/openchat-agent/src/constants.ts deleted file mode 100644 index 9017bb859c..0000000000 --- a/frontend/openchat-agent/src/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const MAX_MESSAGES = 100; -export const MAX_EVENTS = 500; -export const MAX_MISSING = 30; -export const OPENCHAT_BOT_USER_ID = "zzyk3-openc-hatbo-tq7my-cai"; -export const OPENCHAT_BOT_USERNAME = "OpenChatBot"; -export const OPENCHAT_BOT_AVATAR_URL = "assets/robot.svg"; diff --git a/frontend/openchat-agent/src/services/candidService.ts b/frontend/openchat-agent/src/services/candidService.ts index 2420977731..7ed6a61eca 100644 --- a/frontend/openchat-agent/src/services/candidService.ts +++ b/frontend/openchat-agent/src/services/candidService.ts @@ -1,7 +1,7 @@ import { Actor, HttpAgent, type Identity } from "@dfinity/agent"; import type { IDL } from "@dfinity/candid"; import type { Principal } from "@dfinity/principal"; -import { AuthError, DestinationInvalidError, SessionExpiryError } from "openchat-shared"; +import { AuthError, DestinationInvalidError, SessionExpiryError, offline } from "openchat-shared"; import type { AgentConfig } from "../config"; import { ReplicaNotUpToDateError, toCanisterResponseError } from "./error"; @@ -21,7 +21,7 @@ export abstract class CandidService { const host = config.icUrl; const agent = new HttpAgent({ identity: this.identity, host, retryTimes: 5 }); const isMainnet = config.icUrl.includes("icp-api.io"); - if (!isMainnet && navigator.onLine) { + if (!isMainnet && !offline()) { agent.fetchRootKey(); } return Actor.createActor(factory, { diff --git a/frontend/openchat-agent/src/services/community/community.client.ts b/frontend/openchat-agent/src/services/community/community.client.ts index c991c45e20..c81610e7bf 100644 --- a/frontend/openchat-agent/src/services/community/community.client.ts +++ b/frontend/openchat-agent/src/services/community/community.client.ts @@ -130,14 +130,20 @@ import type { ClaimPrizeResponse, OptionalChatPermissions, } from "openchat-shared"; -import { textToCode, DestinationInvalidError } from "openchat-shared"; +import { + textToCode, + DestinationInvalidError, + offline, + MAX_EVENTS, + MAX_MESSAGES, + MAX_MISSING, +} from "openchat-shared"; import { apiOptionalGroupPermissions, apiUpdatedRules, getMessagesByMessageIndexResponse, } from "../group/mappers"; import { DataClient } from "../data/data.client"; -import { MAX_EVENTS, MAX_MESSAGES, MAX_MISSING } from "../../constants"; import { getEventsResponse } from "../group/mappers"; import { type Database, @@ -740,7 +746,7 @@ export class CommunityClient extends CandidService { ): Promise { const fromCache = await getCachedCommunityDetails(this.db, id.communityId); if (fromCache !== undefined) { - if (fromCache.lastUpdated >= communityLastUpdated || !navigator.onLine) { + if (fromCache.lastUpdated >= communityLastUpdated || offline()) { return fromCache; } else { return this.getCommunityDetailsUpdates(id, fromCache); @@ -807,7 +813,7 @@ export class CommunityClient extends CandidService { ): Promise { const fromCache = await getCachedGroupDetails(this.db, chatId.channelId); if (fromCache !== undefined) { - if (fromCache.timestamp >= chatLastUpdated || !navigator.onLine) { + if (fromCache.timestamp >= chatLastUpdated || offline()) { return fromCache; } else { return this.getChannelDetailsUpdates(chatId, fromCache); @@ -1115,8 +1121,8 @@ export class CommunityClient extends CandidService { gate === undefined ? { NoChange: null } : gate.kind === "no_gate" - ? { SetToNone: null } - : { SetToSome: apiAccessGate(gate) }, + ? { SetToNone: null } + : { SetToSome: apiAccessGate(gate) }, avatar: avatar === undefined ? { NoChange: null } @@ -1155,8 +1161,8 @@ export class CommunityClient extends CandidService { gate === undefined ? { NoChange: null } : gate.kind === "no_gate" - ? { SetToNone: null } - : { SetToSome: apiAccessGate(gate) }, + ? { SetToNone: null } + : { SetToSome: apiAccessGate(gate) }, avatar: avatar === undefined ? { NoChange: null } diff --git a/frontend/openchat-agent/src/services/group/group.client.ts b/frontend/openchat-agent/src/services/group/group.client.ts index d74292e1c3..6067e66e3b 100644 --- a/frontend/openchat-agent/src/services/group/group.client.ts +++ b/frontend/openchat-agent/src/services/group/group.client.ts @@ -48,7 +48,14 @@ import type { OptionalChatPermissions, ToggleMuteNotificationResponse, } from "openchat-shared"; -import { DestinationInvalidError, textToCode } from "openchat-shared"; +import { + DestinationInvalidError, + offline, + textToCode, + MAX_EVENTS, + MAX_MESSAGES, + MAX_MISSING, +} from "openchat-shared"; import { CandidService } from "../candidService"; import { apiRole, @@ -109,7 +116,6 @@ import { } from "../common/chatMappers"; import { DataClient } from "../data/data.client"; import { mergeGroupChatDetails } from "../../utils/chat"; -import { MAX_EVENTS, MAX_MESSAGES, MAX_MISSING } from "../../constants"; import { publicSummaryResponse } from "../common/publicSummaryMapper"; import { apiOptionUpdate, identity } from "../../utils/mapping"; import { generateUint64 } from "../../utils/rng"; @@ -574,7 +580,7 @@ export class GroupClient extends CandidService { async getGroupDetails(chatLastUpdated: bigint): Promise { const fromCache = await getCachedGroupDetails(this.db, this.chatId.groupId); if (fromCache !== undefined) { - if (fromCache.timestamp >= chatLastUpdated || !navigator.onLine) { + if (fromCache.timestamp >= chatLastUpdated || offline()) { return fromCache; } else { return this.getGroupDetailsUpdates(fromCache); diff --git a/frontend/openchat-agent/src/services/openchatAgent.ts b/frontend/openchat-agent/src/services/openchatAgent.ts index 5e6277b581..32689f5be7 100644 --- a/frontend/openchat-agent/src/services/openchatAgent.ts +++ b/frontend/openchat-agent/src/services/openchatAgent.ts @@ -191,6 +191,7 @@ import { CommonResponses, applyOptionUpdate, ANON_USER_ID, + offline, Stream, waitAll, } from "openchat-shared"; @@ -360,7 +361,7 @@ export class OpenChatAgent extends EventTarget { msg: Message, threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve("failure"); + if (offline()) return Promise.resolve("failure"); switch (chatId.kind) { case "direct_chat": @@ -382,7 +383,7 @@ export class OpenChatAgent extends EventTarget { ): Promise<[SendMessageResponse, Message]> { const { chatId, threadRootMessageIndex } = messageContext; - if (!navigator.onLine) { + if (offline()) { recordFailedMessage(this.db, chatId, event, threadRootMessageIndex); return Promise.resolve([CommonResponses.offline(), event.event]); } @@ -521,7 +522,7 @@ export class OpenChatAgent extends EventTarget { } createGroupChat(candidate: CandidateGroupChat): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); if (candidate.id.kind === "channel") { return this.communityClient(candidate.id.communityId).createChannel(candidate); @@ -541,7 +542,7 @@ export class OpenChatAgent extends EventTarget { gate?: AccessGate, isPublic?: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -578,7 +579,7 @@ export class OpenChatAgent extends EventTarget { return Promise.resolve("success"); } - if (!navigator.onLine) return Promise.resolve("failure"); + if (offline()) return Promise.resolve("failure"); const communityLocalUserIndex = await this.communityClient(id.communityId).localUserIndex(); return this.createLocalUserIndexClient(communityLocalUserIndex).inviteUsersToCommunity( @@ -595,7 +596,7 @@ export class OpenChatAgent extends EventTarget { return Promise.resolve("success"); } - if (!navigator.onLine) return Promise.resolve("failure"); + if (offline()) return Promise.resolve("failure"); switch (chatId.kind) { case "group_chat": @@ -1200,7 +1201,7 @@ export class OpenChatAgent extends EventTarget { } searchUsers(searchTerm: string, maxResults = 20): Promise { - if (!navigator.onLine) return Promise.resolve([]); + if (offline()) return Promise.resolve([]); return this._userIndexClient .searchUsers(searchTerm, maxResults) @@ -1213,7 +1214,7 @@ export class OpenChatAgent extends EventTarget { pageIndex: number, pageSize = 10, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.communityClient(id.communityId) .exploreChannels(searchTerm, pageIndex, pageSize) @@ -1238,7 +1239,7 @@ export class OpenChatAgent extends EventTarget { flags: number, languages: string[], ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this._groupIndexClient .exploreCommunities(searchTerm, pageIndex, pageSize, flags, languages) @@ -1258,7 +1259,7 @@ export class OpenChatAgent extends EventTarget { } searchGroups(searchTerm: string, maxResults = 10): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this._groupIndexClient.searchGroups(searchTerm, maxResults).then((res) => { if (res.kind === "success") { @@ -1277,7 +1278,7 @@ export class OpenChatAgent extends EventTarget { userIds: string[], maxResults = 10, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -1301,7 +1302,7 @@ export class OpenChatAgent extends EventTarget { searchTerm: string, maxResults = 10, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.userClient.searchDirectChat(chatId, searchTerm, maxResults); } @@ -1676,19 +1677,19 @@ export class OpenChatAgent extends EventTarget { } setModerationFlags(flags: number): Promise { - if (!navigator.onLine) return Promise.resolve(false); + if (offline()) return Promise.resolve(false); return this._userIndexClient.setModerationFlags(flags); } checkUsername(username: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._userIndexClient.checkUsername(username); } setUsername(userId: string, username: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._userIndexClient.setUsername(userId, username); } @@ -1697,7 +1698,7 @@ export class OpenChatAgent extends EventTarget { userId: string, displayName: string | undefined, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._userIndexClient.setDisplayName(userId, displayName); } @@ -1707,7 +1708,7 @@ export class OpenChatAgent extends EventTarget { userId: string, newRole: MemberRole, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -1722,7 +1723,7 @@ export class OpenChatAgent extends EventTarget { } deleteGroup(chatId: MultiUserChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -1733,7 +1734,7 @@ export class OpenChatAgent extends EventTarget { } removeMember(chatId: MultiUserChatIdentifier, userId: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -1747,7 +1748,7 @@ export class OpenChatAgent extends EventTarget { } blockUserFromDirectChat(userId: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.blockUser(userId); } @@ -1756,7 +1757,7 @@ export class OpenChatAgent extends EventTarget { chatId: MultiUserChatIdentifier, userId: string, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); if (chatId.kind === "channel") throw new Error("TODO - blockUserFromChannel not implemented"); @@ -1767,7 +1768,7 @@ export class OpenChatAgent extends EventTarget { chatId: MultiUserChatIdentifier, userId: string, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); if (chatId.kind === "channel") throw new Error("TODO - unblockUserFromChannel not implemented"); @@ -1775,13 +1776,13 @@ export class OpenChatAgent extends EventTarget { } unblockUserFromDirectChat(userId: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.unblockUser(userId); } leaveGroup(chatId: MultiUserChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); if (chatIdentifiersEqual(this._groupInvite?.chatId, chatId)) { this._groupInvite = undefined; @@ -1798,7 +1799,7 @@ export class OpenChatAgent extends EventTarget { chatId: MultiUserChatIdentifier, _credential?: string, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -1826,7 +1827,7 @@ export class OpenChatAgent extends EventTarget { id: CommunityIdentifier, _credential?: string, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); const inviteCode = this.getProvidedCommunityInviteCode(id.communityId); const localUserIndex = await this.communityClient(id.communityId).localUserIndex(); @@ -1852,7 +1853,7 @@ export class OpenChatAgent extends EventTarget { displayName: string | undefined, threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -1890,7 +1891,7 @@ export class OpenChatAgent extends EventTarget { reaction: string, threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -1924,7 +1925,7 @@ export class OpenChatAgent extends EventTarget { threadRootMessageIndex?: number, asPlatformModerator?: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -1954,7 +1955,7 @@ export class OpenChatAgent extends EventTarget { threadRootMessageIndex?: number, asPlatformModerator?: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.communityClient(chatId.communityId).deleteMessages( chatId, @@ -1970,7 +1971,7 @@ export class OpenChatAgent extends EventTarget { threadRootMessageIndex?: number, asPlatformModerator?: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.getGroupClient(chatId).deleteMessage( messageId, @@ -1984,7 +1985,7 @@ export class OpenChatAgent extends EventTarget { messageId: bigint, threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.deleteMessage(otherUserId, messageId, threadRootMessageIndex); } @@ -1994,7 +1995,7 @@ export class OpenChatAgent extends EventTarget { messageId: bigint, threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -2041,7 +2042,7 @@ export class OpenChatAgent extends EventTarget { chatId: ChatIdentifier, muted: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -2099,7 +2100,7 @@ export class OpenChatAgent extends EventTarget { } getBio(userId?: string): Promise { - if (!navigator.onLine) return Promise.resolve(""); + if (offline()) return Promise.resolve(""); const userClient = userId ? UserClient.create(userId, this.identity, this.config, this.db) @@ -2115,7 +2116,7 @@ export class OpenChatAgent extends EventTarget { } setBio(bio: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.setBio(bio); } @@ -2125,7 +2126,7 @@ export class OpenChatAgent extends EventTarget { displayName: string | undefined, referralCode: string | undefined, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); const localUserIndex = await this._userIndexClient.userRegistrationCanister(); return this.createLocalUserIndexClient(localUserIndex).registerUser( @@ -2184,7 +2185,7 @@ export class OpenChatAgent extends EventTarget { } pinMessage(chatId: MultiUserChatIdentifier, messageIndex: number): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -2198,7 +2199,7 @@ export class OpenChatAgent extends EventTarget { chatId: MultiUserChatIdentifier, messageIndex: number, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -2215,7 +2216,7 @@ export class OpenChatAgent extends EventTarget { voteType: "register" | "delete", threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -2239,13 +2240,13 @@ export class OpenChatAgent extends EventTarget { withdrawCryptocurrency( domain: PendingCryptocurrencyWithdrawal, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.userClient.withdrawCryptocurrency(domain); } getInviteCode(id: GroupChatIdentifier | CommunityIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (id.kind) { case "community": @@ -2258,7 +2259,7 @@ export class OpenChatAgent extends EventTarget { enableInviteCode( id: GroupChatIdentifier | CommunityIdentifier, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (id.kind) { case "community": @@ -2271,7 +2272,7 @@ export class OpenChatAgent extends EventTarget { disableInviteCode( id: GroupChatIdentifier | CommunityIdentifier, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (id.kind) { case "community": @@ -2284,7 +2285,7 @@ export class OpenChatAgent extends EventTarget { resetInviteCode( id: GroupChatIdentifier | CommunityIdentifier, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (id.kind) { case "community": @@ -2295,25 +2296,25 @@ export class OpenChatAgent extends EventTarget { } pinChat(chatId: ChatIdentifier, favourite: boolean): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.pinChat(chatId, favourite); } unpinChat(chatId: ChatIdentifier, favourite: boolean): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.unpinChat(chatId, favourite); } archiveChat(chatId: ChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.archiveChat(chatId); } unarchiveChat(chatId: ChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.unarchiveChat(chatId); } @@ -2323,7 +2324,7 @@ export class OpenChatAgent extends EventTarget { messageIndex: number, adopt: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -2345,7 +2346,7 @@ export class OpenChatAgent extends EventTarget { } migrateUserPrincipal(userId: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); const userClient = UserClient.create(userId, this.identity, this.config, this.db); return userClient.migrateUserPrincipal(); @@ -2509,43 +2510,43 @@ export class OpenChatAgent extends EventTarget { chatId: GroupChatIdentifier, reason: string | undefined, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.freezeGroup(chatId.groupId, reason); } unfreezeGroup(chatId: GroupChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.unfreezeGroup(chatId.groupId); } deleteFrozenGroup(chatId: GroupChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.deleteFrozenGroup(chatId.groupId); } addHotGroupExclusion(chatId: GroupChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.addHotGroupExclusion(chatId.groupId); } removeHotGroupExclusion(chatId: GroupChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.removeHotGroupExclusion(chatId.groupId); } suspendUser(userId: string, reason: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._userIndexClient.suspendUser(userId, reason); } unsuspendUser(userId: string): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._userIndexClient.unsuspendUser(userId); } @@ -2563,7 +2564,7 @@ export class OpenChatAgent extends EventTarget { } claimPrize(chatId: MultiUserChatIdentifier, messageId: bigint): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); switch (chatId.kind) { case "group_chat": @@ -2583,7 +2584,7 @@ export class OpenChatAgent extends EventTarget { recurring: boolean, expectedPriceE8s: bigint, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this._userIndexClient.payForDiamondMembership( userId, @@ -2598,25 +2599,25 @@ export class OpenChatAgent extends EventTarget { communityId: string, flags: number, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.setCommunityModerationFlags(communityId, flags); } setGroupUpgradeConcurrency(value: number): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.setGroupUpgradeConcurrency(value); } setCommunityUpgradeConcurrency(value: number): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._groupIndexClient.setCommunityUpgradeConcurrency(value); } setUserUpgradeConcurrency(value: number): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._userIndexClient.setUserUpgradeConcurrency(value); } @@ -2625,7 +2626,7 @@ export class OpenChatAgent extends EventTarget { governanceCanisterId: string, stake: bigint, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this._proposalsBotClient.stakeNeuronForSubmittingProposals( governanceCanisterId, @@ -2636,7 +2637,7 @@ export class OpenChatAgent extends EventTarget { updateMarketMakerConfig( config: UpdateMarketMakerConfigArgs, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this._marketMakerClient.updateConfig(config); } @@ -2648,7 +2649,7 @@ export class OpenChatAgent extends EventTarget { notes?: string, threadRootMessageIndex?: number, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.userClient.setMessageReminder( chatId, @@ -2660,7 +2661,7 @@ export class OpenChatAgent extends EventTarget { } cancelMessageReminder(reminderId: bigint): Promise { - if (!navigator.onLine) return Promise.resolve(false); + if (offline()) return Promise.resolve(false); return this.userClient.cancelMessageReminder(reminderId); } @@ -2670,7 +2671,7 @@ export class OpenChatAgent extends EventTarget { } declineInvitation(chatId: MultiUserChatIdentifier): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); switch (chatId.kind) { case "group_chat": @@ -2685,7 +2686,7 @@ export class OpenChatAgent extends EventTarget { historyVisible: boolean, rules: Rules, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.getGroupClient(chatId.groupId).convertToCommunity(historyVisible, rules); } @@ -2717,7 +2718,7 @@ export class OpenChatAgent extends EventTarget { } setCommunityIndexes(communityIndexes: Record): Promise { - if (!navigator.onLine) return Promise.resolve(false); + if (offline()) return Promise.resolve(false); return this.userClient.setCommunityIndexes(communityIndexes); } @@ -2727,7 +2728,7 @@ export class OpenChatAgent extends EventTarget { name: string, userIds: string[], ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.communityClient(communityId).createUserGroup(name, userIds); } @@ -2739,7 +2740,7 @@ export class OpenChatAgent extends EventTarget { usersToAdd: string[], usersToRemove: string[], ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.communityClient(communityId).updateUserGroup( userGroupId, @@ -2753,7 +2754,7 @@ export class OpenChatAgent extends EventTarget { communityId: string, display_name: string | undefined, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); return this.communityClient(communityId).setMemberDisplayName(display_name); } @@ -2762,7 +2763,7 @@ export class OpenChatAgent extends EventTarget { communityId: string, userGroupIds: number[], ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.communityClient(communityId).deleteUserGroups(userGroupIds); } @@ -2780,7 +2781,7 @@ export class OpenChatAgent extends EventTarget { threadRootMessageIndex: number, follow: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve("offline"); + if (offline()) return Promise.resolve("offline"); if (chatId.kind === "channel") { return this.communityClient(chatId.communityId).followThread( @@ -2803,7 +2804,7 @@ export class OpenChatAgent extends EventTarget { proposalRejectionFee: bigint, transactionFee: bigint, ): Promise { - if (!navigator.onLine) return Promise.resolve(CommonResponses.offline()); + if (offline()) return Promise.resolve(CommonResponses.offline()); return this.userClient.submitProposal( governanceCanisterId, @@ -2821,7 +2822,7 @@ export class OpenChatAgent extends EventTarget { messageId: bigint, deleteMessage: boolean, ): Promise { - if (!navigator.onLine) return Promise.resolve(false); + if (offline()) return Promise.resolve(false); if (chatId.kind === "channel") { return this.communityClient(chatId.communityId).reportMessage( diff --git a/frontend/openchat-agent/src/services/user/user.client.ts b/frontend/openchat-agent/src/services/user/user.client.ts index ee853c41bf..bd8eb05db6 100644 --- a/frontend/openchat-agent/src/services/user/user.client.ts +++ b/frontend/openchat-agent/src/services/user/user.client.ts @@ -70,6 +70,9 @@ import type { ExchangeTokenSwapArgs, SwapTokensResponse, TokenSwapStatusResponse, + ApproveTransferResponse, + MessageContext, + PendingCryptocurrencyTransfer, } from "openchat-shared"; import { CandidService } from "../candidService"; import { @@ -109,7 +112,6 @@ import { tokenSwapStatusResponse, approveTransferResponse, } from "./mappers"; -import { MAX_EVENTS, MAX_MESSAGES, MAX_MISSING } from "../../constants"; import { type Database, getCachedEvents, @@ -142,8 +144,7 @@ import { muteNotificationsResponse } from "../notifications/mappers"; import { identity, toVoid } from "../../utils/mapping"; import { generateUint64 } from "../../utils/rng"; import type { AgentConfig } from "../../config"; -import type { ApproveTransferResponse, MessageContext } from "openchat-shared"; -import type { PendingCryptocurrencyTransfer } from "openchat-shared"; +import { MAX_EVENTS, MAX_MESSAGES, MAX_MISSING } from "openchat-shared"; export class UserClient extends CandidService { private userService: UserService; @@ -1213,10 +1214,10 @@ export class UserClient extends CandidService { } approveTransfer( - spender: string, - ledger: string, - amount: bigint, - expiresIn: bigint | undefined + spender: string, + ledger: string, + amount: bigint, + expiresIn: bigint | undefined, ): Promise { return this.handleResponse( this.userService.approve_transfer({ @@ -1230,5 +1231,5 @@ export class UserClient extends CandidService { }), approveTransferResponse, ); - } + } } diff --git a/frontend/openchat-agent/src/utils/caching.ts b/frontend/openchat-agent/src/utils/caching.ts index d29a6ad0d3..850e0d924d 100644 --- a/frontend/openchat-agent/src/utils/caching.ts +++ b/frontend/openchat-agent/src/utils/caching.ts @@ -1,4 +1,3 @@ -import { MAX_EVENTS, MAX_MESSAGES } from "../constants"; import { openDB, type DBSchema, @@ -36,6 +35,8 @@ import { chatIdentifierToString, ChatMap, MessageContextMap, + MAX_EVENTS, + MAX_MESSAGES, } from "openchat-shared"; import type { Principal } from "@dfinity/principal"; diff --git a/frontend/openchat-agent/src/utils/chat.ts b/frontend/openchat-agent/src/utils/chat.ts index a6260dcdab..64a873b310 100644 --- a/frontend/openchat-agent/src/utils/chat.ts +++ b/frontend/openchat-agent/src/utils/chat.ts @@ -23,12 +23,18 @@ import type { ChannelIdentifier, UserGroupDetails, } from "openchat-shared"; -import { ChatMap, applyOptionUpdate, bigIntMax, mapOptionUpdate } from "openchat-shared"; +import { + ChatMap, + applyOptionUpdate, + bigIntMax, + mapOptionUpdate, + OPENCHAT_BOT_AVATAR_URL, + OPENCHAT_BOT_USER_ID, +} from "openchat-shared"; import { toRecord } from "./list"; import { identity } from "./mapping"; import Identicon from "identicon.js"; import md5 from "md5"; -import { OPENCHAT_BOT_AVATAR_URL, OPENCHAT_BOT_USER_ID } from "../constants"; // this is used to merge both the overall list of chats with updates and also the list of participants // within a group chat @@ -401,8 +407,8 @@ export function buildUserAvatarUrl(pattern: string, userId: string, avatarId?: b return avatarId !== undefined ? buildBlobUrl(pattern, userId, avatarId, "avatar") : userId === OPENCHAT_BOT_USER_ID - ? OPENCHAT_BOT_AVATAR_URL - : buildIdenticonUrl(userId); + ? OPENCHAT_BOT_AVATAR_URL + : buildIdenticonUrl(userId); } function buildIdenticonUrl(userId: string): string { diff --git a/frontend/openchat-client/src/liveState.ts b/frontend/openchat-client/src/liveState.ts index 12fb86e478..86060c0924 100644 --- a/frontend/openchat-client/src/liveState.ts +++ b/frontend/openchat-client/src/liveState.ts @@ -66,7 +66,7 @@ import { import { type GlobalState, chatListScopeStore, globalStateStore } from "./stores/global"; import type { DraftMessage, DraftMessagesByThread } from "./stores/draftMessageFactory"; import { draftThreadMessages } from "./stores/draftThreadMessages"; -import { networkStatus, type NetworkStatus } from "./stores/network"; +import { offlineStore } from "./stores/network"; /** * Any stores that we reference inside the OpenChat client can be added here so that we always have the up to date current value @@ -119,10 +119,10 @@ export class LiveState { anonUser!: boolean; suspendedUser!: boolean; platformModerator!: boolean; - networkStatus!: NetworkStatus; + offlineStore!: boolean; constructor() { - networkStatus.subscribe((status) => (this.networkStatus = status)); + offlineStore.subscribe((offline) => (this.offlineStore = offline)); currentUser.subscribe((user) => (this.user = user)); anonUser.subscribe((anon) => (this.anonUser = anon)); suspendedUser.subscribe((suspended) => (this.suspendedUser = suspended)); diff --git a/frontend/openchat-client/src/openchat.ts b/frontend/openchat-client/src/openchat.ts index c7f92c3688..7e9457f8db 100644 --- a/frontend/openchat-client/src/openchat.ts +++ b/frontend/openchat-client/src/openchat.ts @@ -346,6 +346,9 @@ import type { DexId, SwapTokensResponse, TokenSwapStatusResponse, + Member, + Level, + VersionedRules, } from "openchat-shared"; import { AuthProvider, @@ -379,6 +382,7 @@ import { contentTypeToPermission, anonymousUser, ANON_USER_ID, + isPaymentGate, } from "openchat-shared"; import { failedMessagesStore } from "./stores/failedMessages"; import { @@ -420,13 +424,9 @@ import { localCommunitySummaryUpdates } from "./stores/localCommunitySummaryUpda import { hasFlag, moderationFlags } from "./stores/flagStore"; import { hasOwnerRights } from "./utils/permissions"; import { isDisplayNameValid, isUsernameValid } from "./utils/validation"; -import type { Member } from "openchat-shared"; -import type { Level } from "openchat-shared"; import type { DraftMessage } from "./stores/draftMessageFactory"; -import type { VersionedRules } from "openchat-shared"; import { verifyCredential } from "./utils/credentials"; -import { networkStatus } from "./stores/network"; -import { isPaymentGate } from "openchat-shared"; +import { offlineStore } from "./stores/network"; const UPGRADE_POLL_INTERVAL = 1000; const MARK_ONLINE_INTERVAL = 61 * 1000; @@ -1129,33 +1129,35 @@ export class OpenChat extends OpenChatAgentWorker { } async approveAccessGatePayment(group: MultiUserChat | CommunitySummary): Promise { - // If there is no payment gate then do nothing + // If there is no payment gate then do nothing if (!isPaymentGate(group.gate)) { // If this is a channel there might still be a payment gate on the community if (group.kind === "channel") { - return this.approveAccessGatePayment(this._liveState.communities.get({ - kind: "community", - communityId: group.id.communityId - })!); + return this.approveAccessGatePayment( + this._liveState.communities.get({ + kind: "community", + communityId: group.id.communityId, + })!, + ); } else { return true; } } - // If there is a payment gateway then first call the user's canister to get an - // approval for the group/community to transfer the payment + // If there is a payment gateway then first call the user's canister to get an + // approval for the group/community to transfer the payment const spender = group.kind === "group_chat" ? group.id.groupId : group.id.communityId; - + const token = this.getTokenDetailsForAccessGate(group.gate); if (token === undefined) { return false; } - const response = await this.sendRequest({ - kind: "approveTransfer", - spender, - ledger: group.gate.ledgerCanister, + const response = await this.sendRequest({ + kind: "approveTransfer", + spender, + ledger: group.gate.ledgerCanister, amount: group.gate.amount - token.transferFee, // The user should pay only the amount not amount+fee so it is a round number expiresIn: BigInt(5 * 60 * 1000), // Allow 5 mins for the join_group call before the approval expires }); @@ -1172,7 +1174,7 @@ export class OpenChat extends OpenChatAgentWorker { chat: MultiUserChat, credential?: string, ): Promise<"success" | "blocked" | "failure" | "gate_check_failed"> { - if (!await this.approveAccessGatePayment(chat)) { + if (!(await this.approveAccessGatePayment(chat))) { return "gate_check_failed"; } @@ -2391,7 +2393,7 @@ export class OpenChat extends OpenChatAgentWorker { getPaymentAmount(gate: AccessGate): bigint | undefined { return isPaymentGate(gate) ? gate.amount : undefined; } - + getMinStakeInTokens(gate: AccessGate): number | undefined { if (isNeuronGate(gate)) { return gate.minStakeE8s ? gate.minStakeE8s / E8S_PER_TOKEN : undefined; @@ -5384,7 +5386,7 @@ export class OpenChat extends OpenChatAgentWorker { community: CommunitySummary, credential?: string, ): Promise<"success" | "failure" | "gate_check_failed"> { - if (!await this.approveAccessGatePayment(community)) { + if (!(await this.approveAccessGatePayment(community))) { return "gate_check_failed"; } @@ -5770,7 +5772,7 @@ export class OpenChat extends OpenChatAgentWorker { selectedThreadRootMessageIndex = selectedThreadRootMessageIndex; selectedMessageContext = selectedMessageContext; userGroupSummaries = userGroupSummaries; - networkStatus = networkStatus; + offlineStore = offlineStore; // current community stores chatListScope = chatListScopeStore; diff --git a/frontend/openchat-client/src/stores/markRead.ts b/frontend/openchat-client/src/stores/markRead.ts index 8a5929b9b4..16dd0de799 100644 --- a/frontend/openchat-client/src/stores/markRead.ts +++ b/frontend/openchat-client/src/stores/markRead.ts @@ -15,7 +15,7 @@ import { } from "openchat-shared"; import { unconfirmed } from "./unconfirmed"; import type { OpenChat } from "../openchat"; -import { networkStatus } from "./network"; +import { offlineStore } from "./network"; const MARK_READ_INTERVAL = 10 * 1000; @@ -417,8 +417,8 @@ export function startMessagesReadTracker(api: OpenChat): void { if (networkUnsub !== undefined) { networkUnsub(); } - networkUnsub = networkStatus.subscribe((status) => { - if (status === "offline") { + networkUnsub = offlineStore.subscribe((offline) => { + if (offline) { messagesRead.stop(); } else { messagesRead.start(api); diff --git a/frontend/openchat-client/src/stores/network.ts b/frontend/openchat-client/src/stores/network.ts index 965cd4c4ed..c5c399fbed 100644 --- a/frontend/openchat-client/src/stores/network.ts +++ b/frontend/openchat-client/src/stores/network.ts @@ -1,21 +1,34 @@ -import { writable } from "svelte/store"; +import { derived, writable } from "svelte/store"; +import { MIN_DOWNLINK } from "openchat-shared"; -export type NetworkStatus = "offline" | "online"; - -export const networkStatus = writable( - navigator.onLine ? "online" : "offline", - (set) => { - function online() { - set("online"); - } - function offline() { - set("offline"); - } - window.addEventListener("online", online); - window.addEventListener("offline", offline); +const networkInformation = writable(undefined, (set) => { + if ("connection" in navigator) { + const update = () => set(navigator.connection); + navigator.connection?.addEventListener("change", update); + update(); return () => { - window.removeEventListener("online", online); - window.removeEventListener("offline", offline); + navigator.connection?.removeEventListener("change", update); }; + } +}); + +const networkOffline = writable(!navigator.onLine, (set) => { + const online = () => set(false); + const offline = () => set(true); + window.addEventListener("online", online); + window.addEventListener("offline", offline); + return () => { + window.removeEventListener("online", online); + window.removeEventListener("offline", offline); + }; +}); + +export const offlineStore = derived( + [networkInformation, networkOffline], + ([$networkInformation, $networkOffline]) => { + return ( + $networkOffline || + ($networkInformation !== undefined && $networkInformation.downlink < MIN_DOWNLINK) + ); }, ); diff --git a/frontend/openchat-client/src/types.d.ts b/frontend/openchat-client/src/types.d.ts new file mode 100644 index 0000000000..4d88be43a1 --- /dev/null +++ b/frontend/openchat-client/src/types.d.ts @@ -0,0 +1,14 @@ +type NetworkType = "slow-2g" | "2g" | "3g" | "4g"; + +interface NetworkInformation extends Events { + readonly downlink: number; + readonly effectiveType: NetworkType; + readonly rtt: number; + readonly saveData: boolean; + addEventListener: (ev: string, cb: () => void) => void; + removeEventListener: (ev: string, cb: () => void) => void; +} + +interface Navigator { + connection?: NetworkInformation; +} diff --git a/frontend/openchat-client/src/utils/poller.ts b/frontend/openchat-client/src/utils/poller.ts index 08ddddb228..7ff9b6faa1 100644 --- a/frontend/openchat-client/src/utils/poller.ts +++ b/frontend/openchat-client/src/utils/poller.ts @@ -1,6 +1,6 @@ import { derived, type Unsubscriber } from "svelte/store"; import { background } from "../stores/background"; -import { networkStatus } from "../stores/network"; +import { offlineStore } from "../stores/network"; type PollerEnvironment = { background: boolean; @@ -23,13 +23,10 @@ export class Poller { private idleInterval?: number, private immediate?: boolean, // whether to kick off the first iteration immediately ) { - const statusStore = derived( - [background, networkStatus], - ([$background, $networkStatus]) => ({ - background: $background, - offline: $networkStatus === "offline", - }), - ); + const statusStore = derived([background, offlineStore], ([$background, $offlineStore]) => ({ + background: $background, + offline: $offlineStore, + })); // when the poller environment changes, restart this.unsubscribeStatus = statusStore.subscribe((status) => { @@ -61,8 +58,8 @@ export class Poller { this.lastExecutionTimestamp !== undefined ? Math.max(0, this.lastExecutionTimestamp + interval - Date.now()) : this.immediate - ? 0 - : interval; + ? 0 + : interval; const runThenLoop = () => { if (this.stopped || this.runnerId !== runnerId) return; diff --git a/frontend/openchat-shared/src/constants.ts b/frontend/openchat-shared/src/constants.ts index 86fd2e7625..c46a6c9c7d 100644 --- a/frontend/openchat-shared/src/constants.ts +++ b/frontend/openchat-shared/src/constants.ts @@ -1 +1,10 @@ export const ONLINE_THRESHOLD = 120; +export const MAX_MESSAGES = 100; +export const MAX_EVENTS = 500; +export const MAX_MISSING = 30; +export const OPENCHAT_BOT_USER_ID = "zzyk3-openc-hatbo-tq7my-cai"; +export const OPENCHAT_BOT_USERNAME = "OpenChatBot"; +export const OPENCHAT_BOT_AVATAR_URL = "assets/robot.svg"; + +// downlink is the effective bandwidth estimate in megabits per second, rounded to the nearest multiple of 25 kilobits per seconds. +export const MIN_DOWNLINK = 0.05; diff --git a/frontend/openchat-shared/src/index.ts b/frontend/openchat-shared/src/index.ts index 987d906f6c..4bb27d7397 100644 --- a/frontend/openchat-shared/src/index.ts +++ b/frontend/openchat-shared/src/index.ts @@ -1,2 +1,3 @@ export * from "./domain"; -export * from "./utils"; \ No newline at end of file +export * from "./utils"; +export * from "./constants"; diff --git a/frontend/openchat-shared/src/types.d.ts b/frontend/openchat-shared/src/types.d.ts new file mode 100644 index 0000000000..769ac95629 --- /dev/null +++ b/frontend/openchat-shared/src/types.d.ts @@ -0,0 +1,12 @@ +type NetworkType = "slow-2g" | "2g" | "3g" | "4g"; + +interface NetworkInformation extends Events { + readonly downlink: number; + readonly effectiveType: NetworkType; + readonly rtt: number; + readonly saveData: boolean; +} + +interface Navigator { + connection?: NetworkInformation; +} diff --git a/frontend/openchat-shared/src/utils/index.ts b/frontend/openchat-shared/src/utils/index.ts index 4443693035..e9f777ecf4 100644 --- a/frontend/openchat-shared/src/utils/index.ts +++ b/frontend/openchat-shared/src/utils/index.ts @@ -9,3 +9,4 @@ export * from "./notifications"; export * from "./set"; export * from "./string"; export * from "./promise"; +export * from "./network"; diff --git a/frontend/openchat-shared/src/utils/logging.ts b/frontend/openchat-shared/src/utils/logging.ts index aa2702d91a..1b07026a39 100644 --- a/frontend/openchat-shared/src/utils/logging.ts +++ b/frontend/openchat-shared/src/utils/logging.ts @@ -5,6 +5,7 @@ export type Logger = { }; import Rollbar from "rollbar"; +import { offline } from "./network"; let rollbar: Rollbar | undefined; @@ -33,7 +34,7 @@ export function inititaliseLogger(apikey: string, version: string, env: string): return { error(message?: unknown, ...optionalParams: unknown[]): void { console.error(message as string, optionalParams); - if (navigator.onLine) { + if (!offline()) { rollbar?.error(message as string, optionalParams); } }, diff --git a/frontend/openchat-shared/src/utils/network.ts b/frontend/openchat-shared/src/utils/network.ts new file mode 100644 index 0000000000..df386a9bc9 --- /dev/null +++ b/frontend/openchat-shared/src/utils/network.ts @@ -0,0 +1,13 @@ +import { MIN_DOWNLINK } from "../constants"; + +export function offline(): boolean { + return !navigator.onLine || criticalBandwith(); +} + +function criticalBandwith(): boolean { + return ( + "connection" in navigator && + navigator.connection !== undefined && + navigator.connection.downlink < MIN_DOWNLINK + ); +}