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 // eslint-disable-next-line
const noop = () => {}; const noop = () => {};
window.Whisper = window.Whisper || {};
window.Whisper.events = {
on: noop,
};
window.SignalWindow = window.SignalWindow || {}; window.SignalWindow = window.SignalWindow || {};
window.SignalWindow.log = { window.SignalWindow.log = {
fatal: console.error.bind(console), 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 // Adding it to the body as well so that we can cover modals and other
// components that are rendered outside of this decorator container // components that are rendered outside of this decorator container
if (theme === 'light') { if (theme === 'light') {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme'); document.body.classList.remove('dark-theme');
} else { } else {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme'); document.body.classList.add('dark-theme');
} }

View file

@ -397,6 +397,10 @@
}, },
"loadingMessages": { "loadingMessages": {
"message": "Loading messages. $count$ so far...", "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" "description": "Message shown on the loading screen when we're catching up on the backlog of messages"
}, },
"view": { "view": {

View file

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

View file

@ -100,17 +100,13 @@
<div class="app-loading-screen app-loading-screen--without-titlebar"> <div class="app-loading-screen app-loading-screen--without-titlebar">
<div class="module-title-bar-drag-area"></div> <div class="module-title-bar-drag-area"></div>
<div class="content">
<div class="module-splash-screen__logo module-img--150"></div> <div class="module-splash-screen__logo module-img--150"></div>
<div class="container"> <div class="app-loading-screen__progress--container">
<span class="dot"></span> <div class="app-loading-screen__progress--bar"></div>
<span class="dot"></span>
<span class="dot"></span>
</div> </div>
<div class="message">&nbsp;</div> <div class="message">&nbsp;</div>
</div> </div>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
document document

View file

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

View file

@ -208,7 +208,7 @@ $loading-height: 16px;
opacity: 1; opacity: 1;
} }
100% { 100% {
opacity: 0; opacity: 0.3;
} }
} }
@ -224,6 +224,7 @@ $loading-height: 16px;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
padding: 0 16px;
&--without-titlebar { &--without-titlebar {
/* There is no titlebar during loading screen on Windows */ /* There is no titlebar during loading screen on Windows */
@ -242,24 +243,15 @@ $loading-height: 16px;
color: $color-white; color: $color-white;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: center;
justify-content: center; justify-content: center;
user-select: none; user-select: none;
.content { /* Currently only used in loading window */
text-align: center;
}
.container { .container {
margin-left: auto; display: flex;
margin-right: auto; gap: 7px;
width: 78px; margin: 8px 0 24px 0;
height: 22px;
}
.message {
max-width: 35em;
margin-left: auto;
margin-right: auto;
}
.dot { .dot {
width: 14px; width: 14px;
@ -280,6 +272,32 @@ $loading-height: 16px;
} }
} }
&__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;
}
}
.full-screen-flow { .full-screen-flow {
position: absolute; position: absolute;
left: 0; left: 0;

View file

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

View file

@ -1181,6 +1181,7 @@ export async function startApp(): Promise<void> {
actionCreators.crashReports, actionCreators.crashReports,
store.dispatch store.dispatch
), ),
inbox: bindActionCreators(actionCreators.inbox, store.dispatch),
emojis: bindActionCreators(actionCreators.emojis, store.dispatch), emojis: bindActionCreators(actionCreators.emojis, store.dispatch),
expiration: bindActionCreators(actionCreators.expiration, store.dispatch), expiration: bindActionCreators(actionCreators.expiration, store.dispatch),
globalModals: bindActionCreators( globalModals: bindActionCreators(
@ -2917,9 +2918,19 @@ export async function startApp(): Promise<void> {
maxSize: Infinity, maxSize: Infinity,
}); });
const throttledSetInboxEnvelopeTimestamp = throttle(
serverTimestamp => {
window.reduxActions.inbox.setInboxEnvelopeTimestamp(serverTimestamp);
},
100,
{ leading: false }
);
async function onEnvelopeReceived({ async function onEnvelopeReceived({
envelope, envelope,
}: EnvelopeEvent): Promise<void> { }: EnvelopeEvent): Promise<void> {
throttledSetInboxEnvelopeTimestamp(envelope.serverTimestamp);
const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) { if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) {
const { mergePromises, conversation } = 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 type { LocalizerType } from '../types/Util';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { SECOND } from '../util/durations'; import { SECOND, DAY } from '../util/durations';
import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed'; import { ToastStickerPackInstallFailed } from './ToastStickerPackInstallFailed';
import { WhatsNewLink } from './WhatsNewLink'; import { WhatsNewLink } from './WhatsNewLink';
import { showToast } from '../util/showToast'; import { showToast } from '../util/showToast';
@ -17,6 +17,8 @@ import { TargetedMessageSource } from '../state/ducks/conversationsEnums';
import { usePrevious } from '../hooks/usePrevious'; import { usePrevious } from '../hooks/usePrevious';
export type PropsType = { export type PropsType = {
firstEnvelopeTimestamp: number | undefined;
envelopeTimestamp: number | undefined;
hasInitialLoadCompleted: boolean; hasInitialLoadCompleted: boolean;
i18n: LocalizerType; i18n: LocalizerType;
isCustomizingPreferredReactions: boolean; isCustomizingPreferredReactions: boolean;
@ -35,6 +37,8 @@ export type PropsType = {
}; };
export function Inbox({ export function Inbox({
firstEnvelopeTimestamp,
envelopeTimestamp,
hasInitialLoadCompleted, hasInitialLoadCompleted,
i18n, i18n,
isCustomizingPreferredReactions, isCustomizingPreferredReactions,
@ -51,7 +55,6 @@ export function Inbox({
showConversation, showConversation,
showWhatsNewModal, showWhatsNewModal,
}: PropsType): JSX.Element { }: PropsType): JSX.Element {
const [loadingMessageCount, setLoadingMessageCount] = useState(0);
const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] = const [internalHasInitialLoadCompleted, setInternalHasInitialLoadCompleted] =
useState(hasInitialLoadCompleted); useState(hasInitialLoadCompleted);
@ -123,13 +126,11 @@ export function Inbox({
showToast(ToastStickerPackInstallFailed); showToast(ToastStickerPackInstallFailed);
} }
window.Whisper.events.on('loadingProgress', setLoadingMessageCount);
window.Whisper.events.on('pack-install-failed', packInstallFailed); window.Whisper.events.on('pack-install-failed', packInstallFailed);
window.Whisper.events.on('refreshConversation', refreshConversation); window.Whisper.events.on('refreshConversation', refreshConversation);
window.Whisper.events.on('setupAsNewDevice', unload); window.Whisper.events.on('setupAsNewDevice', unload);
return () => { return () => {
window.Whisper.events.off('loadingProgress', setLoadingMessageCount);
window.Whisper.events.off('pack-install-failed', packInstallFailed); window.Whisper.events.off('pack-install-failed', packInstallFailed);
window.Whisper.events.off('refreshConversation', refreshConversation); window.Whisper.events.off('refreshConversation', refreshConversation);
window.Whisper.events.off('setupAsNewDevice', unload); window.Whisper.events.off('setupAsNewDevice', unload);
@ -175,27 +176,46 @@ export function Inbox({
}, [hasInitialLoadCompleted]); }, [hasInitialLoadCompleted]);
if (!internalHasInitialLoadCompleted) { 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 ( return (
<div className="app-loading-screen"> <div className="app-loading-screen">
<div className="module-title-bar-drag-area" /> <div className="module-title-bar-drag-area" />
<div className="content">
<div className="module-splash-screen__logo module-img--150" /> <div className="module-splash-screen__logo module-img--150" />
<div className="container"> <div className="app-loading-screen__progress--container">
<span className="dot" /> <div
<span className="dot" /> className="app-loading-screen__progress--bar"
<span className="dot" /> style={{ transform: `translateX(${loadingProgress - 100}%)` }}
/>
</div> </div>
<div className="message"> <div className="message">
{loadingMessageCount {envelopeTimestamp
? i18n('loadingMessages', { ? i18n('icu:loadingMessages', {
count: String(loadingMessageCount), daysAgo: Math.max(
1,
Math.round((now - envelopeTimestamp) / DAY)
),
}) })
: i18n('loading')} : i18n('loading')}
</div> </div>
<div id="toast" /> <div id="toast" />
</div> </div>
</div>
); );
} }

View file

@ -13,6 +13,7 @@ import { actions as crashReports } from './ducks/crashReports';
import { actions as emojis } from './ducks/emojis'; import { actions as emojis } from './ducks/emojis';
import { actions as expiration } from './ducks/expiration'; import { actions as expiration } from './ducks/expiration';
import { actions as globalModals } from './ducks/globalModals'; import { actions as globalModals } from './ducks/globalModals';
import { actions as inbox } from './ducks/inbox';
import { actions as items } from './ducks/items'; import { actions as items } from './ducks/items';
import { actions as lightbox } from './ducks/lightbox'; import { actions as lightbox } from './ducks/lightbox';
import { actions as linkPreviews } from './ducks/linkPreviews'; import { actions as linkPreviews } from './ducks/linkPreviews';
@ -42,6 +43,7 @@ export const actionCreators: ReduxActions = {
emojis, emojis,
expiration, expiration,
globalModals, globalModals,
inbox,
items, items,
lightbox, lightbox,
linkPreviews, linkPreviews,
@ -71,6 +73,7 @@ export const mapDispatchToProps = {
...emojis, ...emojis,
...expiration, ...expiration,
...globalModals, ...globalModals,
...inbox,
...items, ...items,
...lightbox, ...lightbox,
...linkPreviews, ...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 crashReports } from './ducks/crashReports';
import { getEmptyState as expiration } from './ducks/expiration'; import { getEmptyState as expiration } from './ducks/expiration';
import { getEmptyState as globalModals } from './ducks/globalModals'; import { getEmptyState as globalModals } from './ducks/globalModals';
import { getEmptyState as inbox } from './ducks/inbox';
import { getEmptyState as lightbox } from './ducks/lightbox'; import { getEmptyState as lightbox } from './ducks/lightbox';
import { getEmptyState as linkPreviews } from './ducks/linkPreviews'; import { getEmptyState as linkPreviews } from './ducks/linkPreviews';
import { getEmptyState as mediaGallery } from './ducks/mediaGallery'; import { getEmptyState as mediaGallery } from './ducks/mediaGallery';
@ -115,6 +116,7 @@ export function getInitialState({
emojis: emojis(), emojis: emojis(),
expiration: expiration(), expiration: expiration(),
globalModals: globalModals(), globalModals: globalModals(),
inbox: inbox(),
items, items,
lightbox: lightbox(), lightbox: lightbox(),
linkPreviews: linkPreviews(), 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 emojis } from './ducks/emojis';
import { reducer as expiration } from './ducks/expiration'; import { reducer as expiration } from './ducks/expiration';
import { reducer as globalModals } from './ducks/globalModals'; import { reducer as globalModals } from './ducks/globalModals';
import { reducer as inbox } from './ducks/inbox';
import { reducer as items } from './ducks/items'; import { reducer as items } from './ducks/items';
import { reducer as lightbox } from './ducks/lightbox'; import { reducer as lightbox } from './ducks/lightbox';
import { reducer as linkPreviews } from './ducks/linkPreviews'; import { reducer as linkPreviews } from './ducks/linkPreviews';
@ -44,6 +45,7 @@ export const reducer = combineReducers({
emojis, emojis,
expiration, expiration,
globalModals, globalModals,
inbox,
items, items,
lightbox, lightbox,
linkPreviews, linkPreviews,

View file

@ -37,6 +37,12 @@ export function SmartInbox(): JSX.Element {
const isCustomizingPreferredReactions = useSelector( const isCustomizingPreferredReactions = useSelector(
getIsCustomizingPreferredReactions 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>( const { hasInitialLoadCompleted } = useSelector<StateType, AppStateType>(
state => state.app state => state.app
); );
@ -54,6 +60,8 @@ export function SmartInbox(): JSX.Element {
return ( return (
<Inbox <Inbox
envelopeTimestamp={envelopeTimestamp}
firstEnvelopeTimestamp={firstEnvelopeTimestamp}
hasInitialLoadCompleted={hasInitialLoadCompleted} hasInitialLoadCompleted={hasInitialLoadCompleted}
i18n={i18n} i18n={i18n}
isCustomizingPreferredReactions={isCustomizingPreferredReactions} 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 emojis } from './ducks/emojis';
import type { actions as expiration } from './ducks/expiration'; import type { actions as expiration } from './ducks/expiration';
import type { actions as globalModals } from './ducks/globalModals'; 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 items } from './ducks/items';
import type { actions as lightbox } from './ducks/lightbox'; import type { actions as lightbox } from './ducks/lightbox';
import type { actions as linkPreviews } from './ducks/linkPreviews'; import type { actions as linkPreviews } from './ducks/linkPreviews';
@ -41,6 +42,7 @@ export type ReduxActions = {
emojis: typeof emojis; emojis: typeof emojis;
expiration: typeof expiration; expiration: typeof expiration;
globalModals: typeof globalModals; globalModals: typeof globalModals;
inbox: typeof inbox;
items: typeof items; items: typeof items;
lightbox: typeof lightbox; lightbox: typeof lightbox;
linkPreviews: typeof linkPreviews; linkPreviews: typeof linkPreviews;