diff --git a/ts/components/dialog/ModalContainer.tsx b/ts/components/dialog/ModalContainer.tsx index 0eecfde9e..8341171e0 100644 --- a/ts/components/dialog/ModalContainer.tsx +++ b/ts/components/dialog/ModalContainer.tsx @@ -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 && } + {editProfileModalState && } + {onionPathModalState && } + {reactListModalState && } + {debugMenuModalState && } + {/* Actions */} {banOrUnbanUserModalState && } {blockOrUnblockModalState && } {inviteModalState && } @@ -80,13 +91,8 @@ export const ModalContainer = () => { {updateGroupNameModalState && } {userDetailsModalState && } {changeNicknameModal && } - {editProfileModalState && } - {onionPathModalState && } {enterPasswordModalState && } - {sessionPasswordModalState && } {deleteAccountModalState && } - {confirmModalState && } - {reactListModalState && } {reactClearAllModalState && } {editProfilePictureModalState && ( @@ -94,8 +100,10 @@ export const ModalContainer = () => { {hideRecoveryPasswordModalState && ( )} - {openUrlModalState && } {lightBoxOptions && } + {openUrlModalState && } + {/* Should be on top of all other modals */} + {confirmModalState && } ); }; diff --git a/ts/components/dialog/debug/DebugMenuModal.tsx b/ts/components/dialog/debug/DebugMenuModal.tsx new file mode 100644 index 000000000..b0b5d4d34 --- /dev/null +++ b/ts/components/dialog/debug/DebugMenuModal.tsx @@ -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 ( + + + + + + + + + + + + + + ); +} diff --git a/ts/components/dialog/debug/components.tsx b/ts/components/dialog/debug/components.tsx new file mode 100644 index 000000000..d7632fe88 --- /dev/null +++ b/ts/components/dialog/debug/components.tsx @@ -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 ( + <> +

Actions

+ + + { + void saveLogToDesktop(); + }} + > + + + + {window.getCommitHash() ? ( + { + void shell.openExternal( + `https://github.com/session-foundation/session-desktop/commit/${window.getCommitHash()}` + ); + }} + > + Go to commit + + ) : null} + + { + void shell.openExternal( + `https://github.com/session-foundation/session-desktop/releases/tag/v${window.getVersion()}` + ); + }} + > + + + + { + 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'); + } + }} + > + + {!loadingLatestRelease ? 'Check latest release' : null} + + + + ); +}; + +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 ( + + + {flag} + {untestedFlags.includes(flag) ? Untested : null} + + void handleFeatureFlagToggle(forceUpdate, flag, parentFlag)} + /> + + ); +}; + +export const FeatureFlags = ({ flags }: { flags: Record }) => { + const forceUpdate = useUpdate(); + return ( + + +

Feature Flags

+ Experimental +
+ + Changes are temporary. You can clear them by reloading the window or restarting the app. + + + {Object.entries(flags).map(([key, value]) => { + const flag = key as SessionFeatureFlagsKeys; + if (unsupportedFlags.includes(flag)) { + return null; + } + + if (!isBoolean(value)) { + return ( + <> +

{flag}

+ {Object.entries(value).map(([k, v]: [string, any]) => { + const nestedFlag = k as SessionFeatureFlagsKeys; + return ( + + ); + })} + + ); + } + return ; + })} +
+ ); +}; + +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 ( + + + +

About

+ +
+ + {aboutInfo.map((info, index) => ( + +

{info}

+ +
+ ))} + +
+
+ ); +}; + +export const OtherInfo = () => { + const otherInfo = useAsync(async () => { + const { id, vbid } = await window.getUserKeys(); + return [`${localize('accountIdYours')}: ${id}`, `VBID: ${vbid}`]; + }, []); + + return ( + + + +

Other Info

+ {otherInfo.value ? ( + + ) : null} +
+ + {otherInfo.loading ? ( +

{localize('loading')}

+ ) : otherInfo.error ? ( +

+ + {localize('theError')}: {otherInfo.error.message || localize('errorUnknown')} + +

+ ) : null} + {otherInfo.value + ? otherInfo.value.map((info, index) => ( + +

{info}

+ +
+ )) + : null} +
+
+ ); +}; diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index d19fc027e..787a78e33 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -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) { + return { ...state, debugMenuModal: action.payload }; + }, }, }); @@ -209,5 +216,6 @@ export const { updateHideRecoveryPasswordModal, updateOpenUrlModal, updateLightBoxOptions, + updateDebugMenuModal, } = actions; export const modalReducer = reducer; diff --git a/ts/state/selectors/modal.ts b/ts/state/selectors/modal.ts index bfb0913b3..2891b4038 100644 --- a/ts/state/selectors/modal.ts +++ b/ts/state/selectors/modal.ts @@ -142,3 +142,5 @@ export const getLightBoxOptions = createSelector( getModal, (state: ModalState): LightBoxOptions => state.lightBoxOptions ); + +export const getDebugMenuModalState = (state: StateType) => getModal(state).debugMenuModal;