Timeline date headers
This commit is contained in:
parent
0fa069f260
commit
f9440bf594
41 changed files with 1183 additions and 771 deletions
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { trigger } from '../../shims/events';
|
||||
|
@ -23,6 +23,7 @@ export type UserStateType = {
|
|||
i18n: LocalizerType;
|
||||
interactionMode: 'mouse' | 'keyboard';
|
||||
theme: ThemeType;
|
||||
version: string;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
@ -98,6 +99,7 @@ export function getEmptyState(): UserStateType {
|
|||
},
|
||||
}
|
||||
),
|
||||
version: '0.0.0',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// Copyright 2019-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { isInteger } from 'lodash';
|
||||
|
||||
import { ITEM_NAME as UNIVERSAL_EXPIRE_TIMER_ITEM } from '../../util/universalExpireTimer';
|
||||
import type { ConfigMapType } from '../../RemoteConfig';
|
||||
import type { ConfigKeyType, ConfigMapType } from '../../RemoteConfig';
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { ItemsStateType } from '../ducks/items';
|
||||
|
@ -15,6 +15,7 @@ import type {
|
|||
} from '../../types/Colors';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors';
|
||||
import { getPreferredReactionEmoji as getPreferredReactionEmojiFromStoredValue } from '../../reactions/preferredReactionEmoji';
|
||||
import { getIsAlpha, getIsBeta } from './user';
|
||||
|
||||
const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
||||
|
||||
|
@ -42,15 +43,39 @@ export const getUniversalExpireTimer = createSelector(
|
|||
(state: ItemsStateType): number => state[UNIVERSAL_EXPIRE_TIMER_ITEM] || 0
|
||||
);
|
||||
|
||||
const isRemoteConfigFlagEnabled = (
|
||||
config: Readonly<ConfigMapType>,
|
||||
key: ConfigKeyType
|
||||
): boolean => Boolean(config[key]?.enabled);
|
||||
|
||||
const getRemoteConfig = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): ConfigMapType | undefined => state.remoteConfig
|
||||
(state: ItemsStateType): ConfigMapType => state.remoteConfig || {}
|
||||
);
|
||||
|
||||
export const getUsernamesEnabled = createSelector(
|
||||
getRemoteConfig,
|
||||
(remoteConfig?: ConfigMapType): boolean =>
|
||||
Boolean(remoteConfig?.['desktop.usernames']?.enabled)
|
||||
(remoteConfig: ConfigMapType): boolean =>
|
||||
isRemoteConfigFlagEnabled(remoteConfig, 'desktop.usernames')
|
||||
);
|
||||
|
||||
export const getAreFloatingDateHeadersEnabled = createSelector(
|
||||
getRemoteConfig,
|
||||
getIsAlpha,
|
||||
getIsBeta,
|
||||
(remoteConfig: ConfigMapType, isAlpha, isBeta): boolean => {
|
||||
if (
|
||||
isAlpha ||
|
||||
isRemoteConfigFlagEnabled(remoteConfig, 'desktop.internalUser')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const remoteConfigKey: ConfigKeyType = isBeta
|
||||
? 'desktop.floatingDateHeaders.beta'
|
||||
: 'desktop.floatingDateHeaders.production';
|
||||
return isRemoteConfigFlagEnabled(remoteConfig, remoteConfigKey);
|
||||
}
|
||||
);
|
||||
|
||||
export const getDefaultConversationColor = createSelector(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// Copyright 2021-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
|
@ -670,6 +670,7 @@ export const getBubblePropsForMessage = createSelectorCreator(memoizeByRoot)(
|
|||
(_, data): TimelineItemType => ({
|
||||
type: 'message' as const,
|
||||
data,
|
||||
timestamp: data.timestamp,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -678,94 +679,113 @@ export function getPropsForBubble(
|
|||
message: MessageWithUIFieldsType,
|
||||
options: GetPropsForBubbleOptions
|
||||
): TimelineItemType {
|
||||
// eslint-disable-next-line camelcase
|
||||
const { received_at_ms: receivedAt, timestamp: messageTimestamp } = message;
|
||||
const timestamp = receivedAt || messageTimestamp;
|
||||
|
||||
if (isUnsupportedMessage(message)) {
|
||||
return {
|
||||
type: 'unsupportedMessage',
|
||||
data: getPropsForUnsupportedMessage(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isGroupV2Change(message)) {
|
||||
return {
|
||||
type: 'groupV2Change',
|
||||
data: getPropsForGroupV2Change(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isGroupV1Migration(message)) {
|
||||
return {
|
||||
type: 'groupV1Migration',
|
||||
data: getPropsForGroupV1Migration(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isMessageHistoryUnsynced(message)) {
|
||||
return {
|
||||
type: 'linkNotification',
|
||||
data: null,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isExpirationTimerUpdate(message)) {
|
||||
return {
|
||||
type: 'timerNotification',
|
||||
data: getPropsForTimerNotification(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isKeyChange(message)) {
|
||||
return {
|
||||
type: 'safetyNumberNotification',
|
||||
data: getPropsForSafetyNumberNotification(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isVerifiedChange(message)) {
|
||||
return {
|
||||
type: 'verificationNotification',
|
||||
data: getPropsForVerificationNotification(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isGroupUpdate(message)) {
|
||||
return {
|
||||
type: 'groupNotification',
|
||||
data: getPropsForGroupNotification(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isEndSession(message)) {
|
||||
return {
|
||||
type: 'resetSessionNotification',
|
||||
data: null,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isCallHistory(message)) {
|
||||
return {
|
||||
type: 'callHistory',
|
||||
data: getPropsForCallHistory(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isProfileChange(message)) {
|
||||
return {
|
||||
type: 'profileChange',
|
||||
data: getPropsForProfileChange(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isUniversalTimerNotification(message)) {
|
||||
return {
|
||||
type: 'universalTimerNotification',
|
||||
data: null,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isChangeNumberNotification(message)) {
|
||||
return {
|
||||
type: 'changeNumberNotification',
|
||||
data: getPropsForChangeNumberNotification(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isChatSessionRefreshed(message)) {
|
||||
return {
|
||||
type: 'chatSessionRefreshed',
|
||||
data: null,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
if (isDeliveryIssue(message)) {
|
||||
return {
|
||||
type: 'deliveryIssue',
|
||||
data: getPropsForDeliveryIssue(message, options),
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
@ -9,6 +9,8 @@ import type { UUIDStringType } from '../../types/UUID';
|
|||
import type { StateType } from '../reducer';
|
||||
import type { UserStateType } from '../ducks/user';
|
||||
|
||||
import { isAlpha, isBeta } from '../../util/version';
|
||||
|
||||
export const getUser = (state: StateType): UserStateType => state.user;
|
||||
|
||||
export const getUserNumber = createSelector(
|
||||
|
@ -70,3 +72,12 @@ export const getTheme = createSelector(
|
|||
getUser,
|
||||
(state: UserStateType): ThemeType => state.theme
|
||||
);
|
||||
|
||||
const getVersion = createSelector(
|
||||
getUser,
|
||||
(state: UserStateType) => state.version
|
||||
);
|
||||
|
||||
export const getIsAlpha = createSelector(getVersion, isAlpha);
|
||||
|
||||
export const getIsBeta = createSelector(getVersion, isBeta);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// Copyright 2019-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isEmpty, mapValues, pick } from 'lodash';
|
||||
|
@ -18,6 +18,7 @@ import { Timeline } from '../../components/conversation/Timeline';
|
|||
import type { StateType } from '../reducer';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
|
||||
import { getAreFloatingDateHeadersEnabled } from '../selectors/items';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import {
|
||||
getConversationByUuidSelector,
|
||||
|
@ -25,6 +26,7 @@ import {
|
|||
getConversationSelector,
|
||||
getConversationsByTitleSelector,
|
||||
getInvitedContactsForNewlyCreatedGroup,
|
||||
getMessageSelector,
|
||||
getSelectedMessage,
|
||||
} from '../selectors/conversations';
|
||||
|
||||
|
@ -32,13 +34,12 @@ import { SmartTimelineItem } from './TimelineItem';
|
|||
import { SmartTypingBubble } from './TypingBubble';
|
||||
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||
import { SmartHeroRow } from './HeroRow';
|
||||
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
import { renderReactionPicker } from './renderReactionPicker';
|
||||
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import { assert } from '../../util/assert';
|
||||
import { assert, strictAssert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||
import {
|
||||
|
@ -114,6 +115,7 @@ function renderItem({
|
|||
containerElementRef,
|
||||
containerWidthBreakpoint,
|
||||
conversationId,
|
||||
isOldestTimelineItem,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
onHeightChange,
|
||||
|
@ -123,6 +125,7 @@ function renderItem({
|
|||
containerElementRef: RefObject<HTMLElement>;
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
conversationId: string;
|
||||
isOldestTimelineItem: boolean;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
onHeightChange: (messageId: string) => unknown;
|
||||
|
@ -134,6 +137,7 @@ function renderItem({
|
|||
containerElementRef={containerElementRef}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
conversationId={conversationId}
|
||||
isOldestTimelineItem={isOldestTimelineItem}
|
||||
messageId={messageId}
|
||||
previousMessageId={previousMessageId}
|
||||
nextMessageId={nextMessageId}
|
||||
|
@ -164,9 +168,6 @@ function renderHeroRow(
|
|||
/>
|
||||
);
|
||||
}
|
||||
function renderLoadingRow(id: string): JSX.Element {
|
||||
return <SmartTimelineLoadingRow id={id} />;
|
||||
}
|
||||
function renderTypingBubble(id: string): JSX.Element {
|
||||
return <SmartTypingBubble id={id} />;
|
||||
}
|
||||
|
@ -294,6 +295,13 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
const conversationMessages = getConversationMessagesSelector(state)(id);
|
||||
const selectedMessage = getSelectedMessage(state);
|
||||
|
||||
const messageSelector = getMessageSelector(state);
|
||||
const getTimestampForMessage = (messageId: string): number => {
|
||||
const result = messageSelector(messageId)?.timestamp;
|
||||
strictAssert(result, 'Expected a message');
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
...pick(conversation, [
|
||||
|
@ -314,13 +322,15 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
warning: getWarning(conversation, state),
|
||||
contactSpoofingReview: getContactSpoofingReview(id, state),
|
||||
|
||||
areFloatingDateHeadersEnabled: getAreFloatingDateHeadersEnabled(state),
|
||||
|
||||
getTimestampForMessage,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
theme: getTheme(state),
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
renderHeroRow,
|
||||
renderLoadingRow,
|
||||
renderTypingBubble,
|
||||
...actions,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// Copyright 2019-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { RefObject } from 'react';
|
||||
|
@ -23,6 +23,7 @@ import { SmartUniversalTimerNotification } from './UniversalTimerNotification';
|
|||
type ExternalProps = {
|
||||
containerElementRef: RefObject<HTMLElement>;
|
||||
conversationId: string;
|
||||
isOldestTimelineItem: boolean;
|
||||
messageId: string;
|
||||
nextMessageId: undefined | string;
|
||||
previousMessageId: undefined | string;
|
||||
|
@ -40,6 +41,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
const {
|
||||
containerElementRef,
|
||||
conversationId,
|
||||
isOldestTimelineItem,
|
||||
messageId,
|
||||
nextMessageId,
|
||||
previousMessageId,
|
||||
|
@ -70,6 +72,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
conversationColor: conversation?.conversationColor,
|
||||
customColor: conversation?.customColor,
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
isOldestTimelineItem,
|
||||
isSelected,
|
||||
renderContact,
|
||||
renderUniversalTimerNotification,
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
|
||||
import type { STATE_ENUM } from '../../components/conversation/TimelineLoadingRow';
|
||||
import { TimelineLoadingRow } from '../../components/conversation/TimelineLoadingRow';
|
||||
import { LOAD_COUNTDOWN } from '../../components/conversation/Timeline';
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getConversationMessagesSelector } from '../selectors/conversations';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
const conversation = getConversationMessagesSelector(state)(id);
|
||||
if (!conversation) {
|
||||
throw new Error(`Did not find conversation ${id} in state!`);
|
||||
}
|
||||
|
||||
const { isLoadingMessages, loadCountdownStart } = conversation;
|
||||
|
||||
let loadingState: STATE_ENUM;
|
||||
|
||||
if (isLoadingMessages) {
|
||||
loadingState = 'loading';
|
||||
} else if (isNumber(loadCountdownStart)) {
|
||||
loadingState = 'countdown';
|
||||
} else {
|
||||
loadingState = 'idle';
|
||||
}
|
||||
|
||||
const duration = loadingState === 'countdown' ? LOAD_COUNTDOWN : undefined;
|
||||
const expiresAt =
|
||||
loadingState === 'countdown' && loadCountdownStart
|
||||
? loadCountdownStart + LOAD_COUNTDOWN
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
state: loadingState,
|
||||
duration,
|
||||
expiresAt,
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartTimelineLoadingRow = smart(TimelineLoadingRow);
|
Loading…
Add table
Add a link
Reference in a new issue