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 (