From 3264c3d5093c5021e60d0770022ac80773d51880 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Tue, 28 Mar 2023 13:31:24 -0700 Subject: [PATCH] Display "days ago" in loading screen --- .storybook/preview-head.html | 5 ++ .storybook/preview.tsx | 2 + _locales/en/messages.json | 4 ++ app/main.ts | 2 +- background.html | 12 ++--- loading.html | 14 +++-- stylesheets/_global.scss | 76 ++++++++++++++++---------- stylesheets/_modules.scss | 2 +- ts/background.ts | 11 ++++ ts/components/Inbox.stories.tsx | 96 +++++++++++++++++++++++++++++++++ ts/components/Inbox.tsx | 58 +++++++++++++------- ts/state/actions.ts | 3 ++ ts/state/ducks/inbox.ts | 83 ++++++++++++++++++++++++++++ ts/state/getInitialState.ts | 2 + ts/state/reducer.ts | 2 + ts/state/smart/Inbox.tsx | 8 +++ ts/state/types.ts | 2 + 17 files changed, 316 insertions(+), 66 deletions(-) create mode 100644 ts/components/Inbox.stories.tsx create mode 100644 ts/state/ducks/inbox.ts diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index aacd84c9e..086a0ee57 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -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), diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index cd0316b0e..a067109d4 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -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'); } diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 461ab88d4..0257573c2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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": { diff --git a/app/main.ts b/app/main.ts index 75bbc1ca7..085fdbb9f 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1821,7 +1821,7 @@ app.on('ready', async () => { loadingWindow = new BrowserWindow({ show: false, width: 300, - height: 265, + height: 280, resizable: false, frame: false, backgroundColor, diff --git a/background.html b/background.html index 9980afcfd..9f6f0f9be 100644 --- a/background.html +++ b/background.html @@ -100,15 +100,11 @@
-
- -
- - - -
-
 
+ +
+
+
 
diff --git a/loading.html b/loading.html index 7bc05c79a..0b27c6cbf 100644 --- a/loading.html +++ b/loading.html @@ -25,15 +25,13 @@
-
- -
- - - -
-
+ +
+ + +
+
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 926361bf5..660e66331 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -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; - } } } diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index d3bd1d98d..9f469b5ed 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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; diff --git a/ts/background.ts b/ts/background.ts index 6e587287b..62e85b3dc 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1181,6 +1181,7 @@ export async function startApp(): Promise { 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 { maxSize: Infinity, }); + const throttledSetInboxEnvelopeTimestamp = throttle( + serverTimestamp => { + window.reduxActions.inbox.setInboxEnvelopeTimestamp(serverTimestamp); + }, + 100, + { leading: false } + ); + async function onEnvelopeReceived({ envelope, }: EnvelopeEvent): Promise { + throttledSetInboxEnvelopeTimestamp(envelope.serverTimestamp); + const ourUuid = window.textsecure.storage.user.getUuid()?.toString(); if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) { const { mergePromises, conversation } = diff --git a/ts/components/Inbox.stories.tsx b/ts/components/Inbox.stories.tsx new file mode 100644 index 000000000..06106abae --- /dev/null +++ b/ts/components/Inbox.stories.tsx @@ -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 = ({ + 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 ( +
} + renderCustomizingPreferredReactionsModal={() =>
} + renderLeftPane={() =>
} + renderMiniPlayer={() =>
} + /> + ); +}; + +export const Default = Template.bind({}); +Default.story = { + name: 'Default', +}; diff --git a/ts/components/Inbox.tsx b/ts/components/Inbox.tsx index 6a40bd398..9f01b6bcb 100644 --- a/ts/components/Inbox.tsx +++ b/ts/components/Inbox.tsx @@ -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 (
-
-
-
- - - -
-
- {loadingMessageCount - ? i18n('loadingMessages', { - count: String(loadingMessageCount), - }) - : i18n('loading')} -
-
+
+
+
+
+ {envelopeTimestamp + ? i18n('icu:loadingMessages', { + daysAgo: Math.max( + 1, + Math.round((now - envelopeTimestamp) / DAY) + ), + }) + : i18n('loading')} +
+
); } diff --git a/ts/state/actions.ts b/ts/state/actions.ts index 9b920da8e..0e3664204 100644 --- a/ts/state/actions.ts +++ b/ts/state/actions.ts @@ -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, diff --git a/ts/state/ducks/inbox.ts b/ts/state/ducks/inbox.ts new file mode 100644 index 000000000..4e78cf043 --- /dev/null +++ b/ts/state/ducks/inbox.ts @@ -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; + +// 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 = getEmptyState(), + action: Readonly +): 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; +} diff --git a/ts/state/getInitialState.ts b/ts/state/getInitialState.ts index 6c74879e4..b8ace8bf3 100644 --- a/ts/state/getInitialState.ts +++ b/ts/state/getInitialState.ts @@ -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(), diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 50c07d473..171cd0b48 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -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, diff --git a/ts/state/smart/Inbox.tsx b/ts/state/smart/Inbox.tsx index cc7265b38..0cfc41117 100644 --- a/ts/state/smart/Inbox.tsx +++ b/ts/state/smart/Inbox.tsx @@ -37,6 +37,12 @@ export function SmartInbox(): JSX.Element { const isCustomizingPreferredReactions = useSelector( getIsCustomizingPreferredReactions ); + const envelopeTimestamp = useSelector( + state => state.inbox.envelopeTimestamp + ); + const firstEnvelopeTimestamp = useSelector( + state => state.inbox.firstEnvelopeTimestamp + ); const { hasInitialLoadCompleted } = useSelector( state => state.app ); @@ -54,6 +60,8 @@ export function SmartInbox(): JSX.Element { return (