Internal tool to test megaphone

This commit is contained in:
ayumi-signal
2026-02-23 10:13:49 -08:00
committed by GitHub
parent b61c2029c4
commit 491de86ad3
10 changed files with 120 additions and 5 deletions

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -631,6 +631,7 @@ export default {
action('generateDonationReceiptBlob')();
return new Blob();
},
addVisibleMegaphone: async () => Promise.resolve(),
internalDeleteAllMegaphones: async () => {
return Promise.resolve(0);
},

View File

@@ -101,6 +101,7 @@ import { AxoButton } from '../axo/AxoButton.dom.js';
import type { ExternalProps as SmartNotificationProfilesProps } from '../state/smart/PreferencesNotificationProfiles.preload.js';
import type { LocalBackupExportMetadata } from '../types/LocalExport.std.js';
import { isDonationsPage } from './PreferencesDonations.dom.js';
import type { VisibleRemoteMegaphoneType } from '../types/Megaphone.std.js';
const { isNumber, noop, partition } = lodash;
@@ -289,6 +290,7 @@ type PropsFunctionType = {
receipt: DonationReceipt,
i18n: LocalizerType
) => Promise<Blob>;
addVisibleMegaphone: (megaphone: VisibleRemoteMegaphoneType) => void;
// Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType;
@@ -541,6 +543,7 @@ export function Preferences({
internalAddDonationReceipt,
saveAttachmentToDisk,
generateDonationReceiptBlob,
addVisibleMegaphone,
internalDeleteAllMegaphones,
__dangerouslyRunAbitraryReadOnlySqlQuery,
cqsTestMode,
@@ -2327,6 +2330,7 @@ export function Preferences({
internalAddDonationReceipt={internalAddDonationReceipt}
saveAttachmentToDisk={saveAttachmentToDisk}
generateDonationReceiptBlob={generateDonationReceiptBlob}
addVisibleMegaphone={addVisibleMegaphone}
internalDeleteAllMegaphones={internalDeleteAllMegaphones}
__dangerouslyRunAbitraryReadOnlySqlQuery={
__dangerouslyRunAbitraryReadOnlySqlQuery

View File

@@ -21,6 +21,8 @@ import { getHumanDonationAmount } from '../util/currency.dom.js';
import { AutoSizeTextArea } from './AutoSizeTextArea.dom.js';
import { AxoButton } from '../axo/AxoButton.dom.js';
import { AxoSwitch } from '../axo/AxoSwitch.dom.js';
import type { VisibleRemoteMegaphoneType } from '../types/Megaphone.std.js';
import { internalGetTestMegaphone } from '../util/getTestMegaphone.std.js';
const log = createLogger('PreferencesInternal');
@@ -33,6 +35,7 @@ export function PreferencesInternal({
internalAddDonationReceipt,
saveAttachmentToDisk,
generateDonationReceiptBlob,
addVisibleMegaphone,
internalDeleteAllMegaphones,
__dangerouslyRunAbitraryReadOnlySqlQuery,
cqsTestMode,
@@ -55,6 +58,7 @@ export function PreferencesInternal({
receipt: DonationReceipt,
i18n: LocalizerType
) => Promise<Blob>;
addVisibleMegaphone: (megaphone: VisibleRemoteMegaphoneType) => void;
internalDeleteAllMegaphones: () => Promise<number>;
__dangerouslyRunAbitraryReadOnlySqlQuery: (
readonlySqlQuery: string
@@ -73,6 +77,9 @@ export function PreferencesInternal({
BackupValidationResultType | undefined
>();
const [showMegaphoneResult, setShowMegaphoneResult] = useState<
string | undefined
>();
const [deleteAllMegaphonesResult, setDeleteAllMegaphonesResult] = useState<
number | undefined
>();
@@ -482,6 +489,42 @@ export function PreferencesInternal({
</SettingsRow>
<SettingsRow title="Megaphones">
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
Show a test megaphone in memory. Disappears on restart.
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<AxoButton.Root
variant="secondary"
size="lg"
onClick={async () => {
const megaphone = internalGetTestMegaphone();
addVisibleMegaphone(megaphone);
setShowMegaphoneResult(
`Megaphone shown. Go to Chats tab to view.\n${JSON.stringify(megaphone, null, 2)}`
);
}}
>
Show megaphone
</AxoButton.Root>
</div>
{showMegaphoneResult != null && (
<AutoSizeTextArea
i18n={i18n}
value={showMegaphoneResult}
onChange={() => null}
readOnly
placeholder=""
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
/>
)}
</FlowingSettingsControl>
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
Delete local records of remote megaphones

View File

@@ -26,7 +26,7 @@ export default {
secondaryCtaText: 'Not now',
title: 'Donate to Signal',
body: 'Signal is powered by people like you. Show your support today!',
imagePath: '/fixtures/donate-heart.png',
imagePath: 'images/donate-heart.png',
isFullSize: true,
onClickNarrowMegaphone: action('onClickNarrowMegaphone'),
onInteractWithMegaphone: action('onInteractWithMegaphone'),

View File

@@ -17,6 +17,7 @@ import type {
import type { ChangeLocationAction } from './nav.std.js';
import { actions as navActions } from './nav.std.js';
import { NavTab, SettingsPage } from '../../types/Nav.std.js';
import { isTestMegaphoneId } from '../../util/getTestMegaphone.std.js';
const log = createLogger('megaphones');
@@ -88,10 +89,14 @@ function interactWithMegaphone(
RemoveVisibleMegaphoneAction | ChangeLocationAction
> {
return async dispatch => {
const isTest = isTestMegaphoneId(megaphoneId);
if (ctaId === 'donate' || ctaId === 'finish') {
try {
log.info(`Finishing megaphone ${megaphoneId}, ctaId=${ctaId}`);
await DataWriter.finishMegaphone(megaphoneId);
if (!isTest) {
await DataWriter.finishMegaphone(megaphoneId);
}
} catch (error) {
log.error(
`Failed to finish megaphone ${megaphoneId}`,
@@ -112,7 +117,9 @@ function interactWithMegaphone(
} else if (ctaId === 'snooze') {
try {
log.info(`Snoozing megaphone ${megaphoneId}`);
await DataWriter.snoozeMegaphone(megaphoneId);
if (!isTest) {
await DataWriter.snoozeMegaphone(megaphoneId);
}
} catch (error) {
log.error(
`Failed to snooze megaphone ${megaphoneId}`,

View File

@@ -11,6 +11,10 @@ import {
type VisibleRemoteMegaphoneType,
} from '../../types/Megaphone.std.js';
import type { StateSelector } from '../types.std.js';
import {
isTestMegaphone,
TEST_MEGAPHONE_IMAGE,
} from '../../util/getTestMegaphone.std.js';
export function getMegaphonesState(
state: Readonly<StateType>
@@ -36,6 +40,8 @@ export const getVisibleMegaphonesForDisplay: StateSelector<
secondaryCtaText: megaphone.secondaryCtaText,
title: megaphone.title,
body: megaphone.body,
imagePath: getAbsoluteMegaphoneImageFilePath(megaphone.imagePath),
imagePath: isTestMegaphone(megaphone)
? TEST_MEGAPHONE_IMAGE
: getAbsoluteMegaphoneImageFilePath(megaphone.imagePath),
}))
);

View File

@@ -120,6 +120,7 @@ import { DonationsErrorBoundary } from '../../components/DonationsErrorBoundary.
import type { SmartPreferencesChatFoldersPageProps } from './PreferencesChatFoldersPage.preload.js';
import type { SmartPreferencesEditChatFolderPageProps } from './PreferencesEditChatFolderPage.preload.js';
import type { ExternalProps as SmartNotificationProfilesProps } from './PreferencesNotificationProfiles.preload.js';
import { useMegaphonesActions } from '../ducks/megaphones.preload.js';
const DEFAULT_NOTIFICATION_SETTING = 'message';
@@ -225,6 +226,7 @@ export function SmartPreferences(): React.JSX.Element | null {
const { showToast } = useToastActions();
const { internalAddDonationReceipt } = useDonationsActions();
const { startPlaintextExport, startLocalBackupExport } = useBackupActions();
const { addVisibleMegaphone } = useMegaphonesActions();
// Selectors
@@ -986,6 +988,7 @@ export function SmartPreferences(): React.JSX.Element | null {
internalAddDonationReceipt={internalAddDonationReceipt}
saveAttachmentToDisk={saveAttachmentToDisk}
generateDonationReceiptBlob={generateDonationReceiptBlob}
addVisibleMegaphone={addVisibleMegaphone}
internalDeleteAllMegaphones={internalDeleteAllMegaphones}
__dangerouslyRunAbitraryReadOnlySqlQuery={
__dangerouslyRunAbitraryReadOnlySqlQuery

View File

@@ -35,7 +35,7 @@ const FAKE_MEGAPHONE: RemoteMegaphoneType = {
localeFetched: 'en',
title: 'megaphone',
body: 'cats',
imagePath: '../../../fixtures/donate-heart.png',
imagePath: '../../../images/donate-heart.png',
primaryCtaText: 'donate',
secondaryCtaText: 'snooze',
snoozeCount: 0,

View File

@@ -0,0 +1,51 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type {
RemoteMegaphoneId,
VisibleRemoteMegaphoneType,
} from '../types/Megaphone.std.js';
import { MegaphoneCtaId } from '../types/Megaphone.std.js';
import { DAY } from './durations/index.std.js';
const INTERNAL_TEST_ID = 'INTERNAL_TEST' as RemoteMegaphoneId;
export const TEST_MEGAPHONE_IMAGE = 'images/donate-heart.png';
export function internalGetTestMegaphone(
props?: Partial<VisibleRemoteMegaphoneType>
): VisibleRemoteMegaphoneType {
return {
priority: 100,
desktopMinVersion: '1.0.0',
dontShowBeforeEpochMs: Date.now() - 1 * DAY,
dontShowAfterEpochMs: Date.now() + 14 * DAY,
showForNumberOfDays: 30,
conditionalId: 'internal',
primaryCtaId: MegaphoneCtaId.Donate,
primaryCtaData: null,
secondaryCtaId: MegaphoneCtaId.Snooze,
secondaryCtaData: { snoozeDurationDays: [5, 7, 100] },
localeFetched: 'en',
title: 'Donate Today',
body: 'As a nonprofit, Signal needs your support.',
imagePath: TEST_MEGAPHONE_IMAGE,
primaryCtaText: 'Donate',
secondaryCtaText: 'Snooze',
snoozeCount: 0,
snoozedAt: null,
shownAt: null,
isFinished: false,
...props,
id: INTERNAL_TEST_ID,
};
}
export function isTestMegaphone(
megaphone: VisibleRemoteMegaphoneType
): boolean {
return megaphone.id === INTERNAL_TEST_ID;
}
export function isTestMegaphoneId(id: RemoteMegaphoneId): boolean {
return id === INTERNAL_TEST_ID;
}