Update header actions/add hiddenFromConversationSearch

This commit is contained in:
Jamie Kyle 2023-06-29 11:40:00 -07:00 committed by GitHub
parent 00250e535c
commit af4ad55c68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 27 deletions

View file

@ -368,11 +368,11 @@
"description": "Undoes Archive Conversation action, and moves archived conversation back to the main conversation list"
},
"icu:pinConversation": {
"messageformat": "Pin Chat",
"messageformat": "Pin chat",
"description": "Shown in menu for conversation, and pins the conversation to the top of the conversation list"
},
"icu:unpinConversation": {
"messageformat": "Unpin Chat",
"messageformat": "Unpin chat",
"description": "Undoes Archive Conversation action, and unpins the conversation from the top of the conversation list"
},
"icu:pinnedConversationsFull": {
@ -1282,7 +1282,35 @@
},
"icu:deleteConversationConfirmation": {
"messageformat": "Permanently delete this chat?",
"description": "Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
"description": "(deleted 06/27/2023) Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
},
"icu:ConversationHeader__DeleteMessagesConfirmation__title": {
"messageformat": "Delete chat?",
"description": "Conversation Header > Delete Action > Delete Messages Confirmation Modal > Title"
},
"icu:ConversationHeader__DeleteMessagesConfirmation__description": {
"messageformat": "This chat will be deleted from this device.",
"description": "Conversation Header > Delete Action > Delete Messages Confirmation Modal > Description"
},
"icu:ConversationHeader__ContextMenu__LeaveGroupAction__title": {
"messageformat": "Leave group",
"description": "This is a button to leave a group"
},
"icu:ConversationHeader__LeaveGroupConfirmation__title": {
"messageformat": "Do you really want to leave?",
"description": "Conversation Header > Leave Group Action > Leave Group Confirmation Modal > Title"
},
"icu:ConversationHeader__LeaveGroupConfirmation__description": {
"messageformat": "You will no longer be able to send or receive messages in this group.",
"description": "Conversation Header > Leave Group Action > Leave Group Confirmation Modal > Description"
},
"icu:ConversationHeader__LeaveGroupConfirmation__confirmButton": {
"messageformat": "Leave",
"description": "Conversation Header > Leave Group Action > Leave Group Confirmation Modal > Confirm Button"
},
"icu:ConversationHeader__CannotLeaveGroupBecauseYouAreLastAdminAlert__description": {
"messageformat": "Before you leave, you must choose at least one new admin for this group.",
"description": "Conversation Header > Leave Group Action > Cannot Leave Group Because You Are Last Admin Alert > Description"
},
"icu:sessionEnded": {
"messageformat": "Secure session reset",

View file

@ -32,6 +32,7 @@ type ItemsType = Array<{
const commonProps = {
...getDefaultConversation(),
cannotLeaveBecauseYouAreLastAdmin: false,
showBackButton: false,
outgoingCallButtonStyle: OutgoingCallButtonStyle.Both,
@ -39,6 +40,7 @@ const commonProps = {
setDisappearingMessages: action('setDisappearingMessages'),
destroyMessages: action('destroyMessages'),
leaveGroup: action('leaveGroup'),
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),

View file

@ -39,6 +39,7 @@ import {
} from '../../hooks/useKeyboardShortcuts';
import { PanelType } from '../../types/Panels';
import { UserText } from '../UserText';
import { Alert } from '../Alert';
export enum OutgoingCallButtonStyle {
None,
@ -49,6 +50,7 @@ export enum OutgoingCallButtonStyle {
export type PropsDataType = {
badge?: BadgeType;
cannotLeaveBecauseYouAreLastAdmin: boolean;
conversationTitle?: string;
hasStories?: HasStories;
isMissingMandatoryProfileSharing?: boolean;
@ -86,6 +88,7 @@ export type PropsDataType = {
export type PropsActionsType = {
destroyMessages: (conversationId: string) => void;
leaveGroup: (conversationId: string) => void;
onArchive: (conversationId: string) => void;
onMarkUnread: (conversationId: string) => void;
toggleSelectMode: (on: boolean) => void;
@ -119,6 +122,8 @@ enum ModalState {
type StateType = {
hasDeleteMessagesConfirmation: boolean;
hasLeaveGroupConfirmation: boolean;
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: boolean;
isNarrow: boolean;
modalState: ModalState;
};
@ -139,6 +144,8 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
this.state = {
hasDeleteMessagesConfirmation: false,
hasLeaveGroupConfirmation: false,
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false,
isNarrow: false,
modalState: ModalState.NothingOpen,
};
@ -338,6 +345,7 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
const {
acceptedMessageRequest,
canChangeTimer,
cannotLeaveBecauseYouAreLastAdmin,
expireTimer,
groupVersion,
i18n,
@ -532,11 +540,6 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
{i18n('icu:viewRecentMedia')}
</MenuItem>
<MenuItem divider />
{!markedUnread ? (
<MenuItem onClick={() => onMarkUnread(id)}>
{i18n('icu:markUnread')}
</MenuItem>
) : null}
<MenuItem
onClick={() => {
toggleSelectMode(true);
@ -544,6 +547,21 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
>
{i18n('icu:ConversationHeader__menu__selectMessages')}
</MenuItem>
<MenuItem divider />
{!markedUnread ? (
<MenuItem onClick={() => onMarkUnread(id)}>
{i18n('icu:markUnread')}
</MenuItem>
) : null}
{isPinned ? (
<MenuItem onClick={() => setPinned(id, false)}>
{i18n('icu:unpinConversation')}
</MenuItem>
) : (
<MenuItem onClick={() => setPinned(id, true)}>
{i18n('icu:pinConversation')}
</MenuItem>
)}
{isArchived ? (
<MenuItem onClick={() => onMoveToInbox(id)}>
{i18n('icu:moveConversationToInbox')}
@ -558,20 +576,28 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
>
{i18n('icu:deleteMessages')}
</MenuItem>
{isPinned ? (
<MenuItem onClick={() => setPinned(id, false)}>
{i18n('icu:unpinConversation')}
</MenuItem>
) : (
<MenuItem onClick={() => setPinned(id, true)}>
{i18n('icu:pinConversation')}
{isGroup && (
<MenuItem
onClick={() => {
if (cannotLeaveBecauseYouAreLastAdmin) {
this.setState({
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
});
} else {
this.setState({ hasLeaveGroupConfirmation: true });
}
}}
>
{i18n(
'icu:ConversationHeader__ContextMenu__LeaveGroupAction__title'
)}
</MenuItem>
)}
</ContextMenu>
);
}
private renderConfirmationDialog(): ReactNode {
private renderDeleteMessagesConfirmationDialog(): ReactNode {
const { hasDeleteMessagesConfirmation } = this.state;
const { destroyMessages, i18n, id } = this.props;
@ -582,6 +608,9 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
return (
<ConfirmationDialog
dialogName="ConversationHeader.destroyMessages"
title={i18n(
'icu:ConversationHeader__DeleteMessagesConfirmation__title'
)}
actions={[
{
action: () => {
@ -597,11 +626,79 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
this.setState({ hasDeleteMessagesConfirmation: false });
}}
>
{i18n('icu:deleteConversationConfirmation')}
{i18n(
'icu:ConversationHeader__DeleteMessagesConfirmation__description'
)}
</ConfirmationDialog>
);
}
private renderLeaveGroupConfirmationDialog(): ReactNode {
const { hasLeaveGroupConfirmation } = this.state;
const { cannotLeaveBecauseYouAreLastAdmin, leaveGroup, i18n, id } =
this.props;
if (!hasLeaveGroupConfirmation) {
return;
}
return (
<ConfirmationDialog
dialogName="ConversationHeader.leaveGroup"
title={i18n('icu:ConversationHeader__LeaveGroupConfirmation__title')}
actions={[
{
disabled: cannotLeaveBecauseYouAreLastAdmin,
action: () => {
this.setState({ hasLeaveGroupConfirmation: false });
if (!cannotLeaveBecauseYouAreLastAdmin) {
leaveGroup(id);
} else {
this.setState({
hasLeaveGroupConfirmation: false,
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: true,
});
}
},
style: 'negative',
text: i18n(
'icu:ConversationHeader__LeaveGroupConfirmation__confirmButton'
),
},
]}
i18n={i18n}
onClose={() => {
this.setState({ hasLeaveGroupConfirmation: false });
}}
>
{i18n('icu:ConversationHeader__LeaveGroupConfirmation__description')}
</ConfirmationDialog>
);
}
private renderCannotLeaveGroupBecauseYouAreLastAdminAlert() {
const { hasCannotLeaveGroupBecauseYouAreLastAdminAlert } = this.state;
const { i18n } = this.props;
if (!hasCannotLeaveGroupBecauseYouAreLastAdminAlert) {
return;
}
return (
<Alert
i18n={i18n}
body={i18n(
'icu:ConversationHeader__CannotLeaveGroupBecauseYouAreLastAdminAlert__description'
)}
onClose={() => {
this.setState({
hasCannotLeaveGroupBecauseYouAreLastAdminAlert: false,
});
}}
/>
);
}
private renderHeader(): ReactNode {
const { conversationTitle, groupVersion, pushPanelForConversation, type } =
this.props;
@ -711,7 +808,9 @@ export class ConversationHeader extends React.Component<PropsType, StateType> {
return (
<>
{modalNode}
{this.renderConfirmationDialog()}
{this.renderDeleteMessagesConfirmationDialog()}
{this.renderLeaveGroupConfirmationDialog()}
{this.renderCannotLeaveGroupBecauseYouAreLastAdminAlert()}
<Measure
bounds
onResize={({ bounds }) => {

View file

@ -133,6 +133,20 @@ type ActionProps = {
export type Props = StateProps & ActionProps;
export function getCannotLeaveBecauseYouAreLastAdmin(
memberships: ReadonlyArray<GroupV2Membership>,
isAdmin: boolean
): boolean {
const otherMemberships = memberships.filter(({ member }) => !member.isMe);
const isJustMe = otherMemberships.length === 0;
const isAnyoneElseAnAdmin = otherMemberships.some(
membership => membership.isAdmin
);
const cannotLeaveBecauseYouAreLastAdmin =
isAdmin && !isJustMe && !isAnyoneElseAnAdmin;
return cannotLeaveBecauseYouAreLastAdmin;
}
export function ConversationDetails({
acceptConversation,
addMembersToGroup,
@ -196,13 +210,8 @@ export function ConversationDetails({
const invitesCount =
pendingMemberships.length + pendingApprovalMemberships.length;
const otherMemberships = memberships.filter(({ member }) => !member.isMe);
const isJustMe = otherMemberships.length === 0;
const isAnyoneElseAnAdmin = otherMemberships.some(
membership => membership.isAdmin
);
const cannotLeaveBecauseYouAreLastAdmin =
isAdmin && !isJustMe && !isAnyoneElseAnAdmin;
getCannotLeaveBecauseYouAreLastAdmin(memberships, isAdmin);
const onCloseModal = useCallback(() => {
setModalState(ModalState.NothingOpen);

1
ts/model-types.d.ts vendored
View file

@ -313,6 +313,7 @@ export type ConversationAttributesType = {
draftBodyRanges?: DraftBodyRanges;
draftTimestamp?: number | null;
hideStory?: boolean;
hiddenFromConversationSearch?: boolean;
inbox_position?: number;
// When contact is removed - it is initially placed into `justNotification`
// removal stage. In this stage user can still send messages (which will

View file

@ -361,6 +361,7 @@ export class ConversationModel extends window.Backbone
this.unset('tokens');
this.on('change:members change:membersV2', this.fetchContacts);
this.on('change:isArchived', this.onArchiveChange);
this.typingRefreshTimer = null;
this.typingPauseTimer = null;
@ -4196,6 +4197,17 @@ export class ConversationModel extends window.Backbone
}
}
private onArchiveChange() {
const isArchived = this.get('isArchived');
if (isArchived) {
return;
}
if (!this.get('hiddenFromConversationSearch')) {
return;
}
this.set('hiddenFromConversationSearch', false);
}
setMarkedUnread(markedUnread: boolean): void {
const previousMarkedUnread = this.get('markedUnread');
@ -4892,6 +4904,9 @@ export class ConversationModel extends window.Backbone
active_at: null,
pendingUniversalTimer: undefined,
});
if (isGroup(this.attributes)) {
this.set('hiddenFromConversationSearch', true);
}
window.Signal.Data.updateConversation(this.attributes);
await window.Signal.Data.removeAllMessagesInConversation(this.id, {

View file

@ -242,6 +242,7 @@ export type ConversationType = ReadonlyDeep<
customColorId?: string;
discoveredUnregisteredAt?: number;
hideStory?: boolean;
hiddenFromConversationSearch?: boolean;
isArchived?: boolean;
isBlocked?: boolean;
removalStage?: 'justNotification' | 'messageRequest';

View file

@ -12,6 +12,7 @@ import {
} from '../../components/conversation/ConversationHeader';
import { getPreferredBadgeSelector } from '../selectors/badges';
import {
getConversationByUuidSelector,
getConversationSelector,
getConversationTitle,
isMissingRequiredProfileSharing,
@ -35,6 +36,8 @@ import { strictAssert } from '../../util/assert';
import { isSignalConversation } from '../../util/isSignalConversation';
import { useSearchActions } from '../ducks/search';
import { useStoriesActions } from '../ducks/stories';
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
import { getGroupMemberships } from '../../util/getGroupMemberships';
export type OwnProps = {
id: string;
@ -79,6 +82,7 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
if (!conversation) {
throw new Error('Could not find conversation');
}
const isAdmin = Boolean(conversation.areWeAdmin);
const hasStoriesSelector = useSelector(getHasStoriesSelector);
const hasStories = hasStoriesSelector(id);
@ -97,6 +101,7 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
const {
destroyMessages,
leaveGroup,
onArchive,
onMarkUnread,
onMoveToInbox,
@ -114,6 +119,14 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
const { searchInConversation } = useSearchActions();
const { viewUserStories } = useStoriesActions();
const conversationByUuidSelector = useSelector(getConversationByUuidSelector);
const groupMemberships = getGroupMemberships(
conversation,
conversationByUuidSelector
);
const cannotLeaveBecauseYouAreLastAdmin =
getCannotLeaveBecauseYouAreLastAdmin(groupMemberships.memberships, isAdmin);
return (
<ConversationHeader
{...pick(conversation, [
@ -141,6 +154,7 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
'unblurredAvatarPath',
])}
badge={badge}
cannotLeaveBecauseYouAreLastAdmin={cannotLeaveBecauseYouAreLastAdmin}
conversationTitle={conversationTitle}
destroyMessages={destroyMessages}
hasStories={hasStories}
@ -151,6 +165,7 @@ export function SmartConversationHeader({ id }: OwnProps): JSX.Element {
)}
isSignalConversation={isSignalConversation(conversation)}
isSMSOnly={isConversationSMSOnly(conversation)}
leaveGroup={leaveGroup}
onArchive={onArchive}
onMarkUnread={onMarkUnread}
onMoveToInbox={onMoveToInbox}

View file

@ -70,7 +70,7 @@ describe('storage service', function needsName() {
await moreButton.click();
const pinButton = conversationStack.locator(
'.react-contextmenu-item >> "Pin Chat"'
'.react-contextmenu-item >> "Pin chat"'
);
await pinButton.click();
@ -114,7 +114,7 @@ describe('storage service', function needsName() {
await moreButton.click();
const pinButton = conversationStack.locator(
'.react-contextmenu-item >> "Pin Chat"'
'.react-contextmenu-item >> "Pin chat"'
);
await pinButton.click();

View file

@ -90,6 +90,10 @@ function searchConversations(
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
const currentConversations = conversations.filter(conversation => {
return !conversation.left && !conversation.hiddenFromConversationSearch;
});
// Escape the search term
let extendedSearchTerm = searchTerm;
@ -98,7 +102,7 @@ function searchConversations(
extendedSearchTerm += ` | ${phoneNumber.e164}`;
}
const index = getCachedFuseIndex(conversations, FUSE_OPTIONS);
const index = getCachedFuseIndex(currentConversations, FUSE_OPTIONS);
return index.search(extendedSearchTerm);
}

View file

@ -173,6 +173,9 @@ export function getConversation(model: ConversationModel): ConversationType {
groupId: attributes.groupId,
groupLink: buildGroupLink(attributes),
hideStory: Boolean(attributes.hideStory),
hiddenFromConversationSearch: Boolean(
attributes.hiddenFromConversationSearch
),
inboxPosition,
isArchived: attributes.isArchived,
isBlocked: isBlocked(attributes),