Display "days ago" in loading screen

This commit is contained in:
Fedor Indutny 2023-03-28 13:31:24 -07:00 committed by GitHub
parent c02c8d9640
commit 3264c3d509
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 316 additions and 66 deletions

View file

@ -12,6 +12,11 @@
// eslint-disable-next-line
const noop = () => {};
window.Whisper = window.Whisper || {};
window.Whisper.events = {
on: noop,
};
window.SignalWindow = window.SignalWindow || {};
window.SignalWindow.log = {
fatal: console.error.bind(console),

View file

@ -47,8 +47,10 @@ const withModeAndThemeProvider = (Story, context) => {
// Adding it to the body as well so that we can cover modals and other
// components that are rendered outside of this decorator container
if (theme === 'light') {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
} else {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
}

View file

@ -397,6 +397,10 @@
},
"loadingMessages": {
"message": "Loading messages. $count$ so far...",
"description": "(deleted 03/25/2023) Message shown on the loading screen when we're catching up on the backlog of messages"
},
"icu:loadingMessages": {
"messageformat": "Loading messages from {daysAgo, plural, one {1 day} other {# days}} ago...",
"description": "Message shown on the loading screen when we're catching up on the backlog of messages"
},
"view": {

View file

@ -1821,7 +1821,7 @@ app.on('ready', async () => {
loadingWindow = new BrowserWindow({
show: false,
width: 300,
height: 265,
height: 280,
resizable: false,
frame: false,
backgroundColor,

View file

@ -100,15 +100,11 @@
<div class="app-loading-screen app-loading-screen--without-titlebar">
<div class="module-title-bar-drag-area"></div>
<div class="content">
<div class="module-splash-screen__logo module-img--150"></div>
<div class="container">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<div class="message">&nbsp;</div>
<div class="module-splash-screen__logo module-img--150"></div>
<div class="app-loading-screen__progress--container">
<div class="app-loading-screen__progress--bar"></div>
</div>
<div class="message">&nbsp;</div>
</div>
</div>

View file

@ -25,15 +25,13 @@
</head>
<body>
<div class="app-migration-screen app-loading-screen">
<div class="content">
<div class="module-splash-screen__logo module-img--150"></div>
<div class="container">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<div id="message"></div>
<div class="module-splash-screen__logo module-img--150"></div>
<div class="container">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
<div id="message"></div>
</div>
<script type="text/javascript" src="ts/windows/loading/start.js"></script>
</body>

View file

@ -208,7 +208,7 @@ $loading-height: 16px;
opacity: 1;
}
100% {
opacity: 0;
opacity: 0.3;
}
}
@ -224,6 +224,7 @@ $loading-height: 16px;
right: 0;
top: 0;
bottom: 0;
padding: 0 16px;
&--without-titlebar {
/* There is no titlebar during loading screen on Windows */
@ -242,41 +243,58 @@ $loading-height: 16px;
color: $color-white;
display: flex;
flex-direction: column;
align-items: stretch;
align-items: center;
justify-content: center;
user-select: none;
.content {
text-align: center;
}
/* Currently only used in loading window */
.container {
margin-left: auto;
margin-right: auto;
width: 78px;
height: 22px;
display: flex;
gap: 7px;
margin: 8px 0 24px 0;
.dot {
width: 14px;
height: 14px;
border: 3px solid $color-white;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms;
&:nth-child(2) {
animation: loading 1500ms ease infinite 333ms;
}
&:nth-child(3) {
animation: loading 1500ms ease infinite 666ms;
}
}
}
&__progress {
&--container {
background: $color-white-alpha-20;
border-radius: 2px;
height: 4px;
max-width: 400px;
overflow: hidden;
width: 100%;
margin: 16px 0 24px 0;
}
&--bar {
background: $color-white;
border-radius: 2px;
display: block;
height: 100%;
width: 100%;
transform: translateX(-100%);
transition: transform 500ms ease-out;
}
}
.message {
max-width: 35em;
margin-left: auto;
margin-right: auto;
}
.dot {
width: 14px;
height: 14px;
border: 3px solid $color-white;
border-radius: 50%;
float: left;
margin: 0 6px;
transform: scale(0);
animation: loading 1500ms ease infinite 0ms;
&:nth-child(2) {
animation: loading 1500ms ease infinite 333ms;
}
&:nth-child(3) {
animation: loading 1500ms ease infinite 666ms;
}
}
}

View file

@ -25,7 +25,7 @@
.module-splash-screen__logo {
@include color-svg('../images/signal-logo.svg', $color-white);
margin: 24px auto;
margin: 24px 0;
&.module-img--256 {
height: 256px;

View file

@ -1181,6 +1181,7 @@ export async function startApp(): Promise<void> {
actionCreators.crashReports,
store.dispatch
),
inbox: bindActionCreators(actionCreators.inbox, store.dispatch),
emojis: bindActionCreators(actionCreators.emojis, store.dispatch),
expiration: bindActionCreators(actionCreators.expiration, store.dispatch),
globalModals: bindActionCreators(
@ -2917,9 +2918,19 @@ export async function startApp(): Promise<void> {
maxSize: Infinity,
});
const throttledSetInboxEnvelopeTimestamp = throttle(
serverTimestamp => {
window.reduxActions.inbox.setInboxEnvelopeTimestamp(serverTimestamp);
},
100,
{ leading: false }
);
async function onEnvelopeReceived({
envelope,
}: EnvelopeEvent): Promise<void> {
throttledSetInboxEnvelopeTimestamp(envelope.serverTimestamp);
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) {
const { mergePromises, conversation } =

View file

@ -0,0 +1,96 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useEffect, useMemo } from 'react';
import type { Meta, Story } from '@storybook/react';
import { noop } from 'lodash';
import { Inbox } from './Inbox';
import type { PropsType } from './Inbox';
import { DAY, SECOND } from '../util/durations';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/Inbox',
argTypes: {
i18n: {
defaultValue: i18n,
},
hasInitialLoadCompleted: {
defaultValue: false,
},
daysAgo: {
control: 'select',
defaultValue: undefined,
options: [undefined, 1, 2, 3, 7, 14, 21],
},
isCustomizingPreferredReactions: {
defaultValue: false,
},
onConversationClosed: {
action: true,
},
onConversationOpened: {
action: true,
},
scrollToMessage: {
action: true,
},
showConversation: {
action: true,
},
showWhatsNewModal: {
action: true,
},
},
} as Meta;
// eslint-disable-next-line react/function-component-definition
const Template: Story<PropsType & { daysAgo?: number }> = ({
daysAgo,
...args
}) => {
const now = useMemo(() => Date.now(), []);
const [offset, setOffset] = useState(0);
useEffect(() => {
if (daysAgo === undefined) {
setOffset(0);
return noop;
}
const interval = setInterval(() => {
setOffset(prevValue => (prevValue + 1 / 4) % daysAgo);
}, SECOND / 10);
return () => clearInterval(interval);
}, [now, daysAgo]);
const firstEnvelopeTimestamp =
daysAgo === undefined ? undefined : now - daysAgo * DAY;
const envelopeTimestamp =
firstEnvelopeTimestamp === undefined
? undefined
: firstEnvelopeTimestamp + offset * DAY;
return (
<Inbox
{...args}
firstEnvelopeTimestamp={firstEnvelopeTimestamp}
envelopeTimestamp={envelopeTimestamp}
renderConversationView={() => <div />}
renderCustomizingPreferredReactionsModal={() => <div />}
renderLeftPane={() => <div />}
renderMiniPlayer={() => <div />}
/>
);
};
export const Default = Template.bind({});
Default.story = {
name: 'Default',
};

View file

@ -8,7 +8,7 @@ import type { ShowConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import * as log from '../logging/log';
import { SECOND } from '../util/durations';
import { SECOND, DAY } from '../util/durations';
import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed';
import { WhatsNewLink } from './WhatsNewLink';
import { showToast } from '../util/showToast';
@ -17,6 +17,8 @@ import { TargetedMessageSource } from '../state/ducks/conversationsEnums';
import { usePrevious } from '../hooks/usePrevious';
export type PropsType = {
firstEnvelopeTimestamp: number | undefined;
envelopeTimestamp: number | undefined;
hasInitialLoadCompleted: boolean;
i18n: LocalizerType;
isCustomizingPreferredReactions: boolean;
@ -35,6 +37,8 @@ export type PropsType = {
};
export function Inbox({
firstEnvelopeTimestamp,
envelopeTimestamp,
hasInitialLoadCompleted,
i18n,
isCustomizingPreferredReactions,
@ -51,7 +55,6 @@ export function Inbox({
showConversation,
showWhatsNewModal,
}: PropsType): JSX.Element {
const [loadingMessageCount, setLoadingMessageCount] = useState(0);
const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] =
useState(hasInitialLoadCompleted);
@ -123,13 +126,11 @@ export function Inbox({
showToast(ToastStickerPackInstallFailed);
}
window.Whisper.events.on('loadingProgress', setLoadingMessageCount);
window.Whisper.events.on('pack-install-failed', packInstallFailed);
window.Whisper.events.on('refreshConversation', refreshConversation);
window.Whisper.events.on('setupAsNewDevice', unload);
return () => {
window.Whisper.events.off('loadingProgress', setLoadingMessageCount);
window.Whisper.events.off('pack-install-failed', packInstallFailed);
window.Whisper.events.off('refreshConversation', refreshConversation);
window.Whisper.events.off('setupAsNewDevice', unload);
@ -175,26 +176,45 @@ export function Inbox({
}, [hasInitialLoadCompleted]);
if (!internalHasInitialLoadCompleted) {
const now = Date.now();
let loadingProgress = 0;
if (
firstEnvelopeTimestamp !== undefined &&
envelopeTimestamp !== undefined
) {
loadingProgress =
Math.max(
0,
Math.min(
1,
Math.max(0, envelopeTimestamp - firstEnvelopeTimestamp) /
Math.max(1e-23, now - firstEnvelopeTimestamp)
)
) * 100;
}
return (
<div className="app-loading-screen">
<div className="module-title-bar-drag-area" />
<div className="content">
<div className="module-splash-screen__logo module-img--150" />
<div className="container">
<span className="dot" />
<span className="dot" />
<span className="dot" />
</div>
<div className="message">
{loadingMessageCount
? i18n('loadingMessages', {
count: String(loadingMessageCount),
})
: i18n('loading')}
</div>
<div id="toast" />
<div className="module-splash-screen__logo module-img--150" />
<div className="app-loading-screen__progress--container">
<div
className="app-loading-screen__progress--bar"
style={{ transform: `translateX(${loadingProgress - 100}%)` }}
/>
</div>
<div className="message">
{envelopeTimestamp
? i18n('icu:loadingMessages', {
daysAgo: Math.max(
1,
Math.round((now - envelopeTimestamp) / DAY)
),
})
: i18n('loading')}
</div>
<div id="toast" />
</div>
);
}

View file

@ -13,6 +13,7 @@ import { actions as crashReports } from './ducks/crashReports';
import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration';
import { actions as globalModals } from './ducks/globalModals';
import { actions as inbox } from './ducks/inbox';
import { actions as items } from './ducks/items';
import { actions as lightbox } from './ducks/lightbox';
import { actions as linkPreviews } from './ducks/linkPreviews';
@ -42,6 +43,7 @@ export const actionCreators: ReduxActions = {
emojis,
expiration,
globalModals,
inbox,
items,
lightbox,
linkPreviews,
@ -71,6 +73,7 @@ export const mapDispatchToProps = {
...emojis,
...expiration,
...globalModals,
...inbox,
...items,
...lightbox,
...linkPreviews,

83
ts/state/ducks/inbox.ts Normal file
View file

@ -0,0 +1,83 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
// State
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type InboxStateType = Readonly<{
firstEnvelopeTimestamp: number | undefined;
envelopeTimestamp: number | undefined;
}>;
// Actions
const SET_ENVELOPE_TIMESTAMP = 'INBOX/SET_INBOX_ENVELOPE_TIMESTAMP';
type SetInboxEnvelopeTimestampActionType = ReadonlyDeep<{
type: typeof SET_ENVELOPE_TIMESTAMP;
payload: {
envelopeTimestamp: number | undefined;
};
}>;
export type InboxActionType = ReadonlyDeep<SetInboxEnvelopeTimestampActionType>;
// Action Creators
export const actions = {
setInboxEnvelopeTimestamp,
};
function setInboxEnvelopeTimestamp(
envelopeTimestamp: number | undefined
): SetInboxEnvelopeTimestampActionType {
return {
type: SET_ENVELOPE_TIMESTAMP,
payload: { envelopeTimestamp },
};
}
// Reducer
export function getEmptyState(): InboxStateType {
return {
firstEnvelopeTimestamp: undefined,
envelopeTimestamp: undefined,
};
}
export function reducer(
state: Readonly<InboxStateType> = getEmptyState(),
action: Readonly<InboxActionType>
): InboxStateType {
if (!state) {
return getEmptyState();
}
if (action.type === SET_ENVELOPE_TIMESTAMP) {
const { payload } = action;
const { envelopeTimestamp: providedTimestamp } = payload;
// Ensure monotonicity
let { envelopeTimestamp } = state;
if (providedTimestamp !== undefined) {
envelopeTimestamp = Math.max(
providedTimestamp,
envelopeTimestamp ?? providedTimestamp
);
}
const firstEnvelopeTimestamp =
state.firstEnvelopeTimestamp ?? envelopeTimestamp;
return {
...state,
envelopeTimestamp,
firstEnvelopeTimestamp,
};
}
return state;
}

View file

@ -11,6 +11,7 @@ import { getEmptyState as conversations } from './ducks/conversations';
import { getEmptyState as crashReports } from './ducks/crashReports';
import { getEmptyState as expiration } from './ducks/expiration';
import { getEmptyState as globalModals } from './ducks/globalModals';
import { getEmptyState as inbox } from './ducks/inbox';
import { getEmptyState as lightbox } from './ducks/lightbox';
import { getEmptyState as linkPreviews } from './ducks/linkPreviews';
import { getEmptyState as mediaGallery } from './ducks/mediaGallery';
@ -115,6 +116,7 @@ export function getInitialState({
emojis: emojis(),
expiration: expiration(),
globalModals: globalModals(),
inbox: inbox(),
items,
lightbox: lightbox(),
linkPreviews: linkPreviews(),

View file

@ -15,6 +15,7 @@ import { reducer as crashReports } from './ducks/crashReports';
import { reducer as emojis } from './ducks/emojis';
import { reducer as expiration } from './ducks/expiration';
import { reducer as globalModals } from './ducks/globalModals';
import { reducer as inbox } from './ducks/inbox';
import { reducer as items } from './ducks/items';
import { reducer as lightbox } from './ducks/lightbox';
import { reducer as linkPreviews } from './ducks/linkPreviews';
@ -44,6 +45,7 @@ export const reducer = combineReducers({
emojis,
expiration,
globalModals,
inbox,
items,
lightbox,
linkPreviews,

View file

@ -37,6 +37,12 @@ export function SmartInbox(): JSX.Element {
const isCustomizingPreferredReactions = useSelector(
getIsCustomizingPreferredReactions
);
const envelopeTimestamp = useSelector<StateType, number | undefined>(
state => state.inbox.envelopeTimestamp
);
const firstEnvelopeTimestamp = useSelector<StateType, number | undefined>(
state => state.inbox.firstEnvelopeTimestamp
);
const { hasInitialLoadCompleted } = useSelector<StateType, AppStateType>(
state => state.app
);
@ -54,6 +60,8 @@ export function SmartInbox(): JSX.Element {
return (
<Inbox
envelopeTimestamp={envelopeTimestamp}
firstEnvelopeTimestamp={firstEnvelopeTimestamp}
hasInitialLoadCompleted={hasInitialLoadCompleted}
i18n={i18n}
isCustomizingPreferredReactions={isCustomizingPreferredReactions}

View file

@ -13,6 +13,7 @@ import type { actions as crashReports } from './ducks/crashReports';
import type { actions as emojis } from './ducks/emojis';
import type { actions as expiration } from './ducks/expiration';
import type { actions as globalModals } from './ducks/globalModals';
import type { actions as inbox } from './ducks/inbox';
import type { actions as items } from './ducks/items';
import type { actions as lightbox } from './ducks/lightbox';
import type { actions as linkPreviews } from './ducks/linkPreviews';
@ -41,6 +42,7 @@ export type ReduxActions = {
emojis: typeof emojis;
expiration: typeof expiration;
globalModals: typeof globalModals;
inbox: typeof inbox;
items: typeof items;
lightbox: typeof lightbox;
linkPreviews: typeof linkPreviews;