Moves showStickerPackPreview to globalModals

This commit is contained in:
Josh Perez 2022-12-09 14:01:46 -05:00 committed by GitHub
parent c0ebafe2bc
commit 7c68f9ef1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 104 additions and 124 deletions

View file

@ -37,6 +37,9 @@ export type PropsType = {
// SignalConnectionsModal // SignalConnectionsModal
isSignalConnectionsVisible: boolean; isSignalConnectionsVisible: boolean;
toggleSignalConnectionsModal: () => unknown; toggleSignalConnectionsModal: () => unknown;
// StickerPackPreviewModal
stickerPackPreviewId?: string;
renderStickerPreviewModal: () => JSX.Element | null;
// StoriesSettings // StoriesSettings
isStoriesSettingsVisible: boolean; isStoriesSettingsVisible: boolean;
renderStoriesSettings: () => JSX.Element; renderStoriesSettings: () => JSX.Element;
@ -72,6 +75,9 @@ export function GlobalModalContainer({
// SignalConnectionsModal // SignalConnectionsModal
isSignalConnectionsVisible, isSignalConnectionsVisible,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
// StickerPackPreviewModal
stickerPackPreviewId,
renderStickerPreviewModal,
// StoriesSettings // StoriesSettings
isStoriesSettingsVisible, isStoriesSettingsVisible,
renderStoriesSettings, renderStoriesSettings,
@ -158,5 +164,9 @@ export function GlobalModalContainer({
return renderStoriesSettings(); return renderStoriesSettings();
} }
if (stickerPackPreviewId) {
return renderStickerPreviewModal();
}
return null; return null;
} }

View file

@ -68,7 +68,7 @@ export const StickerManager = React.memo(function StickerManagerInner({
<StickerPreviewModal <StickerPreviewModal
i18n={i18n} i18n={i18n}
pack={packToPreview} pack={packToPreview}
onClose={clearPackToPreview} closeStickerPackPreview={clearPackToPreview}
downloadStickerPack={downloadStickerPack} downloadStickerPack={downloadStickerPack}
installStickerPack={installStickerPack} installStickerPack={installStickerPack}
uninstallStickerPack={uninstallStickerPack} uninstallStickerPack={uninstallStickerPack}

View file

@ -64,7 +64,7 @@ export function Full(): JSX.Element {
return ( return (
<StickerPreviewModal <StickerPreviewModal
onClose={action('onClose')} closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')} installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')} uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')} downloadStickerPack={action('downloadStickerPack')}
@ -93,7 +93,7 @@ export function JustFourStickers(): JSX.Element {
return ( return (
<StickerPreviewModal <StickerPreviewModal
onClose={action('onClose')} closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')} installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')} uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')} downloadStickerPack={action('downloadStickerPack')}
@ -110,7 +110,7 @@ JustFourStickers.story = {
export function InitialDownload(): JSX.Element { export function InitialDownload(): JSX.Element {
return ( return (
<StickerPreviewModal <StickerPreviewModal
onClose={action('onClose')} closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')} installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')} uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')} downloadStickerPack={action('downloadStickerPack')}
@ -128,7 +128,7 @@ InitialDownload.story = {
export function PackDeleted(): JSX.Element { export function PackDeleted(): JSX.Element {
return ( return (
<StickerPreviewModal <StickerPreviewModal
onClose={action('onClose')} closeStickerPackPreview={action('closeStickerPackPreview')}
installStickerPack={action('installStickerPack')} installStickerPack={action('installStickerPack')}
uninstallStickerPack={action('uninstallStickerPack')} uninstallStickerPack={action('uninstallStickerPack')}
downloadStickerPack={action('downloadStickerPack')} downloadStickerPack={action('downloadStickerPack')}

View file

@ -13,7 +13,7 @@ import { Spinner } from '../Spinner';
import { useRestoreFocus } from '../../hooks/useRestoreFocus'; import { useRestoreFocus } from '../../hooks/useRestoreFocus';
export type OwnProps = { export type OwnProps = {
readonly onClose: () => unknown; readonly closeStickerPackPreview: () => unknown;
readonly downloadStickerPack: ( readonly downloadStickerPack: (
packId: string, packId: string,
packKey: string, packKey: string,
@ -76,7 +76,7 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
props: Props props: Props
) { ) {
const { const {
onClose, closeStickerPackPreview,
pack, pack,
i18n, i18n,
downloadStickerPack, downloadStickerPack,
@ -119,9 +119,9 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
React.useEffect(() => { React.useEffect(() => {
if (!pack) { if (!pack) {
onClose(); closeStickerPackPreview();
} }
}, [pack, onClose]); }, [pack, closeStickerPackPreview]);
const isInstalled = Boolean(pack && pack.status === 'installed'); const isInstalled = Boolean(pack && pack.status === 'installed');
const handleToggleInstall = React.useCallback(() => { const handleToggleInstall = React.useCallback(() => {
@ -132,16 +132,16 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
setConfirmingUninstall(true); setConfirmingUninstall(true);
} else if (pack.status === 'ephemeral') { } else if (pack.status === 'ephemeral') {
downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' }); downloadStickerPack(pack.id, pack.key, { finalStatus: 'installed' });
onClose(); closeStickerPackPreview();
} else { } else {
installStickerPack(pack.id, pack.key); installStickerPack(pack.id, pack.key);
onClose(); closeStickerPackPreview();
} }
}, [ }, [
downloadStickerPack, downloadStickerPack,
installStickerPack, installStickerPack,
isInstalled, isInstalled,
onClose, closeStickerPackPreview,
pack, pack,
setConfirmingUninstall, setConfirmingUninstall,
]); ]);
@ -152,13 +152,13 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
} }
uninstallStickerPack(pack.id, pack.key); uninstallStickerPack(pack.id, pack.key);
setConfirmingUninstall(false); setConfirmingUninstall(false);
// onClose is called by <ConfirmationDialog /> // closeStickerPackPreview is called by <ConfirmationDialog />'s onClose
}, [uninstallStickerPack, setConfirmingUninstall, pack]); }, [uninstallStickerPack, setConfirmingUninstall, pack]);
React.useEffect(() => { React.useEffect(() => {
const handler = ({ key }: KeyboardEvent) => { const handler = ({ key }: KeyboardEvent) => {
if (key === 'Escape') { if (key === 'Escape') {
onClose(); closeStickerPackPreview();
} }
}; };
@ -167,15 +167,15 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
return () => { return () => {
document.removeEventListener('keydown', handler); document.removeEventListener('keydown', handler);
}; };
}, [onClose]); }, [closeStickerPackPreview]);
const handleClickToClose = React.useCallback( const handleClickToClose = React.useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
onClose(); closeStickerPackPreview();
} }
}, },
[onClose] [closeStickerPackPreview]
); );
return root return root
@ -192,7 +192,7 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
<ConfirmationDialog <ConfirmationDialog
dialogName="StickerPreviewModal.confirmUninstall" dialogName="StickerPreviewModal.confirmUninstall"
i18n={i18n} i18n={i18n}
onClose={onClose} onClose={closeStickerPackPreview}
actions={[ actions={[
{ {
style: 'negative', style: 'negative',
@ -211,7 +211,7 @@ export const StickerPreviewModal = React.memo(function StickerPreviewModalInner(
</h2> </h2>
<button <button
type="button" type="button"
onClick={onClose} onClick={closeStickerPackPreview}
className="module-sticker-manager__preview-modal__container__header__close-button" className="module-sticker-manager__preview-modal__container__header__close-button"
aria-label={i18n('close')} aria-label={i18n('close')}
/> />

View file

@ -35,7 +35,6 @@ import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions
import { createPendingInvites } from './state/roots/createPendingInvites'; import { createPendingInvites } from './state/roots/createPendingInvites';
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer'; import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import { createStickerManager } from './state/roots/createStickerManager'; import { createStickerManager } from './state/roots/createStickerManager';
import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal'; import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import { createStore } from './state/createStore'; import { createStore } from './state/createStore';
@ -417,7 +416,6 @@ export const setup = (options: {
createSafetyNumberViewer, createSafetyNumberViewer,
createShortcutGuideModal, createShortcutGuideModal,
createStickerManager, createStickerManager,
createStickerPreviewModal,
}; };
const Ducks = { const Ducks = {

View file

@ -10,6 +10,7 @@ import type { SafetyNumberChangeSource } from '../../components/SafetyNumberChan
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import * as SingleServePromise from '../../services/singleServePromise'; import * as SingleServePromise from '../../services/singleServePromise';
import * as Stickers from '../../types/Stickers';
import { getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { getMessagePropsSelector } from '../selectors/message'; import { getMessagePropsSelector } from '../selectors/message';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
@ -49,6 +50,7 @@ export type GlobalModalsStateType = Readonly<{
profileEditorHasError: boolean; profileEditorHasError: boolean;
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType; safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
safetyNumberModalContactId?: string; safetyNumberModalContactId?: string;
stickerPackPreviewId?: string;
userNotFoundModalState?: UserNotFoundModalStateType; userNotFoundModalState?: UserNotFoundModalStateType;
}>; }>;
@ -76,6 +78,8 @@ export const SHOW_SEND_ANYWAY_DIALOG = 'globalModals/SHOW_SEND_ANYWAY_DIALOG';
const HIDE_SEND_ANYWAY_DIALOG = 'globalModals/HIDE_SEND_ANYWAY_DIALOG'; const HIDE_SEND_ANYWAY_DIALOG = 'globalModals/HIDE_SEND_ANYWAY_DIALOG';
const SHOW_GV2_MIGRATION_DIALOG = 'globalModals/SHOW_GV2_MIGRATION_DIALOG'; const SHOW_GV2_MIGRATION_DIALOG = 'globalModals/SHOW_GV2_MIGRATION_DIALOG';
const CLOSE_GV2_MIGRATION_DIALOG = 'globalModals/CLOSE_GV2_MIGRATION_DIALOG'; const CLOSE_GV2_MIGRATION_DIALOG = 'globalModals/CLOSE_GV2_MIGRATION_DIALOG';
const SHOW_STICKER_PACK_PREVIEW = 'globalModals/SHOW_STICKER_PACK_PREVIEW';
const CLOSE_STICKER_PACK_PREVIEW = 'globalModals/CLOSE_STICKER_PACK_PREVIEW';
export type ContactModalStateType = { export type ContactModalStateType = {
contactId: string; contactId: string;
@ -173,6 +177,15 @@ type HideSendAnywayDialogActiontype = {
type: typeof HIDE_SEND_ANYWAY_DIALOG; type: typeof HIDE_SEND_ANYWAY_DIALOG;
}; };
type ShowStickerPackPreviewActionType = {
type: typeof SHOW_STICKER_PACK_PREVIEW;
payload: string;
};
type CloseStickerPackPreviewActionType = {
type: typeof CLOSE_STICKER_PACK_PREVIEW;
};
export type GlobalModalsActionType = export type GlobalModalsActionType =
| StartMigrationToGV2ActionType | StartMigrationToGV2ActionType
| CloseGV2MigrationDialogActionType | CloseGV2MigrationDialogActionType
@ -186,6 +199,8 @@ export type GlobalModalsActionType =
| ShowStoriesSettingsActionType | ShowStoriesSettingsActionType
| HideSendAnywayDialogActiontype | HideSendAnywayDialogActiontype
| ShowSendAnywayDialogActionType | ShowSendAnywayDialogActionType
| CloseStickerPackPreviewActionType
| ShowStickerPackPreviewActionType
| ToggleForwardMessageModalActionType | ToggleForwardMessageModalActionType
| ToggleProfileEditorActionType | ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType | ToggleProfileEditorErrorActionType
@ -214,6 +229,8 @@ export const actions = {
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
showGV2MigrationDialog, showGV2MigrationDialog,
closeGV2MigrationDialog, closeGV2MigrationDialog,
showStickerPackPreview,
closeStickerPackPreview,
}; };
export const useGlobalModalActions = (): BoundActionCreatorsMapObject< export const useGlobalModalActions = (): BoundActionCreatorsMapObject<
@ -417,6 +434,40 @@ function hideBlockingSafetyNumberChangeDialog(): HideSendAnywayDialogActiontype
}; };
} }
function closeStickerPackPreview(): ThunkAction<
void,
RootStateType,
unknown,
CloseStickerPackPreviewActionType
> {
return async (dispatch, getState) => {
const packId = getState().globalModals.stickerPackPreviewId;
if (!packId) {
return;
}
await Stickers.removeEphemeralPack(packId);
dispatch({
type: CLOSE_STICKER_PACK_PREVIEW,
});
};
}
function showStickerPackPreview(
packId: string,
packKey: string
): ShowStickerPackPreviewActionType {
// Intentionally not awaiting this so that we can show the modal right away.
// The modal has a loading spinner on it.
Stickers.downloadEphemeralPack(packId, packKey);
return {
type: SHOW_STICKER_PACK_PREVIEW,
payload: packId,
};
}
// Reducer // Reducer
export function getEmptyState(): GlobalModalsStateType { export function getEmptyState(): GlobalModalsStateType {
@ -552,5 +603,19 @@ export function reducer(
}; };
} }
if (action.type === CLOSE_STICKER_PACK_PREVIEW) {
return {
...state,
stickerPackPreviewId: undefined,
};
}
if (action.type === SHOW_STICKER_PACK_PREVIEW) {
return {
...state,
stickerPackPreviewId: action.payload,
};
}
return state; return state;
} }

View file

@ -1,19 +0,0 @@
// Copyright 2019-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { Provider } from 'react-redux';
import type { Store } from 'redux';
import type { ExternalProps } from '../smart/StickerPreviewModal';
import { SmartStickerPreviewModal } from '../smart/StickerPreviewModal';
export const createStickerPreviewModal = (
store: Store,
props: ExternalProps
): React.ReactElement => (
<Provider store={store}>
<SmartStickerPreviewModal {...props} />
</Provider>
);

View file

@ -16,6 +16,7 @@ import { mapDispatchToProps } from '../actions';
import { getIntl, getTheme } from '../selectors/user'; import { getIntl, getTheme } from '../selectors/user';
import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal'; import { SmartAddUserToAnotherGroupModal } from './AddUserToAnotherGroupModal';
import { SmartStickerPreviewModal } from './StickerPreviewModal';
function renderProfileEditor(): JSX.Element { function renderProfileEditor(): JSX.Element {
return <SmartProfileEditorModal />; return <SmartProfileEditorModal />;
@ -40,6 +41,8 @@ function renderSendAnywayDialog(): JSX.Element {
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
const i18n = getIntl(state); const i18n = getIntl(state);
const { stickerPackPreviewId } = state.globalModals;
return { return {
...state.globalModals, ...state.globalModals,
hasSafetyNumberChangeModal: getConversationsStoppingSend(state).length > 0, hasSafetyNumberChangeModal: getConversationsStoppingSend(state).length > 0,
@ -49,6 +52,10 @@ const mapStateToProps = (state: StateType) => {
renderForwardMessageModal, renderForwardMessageModal,
renderProfileEditor, renderProfileEditor,
renderStoriesSettings, renderStoriesSettings,
renderStickerPreviewModal: () =>
stickerPackPreviewId ? (
<SmartStickerPreviewModal packId={stickerPackPreviewId} />
) : null,
renderSafetyNumber: () => ( renderSafetyNumber: () => (
<SmartSafetyNumberModal <SmartSafetyNumberModal
contactID={String(state.globalModals.safetyNumberModalContactId)} contactID={String(state.globalModals.safetyNumberModalContactId)}

View file

@ -15,7 +15,6 @@ import {
export type ExternalProps = { export type ExternalProps = {
packId: string; packId: string;
readonly onClose: () => unknown;
}; };
const mapStateToProps = (state: StateType, props: ExternalProps) => { const mapStateToProps = (state: StateType, props: ExternalProps) => {

View file

@ -441,50 +441,7 @@ export function createIPCEvents(
log.warn('showStickerPack: Not registered, returning early'); log.warn('showStickerPack: Not registered, returning early');
return; return;
} }
if (window.isShowingModal) { window.reduxActions.globalModals.showStickerPackPreview(packId, key);
log.warn('showStickerPack: Already showing modal, returning early');
return;
}
try {
window.isShowingModal = true;
// Kick off the download
Stickers.downloadEphemeralPack(packId, key);
const props = {
packId,
onClose: async () => {
window.isShowingModal = false;
stickerPreviewModalView.remove();
await Stickers.removeEphemeralPack(packId);
},
};
const stickerPreviewModalView = new ReactWrapperView({
className: 'sticker-preview-modal-wrapper',
JSX: window.Signal.State.Roots.createStickerPreviewModal(
window.reduxStore,
props
),
});
} catch (error) {
window.isShowingModal = false;
log.error(
'showStickerPack: Ran into an error!',
Errors.toLogFormat(error)
);
const errorView = new ReactWrapperView({
className: 'error-modal-wrapper',
JSX: (
<ErrorModal
i18n={window.i18n}
onClose={() => {
errorView.remove();
}}
/>
),
});
}
}, },
showGroupViaLink: async hash => { showGroupViaLink: async hash => {
// We can get these events even if the user has never linked this instance. // We can get these events even if the user has never linked this instance.
@ -492,10 +449,6 @@ export function createIPCEvents(
log.warn('showGroupViaLink: Not registered, returning early'); log.warn('showGroupViaLink: Not registered, returning early');
return; return;
} }
if (window.isShowingModal) {
log.warn('showGroupViaLink: Already showing modal, returning early');
return;
}
try { try {
await window.Signal.Groups.joinViaLink(hash); await window.Signal.Groups.joinViaLink(hash);
} catch (error) { } catch (error) {
@ -517,7 +470,6 @@ export function createIPCEvents(
), ),
}); });
} }
window.isShowingModal = false;
}, },
async showConversationViaSignalDotMe(hash: string) { async showConversationViaSignalDotMe(hash: string) {
if (!window.Signal.Util.Registration.everDone()) { if (!window.Signal.Util.Registration.everDone()) {
@ -559,13 +511,7 @@ export function createIPCEvents(
} }
log.info('showConversationViaSignalDotMe: invalid E164'); log.info('showConversationViaSignalDotMe: invalid E164');
if (window.isShowingModal) { showUnknownSgnlLinkModal();
log.info(
'showConversationViaSignalDotMe: a modal is already showing. Doing nothing'
);
} else {
showUnknownSgnlLinkModal();
}
}, },
unknownSignalLink: () => { unknownSignalLink: () => {

View file

@ -11,7 +11,6 @@ import { render } from 'mustache';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import { isGIF } from '../types/Attachment'; import { isGIF } from '../types/Attachment';
import * as Stickers from '../types/Stickers';
import type { MIMEType } from '../types/MIME'; import type { MIMEType } from '../types/MIME';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import type { MessageAttributesType } from '../model-types.d'; import type { MessageAttributesType } from '../model-types.d';
@ -1108,29 +1107,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}); });
} }
showStickerPackPreview(packId: string, packKey: string): void {
Stickers.downloadEphemeralPack(packId, packKey);
const props = {
packId,
onClose: async () => {
if (this.stickerPreviewModalView) {
this.stickerPreviewModalView.remove();
this.stickerPreviewModalView = undefined;
}
await Stickers.removeEphemeralPack(packId);
},
};
this.stickerPreviewModalView = new ReactWrapperView({
className: 'sticker-preview-modal-wrapper',
JSX: window.Signal.State.Roots.createStickerPreviewModal(
window.reduxStore,
props
),
});
}
showLightboxForMedia( showLightboxForMedia(
selectedMediaItem: MediaItemType, selectedMediaItem: MediaItemType,
media: Array<MediaItemType> = [] media: Array<MediaItemType> = []
@ -1195,7 +1171,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
const sticker = message.get('sticker'); const sticker = message.get('sticker');
if (sticker) { if (sticker) {
const { packId, packKey } = sticker; const { packId, packKey } = sticker;
this.showStickerPackPreview(packId, packKey); window.reduxActions.globalModals.showStickerPackPreview(packId, packKey);
return; return;
} }

2
ts/window.d.ts vendored
View file

@ -48,7 +48,6 @@ import type { createPendingInvites } from './state/roots/createPendingInvites';
import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer'; import type { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import type { createShortcutGuideModal } from './state/roots/createShortcutGuideModal'; import type { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import type { createStickerManager } from './state/roots/createStickerManager'; import type { createStickerManager } from './state/roots/createStickerManager';
import type { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
import type * as appDuck from './state/ducks/app'; import type * as appDuck from './state/ducks/app';
import type * as callingDuck from './state/ducks/calling'; import type * as callingDuck from './state/ducks/calling';
import type * as conversationsDuck from './state/ducks/conversations'; import type * as conversationsDuck from './state/ducks/conversations';
@ -179,7 +178,6 @@ export type SignalCoreType = {
createSafetyNumberViewer: typeof createSafetyNumberViewer; createSafetyNumberViewer: typeof createSafetyNumberViewer;
createShortcutGuideModal: typeof createShortcutGuideModal; createShortcutGuideModal: typeof createShortcutGuideModal;
createStickerManager: typeof createStickerManager; createStickerManager: typeof createStickerManager;
createStickerPreviewModal: typeof createStickerPreviewModal;
}; };
Ducks: { Ducks: {
app: typeof appDuck; app: typeof appDuck;