Invite via Link: Display users that require Premium (#4461)
This commit is contained in:
parent
8d942e62bb
commit
0631a9111d
@ -84,5 +84,5 @@ export { default as Management } from '../components/right/management/Management
|
||||
|
||||
export { default as PaymentModal } from '../components/payment/PaymentModal';
|
||||
export { default as ReceiptModal } from '../components/payment/ReceiptModal';
|
||||
export { default as InviteViaLinkModal } from '../components/main/InviteViaLinkModal';
|
||||
export { default as InviteViaLinkModal } from '../components/modals/inviteViaLink/InviteViaLinkModal';
|
||||
export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/OneTimeMediaModal';
|
||||
|
||||
@ -48,26 +48,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.Avatar {
|
||||
margin: 0 0 0 -0.75rem;
|
||||
font-size: 0.75rem;
|
||||
|
||||
&:first-child {
|
||||
width: 2rem !important;
|
||||
height: 2rem !important;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
width: 2.25rem !important;
|
||||
height: 2.25rem !important;
|
||||
border: 0.125rem solid var(--color-background);
|
||||
}
|
||||
}
|
||||
.avatars .Avatar {
|
||||
width: 2.25rem !important;
|
||||
height: 2.25rem !important;
|
||||
margin-inline-end: unset !important;
|
||||
}
|
||||
|
||||
.join {
|
||||
|
||||
@ -14,7 +14,7 @@ import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import './GroupCallTopPane.scss';
|
||||
@ -110,14 +110,9 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
||||
<span className="title">{lang('VoipGroupVoiceChat')}</span>
|
||||
<span className="participants">{lang('Participants', renderingParticipantCount ?? 0, 'i')}</span>
|
||||
</div>
|
||||
<div className="avatars">
|
||||
{renderingFetchedParticipants?.map((peer) => (
|
||||
<Avatar
|
||||
key={peer.id}
|
||||
peer={peer}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{Boolean(renderingFetchedParticipants?.length) && (
|
||||
<AvatarList size="small" peers={renderingFetchedParticipants} className="avatars" />
|
||||
)}
|
||||
<Button round className="join">
|
||||
{lang('VoipChatJoin')}
|
||||
</Button>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.root {
|
||||
display: flex;
|
||||
position: relative;
|
||||
--spacing: calc(var(--size) * 0.4);
|
||||
--spacing-gap: calc(var(--size) * 0.04);
|
||||
--size: 0px;
|
||||
@ -58,3 +59,21 @@
|
||||
.root[dir="rtl"] .avatar {
|
||||
--offset: calc(100% + var(--half-size) - var(--spacing));
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
right: -1px;
|
||||
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
|
||||
border: 1px solid var(--color-background);
|
||||
border-radius: 1rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
font-weight: 500;
|
||||
|
||||
padding: 0rem 0.25rem;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiPeer } from '../../api/types';
|
||||
import type { AvatarSize } from './Avatar';
|
||||
@ -12,25 +12,41 @@ import Avatar from './Avatar';
|
||||
|
||||
import styles from './AvatarList.module.scss';
|
||||
|
||||
const DEFAULT_LIMIT = 3;
|
||||
|
||||
type OwnProps = {
|
||||
size: AvatarSize;
|
||||
peers?: ApiPeer[];
|
||||
className?: string;
|
||||
limit?: number;
|
||||
badgeText?: string;
|
||||
};
|
||||
|
||||
const AvatarList: FC<OwnProps> = ({
|
||||
peers,
|
||||
size,
|
||||
className,
|
||||
limit = DEFAULT_LIMIT,
|
||||
badgeText,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const renderingBadgeText = useMemo(() => {
|
||||
if (badgeText) return badgeText;
|
||||
if (!peers?.length || peers.length <= limit) return undefined;
|
||||
return `+${peers.length - limit}`;
|
||||
}, [badgeText, peers, limit]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(className, styles.root, styles[`size-${size}`])}
|
||||
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
||||
>
|
||||
{peers?.map((peer) => <Avatar size={size} peer={peer} className={styles.avatar} />)}
|
||||
{peers?.slice(0, limit).map((peer) => <Avatar size={size} peer={peer} className={styles.avatar} />)}
|
||||
{renderingBadgeText && (
|
||||
<div className={styles.badge}>
|
||||
{renderingBadgeText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -9,7 +9,6 @@ import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { isUserId } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { buildCollectionByKey } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
@ -30,6 +29,9 @@ type OwnProps = {
|
||||
className?: string;
|
||||
itemIds: string[];
|
||||
selectedIds: string[];
|
||||
lockedSelectedIds?: string[];
|
||||
lockedUnselectedIds?: string[];
|
||||
lockedUnselectedSubtitle?: string;
|
||||
filterValue?: string;
|
||||
filterPlaceholder?: string;
|
||||
notFoundText?: string;
|
||||
@ -38,12 +40,11 @@ type OwnProps = {
|
||||
noScrollRestore?: boolean;
|
||||
isSearchable?: boolean;
|
||||
isRoundCheckbox?: boolean;
|
||||
lockedIds?: string[];
|
||||
forceShowSelf?: boolean;
|
||||
isViewOnly?: boolean;
|
||||
onSelectedIdsChange?: (ids: string[]) => void;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (id: string) => void;
|
||||
onDisabledClick?: (id: string, isSelected: boolean) => void;
|
||||
onLoadMore?: () => void;
|
||||
isCountryList?: boolean;
|
||||
countryList?: ApiCountry[];
|
||||
@ -67,7 +68,9 @@ const Picker: FC<OwnProps> = ({
|
||||
noScrollRestore,
|
||||
isSearchable,
|
||||
isRoundCheckbox,
|
||||
lockedIds,
|
||||
lockedSelectedIds,
|
||||
lockedUnselectedIds,
|
||||
lockedUnselectedSubtitle,
|
||||
forceShowSelf,
|
||||
isViewOnly,
|
||||
onSelectedIdsChange,
|
||||
@ -90,32 +93,39 @@ const Picker: FC<OwnProps> = ({
|
||||
}, FOCUS_DELAY_MS);
|
||||
}, [isSearchable]);
|
||||
|
||||
const [lockedSelectedIds, unlockedSelectedIds] = useMemo(() => {
|
||||
if (!lockedIds?.length) return [MEMO_EMPTY_ARRAY, selectedIds];
|
||||
const unlockedIds = selectedIds.filter((id) => !lockedIds.includes(id));
|
||||
return [lockedIds, unlockedIds];
|
||||
}, [selectedIds, lockedIds]);
|
||||
const lockedSelectedIdsSet = useMemo(() => new Set(lockedSelectedIds), [lockedSelectedIds]);
|
||||
const lockedUnselectedIdsSet = useMemo(() => new Set(lockedUnselectedIds), [lockedUnselectedIds]);
|
||||
|
||||
const lockedIdsSet = useMemo(() => new Set(lockedIds), [lockedIds]);
|
||||
const unlockedSelectedIds = useMemo(() => {
|
||||
return selectedIds.filter((id) => !lockedSelectedIdsSet.has(id));
|
||||
}, [lockedSelectedIdsSet, selectedIds]);
|
||||
|
||||
const sortedItemIds = useMemo(() => {
|
||||
const lockedBucket: string[] = [];
|
||||
const lockedSelectedBucket: string[] = [];
|
||||
const unlockedBucket: string[] = [];
|
||||
const lockedUnselectableBucket: string[] = [];
|
||||
|
||||
itemIds.forEach((id) => {
|
||||
if (lockedIdsSet.has(id)) {
|
||||
lockedBucket.push(id);
|
||||
if (lockedSelectedIdsSet.has(id)) {
|
||||
lockedSelectedBucket.push(id);
|
||||
} else if (lockedUnselectedIdsSet.has(id)) {
|
||||
lockedUnselectableBucket.push(id);
|
||||
} else {
|
||||
unlockedBucket.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return lockedBucket.concat(unlockedBucket);
|
||||
}, [itemIds, lockedIdsSet]);
|
||||
return lockedSelectedBucket.concat(unlockedBucket).concat(lockedUnselectableBucket);
|
||||
}, [itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]);
|
||||
|
||||
const handleItemClick = useLastCallback((id: string) => {
|
||||
if (lockedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id);
|
||||
if (lockedSelectedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lockedUnselectedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -144,13 +154,20 @@ const Picker: FC<OwnProps> = ({
|
||||
}, [countryList]);
|
||||
|
||||
const renderChatInfo = (id: string) => {
|
||||
const isUnselectable = lockedUnselectedIdsSet.has(id);
|
||||
if (isCountryList && countriesByIso) {
|
||||
const country = countriesByIso[id];
|
||||
return <div>{country.defaultName}</div>;
|
||||
} else if (isUserId(id)) {
|
||||
return <PrivateChatInfo forceShowSelf={forceShowSelf} userId={id} />;
|
||||
return (
|
||||
<PrivateChatInfo
|
||||
forceShowSelf={forceShowSelf}
|
||||
userId={id}
|
||||
status={isUnselectable ? lockedUnselectedSubtitle : undefined}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <GroupChatInfo chatId={id} />;
|
||||
return <GroupChatInfo chatId={id} status={isUnselectable ? lockedUnselectedSubtitle : undefined} />;
|
||||
}
|
||||
};
|
||||
|
||||
@ -158,7 +175,7 @@ const Picker: FC<OwnProps> = ({
|
||||
<div className={buildClassName('Picker', className)}>
|
||||
{isSearchable && (
|
||||
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lockedSelectedIds.map((id, i) => (
|
||||
{lockedSelectedIds?.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
peerId={id}
|
||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||
@ -171,7 +188,7 @@ const Picker: FC<OwnProps> = ({
|
||||
<PickerSelectedItem
|
||||
peerId={id}
|
||||
isMinimized={
|
||||
shouldMinimize && i + lockedSelectedIds.length < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
|
||||
shouldMinimize && i + (lockedSelectedIds?.length || 0) < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
|
||||
}
|
||||
canClose
|
||||
onClick={handleItemClick}
|
||||
@ -196,11 +213,13 @@ const Picker: FC<OwnProps> = ({
|
||||
noScrollRestore={noScrollRestore}
|
||||
>
|
||||
{viewportIds.map((id) => {
|
||||
const shouldRenderLockIcon = lockedUnselectedIdsSet.has(id);
|
||||
const isLocked = lockedSelectedIdsSet.has(id) || shouldRenderLockIcon;
|
||||
const renderCheckbox = () => {
|
||||
return isViewOnly ? undefined : (
|
||||
return (isViewOnly || shouldRenderLockIcon) ? undefined : (
|
||||
<Checkbox
|
||||
label=""
|
||||
disabled={lockedIdsSet.has(id)}
|
||||
disabled={isLocked}
|
||||
checked={selectedIds.includes(id)}
|
||||
round={isRoundCheckbox}
|
||||
/>
|
||||
@ -210,9 +229,10 @@ const Picker: FC<OwnProps> = ({
|
||||
<ListItem
|
||||
key={id}
|
||||
className={buildClassName('chat-item-clickable picker-list-item', isRoundCheckbox && 'chat-item')}
|
||||
disabled={lockedIdsSet.has(id)}
|
||||
disabled={isLocked}
|
||||
inactive={isViewOnly}
|
||||
allowDisabledClick={Boolean(onDisabledClick)}
|
||||
secondaryIcon={shouldRenderLockIcon ? 'lock-badge' : undefined}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleItemClick(id)}
|
||||
ripple
|
||||
|
||||
@ -170,7 +170,7 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
|
||||
<div className="settings-item settings-item-chatlist">
|
||||
<Picker
|
||||
itemIds={itemIds}
|
||||
lockedIds={lockedIds}
|
||||
lockedSelectedIds={lockedIds}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
selectedIds={selectedIds}
|
||||
onDisabledClick={handleClickDisabled}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
.contentText {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.userPicker {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback,
|
||||
useEffect,
|
||||
useMemo, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
|
||||
import { getUserFullName } from '../../global/helpers';
|
||||
import { selectChat } from '../../global/selectors';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
import Picker from '../common/Picker';
|
||||
import Button from '../ui/Button';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
import styles from './InviteViaLinkModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
chatId?: string;
|
||||
userIds?: string[];
|
||||
};
|
||||
|
||||
const InviteViaLinkModal: FC<OwnProps> = ({
|
||||
chatId, userIds,
|
||||
}) => {
|
||||
const { sendInviteMessages, closeInviteViaLinkModal } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userIds) {
|
||||
setSelectedMemberIds(userIds);
|
||||
}
|
||||
}, [userIds]);
|
||||
|
||||
const handleClose = useLastCallback(() => closeInviteViaLinkModal());
|
||||
const handleSkip = useLastCallback(() => closeInviteViaLinkModal());
|
||||
|
||||
const handleSendInviteLink = useCallback(() => {
|
||||
sendInviteMessages({ chatId: chatId!, userIds: selectedMemberIds! });
|
||||
closeInviteViaLinkModal();
|
||||
}, [selectedMemberIds, chatId]);
|
||||
|
||||
const userNames = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
return userIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
|
||||
}, [userIds]);
|
||||
|
||||
const canSendInviteLink = useMemo(() => {
|
||||
if (!chatId) {
|
||||
return false;
|
||||
}
|
||||
const chat = selectChat(getGlobal(), chatId);
|
||||
return Boolean(chat?.isCreator || chat?.adminRights?.inviteUsers);
|
||||
}, [chatId]);
|
||||
|
||||
const contentText = useMemo(() => {
|
||||
const langKey = canSendInviteLink
|
||||
? 'SendInviteLink.TextAvailableSingleUser'
|
||||
: 'SendInviteLink.TextUnavailableSingleUser';
|
||||
return renderText(lang(langKey, userNames), ['simple_markdown']);
|
||||
}, [userNames, lang, canSendInviteLink]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={Boolean(userIds && chatId)}
|
||||
title={canSendInviteLink ? lang('SendInviteLink.InviteTitle') : lang('SendInviteLink.LinkUnavailableTitle')}
|
||||
onClose={handleClose}
|
||||
isSlim
|
||||
>
|
||||
<p className={styles.contentText}>
|
||||
{contentText}
|
||||
</p>
|
||||
<Picker
|
||||
className={styles.userPicker}
|
||||
itemIds={userIds!}
|
||||
selectedIds={selectedMemberIds}
|
||||
onSelectedIdsChange={setSelectedMemberIds}
|
||||
isViewOnly={!canSendInviteLink}
|
||||
isRoundCheckbox
|
||||
/>
|
||||
<div className="dialog-buttons">
|
||||
{canSendInviteLink && (
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={handleSendInviteLink}
|
||||
disabled={!selectedMemberIds.length}
|
||||
>
|
||||
{lang('SendInviteLink.ActionInvite')}
|
||||
</Button>
|
||||
)}
|
||||
{canSendInviteLink && (
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={handleSkip}
|
||||
>
|
||||
{lang('SendInviteLink.ActionSkip')}
|
||||
</Button>
|
||||
)}
|
||||
{!canSendInviteLink && (
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={handleClose}
|
||||
>
|
||||
{lang('SendInviteLink.ActionClose')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InviteViaLinkModal);
|
||||
@ -79,6 +79,7 @@ import AttachBotInstallModal from '../modals/attachBotInstall/AttachBotInstallMo
|
||||
import BoostModal from '../modals/boost/BoostModal.async';
|
||||
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
|
||||
import GiftCodeModal from '../modals/giftcode/GiftCodeModal.async';
|
||||
import InviteViaLinkModal from '../modals/inviteViaLink/InviteViaLinkModal.async';
|
||||
import MapModal from '../modals/map/MapModal.async';
|
||||
import OneTimeMediaModal from '../modals/oneTimeMedia/OneTimeMediaModal.async';
|
||||
import UrlAuthModal from '../modals/urlAuth/UrlAuthModal.async';
|
||||
@ -97,7 +98,6 @@ import DraftRecipientPicker from './DraftRecipientPicker.async';
|
||||
import ForwardRecipientPicker from './ForwardRecipientPicker.async';
|
||||
import GameModal from './GameModal';
|
||||
import HistoryCalendar from './HistoryCalendar.async';
|
||||
import InviteViaLinkModal from './InviteViaLinkModal.async';
|
||||
import NewContactModal from './NewContactModal.async';
|
||||
import Notifications from './Notifications.async';
|
||||
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
||||
@ -606,7 +606,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||
<DeleteFolderDialog folder={deleteFolderDialog} />
|
||||
<ReactionPicker isOpen={isReactionPickerOpen} />
|
||||
<InviteViaLinkModal userIds={inviteViaLinkModal?.restrictedUserIds} chatId={inviteViaLinkModal?.chatId} />
|
||||
<InviteViaLinkModal missingUsers={inviteViaLinkModal?.missingUsers} chatId={inviteViaLinkModal?.chatId} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -86,7 +86,7 @@ const ChatlistAlready: FC<OwnProps> = ({ invite, folder }) => {
|
||||
</div>
|
||||
<Picker
|
||||
itemIds={invite.alreadyPeerIds}
|
||||
lockedIds={invite.alreadyPeerIds}
|
||||
lockedSelectedIds={invite.alreadyPeerIds}
|
||||
selectedIds={invite.alreadyPeerIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -71,7 +71,7 @@ const ChatlistNew: FC<OwnProps> = ({ invite }) => {
|
||||
</div>
|
||||
<Picker
|
||||
itemIds={invite.peerIds}
|
||||
lockedIds={joinedIds}
|
||||
lockedSelectedIds={joinedIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
/>
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './InviteViaLinkModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const InviteViaLinkModalAsync: FC<OwnProps> = (props) => {
|
||||
const { userIds, chatId } = props;
|
||||
const InviteViaLinkModal = useModuleLoader(Bundles.Extra, 'InviteViaLinkModal', !(userIds && chatId));
|
||||
const { chatId } = props;
|
||||
const InviteViaLinkModal = useModuleLoader(Bundles.Extra, 'InviteViaLinkModal', !chatId);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return InviteViaLinkModal ? <InviteViaLinkModal {...props} /> : undefined;
|
||||
@ -0,0 +1,38 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.contentText {
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center !important;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.userPicker {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sendInvites, .avatarList {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
206
src/components/modals/inviteViaLink/InviteViaLinkModal.tsx
Normal file
206
src/components/modals/inviteViaLink/InviteViaLinkModal.tsx
Normal file
@ -0,0 +1,206 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback,
|
||||
useEffect,
|
||||
useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiMissingInvitedUser } from '../../../api/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectChat } from '../../../global/selectors';
|
||||
import { partition } from '../../../util/iteratees';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Picker from '../../common/Picker';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Separator from '../../ui/Separator';
|
||||
|
||||
import styles from './InviteViaLinkModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
chatId?: string;
|
||||
missingUsers?: ApiMissingInvitedUser[];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
};
|
||||
|
||||
const InviteViaLinkModal: FC<OwnProps & StateProps> = ({
|
||||
missingUsers,
|
||||
chat,
|
||||
}) => {
|
||||
const { sendInviteMessages, closeInviteViaLinkModal, openPremiumModal } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
|
||||
const userIds = useMemo(() => missingUsers?.map((user) => user.id), [missingUsers]);
|
||||
const [unselectableIds, selectableIds] = useMemo(() => {
|
||||
if (!missingUsers?.length) return [[], []];
|
||||
const [requirePremiumIds, regularIds] = partition(missingUsers, (user) => user.isRequiringPremiumToMessage);
|
||||
return [requirePremiumIds.map((user) => user.id), regularIds.map((user) => user.id)];
|
||||
}, [missingUsers]);
|
||||
|
||||
const invitableWithPremiumIds = useMemo(() => {
|
||||
return missingUsers?.filter((user) => user.isRequiringPremiumToInvite || user.isRequiringPremiumToMessage)
|
||||
.map((user) => user.id);
|
||||
}, [missingUsers]);
|
||||
|
||||
const isEveryPremiumBlocksPm = useMemo(() => {
|
||||
if (!missingUsers) return undefined;
|
||||
return !missingUsers.some((user) => user.isRequiringPremiumToInvite && !user.isRequiringPremiumToMessage);
|
||||
}, [missingUsers]);
|
||||
|
||||
const topListPeers = useMemo(() => {
|
||||
const users = getGlobal().users.byId;
|
||||
return invitableWithPremiumIds?.map((id) => users[id]);
|
||||
}, [invitableWithPremiumIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMemberIds(selectableIds);
|
||||
}, [selectableIds]);
|
||||
|
||||
const handleClose = useLastCallback(() => closeInviteViaLinkModal());
|
||||
|
||||
const handleSendInviteLink = useCallback(() => {
|
||||
sendInviteMessages({ chatId: chat!.id, userIds: selectedMemberIds! });
|
||||
closeInviteViaLinkModal();
|
||||
}, [selectedMemberIds, chat]);
|
||||
|
||||
const handleOpenPremiumModal = useCallback(() => {
|
||||
openPremiumModal();
|
||||
}, []);
|
||||
|
||||
const canSendInviteLink = useMemo(() => {
|
||||
if (!chat) return undefined;
|
||||
return Boolean(chat?.isCreator || chat?.adminRights?.inviteUsers);
|
||||
}, [chat]);
|
||||
|
||||
const inviteSectionText = useMemo(() => {
|
||||
return canSendInviteLink
|
||||
? lang(missingUsers?.length === 1 ? 'InviteBlockedOneMessage' : 'InviteBlockedManyMessage')
|
||||
: lang('InviteRestrictedUsers2', missingUsers?.length);
|
||||
}, [canSendInviteLink, lang, missingUsers?.length]);
|
||||
|
||||
const premiumSectionText = useMemo(() => {
|
||||
if (!invitableWithPremiumIds?.length || !topListPeers?.length) return undefined;
|
||||
const prefix = isEveryPremiumBlocksPm ? 'InviteMessagePremiumBlocked' : 'InvitePremiumBlocked';
|
||||
let langKey = `${prefix}One`;
|
||||
let params = [getUserFullName(topListPeers[0])];
|
||||
if (invitableWithPremiumIds.length === 2) {
|
||||
langKey = `${prefix}Two`;
|
||||
params = [getUserFullName(topListPeers[0]), getUserFullName(topListPeers[1])];
|
||||
} else if (invitableWithPremiumIds.length === 3) {
|
||||
langKey = `${prefix}Three`;
|
||||
params = [getUserFullName(topListPeers[0]), getUserFullName(topListPeers[1]), getUserFullName(topListPeers[2])];
|
||||
} else if (invitableWithPremiumIds.length > 3) {
|
||||
langKey = `${prefix}Many`;
|
||||
params = [
|
||||
getUserFullName(topListPeers[0]),
|
||||
getUserFullName(topListPeers[1]),
|
||||
(invitableWithPremiumIds!.length - 2).toString(),
|
||||
];
|
||||
}
|
||||
|
||||
return lang(langKey, params, undefined, topListPeers.length);
|
||||
}, [invitableWithPremiumIds, isEveryPremiumBlocksPm, lang, topListPeers]);
|
||||
|
||||
if (!userIds) return undefined;
|
||||
|
||||
const hasPremiumSection = Boolean(topListPeers?.length);
|
||||
const hasSelectableSection = Boolean(selectableIds?.length);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={Boolean(userIds && chat)}
|
||||
contentClassName={styles.content}
|
||||
onClose={handleClose}
|
||||
isSlim
|
||||
>
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
className={styles.closeButton}
|
||||
ariaLabel={lang('Close')}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<i className="icon icon-close" />
|
||||
</Button>
|
||||
{premiumSectionText && (
|
||||
<>
|
||||
<AvatarList
|
||||
className={styles.avatarList}
|
||||
peers={topListPeers}
|
||||
size="large"
|
||||
/>
|
||||
<h3 className={styles.title}>
|
||||
{canSendInviteLink ? lang('InvitePremiumBlockedTitle') : lang('ChannelInviteViaLinkRestricted')}
|
||||
</h3>
|
||||
<p className={styles.contentText}>
|
||||
{renderText(premiumSectionText, ['simple_markdown'])}
|
||||
</p>
|
||||
<Button
|
||||
withPremiumGradient
|
||||
isShiny
|
||||
size="smaller"
|
||||
onClick={handleOpenPremiumModal}
|
||||
>
|
||||
{lang('InvitePremiumBlockedSubscribe')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{hasPremiumSection && hasSelectableSection && (
|
||||
<Separator className={styles.separator}>
|
||||
{lang('InvitePremiumBlockedOr')}
|
||||
</Separator>
|
||||
)}
|
||||
{hasSelectableSection && (
|
||||
<>
|
||||
<h3 className={styles.title}>{lang('InviteBlockedTitle')}</h3>
|
||||
<p className={styles.contentText}>
|
||||
{inviteSectionText}
|
||||
</p>
|
||||
<Picker
|
||||
className={styles.userPicker}
|
||||
itemIds={userIds!}
|
||||
selectedIds={selectedMemberIds}
|
||||
lockedUnselectedIds={unselectableIds}
|
||||
lockedUnselectedSubtitle={lang('InvitePremiumBlockedUser')}
|
||||
onSelectedIdsChange={setSelectedMemberIds}
|
||||
isViewOnly={!canSendInviteLink}
|
||||
isRoundCheckbox
|
||||
/>
|
||||
{canSendInviteLink && (
|
||||
<Button
|
||||
className={styles.sendInvites}
|
||||
size="smaller"
|
||||
onClick={handleSendInviteLink}
|
||||
disabled={!selectedMemberIds.length}
|
||||
>
|
||||
{lang('SendInviteLink.ActionInvite')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||
|
||||
return {
|
||||
chat,
|
||||
};
|
||||
},
|
||||
)(InviteViaLinkModal));
|
||||
@ -42,7 +42,7 @@ function AllowDenyList({
|
||||
key={id}
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedIds ?? MEMO_EMPTY_ARRAY}
|
||||
lockedIds={lockedIds}
|
||||
lockedSelectedIds={lockedIds}
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={lang('Search')}
|
||||
searchInputId={`${id}-picker-search`}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type {
|
||||
ApiChat, ApiChatFolder, ApiChatlistExportedInvite,
|
||||
ApiChatMember, ApiError, ApiUser,
|
||||
ApiChatMember, ApiError, ApiMissingInvitedUser, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type {
|
||||
@ -59,7 +59,6 @@ import {
|
||||
addSimilarChannels,
|
||||
addUsers,
|
||||
addUserStatuses,
|
||||
addUsersToRestrictedInviteList,
|
||||
deleteChatMessages,
|
||||
deleteTopic,
|
||||
leaveChat,
|
||||
@ -80,6 +79,7 @@ import {
|
||||
updateChatsLastMessageId,
|
||||
updateListedTopicIds,
|
||||
updateManagementProgress,
|
||||
updateMissingInvitedUsers,
|
||||
updatePeerFullInfo,
|
||||
updateThread,
|
||||
updateThreadInfo,
|
||||
@ -692,11 +692,11 @@ addActionHandler('createChannel', async (global, actions, payload): Promise<void
|
||||
setGlobal(global);
|
||||
|
||||
let createdChannel: ApiChat | undefined;
|
||||
let restrictedUserIds: string[] | undefined;
|
||||
let missingInvitedUsers: ApiMissingInvitedUser[] | undefined;
|
||||
try {
|
||||
const result = await callApi('createChannel', { title, about, users });
|
||||
createdChannel = result?.channel;
|
||||
restrictedUserIds = result?.missingUsers?.map(({ id }) => id);
|
||||
missingInvitedUsers = result?.missingUsers;
|
||||
} catch (error) {
|
||||
global = getGlobal();
|
||||
|
||||
@ -732,9 +732,9 @@ addActionHandler('createChannel', async (global, actions, payload): Promise<void
|
||||
setGlobal(global);
|
||||
actions.openChat({ id: channelId, shouldReplaceHistory: true, tabId });
|
||||
|
||||
if (restrictedUserIds) {
|
||||
if (missingInvitedUsers) {
|
||||
global = getGlobal();
|
||||
global = addUsersToRestrictedInviteList(global, restrictedUserIds, channelId, tabId);
|
||||
global = updateMissingInvitedUsers(global, channelId, missingInvitedUsers, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
@ -862,7 +862,6 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise<vo
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
let createdChatId: string | undefined;
|
||||
try {
|
||||
const { chat: createdChat, missingUsers } = await callApi('createGroupChat', {
|
||||
title,
|
||||
@ -874,7 +873,6 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise<vo
|
||||
}
|
||||
|
||||
const { id: chatId } = createdChat;
|
||||
createdChatId = chatId;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateChat(global, chatId, createdChat);
|
||||
@ -891,10 +889,9 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise<vo
|
||||
tabId,
|
||||
});
|
||||
|
||||
const restrictedUserIds = missingUsers?.map(({ id }) => id);
|
||||
if (restrictedUserIds) {
|
||||
if (missingUsers) {
|
||||
global = getGlobal();
|
||||
global = addUsersToRestrictedInviteList(global, restrictedUserIds, chatId, tabId);
|
||||
global = updateMissingInvitedUsers(global, chatId, missingUsers, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
@ -915,10 +912,6 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise<vo
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
} else if ((err as ApiError).message === 'USER_PRIVACY_RESTRICTED') {
|
||||
global = getGlobal();
|
||||
global = addUsersToRestrictedInviteList(global, users.map(({ id }) => id), createdChatId!, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1926,7 +1919,7 @@ addActionHandler('loadMoreMembers', async (global, actions, payload): Promise<vo
|
||||
addActionHandler('addChatMembers', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, memberIds, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
const users = (memberIds as string[]).map((userId) => selectUser(global, userId)).filter(Boolean);
|
||||
const users = memberIds.map((userId) => selectUser(global, userId)).filter(Boolean);
|
||||
|
||||
if (!chat || !users.length) {
|
||||
return;
|
||||
@ -1934,10 +1927,9 @@ addActionHandler('addChatMembers', async (global, actions, payload): Promise<voi
|
||||
|
||||
actions.setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Loading, tabId });
|
||||
const missingUsers = await callApi('addChatMembers', chat, users);
|
||||
const restrictedUserIds = missingUsers?.map((user) => user.id);
|
||||
if (restrictedUserIds) {
|
||||
if (missingUsers) {
|
||||
global = getGlobal();
|
||||
global = addUsersToRestrictedInviteList(global, restrictedUserIds, chat.id, tabId);
|
||||
global = updateMissingInvitedUsers(global, chatId, missingUsers, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
actions.setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Closed, tabId });
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { ApiUser, ApiUserFullInfo, ApiUserStatus } from '../../api/types';
|
||||
import type {
|
||||
ApiMissingInvitedUser, ApiUser, ApiUserFullInfo, ApiUserStatus,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState, TabArgs, TabState } from '../types';
|
||||
|
||||
import { areDeepEqual } from '../../util/areDeepEqual';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { omit, pick, unique } from '../../util/iteratees';
|
||||
import { omit, pick } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { selectTabState } from '../selectors';
|
||||
import { updateChat } from './chats';
|
||||
@ -264,17 +266,21 @@ export function closeNewContactDialog<T extends GlobalState>(
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function addUsersToRestrictedInviteList<T extends GlobalState>(
|
||||
export function updateMissingInvitedUsers<T extends GlobalState>(
|
||||
global: T,
|
||||
userIds: string[],
|
||||
chatId: string,
|
||||
missingUsers: ApiMissingInvitedUser[],
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const { inviteViaLinkModal } = selectTabState(global, tabId);
|
||||
if (!missingUsers.length) {
|
||||
return updateTabState(global, {
|
||||
inviteViaLinkModal: undefined,
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
return updateTabState(global, {
|
||||
inviteViaLinkModal: {
|
||||
...inviteViaLinkModal,
|
||||
restrictedUserIds: unique([...inviteViaLinkModal?.restrictedUserIds ?? [], ...userIds]),
|
||||
missingUsers,
|
||||
chatId,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
@ -33,6 +33,7 @@ import type {
|
||||
ApiKeyboardButton,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiMissingInvitedUser,
|
||||
ApiMyBoost,
|
||||
ApiNewPoll,
|
||||
ApiNotification,
|
||||
@ -702,7 +703,7 @@ export type TabState = {
|
||||
};
|
||||
|
||||
inviteViaLinkModal?: {
|
||||
restrictedUserIds: string[];
|
||||
missingUsers: ApiMissingInvitedUser[];
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
|
||||
@ -163,7 +163,7 @@ export function split<T extends any>(array: T[], chunkSize: number) {
|
||||
}
|
||||
|
||||
export function partition<T extends unknown>(
|
||||
array: T[], filter: (value: T, index: number, array: T[]) => boolean,
|
||||
array: T[], filter: (value: T, index: number, array: T[]) => boolean | undefined,
|
||||
): [T[], T[]] {
|
||||
const pass: T[] = [];
|
||||
const fail: T[] = [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user