feat: added debug menu modal

ordered modals to take into account their z-indexes
pull/3281/head
yougotwill 3 months ago
parent 6001337581
commit e1aaa52786

@ -5,6 +5,7 @@ import {
getBlockOrUnblockUserModalState,
getChangeNickNameDialog,
getConfirmModal,
getDebugMenuModalState,
getDeleteAccountModalState,
getEditProfileDialog,
getEditProfilePictureModalState,
@ -43,6 +44,7 @@ import { UserDetailsDialog } from './UserDetailsDialog';
import { EditProfileDialog } from './edit-profile/EditProfileDialog';
import { OpenUrlModal } from './OpenUrlModal';
import { BlockOrUnblockDialog } from './blockOrUnblock/BlockOrUnblockDialog';
import { DebugMenuModal } from './debug/DebugMenuModal';
export const ModalContainer = () => {
const confirmModalState = useSelector(getConfirmModal);
@ -66,9 +68,18 @@ export const ModalContainer = () => {
const hideRecoveryPasswordModalState = useSelector(getHideRecoveryPasswordModalState);
const openUrlModalState = useSelector(getOpenUrlModalState);
const lightBoxOptions = useSelector(getLightBoxOptions);
const debugMenuModalState = useSelector(getDebugMenuModalState);
// NOTE the order of the modals is important for the z-index
return (
<>
{/* Screens */}
{sessionPasswordModalState && <SessionSetPasswordDialog {...sessionPasswordModalState} />}
{editProfileModalState && <EditProfileDialog {...editProfileModalState} />}
{onionPathModalState && <OnionPathModal {...onionPathModalState} />}
{reactListModalState && <ReactListModal {...reactListModalState} />}
{debugMenuModalState && <DebugMenuModal {...debugMenuModalState} />}
{/* Actions */}
{banOrUnbanUserModalState && <BanOrUnBanUserDialog {...banOrUnbanUserModalState} />}
{blockOrUnblockModalState && <BlockOrUnblockDialog {...blockOrUnblockModalState} />}
{inviteModalState && <InviteContactsDialog {...inviteModalState} />}
@ -80,13 +91,8 @@ export const ModalContainer = () => {
{updateGroupNameModalState && <UpdateGroupNameDialog {...updateGroupNameModalState} />}
{userDetailsModalState && <UserDetailsDialog {...userDetailsModalState} />}
{changeNicknameModal && <SessionNicknameDialog {...changeNicknameModal} />}
{editProfileModalState && <EditProfileDialog {...editProfileModalState} />}
{onionPathModalState && <OnionPathModal {...onionPathModalState} />}
{enterPasswordModalState && <EnterPasswordModal {...enterPasswordModalState} />}
{sessionPasswordModalState && <SessionSetPasswordDialog {...sessionPasswordModalState} />}
{deleteAccountModalState && <DeleteAccountModal {...deleteAccountModalState} />}
{confirmModalState && <SessionConfirm {...confirmModalState} />}
{reactListModalState && <ReactListModal {...reactListModalState} />}
{reactClearAllModalState && <ReactClearAllModal {...reactClearAllModalState} />}
{editProfilePictureModalState && (
<EditProfilePictureModal {...editProfilePictureModalState} />
@ -94,8 +100,10 @@ export const ModalContainer = () => {
{hideRecoveryPasswordModalState && (
<HideRecoveryPasswordDialog {...hideRecoveryPasswordModalState} />
)}
{openUrlModalState && <OpenUrlModal {...openUrlModalState} />}
{lightBoxOptions && <LightboxGallery {...lightBoxOptions} />}
{openUrlModalState && <OpenUrlModal {...openUrlModalState} />}
{/* Should be on top of all other modals */}
{confirmModalState && <SessionConfirm {...confirmModalState} />}
</>
);
};

@ -0,0 +1,60 @@
import { AnimatePresence } from 'framer-motion';
import styled from 'styled-components';
import { useDispatch } from 'react-redux';
import { Flex } from '../../basic/Flex';
import { SpacerMD, SpacerSM } from '../../basic/Text';
import { updateDebugMenuModal } from '../../../state/ducks/modalDialog';
import { AboutInfo, DebugActions, FeatureFlags, OtherInfo } from './components';
import { SessionWrapperModal } from '../../SessionWrapperModal';
const StyledContent = styled(Flex)`
padding-inline: var(--margins-sm);
h2 {
font-size: var(--font-size-xl);
}
h2,
h3 {
margin: var(--margins-md) 0;
padding: 0;
text-decoration: underline;
}
p,
i {
line-height: 1.4;
margin: 0;
padding: 0;
text-align: start;
}
`;
export function DebugMenuModal() {
const dispatch = useDispatch();
const onClose = () => {
dispatch(updateDebugMenuModal(null));
};
return (
<AnimatePresence>
<SessionWrapperModal title={'Debug Menu'} onClose={onClose} showExitIcon={true}>
<StyledContent
container={true}
flexDirection="column"
alignItems="flex-start"
padding="var(--margins-sm) 0"
>
<DebugActions />
<SpacerSM />
<FeatureFlags flags={window.sessionFeatureFlags} />
<SpacerMD />
<AboutInfo />
<OtherInfo />
<SpacerMD />
</StyledContent>
</SessionWrapperModal>
</AnimatePresence>
);
}

@ -0,0 +1,310 @@
import { isBoolean } from 'lodash';
import useUpdate from 'react-use/lib/useUpdate';
import useAsync from 'react-use/lib/useAsync';
import { shell } from 'electron';
import useBoolean from 'react-use/lib/useBoolean';
import type { SessionFeatureFlagsKeys } from '../../../window';
import { Flex } from '../../basic/Flex';
import { SessionToggle } from '../../basic/SessionToggle';
import { HintText, SpacerXS } from '../../basic/Text';
import { localize } from '../../../localization/localeTools';
import { CopyToClipboardIcon } from '../../buttons';
import { saveLogToDesktop } from '../../../util/logging';
import { Localizer } from '../../basic/Localizer';
import { SessionButton } from '../../basic/SessionButton';
import { ToastUtils, UserUtils } from '../../../session/utils';
import { getLatestReleaseFromFileServer } from '../../../session/apis/file_server_api/FileServerApi';
import { SessionSpinner } from '../../loading';
export const DebugActions = () => {
const [loadingLatestRelease, setLoadingLatestRelease] = useBoolean(false);
return (
<>
<h2>Actions</h2>
<SpacerXS />
<Flex
container={true}
width="100%"
justifyContent="flex-start"
alignItems="flex-start"
flexWrap="wrap"
flexGap="var(--margins-md) var(--margins-lg)"
>
<SessionButton
onClick={() => {
void saveLogToDesktop();
}}
>
<Localizer token="helpReportABugExportLogs" />
</SessionButton>
{window.getCommitHash() ? (
<SessionButton
onClick={() => {
void shell.openExternal(
`https://github.com/session-foundation/session-desktop/commit/${window.getCommitHash()}`
);
}}
>
Go to commit
</SessionButton>
) : null}
<SessionButton
onClick={() => {
void shell.openExternal(
`https://github.com/session-foundation/session-desktop/releases/tag/v${window.getVersion()}`
);
}}
>
<Localizer token="updateReleaseNotes" />
</SessionButton>
<SessionButton
onClick={async () => {
const userEd25519SecretKey = (await UserUtils.getUserED25519KeyPairBytes())
?.privKeyBytes;
if (!userEd25519SecretKey) {
window.log.error('[debugMenu] no userEd25519SecretKey');
return;
}
setLoadingLatestRelease(true);
const versionNumber = await getLatestReleaseFromFileServer(userEd25519SecretKey);
setLoadingLatestRelease(false);
if (versionNumber) {
ToastUtils.pushToastInfo('debugLatestRelease', `v${versionNumber}`);
} else {
ToastUtils.pushToastError('debugLatestRelease', 'Failed to fetch latest release');
}
}}
>
<SessionSpinner loading={loadingLatestRelease} color={'var(--text-primary-color)'} />
{!loadingLatestRelease ? 'Check latest release' : null}
</SessionButton>
</Flex>
</>
);
};
const unsupportedFlags = ['useTestNet'];
const untestedFlags = ['useOnionRequests', 'useClosedGroupV3', 'replaceLocalizedStringsWithKeys'];
const handleFeatureFlagToggle = async (
forceUpdate: () => void,
flag: SessionFeatureFlagsKeys,
parentFlag?: SessionFeatureFlagsKeys
) => {
const currentValue = parentFlag
? (window as any).sessionFeatureFlags[parentFlag][flag]
: (window as any).sessionFeatureFlags[flag];
if (parentFlag) {
(window as any).sessionFeatureFlags[parentFlag][flag] = !currentValue;
window.log.debug(`[debugMenu] toggled ${parentFlag}.${flag} to ${!currentValue}`);
} else {
(window as any).sessionFeatureFlags[flag] = !currentValue;
window.log.debug(`[debugMenu] toggled ${flag} to ${!currentValue}`);
}
forceUpdate();
};
const FlagToggle = ({
forceUpdate,
flag,
value,
parentFlag,
}: {
forceUpdate: () => void;
flag: SessionFeatureFlagsKeys;
value: any;
parentFlag?: SessionFeatureFlagsKeys;
}) => {
const key = `feature-flag-toggle${parentFlag ? `-${parentFlag}` : ''}-${flag}`;
return (
<Flex
key={key}
id={key}
container={true}
width="100%"
alignItems="center"
justifyContent="space-between"
>
<span>
{flag}
{untestedFlags.includes(flag) ? <HintText>Untested</HintText> : null}
</span>
<SessionToggle
active={value}
onClick={() => void handleFeatureFlagToggle(forceUpdate, flag, parentFlag)}
/>
</Flex>
);
};
export const FeatureFlags = ({ flags }: { flags: Record<string, any> }) => {
const forceUpdate = useUpdate();
return (
<Flex
container={true}
width={'100%'}
flexDirection="column"
justifyContent="flex-start"
alignItems="flex-start"
flexGap="var(--margins-xs)"
>
<Flex container={true} alignItems="center">
<h2>Feature Flags</h2>
<HintText>Experimental</HintText>
</Flex>
<i>
Changes are temporary. You can clear them by reloading the window or restarting the app.
</i>
<SpacerXS />
{Object.entries(flags).map(([key, value]) => {
const flag = key as SessionFeatureFlagsKeys;
if (unsupportedFlags.includes(flag)) {
return null;
}
if (!isBoolean(value)) {
return (
<>
<h3>{flag}</h3>
{Object.entries(value).map(([k, v]: [string, any]) => {
const nestedFlag = k as SessionFeatureFlagsKeys;
return (
<FlagToggle
forceUpdate={forceUpdate}
flag={nestedFlag}
value={v}
parentFlag={flag}
/>
);
})}
</>
);
}
return <FlagToggle forceUpdate={forceUpdate} flag={flag} value={value} />;
})}
</Flex>
);
};
export const AboutInfo = () => {
const environmentStates = [];
if (window.getEnvironment() !== 'production') {
environmentStates.push(window.getEnvironment());
}
if (window.getAppInstance()) {
environmentStates.push(window.getAppInstance());
}
const aboutInfo = [
`${localize('updateVersion').withArgs({ version: window.getVersion() })}`,
`${localize('systemInformationDesktop').withArgs({ information: window.getOSRelease() })}`,
`${localize('commitHashDesktop').withArgs({ hash: window.getCommitHash() || window.i18n('unknown') })}`,
`${environmentStates.join(' - ')}`,
];
return (
<Flex
container={true}
width={'100%'}
flexDirection="column"
justifyContent="flex-start"
alignItems="flex-start"
flexWrap="wrap"
>
<SpacerXS />
<Flex container={true} width="100%" alignItems="center" flexGap="var(--margins-xs)">
<h2>About</h2>
<CopyToClipboardIcon iconSize={'medium'} copyContent={aboutInfo.join('\n')} />
</Flex>
<Flex
container={true}
width="100%"
flexDirection="column"
justifyContent="space-between"
alignItems="center"
flexGap="var(--margins-xs)"
>
{aboutInfo.map((info, index) => (
<Flex
key={`debug-about-info-${index}`}
container={true}
width="100%"
alignItems="flex-start"
flexGap="var(--margins-xs)"
>
<p style={{ userSelect: 'text', lineHeight: 1.5 }}>{info}</p>
<CopyToClipboardIcon iconSize={'medium'} copyContent={info} />
</Flex>
))}
<SpacerXS />
</Flex>
</Flex>
);
};
export const OtherInfo = () => {
const otherInfo = useAsync(async () => {
const { id, vbid } = await window.getUserKeys();
return [`${localize('accountIdYours')}: ${id}`, `VBID: ${vbid}`];
}, []);
return (
<Flex
container={true}
width={'100%'}
flexDirection="column"
justifyContent="flex-start"
alignItems="flex-start"
flexWrap="wrap"
>
<SpacerXS />
<Flex container={true} width="100%" alignItems="center" flexGap="var(--margins-xs)">
<h2>Other Info</h2>
{otherInfo.value ? (
<CopyToClipboardIcon iconSize={'medium'} copyContent={otherInfo.value.join('\n')} />
) : null}
</Flex>
<Flex
container={true}
width="100%"
flexDirection="column"
justifyContent="space-between"
alignItems="center"
flexGap="var(--margins-xs)"
>
{otherInfo.loading ? (
<p style={{ userSelect: 'text' }}>{localize('loading')}</p>
) : otherInfo.error ? (
<p style={{ userSelect: 'text' }}>
<span style={{ color: 'var(--danger-color)' }}>
{localize('theError')}: {otherInfo.error.message || localize('errorUnknown')}
</span>
</p>
) : null}
{otherInfo.value
? otherInfo.value.map((info, index) => (
<Flex
key={`debug-other-info-${index}`}
container={true}
width="100%"
alignItems="flex-start"
flexGap="var(--margins-xs)"
>
<p style={{ userSelect: 'text', lineHeight: 1.5 }}>{info}</p>
<CopyToClipboardIcon iconSize={'medium'} copyContent={info} />
</Flex>
))
: null}
</Flex>
</Flex>
);
};

@ -54,6 +54,8 @@ export type LightBoxOptions = {
onClose?: () => void;
} | null;
export type DebugMenuModalState = object | null;
export type ModalState = {
confirmModal: ConfirmModalState;
inviteContactModal: InviteContactModalState;
@ -76,6 +78,7 @@ export type ModalState = {
hideRecoveryPasswordModalState: HideRecoveryPasswordModalState;
openUrlModal: OpenUrlModalState;
lightBoxOptions: LightBoxOptions;
debugMenuModal: DebugMenuModalState;
};
export const initialModalState: ModalState = {
@ -100,6 +103,7 @@ export const initialModalState: ModalState = {
hideRecoveryPasswordModalState: null,
openUrlModal: null,
lightBoxOptions: null,
debugMenuModal: null,
};
const ModalSlice = createSlice({
@ -183,6 +187,9 @@ const ModalSlice = createSlice({
return { ...state, lightBoxOptions };
},
updateDebugMenuModal(state, action: PayloadAction<DebugMenuModalState>) {
return { ...state, debugMenuModal: action.payload };
},
},
});
@ -209,5 +216,6 @@ export const {
updateHideRecoveryPasswordModal,
updateOpenUrlModal,
updateLightBoxOptions,
updateDebugMenuModal,
} = actions;
export const modalReducer = reducer;

@ -142,3 +142,5 @@ export const getLightBoxOptions = createSelector(
getModal,
(state: ModalState): LightBoxOptions => state.lightBoxOptions
);
export const getDebugMenuModalState = (state: StateType) => getModal(state).debugMenuModal;

Loading…
Cancel
Save