Display "days ago" in loading screen
This commit is contained in:
parent
c02c8d9640
commit
3264c3d509
17 changed files with 316 additions and 66 deletions
|
@ -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),
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -1821,7 +1821,7 @@ app.on('ready', async () => {
|
|||
loadingWindow = new BrowserWindow({
|
||||
show: false,
|
||||
width: 300,
|
||||
height: 265,
|
||||
height: 280,
|
||||
resizable: false,
|
||||
frame: false,
|
||||
backgroundColor,
|
||||
|
|
|
@ -100,17 +100,13 @@
|
|||
<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 class="app-loading-screen__progress--container">
|
||||
<div class="app-loading-screen__progress--bar"></div>
|
||||
</div>
|
||||
<div class="message"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
document
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
</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>
|
||||
|
@ -34,7 +33,6 @@
|
|||
</div>
|
||||
<div id="message"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="ts/windows/loading/start.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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,24 +243,15 @@ $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;
|
||||
}
|
||||
.message {
|
||||
max-width: 35em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
margin: 8px 0 24px 0;
|
||||
|
||||
.dot {
|
||||
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 {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 } =
|
||||
|
|
96
ts/components/Inbox.stories.tsx
Normal file
96
ts/components/Inbox.stories.tsx
Normal 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',
|
||||
};
|
|
@ -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,27 +176,46 @@ 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 className="app-loading-screen__progress--container">
|
||||
<div
|
||||
className="app-loading-screen__progress--bar"
|
||||
style={{ transform: `translateX(${loadingProgress - 100}%)` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="message">
|
||||
{loadingMessageCount
|
||||
? i18n('loadingMessages', {
|
||||
count: String(loadingMessageCount),
|
||||
{envelopeTimestamp
|
||||
? i18n('icu:loadingMessages', {
|
||||
daysAgo: Math.max(
|
||||
1,
|
||||
Math.round((now - envelopeTimestamp) / DAY)
|
||||
),
|
||||
})
|
||||
: i18n('loading')}
|
||||
</div>
|
||||
<div id="toast" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
83
ts/state/ducks/inbox.ts
Normal 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;
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue