Manage Group Permissions: Implement paid messages settings for groups (#5845)

This commit is contained in:
Alexander Zinchuk 2025-04-23 18:59:41 +02:00
parent 752c6f4fab
commit 888e740b39
17 changed files with 431 additions and 111 deletions

View File

@ -623,6 +623,7 @@ async function getFullChannelInfo(
hasScheduled, hasScheduled,
stargiftsCount, stargiftsCount,
stargiftsAvailable, stargiftsAvailable,
paidMessagesAvailable,
} = result.fullChat; } = result.fullChat;
if (chatPhoto) { if (chatPhoto) {
@ -717,6 +718,7 @@ async function getFullChannelInfo(
hasScheduledMessages: hasScheduled, hasScheduledMessages: hasScheduled,
starGiftCount: stargiftsCount, starGiftCount: stargiftsCount,
areStarGiftsAvailable: Boolean(stargiftsAvailable), areStarGiftsAvailable: Boolean(stargiftsAvailable),
arePaidMessagesAvailable: paidMessagesAvailable,
}, },
chats, chats,
userStatusesById: statusesById, userStatusesById: statusesById,
@ -2022,6 +2024,19 @@ export async function fetchChannelRecommendations({ chat }: { chat?: ApiChat })
}; };
} }
export async function updatePaidMessagesPrice({
chat, paidMessagesStars,
}: {
chat?: ApiChat; paidMessagesStars: number;
}) {
return invokeRequest(new GramJs.channels.UpdatePaidMessagesPrice({
channel: chat && buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
sendPaidMessagesStars: BigInt(paidMessagesStars),
}), {
shouldReturnTrue: true,
});
}
export async function fetchSponsoredPeer({ query }: { query: string }) { export async function fetchSponsoredPeer({ query }: { query: string }) {
const result = await invokeRequest(new GramJs.contacts.GetSponsoredPeers({ q: query })); const result = await invokeRequest(new GramJs.contacts.GetSponsoredPeers({ q: query }));
if (!result || result instanceof GramJs.contacts.SponsoredPeersEmpty) return undefined; if (!result || result instanceof GramJs.contacts.SponsoredPeersEmpty) return undefined;

View File

@ -142,6 +142,7 @@ export interface ApiChatFullInfo {
hasScheduledMessages?: boolean; hasScheduledMessages?: boolean;
starGiftCount?: number; starGiftCount?: number;
areStarGiftsAvailable?: boolean; areStarGiftsAvailable?: boolean;
arePaidMessagesAvailable?: true;
boostsApplied?: number; boostsApplied?: number;
boostsToUnrestrict?: number; boostsToUnrestrict?: number;

View File

@ -1894,7 +1894,7 @@
"ExceptionTitlePrivacyChargeForMessages" = "Remove fee"; "ExceptionTitlePrivacyChargeForMessages" = "Remove fee";
"ExceptionDescriptionPrivacyChargeForMessages" = "Add users or entire groups who won't be charged for sending messages to you."; "ExceptionDescriptionPrivacyChargeForMessages" = "Add users or entire groups who won't be charged for sending messages to you.";
"SectionTitleStarsForForMessages" = "Set your price per message"; "SectionTitleStarsForForMessages" = "Set your price per message";
"SectionDescriptionStarsForForMessages" = "You will receive {percent}% of the selected fee (~{amount}) for each incoming message."; "SectionDescriptionStarsForForMessages" = "You will receive {percent} of the selected fee (~{amount}) for each incoming message.";
"SubtitlePrivacyAddUsers" = "Add Users"; "SubtitlePrivacyAddUsers" = "Add Users";
"PrivacyPaidMessagesValue" = "Paid"; "PrivacyPaidMessagesValue" = "Paid";
"FirstMessageInPaidMessagesChat" = "**{user}** charges {amount} for each message."; "FirstMessageInPaidMessagesChat" = "**{user}** charges {amount} for each message.";
@ -1931,6 +1931,9 @@
"DescriptionRestrictedMedia" = "Posting media content is not allowed in this group."; "DescriptionRestrictedMedia" = "Posting media content is not allowed in this group.";
"DescriptionScheduledPaidMediaNotAllowed" = "Posting scheduled paid media content is not allowed"; "DescriptionScheduledPaidMediaNotAllowed" = "Posting scheduled paid media content is not allowed";
"DescriptionScheduledPaidMessagesNotAllowed" = "Scheduled paid messages is not allowed"; "DescriptionScheduledPaidMessagesNotAllowed" = "Scheduled paid messages is not allowed";
"GroupMessagesChargePrice" = "Charge Stars for Messages";
"RightsChargeStarsAbout" = "If you turn this on, regular members of the group will have to pay Stars to send messages.";
"SetPriceGroupDescription" = "Your group will receive {percent} of the selected fee (~{amount}) for each incoming messages.";
"UnlockButtonTitle" = "Unlock with Telegram Premium"; "UnlockButtonTitle" = "Unlock with Telegram Premium";
"FrozenAccountModalTitle" = "Your Account is Frozen"; "FrozenAccountModalTitle" = "Your Account is Frozen";
"FrozenAccountViolationTitle" = "Violation of Terms"; "FrozenAccountViolationTitle" = "Violation of Terms";

View File

@ -0,0 +1,115 @@
import React, {
memo,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import {
DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
MINIMUM_CHARGE_FOR_MESSAGES,
} from '../../../config';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import { formatPercent } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Button from '../../ui/Button';
import Icon from '../icons/Icon';
import PaidMessageSlider from './PaidMessageSlider';
type OwnProps = {
chargeForMessages: number;
canChangeChargeForMessages?: boolean;
isGroupChat?: boolean;
onChange: (value: number) => void;
};
type StateProps = {
starsUsdWithdrawRate: number;
starsPaidMessageAmountMax: number;
starsPaidMessageCommissionPermille: number;
};
function PaidMessagePrice({
starsUsdWithdrawRate,
starsPaidMessageAmountMax,
starsPaidMessageCommissionPermille,
canChangeChargeForMessages,
isGroupChat,
chargeForMessages,
onChange,
}: OwnProps & StateProps) {
const { openPremiumModal } = getActions();
const lang = useLang();
const handleChargeForMessagesChange = useLastCallback((value: number) => {
onChange?.(value);
});
const handleUnlockWithPremium = useLastCallback(() => {
openPremiumModal({ initialSection: 'message_privacy' });
});
return (
<>
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('SectionTitleStarsForForMessages')}
</h4>
<PaidMessageSlider
defaultValue={chargeForMessages}
min={MINIMUM_CHARGE_FOR_MESSAGES}
max={starsPaidMessageAmountMax}
value={chargeForMessages}
onChange={handleChargeForMessagesChange}
canChangeChargeForMessages={canChangeChargeForMessages}
readOnly={!canChangeChargeForMessages}
/>
{!canChangeChargeForMessages && (
<Button
color="primary"
fluid
size="smaller"
noForcedUpperCase
className="settings-unlock-button"
onClick={handleUnlockWithPremium}
>
<span className="settings-unlock-button-title">
{lang('UnlockButtonTitle')}
<Icon name="lock-badge" className="settings-unlock-button-icon" />
</span>
</Button>
)}
{canChangeChargeForMessages && (
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
{lang(isGroupChat ? 'SetPriceGroupDescription' : 'SectionDescriptionStarsForForMessages', {
percent: formatPercent(starsPaidMessageCommissionPermille * 100),
amount: formatCurrencyAsString(
chargeForMessages * starsUsdWithdrawRate * starsPaidMessageCommissionPermille,
'USD',
lang.code,
),
}, {
withNodes: true,
})}
</p>
)}
</>
);
}
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const starsUsdWithdrawRateX1000 = global.appConfig?.starsUsdWithdrawRateX1000;
const starsUsdWithdrawRate = starsUsdWithdrawRateX1000 ? starsUsdWithdrawRateX1000 / 1000 : 1;
const configStarsPaidMessageCommissionPermille = global.appConfig?.starsPaidMessageCommissionPermille;
const starsPaidMessageCommissionPermille = configStarsPaidMessageCommissionPermille
? configStarsPaidMessageCommissionPermille / 1000 : 100;
return {
starsPaidMessageCommissionPermille,
starsUsdWithdrawRate,
starsPaidMessageAmountMax: global.appConfig?.starsPaidMessageAmountMax || DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
};
},
)(PaidMessagePrice));

View File

@ -0,0 +1,121 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import { formatStarsAsText } from '../../../util/localization/format';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../icons/Icon';
type OwnProps = {
min?: number;
max: number;
value: number;
disabled?: boolean;
readOnly?: boolean;
bold?: boolean;
className?: string;
defaultValue: number;
onChange: (value: number) => void;
canChangeChargeForMessages?: boolean;
};
const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000];
const PaidMessageSlider: FC<OwnProps> = ({
min = 0,
max,
value,
disabled,
readOnly,
bold,
className,
defaultValue,
onChange,
canChangeChargeForMessages,
}) => {
const lang = useLang();
const points = useMemo(() => {
const result = [];
for (let i = 0; i < DEFAULT_POINTS.length; i++) {
if (DEFAULT_POINTS[i] < max) {
result.push(DEFAULT_POINTS[i]);
}
if (DEFAULT_POINTS[i] >= max) {
result.push(max);
break;
}
}
return result;
}, [max]);
const handleChange = useLastCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(event.currentTarget.value);
onChange(getValue(points, newValue));
});
const mainClassName = buildClassName(
className,
'RangeSlider',
disabled && 'disabled',
readOnly && 'readOnly',
bold && 'bold',
);
function renderTopRow() {
return (
<div className="slider-top-row" dir={lang.isRtl ? 'rtl' : undefined}>
<span className="value-min" dir="auto">{lang.number(min)}</span>
<span className="settings-range-value">
{!canChangeChargeForMessages && (<Icon name="lock-badge" />)}
{formatStarsAsText(lang, getValue(points, getProgress(points, value)))}
</span>
<span className="value-max" dir="auto">{lang.number(max)}</span>
</div>
);
}
return (
<div className={mainClassName}>
{renderTopRow()}
<div className="slider-main">
<div
className="slider-fill-track"
style={`width: ${(getProgress(points, value) / points.length) * 100}%`}
/>
<input
min={0}
max={points.length}
defaultValue={getProgress(points, defaultValue)}
step="any"
type="range"
className="RangeSlider__input"
onChange={handleChange}
/>
</div>
</div>
);
};
function getProgress(points: number[], value: number) {
const pointIndex = points.findIndex((point) => value <= point);
const prevPoint = points[pointIndex - 1] || 1;
const nextPoint = points[pointIndex] || points[points.length - 1];
const progress = (value - prevPoint) / (nextPoint - prevPoint);
return pointIndex + progress;
}
function getValue(points: number[], progress: number) {
const pointIndex = Math.floor(progress);
const prevPoint = points[pointIndex - 1] || 1;
const nextPoint = points[pointIndex] || points[points.length - 1];
const pointValue = prevPoint + (nextPoint - prevPoint) * (progress - pointIndex);
return pointValue < 100 ? Math.round(pointValue) : Math.round(pointValue / 10) * 10;
}
export default memo(PaidMessageSlider);

View File

@ -7,16 +7,12 @@ import { SettingsScreens } from '../../../types';
import { import {
DEFAULT_CHARGE_FOR_MESSAGES, DEFAULT_CHARGE_FOR_MESSAGES,
DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
MINIMUM_CHARGE_FOR_MESSAGES,
} from '../../../config'; } from '../../../config';
import { import {
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectNewNoncontactPeersRequirePremium, selectNewNoncontactPeersRequirePremium,
selectNonContactPeersPaidStars, selectNonContactPeersPaidStars,
} from '../../../global/selectors'; } from '../../../global/selectors';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import { formatStarsAsText } from '../../../util/localization/format';
import useDebouncedCallback from '../../../hooks/useDebouncedCallback'; import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
@ -24,11 +20,9 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon'; import PaidMessagePrice from '../../common/paidMessage/PaidMessagePrice';
import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup'; import RadioGroup from '../../ui/RadioGroup';
import RangeSlider from '../../ui/RangeSlider';
import PremiumStatusItem from './PremiumStatusItem'; import PremiumStatusItem from './PremiumStatusItem';
import PrivacyLockedOption from './PrivacyLockedOption'; import PrivacyLockedOption from './PrivacyLockedOption';
@ -44,9 +38,6 @@ type StateProps = {
canLimitNewMessagesWithoutPremium?: boolean; canLimitNewMessagesWithoutPremium?: boolean;
canChargeForMessages?: boolean; canChargeForMessages?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
starsUsdWithdrawRate: number;
starsPaidMessageCommissionPermille: number;
starsPaidMessageAmountMax?: number;
nonContactPeersPaidStars: number; nonContactPeersPaidStars: number;
noPaidReactionsForUsersCount: number; noPaidReactionsForUsersCount: number;
}; };
@ -59,14 +50,11 @@ function PrivacyMessages({
shouldChargeForMessages, shouldChargeForMessages,
nonContactPeersPaidStars, nonContactPeersPaidStars,
isCurrentUserPremium, isCurrentUserPremium,
starsPaidMessageCommissionPermille,
starsPaidMessageAmountMax,
starsUsdWithdrawRate,
noPaidReactionsForUsersCount, noPaidReactionsForUsersCount,
onReset, onReset,
onScreenSelect, onScreenSelect,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { updateGlobalPrivacySettings, openPremiumModal } = getActions(); const { updateGlobalPrivacySettings } = getActions();
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
@ -131,68 +119,6 @@ function PrivacyMessages({
updateGlobalPrivacySettingsWithDebounced(value); updateGlobalPrivacySettingsWithDebounced(value);
}, [setChargeForMessages, updateGlobalPrivacySettingsWithDebounced]); }, [setChargeForMessages, updateGlobalPrivacySettingsWithDebounced]);
const renderValueForStarsRange = useCallback((value: number) => {
return (
<span className="settings-range-value">
{!canChangeChargeForMessages && (<Icon name="lock-badge" />)}
{formatStarsAsText(lang, value)}
</span>
);
}, [lang, canChangeChargeForMessages]);
const handleUnlockWithPremium = useLastCallback(() => {
openPremiumModal({ initialSection: 'message_privacy' });
});
function renderSectionStarsAmountForPaidMessages() {
return (
<div className="settings-item fluid-container">
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>
{lang('SectionTitleStarsForForMessages')}
</h4>
<RangeSlider
isCenteredLayout
min={MINIMUM_CHARGE_FOR_MESSAGES}
max={starsPaidMessageAmountMax}
value={chargeForMessages}
onChange={handleChargeForMessagesChange}
renderValue={renderValueForStarsRange}
readOnly={!canChangeChargeForMessages}
/>
{!isCurrentUserPremium && (
<Button
color="primary"
fluid
size="smaller"
noForcedUpperCase
className="settings-unlock-button"
onClick={handleUnlockWithPremium}
>
<span className="settings-unlock-button-title">
{lang('UnlockButtonTitle')}
<Icon name="lock-badge" className="settings-unlock-button-icon" />
</span>
</Button>
)}
{isCurrentUserPremium && (
<p className="settings-item-description-larger" dir={oldLang.isRtl ? 'rtl' : undefined}>
{lang('SectionDescriptionStarsForForMessages', {
percent: starsPaidMessageCommissionPermille * 100,
amount: formatCurrencyAsString(
chargeForMessages * starsUsdWithdrawRate * starsPaidMessageCommissionPermille,
'USD',
lang.code,
),
}, {
withNodes: true,
})}
</p>
)}
</div>
);
}
function renderSectionNoPaidMessagesForUsers() { function renderSectionNoPaidMessagesForUsers() {
const itemSubtitle = !noPaidReactionsForUsersCount ? lang('SubtitlePrivacyAddUsers') const itemSubtitle = !noPaidReactionsForUsersCount ? lang('SubtitlePrivacyAddUsers')
: oldLang('Users', noPaidReactionsForUsersCount, 'i'); : oldLang('Users', noPaidReactionsForUsersCount, 'i');
@ -248,7 +174,15 @@ function PrivacyMessages({
{privacyDescription} {privacyDescription}
</p> </p>
</div> </div>
{selectedValue === 'charge_for_messages' && renderSectionStarsAmountForPaidMessages()} {selectedValue === 'charge_for_messages' && (
<div className="settings-item fluid-container">
<PaidMessagePrice
canChangeChargeForMessages={canChangeChargeForMessages}
chargeForMessages={chargeForMessages}
onChange={handleChargeForMessagesChange}
/>
</div>
)}
{canChangeChargeForMessages && selectedValue === 'charge_for_messages' && renderSectionNoPaidMessagesForUsers()} {canChangeChargeForMessages && selectedValue === 'charge_for_messages' && renderSectionNoPaidMessagesForUsers()}
{!isCurrentUserPremium && selectedValue !== 'charge_for_messages' {!isCurrentUserPremium && selectedValue !== 'charge_for_messages'
&& <PremiumStatusItem premiumSection="message_privacy" />} && <PremiumStatusItem premiumSection="message_privacy" />}
@ -259,12 +193,6 @@ function PrivacyMessages({
export default memo(withGlobal<OwnProps>((global): StateProps => { export default memo(withGlobal<OwnProps>((global): StateProps => {
const nonContactPeersPaidStars = selectNonContactPeersPaidStars(global); const nonContactPeersPaidStars = selectNonContactPeersPaidStars(global);
const starsUsdWithdrawRateX1000 = global.appConfig?.starsUsdWithdrawRateX1000;
const starsUsdWithdrawRate = starsUsdWithdrawRateX1000 ? starsUsdWithdrawRateX1000 / 1000 : 1;
const configStarsPaidMessageCommissionPermille = global.appConfig?.starsPaidMessageCommissionPermille;
const starsPaidMessageCommissionPermille = configStarsPaidMessageCommissionPermille
? configStarsPaidMessageCommissionPermille / 1000 : 100;
const noPaidReactionsForUsersCount = global.settings.privacy.noPaidMessages?.allowUserIds.length || 0; const noPaidReactionsForUsersCount = global.settings.privacy.noPaidMessages?.allowUserIds.length || 0;
return { return {
@ -274,9 +202,6 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
canLimitNewMessagesWithoutPremium: global.appConfig?.canLimitNewMessagesWithoutPremium, canLimitNewMessagesWithoutPremium: global.appConfig?.canLimitNewMessagesWithoutPremium,
canChargeForMessages: global.appConfig?.starsPaidMessagesAvailable, canChargeForMessages: global.appConfig?.starsPaidMessagesAvailable,
starsPaidMessageAmountMax: global.appConfig?.starsPaidMessageAmountMax || DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
starsPaidMessageCommissionPermille,
starsUsdWithdrawRate,
noPaidReactionsForUsersCount, noPaidReactionsForUsersCount,
}; };
})(PrivacyMessages)); })(PrivacyMessages));

View File

@ -125,6 +125,7 @@
color: var(--color-primary); color: var(--color-primary);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
margin-inline-start: 2rem;
} }
.settings-item-simple, .settings-item-simple,
@ -283,11 +284,6 @@
} }
} }
.RangeSlider {
margin-bottom: 1.0625rem;
padding-inline: 1rem;
}
.radio-group { .radio-group {
.Radio:last-child { .Radio:last-child {
margin-bottom: 0; margin-bottom: 0;

View File

@ -3,7 +3,11 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types'; import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types';
import { GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, TME_LINK_PREFIX } from '../../../config'; import {
GENERAL_TOPIC_ID,
SERVICE_NOTIFICATIONS_USER_ID,
TME_LINK_PREFIX,
} from '../../../config';
import { import {
getMessageInvoice, getMessageText, isChatChannel, getMessageInvoice, getMessageText, isChatChannel,
} from '../../../global/helpers'; } from '../../../global/helpers';

View File

@ -96,7 +96,7 @@
box-shadow: 0 1px 2px var(--color-default-shadow); box-shadow: 0 1px 2px var(--color-default-shadow);
.RangeSlider { .RangeSlider {
margin-bottom: 0; margin: 0;
input[type="range"] { input[type="range"] {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -1,25 +1,38 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, {
memo, useCallback, useMemo, useState, memo, useCallback, useEffect,
useMemo, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiChatBannedRights, ApiChatMember } from '../../../api/types'; import type { ApiChat, ApiChatBannedRights, ApiChatMember } from '../../../api/types';
import { ManagementScreens } from '../../../types'; import { ManagementProgress, ManagementScreens } from '../../../types';
import { selectChat, selectChatFullInfo } from '../../../global/selectors'; import {
DEFAULT_CHARGE_FOR_MESSAGES,
} from '../../../config';
import {
selectChat,
selectChatFullInfo,
selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useManagePermissions from '../hooks/useManagePermissions'; import useManagePermissions from '../hooks/useManagePermissions';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import PaidMessagePrice from '../../common/paidMessage/PaidMessagePrice';
import PrivateChatInfo from '../../common/PrivateChatInfo'; import PrivateChatInfo from '../../common/PrivateChatInfo';
import PermissionCheckboxList from '../../main/PermissionCheckboxList'; import PermissionCheckboxList from '../../main/PermissionCheckboxList';
import FloatingActionButton from '../../ui/FloatingActionButton'; import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Spinner from '../../ui/Spinner'; import Spinner from '../../ui/Spinner';
import Switcher from '../../ui/Switcher';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
@ -31,9 +44,13 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
progress?: ManagementProgress;
currentUserId?: string; currentUserId?: string;
removedUsersCount: number; removedUsersCount: number;
members?: ApiChatMember[]; members?: ApiChatMember[];
arePaidMessagesAvailable?: boolean;
groupPeersPaidStars: number;
canChargeForMessages?: boolean;
}; };
const ITEM_HEIGHT = 48; const ITEM_HEIGHT = 48;
@ -83,18 +100,23 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
onScreenSelect, onScreenSelect,
onChatMemberSelect, onChatMemberSelect,
chat, chat,
progress,
currentUserId, currentUserId,
removedUsersCount, removedUsersCount,
members, members,
onClose, onClose,
isActive, isActive,
arePaidMessagesAvailable,
canChargeForMessages,
groupPeersPaidStars,
}) => { }) => {
const { updateChatDefaultBannedRights } = getActions(); const { updateChatDefaultBannedRights, updatePaidMessagesPrice } = getActions();
const { const {
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading, permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
} = useManagePermissions(chat?.defaultBannedRights); } = useManagePermissions(chat?.defaultBannedRights);
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
useHistoryBack({ useHistoryBack({
isActive, isActive,
@ -116,14 +138,41 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false); const [isMediaDropdownOpen, setIsMediaDropdownOpen] = useState(false);
const handleSavePermissions = useCallback(() => { const [isPriceForMessagesChanged, markPriceForMessagesChanged, unmarkPriceForMessagesChanged] = useFlag();
const [isPriceForMessagesOpen, setIsPriceForMessagesOpen] = useState(canChargeForMessages);
const [chargeForMessages, setChargeForMessages] = useState<number>(groupPeersPaidStars);
useEffect(() => {
if (progress === ManagementProgress.Complete) {
unmarkPriceForMessagesChanged();
}
}, [progress]);
const handleSavePermissions = useLastCallback(() => {
if (!chat) { if (!chat) {
return; return;
} }
setIsLoading(true); setIsLoading(true);
updateChatDefaultBannedRights({ chatId: chat.id, bannedRights: permissions }); updateChatDefaultBannedRights({ chatId: chat.id, bannedRights: permissions });
}, [chat, permissions, setIsLoading, updateChatDefaultBannedRights]); });
const handleUpdatePaidMessagesPrice = useLastCallback(() => {
if (!chat) return;
updatePaidMessagesPrice({
chatId: chat?.id,
paidMessagesStars: isPriceForMessagesOpen ? chargeForMessages : 0,
});
});
const handleUpdatePermissions = useLastCallback(() => {
if (isPriceForMessagesChanged) {
handleUpdatePaidMessagesPrice();
}
if (havePermissionChanged) {
handleSavePermissions();
}
});
const exceptionMembers = useMemo(() => { const exceptionMembers = useMemo(() => {
if (!members) { if (!members) {
@ -157,11 +206,24 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
return result; return result;
} }
const translatedString = lang(langKey); const translatedString = oldLang(langKey);
return `${result}${!result.length ? translatedString : `, ${translatedString}`}`; return `${result}${!result.length ? translatedString : `, ${translatedString}`}`;
}, ''); }, '');
}, [chat, lang]); }, [chat, oldLang]);
const handleChargeStarsForMessages = useLastCallback(() => {
setIsPriceForMessagesOpen(!isPriceForMessagesOpen);
markPriceForMessagesChanged();
});
const handleChargeForMessagesChange = useLastCallback((value: number) => {
setChargeForMessages(value);
markPriceForMessagesChanged();
});
const arePermissionsChanged = isPriceForMessagesChanged || havePermissionChanged;
const arePermissionsLoading = progress === ManagementProgress.InProgress || isLoading;
return ( return (
<div <div
@ -187,6 +249,43 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
/> />
</div> </div>
{arePaidMessagesAvailable && (
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<ListItem onClick={handleChargeStarsForMessages}>
<span>{lang('GroupMessagesChargePrice')}</span>
<Switcher
id="charge_for_messages"
label={lang('GroupMessagesChargePrice')}
checked={isPriceForMessagesOpen}
/>
</ListItem>
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('RightsChargeStarsAbout')}
</p>
</div>
)}
{isPriceForMessagesOpen && (
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<PaidMessagePrice
canChangeChargeForMessages
isGroupChat
chargeForMessages={chargeForMessages}
onChange={handleChargeForMessagesChange}
/>
</div>
)}
<div <div
className={buildClassName( className={buildClassName(
'section', 'section',
@ -237,12 +336,12 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
</div> </div>
<FloatingActionButton <FloatingActionButton
isShown={havePermissionChanged} isShown={arePermissionsChanged}
onClick={handleSavePermissions} onClick={handleUpdatePermissions}
ariaLabel={lang('Save')} ariaLabel={lang('Save')}
disabled={isLoading} disabled={arePermissionsLoading}
> >
{isLoading ? ( {arePermissionsLoading ? (
<Spinner color="white" /> <Spinner color="white" />
) : ( ) : (
<Icon name="check" /> <Icon name="check" />
@ -256,12 +355,20 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const fullInfo = selectChatFullInfo(global, chatId); const fullInfo = selectChatFullInfo(global, chatId);
const { progress } = selectTabState(global).management;
const paidMessagesStars = chat?.paidMessagesStars;
const configStarsPaidMessageCommissionPermille = global.appConfig?.starsPaidMessageCommissionPermille;
return { return {
chat, chat,
progress,
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
removedUsersCount: fullInfo?.kickedMembers?.length || 0, removedUsersCount: fullInfo?.kickedMembers?.length || 0,
members: fullInfo?.members, members: fullInfo?.members,
arePaidMessagesAvailable: Boolean(fullInfo?.arePaidMessagesAvailable && configStarsPaidMessageCommissionPermille),
canChargeForMessages: Boolean(paidMessagesStars && configStarsPaidMessageCommissionPermille),
groupPeersPaidStars: paidMessagesStars || DEFAULT_CHARGE_FOR_MESSAGES,
}; };
}, },
)(ManageGroupPermissions)); )(ManageGroupPermissions));

View File

@ -126,9 +126,7 @@
} }
.RangeSlider { .RangeSlider {
margin-top: 2rem; margin-top: 1rem;
margin-inline-start: 1rem;
margin-inline-end: 1rem;
} }
.button-position { .button-position {

View File

@ -18,7 +18,9 @@
.RangeSlider { .RangeSlider {
--slider-color: var(--color-primary); --slider-color: var(--color-primary);
margin-bottom: 1rem; margin: 0.5rem 0 0;
margin-inline-start: 1rem;
margin-inline-end: 1rem;
&.disabled { &.disabled {
pointer-events: none; pointer-events: none;

View File

@ -2896,6 +2896,27 @@ addActionHandler('toggleChannelRecommendations', (global, actions, payload): Act
setGlobal(global); setGlobal(global);
}); });
addActionHandler('updatePaidMessagesPrice', async (global, actions, payload): Promise<void> => {
const { chatId, paidMessagesStars, tabId = getCurrentTabId() } = payload;
const chat = chatId ? selectChat(global, chatId) : undefined;
if (!chat) return;
global = updateManagementProgress(global, ManagementProgress.InProgress, tabId);
setGlobal(global);
const result = await callApi('updatePaidMessagesPrice', {
chat,
paidMessagesStars,
});
if (!result) return;
global = getGlobal();
global = updateManagementProgress(global, ManagementProgress.Complete, tabId);
global = updateChat(global, chatId, { paidMessagesStars });
setGlobal(global);
});
addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise<void> => { addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Promise<void> => {
const { slug, tabId = getCurrentTabId() } = payload; const { slug, tabId = getCurrentTabId() } = payload;
const result = await callApi('resolveBusinessChatLink', { slug }); const result = await callApi('resolveBusinessChatLink', { slug });

View File

@ -1058,6 +1058,10 @@ export interface ActionPayloads {
chatId: string; chatId: string;
isEnabled: boolean; isEnabled: boolean;
}; };
updatePaidMessagesPrice: {
chatId: string;
paidMessagesStars: number;
} & WithTabId;
updateChat: { updateChat: {
chatId: string; chatId: string;

View File

@ -1709,6 +1709,7 @@ channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = U
channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates; channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates;
channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats; channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;
channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
channels.updatePaidMessagesPrice#fc84653f channel:InputChannel send_paid_messages_stars:long = Updates;
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool; bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.allowSendMessage#f132e3ef bot:InputUser = Updates;

View File

@ -277,6 +277,7 @@
"channels.getChannelRecommendations", "channels.getChannelRecommendations",
"channels.searchPosts", "channels.searchPosts",
"channels.reportSpam", "channels.reportSpam",
"channels.updatePaidMessagesPrice",
"bots.getBotRecommendations", "bots.getBotRecommendations",
"bots.canSendMessage", "bots.canSendMessage",
"bots.allowSendMessage", "bots.allowSendMessage",

View File

@ -1475,6 +1475,8 @@ export interface LangPair {
'DescriptionRestrictedMedia': undefined; 'DescriptionRestrictedMedia': undefined;
'DescriptionScheduledPaidMediaNotAllowed': undefined; 'DescriptionScheduledPaidMediaNotAllowed': undefined;
'DescriptionScheduledPaidMessagesNotAllowed': undefined; 'DescriptionScheduledPaidMessagesNotAllowed': undefined;
'GroupMessagesChargePrice': undefined;
'RightsChargeStarsAbout': undefined;
'UnlockButtonTitle': undefined; 'UnlockButtonTitle': undefined;
'PrivacySubscribeToTelegramPremium': undefined; 'PrivacySubscribeToTelegramPremium': undefined;
'PrivacyDisableLimitedEditionStarGifts': undefined; 'PrivacyDisableLimitedEditionStarGifts': undefined;
@ -2387,6 +2389,10 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'PaidMessageTransactionDescription': { 'PaidMessageTransactionDescription': {
'percent': V; 'percent': V;
}; };
'SetPriceGroupDescription': {
'percent': V;
'amount': V;
};
'FrozenAccountAppealSubtitle': { 'FrozenAccountAppealSubtitle': {
'botLink': V; 'botLink': V;
'date': V; 'date': V;