@@ -209,6 +211,7 @@ export function DebugMenuModal() {
allowOutsideClick={false}
>
{
);
};
+export const DebugUrlInteractionsSection = () => {
+ const [urlInteractions, setUrlInteractions] = useState(getUrlInteractions());
+
+ const refresh = useCallback(() => setUrlInteractions(getUrlInteractions()), []);
+ const removeUrl = useCallback(async (url: string) => removeUrlInteractionHistory(url), []);
+ const clearAll = useCallback(async () => {
+ await clearAllUrlInteractions();
+ refresh();
+ }, [refresh]);
+
+ return (
+
+
+ Clear All
+
+ Refresh
+
+
+ | URL |
+ Interactions |
+ Last Updated |
+
+ {urlInteractions.map(({ url, interactions, lastUpdated }) => {
+ const updatedStr = formatRoundedUpTimeUntilTimestamp(lastUpdated);
+ return (
+
+ | {url} | {interactions.map(urlInteractionToString).join(', ')} | {' '}
+ {updatedStr} | {' '}
+
+ removeUrl(url)}
+ >
+
+
+ |
+
+ );
+ })}
+
+
+ );
+};
+
export const ExperimentalActions = ({ forceUpdate }: { forceUpdate: () => void }) => {
const dispatch = useDispatch();
// const refreshedAt = useReleasedFeaturesRefreshedAt();
diff --git a/ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx b/ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx
new file mode 100644
index 0000000000..fbf9c14af3
--- /dev/null
+++ b/ts/components/dialog/debug/playgrounds/CTAPlaygroundPage.tsx
@@ -0,0 +1,69 @@
+import useUpdate from 'react-use/lib/useUpdate';
+import { ProDebugSection } from '../FeatureFlags';
+import { SpacerLG } from '../../../basic/Text';
+import { useShowSessionCTACbWithVariant } from '../../SessionCTA';
+import { Flex } from '../../../basic/Flex';
+import { LucideIcon } from '../../../icon/LucideIcon';
+import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide';
+import { DebugButton } from '../components';
+import { DebugMenuPageProps, DebugMenuSection } from '../DebugMenuModal';
+import { CTAVariant } from '../../cta/types';
+
+export function CTAPlaygroundPage(props: DebugMenuPageProps) {
+ const forceUpdate = useUpdate();
+ const handleClick = useShowSessionCTACbWithVariant();
+
+ return (
+ <>
+
+ Call to Actions (CTAs)
+
+ {' '}
+ {'Pro CTAs only work if pro is available, toggle it above!'}
+
+
+
+ Feature CTAs
+ handleClick(CTAVariant.PRO_GENERIC)}>Generic
+ handleClick(CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT)}>
+ Character Limit
+
+ handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT)}>
+ Pinned Conversations
+
+ handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT_GRANDFATHERED)}
+ >
+ Pinned Conversations (Grandfathered)
+
+ handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE)}>
+ Animated Profile Picture
+
+ handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE_ACTIVATED)}>
+ Animated Profile Picture (Has pro)
+
+
+ Pro Group CTAs WIP
+
+ handleClick(CTAVariant.PRO_GROUP_ACTIVATED)}>
+ Group Activated
+
+ handleClick(CTAVariant.PRO_GROUP_NON_ADMIN)}>
+ Group (Non-Admin)
+
+ handleClick(CTAVariant.PRO_GROUP_ADMIN)}>
+ Group (Admin)
+
+ Special CTAs
+ handleClick(CTAVariant.PRO_EXPIRING_SOON)}>
+ Expiring Soon
+
+ handleClick(CTAVariant.PRO_EXPIRED)}>Expired
+
+ >
+ );
+}
diff --git a/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx b/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
index 914908ea2c..fa78d7eb12 100644
--- a/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
+++ b/ts/components/dialog/debug/playgrounds/ProPlaygroundPage.tsx
@@ -1,16 +1,17 @@
import useUpdate from 'react-use/lib/useUpdate';
import { ProDebugSection } from '../FeatureFlags';
import { SpacerLG } from '../../../basic/Text';
-import { ProCTAVariant, useShowSessionProInfoDialogCbWithVariant } from '../../SessionProInfoModal';
+import { useShowSessionCTACbWithVariant } from '../../SessionCTA';
import { Flex } from '../../../basic/Flex';
import { LucideIcon } from '../../../icon/LucideIcon';
import { LUCIDE_ICONS_UNICODE } from '../../../icon/lucide';
import { DebugButton } from '../components';
import { DebugMenuPageProps, DebugMenuSection } from '../DebugMenuModal';
+import { CTAVariant } from '../../cta/types';
export function ProPlaygroundPage(props: DebugMenuPageProps) {
const forceUpdate = useUpdate();
- const handleClick = useShowSessionProInfoDialogCbWithVariant();
+ const handleClick = useShowSessionCTACbWithVariant();
return (
<>
@@ -27,41 +28,41 @@ export function ProPlaygroundPage(props: DebugMenuPageProps) {
Feature CTAs
- handleClick(ProCTAVariant.GENERIC)}>Generic
- handleClick(ProCTAVariant.MESSAGE_CHARACTER_LIMIT)}>
+ handleClick(CTAVariant.PRO_GENERIC)}>Generic
+ handleClick(CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT)}>
Character Limit
- handleClick(ProCTAVariant.PINNED_CONVERSATION_LIMIT)}>
+ handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT)}>
Pinned Conversations
handleClick(ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED)}
+ onClick={() => handleClick(CTAVariant.PRO_PINNED_CONVERSATION_LIMIT_GRANDFATHERED)}
>
Pinned Conversations (Grandfathered)
- handleClick(ProCTAVariant.ANIMATED_DISPLAY_PICTURE)}>
+ handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE)}>
Animated Profile Picture
- handleClick(ProCTAVariant.ANIMATED_DISPLAY_PICTURE_ACTIVATED)}>
+ handleClick(CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE_ACTIVATED)}>
Animated Profile Picture (Has pro)
Pro Group CTAs WIP
- handleClick(ProCTAVariant.GROUP_ACTIVATED)}>
+ handleClick(CTAVariant.PRO_GROUP_ACTIVATED)}>
Group Activated
- handleClick(ProCTAVariant.GROUP_NON_ADMIN)}>
+ handleClick(CTAVariant.PRO_GROUP_NON_ADMIN)}>
Group (Non-Admin)
- handleClick(ProCTAVariant.GROUP_ADMIN)}>
+ handleClick(CTAVariant.PRO_GROUP_ADMIN)}>
Group (Admin)
Special CTAs
- handleClick(ProCTAVariant.EXPIRING_SOON)}>
+ handleClick(CTAVariant.PRO_EXPIRING_SOON)}>
Expiring Soon
- handleClick(ProCTAVariant.EXPIRED)}>Expired
+ handleClick(CTAVariant.PRO_EXPIRED)}>Expired
>
);
diff --git a/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx b/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx
index b058000bc0..ecbee53c51 100644
--- a/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/DefaultSettingsPage.tsx
@@ -42,7 +42,7 @@ import {
useProAccessDetails,
} from '../../../../hooks/useHasPro';
import { NetworkTime } from '../../../../util/NetworkTime';
-import { DURATION_SECONDS } from '../../../../session/constants';
+import { APP_URL, DURATION_SECONDS } from '../../../../session/constants';
import { getFeatureFlag } from '../../../../state/ducks/types/releasedFeaturesReduxTypes';
import { useUserSettingsCloseAction } from './userSettingsHooks';
@@ -146,7 +146,7 @@ function MiscSection() {
}
text={{ token: 'donate' }}
onClick={() => {
- showLinkVisitWarningDialog('https://session.foundation/donate#app', dispatch);
+ showLinkVisitWarningDialog(APP_URL.DONATE, dispatch);
}}
dataTestId="donate-settings-menu-item"
/>
diff --git a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
index cf1862c326..ed537586a5 100644
--- a/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
+++ b/ts/components/dialog/user-settings/pages/user-pro/ProNonOriginatingPage.tsx
@@ -16,7 +16,7 @@ import { LucideIcon } from '../../../../icon/LucideIcon';
import { LUCIDE_ICONS_UNICODE, WithLucideUnicode } from '../../../../icon/lucide';
import { SessionButton, SessionButtonColor } from '../../../../basic/SessionButton';
import { showLinkVisitWarningDialog } from '../../../OpenUrlModal';
-import { proButtonProps } from '../../../SessionProInfoModal';
+import { proButtonProps } from '../../../SessionCTA';
import { Flex } from '../../../../basic/Flex';
import type { ProNonOriginatingPageVariant } from '../../../../../types/ReduxTypes';
import { useCurrentNeverHadPro, useProAccessDetails } from '../../../../../hooks/useHasPro';
diff --git a/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx b/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx
index 91790a4330..884fe52ca5 100644
--- a/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx
+++ b/ts/components/dialog/user-settings/pages/user-pro/ProSettingsPage.tsx
@@ -42,7 +42,7 @@ import {
useProAccessDetails,
} from '../../../../../hooks/useHasPro';
import { SessionButton, SessionButtonColor } from '../../../../basic/SessionButton';
-import { proButtonProps } from '../../../SessionProInfoModal';
+import { proButtonProps } from '../../../SessionCTA';
import { useIsProGroupsAvailable } from '../../../../../hooks/useIsProAvailable';
import { SpacerMD } from '../../../../basic/Text';
import LIBSESSION_CONSTANTS from '../../../../../session/utils/libsession/libsession_constants';
diff --git a/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx b/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx
index 01e51ed486..faa3a6e331 100644
--- a/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx
+++ b/ts/components/dialog/user-settings/pages/userSettingsHooks.tsx
@@ -6,7 +6,7 @@ import {
type UserSettingsPage,
} from '../../../../state/ducks/modalDialog';
import { assertUnreachable } from '../../../../types/sqlSharedTypes';
-import { handleProTriggeredCTAs } from '../../SessionProInfoModal';
+import { handleTriggeredProCTAs } from '../../SessionCTA';
export function useUserSettingsTitle(page: UserSettingsModalState | undefined) {
if (!page) {
@@ -80,7 +80,7 @@ export function useUserSettingsCloseAction(props: UserSettingsModalState) {
case 'proNonOriginating':
return () => {
dispatch(userSettingsModal(null));
- void handleProTriggeredCTAs(dispatch);
+ void handleTriggeredProCTAs(dispatch);
props.afterCloseAction?.();
};
diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx
index 7eacd64fd4..edc162209b 100644
--- a/ts/components/leftpane/ActionsPanel.tsx
+++ b/ts/components/leftpane/ActionsPanel.tsx
@@ -61,6 +61,8 @@ import { useDebugMenuModal } from '../../state/selectors/modal';
import { useFeatureFlag } from '../../state/ducks/types/releasedFeaturesReduxTypes';
import { useDebugKey } from '../../hooks/useDebugKey';
import { UpdateProRevocationList } from '../../session/utils/job_runners/jobs/UpdateProRevocationListJob';
+import { proBackendDataActions } from '../../state/ducks/proBackendData';
+import { handleTriggeredProCTAs } from '../dialog/SessionCTA';
const StyledContainerAvatar = styled.div`
padding: var(--margins-lg);
@@ -114,6 +116,12 @@ const doAppStartUp = async () => {
void SnodePool.getFreshSwarmFor(UserUtils.getOurPubKeyStrFromCache()).then(() => {
// trigger any other actions that need to be done after the swarm is ready
window.inboxStore?.dispatch(networkDataActions.fetchInfoFromSeshServer() as any);
+ window.inboxStore?.dispatch(
+ proBackendDataActions.refreshGetProDetailsFromProBackend({}) as any
+ );
+ if (window.inboxStore) {
+ void handleTriggeredProCTAs(window.inboxStore.dispatch);
+ }
}); // refresh our swarm on start to speed up the first message fetching event
void Data.cleanupOrphanedAttachments();
diff --git a/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx b/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx
index 37ee0a40ba..37975b30ab 100644
--- a/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx
+++ b/ts/components/menuAndSettingsHooks/UseTogglePinConversationHandler.tsx
@@ -8,14 +8,12 @@ import {
useIsPrivate,
useIsPrivateAndFriend,
} from '../../hooks/useParamSelector';
-import {
- ProCTAVariant,
- useShowSessionProInfoDialogCbWithVariant,
-} from '../dialog/SessionProInfoModal';
+import { useShowSessionCTACbWithVariant } from '../dialog/SessionCTA';
import { Constants } from '../../session';
import { getPinnedConversationsCount } from '../../state/selectors/conversations';
import { useIsMessageRequestOverlayShown } from '../../state/selectors/section';
import { useCurrentUserHasPro } from '../../hooks/useHasPro';
+import { CTAVariant } from '../dialog/cta/types';
function useShowPinUnpin(conversationId: string) {
const isPrivateAndFriend = useIsPrivateAndFriend(conversationId);
@@ -46,7 +44,7 @@ export function useTogglePinConversationHandler(id: string) {
const isProAvailable = useIsProAvailable();
const hasPro = useCurrentUserHasPro();
- const handleShowProDialog = useShowSessionProInfoDialogCbWithVariant();
+ const handleShowProDialog = useShowSessionCTACbWithVariant();
const showPinUnpin = useShowPinUnpin(id);
@@ -66,7 +64,7 @@ export function useTogglePinConversationHandler(id: string) {
return () =>
handleShowProDialog(
pinnedConversationsCount > Constants.CONVERSATION.MAX_PINNED_CONVERSATIONS_STANDARD
- ? ProCTAVariant.PINNED_CONVERSATION_LIMIT_GRANDFATHERED
- : ProCTAVariant.PINNED_CONVERSATION_LIMIT
+ ? CTAVariant.PRO_PINNED_CONVERSATION_LIMIT_GRANDFATHERED
+ : CTAVariant.PRO_PINNED_CONVERSATION_LIMIT
);
}
diff --git a/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx b/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx
index b89fc95ebb..39457beefd 100644
--- a/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx
+++ b/ts/components/menuAndSettingsHooks/useProBadgeOnClickCb.tsx
@@ -1,13 +1,11 @@
import { useDispatch } from 'react-redux';
import { useIsProAvailable } from '../../hooks/useIsProAvailable';
import { ProMessageFeature } from '../../models/proMessageFeature';
-import { SessionProInfoState, updateSessionProInfoModal } from '../../state/ducks/modalDialog';
+import { SessionCTAState, updateSessionCTA } from '../../state/ducks/modalDialog';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import type { ContactNameContext } from '../conversation/ContactName/ContactNameContext';
-import {
- ProCTAVariant,
- useShowSessionProInfoDialogCbWithVariant,
-} from '../dialog/SessionProInfoModal';
+import { useShowSessionCTACbWithVariant } from '../dialog/SessionCTA';
+import { CTAVariant } from '../dialog/cta/types';
type WithUserHasPro = { userHasPro: boolean };
type WithMessageSentWithProFeat = { messageSentWithProFeat: Array | null };
@@ -18,7 +16,7 @@ type WithContactNameContext = { contactNameContext: ContactNameContext };
type WithIsGroupV2 = { isGroupV2: boolean };
type WithIsBlinded = { isBlinded: boolean };
type WithProvidedCb = { providedCb: (() => void) | null };
-type WithProCTA = { cta: SessionProInfoState };
+type WithProCTA = { cta: SessionCTAState };
type ProBadgeContext =
| { context: 'edit-profile-pic'; args: WithProCTA }
@@ -99,14 +97,14 @@ function isContactNameNoShowContext(context: ContactNameContext) {
}
}
-function proFeatureToVariant(proFeature: ProMessageFeature): ProCTAVariant {
+function proFeatureToVariant(proFeature: ProMessageFeature): CTAVariant {
switch (proFeature) {
case ProMessageFeature.PRO_INCREASED_MESSAGE_LENGTH:
- return ProCTAVariant.MESSAGE_CHARACTER_LIMIT;
+ return CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT;
case ProMessageFeature.PRO_ANIMATED_DISPLAY_PICTURE:
- return ProCTAVariant.ANIMATED_DISPLAY_PICTURE;
+ return CTAVariant.PRO_ANIMATED_DISPLAY_PICTURE;
case ProMessageFeature.PRO_BADGE:
- return ProCTAVariant.GENERIC;
+ return CTAVariant.PRO_GENERIC;
default:
assertUnreachable(proFeature, 'ProFeatureToVariant: unknown case');
throw new Error('unreachable');
@@ -124,7 +122,7 @@ export function useProBadgeOnClickCb(
opts: ProBadgeContext
): ShowTagWithCb | ShowTagNoCb | DoNotShowTag {
const dispatch = useDispatch();
- const handleShowProInfoModal = useShowSessionProInfoDialogCbWithVariant();
+ const handleShowProInfoModal = useShowSessionCTACbWithVariant();
const isProAvailable = useIsProAvailable();
if (!isProAvailable) {
@@ -137,7 +135,7 @@ export function useProBadgeOnClickCb(
if (context === 'edit-profile-pic') {
return {
show: true,
- cb: () => dispatch(updateSessionProInfoModal(args.cta)),
+ cb: () => dispatch(updateSessionCTA(args.cta)),
};
}
@@ -171,7 +169,7 @@ export function useProBadgeOnClickCb(
cb: () => {
handleShowProInfoModal(
multiProFeatUsed
- ? ProCTAVariant.GENERIC
+ ? CTAVariant.PRO_GENERIC
: proFeatureToVariant(messageSentWithProFeat[0])
);
},
@@ -202,7 +200,7 @@ export function useProBadgeOnClickCb(
// if this is a groupv2, the badge should open the "groupv2 activated" modal onclick
return {
show: true,
- cb: () => handleShowProInfoModal(ProCTAVariant.GROUP_ACTIVATED),
+ cb: () => handleShowProInfoModal(CTAVariant.PRO_GROUP_ACTIVATED),
};
}
@@ -211,7 +209,7 @@ export function useProBadgeOnClickCb(
return showNoCb;
}
// FOMO: user shown has pro but we don't: show CTA on click
- return { show: true, cb: () => handleShowProInfoModal(ProCTAVariant.GENERIC) };
+ return { show: true, cb: () => handleShowProInfoModal(CTAVariant.PRO_GENERIC) };
}
if (context === 'character-count') {
@@ -222,7 +220,7 @@ export function useProBadgeOnClickCb(
// FOMO
return {
show: true,
- cb: () => handleShowProInfoModal(ProCTAVariant.MESSAGE_CHARACTER_LIMIT),
+ cb: () => handleShowProInfoModal(CTAVariant.PRO_MESSAGE_CHARACTER_LIMIT),
};
}
@@ -255,7 +253,7 @@ export function useProBadgeOnClickCb(
return showNoCb;
}
- return { show: true, cb: () => handleShowProInfoModal(ProCTAVariant.GENERIC) };
+ return { show: true, cb: () => handleShowProInfoModal(CTAVariant.PRO_GENERIC) };
}
return showNoCb;
}
diff --git a/ts/data/data.ts b/ts/data/data.ts
index 00fa627c18..1ba273c310 100644
--- a/ts/data/data.ts
+++ b/ts/data/data.ts
@@ -60,6 +60,15 @@ async function getPasswordHash(): Promise {
return channels.getPasswordHash();
}
+// Note: once we have this timestamp there is no reason it should change
+let cachedDBCreationTimestampMs: null | number = null;
+async function getDBCreationTimestampMs(): Promise {
+ if (!cachedDBCreationTimestampMs) {
+ cachedDBCreationTimestampMs = await channels.getDBCreationTimestampMs();
+ }
+ return cachedDBCreationTimestampMs;
+}
+
// Guard Nodes
async function getGuardNodes(): Promise> {
return channels.getGuardNodes();
@@ -781,6 +790,7 @@ export const Data = {
close,
removeDB,
getPasswordHash,
+ getDBCreationTimestampMs,
// items table logic
createOrUpdateItem,
diff --git a/ts/data/dataInit.ts b/ts/data/dataInit.ts
index 0f2ce7fb44..11c1a8811e 100644
--- a/ts/data/dataInit.ts
+++ b/ts/data/dataInit.ts
@@ -16,6 +16,7 @@ const channelsToMake = new Set([
'close',
'removeDB',
'getPasswordHash',
+ 'getDBCreationTimestampMs',
'getGuardNodes',
'updateGuardNodes',
'createOrUpdateItem',
diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts
index f265c6da97..9a6505709d 100644
--- a/ts/data/settings-key.ts
+++ b/ts/data/settings-key.ts
@@ -69,6 +69,7 @@ export const SettingsKey = {
numberId: 'number_id',
localAttachmentEncryptionKey,
spellCheckEnabled: 'spell-check',
+ urlInteractions: 'urlInteractions',
proMasterKeyHex: 'proMasterKeyHex',
proRotatingPrivateKeyHex: 'proRotatingPrivateKeyHex',
/**
diff --git a/ts/localization/localeTools.ts b/ts/localization/localeTools.ts
index 15e4269365..e9b584ecda 100644
--- a/ts/localization/localeTools.ts
+++ b/ts/localization/localeTools.ts
@@ -10,6 +10,12 @@ import {
type TokensSimpleAndArgs,
} from './locales';
+// NOTE: this forces a plural string to use the "1" variant
+export const PLURAL_COUNT_ONE = 1;
+
+// NOTE: this forces a plural string to use the "other" variant
+export const PLURAL_COUNT_OTHER = 99;
+
// Note: those two functions are actually duplicates of Errors.toString.
// We should maybe make that a module that we reuse?
function withClause(error: unknown) {
diff --git a/ts/node/fs_utility.ts b/ts/node/fs_utility.ts
new file mode 100644
index 0000000000..ef077c0b7f
--- /dev/null
+++ b/ts/node/fs_utility.ts
@@ -0,0 +1,26 @@
+import fs from 'fs';
+
+export function getFileCreationTimestampMs(filePath: string): number | null {
+ if (!filePath || typeof filePath !== 'string') {
+ console.warn(`Invalid file path provided ${filePath}`);
+ return null;
+ }
+
+ try {
+ const stats = fs.statSync(filePath);
+
+ if (
+ typeof stats.birthtimeMs !== 'number' ||
+ !Number.isFinite(stats.birthtimeMs) ||
+ stats.birthtimeMs <= 0
+ ) {
+ console.warn(`Birth time is not a valid number for file: ${filePath}`);
+ return null;
+ }
+
+ return stats.birthtimeMs;
+ } catch (error) {
+ console.error(`Failed to get creation time for file ${filePath}`, error);
+ return null;
+ }
+}
diff --git a/ts/node/sql.ts b/ts/node/sql.ts
index c2f312ad33..b989190985 100644
--- a/ts/node/sql.ts
+++ b/ts/node/sql.ts
@@ -86,6 +86,7 @@ import {
} from './sqlInstance';
import { OpenGroupV2Room } from '../data/types';
import { tr } from '../localization/localeTools';
+import { getFileCreationTimestampMs } from './fs_utility';
// eslint:disable: function-name non-literal-fs-path
@@ -271,6 +272,16 @@ function removePasswordHash() {
removeItemById(PASS_HASH_ID);
}
+function getDBCreationTimestampMs(): number | null {
+ if (!databaseFilePath) {
+ return null;
+ }
+ if (process.env.DB_CREATION_TIMESTAMP) {
+ return Number.parseInt(process.env.DB_CREATION_TIMESTAMP, 10);
+ }
+ return getFileCreationTimestampMs(databaseFilePath);
+}
+
function getIdentityKeyById(id: string, instance: BetterSqlite3.Database) {
return getById(IDENTITY_KEYS_TABLE, id, instance);
}
@@ -2533,6 +2544,7 @@ export const sqlNode = {
close,
removeDB,
setSQLPassword,
+ getDBCreationTimestampMs,
getPasswordHash,
savePasswordHash,
diff --git a/ts/session/apis/seed_node_api/SeedNodeAPI.ts b/ts/session/apis/seed_node_api/SeedNodeAPI.ts
index 9bd8c294f5..4d54b03dd4 100644
--- a/ts/session/apis/seed_node_api/SeedNodeAPI.ts
+++ b/ts/session/apis/seed_node_api/SeedNodeAPI.ts
@@ -1,7 +1,8 @@
import https from 'https';
import tls from 'tls';
-
+import { setDefaultAutoSelectFamilyAttemptTimeout } from 'net';
import _ from 'lodash';
+
// eslint-disable-next-line import/no-named-default
import { default as insecureNodeFetch } from 'node-fetch';
import pRetry from 'p-retry';
@@ -275,7 +276,10 @@ async function getSnodesFromSeedUrl(urlObj: URL): Promise> {
agent: sslAgent,
};
window?.log?.info(`insecureNodeFetch => plaintext for getSnodesFromSeedUrl ${url}`);
-
+ // Note: node has a default timeout of 250ms to pick ipv4 or ipv6 address, but sometimes it times out
+ // Increase that duration to 500ms as it seems to be resolving our issues.
+ // see https://github.com/nodejs/undici/issues/2990#issuecomment-2408883876
+ setDefaultAutoSelectFamilyAttemptTimeout(500);
const response = await insecureNodeFetch(url, fetchOptions);
if (response.status !== 200) {
diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts
index 2f31927d12..78f6bc6436 100644
--- a/ts/session/apis/snode_api/swarmPolling.ts
+++ b/ts/session/apis/snode_api/swarmPolling.ts
@@ -977,7 +977,7 @@ export class SwarmPolling {
);
}
- const allResultsFromUserProfile = await Promise.allSettled(
+ const firstResultWithMessagesUserProfile = await Promise.any(
swarmSnodes.map(async toPollFrom => {
// Note: always print something so we know if the polling is hanging
window.log.info(
@@ -996,21 +996,25 @@ export class SwarmPolling {
window.log.info(
`[onboarding] pollOnceForOurDisplayName of ${ed25519Str(pubkey.key)} from snode: ${ed25519Str(toPollFrom.pubkey_ed25519)} namespaces: ${[SnodeNamespaces.UserProfile]} returned: ${retrieved?.length}`
);
+ if (!retrieved?.length) {
+ /**
+ * Sometimes, a snode is out of sync with its swarm but still replies with what he thinks is the swarm's content.
+ * When that happens, we can get a "no display name" error, as indeed, that snode didn't have a config message on user profile.
+ * To fix this, we've added a check over all of the snodes of our swarm, and we pick the first one that reports having a config message on user profile.
+ * This won't take care of the case where a snode has a message with an empty display name, but it's not the root issue that this was added for.
+ */
+
+ throw new Error(
+ `pollOnceForOurDisplayName of ${ed25519Str(pubkey.key)} from snode: ${ed25519Str(toPollFrom.pubkey_ed25519)} no results from user profile`
+ );
+ }
return retrieved;
})
);
- const resultsFromUserProfile = flatten(
- compact(
- allResultsFromUserProfile
- .filter(promise => promise.status === 'fulfilled')
- .map(promise => promise.value)
- )
- );
-
// check if we just fetched the details from the config namespaces.
// If yes, merge them together and exclude them from the rest of the messages.
- if (!resultsFromUserProfile?.length) {
+ if (!firstResultWithMessagesUserProfile?.length) {
throw new NotFoundError('[pollOnceForOurDisplayName] resultsFromUserProfile is empty');
}
@@ -1021,7 +1025,7 @@ export class SwarmPolling {
}
const userConfigMessagesWithNamespace: Array> =
- resultsFromUserProfile.map(r => {
+ firstResultWithMessagesUserProfile.map(r => {
return (r.messages.messages || []).map(m => {
return { ...m, namespace: SnodeNamespaces.UserProfile };
});
diff --git a/ts/session/constants.ts b/ts/session/constants.ts
index 672a6bcc0f..5de3d6fa9a 100644
--- a/ts/session/constants.ts
+++ b/ts/session/constants.ts
@@ -135,3 +135,7 @@ export const PASSWORD_LENGTH = {
*/
MAX_PASSWORD_LEN: 256,
};
+
+export enum APP_URL {
+ DONATE = 'https://getsession.org/donate',
+}
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 3257f0de22..195bd0d439 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -33,7 +33,7 @@ import { sectionActions } from './section';
import { ed25519Str } from '../../session/utils/String';
import { UserUtils } from '../../session/utils';
import type { ProMessageFeature } from '../../models/proMessageFeature';
-import { handleProTriggeredCTAs } from '../../components/dialog/SessionProInfoModal';
+import { handleTriggeredProCTAs } from '../../components/dialog/SessionCTA';
export type MessageModelPropsWithoutConvoProps = {
propsForMessage: PropsForMessageWithoutConvoProps;
@@ -1143,7 +1143,7 @@ export async function openConversationWithMessages(args: {
window.inboxStore?.dispatch(sectionActions.resetRightOverlayMode());
if (window.inboxStore) {
- await handleProTriggeredCTAs(window.inboxStore.dispatch);
+ await handleTriggeredProCTAs(window.inboxStore.dispatch);
}
}
diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx
index 2dfb9815cd..24aca31f7a 100644
--- a/ts/state/ducks/modalDialog.tsx
+++ b/ts/state/ducks/modalDialog.tsx
@@ -12,9 +12,9 @@ import type {
ProNonOriginatingPageVariant,
} from '../../types/ReduxTypes';
import { WithConvoId } from '../../session/types/with';
-import type { ProCTAVariant } from '../../components/dialog/SessionProInfoModal';
import type { TrArgs } from '../../localization/localeTools';
import { SessionButtonType } from '../../components/basic/SessionButton';
+import type { CTAVariant } from '../../components/dialog/cta/types';
export type BanType = 'ban' | 'unban';
@@ -91,11 +91,11 @@ export type LocalizedPopupDialogState = {
overrideButtons?: Array;
} | null;
-export type SessionProInfoState = {
- variant: ProCTAVariant;
+export type SessionCTAState = {
+ variant: CTAVariant;
afterActionButtonCallback?: () => void;
// If the action button opens another modal, this callback is called after that next modal is closed.
- // For example: If "ProInfoModal" is opened from the "EditProfilePictureModal", and "ProInfoModal"'s
+ // For example: If "SessionCTA" is opened from the "EditProfilePictureModal", and "SessionCTA"'s
// action button opens the "ProSettingsModal", we want to re-open "EditProfilePictureModal"
// when "ProSettingsModal" closes.
actionButtonNextModalAfterCloseCallback?: () => void;
@@ -185,7 +185,7 @@ export type ModalState = {
hideRecoveryPasswordModal: HideRecoveryPasswordModalState;
openUrlModal: OpenUrlModalState;
localizedPopupDialog: LocalizedPopupDialogState;
- sessionProInfoModal: SessionProInfoState;
+ sessionProInfoModal: SessionCTAState;
lightBoxOptions: LightBoxOptions;
debugMenuModal: DebugMenuModalState;
conversationSettingsModal: ConversationSettingsModalState;
@@ -323,7 +323,7 @@ const ModalSlice = createSlice({
updateLocalizedPopupDialog(state, action: PayloadAction) {
return pushOrPopModal(state, 'localizedPopupDialog', action.payload);
},
- updateSessionProInfoModal(state, action: PayloadAction) {
+ updateSessionCTA(state, action: PayloadAction) {
return pushOrPopModal(state, 'sessionProInfoModal', action.payload);
},
updateLightBoxOptions(state, action: PayloadAction) {
@@ -373,7 +373,7 @@ export const {
updateHideRecoveryPasswordModal,
updateOpenUrlModal,
updateLocalizedPopupDialog,
- updateSessionProInfoModal,
+ updateSessionCTA,
updateLightBoxOptions,
updateDebugMenuModal,
updateConversationSettingsModal,
diff --git a/ts/state/ducks/proBackendData.ts b/ts/state/ducks/proBackendData.ts
index e59959e6bf..6e5c38de97 100644
--- a/ts/state/ducks/proBackendData.ts
+++ b/ts/state/ducks/proBackendData.ts
@@ -173,7 +173,7 @@ async function putProDetailsInStorage(details: ProDetailsResultType) {
await Storage.put(SettingsKey.proDetails, details);
}
-async function handleNewProProof(rotatingPrivKeyHex: string) {
+async function handleNewProProof(rotatingPrivKeyHex: string): Promise {
const masterPrivKeyHex = await getProMasterKeyHex();
const response = await ProBackendAPI.generateProProof({
masterPrivKeyHex,
@@ -188,13 +188,15 @@ async function handleNewProProof(rotatingPrivKeyHex: string) {
signatureHex: response.result.sig,
} satisfies ProProof;
await UserConfigWrapperActions.setProConfig({ proProof, rotatingPrivKeyHex });
- } else {
- window?.log?.error('failed to get new pro proof: ', response);
+ return proProof;
}
+ window?.log?.error('failed to get new pro proof: ', response);
+ return null;
}
async function handleClearProProof() {
- // TODO: remove pro proof from user config
+ await UserConfigWrapperActions.removeProConfig();
+ // TODO: remove access expiry timestamp from synced user config
}
async function handleExpiryCTAs(
@@ -210,6 +212,11 @@ async function handleExpiryCTAs(
const proExpiringSoonCTA = !isUndefined(Storage.get(SettingsKey.proExpiringSoonCTA));
const proExpiredCTA = !isUndefined(Storage.get(SettingsKey.proExpiredCTA));
+ // Remove the pro expired cta item if the user gets pro again
+ if (status === ProStatus.Active && proExpiredCTA) {
+ await Storage.remove(SettingsKey.proExpiredCTA);
+ }
+
if (now < sevenDaysBeforeExpiry) {
// More than 7 days before expiry, remove CTA items if they exist. This means the items were set for a previous cycle of pro access.
if (proExpiringSoonCTA) {
@@ -227,10 +234,30 @@ async function handleExpiryCTAs(
// Between expiry and 30 days after expiry, Expired CTA needs to be marked to be shown if not already
if (status === ProStatus.Expired && !proExpiredCTA) {
await Storage.put(SettingsKey.proExpiredCTA, true);
+ // The expiring soon CTA should be removed if it's set as we want to show it again in the future if needed
+ if (proExpiringSoonCTA) {
+ await Storage.remove(SettingsKey.proExpiringSoonCTA);
+ }
}
}
}
+let lastKnownProofExpiryTimestamp: number | null = null;
+let scheduledProofExpiryTaskTimestamp: number | null = null;
+let scheduledProofExpiryTaskId: ReturnType | null = null;
+let scheduledAccessExpiryTaskTimestamp: number | null = null;
+let scheduledAccessExpiryTaskId: ReturnType | null = null;
+
+function scheduleRefresh(timestampMs: number) {
+ const delay = Math.max(timestampMs - NetworkTime.now(), 15 * DURATION.SECONDS);
+ window?.log?.info(`Scheduling a pro details refresh in ${delay}ms for ${timestampMs}`);
+ return setTimeout(() => {
+ window?.inboxStore?.dispatch(
+ proBackendDataActions.refreshGetProDetailsFromProBackend({}) as any
+ );
+ }, delay);
+}
+
async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, status: ProStatus) {
if (status !== ProStatus.Active) {
return;
@@ -238,10 +265,22 @@ async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, s
const proConfig = await UserConfigWrapperActions.getProConfig();
+ // TODO: if the user config access expiry timestamp is different, set it and sync the user config
+
+ let proofExpiry: number | null = null;
+
if (!proConfig || !proConfig.proProof) {
- const rotatingPrivKeyHex = await UserUtils.getProRotatingPrivateKeyHex();
- await handleNewProProof(rotatingPrivKeyHex);
+ try {
+ const rotatingPrivKeyHex = await UserUtils.getProRotatingPrivateKeyHex();
+ const newProof = await handleNewProProof(rotatingPrivKeyHex);
+ if (newProof) {
+ proofExpiry = newProof.expiryMs;
+ }
+ } catch (e) {
+ window?.log?.error(e);
+ }
} else {
+ proofExpiry = proConfig.proProof.expiryMs;
const sixtyMinutesBeforeAccessExpiry = accessExpiryTsMs - DURATION.HOURS;
const sixtyMinutesBeforeProofExpiry = proConfig.proProof.expiryMs - DURATION.HOURS;
const now = NetworkTime.now();
@@ -251,8 +290,34 @@ async function handleProProof(accessExpiryTsMs: number, autoRenewing: boolean, s
autoRenewing
) {
const rotatingPrivKeyHex = proConfig.rotatingPrivKeyHex;
- await handleNewProProof(rotatingPrivKeyHex);
+ const newProof = await handleNewProProof(rotatingPrivKeyHex);
+ if (newProof) {
+ proofExpiry = newProof.expiryMs;
+ }
+ }
+ }
+
+ const accessExpiryRefreshTimestamp = accessExpiryTsMs + 30 * DURATION.SECONDS;
+ if (accessExpiryRefreshTimestamp !== scheduledAccessExpiryTaskTimestamp) {
+ if (scheduledAccessExpiryTaskId) {
+ clearTimeout(scheduledAccessExpiryTaskId);
+ }
+ scheduledAccessExpiryTaskTimestamp = accessExpiryRefreshTimestamp;
+ scheduledAccessExpiryTaskId = scheduleRefresh(scheduledAccessExpiryTaskTimestamp);
+ }
+
+ if (
+ proofExpiry &&
+ (!scheduledProofExpiryTaskTimestamp || proofExpiry !== lastKnownProofExpiryTimestamp)
+ ) {
+ if (scheduledProofExpiryTaskId) {
+ clearTimeout(scheduledProofExpiryTaskId);
}
+ // Random number of minutes between 10 and 60
+ const minutes = Math.floor(Math.random() * 51) + 10;
+ lastKnownProofExpiryTimestamp = proofExpiry;
+ scheduledProofExpiryTaskTimestamp = proofExpiry - minutes * DURATION.MINUTES;
+ scheduledProofExpiryTaskId = scheduleRefresh(scheduledProofExpiryTaskTimestamp);
}
}
diff --git a/ts/state/onboarding/ducks/modals.ts b/ts/state/onboarding/ducks/modals.ts
index f8d5e27cfe..c8016b12f0 100644
--- a/ts/state/onboarding/ducks/modals.ts
+++ b/ts/state/onboarding/ducks/modals.ts
@@ -2,7 +2,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { TermsOfServicePrivacyDialogProps } from '../../../components/dialog/TermsOfServicePrivacyDialog';
import type { SessionConfirmDialogProps } from '../../../components/dialog/SessionConfirm';
import {
- type SessionProInfoState,
+ type SessionCTAState,
type OpenUrlModalState,
type LocalizedPopupDialogState,
} from '../../ducks/modalDialog';
@@ -20,7 +20,7 @@ export type ModalsState = {
termsOfServicePrivacyModalState: TermsOfServicePrivacyModalState;
openUrlModal: OpenUrlModalState;
localizedPopupDialog: LocalizedPopupDialogState;
- sessionProInfoModal: SessionProInfoState;
+ sessionProInfoModal: SessionCTAState;
};
const initialState: ModalsState = {
@@ -50,7 +50,7 @@ export const modalsSlice = createSlice({
updateLocalizedPopupDialogModal(state, action: PayloadAction) {
return { ...state, localizedPopupDialog: action.payload };
},
- updateSessionProInfoModal(state, action: PayloadAction) {
+ updateSessionProInfoModal(state, action: PayloadAction) {
return { ...state, sessionProInfoModal: action.payload };
},
},
diff --git a/ts/util/i18n/timeLocaleMap.ts b/ts/util/i18n/timeLocaleMap.ts
index 7d324ab2bc..e39559f685 100644
--- a/ts/util/i18n/timeLocaleMap.ts
+++ b/ts/util/i18n/timeLocaleMap.ts
@@ -24,7 +24,6 @@ export const timeLocaleMap: Record = {
en: supportedByDateFns.enUS,
// then overwrite anything that we don't agree with or need to support specifically.
- // @ts-expect-error - When building the locales with --en-only, this break tsc but is a non-issue
'es-419': supportedByDateFns.es,
fa: supportedByDateFns.faIR,
fil: supportedByDateFns.fi,
diff --git a/ts/util/logger/rotatingPinoDest.ts b/ts/util/logger/rotatingPinoDest.ts
index 90038a7400..d96e8dfef8 100644
--- a/ts/util/logger/rotatingPinoDest.ts
+++ b/ts/util/logger/rotatingPinoDest.ts
@@ -6,6 +6,7 @@ import pino from 'pino';
import { DURATION } from '../../session/constants';
import { LogLevel } from './Logging';
+import { getFileCreationTimestampMs } from '../../node/fs_utility';
/**
* Keep at most rotated 3 files, so 4 files total including the "current" one
@@ -52,10 +53,10 @@ export function createRotatingPinoDest({
function maybeRotate(startingIndex = maxSavedLogFiles - 1) {
let pendingFileIndex = startingIndex;
try {
- const { birthtimeMs } = fs.statSync(logFile);
+ const fileCreationTimeMs = getFileCreationTimestampMs(logFile);
// more recent than
- if (birthtimeMs > Date.now() - ROTATION_INTERVAL) {
+ if (fileCreationTimeMs && fileCreationTimeMs > Date.now() - ROTATION_INTERVAL) {
return;
}
diff --git a/ts/util/storage.ts b/ts/util/storage.ts
index 06fc14b59d..1f55caa80e 100644
--- a/ts/util/storage.ts
+++ b/ts/util/storage.ts
@@ -5,6 +5,7 @@ import { deleteSettingsBoolValue, updateSettingsBoolValue } from '../state/ducks
import { Data } from '../data/data';
import { SettingsKey } from '../data/settings-key';
import { ProProofResultType, ProDetailsResultType } from '../session/apis/pro_backend_api/schemas';
+import { UrlInteractionsType } from './urlHistory';
let ready = false;
@@ -15,6 +16,7 @@ type ValueType =
| boolean
| SessionKeyPair
| Array
+ | UrlInteractionsType
| ProDetailsResultType
| ProProofResultType;
type InsertedValueType = { id: string; value: ValueType };
diff --git a/ts/util/urlHistory.ts b/ts/util/urlHistory.ts
new file mode 100644
index 0000000000..264f4dcb1c
--- /dev/null
+++ b/ts/util/urlHistory.ts
@@ -0,0 +1,106 @@
+import { z } from 'zod';
+import { SettingsKey } from '../data/settings-key';
+import { Storage } from './storage';
+import { tr } from '../localization/localeTools';
+
+// NOTE: we currently only want to use url interactions for official urls. Once we have the ability to "trust" a url this can change
+function isValidUrl(url: string): boolean {
+ if (!URL.canParse(url)) {
+ return false;
+ }
+
+ const host = new URL(url).host;
+ return (
+ host === 'getsession.org' ||
+ host === 'session.foundation' ||
+ host === 'token.getsession.org' ||
+ host === 'stake.getsession.org'
+ );
+}
+
+export enum URLInteraction {
+ OPEN = 1,
+ COPY = 2,
+ TRUST = 3,
+}
+
+const UrlInteractionSchema = z.object({
+ url: z.string(),
+ lastUpdated: z.number(),
+ interactions: z.array(z.nativeEnum(URLInteraction)),
+});
+
+const UrlInteractionsSchema = z.array(UrlInteractionSchema);
+
+export type UrlInteractionsType = z.infer;
+
+export function getUrlInteractions() {
+ let interactions: UrlInteractionsType = [];
+ const rawInteractions = Storage.get(SettingsKey.urlInteractions);
+ const result = UrlInteractionsSchema.safeParse(rawInteractions);
+ if (result.error) {
+ window?.log?.error(`failed to parse ${SettingsKey.urlInteractions}`, result.error);
+ } else {
+ interactions = result.data;
+ }
+ return interactions;
+}
+
+export async function registerUrlInteraction(url: string, interaction: URLInteraction) {
+ if (!isValidUrl(url)) {
+ return;
+ }
+
+ const interactions = getUrlInteractions();
+ const idx = interactions.findIndex(item => item.url === url);
+ if (idx !== -1) {
+ if (!interactions[idx].interactions.includes(interaction)) {
+ interactions[idx].interactions.push(interaction);
+ }
+ } else {
+ interactions.push({
+ interactions: [interaction],
+ url,
+ lastUpdated: Date.now(),
+ });
+ }
+
+ await Storage.put(SettingsKey.urlInteractions, interactions);
+}
+
+export function getUrlInteractionsForUrl(url: string): Array {
+ if (!isValidUrl(url)) {
+ return [];
+ }
+
+ const interactions = getUrlInteractions();
+ return interactions.find(item => item.url === url)?.interactions ?? [];
+}
+
+export async function clearAllUrlInteractions() {
+ await Storage.put(SettingsKey.urlInteractions, []);
+}
+
+export async function removeUrlInteractionHistory(url: string) {
+ const interactions = getUrlInteractions();
+ const idx = interactions.findIndex(item => item.url === url);
+ if (idx !== -1) {
+ interactions.splice(idx);
+ }
+
+ await Storage.put(SettingsKey.urlInteractions, interactions);
+}
+
+export function urlInteractionToString(interaction: URLInteraction) {
+ switch (interaction) {
+ case URLInteraction.OPEN:
+ return tr('open');
+ case URLInteraction.COPY:
+ return tr('copy');
+ case URLInteraction.TRUST:
+ // TODO: use localized string once it exists
+ return 'Trust';
+ default:
+ return tr('unknown');
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 5388305ed8..ec158db2c3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3865,9 +3865,9 @@ foreground-child@^3.1.0:
signal-exit "^4.0.1"
form-data@^4.0.0, form-data@^4.0.4:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
- integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
+ version "4.0.5"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
+ integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
@@ -5067,9 +5067,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
-"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.1/libsession_util_nodejs-v0.6.1.tar.gz":
- version "0.6.1"
- resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.1/libsession_util_nodejs-v0.6.1.tar.gz#472903f8d4a2aa8501fe5f69f3540b20f9880a14"
+"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.2/libsession_util_nodejs-v0.6.2.tar.gz":
+ version "0.6.2"
+ resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.6.2/libsession_util_nodejs-v0.6.2.tar.gz#875a144fcf495aa13fc6cc80950702de970c467e"
dependencies:
cmake-js "7.3.1"
node-addon-api "^8.3.1"