feat: added notice banner component

show notice when conversation is in legacy mode, extracted some UI components from the ConversationHeader, updated featureReleaseTimestamp
pull/2660/head
William Grant 2 years ago
parent 93005d32d8
commit 4dbbada728

@ -211,6 +211,7 @@
"timerOption_2_weeks_abbreviated": "2w",
"disappearingMessages": "Disappearing messages",
"disappearingMessagesSubtitle": "This setting applies to everyone in this conversation",
"disappearingMessagesModeOutdated": "$name$ is using an outdated client. Disappearing messages may not work as expected.",
"disappearingMessagesModeLabel": "Delete Type",
"disappearingMessagesModeOff": "Off",
"disappearingMessagesModeAfterRead": "Disappear After Read",

@ -0,0 +1,31 @@
import React from 'react';
import styled from 'styled-components';
import { Flex } from './basic/Flex';
const StyledNoticeBanner = styled(Flex)`
background-color: var(--primary-color);
color: var(--background-primary-color);
min-height: 30px;
font-size: var(--font-size-lg);
padding: 0 var(--margins-sm);
text-align: center;
`;
type NoticeBannerProps = {
text: string;
};
export const NoticeBanner = (props: NoticeBannerProps) => {
const { text } = props;
return (
<StyledNoticeBanner
container={true}
flexDirection={'row'}
justifyContent={'center'}
alignItems={'center'}
>
{text}
</StyledNoticeBanner>
);
};

@ -1,283 +0,0 @@
import React from 'react';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { contextMenu } from 'react-contexify';
import styled from 'styled-components';
import { ConversationNotificationSettingType } from '../../models/conversationAttributes';
import {
getIsSelectedActive,
getIsSelectedBlocked,
getIsSelectedNoteToSelf,
getIsSelectedPrivate,
getSelectedConversationIsPublic,
getSelectedConversationKey,
getSelectedMessageIds,
isMessageDetailView,
isMessageSelectionMode,
} from '../../state/selectors/conversations';
import { useDispatch, useSelector } from 'react-redux';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../interactions/conversations/unsendingInteractions';
import {
closeMessageDetailsView,
openRightPanel,
resetSelectedMessageIds,
} from '../../state/ducks/conversations';
import { callRecipient } from '../../interactions/conversationInteractions';
import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call';
import { useIsRequest } from '../../hooks/useParamSelector';
import {
SessionButton,
SessionButtonColor,
SessionButtonShape,
SessionButtonType,
} from '../basic/SessionButton';
import { SessionIconButton } from '../icon';
import { ConversationHeaderMenu } from '../menu/ConversationHeaderMenu';
import { Flex } from '../basic/Flex';
import { ConversationHeaderTitle } from './ConversationHeaderTitle';
export interface TimerOption {
name: string;
value: number;
}
export type ConversationHeaderProps = {
conversationKey: string;
name?: string;
profileName?: string;
avatarPath: string | null;
isMe: boolean;
isGroup: boolean;
isPrivate: boolean;
isPublic: boolean;
weAreAdmin: boolean;
// We might not always have the full list of members,
// e.g. for open groups where we could have thousands
// of members. We'll keep this for now (for closed chats)
members: Array<any>;
// not equal members.length (see above)
subscriberCount?: number;
expirationSettingName?: string;
currentNotificationSetting: ConversationNotificationSettingType;
hasNickname: boolean;
isBlocked: boolean;
isKickedFromGroup: boolean;
left: boolean;
};
const SelectionOverlay = () => {
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversationKey = useSelector(getSelectedConversationKey);
const isPublic = useSelector(getSelectedConversationIsPublic);
const dispatch = useDispatch();
const { i18n } = window;
function onCloseOverlay() {
dispatch(resetSelectedMessageIds());
}
function onDeleteSelectedMessages() {
if (selectedConversationKey) {
void deleteMessagesById(selectedMessageIds, selectedConversationKey);
}
}
function onDeleteSelectedMessagesForEveryone() {
if (selectedConversationKey) {
void deleteMessagesByIdForEveryone(selectedMessageIds, selectedConversationKey);
}
}
const isOnlyServerDeletable = isPublic;
const deleteMessageButtonText = i18n('delete');
const deleteForEveryoneMessageButtonText = i18n('deleteForEveryone');
return (
<div className="message-selection-overlay">
<div className="close-button">
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
</div>
<div className="button-group">
{!isOnlyServerDeletable && (
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={deleteMessageButtonText}
onClick={onDeleteSelectedMessages}
/>
)}
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={deleteForEveryoneMessageButtonText}
onClick={onDeleteSelectedMessagesForEveryone}
/>
</div>
</div>
);
};
const TripleDotContainer = styled.div`
user-select: none;
flex-grow: 0;
flex-shrink: 0;
`;
const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => {
const { showBackButton } = props;
if (showBackButton) {
return null;
}
return (
<TripleDotContainer
role="button"
onClick={(e: any) => {
contextMenu.show({
id: props.triggerId,
event: e,
});
}}
data-testid="three-dots-conversation-options"
>
<SessionIconButton iconType="ellipses" iconSize="medium" />
</TripleDotContainer>
);
};
const AvatarHeader = (props: {
pubkey: string;
showBackButton: boolean;
onAvatarClick?: (pubkey: string) => void;
}) => {
const { pubkey, onAvatarClick, showBackButton } = props;
return (
<span className="module-conversation-header__avatar">
<Avatar
size={AvatarSize.S}
onAvatarClick={() => {
// do not allow right panel to appear if another button is shown on the SessionConversation
if (onAvatarClick && !showBackButton) {
onAvatarClick(pubkey);
}
}}
pubkey={pubkey}
dataTestId="conversation-options-avatar"
/>
</span>
);
};
const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => {
const { onGoBack, showBackButton } = props;
if (!showBackButton) {
return null;
}
return (
<SessionIconButton
iconType="chevron"
iconSize="large"
iconRotation={90}
onClick={onGoBack}
dataTestId="back-button-message-details"
/>
);
};
const CallButton = () => {
const isPrivate = useSelector(getIsSelectedPrivate);
const isBlocked = useSelector(getIsSelectedBlocked);
const activeAt = useSelector(getIsSelectedActive);
const isMe = useSelector(getIsSelectedNoteToSelf);
const selectedConvoKey = useSelector(getSelectedConversationKey);
const hasIncomingCall = useSelector(getHasIncomingCall);
const hasOngoingCall = useSelector(getHasOngoingCall);
const canCall = !(hasIncomingCall || hasOngoingCall);
const isRequest = useIsRequest(selectedConvoKey);
if (!isPrivate || isMe || !selectedConvoKey || isBlocked || !activeAt || isRequest) {
return null;
}
return (
<SessionIconButton
iconType="phone"
iconSize="large"
iconPadding="2px"
// negative margin to keep conversation header title centered
margin="0 10px 0 -32px"
onClick={() => {
void callRecipient(selectedConvoKey, canCall);
}}
/>
);
};
export const ConversationHeaderWithDetails = () => {
const isSelectionMode = useSelector(isMessageSelectionMode);
const isMessageDetailOpened = useSelector(isMessageDetailView);
const selectedConvoKey = useSelector(getSelectedConversationKey);
const dispatch = useDispatch();
if (!selectedConvoKey) {
return null;
}
const triggerId = 'conversation-header';
return (
<div className="module-conversation-header">
<div className="conversation-header--items-wrapper">
<BackButton
onGoBack={() => {
dispatch(closeMessageDetailsView());
}}
showBackButton={isMessageDetailOpened}
/>
<TripleDotsMenu triggerId={triggerId} showBackButton={isMessageDetailOpened} />
<ConversationHeaderTitle />
{!isSelectionMode && (
<Flex
container={true}
flexDirection="row"
alignItems="center"
flexGrow={0}
flexShrink={0}
>
<CallButton />
<AvatarHeader
onAvatarClick={() => {
dispatch(openRightPanel());
}}
pubkey={selectedConvoKey}
showBackButton={isMessageDetailOpened}
/>
</Flex>
)}
<ConversationHeaderMenu triggerId={triggerId} />
</div>
{isSelectionMode && <SelectionOverlay />}
</div>
);
};

