mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-09-28 16:09:49 +01:00
Rework conversation panel layout and sub-header styles
This commit is contained in:
@@ -11,15 +11,10 @@
|
||||
|
||||
&__pane {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(
|
||||
100% - #{variables.$header-height} - var(--title-bar-drag-area-height)
|
||||
);
|
||||
inset-inline-start: 0;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: calc(#{variables.$header-height} + var(--title-bar-drag-area-height));
|
||||
width: 100%;
|
||||
z-index: variables.$z-index-base;
|
||||
|
||||
@include mixins.light-theme() {
|
||||
@@ -33,23 +28,21 @@
|
||||
|
||||
&__timeline {
|
||||
&--container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
& {
|
||||
-webkit-padding-start: 0px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,5 +61,6 @@
|
||||
}
|
||||
|
||||
&__header {
|
||||
z-index: variables.$z-index-above-base;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,6 @@
|
||||
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
&:last-child {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
@include mixins.light-theme {
|
||||
color: variables.$color-gray-65;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@use '../variables';
|
||||
|
||||
.module-TimelineWarnings {
|
||||
inset-inline-start: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: variables.$z-index-above-above-base;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -196,7 +196,6 @@
|
||||
@use 'components/TimelineDateHeader.scss';
|
||||
@use 'components/TimelineFloatingHeader.scss';
|
||||
@use 'components/TimelineWarning.scss';
|
||||
@use 'components/TimelineWarnings.scss';
|
||||
@use 'components/Toast.scss';
|
||||
@use 'components/ToastManager.scss';
|
||||
@use 'components/Waveform.scss';
|
||||
|
||||
@@ -114,6 +114,16 @@
|
||||
--color-shadow-elevation-5: light-dark(--alpha(#000 / 20%), --alpha(#000 / 40%));
|
||||
--color-shadow-outline: light-dark(--alpha(#000 / 12%), /* */ transparent);
|
||||
--color-shadow-highlight: light-dark(/* */ transparent, --alpha(#FFF / 08%));
|
||||
|
||||
|
||||
/**
|
||||
* Colors/Legacy
|
||||
* -------------
|
||||
* These should all eventually be removed, but in places where we need new
|
||||
* components to specifically match the colors of older components, we can
|
||||
* add them here.
|
||||
*/
|
||||
--color-legacy-conversation-header-bg: light-dark(#fff, #121212);
|
||||
}
|
||||
|
||||
@layer theme {
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
} from '../types/Util.std.js';
|
||||
import { strictAssert } from '../util/assert.std.js';
|
||||
|
||||
export type I18nComponentParts = ReadonlyArray<string | React.JSX.Element>;
|
||||
|
||||
export type Props<Key extends keyof ICUJSXMessageParamsByKeyType> = {
|
||||
/** The translation string id */
|
||||
id: Key;
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { ComponentProps } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { times } from 'lodash';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
@@ -19,6 +21,8 @@ import {
|
||||
} from './ConversationHeader.dom.js';
|
||||
import { gifUrl } from '../../storybook/Fixtures.std.js';
|
||||
import { ThemeType } from '../../types/Util.std.js';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
|
||||
import { CollidingAvatars } from '../CollidingAvatars.dom.js';
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/ConversationHeader',
|
||||
@@ -31,6 +35,20 @@ type ItemsType = Array<{
|
||||
props: Omit<ComponentProps<typeof ConversationHeader>, 'theme'>;
|
||||
}>;
|
||||
|
||||
const alice = getDefaultConversation();
|
||||
const bob = getDefaultConversation();
|
||||
|
||||
const renderCollidingAvatars = () => (
|
||||
<CollidingAvatars i18n={i18n} conversations={[alice, bob]} />
|
||||
);
|
||||
const renderMiniPlayer = () => (
|
||||
<div>If active, this is where smart mini player would be</div>
|
||||
);
|
||||
|
||||
const renderPinnedMessagesBar = () => (
|
||||
<div>If active, this is where the smart pinned messages bar would be</div>
|
||||
);
|
||||
|
||||
const commonConversation = getDefaultConversation();
|
||||
const commonProps: PropsType = {
|
||||
...commonConversation,
|
||||
@@ -74,6 +92,19 @@ const commonProps: PropsType = {
|
||||
onViewAllMedia: action('onViewAllMedia'),
|
||||
onViewConversationDetails: action('onViewConversationDetails'),
|
||||
onViewUserStories: action('onViewUserStories'),
|
||||
|
||||
contactSpoofingWarning: null,
|
||||
acknowledgeGroupMemberNameCollisions: action(
|
||||
'acknowledgeGroupMemberNameCollisions'
|
||||
),
|
||||
reviewConversationNameCollision: action('reviewConversationNameCollision'),
|
||||
renderCollidingAvatars,
|
||||
|
||||
shouldShowMiniPlayer: false,
|
||||
renderMiniPlayer,
|
||||
|
||||
shouldShowPinnedMessagesBar: false,
|
||||
renderPinnedMessagesBar,
|
||||
};
|
||||
|
||||
export function PrivateConvo(): React.JSX.Element {
|
||||
@@ -511,3 +542,80 @@ export function GroupConversationInCurrentCall(): React.JSX.Element {
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function WithSameNameInDirectConversationWarning(): React.JSX.Element {
|
||||
const props: PropsType = {
|
||||
...commonProps,
|
||||
contactSpoofingWarning: {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversationId: '123',
|
||||
},
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function WithSameNameInGroupConversationWarning(): React.JSX.Element {
|
||||
const props: PropsType = {
|
||||
...commonProps,
|
||||
contactSpoofingWarning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions: {},
|
||||
groupNameCollisions: {
|
||||
Alice: times(2, () => generateUuid()),
|
||||
},
|
||||
},
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function WithSameNamesInGroupConversationWarning(): React.JSX.Element {
|
||||
const props: PropsType = {
|
||||
...commonProps,
|
||||
contactSpoofingWarning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions: {},
|
||||
groupNameCollisions: {
|
||||
Alice: times(2, () => generateUuid()),
|
||||
Bob: times(3, () => generateUuid()),
|
||||
},
|
||||
},
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function WithJustMiniPlayer(): React.JSX.Element {
|
||||
const props: PropsType = {
|
||||
...commonProps,
|
||||
shouldShowMiniPlayer: true,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function WithJustPinnedMessagesBar(): React.JSX.Element {
|
||||
const props: PropsType = {
|
||||
...commonProps,
|
||||
shouldShowPinnedMessagesBar: true,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
export function WithMinPlayerAndPinnedMessagesBar(): React.JSX.Element {
|
||||
const props: PropsType = {
|
||||
...commonProps,
|
||||
shouldShowMiniPlayer: true,
|
||||
shouldShowPinnedMessagesBar: true,
|
||||
};
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
|
||||
return <ConversationHeader {...props} theme={theme} />;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import classNames from 'classnames';
|
||||
import type { RefObject } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { BadgeType } from '../../badges/types.std.js';
|
||||
import {
|
||||
useKeyboardShortcuts,
|
||||
@@ -35,6 +36,22 @@ import { InAnotherCallTooltip } from './InAnotherCallTooltip.dom.js';
|
||||
import { DeleteMessagesConfirmationDialog } from '../DeleteMessagesConfirmationDialog.dom.js';
|
||||
import { AxoDropdownMenu } from '../../axo/AxoDropdownMenu.dom.js';
|
||||
import { strictAssert } from '../../util/assert.std.js';
|
||||
import {
|
||||
TimelineWarning,
|
||||
TimelineWarningCustomInfo,
|
||||
TimelineWarningLink,
|
||||
} from './TimelineWarning.dom.js';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
|
||||
import type { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions.std.js';
|
||||
import { hasUnacknowledgedCollisions } from '../../util/groupMemberNameCollisions.std.js';
|
||||
import type { I18nComponentParts } from '../I18n.dom.js';
|
||||
import { I18n } from '../I18n.dom.js';
|
||||
import type { SmartCollidingAvatarsProps } from '../../state/smart/CollidingAvatars.dom.js';
|
||||
import type {
|
||||
ContactSpoofingWarning,
|
||||
MultipleGroupMembersWithSameTitleContactSpoofingWarning,
|
||||
} from '../../state/selectors/timeline.preload.js';
|
||||
import { tw } from '../../axo/tw.dom.js';
|
||||
|
||||
function HeaderInfoTitle({
|
||||
name,
|
||||
@@ -92,6 +109,22 @@ export enum OutgoingCallButtonStyle {
|
||||
Join,
|
||||
}
|
||||
|
||||
export type RenderCollidingAvatars = (
|
||||
props: SmartCollidingAvatarsProps
|
||||
) => React.JSX.Element;
|
||||
|
||||
export type RenderMiniPlayer = (options: {
|
||||
shouldFlow: boolean;
|
||||
}) => React.JSX.Element;
|
||||
export type RenderPinnedMessagesBar = () => React.JSX.Element;
|
||||
|
||||
export type AcknowledgeGroupMemberNameCollisions = (
|
||||
conversationId: string,
|
||||
groupNameCollisions: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>
|
||||
) => void;
|
||||
|
||||
export type ReviewConversationNameCollission = () => void;
|
||||
|
||||
export type PropsDataType = {
|
||||
addedByName: ContactNameData | null;
|
||||
badge?: BadgeType;
|
||||
@@ -109,6 +142,15 @@ export type PropsDataType = {
|
||||
outgoingCallButtonStyle: OutgoingCallButtonStyle;
|
||||
sharedGroupNames: ReadonlyArray<string>;
|
||||
theme: ThemeType;
|
||||
|
||||
contactSpoofingWarning: ContactSpoofingWarning | null;
|
||||
renderCollidingAvatars: RenderCollidingAvatars;
|
||||
|
||||
shouldShowMiniPlayer: boolean;
|
||||
renderMiniPlayer: RenderMiniPlayer;
|
||||
|
||||
shouldShowPinnedMessagesBar: boolean;
|
||||
renderPinnedMessagesBar: RenderPinnedMessagesBar;
|
||||
};
|
||||
|
||||
export type PropsActionsType = {
|
||||
@@ -138,6 +180,9 @@ export type PropsActionsType = {
|
||||
onViewAllMedia: () => void;
|
||||
onViewConversationDetails: () => void;
|
||||
onViewUserStories: () => void;
|
||||
|
||||
acknowledgeGroupMemberNameCollisions: AcknowledgeGroupMemberNameCollisions;
|
||||
reviewConversationNameCollision: ReviewConversationNameCollission;
|
||||
};
|
||||
|
||||
export type PropsHousekeepingType = {
|
||||
@@ -189,6 +234,17 @@ export const ConversationHeader = memo(function ConversationHeader({
|
||||
setLocalDeleteWarningShown,
|
||||
sharedGroupNames,
|
||||
theme,
|
||||
|
||||
contactSpoofingWarning,
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
reviewConversationNameCollision,
|
||||
renderCollidingAvatars,
|
||||
|
||||
shouldShowMiniPlayer,
|
||||
renderMiniPlayer,
|
||||
|
||||
shouldShowPinnedMessagesBar,
|
||||
renderPinnedMessagesBar,
|
||||
}: PropsType): React.JSX.Element | null {
|
||||
// Comes from a third-party dependency
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -276,106 +332,110 @@ export const ConversationHeader = memo(function ConversationHeader({
|
||||
>
|
||||
{measureRef => (
|
||||
<div
|
||||
className={classNames('module-ConversationHeader', {
|
||||
'module-ConversationHeader--narrow': isNarrow,
|
||||
})}
|
||||
className={tw('flex flex-col shadow-elevation-1')}
|
||||
ref={measureRef}
|
||||
>
|
||||
<HeaderContent
|
||||
conversation={conversation}
|
||||
badge={badge ?? null}
|
||||
hasStories={hasStories ?? null}
|
||||
headerRef={headerRef}
|
||||
i18n={i18n}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
theme={theme}
|
||||
onViewUserStories={onViewUserStories}
|
||||
onViewConversationDetails={onViewConversationDetails}
|
||||
isSignalConversation={isSignalConversation ?? false}
|
||||
/>
|
||||
{!isSmsOnlyOrUnregistered && !isSignalConversation && (
|
||||
<OutgoingCallButtons
|
||||
<div
|
||||
className={classNames('module-ConversationHeader', {
|
||||
'module-ConversationHeader--narrow': isNarrow,
|
||||
})}
|
||||
>
|
||||
<HeaderContent
|
||||
conversation={conversation}
|
||||
hasActiveCall={hasActiveCall}
|
||||
badge={badge ?? null}
|
||||
hasStories={hasStories ?? null}
|
||||
headerRef={headerRef}
|
||||
i18n={i18n}
|
||||
isNarrow={isNarrow}
|
||||
onOutgoingAudioCall={onOutgoingAudioCall}
|
||||
onOutgoingVideoCall={onOutgoingVideoCall}
|
||||
outgoingCallButtonStyle={outgoingCallButtonStyle}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSearchInConversation}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--search'
|
||||
)}
|
||||
aria-label={i18n('icu:search')}
|
||||
/>
|
||||
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger disabled={isSelectMode}>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--more'
|
||||
)}
|
||||
aria-label={i18n('icu:moreInfo')}
|
||||
/>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<HeaderDropdownMenuContent
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
isMissingMandatoryProfileSharing={
|
||||
isMissingMandatoryProfileSharing ?? false
|
||||
}
|
||||
isSelectMode={isSelectMode}
|
||||
isSignalConversation={isSignalConversation ?? false}
|
||||
onChangeDisappearingMessages={
|
||||
onConversationDisappearingMessagesChange
|
||||
}
|
||||
onChangeMuteExpiration={onConversationMuteExpirationChange}
|
||||
onConversationAccept={onConversationAccept}
|
||||
onConversationArchive={onConversationArchive}
|
||||
onConversationBlock={() => {
|
||||
setMessageRequestState(MessageRequestState.blocking);
|
||||
}}
|
||||
onConversationDelete={() => {
|
||||
setMessageRequestState(MessageRequestState.deleting);
|
||||
}}
|
||||
onConversationDeleteMessages={() => {
|
||||
setHasDeleteMessagesConfirmation(true);
|
||||
}}
|
||||
onConversationLeaveGroup={() => {
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
setHasCannotLeaveGroupBecauseYouAreLastAdminAlert(true);
|
||||
} else {
|
||||
setHasLeaveGroupConfirmation(true);
|
||||
}
|
||||
}}
|
||||
onConversationMarkUnread={onConversationMarkUnread}
|
||||
onConversationPin={onConversationPin}
|
||||
onConversationReportAndMaybeBlock={() => {
|
||||
setMessageRequestState(
|
||||
MessageRequestState.reportingAndMaybeBlocking
|
||||
);
|
||||
}}
|
||||
onConversationUnarchive={onConversationUnarchive}
|
||||
onConversationUnblock={() => {
|
||||
setMessageRequestState(MessageRequestState.unblocking);
|
||||
}}
|
||||
onConversationUnpin={onConversationUnpin}
|
||||
onSelectModeEnter={onSelectModeEnter}
|
||||
onSetupCustomDisappearingTimeout={() => {
|
||||
setHasCustomDisappearingTimeoutModal(true);
|
||||
}}
|
||||
onShowMembers={onShowMembers}
|
||||
onViewAllMedia={onViewAllMedia}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
theme={theme}
|
||||
onViewUserStories={onViewUserStories}
|
||||
onViewConversationDetails={onViewConversationDetails}
|
||||
isSignalConversation={isSignalConversation ?? false}
|
||||
/>
|
||||
</AxoDropdownMenu.Root>
|
||||
{!isSmsOnlyOrUnregistered && !isSignalConversation && (
|
||||
<OutgoingCallButtons
|
||||
conversation={conversation}
|
||||
hasActiveCall={hasActiveCall}
|
||||
i18n={i18n}
|
||||
isNarrow={isNarrow}
|
||||
onOutgoingAudioCall={onOutgoingAudioCall}
|
||||
onOutgoingVideoCall={onOutgoingVideoCall}
|
||||
outgoingCallButtonStyle={outgoingCallButtonStyle}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSearchInConversation}
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--search'
|
||||
)}
|
||||
aria-label={i18n('icu:search')}
|
||||
/>
|
||||
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger disabled={isSelectMode}>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'module-ConversationHeader__button',
|
||||
'module-ConversationHeader__button--more'
|
||||
)}
|
||||
aria-label={i18n('icu:moreInfo')}
|
||||
/>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
<HeaderDropdownMenuContent
|
||||
i18n={i18n}
|
||||
conversation={conversation}
|
||||
isMissingMandatoryProfileSharing={
|
||||
isMissingMandatoryProfileSharing ?? false
|
||||
}
|
||||
isSelectMode={isSelectMode}
|
||||
isSignalConversation={isSignalConversation ?? false}
|
||||
onChangeDisappearingMessages={
|
||||
onConversationDisappearingMessagesChange
|
||||
}
|
||||
onChangeMuteExpiration={onConversationMuteExpirationChange}
|
||||
onConversationAccept={onConversationAccept}
|
||||
onConversationArchive={onConversationArchive}
|
||||
onConversationBlock={() => {
|
||||
setMessageRequestState(MessageRequestState.blocking);
|
||||
}}
|
||||
onConversationDelete={() => {
|
||||
setMessageRequestState(MessageRequestState.deleting);
|
||||
}}
|
||||
onConversationDeleteMessages={() => {
|
||||
setHasDeleteMessagesConfirmation(true);
|
||||
}}
|
||||
onConversationLeaveGroup={() => {
|
||||
if (cannotLeaveBecauseYouAreLastAdmin) {
|
||||
setHasCannotLeaveGroupBecauseYouAreLastAdminAlert(true);
|
||||
} else {
|
||||
setHasLeaveGroupConfirmation(true);
|
||||
}
|
||||
}}
|
||||
onConversationMarkUnread={onConversationMarkUnread}
|
||||
onConversationPin={onConversationPin}
|
||||
onConversationReportAndMaybeBlock={() => {
|
||||
setMessageRequestState(
|
||||
MessageRequestState.reportingAndMaybeBlocking
|
||||
);
|
||||
}}
|
||||
onConversationUnarchive={onConversationUnarchive}
|
||||
onConversationUnblock={() => {
|
||||
setMessageRequestState(MessageRequestState.unblocking);
|
||||
}}
|
||||
onConversationUnpin={onConversationUnpin}
|
||||
onSelectModeEnter={onSelectModeEnter}
|
||||
onSetupCustomDisappearingTimeout={() => {
|
||||
setHasCustomDisappearingTimeoutModal(true);
|
||||
}}
|
||||
onShowMembers={onShowMembers}
|
||||
onViewAllMedia={onViewAllMedia}
|
||||
onViewConversationDetails={onViewConversationDetails}
|
||||
/>
|
||||
</AxoDropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
@@ -393,6 +453,21 @@ export const ConversationHeader = memo(function ConversationHeader({
|
||||
deleteConversation={onConversationDelete}
|
||||
onChangeState={setMessageRequestState}
|
||||
/>
|
||||
|
||||
<ConversationSubheader
|
||||
i18n={i18n}
|
||||
contactSpoofingWarning={contactSpoofingWarning}
|
||||
conversationId={conversation.id}
|
||||
acknowledgeGroupMemberNameCollisions={
|
||||
acknowledgeGroupMemberNameCollisions
|
||||
}
|
||||
reviewConversationNameCollision={reviewConversationNameCollision}
|
||||
renderCollidingAvatars={renderCollidingAvatars}
|
||||
shouldShowMiniPlayer={shouldShowMiniPlayer}
|
||||
renderMiniPlayer={renderMiniPlayer}
|
||||
shouldShowPinnedMessagesBar={shouldShowPinnedMessagesBar}
|
||||
renderPinnedMessagesBar={renderPinnedMessagesBar}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SizeObserver>
|
||||
@@ -1052,3 +1127,217 @@ function CannotLeaveGroupBecauseYouAreLastAdminAlert({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ConversationSubheader(props: {
|
||||
i18n: LocalizerType;
|
||||
|
||||
conversationId: string;
|
||||
|
||||
contactSpoofingWarning: ContactSpoofingWarning | null;
|
||||
reviewConversationNameCollision: ReviewConversationNameCollission;
|
||||
acknowledgeGroupMemberNameCollisions: AcknowledgeGroupMemberNameCollisions;
|
||||
renderCollidingAvatars: RenderCollidingAvatars;
|
||||
|
||||
shouldShowMiniPlayer: boolean;
|
||||
renderMiniPlayer: RenderMiniPlayer;
|
||||
|
||||
shouldShowPinnedMessagesBar: boolean;
|
||||
renderPinnedMessagesBar: RenderPinnedMessagesBar;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
const [
|
||||
hasDismissedDirectContactSpoofingWarning,
|
||||
setHasDismissedDirectContactSpoofingWarning,
|
||||
] = useState(false);
|
||||
|
||||
const renderableContactSpoofingWarning = getRenderableContactSpoofingWarning(
|
||||
props.contactSpoofingWarning,
|
||||
hasDismissedDirectContactSpoofingWarning
|
||||
);
|
||||
|
||||
const handleDismissDirectContactSpoofingWarning = useCallback(() => {
|
||||
setHasDismissedDirectContactSpoofingWarning(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderableContactSpoofingWarning != null && (
|
||||
<>
|
||||
{renderableContactSpoofingWarning.type ===
|
||||
ContactSpoofingType.DirectConversationWithSameTitle && (
|
||||
<DirectConversationWithSameTitleWarning
|
||||
i18n={i18n}
|
||||
reviewConversationNameCollision={
|
||||
props.reviewConversationNameCollision
|
||||
}
|
||||
onDismissDirectContactSpoofingWarning={
|
||||
handleDismissDirectContactSpoofingWarning
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{renderableContactSpoofingWarning.type ===
|
||||
ContactSpoofingType.MultipleGroupMembersWithSameTitle && (
|
||||
<MultipleGroupMembersWithSameTitleWarning
|
||||
i18n={i18n}
|
||||
conversationId={props.conversationId}
|
||||
contactSpoofingWarning={renderableContactSpoofingWarning}
|
||||
acknowledgeGroupMemberNameCollisions={
|
||||
props.acknowledgeGroupMemberNameCollisions
|
||||
}
|
||||
reviewConversationNameCollision={
|
||||
props.reviewConversationNameCollision
|
||||
}
|
||||
renderCollidingAvatars={props.renderCollidingAvatars}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{props.shouldShowMiniPlayer &&
|
||||
props.renderMiniPlayer({ shouldFlow: true })}
|
||||
{!props.shouldShowMiniPlayer &&
|
||||
props.shouldShowPinnedMessagesBar &&
|
||||
props.renderPinnedMessagesBar()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getRenderableContactSpoofingWarning(
|
||||
contactSpoofingWarning: ContactSpoofingWarning | null,
|
||||
hasDismissedDirectContactSpoofingWarning: boolean
|
||||
): ContactSpoofingWarning | null {
|
||||
if (contactSpoofingWarning == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
contactSpoofingWarning.type ===
|
||||
ContactSpoofingType.DirectConversationWithSameTitle
|
||||
) {
|
||||
const shouldRender = !hasDismissedDirectContactSpoofingWarning;
|
||||
return shouldRender ? contactSpoofingWarning : null;
|
||||
}
|
||||
|
||||
if (
|
||||
contactSpoofingWarning.type ===
|
||||
ContactSpoofingType.MultipleGroupMembersWithSameTitle
|
||||
) {
|
||||
const shouldRender = hasUnacknowledgedCollisions(
|
||||
contactSpoofingWarning.acknowledgedGroupNameCollisions,
|
||||
contactSpoofingWarning.groupNameCollisions
|
||||
);
|
||||
|
||||
return shouldRender ? contactSpoofingWarning : null;
|
||||
}
|
||||
|
||||
throw missingCaseError(contactSpoofingWarning);
|
||||
}
|
||||
|
||||
function DirectConversationWithSameTitleWarning(props: {
|
||||
i18n: LocalizerType;
|
||||
reviewConversationNameCollision: ReviewConversationNameCollission;
|
||||
onDismissDirectContactSpoofingWarning: () => void;
|
||||
}) {
|
||||
const { i18n } = props;
|
||||
|
||||
return (
|
||||
<TimelineWarning
|
||||
i18n={i18n}
|
||||
onClose={props.onDismissDirectContactSpoofingWarning}
|
||||
>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-name--link"
|
||||
components={{
|
||||
// This is a render props, not a component
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
reviewRequestLink: parts => (
|
||||
<TimelineWarningLink
|
||||
onClick={props.reviewConversationNameCollision}
|
||||
>
|
||||
{parts}
|
||||
</TimelineWarningLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TimelineWarning>
|
||||
);
|
||||
}
|
||||
|
||||
function MultipleGroupMembersWithSameTitleWarning(props: {
|
||||
i18n: LocalizerType;
|
||||
conversationId: string;
|
||||
contactSpoofingWarning: MultipleGroupMembersWithSameTitleContactSpoofingWarning;
|
||||
acknowledgeGroupMemberNameCollisions: AcknowledgeGroupMemberNameCollisions;
|
||||
reviewConversationNameCollision: ReviewConversationNameCollission;
|
||||
renderCollidingAvatars: RenderCollidingAvatars;
|
||||
}) {
|
||||
const {
|
||||
i18n,
|
||||
conversationId,
|
||||
contactSpoofingWarning,
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
reviewConversationNameCollision,
|
||||
renderCollidingAvatars,
|
||||
} = props;
|
||||
const { groupNameCollisions } = contactSpoofingWarning;
|
||||
|
||||
const numberOfSharedNames = Object.keys(groupNameCollisions).length;
|
||||
const conversationIds = Object.values(groupNameCollisions).flat(1);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
acknowledgeGroupMemberNameCollisions(conversationId, groupNameCollisions);
|
||||
}, [
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
conversationId,
|
||||
groupNameCollisions,
|
||||
]);
|
||||
|
||||
const reviewRequestLink = useCallback(
|
||||
(parts: I18nComponentParts) => {
|
||||
return (
|
||||
<TimelineWarningLink onClick={reviewConversationNameCollision}>
|
||||
{parts}
|
||||
</TimelineWarningLink>
|
||||
);
|
||||
},
|
||||
[reviewConversationNameCollision]
|
||||
);
|
||||
|
||||
if (numberOfSharedNames === 1) {
|
||||
return (
|
||||
<TimelineWarning
|
||||
i18n={i18n}
|
||||
onClose={handleClose}
|
||||
customInfo={
|
||||
conversationIds.length >= 2 ? (
|
||||
<TimelineWarningCustomInfo>
|
||||
{renderCollidingAvatars({ conversationIds })}
|
||||
</TimelineWarningCustomInfo>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-name-in-group--link"
|
||||
components={{
|
||||
count: conversationIds.length,
|
||||
reviewRequestLink,
|
||||
}}
|
||||
/>
|
||||
</TimelineWarning>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TimelineWarning i18n={i18n} onClose={handleClose}>
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-names-in-group--link"
|
||||
components={{
|
||||
count: numberOfSharedNames,
|
||||
reviewRequestLink,
|
||||
}}
|
||||
/>
|
||||
</TimelineWarning>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import lodash from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
@@ -15,22 +14,15 @@ import { StorybookThemeContext } from '../../../.storybook/StorybookThemeContext
|
||||
import { ConversationHero } from './ConversationHero.dom.js';
|
||||
import { getDefaultConversation } from '../../test-helpers/getDefaultConversation.std.js';
|
||||
import { TypingBubble } from './TypingBubble.dom.js';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
|
||||
import { ReadStatus } from '../../messages/MessageReadStatus.std.js';
|
||||
import type { WidthBreakpoint } from '../_util.std.js';
|
||||
import { ThemeType } from '../../types/Util.std.js';
|
||||
import { MessageInteractivity, TextDirection } from './Message.dom.js';
|
||||
import { PaymentEventKind } from '../../types/Payment.std.js';
|
||||
import type { PropsData as TimelineMessageProps } from './TimelineMessage.dom.js';
|
||||
import { CollidingAvatars } from '../CollidingAvatars.dom.js';
|
||||
|
||||
const { times } = lodash;
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
const alice = getDefaultConversation();
|
||||
const bob = getDefaultConversation();
|
||||
|
||||
export default {
|
||||
title: 'Components/Conversation/Timeline',
|
||||
argTypes: {},
|
||||
@@ -455,16 +447,6 @@ const renderTypingBubble = () => (
|
||||
theme={ThemeType.light}
|
||||
/>
|
||||
);
|
||||
const renderCollidingAvatars = () => (
|
||||
<CollidingAvatars i18n={i18n} conversations={[alice, bob]} />
|
||||
);
|
||||
const renderMiniPlayer = () => (
|
||||
<div>If active, this is where smart mini player would be</div>
|
||||
);
|
||||
|
||||
const renderPinnedMessagesBar = () => (
|
||||
<div>If active, this is where the smart pinned messages bar would be</div>
|
||||
);
|
||||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
discardMessages: action('discardMessages'),
|
||||
@@ -485,23 +467,17 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
isNearBottom: null,
|
||||
scrollToIndex: overrideProps.scrollToIndex ?? null,
|
||||
scrollToIndexCounter: 0,
|
||||
shouldShowMiniPlayer: Boolean(overrideProps.shouldShowMiniPlayer),
|
||||
shouldShowPinnedMessagesBar: false,
|
||||
totalUnseen: overrideProps.totalUnseen ?? 0,
|
||||
oldestUnseenIndex: overrideProps.oldestUnseenIndex ?? 0,
|
||||
invitedContactsForNewlyCreatedGroup:
|
||||
overrideProps.invitedContactsForNewlyCreatedGroup || [],
|
||||
warning: overrideProps.warning,
|
||||
hasContactSpoofingReview: false,
|
||||
conversationType: 'direct',
|
||||
|
||||
id: uuid(),
|
||||
renderItem,
|
||||
renderHeroRow,
|
||||
renderMiniPlayer,
|
||||
renderPinnedMessagesBar,
|
||||
renderTypingBubble,
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
isSomeoneTyping: overrideProps.isSomeoneTyping || false,
|
||||
|
||||
@@ -595,57 +571,3 @@ export function WithInvitedContactsForANewlyCreatedGroup(): React.JSX.Element {
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
export function WithSameNameInDirectConversationWarning(): React.JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
|
||||
// Just to pacify type-script
|
||||
safeConversationId: '123',
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
export function WithSameNameInGroupConversationWarning(): React.JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions: {},
|
||||
groupNameCollisions: {
|
||||
Alice: times(2, () => uuid()),
|
||||
},
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
export function WithSameNamesInGroupConversationWarning(): React.JSX.Element {
|
||||
const props = useProps({
|
||||
warning: {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions: {},
|
||||
groupNameCollisions: {
|
||||
Alice: times(2, () => uuid()),
|
||||
Bob: times(3, () => uuid()),
|
||||
},
|
||||
},
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
export function WithJustMiniPlayer(): React.JSX.Element {
|
||||
const props = useProps({
|
||||
shouldShowMiniPlayer: true,
|
||||
items: [],
|
||||
});
|
||||
|
||||
return <Timeline {...props} />;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import classNames from 'classnames';
|
||||
import type { ReactNode, RefObject, UIEvent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import {
|
||||
ScrollDownButton,
|
||||
ScrollDownButtonVariant,
|
||||
@@ -21,14 +20,8 @@ import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary.std.
|
||||
import { WidthBreakpoint } from '../_util.std.js';
|
||||
|
||||
import { ErrorBoundary } from './ErrorBoundary.dom.js';
|
||||
import { I18n } from '../I18n.dom.js';
|
||||
import { TimelineWarning } from './TimelineWarning.dom.js';
|
||||
import { TimelineWarnings } from './TimelineWarnings.dom.js';
|
||||
import { NewlyCreatedGroupInvitedContactsDialog } from '../NewlyCreatedGroupInvitedContactsDialog.dom.js';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
|
||||
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from '../../state/smart/ContactSpoofingReviewDialog.preload.js';
|
||||
import type { GroupNameCollisionsWithIdsByTitle } from '../../util/groupMemberNameCollisions.std.js';
|
||||
import { hasUnacknowledgedCollisions } from '../../util/groupMemberNameCollisions.std.js';
|
||||
import { TimelineFloatingHeader } from './TimelineFloatingHeader.dom.js';
|
||||
import {
|
||||
getScrollAnchorBeforeUpdate,
|
||||
@@ -62,18 +55,6 @@ const LOAD_NEWER_THRESHOLD = 5;
|
||||
|
||||
const DELAY_BEFORE_MARKING_READ_AFTER_FOCUS = SECOND;
|
||||
|
||||
export type WarningType = ReadonlyDeep<
|
||||
| {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
safeConversationId: string;
|
||||
}
|
||||
| {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
acknowledgedGroupNameCollisions: GroupNameCollisionsWithIdsByTitle;
|
||||
groupNameCollisions: GroupNameCollisionsWithIdsByTitle;
|
||||
}
|
||||
>;
|
||||
|
||||
export type PropsDataType = {
|
||||
haveNewest: boolean;
|
||||
haveOldest: boolean;
|
||||
@@ -102,10 +83,7 @@ type PropsHousekeepingType = {
|
||||
targetedMessageId?: string;
|
||||
invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
|
||||
selectedMessageId?: string;
|
||||
shouldShowMiniPlayer: boolean;
|
||||
shouldShowPinnedMessagesBar: boolean;
|
||||
|
||||
warning?: WarningType;
|
||||
hasContactSpoofingReview: boolean | undefined;
|
||||
|
||||
discardMessages: (
|
||||
@@ -123,9 +101,6 @@ type PropsHousekeepingType = {
|
||||
theme: ThemeType;
|
||||
|
||||
updateVisibleMessages?: (messageIds: Array<string>) => void;
|
||||
renderCollidingAvatars: (_: {
|
||||
conversationIds: ReadonlyArray<string>;
|
||||
}) => React.JSX.Element;
|
||||
renderContactSpoofingReviewDialog: (
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
) => React.JSX.Element;
|
||||
@@ -143,17 +118,11 @@ type PropsHousekeepingType = {
|
||||
previousMessageId: undefined | string;
|
||||
unreadIndicatorPlacement: undefined | UnreadIndicatorPlacement;
|
||||
}) => React.JSX.Element;
|
||||
renderMiniPlayer: (options: { shouldFlow: boolean }) => React.JSX.Element;
|
||||
renderPinnedMessagesBar: () => React.JSX.Element;
|
||||
|
||||
renderTypingBubble: (id: string) => React.JSX.Element;
|
||||
};
|
||||
|
||||
export type PropsActionsType = {
|
||||
// From Model
|
||||
acknowledgeGroupMemberNameCollisions: (
|
||||
conversationId: string,
|
||||
groupNameCollisions: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>
|
||||
) => void;
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup: () => void;
|
||||
clearTargetedMessage: () => unknown;
|
||||
closeContactSpoofingReview: () => void;
|
||||
@@ -172,7 +141,6 @@ export type PropsActionsType = {
|
||||
messageId: string | undefined
|
||||
) => void;
|
||||
setIsNearBottom: (conversationId: string, isNearBottom: boolean) => void;
|
||||
reviewConversationNameCollision: () => void;
|
||||
scrollToOldestUnreadMention: (conversationId: string) => unknown;
|
||||
};
|
||||
|
||||
@@ -183,9 +151,7 @@ export type PropsType = PropsDataType &
|
||||
type StateType = {
|
||||
scrollLocked: boolean;
|
||||
scrollLockHeight: number | undefined;
|
||||
hasDismissedDirectContactSpoofingWarning: boolean;
|
||||
hasRecentlyScrolled: boolean;
|
||||
lastMeasuredWarningHeight: number;
|
||||
newestBottomVisibleMessageId?: string;
|
||||
oldestPartiallyVisibleMessageId?: string;
|
||||
widthBreakpoint: WidthBreakpoint;
|
||||
@@ -224,10 +190,7 @@ export class Timeline extends React.Component<
|
||||
scrollLocked: false,
|
||||
scrollLockHeight: undefined,
|
||||
hasRecentlyScrolled: true,
|
||||
hasDismissedDirectContactSpoofingWarning: false,
|
||||
|
||||
// These may be swiftly overridden.
|
||||
lastMeasuredWarningHeight: 0,
|
||||
widthBreakpoint: WidthBreakpoint.Wide,
|
||||
};
|
||||
|
||||
@@ -913,7 +876,6 @@ export class Timeline extends React.Component<
|
||||
|
||||
public override render(): React.JSX.Element | null {
|
||||
const {
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup,
|
||||
closeContactSpoofingReview,
|
||||
conversationType,
|
||||
@@ -931,17 +893,11 @@ export class Timeline extends React.Component<
|
||||
items,
|
||||
messageLoadingState,
|
||||
oldestUnseenIndex,
|
||||
renderCollidingAvatars,
|
||||
renderContactSpoofingReviewDialog,
|
||||
renderHeroRow,
|
||||
renderItem,
|
||||
renderMiniPlayer,
|
||||
renderPinnedMessagesBar,
|
||||
renderTypingBubble,
|
||||
reviewConversationNameCollision,
|
||||
scrollToOldestUnreadMention,
|
||||
shouldShowMiniPlayer,
|
||||
shouldShowPinnedMessagesBar,
|
||||
theme,
|
||||
totalUnseen,
|
||||
unreadCount,
|
||||
@@ -951,7 +907,6 @@ export class Timeline extends React.Component<
|
||||
scrollLocked,
|
||||
scrollLockHeight,
|
||||
hasRecentlyScrolled,
|
||||
lastMeasuredWarningHeight,
|
||||
newestBottomVisibleMessageId,
|
||||
oldestPartiallyVisibleMessageId,
|
||||
widthBreakpoint,
|
||||
@@ -1005,11 +960,6 @@ export class Timeline extends React.Component<
|
||||
<TimelineFloatingHeader
|
||||
i18n={i18n}
|
||||
isLoading={isLoadingMessages}
|
||||
style={
|
||||
lastMeasuredWarningHeight
|
||||
? { marginTop: lastMeasuredWarningHeight }
|
||||
: undefined
|
||||
}
|
||||
timestamp={oldestPartiallyVisibleMessageTimestamp}
|
||||
visible={
|
||||
(hasRecentlyScrolled || isLoadingMessages) &&
|
||||
@@ -1086,118 +1036,6 @@ export class Timeline extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
const warning = Timeline.getWarning(this.props, this.state);
|
||||
let headerElements: ReactNode;
|
||||
if (warning || shouldShowMiniPlayer || shouldShowPinnedMessagesBar) {
|
||||
let text: ReactNode | undefined;
|
||||
let icon: ReactNode | undefined;
|
||||
let onClose: () => void;
|
||||
if (warning) {
|
||||
icon = (
|
||||
<TimelineWarning.IconContainer>
|
||||
<TimelineWarning.GenericIcon />
|
||||
</TimelineWarning.IconContainer>
|
||||
);
|
||||
switch (warning.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle:
|
||||
text = (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-name--link"
|
||||
components={{
|
||||
// This is a render props, not a component
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
reviewRequestLink: parts => (
|
||||
<TimelineWarning.Link
|
||||
onClick={reviewConversationNameCollision}
|
||||
>
|
||||
{parts}
|
||||
</TimelineWarning.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
hasDismissedDirectContactSpoofingWarning: true,
|
||||
});
|
||||
};
|
||||
break;
|
||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle: {
|
||||
const { groupNameCollisions } = warning;
|
||||
const numberOfSharedNames = Object.keys(groupNameCollisions).length;
|
||||
const reviewRequestLink = (
|
||||
parts: Array<string | React.JSX.Element>
|
||||
): React.JSX.Element => (
|
||||
<TimelineWarning.Link onClick={reviewConversationNameCollision}>
|
||||
{parts}
|
||||
</TimelineWarning.Link>
|
||||
);
|
||||
if (numberOfSharedNames === 1) {
|
||||
const [conversationIds] = [...Object.values(groupNameCollisions)];
|
||||
if (conversationIds.length >= 2) {
|
||||
icon = (
|
||||
<TimelineWarning.CustomInfo>
|
||||
{renderCollidingAvatars({ conversationIds })}
|
||||
</TimelineWarning.CustomInfo>
|
||||
);
|
||||
}
|
||||
text = (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-name-in-group--link"
|
||||
components={{
|
||||
count: conversationIds.length,
|
||||
reviewRequestLink,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
text = (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:ContactSpoofing__same-names-in-group--link"
|
||||
components={{
|
||||
count: numberOfSharedNames,
|
||||
reviewRequestLink,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
onClose = () => {
|
||||
acknowledgeGroupMemberNameCollisions(id, groupNameCollisions);
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(warning);
|
||||
}
|
||||
}
|
||||
|
||||
headerElements = (
|
||||
<SizeObserver
|
||||
onSizeChange={size => {
|
||||
this.setState({ lastMeasuredWarningHeight: size.height });
|
||||
}}
|
||||
>
|
||||
{measureRef => (
|
||||
<TimelineWarnings ref={measureRef}>
|
||||
{shouldShowMiniPlayer && renderMiniPlayer({ shouldFlow: true })}
|
||||
{!shouldShowMiniPlayer &&
|
||||
shouldShowPinnedMessagesBar &&
|
||||
renderPinnedMessagesBar()}
|
||||
{text && (
|
||||
<TimelineWarning i18n={i18n} onClose={onClose}>
|
||||
{icon}
|
||||
<TimelineWarning.Text>{text}</TimelineWarning.Text>
|
||||
</TimelineWarning>
|
||||
)}
|
||||
</TimelineWarnings>
|
||||
)}
|
||||
</SizeObserver>
|
||||
);
|
||||
}
|
||||
|
||||
let contactSpoofingReviewDialog: ReactNode;
|
||||
if (hasContactSpoofingReview) {
|
||||
contactSpoofingReviewDialog = renderContactSpoofingReviewDialog({
|
||||
@@ -1237,8 +1075,6 @@ export class Timeline extends React.Component<
|
||||
onKeyDown={this.#handleKeyDown}
|
||||
ref={ref}
|
||||
>
|
||||
{headerElements}
|
||||
|
||||
{floatingHeader}
|
||||
|
||||
<main
|
||||
@@ -1261,14 +1097,7 @@ export class Timeline extends React.Component<
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{haveOldest && (
|
||||
<>
|
||||
{Timeline.getWarning(this.props, this.state) && (
|
||||
<div style={{ height: lastMeasuredWarningHeight }} />
|
||||
)}
|
||||
{renderHeroRow(id)}
|
||||
</>
|
||||
)}
|
||||
{haveOldest && renderHeroRow(id)}
|
||||
|
||||
{messageNodes}
|
||||
|
||||
@@ -1318,31 +1147,6 @@ export class Timeline extends React.Component<
|
||||
</ScrollerLockContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
private static getWarning(
|
||||
{ warning }: PropsType,
|
||||
state: StateType
|
||||
): undefined | WarningType {
|
||||
if (!warning) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (warning.type) {
|
||||
case ContactSpoofingType.DirectConversationWithSameTitle: {
|
||||
const { hasDismissedDirectContactSpoofingWarning } = state;
|
||||
return hasDismissedDirectContactSpoofingWarning ? undefined : warning;
|
||||
}
|
||||
case ContactSpoofingType.MultipleGroupMembersWithSameTitle:
|
||||
return hasUnacknowledgedCollisions(
|
||||
warning.acknowledgedGroupNameCollisions,
|
||||
warning.groupNameCollisions
|
||||
)
|
||||
? warning
|
||||
: undefined;
|
||||
default:
|
||||
throw missingCaseError(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getMessageIdFromElement(
|
||||
|
||||
@@ -13,71 +13,57 @@ const TEXT_CLASS_NAME = `${CLASS_NAME}__text`;
|
||||
const LINK_CLASS_NAME = `${TEXT_CLASS_NAME}__link`;
|
||||
const CLOSE_BUTTON_CLASS_NAME = `${CLASS_NAME}__close-button`;
|
||||
|
||||
type PropsType = {
|
||||
type TimelineWarningProps = Readonly<{
|
||||
customInfo?: ReactNode;
|
||||
children: ReactNode;
|
||||
i18n: LocalizerType;
|
||||
onClose: () => void;
|
||||
};
|
||||
}>;
|
||||
|
||||
export function TimelineWarning({
|
||||
children,
|
||||
i18n,
|
||||
onClose,
|
||||
}: Readonly<PropsType>): React.JSX.Element {
|
||||
export function TimelineWarning(
|
||||
props: TimelineWarningProps
|
||||
): React.JSX.Element {
|
||||
const { i18n } = props;
|
||||
return (
|
||||
<div className={CLASS_NAME}>
|
||||
{children}
|
||||
{props.customInfo}
|
||||
{props.customInfo == null && (
|
||||
<div className={ICON_CONTAINER_CLASS_NAME}>
|
||||
<div className={GENERIC_ICON_CLASS_NAME} />
|
||||
</div>
|
||||
)}
|
||||
<div className={TEXT_CLASS_NAME}>{props.children}</div>
|
||||
<button
|
||||
aria-label={i18n('icu:close')}
|
||||
className={CLOSE_BUTTON_CLASS_NAME}
|
||||
onClick={onClose}
|
||||
onClick={props.onClose}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconContainer({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): React.JSX.Element {
|
||||
return <div className={ICON_CONTAINER_CLASS_NAME}>{children}</div>;
|
||||
}
|
||||
|
||||
TimelineWarning.IconContainer = IconContainer;
|
||||
|
||||
function GenericIcon() {
|
||||
return <div className={GENERIC_ICON_CLASS_NAME} />;
|
||||
}
|
||||
|
||||
TimelineWarning.GenericIcon = GenericIcon;
|
||||
|
||||
function Text({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): React.JSX.Element {
|
||||
return <div className={TEXT_CLASS_NAME}>{children}</div>;
|
||||
}
|
||||
|
||||
TimelineWarning.Text = Text;
|
||||
|
||||
type LinkProps = {
|
||||
type TimelineWarningLinkProps = Readonly<{
|
||||
children: ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
}>;
|
||||
|
||||
function Link({ children, onClick }: Readonly<LinkProps>): React.JSX.Element {
|
||||
export function TimelineWarningLink(
|
||||
props: TimelineWarningLinkProps
|
||||
): React.JSX.Element {
|
||||
return (
|
||||
<button className={LINK_CLASS_NAME} onClick={onClick} type="button">
|
||||
{children}
|
||||
<button className={LINK_CLASS_NAME} onClick={props.onClick} type="button">
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
TimelineWarning.Link = Link;
|
||||
export type TimelineWarningCustomInfoProps = Readonly<{ children: ReactNode }>;
|
||||
|
||||
function CustomInfo({
|
||||
children,
|
||||
}: Readonly<{ children: ReactNode }>): React.JSX.Element {
|
||||
return <div className="module-TimelineWarning__custom_info">{children}</div>;
|
||||
export function TimelineWarningCustomInfo(
|
||||
props: TimelineWarningCustomInfoProps
|
||||
): React.JSX.Element {
|
||||
return (
|
||||
<div className="module-TimelineWarning__custom_info">{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
TimelineWarning.CustomInfo = CustomInfo;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
const CLASS_NAME = 'module-TimelineWarnings';
|
||||
|
||||
type PropsType = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const TimelineWarnings = forwardRef<HTMLDivElement, PropsType>(
|
||||
function TimelineWarningsInner({ children }, ref) {
|
||||
return (
|
||||
<div className={CLASS_NAME} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -161,8 +161,10 @@ function Container(props: {
|
||||
>
|
||||
<AriaClickable.Root
|
||||
className={tw(
|
||||
'flex h-14 items-center bg-background-primary py-2.5 pe-3 select-none',
|
||||
'flex h-14 items-center py-2.5 pe-3 select-none',
|
||||
'rounded-xs',
|
||||
'bg-legacy-conversation-header-bg',
|
||||
'border-t-[0.5px] border-t-border-primary',
|
||||
'outline-0 outline-border-focused',
|
||||
'data-[focused]:outline-[2.5px]',
|
||||
props.pinsCount === 1 && 'ps-4'
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { isEmpty } from 'lodash';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { TimelineItemType } from '../../components/conversation/TimelineItem.dom.js';
|
||||
|
||||
import type { StateType } from '../reducer.preload.js';
|
||||
@@ -12,6 +15,8 @@ import {
|
||||
getMessages,
|
||||
getCachedConversationMemberColorsSelector,
|
||||
getPinnedMessagesMessageIds,
|
||||
getSafeConversationWithSameTitle,
|
||||
getConversationByServiceIdSelector,
|
||||
} from './conversations.dom.js';
|
||||
import { getAccountSelector } from './accounts.std.js';
|
||||
import {
|
||||
@@ -26,6 +31,16 @@ import { getActiveCall, getCallSelector } from './calling.std.js';
|
||||
import { getPropsForBubble } from './message.preload.js';
|
||||
import { getCallHistorySelector } from './callHistory.std.js';
|
||||
import { useProxySelector } from '../../hooks/useProxySelector.std.js';
|
||||
import type { StateSelector } from '../types.std.js';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
|
||||
import {
|
||||
dehydrateCollisionsWithConversations,
|
||||
getCollisionsFromMemberships,
|
||||
type GroupNameCollisionsWithIdsByTitle,
|
||||
} from '../../util/groupMemberNameCollisions.std.js';
|
||||
import type { ConversationType } from '../ducks/conversations.preload.js';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.js';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships.dom.js';
|
||||
|
||||
const getTimelineItem = (
|
||||
state: StateType,
|
||||
@@ -92,3 +107,85 @@ export const useTimelineItem = (
|
||||
|
||||
return useProxySelector(getTimelineItem, messageId, contactNameColors);
|
||||
};
|
||||
|
||||
export type DirectConversationWithSameTitleContactSpoofingWarning =
|
||||
ReadonlyDeep<{
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle;
|
||||
safeConversationId: string;
|
||||
}>;
|
||||
|
||||
export type MultipleGroupMembersWithSameTitleContactSpoofingWarning =
|
||||
ReadonlyDeep<{
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle;
|
||||
acknowledgedGroupNameCollisions: GroupNameCollisionsWithIdsByTitle;
|
||||
groupNameCollisions: GroupNameCollisionsWithIdsByTitle;
|
||||
}>;
|
||||
|
||||
export type ContactSpoofingWarning = ReadonlyDeep<
|
||||
| DirectConversationWithSameTitleContactSpoofingWarning
|
||||
| MultipleGroupMembersWithSameTitleContactSpoofingWarning
|
||||
>;
|
||||
|
||||
export type ContactSpoofingWarningSelector = (
|
||||
conversation: ConversationType
|
||||
) => ContactSpoofingWarning | null;
|
||||
|
||||
export const getContactSpoofingWarningSelector: StateSelector<ContactSpoofingWarningSelector> =
|
||||
createSelector(
|
||||
state => state,
|
||||
rootState => {
|
||||
return (conversation): ContactSpoofingWarning | null => {
|
||||
switch (conversation.type) {
|
||||
case 'direct':
|
||||
if (
|
||||
!conversation.acceptedMessageRequest &&
|
||||
!conversation.isBlocked
|
||||
) {
|
||||
const safeConversation = getSafeConversationWithSameTitle(
|
||||
rootState,
|
||||
{
|
||||
possiblyUnsafeConversation: conversation,
|
||||
}
|
||||
);
|
||||
|
||||
if (safeConversation) {
|
||||
return {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversationId: safeConversation.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
case 'group': {
|
||||
if (conversation.left || conversation.groupVersion !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getConversationByServiceId =
|
||||
getConversationByServiceIdSelector(rootState);
|
||||
|
||||
const { memberships } = getGroupMemberships(
|
||||
conversation,
|
||||
getConversationByServiceId
|
||||
);
|
||||
const groupNameCollisions =
|
||||
getCollisionsFromMemberships(memberships);
|
||||
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
|
||||
if (hasGroupMembersWithSameName) {
|
||||
return {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions:
|
||||
conversation.acknowledgedGroupNameCollisions,
|
||||
groupNameCollisions:
|
||||
dehydrateCollisionsWithConversations(groupNameCollisions),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(conversation);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,13 +6,13 @@ import { CollidingAvatars } from '../../components/CollidingAvatars.dom.js';
|
||||
import { getIntl } from '../selectors/user.std.js';
|
||||
import { getConversationSelector } from '../selectors/conversations.dom.js';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
export type SmartCollidingAvatarsProps = Readonly<{
|
||||
conversationIds: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
export const SmartCollidingAvatars = memo(function SmartCollidingAvatars({
|
||||
conversationIds,
|
||||
}: PropsType) {
|
||||
}: SmartCollidingAvatarsProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
getConversationSelector,
|
||||
getHasPanelOpen,
|
||||
isMissingRequiredProfileSharing as getIsMissingRequiredProfileSharing,
|
||||
getPinnedMessages,
|
||||
getSelectedMessageIds,
|
||||
} from '../selectors/conversations.dom.js';
|
||||
import { getHasStoriesSelector } from '../selectors/stories2.dom.js';
|
||||
@@ -48,6 +49,27 @@ import { getLocalDeleteWarningShown } from '../selectors/items.dom.js';
|
||||
import { isConversationEverUnregistered } from '../../util/isConversationUnregistered.dom.js';
|
||||
import { isDirectConversation } from '../../util/whatTypeOfConversation.dom.js';
|
||||
import type { DurationInSeconds } from '../../util/durations/index.std.js';
|
||||
import { selectAudioPlayerActive } from '../selectors/audioPlayer.preload.js';
|
||||
import type { SmartCollidingAvatarsProps } from './CollidingAvatars.dom.js';
|
||||
import { SmartCollidingAvatars } from './CollidingAvatars.dom.js';
|
||||
import type { SmartMiniPlayerProps } from './MiniPlayer.preload.js';
|
||||
import { SmartMiniPlayer } from './MiniPlayer.preload.js';
|
||||
import { SmartPinnedMessagesBar } from './PinnedMessagesBar.preload.js';
|
||||
import { getContactSpoofingWarningSelector } from '../selectors/timeline.preload.js';
|
||||
|
||||
function renderCollidingAvatars(
|
||||
props: SmartCollidingAvatarsProps
|
||||
): React.JSX.Element {
|
||||
return <SmartCollidingAvatars {...props} />;
|
||||
}
|
||||
|
||||
function renderMiniPlayer(props: SmartMiniPlayerProps): React.JSX.Element {
|
||||
return <SmartMiniPlayer {...props} />;
|
||||
}
|
||||
|
||||
function renderPinnedMessagesBar(): React.JSX.Element {
|
||||
return <SmartPinnedMessagesBar />;
|
||||
}
|
||||
|
||||
export type OwnProps = {
|
||||
id: string;
|
||||
@@ -108,6 +130,17 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
|
||||
const activeCall = useSelector(getActiveCallState);
|
||||
const hasActiveCall = Boolean(activeCall);
|
||||
|
||||
const contactSpoofingWarningSelector = useSelector(
|
||||
getContactSpoofingWarningSelector
|
||||
);
|
||||
const contactSpoofingWarning = contactSpoofingWarningSelector(conversation);
|
||||
|
||||
const activeAudioPlayer = useSelector(selectAudioPlayerActive);
|
||||
const shouldShowMiniPlayer = activeAudioPlayer != null;
|
||||
|
||||
const pinnedMessages = useSelector(getPinnedMessages);
|
||||
const shouldShowPinnedMessagesBar = pinnedMessages.length > 0;
|
||||
|
||||
const {
|
||||
destroyMessages,
|
||||
leaveGroup,
|
||||
@@ -124,6 +157,8 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
|
||||
blockConversation,
|
||||
reportSpam,
|
||||
deleteConversation,
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
reviewConversationNameCollision,
|
||||
} = useConversationsActions();
|
||||
const {
|
||||
onOutgoingAudioCallInConversation,
|
||||
@@ -311,6 +346,16 @@ export const SmartConversationHeader = memo(function SmartConversationHeader({
|
||||
setLocalDeleteWarningShown={setLocalDeleteWarningShown}
|
||||
sharedGroupNames={conversation.sharedGroupNames}
|
||||
theme={theme}
|
||||
contactSpoofingWarning={contactSpoofingWarning}
|
||||
renderCollidingAvatars={renderCollidingAvatars}
|
||||
shouldShowMiniPlayer={shouldShowMiniPlayer}
|
||||
renderMiniPlayer={renderMiniPlayer}
|
||||
shouldShowPinnedMessagesBar={shouldShowPinnedMessagesBar}
|
||||
renderPinnedMessagesBar={renderPinnedMessagesBar}
|
||||
acknowledgeGroupMemberNameCollisions={
|
||||
acknowledgeGroupMemberNameCollisions
|
||||
}
|
||||
reviewConversationNameCollision={reviewConversationNameCollision}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '../selectors/audioPlayer.preload.js';
|
||||
import { getIntl } from '../selectors/user.std.js';
|
||||
|
||||
type Props = Pick<DumbProps, 'shouldFlow'>;
|
||||
export type SmartMiniPlayerProps = Pick<DumbProps, 'shouldFlow'>;
|
||||
|
||||
/**
|
||||
* Wires the dispatch props and shows/hides the MiniPlayer
|
||||
@@ -25,7 +25,7 @@ type Props = Pick<DumbProps, 'shouldFlow'>;
|
||||
*/
|
||||
export const SmartMiniPlayer = memo(function SmartMiniPlayer({
|
||||
shouldFlow,
|
||||
}: Props): React.JSX.Element | null {
|
||||
}: SmartMiniPlayerProps): React.JSX.Element | null {
|
||||
const i18n = useSelector(getIntl);
|
||||
const active = useSelector(selectAudioPlayerActive);
|
||||
const getVoiceNoteTitle = useSelector(selectVoiceNoteTitle);
|
||||
|
||||
@@ -1,46 +1,25 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import lodash from 'lodash';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type { WarningType as TimelineWarningType } from '../../components/conversation/Timeline.dom.js';
|
||||
import { Timeline } from '../../components/conversation/Timeline.dom.js';
|
||||
import { ContactSpoofingType } from '../../util/contactSpoofing.std.js';
|
||||
import { getGroupMemberships } from '../../util/getGroupMemberships.dom.js';
|
||||
import {
|
||||
dehydrateCollisionsWithConversations,
|
||||
getCollisionsFromMemberships,
|
||||
} from '../../util/groupMemberNameCollisions.std.js';
|
||||
import { missingCaseError } from '../../util/missingCaseError.std.js';
|
||||
import { useCallingActions } from '../ducks/calling.preload.js';
|
||||
import {
|
||||
useConversationsActions,
|
||||
type ConversationType,
|
||||
} from '../ducks/conversations.preload.js';
|
||||
import type { StateType } from '../reducer.preload.js';
|
||||
import { selectAudioPlayerActive } from '../selectors/audioPlayer.preload.js';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.js';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges.preload.js';
|
||||
import {
|
||||
getConversationByServiceIdSelector,
|
||||
getConversationMessagesSelector,
|
||||
getConversationSelector,
|
||||
getHasContactSpoofingReview,
|
||||
getInvitedContactsForNewlyCreatedGroup,
|
||||
getMessages,
|
||||
getSafeConversationWithSameTitle,
|
||||
getSelectedConversationId,
|
||||
getTargetedMessage,
|
||||
getPinnedMessages,
|
||||
} from '../selectors/conversations.dom.js';
|
||||
import { getIntl, getTheme } from '../selectors/user.std.js';
|
||||
import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars.dom.js';
|
||||
import { SmartCollidingAvatars } from './CollidingAvatars.dom.js';
|
||||
import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog.preload.js';
|
||||
import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog.preload.js';
|
||||
import { SmartHeroRow } from './HeroRow.preload.js';
|
||||
import { SmartMiniPlayer } from './MiniPlayer.preload.js';
|
||||
import {
|
||||
SmartTimelineItem,
|
||||
type SmartTimelineItemProps,
|
||||
@@ -48,9 +27,6 @@ import {
|
||||
import { SmartTypingBubble } from './TypingBubble.preload.js';
|
||||
import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager.preload.js';
|
||||
import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling.std.js';
|
||||
import { SmartPinnedMessagesBar } from './PinnedMessagesBar.preload.js';
|
||||
|
||||
const { isEmpty } = lodash;
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
@@ -86,12 +62,6 @@ function renderItem({
|
||||
);
|
||||
}
|
||||
|
||||
function renderCollidingAvatars(
|
||||
props: SmartCollidingAvatarsPropsType
|
||||
): React.JSX.Element {
|
||||
return <SmartCollidingAvatars {...props} />;
|
||||
}
|
||||
|
||||
function renderContactSpoofingReviewDialog(
|
||||
props: SmartContactSpoofingReviewDialogPropsType
|
||||
): React.JSX.Element {
|
||||
@@ -101,70 +71,13 @@ function renderContactSpoofingReviewDialog(
|
||||
function renderHeroRow(id: string): React.JSX.Element {
|
||||
return <SmartHeroRow id={id} />;
|
||||
}
|
||||
function renderMiniPlayer(options: { shouldFlow: boolean }): React.JSX.Element {
|
||||
return <SmartMiniPlayer {...options} />;
|
||||
}
|
||||
function renderPinnedMessagesBar(): React.JSX.Element {
|
||||
return <SmartPinnedMessagesBar />;
|
||||
}
|
||||
function renderTypingBubble(conversationId: string): React.JSX.Element {
|
||||
return <SmartTypingBubble conversationId={conversationId} />;
|
||||
}
|
||||
|
||||
const getWarning = (
|
||||
conversation: ReadonlyDeep<ConversationType>,
|
||||
state: Readonly<StateType>
|
||||
): undefined | TimelineWarningType => {
|
||||
switch (conversation.type) {
|
||||
case 'direct':
|
||||
if (!conversation.acceptedMessageRequest && !conversation.isBlocked) {
|
||||
const safeConversation = getSafeConversationWithSameTitle(state, {
|
||||
possiblyUnsafeConversation: conversation,
|
||||
});
|
||||
|
||||
if (safeConversation) {
|
||||
return {
|
||||
type: ContactSpoofingType.DirectConversationWithSameTitle,
|
||||
safeConversationId: safeConversation.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
case 'group': {
|
||||
if (conversation.left || conversation.groupVersion !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const getConversationByServiceId =
|
||||
getConversationByServiceIdSelector(state);
|
||||
|
||||
const { memberships } = getGroupMemberships(
|
||||
conversation,
|
||||
getConversationByServiceId
|
||||
);
|
||||
const groupNameCollisions = getCollisionsFromMemberships(memberships);
|
||||
const hasGroupMembersWithSameName = !isEmpty(groupNameCollisions);
|
||||
if (hasGroupMembersWithSameName) {
|
||||
return {
|
||||
type: ContactSpoofingType.MultipleGroupMembersWithSameTitle,
|
||||
acknowledgedGroupNameCollisions:
|
||||
conversation.acknowledgedGroupNameCollisions,
|
||||
groupNameCollisions:
|
||||
dehydrateCollisionsWithConversations(groupNameCollisions),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(conversation);
|
||||
}
|
||||
};
|
||||
|
||||
export const SmartTimeline = memo(function SmartTimeline({
|
||||
id,
|
||||
}: ExternalProps) {
|
||||
const activeAudioPlayer = useSelector(selectAudioPlayerActive);
|
||||
const conversationMessagesSelector = useSelector(
|
||||
getConversationMessagesSelector
|
||||
);
|
||||
@@ -182,19 +95,8 @@ export const SmartTimeline = memo(function SmartTimeline({
|
||||
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
|
||||
const conversation = conversationSelector(id);
|
||||
const conversationMessages = conversationMessagesSelector(id);
|
||||
const pinnedMessages = useSelector(getPinnedMessages);
|
||||
|
||||
const warning = useSelector(
|
||||
useCallback(
|
||||
(state: StateType) => {
|
||||
return getWarning(conversation, state);
|
||||
},
|
||||
[conversation]
|
||||
)
|
||||
);
|
||||
|
||||
const {
|
||||
acknowledgeGroupMemberNameCollisions,
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup,
|
||||
clearTargetedMessage,
|
||||
closeContactSpoofingReview,
|
||||
@@ -203,7 +105,6 @@ export const SmartTimeline = memo(function SmartTimeline({
|
||||
loadNewestMessages,
|
||||
loadOlderMessages,
|
||||
markMessageRead,
|
||||
reviewConversationNameCollision,
|
||||
scrollToOldestUnreadMention,
|
||||
setCenterMessage,
|
||||
setIsNearBottom,
|
||||
@@ -218,8 +119,6 @@ export const SmartTimeline = memo(function SmartTimeline({
|
||||
[messages]
|
||||
);
|
||||
|
||||
const shouldShowMiniPlayer = activeAudioPlayer != null;
|
||||
const shouldShowPinnedMessagesBar = pinnedMessages.length > 0;
|
||||
const {
|
||||
acceptedMessageRequest,
|
||||
isBlocked = false,
|
||||
@@ -251,9 +150,6 @@ export const SmartTimeline = memo(function SmartTimeline({
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
acknowledgeGroupMemberNameCollisions={
|
||||
acknowledgeGroupMemberNameCollisions
|
||||
}
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup={
|
||||
clearInvitedServiceIdsForNewlyCreatedGroup
|
||||
}
|
||||
@@ -288,28 +184,21 @@ export const SmartTimeline = memo(function SmartTimeline({
|
||||
AttachmentDownloadManager.updateVisibleTimelineMessages
|
||||
}
|
||||
oldestUnseenIndex={oldestUnseenIndex}
|
||||
renderCollidingAvatars={renderCollidingAvatars}
|
||||
renderContactSpoofingReviewDialog={renderContactSpoofingReviewDialog}
|
||||
renderHeroRow={renderHeroRow}
|
||||
renderItem={renderItem}
|
||||
renderMiniPlayer={renderMiniPlayer}
|
||||
renderPinnedMessagesBar={renderPinnedMessagesBar}
|
||||
renderTypingBubble={renderTypingBubble}
|
||||
reviewConversationNameCollision={reviewConversationNameCollision}
|
||||
scrollToIndex={scrollToIndex}
|
||||
scrollToIndexCounter={scrollToIndexCounter}
|
||||
scrollToOldestUnreadMention={scrollToOldestUnreadMention}
|
||||
setCenterMessage={setCenterMessage}
|
||||
setIsNearBottom={setIsNearBottom}
|
||||
shouldShowMiniPlayer={shouldShowMiniPlayer}
|
||||
shouldShowPinnedMessagesBar={shouldShowPinnedMessagesBar}
|
||||
targetedMessageId={targetedMessageId}
|
||||
targetMessage={targetMessage}
|
||||
theme={theme}
|
||||
totalUnseen={totalUnseen}
|
||||
unreadCount={unreadCount}
|
||||
unreadMentionsCount={unreadMentionsCount}
|
||||
warning={warning}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user