Update header actions/add hiddenFromConversationSearch
This commit is contained in:
parent
00250e535c
commit
af4ad55c68
11 changed files with 204 additions and 27 deletions
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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
1
ts/model-types.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -242,6 +242,7 @@ export type ConversationType = ReadonlyDeep<
|
|||
customColorId?: string;
|
||||
discoveredUnregisteredAt?: number;
|
||||
hideStory?: boolean;
|
||||
hiddenFromConversationSearch?: boolean;
|
||||
isArchived?: boolean;
|
||||
isBlocked?: boolean;
|
||||
removalStage?: 'justNotification' | 'messageRequest';
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue