Add more buttons to ContactModal

This commit is contained in:
Fedor Indutny 2024-02-14 12:25:27 -08:00 committed by GitHub
parent 307fb7346d
commit 848ed95bda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 150 additions and 30 deletions

View file

@ -43,6 +43,10 @@
"messageformat": "Add “{contact}” to the group “{group}”", "messageformat": "Add “{contact}” to the group “{group}”",
"description": "Shown in the confirmation dialog body when adding a contact to a group" "description": "Shown in the confirmation dialog body when adding a contact to a group"
}, },
"icu:AddUserToAnotherGroupModal__search-placeholder": {
"messageformat": "Search",
"description": "Placeholder to use when searching for groups in the AddUserToAnotherGroupModal"
},
"icu:AddUserToAnotherGroupModal__toast--user-added-to-group": { "icu:AddUserToAnotherGroupModal__toast--user-added-to-group": {
"messageformat": "{contact} was added to {group}", "messageformat": "{contact} was added to {group}",
"description": "Shown in toast after a user is added to an existing group" "description": "Shown in toast after a user is added to an existing group"
@ -356,7 +360,7 @@
"description": "Placeholder of the search input in left pane when looking up someone by phone number" "description": "Placeholder of the search input in left pane when looking up someone by phone number"
}, },
"icu:LeftPaneFindByHelper__description--findByUsername": { "icu:LeftPaneFindByHelper__description--findByUsername": {
"messageformat": "Enter a full username with its pair of digits.", "messageformat": "Enter a username followed by a dot and its set of numbers.",
"description": "Description displayed under search input in left pane when looking up someone by username" "description": "Description displayed under search input in left pane when looking up someone by username"
}, },
"icu:CountryCodeSelect__placeholder": { "icu:CountryCodeSelect__placeholder": {

View file

@ -7,7 +7,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
margin-top: 4px; margin-top: 4px;
margin-bottom: 16px; margin-bottom: 24px;
&__name { &__name {
@include button-reset(); @include button-reset();
@ -17,7 +17,7 @@
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: baseline;
margin-top: 6px; margin-top: 12px;
cursor: pointer; cursor: pointer;
} }
@ -63,8 +63,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding-block: 7px; padding-block: 6px;
padding-inline: 16px; padding-inline: 24px;
width: 100%; width: 100%;
&:last-child { &:last-child {
@ -94,7 +94,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-inline-end: 10px; margin-inline-end: 12px;
width: 20px; width: 20px;
} }
@ -130,6 +130,19 @@
} }
} }
&__block__bubble-icon {
height: 20px;
width: 20px;
@include light-theme {
@include color-svg('../images/icons/v3/block/block.svg', $color-gray-75);
}
@include dark-theme {
@include color-svg('../images/icons/v3/block/block.svg', $color-gray-15);
}
}
&__make-admin__bubble-icon { &__make-admin__bubble-icon {
height: 20px; height: 20px;
width: 20px; width: 20px;
@ -239,4 +252,33 @@
); );
} }
} }
&__quick-actions {
display: flex;
flex-direction: row;
justify-content: center;
gap: 16px;
margin-block: 16px;
}
&__divider {
// Full width minus margin
width: calc(100% - 48px);
border-style: solid;
border-bottom: none;
border-width: 1px;
@include light-theme {
border-color: $color-gray-15;
}
@include dark-theme {
border-color: $color-gray-75;
}
margin-block: 8px 5px;
margin-inline: 24px;
}
} }

View file

@ -175,7 +175,9 @@ export function AddUserToAnotherGroupModal({
<div className="AddUserToAnotherGroupModal__main-body"> <div className="AddUserToAnotherGroupModal__main-body">
<SearchInput <SearchInput
i18n={i18n} i18n={i18n}
placeholder={i18n('icu:contactSearchPlaceholder')} placeholder={i18n(
'icu:AddUserToAnotherGroupModal__search-placeholder'
)}
onChange={handleSearchInputChange} onChange={handleSearchInputChange}
ref={inputRef} ref={inputRef}
value={searchTerm} value={searchTerm}

View file

@ -31,17 +31,28 @@ const defaultGroup: ConversationType = getDefaultConversation({
export default { export default {
title: 'Components/Conversation/ContactModal', title: 'Components/Conversation/ContactModal',
component: ContactModal, component: ContactModal,
argTypes: {
hasActiveCall: { control: { type: 'boolean' } },
},
args: { args: {
i18n, i18n,
areWeASubscriber: false, areWeASubscriber: false,
areWeAdmin: false, areWeAdmin: false,
badges: [], badges: [],
blockConversation: action('blockConversation'),
contact: defaultContact, contact: defaultContact,
conversation: defaultGroup, conversation: defaultGroup,
hasActiveCall: false,
hasStories: undefined, hasStories: undefined,
hideContactModal: action('hideContactModal'), hideContactModal: action('hideContactModal'),
isAdmin: false, isAdmin: false,
isMember: true, isMember: true,
onOutgoingAudioCallInConversation: action(
'onOutgoingAudioCallInConversation'
),
onOutgoingVideoCallInConversation: action(
'onOutgoingVideoCallInConversation'
),
removeMemberFromGroup: action('removeMemberFromGroup'), removeMemberFromGroup: action('removeMemberFromGroup'),
showConversation: action('showConversation'), showConversation: action('showConversation'),
theme: ThemeType.light, theme: ThemeType.light,

View file

@ -14,16 +14,15 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories'; import type { ViewUserStoriesActionCreatorType } from '../../state/ducks/stories';
import { StoryViewModeType } from '../../types/Stories'; import { StoryViewModeType } from '../../types/Stories';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { About } from './About';
import { Avatar, AvatarSize } from '../Avatar'; import { Avatar, AvatarSize } from '../Avatar';
import { AvatarLightbox } from '../AvatarLightbox'; import { AvatarLightbox } from '../AvatarLightbox';
import { BadgeDialog } from '../BadgeDialog'; import { BadgeDialog } from '../BadgeDialog';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog'; import { RemoveGroupMemberConfirmationDialog } from './RemoveGroupMemberConfirmationDialog';
import { SharedGroupNames } from '../SharedGroupNames';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { UserText } from '../UserText'; import { UserText } from '../UserText';
import { Button, ButtonIconType, ButtonVariant } from '../Button';
export type PropsDataType = { export type PropsDataType = {
areWeASubscriber: boolean; areWeASubscriber: boolean;
@ -36,10 +35,14 @@ export type PropsDataType = {
isAdmin: boolean; isAdmin: boolean;
isMember: boolean; isMember: boolean;
theme: ThemeType; theme: ThemeType;
hasActiveCall: boolean;
}; };
type PropsActionType = { type PropsActionType = {
blockConversation: (id: string) => void;
hideContactModal: () => void; hideContactModal: () => void;
onOutgoingAudioCallInConversation: (conversationId: string) => unknown;
onOutgoingVideoCallInConversation: (conversationId: string) => unknown;
removeMemberFromGroup: (conversationId: string, contactId: string) => void; removeMemberFromGroup: (conversationId: string, contactId: string) => void;
showConversation: ShowConversationType; showConversation: ShowConversationType;
toggleAdmin: (conversationId: string, contactId: string) => void; toggleAdmin: (conversationId: string, contactId: string) => void;
@ -62,19 +65,24 @@ enum SubModalState {
None = 'None', None = 'None',
ToggleAdmin = 'ToggleAdmin', ToggleAdmin = 'ToggleAdmin',
MemberRemove = 'MemberRemove', MemberRemove = 'MemberRemove',
ConfirmingBlock = 'ConfirmingBlock',
} }
export function ContactModal({ export function ContactModal({
areWeAdmin, areWeAdmin,
areWeASubscriber, areWeASubscriber,
badges, badges,
blockConversation,
contact, contact,
conversation, conversation,
hasActiveCall,
hasStories, hasStories,
hideContactModal, hideContactModal,
i18n, i18n,
isAdmin, isAdmin,
isMember, isMember,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
removeMemberFromGroup, removeMemberFromGroup,
showConversation, showConversation,
theme, theme,
@ -160,6 +168,27 @@ export function ContactModal({
/> />
); );
break; break;
case SubModalState.ConfirmingBlock:
modalNode = (
<ConfirmationDialog
dialogName="ContactModal.confirmBlock"
actions={[
{
text: i18n('icu:MessageRequests--block'),
action: () => blockConversation(contact.id),
style: 'affirmative',
},
]}
i18n={i18n}
onClose={() => setSubModalState(SubModalState.None)}
title={i18n('icu:MessageRequests--block-direct-confirm-title', {
title: contact.title,
})}
>
{i18n('icu:MessageRequests--block-direct-confirm-body')}
</ConfirmationDialog>
);
break;
default: { default: {
const state: never = subModalState; const state: never = subModalState;
log.warn(`ContactModal: unexpected ${state}!`); log.warn(`ContactModal: unexpected ${state}!`);
@ -221,31 +250,61 @@ export function ContactModal({
<UserText text={contact.title} /> <UserText text={contact.title} />
<i className="ContactModal__name__chevron" /> <i className="ContactModal__name__chevron" />
</button> </button>
<div className="module-about__container">
<About text={contact.about} />
</div>
{!contact.isMe && ( {!contact.isMe && (
<div className="ContactModal__info"> <div className="ContactModal__quick-actions">
<SharedGroupNames <Button
i18n={i18n} icon={ButtonIconType.message}
sharedGroupNames={contact.sharedGroupNames || []} variant={ButtonVariant.Details}
/> onClick={() => {
hideContactModal();
showConversation({
conversationId: contact?.id,
switchToAssociatedView: true,
});
}}
>
{i18n('icu:ConversationDetails__HeaderButton--Message')}
</Button>
<Button
icon={ButtonIconType.video}
variant={ButtonVariant.Details}
disabled={hasActiveCall}
onClick={() => {
hideContactModal();
onOutgoingVideoCallInConversation(contact.id);
}}
>
{i18n('icu:video')}
</Button>
<Button
icon={ButtonIconType.audio}
variant={ButtonVariant.Details}
disabled={hasActiveCall}
onClick={() => {
hideContactModal();
onOutgoingAudioCallInConversation(contact.id);
}}
>
{i18n('icu:audio')}
</Button>
</div> </div>
)} )}
<div className="ContactModal__divider" />
<div className="ContactModal__button-container"> <div className="ContactModal__button-container">
<button {!contact.isMe && (
type="button" <button
className="ContactModal__button ContactModal__send-message" type="button"
onClick={() => { className="ContactModal__button ContactModal__block"
hideContactModal(); onClick={() =>
showConversation({ conversationId: contact.id }); setSubModalState(SubModalState.ConfirmingBlock)
}} }
> >
<div className="ContactModal__bubble-icon"> <div className="ContactModal__bubble-icon">
<div className="ContactModal__send-message__bubble-icon" /> <div className="ContactModal__block__bubble-icon" />
</div> </div>
<span>{i18n('icu:ContactModal--message')}</span> <span>{i18n('icu:MessageRequests--block')}</span>
</button> </button>
)}
{!contact.isMe && ( {!contact.isMe && (
<button <button
type="button" type="button"

View file

@ -12,6 +12,7 @@ import { getIntl, getTheme } from '../selectors/user';
import { getBadgesSelector } from '../selectors/badges'; import { getBadgesSelector } from '../selectors/badges';
import { getConversationSelector } from '../selectors/conversations'; import { getConversationSelector } from '../selectors/conversations';
import { getHasStoriesSelector } from '../selectors/stories2'; import { getHasStoriesSelector } from '../selectors/stories2';
import { getActiveCallState } from '../selectors/calling';
const mapStateToProps = (state: StateType): PropsDataType => { const mapStateToProps = (state: StateType): PropsDataType => {
const { contactId, conversationId } = const { contactId, conversationId } =
@ -42,6 +43,7 @@ const mapStateToProps = (state: StateType): PropsDataType => {
areWeASubscriber: getAreWeASubscriber(state), areWeASubscriber: getAreWeASubscriber(state),
areWeAdmin, areWeAdmin,
badges: getBadgesSelector(state)(contact.badges), badges: getBadgesSelector(state)(contact.badges),
hasActiveCall: Boolean(getActiveCallState(state)),
contact, contact,
conversation: currentConversation, conversation: currentConversation,
hasStories, hasStories,