session-desktop/ts/components/leftpane/ActionsPanel.tsx

300 lines
10 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useState } from 'react';
import { getConversationController } from '../../session/conversations';
import { syncConfigurationIfNeeded } from '../../session/utils/sync/syncUtils';
2021-05-25 05:19:34 +02:00
import { useDispatch, useSelector } from 'react-redux';
import {
Data,
hasSyncedInitialConfigurationItem,
2021-05-25 05:19:34 +02:00
lastAvatarUploadTimestamp,
} from '../../data/data';
import { getMessageQueue } from '../../session/sending';
// tslint:disable: no-submodule-imports
import useInterval from 'react-use/lib/useInterval';
import useTimeoutFn from 'react-use/lib/useTimeoutFn';
import { clearSearch } from '../../state/ducks/search';
import { resetOverlayMode, SectionType, showLeftPaneSection } from '../../state/ducks/section';
2021-03-16 07:45:36 +01:00
import {
getGlobalUnreadMessageCount,
getOurPrimaryConversation,
2021-03-16 07:45:36 +01:00
} from '../../state/selectors/conversations';
import { getFocusedSection } from '../../state/selectors/section';
import { getOurNumber } from '../../state/selectors/user';
2021-04-22 10:10:10 +02:00
2021-06-24 07:35:42 +02:00
import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachmentsManager';
2021-05-27 03:04:26 +02:00
import { DURATION } from '../../session/constants';
import { debounce, isEmpty, isString } from 'lodash';
import { uploadOurAvatar } from '../../interactions/conversationInteractions';
import { editProfileModal, onionPathModal } from '../../state/ducks/modalDialog';
2021-06-04 03:38:51 +02:00
2020-10-22 07:34:41 +02:00
// tslint:disable-next-line: no-import-side-effect no-submodule-imports
import { ipcRenderer } from 'electron';
import { loadDefaultRooms } from '../../session/apis/open_group_api/opengroupV2/ApiUtil';
import { getOpenGroupManager } from '../../session/apis/open_group_api/opengroupV2/OpenGroupManagerV2';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { UserUtils } from '../../session/utils';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { ActionPanelOnionStatusLight } from '../dialog/OnionStatusPathDialog';
import { SessionIconButton } from '../icon';
import { LeftPaneSectionContainer } from './LeftPaneSectionContainer';
import { getLatestReleaseFromFileServer } from '../../session/apis/file_server_api/FileServerApi';
import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool';
import { isDarkTheme } from '../../state/selectors/theme';
import { ThemeStateType } from '../../themes/constants/colors';
import { switchThemeTo } from '../../themes/switchTheme';
const Section = (props: { type: SectionType }) => {
2021-03-16 07:45:36 +01:00
const ourNumber = useSelector(getOurNumber);
const globalUnreadMessageCount = useSelector(getGlobalUnreadMessageCount);
2021-03-16 07:45:36 +01:00
const dispatch = useDispatch();
const { type } = props;
2021-03-16 07:45:36 +01:00
const isDarkMode = useSelector(isDarkTheme);
2021-03-16 07:45:36 +01:00
const focusedSection = useSelector(getFocusedSection);
const isSelected = focusedSection === props.type;
const handleClick = async () => {
2021-03-16 07:45:36 +01:00
/* tslint:disable:no-void-expression */
if (type === SectionType.Profile) {
dispatch(editProfileModal({}));
} else if (type === SectionType.ColorMode) {
const currentTheme = String(window.Events.getThemeSetting());
const newTheme = (isDarkMode
? currentTheme.replace('dark', 'light')
: currentTheme.replace('light', 'dark')) as ThemeStateType;
// We want to persist the primary color when using the color mode button
await switchThemeTo({
theme: newTheme,
mainWindow: true,
usePrimaryColor: true,
dispatch,
});
2021-05-12 06:12:05 +02:00
} else if (type === SectionType.PathIndicator) {
// Show Path Indicator Modal
2021-06-18 08:41:35 +02:00
dispatch(onionPathModal({}));
2021-03-16 07:45:36 +01:00
} else {
// message section
2021-03-16 07:45:36 +01:00
dispatch(clearSearch());
dispatch(showLeftPaneSection(type));
dispatch(resetOverlayMode());
2021-03-16 07:45:36 +01:00
}
};
if (type === SectionType.Profile) {
return (
<Avatar
size={AvatarSize.XS}
2021-03-16 07:45:36 +01:00
onAvatarClick={handleClick}
pubkey={ourNumber}
dataTestId="leftpane-primary-avatar"
2021-03-16 07:45:36 +01:00
/>
);
}
const unreadToShow = type === SectionType.Message ? globalUnreadMessageCount : undefined;
2021-03-16 07:45:36 +01:00
switch (type) {
case SectionType.Message:
return (
<SessionIconButton
iconSize="medium"
dataTestId="message-section"
iconType={'chatBubble'}
notificationCount={unreadToShow}
onClick={handleClick}
isSelected={isSelected}
/>
);
2021-03-16 07:45:36 +01:00
case SectionType.Settings:
return (
<SessionIconButton
iconSize="medium"
dataTestId="settings-section"
iconType={'gear'}
onClick={handleClick}
isSelected={isSelected}
/>
);
case SectionType.PathIndicator:
return (
<ActionPanelOnionStatusLight
dataTestId="onion-status-section"
handleClick={handleClick}
isSelected={isSelected}
id={'onion-path-indicator-led-id'}
/>
);
case SectionType.ColorMode:
2021-03-16 07:45:36 +01:00
default:
return (
<SessionIconButton
iconSize="medium"
iconType={isDarkMode ? 'moon' : 'sun'}
dataTestId="theme-section"
onClick={handleClick}
isSelected={isSelected}
/>
);
}
2021-03-16 07:45:36 +01:00
};
const cleanUpMediasInterval = DURATION.MINUTES * 60;
// every 1 minute we fetch from the fileserver to check for a new release
// * if there is none, no request to github are made.
// * if there is a version on the fileserver more recent than our current, we fetch github to get the UpdateInfos and trigger an update as usual (asking user via dialog)
const fetchReleaseFromFileServerInterval = 1000 * 60; // try to fetch the latest release from the fileserver every minute
2021-04-13 06:10:31 +02:00
const setupTheme = async () => {
2021-04-30 05:26:33 +02:00
const theme = window.Events.getThemeSetting();
// We don't want to reset the primary color on startup
await switchThemeTo({
theme,
mainWindow: true,
usePrimaryColor: true,
dispatch: window?.inboxStore?.dispatch || undefined,
});
2021-04-30 05:26:33 +02:00
};
// Do this only if we created a new Session ID, or if we already received the initial configuration message
2021-05-06 08:02:47 +02:00
const triggerSyncIfNeeded = async () => {
const us = UserUtils.getOurPubKeyStrFromCache();
await getConversationController()
.get(us)
.setDidApproveMe(true, true);
await getConversationController()
.get(us)
.setIsApproved(true, true);
2021-04-30 05:26:33 +02:00
const didWeHandleAConfigurationMessageAlready =
(await Data.getItemById(hasSyncedInitialConfigurationItem))?.value || false;
2021-04-30 05:26:33 +02:00
if (didWeHandleAConfigurationMessageAlready) {
await syncConfigurationIfNeeded();
}
};
2021-05-25 05:19:34 +02:00
const triggerAvatarReUploadIfNeeded = async () => {
const lastTimeStampAvatarUpload = (await Data.getItemById(lastAvatarUploadTimestamp))?.value || 0;
2021-05-25 05:19:34 +02:00
2021-05-27 03:04:26 +02:00
if (Date.now() - lastTimeStampAvatarUpload > DURATION.DAYS * 14) {
2021-05-25 05:19:34 +02:00
window.log.info('Reuploading avatar...');
// reupload the avatar
2021-06-21 03:42:25 +02:00
await uploadOurAvatar();
2021-05-25 05:19:34 +02:00
}
};
2021-04-30 05:19:46 +02:00
/**
* This function is called only once: on app startup with a logged in user
*/
const doAppStartUp = async () => {
void setupTheme();
2021-04-30 05:26:33 +02:00
// this generates the key to encrypt attachments locally
await Data.generateAttachmentKeyIfEmpty();
2021-04-30 05:26:33 +02:00
// trigger a sync message if needed for our other devices
void triggerSyncIfNeeded();
void getSwarmPollingInstance().start();
2021-04-30 05:19:46 +02:00
void loadDefaultRooms();
// TODOLATER make this a job of the JobRunner
2021-05-25 05:19:34 +02:00
debounce(triggerAvatarReUploadIfNeeded, 200);
/* Postpone a little bit of the polling of sogs messages to let the swarm messages come in first. */
global.setTimeout(() => {
void getOpenGroupManager().startPolling();
}, 10000);
global.setTimeout(() => {
// init the messageQueue. In the constructor, we add all not send messages
// this call does nothing except calling the constructor, which will continue sending message in the pipeline
void getMessageQueue().processAllPending();
}, 3000);
2021-04-30 05:19:46 +02:00
};
async function fetchReleaseFromFSAndUpdateMain() {
try {
window.log.info('[updater] about to fetchReleaseFromFSAndUpdateMain');
const latest = await getLatestReleaseFromFileServer();
window.log.info('[updater] fetched latest release from fileserver: ', latest);
if (isString(latest) && !isEmpty(latest)) {
ipcRenderer.send('set-release-from-file-server', latest);
window.readyForUpdates();
}
} catch (e) {
window.log.warn(e);
}
}
/**
* ActionsPanel is the far left banner (not the left pane).
* The panel with buttons to switch between the message/contact/settings/theme views
*/
2021-03-16 07:45:36 +01:00
export const ActionsPanel = () => {
2021-04-13 06:10:31 +02:00
const [startCleanUpMedia, setStartCleanUpMedia] = useState(false);
2021-03-16 07:45:36 +01:00
const ourPrimaryConversation = useSelector(getOurPrimaryConversation);
2021-03-16 07:45:36 +01:00
// this maxi useEffect is called only once: when the component is mounted.
2021-04-13 06:10:31 +02:00
// For the action panel, it means this is called only one per app start/with a user loggedin
2021-03-16 07:45:36 +01:00
useEffect(() => {
void doAppStartUp();
2021-03-16 07:45:36 +01:00
}, []);
2021-02-05 06:29:37 +01:00
2021-04-13 06:10:31 +02:00
// wait for cleanUpMediasInterval and then start cleaning up medias
// this would be way easier to just be able to not trigger a call with the setInterval
useEffect(() => {
2021-08-04 02:52:24 +02:00
const timeout = setTimeout(() => setStartCleanUpMedia(true), cleanUpMediasInterval);
2021-04-13 06:10:31 +02:00
2021-08-04 02:52:24 +02:00
return () => clearTimeout(timeout);
2021-04-13 06:10:31 +02:00
}, []);
useInterval(cleanUpOldDecryptedMedias, startCleanUpMedia ? cleanUpMediasInterval : null);
2021-04-13 06:10:31 +02:00
useInterval(() => {
void fetchReleaseFromFSAndUpdateMain();
}, fetchReleaseFromFileServerInterval);
2021-03-16 07:45:36 +01:00
if (!ourPrimaryConversation) {
window?.log?.warn('ActionsPanel: ourPrimaryConversation is not set');
2021-11-08 01:03:08 +01:00
return null;
2021-02-05 06:29:37 +01:00
}
2021-03-16 07:45:36 +01:00
useInterval(() => {
void syncConfigurationIfNeeded();
2021-05-27 03:04:26 +02:00
}, DURATION.DAYS * 2);
useInterval(() => {
// trigger an updates from the snodes every hour
void forceRefreshRandomSnodePool();
}, DURATION.HOURS * 1);
useTimeoutFn(() => {
// trigger an updates from the snodes after 5 minutes, once
void forceRefreshRandomSnodePool();
}, DURATION.MINUTES * 5);
2021-05-25 05:19:34 +02:00
useInterval(() => {
// this won't be run every days, but if the app stays open for more than 10 days
void triggerAvatarReUploadIfNeeded();
2021-05-27 03:04:26 +02:00
}, DURATION.DAYS * 1);
2021-05-25 05:19:34 +02:00
2021-03-16 07:45:36 +01:00
return (
<>
<LeftPaneSectionContainer data-testid="leftpane-section-container">
<Section type={SectionType.Profile} />
<Section type={SectionType.Message} />
<Section type={SectionType.Settings} />
2021-06-18 08:41:35 +02:00
<Section type={SectionType.PathIndicator} />
<Section type={SectionType.ColorMode} />
</LeftPaneSectionContainer>
</>
2021-03-16 07:45:36 +01:00
);
};