@ -40,7 +40,7 @@ import { AttachmentTypeWithPath } from '../../types/Attachment';
import { arrayBufferToObjectURL, AttachmentUtil, GoogleChrome } from '../../util';
import { SessionButtonColor } from '../basic/SessionButton';
import { MessageView } from '../MainViewController';
import { ConversationHeaderWithDetails } from './ConversationHeader';
import { ConversationHeaderWithDetails } from './header/ConversationHeader';
import { MessageDetail } from './message/message-item/MessageDetail';
import {
makeImageThumbnailBuffer,
@ -58,6 +58,7 @@ import { markAllReadByConvoId } from '../../interactions/conversationInteraction
import { SessionSpinner } from '../basic/SessionSpinner';
import styled from 'styled-components';
import { RightPanel } from './right-panel/RightPanel';
import { NoticeBanner } from '../NoticeBanner';
// tslint:disable: jsx-curly-spacing
interface State {
@ -254,6 +255,15 @@ export class SessionConversation extends React.Component<Props, State> {
<SessionTheme>
<div className="conversation-header">
<ConversationHeaderWithDetails />
{selectedConversation.expirationType === 'legacy' && (
<NoticeBanner
text={window.i18n('disappearingMessagesModeOutdated', [
selectedConversation?.nickname ||
selectedConversation?.displayNameInProfile ||
selectedConversation.id,
])}
/>
)}
</div>
{isSelectedConvoInitialLoadingInProgress ? (
<ConvoLoadingSpinner />

@ -0,0 +1,104 @@
import React from 'react';
import { ConversationNotificationSettingType } from '../../../models/conversationAttributes';
import {
getSelectedConversationKey,
isMessageDetailView,
isMessageSelectionMode,
} from '../../../state/selectors/conversations';
import { useDispatch, useSelector } from 'react-redux';
import { closeMessageDetailsView, openRightPanel } from '../../../state/ducks/conversations';
import { ConversationHeaderMenu } from '../../menu/ConversationHeaderMenu';
import { Flex } from '../../basic/Flex';
import { ConversationHeaderTitle } from './ConversationHeaderTitle';
import { AvatarHeader, BackButton, CallButton, TripleDotsMenu } from './ConversationHeaderItems';
import { SelectionOverlay } from './ConversationHeaderSelectionOverlay';
export interface TimerOption {
name: string;
value: number;
}
export type ConversationHeaderProps = {
conversationKey: string;
name?: string;
profileName?: string;
avatarPath: string | null;
isMe: boolean;
isGroup: boolean;
isPrivate: boolean;
isPublic: boolean;
weAreAdmin: boolean;
// We might not always have the full list of members,
// e.g. for open groups where we could have thousands
// of members. We'll keep this for now (for closed chats)
members: Array<any>;
// not equal members.length (see above)
subscriberCount?: number;
expirationSettingName?: string;
currentNotificationSetting: ConversationNotificationSettingType;
hasNickname: boolean;
isBlocked: boolean;
isKickedFromGroup: boolean;
left: boolean;
};
export const ConversationHeaderWithDetails = () => {
const isSelectionMode = useSelector(isMessageSelectionMode);
const isMessageDetailOpened = useSelector(isMessageDetailView);
const selectedConvoKey = useSelector(getSelectedConversationKey);
const dispatch = useDispatch();
if (!selectedConvoKey) {
return null;
}
const triggerId = 'conversation-header';
return (
<div className="module-conversation-header">
<div className="conversation-header--items-wrapper">
<BackButton
onGoBack={() => {
dispatch(closeMessageDetailsView());
}}
showBackButton={isMessageDetailOpened}
/>
<TripleDotsMenu triggerId={triggerId} showBackButton={isMessageDetailOpened} />
<ConversationHeaderTitle />
{!isSelectionMode && (
<Flex
container={true}
flexDirection="row"
alignItems="center"
flexGrow={0}
flexShrink={0}
>
<CallButton />
<AvatarHeader
onAvatarClick={() => {
dispatch(openRightPanel());
}}
pubkey={selectedConvoKey}
showBackButton={isMessageDetailOpened}
/>
</Flex>
)}
<ConversationHeaderMenu triggerId={triggerId} />
</div>
{isSelectionMode && <SelectionOverlay />}
</div>
);
};

@ -0,0 +1,115 @@
import React from 'react';
import { contextMenu } from 'react-contexify';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useIsRequest } from '../../../hooks/useParamSelector';
import { callRecipient } from '../../../interactions/conversationInteractions';
import { getHasIncomingCall, getHasOngoingCall } from '../../../state/selectors/call';
import {
getIsSelectedActive,
getIsSelectedBlocked,
getIsSelectedNoteToSelf,
getIsSelectedPrivate,
getSelectedConversationKey,
} from '../../../state/selectors/conversations';
import { Avatar, AvatarSize } from '../../avatar/Avatar';
import { SessionIconButton } from '../../icon';
const TripleDotContainer = styled.div`
user-select: none;
flex-grow: 0;
flex-shrink: 0;
`;
export const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => {
const { showBackButton } = props;
if (showBackButton) {
return null;
}
return (
<TripleDotContainer
role="button"
onClick={(e: any) => {
contextMenu.show({
id: props.triggerId,
event: e,
});
}}
data-testid="three-dots-conversation-options"
>
<SessionIconButton iconType="ellipses" iconSize="medium" />
</TripleDotContainer>
);
};
export const AvatarHeader = (props: {
pubkey: string;
showBackButton: boolean;
onAvatarClick?: (pubkey: string) => void;
}) => {
const { pubkey, onAvatarClick, showBackButton } = props;
return (
<span className="module-conversation-header__avatar">
<Avatar
size={AvatarSize.S}
onAvatarClick={() => {
// do not allow right panel to appear if another button is shown on the SessionConversation
if (onAvatarClick && !showBackButton) {
onAvatarClick(pubkey);
}
}}
pubkey={pubkey}
dataTestId="conversation-options-avatar"
/>
</span>
);
};
export const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => {
const { onGoBack, showBackButton } = props;
if (!showBackButton) {
return null;
}
return (
<SessionIconButton
iconType="chevron"
iconSize="large"
iconRotation={90}
onClick={onGoBack}
dataTestId="back-button-message-details"
/>
);
};
export const CallButton = () => {
const isPrivate = useSelector(getIsSelectedPrivate);
const isBlocked = useSelector(getIsSelectedBlocked);
const activeAt = useSelector(getIsSelectedActive);
const isMe = useSelector(getIsSelectedNoteToSelf);
const selectedConvoKey = useSelector(getSelectedConversationKey);
const hasIncomingCall = useSelector(getHasIncomingCall);
const hasOngoingCall = useSelector(getHasOngoingCall);
const canCall = !(hasIncomingCall || hasOngoingCall);
const isRequest = useIsRequest(selectedConvoKey);
if (!isPrivate || isMe || !selectedConvoKey || isBlocked || !activeAt || isRequest) {
return null;
}
return (
<SessionIconButton
iconType="phone"
iconSize="large"
iconPadding="2px"
// negative margin to keep conversation header title centered
margin="0 10px 0 -32px"
onClick={() => {
void callRecipient(selectedConvoKey, canCall);
}}
/>
);
};

@ -0,0 +1,74 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../../interactions/conversations/unsendingInteractions';
import { resetSelectedMessageIds } from '../../../state/ducks/conversations';
import {
getSelectedConversationIsPublic,
getSelectedConversationKey,
getSelectedMessageIds,
} from '../../../state/selectors/conversations';
import {
SessionButton,
SessionButtonColor,
SessionButtonShape,
SessionButtonType,
} from '../../basic/SessionButton';
import { SessionIconButton } from '../../icon';
export const SelectionOverlay = () => {
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversationKey = useSelector(getSelectedConversationKey);
const isPublic = useSelector(getSelectedConversationIsPublic);
const dispatch = useDispatch();
const { i18n } = window;
function onCloseOverlay() {
dispatch(resetSelectedMessageIds());
}
function onDeleteSelectedMessages() {
if (selectedConversationKey) {
void deleteMessagesById(selectedMessageIds, selectedConversationKey);
}
}
function onDeleteSelectedMessagesForEveryone() {
if (selectedConversationKey) {
void deleteMessagesByIdForEveryone(selectedMessageIds, selectedConversationKey);
}
}
const isOnlyServerDeletable = isPublic;
const deleteMessageButtonText = i18n('delete');
const deleteForEveryoneMessageButtonText = i18n('deleteForEveryone');
return (
<div className="message-selection-overlay">
<div className="close-button">
<SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
</div>
<div className="button-group">
{!isOnlyServerDeletable && (
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={deleteMessageButtonText}
onClick={onDeleteSelectedMessages}
/>
)}
<SessionButton
buttonColor={SessionButtonColor.Danger}
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={deleteForEveryoneMessageButtonText}
onClick={onDeleteSelectedMessagesForEveryone}
/>
</div>
</div>
);
};

@ -1,21 +1,21 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled, { CSSProperties } from 'styled-components';
import { useConversationUsername } from '../../hooks/useParamSelector';
import { ConversationNotificationSettingType } from '../../models/conversationAttributes';
import { closeRightPanel, openRightPanel } from '../../state/ducks/conversations';
import { setRightOverlayMode } from '../../state/ducks/section';
import { useConversationUsername } from '../../../hooks/useParamSelector';
import { ConversationNotificationSettingType } from '../../../models/conversationAttributes';
import { closeRightPanel, openRightPanel } from '../../../state/ducks/conversations';
import { setRightOverlayMode } from '../../../state/ducks/section';
import {
getConversationHeaderTitleProps,
getCurrentNotificationSettingText,
isRightPanelShowing,
} from '../../state/selectors/conversations';
} from '../../../state/selectors/conversations';
import {
DisappearingMessageConversationType,
ExpirationTimerOptions,
} from '../../util/expiringMessages';
import { Flex } from '../basic/Flex';
import { SessionIconButton } from '../icon';
} from '../../../util/expiringMessages';
import { Flex } from '../../basic/Flex';
import { SessionIconButton } from '../../icon';
export const StyledSubtitleContainer = styled.div`
display: flex;

@ -1225,7 +1225,8 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite
// same value in ts/util/releaseFeature.ts but we cannot import since window doesn't exist yet.
// TODO update to agreed value between platforms
const featureReleaseTimestamp = 1676851200000; // unix 13/02/2023
const featureReleaseTimestamp = 1677574800000; // unix 28/02/2023 09:00
// support disppearing messages legacy mode until after the platform agreed timestamp
if (Date.now() < featureReleaseTimestamp) {
db.prepare(

@ -37,7 +37,7 @@ import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions';
import { filter, isEmpty, pick, sortBy } from 'lodash';
import { DisappearingMessageConversationSetting } from '../../util/expiringMessages';
import { ConversationHeaderTitleProps } from '../../components/conversation/ConversationHeaderTitle';
import { ConversationHeaderTitleProps } from '../../components/conversation/header/ConversationHeaderTitle';
export const getConversations = (state: StateType): ConversationsStateType => state.conversations;

@ -211,6 +211,7 @@ export type LocalizerKeys =
| 'timerOption_2_weeks_abbreviated'
| 'disappearingMessages'
| 'disappearingMessagesSubtitle'
| 'disappearingMessagesModeOutdated'
| 'disappearingMessagesModeLabel'
| 'disappearingMessagesModeOff'
| 'disappearingMessagesModeAfterRead'

@ -1,7 +1,7 @@
import { Data } from '../data/data';
// TODO update to agreed value between platforms
const featureReleaseTimestamp = 1676851200000; // unix 13/02/2023
const featureReleaseTimestamp = 1677574800000; // unix 28/02/2023 09:00
// const featureReleaseTimestamp = 1676608378; // test value
let isFeatureReleased: boolean | undefined;

Loading…
Cancel
Save