Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add updates downloading status and progress in UI and replace popups #3089

Open
wants to merge 3 commits into
base: unstable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -579,5 +579,7 @@
"duration": "Duration",
"notApplicable": "N/A",
"unknownError": "Unknown Error",
"displayNameErrorNew": "We were unable to load your display name. Please enter a new display name to continue."
"displayNameErrorNew": "We were unable to load your display name. Please enter a new display name to continue.",
"updateDownloadProgress": "Downloading update: $progress$%",
"updateDownloadedRestart": "Update downloaded. Click to install"
}
4 changes: 4 additions & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ window.setAutoUpdateEnabled = value => {
ipc.send('set-auto-update-setting', !!value);
};

window.autoupdaterCancelDownload = () => ipc.send('autoupdater-cancel-download');
window.autoupdaterAcceptDownload = () => ipc.send('autoupdater-accept-download');
window.autoupdaterInstallAndRestart = () => ipc.send('autoupdater-install-and-restart');

ipc.on('get-ready-for-shutdown', async () => {
const { shutdown } = window.Events || {};
if (!shutdown) {
Expand Down
2 changes: 2 additions & 0 deletions ts/components/SessionInboxView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { initialSectionState } from '../state/ducks/section';
import { getEmptyStagedAttachmentsState } from '../state/ducks/stagedAttachments';
import { initialThemeState } from '../state/ducks/theme';
import { initialUserConfigState } from '../state/ducks/userConfig';
import { initialAppUpdatesState } from '../state/ducks/appUpdates';
import { StateType } from '../state/reducer';
import { SessionMainPanel } from './SessionMainPanel';

Expand Down Expand Up @@ -83,6 +84,7 @@ function createSessionInboxStore() {
call: initialCallState,
sogsRoomInfo: initialSogsRoomInfoState,
settings: getSettingsInitialState(),
appUpdates: initialAppUpdatesState,
};

return createStore(initialState);
Expand Down
6 changes: 6 additions & 0 deletions ts/components/icon/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type SessionIconType =
| 'circle'
| 'circleCheck'
| 'doubleCheckCircleFilled'
| 'close'
| 'circlePlus'
| 'circleElipses'
| 'contacts'
Expand Down Expand Up @@ -149,6 +150,11 @@ export const icons: Record<string, { path: string; viewBox: string; ratio: numbe
viewBox: '1.5 5.5 21 12',
ratio: 1,
},
close: {
path: 'M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z',
viewBox: '0 0 24 24',
ratio: 1,
},
circle: {
path: '\
M 0, 50\
Expand Down
4 changes: 4 additions & 0 deletions ts/components/icon/SessionIconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import styled from 'styled-components';
import { SessionIcon, SessionIconProps } from './SessionIcon';

interface SProps extends SessionIconProps {
onPointerOver?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
onPointerOut?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
isSelected?: boolean;
isHidden?: boolean;
Expand Down Expand Up @@ -85,6 +87,8 @@ const SessionIconButtonInner = React.forwardRef<HTMLButtonElement, SProps>((prop
tabIndex={tabIndex}
onKeyPress={keyPressHandler}
data-testid={dataTestId}
onPointerOver={props.onPointerOver}
onPointerOut={props.onPointerOut}
>
<SessionIcon
iconType={iconType}
Expand Down
10 changes: 10 additions & 0 deletions ts/components/leftpane/ActionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { debounce, isEmpty, isString } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import useInterval from 'react-use/lib/useInterval';
import useTimeoutFn from 'react-use/lib/useTimeoutFn';
import styled from 'styled-components';

import { Data } from '../../data/data';
import { getConversationController } from '../../session/conversations';
Expand Down Expand Up @@ -49,6 +50,7 @@ import { switchThemeTo } from '../../themes/switchTheme';
import { ReleasedFeatures } from '../../util/releaseFeature';
import { getOppositeTheme } from '../../util/theme';
import { SessionNotificationCount } from '../icon/SessionNotificationCount';
import { UpdateStatus } from './UpdateStatus';

const Section = (props: { type: SectionType }) => {
const ourNumber = useSelector(getOurNumber);
Expand Down Expand Up @@ -120,6 +122,8 @@ const Section = (props: { type: SectionType }) => {
isSelected={isSelected}
/>
);
case SectionType.AppUpdateStatus:
return <UpdateStatus />;
case SectionType.PathIndicator:
return (
<ActionPanelOnionStatusLight
Expand All @@ -142,6 +146,10 @@ const Section = (props: { type: SectionType }) => {
}
};

const SpacerFlex = styled.div`
flex: 1;
`;

const cleanUpMediasInterval = DURATION.MINUTES * 60;

// every 1 minute we fetch from the fileserver to check for a new release
Expand Down Expand Up @@ -321,6 +329,8 @@ export const ActionsPanel = () => {
<Section type={SectionType.Profile} />
<Section type={SectionType.Message} />
<Section type={SectionType.Settings} />
<SpacerFlex />
<Section type={SectionType.AppUpdateStatus} />
<Section type={SectionType.PathIndicator} />
<Section type={SectionType.ColorMode} />
</LeftPaneSectionContainer>
Expand Down
1 change: 0 additions & 1 deletion ts/components/leftpane/LeftPaneSectionContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export const LeftPaneSectionContainer = styled.div`

// this is not ideal but it seems that nth-0last-child does not work
#onion-path-indicator-led-id {
margin: auto auto 0px auto;
opacity: 1;
}
`;
237 changes: 237 additions & 0 deletions ts/components/leftpane/UpdateStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import React from 'react';
import ReactDOM from 'react-dom';
import styled, { keyframes } from 'styled-components';
import { useSelector } from 'react-redux';
import { SessionIconButton } from '../icon';
import {
getAppUpdateDownloadProgress,
getAppUpdatesStatus,
} from '../../state/selectors/appUpdates';

const StyledActionsPanelItem = styled.div<{ hover: boolean }>`
position: relative;
padding: 30px 20px;
height: 75px;

--update-button-color: ${props => (props.hover ? '#00F782' : 'var(--text-primary-color)')};

& > span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 32px;
}

& > button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 32px;
height: 32px;
padding: 0 !important;
align-items: center;
justify-content: center;
border-radius: 999px;
}
`;

const rotate = keyframes`
from {
transform: rotate(0deg) translate(-50%, -50%);
}
to {
transform: rotate(360deg) translate(-50%, -50%);
}
`;

const StyledProgressPie = styled.span<{
progressPercentage: number;
insetPercentage: number;
hoverable: boolean;
}>`
position: absolute;
display: block;
border-radius: 50%;
${props =>
props.hoverable
? `
background-color: var(--update-button-color);
transition: var(--default-duration);
`
: `
background-image: conic-gradient(
var(--text-primary-color) 0% ${props.progressPercentage}%,
transparent 0% 0%
);
`}
position: relative;
mask:
radial-gradient(farthest-side, #000 calc(100% - 0.5px), #0000) center /
${props => props.insetPercentage}% ${props => props.insetPercentage}% no-repeat,
linear-gradient(#000 0 0);
mask-composite: destination-out;
animation: ${rotate} 1s linear infinite;
transform-origin: top left;
`;

const StyledTooltip = styled.div<{
visible: boolean;
top: number;
left: number;
}>`
position: absolute;
top: min(${props => props.top}px, calc(100% - 29px));
left: min(${props => props.left}px, calc(100% - 200px));
transform: translateY(-50%);
background-color: var(--right-panel-item-background-color);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
opacity: ${props => (props.visible ? 1 : 0)};
transition: opacity 0.2s;
color: var(--text-primary-color);
padding: 4px 14px;
border-radius: 99px;
width: max-width;
font-weight: 400;
text-size: 14px;

&:after {
content: '';
position: absolute;
left: -7px;
width: 10px;
height: 10px;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
border-right: 8px solid var(--right-panel-item-background-color);
top: 50%;
transform: translateY(-50%);
}
`;

export const UpdateStatus = () => {
const [hover, setHover] = React.useState(false);
const [tooltip, setTooltip] = React.useState<{ visible: boolean; top: number; left: number }>({
visible: false,
top: 0,
left: 0,
});
const progress = useSelector(getAppUpdateDownloadProgress);
const updateStatus = useSelector(getAppUpdatesStatus);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const initialPrompted = React.useRef(false);
const initialPromptedTimer = React.useRef(false);

const progressPercentageNormalized = React.useMemo(() => Math.floor(progress * 100), [progress]);

const handlePointerOver = React.useCallback(() => {
if (!buttonRef.current) {
return;
}
if (initialPromptedTimer.current === true) {
initialPromptedTimer.current = false;
}

setHover(true);
const { top, left, width, height } = buttonRef.current.getBoundingClientRect();
setTooltip({
visible: true,
top: top + height / 2,
left: left + width + 10,
});
}, []);

const handlePointerOut = React.useCallback(() => {
setHover(false);
setTooltip({ visible: false, top: tooltip.top, left: tooltip.left });
}, [tooltip]);

const tooltipText = React.useMemo(() => {
if (updateStatus === 'UPDATE_DOWNLOADED') {
return window.i18n('updateDownloadedRestart');
}
if (updateStatus === 'UPDATE_DOWNLOADING') {
return window.i18n('updateDownloadProgress', [String(progressPercentageNormalized)]);
}
if (updateStatus === 'UPDATE_AVAILABLE') {
return window.i18n('autoUpdateNewVersionMessage');
}
return '';
}, [updateStatus, progressPercentageNormalized]);

React.useEffect(() => {
if (updateStatus === 'UPDATE_AVAILABLE' && buttonRef.current && !initialPrompted.current) {
const { top, left, width, height } = buttonRef.current.getBoundingClientRect();
const position = { top: top + height / 2, left: left + width + 10 };
initialPrompted.current = true;
setTooltip({ visible: true, ...position });
initialPromptedTimer.current = true;
setTimeout(() => {
if (initialPromptedTimer.current === true) {
setTooltip({ visible: false, ...position });
}
}, 5 * 1000);
}
}, [updateStatus, buttonRef]);

if (updateStatus === 'NO_UPDATE_AVAILABLE') {
return null;
}

const handleClick = () => {
if (updateStatus === 'UPDATE_DOWNLOADED') {
window.autoupdaterInstallAndRestart();
}
if (updateStatus === 'UPDATE_AVAILABLE') {
window.autoupdaterAcceptDownload();
}
if (updateStatus === 'UPDATE_DOWNLOADING') {
window.autoupdaterCancelDownload();
}
};

const pointerHandlers = {
onPointerOver: handlePointerOver,
onPointerOut: handlePointerOut,
onClick: handleClick,
};

return (
<StyledActionsPanelItem hover={hover}>
{(updateStatus === 'UPDATE_DOWNLOADING' || updateStatus === 'UPDATE_DOWNLOADED') && (
<StyledProgressPie
progressPercentage={
updateStatus === 'UPDATE_DOWNLOADED' ? 100 : Math.max(0.03, progress) * 100
}
insetPercentage={80}
hoverable={updateStatus === 'UPDATE_DOWNLOADED'}
/>
)}
{updateStatus === 'UPDATE_AVAILABLE' || updateStatus === 'UPDATE_DOWNLOADED' ? (
<SessionIconButton
iconSize={updateStatus === 'UPDATE_AVAILABLE' ? 'huge' : 'small'}
iconType="save"
iconColor={
updateStatus === 'UPDATE_AVAILABLE'
? '#00F782'
: updateStatus === 'UPDATE_DOWNLOADED'
? 'var(--update-button-color)'
: undefined
}
{...pointerHandlers}
ref={buttonRef}
/>
) : (
<SessionIconButton iconSize="small" iconType="close" {...pointerHandlers} ref={buttonRef} />
)}
{ReactDOM.createPortal(
<StyledTooltip visible={tooltip.visible} top={tooltip.top} left={tooltip.left}>
{tooltipText}
</StyledTooltip>,
document.body.querySelector('#root') as Element
)}
</StyledActionsPanelItem>
);
};
13 changes: 13 additions & 0 deletions ts/mains/main_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ import { setLastestRelease } from '../node/latest_desktop_release';
import { load as loadLocale, LocaleMessagesWithNameType } from '../node/locale';
import { isDevProd, isTestIntegration } from '../shared/env_vars';
import { classicDark } from '../themes';
import { acceptDownload, cancelUpdate, installUpdateAndRestart } from '../updater/updater';

// Both of these will be set after app fires the 'ready' event
let logger: Logger | null = null;
Expand Down Expand Up @@ -1150,6 +1151,18 @@ ipc.on('get-native-theme', event => {
event.sender.send('send-native-theme', nativeTheme.shouldUseDarkColors);
});

ipc.on('autoupdater-cancel-download', () => {
void cancelUpdate(getMainWindow, assertLogger());
});

ipc.on('autoupdater-accept-download', () => {
void acceptDownload(getMainWindow, assertLogger());
});

ipc.on('autoupdater-install-and-restart', () => {
void installUpdateAndRestart(assertLogger());
});

nativeTheme.on('updated', () => {
// Inform all renderer processes of the theme change
mainWindow?.webContents.send('native-theme-update', nativeTheme.shouldUseDarkColors);
Expand Down
Loading