Invite via Link: Display users that require Premium (#4461)

This commit is contained in:
Alexander Zinchuk 2024-04-19 13:38:36 +04:00
parent 8d942e62bb
commit 0631a9111d
20 changed files with 373 additions and 224 deletions

View File

@ -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';

View File

@ -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 {

View File

@ -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>

View File

@ -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;
}

View File

@ -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>
);
};

View File

@ -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

View File

@ -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}

View File

@ -1,7 +0,0 @@
.contentText {
color: var(--color-text-secondary);
}
.userPicker {
margin-bottom: 1rem;
}

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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>

View File

@ -71,7 +71,7 @@ const ChatlistNew: FC<OwnProps> = ({ invite }) => {
</div>
<Picker
itemIds={invite.peerIds}
lockedIds={joinedIds}
lockedSelectedIds={joinedIds}
onSelectedIdsChange={setSelectedPeerIds}
selectedIds={selectedPeerIds}
/>

View File

@ -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;

View File

@ -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;
}

View 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));

View File

@ -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`}

View File

@ -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 });

View File

@ -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);

View File

@ -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;
};

View File

@ -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[] = [];