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,
stargiftsCount,
stargiftsAvailable,
paidMessagesAvailable,
} = result.fullChat;
if (chatPhoto) {
@ -717,6 +718,7 @@ async function getFullChannelInfo(
hasScheduledMessages: hasScheduled,
starGiftCount: stargiftsCount,
areStarGiftsAvailable: Boolean(stargiftsAvailable),
arePaidMessagesAvailable: paidMessagesAvailable,
},
chats,
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 }) {
const result = await invokeRequest(new GramJs.contacts.GetSponsoredPeers({ q: query }));
if (!result || result instanceof GramJs.contacts.SponsoredPeersEmpty) return undefined;

View File

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

View File

@ -1894,7 +1894,7 @@
"ExceptionTitlePrivacyChargeForMessages" = "Remove fee";
"ExceptionDescriptionPrivacyChargeForMessages" = "Add users or entire groups who won't be charged for sending messages to you.";
"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";
"PrivacyPaidMessagesValue" = "Paid";
"FirstMessageInPaidMessagesChat" = "**{user}** charges {amount} for each message.";
@ -1931,6 +1931,9 @@
"DescriptionRestrictedMedia" = "Posting media content is not allowed in this group.";
"DescriptionScheduledPaidMediaNotAllowed" = "Posting scheduled paid media content 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";
"FrozenAccountModalTitle" = "Your Account is Frozen";
"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 {
DEFAULT_CHARGE_FOR_MESSAGES,
DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
MINIMUM_CHARGE_FOR_MESSAGES,
} from '../../../config';
import {
selectIsCurrentUserPremium,
selectNewNoncontactPeersRequirePremium,
selectNonContactPeersPaidStars,
} from '../../../global/selectors';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import { formatStarsAsText } from '../../../util/localization/format';
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
import useHistoryBack from '../../../hooks/useHistoryBack';
@ -24,11 +20,9 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import PaidMessagePrice from '../../common/paidMessage/PaidMessagePrice';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import RangeSlider from '../../ui/RangeSlider';
import PremiumStatusItem from './PremiumStatusItem';
import PrivacyLockedOption from './PrivacyLockedOption';
@ -44,9 +38,6 @@ type StateProps = {
canLimitNewMessagesWithoutPremium?: boolean;
canChargeForMessages?: boolean;
isCurrentUserPremium?: boolean;
starsUsdWithdrawRate: number;
starsPaidMessageCommissionPermille: number;
starsPaidMessageAmountMax?: number;
nonContactPeersPaidStars: number;
noPaidReactionsForUsersCount: number;
};
@ -59,14 +50,11 @@ function PrivacyMessages({
shouldChargeForMessages,
nonContactPeersPaidStars,
isCurrentUserPremium,
starsPaidMessageCommissionPermille,
starsPaidMessageAmountMax,
starsUsdWithdrawRate,
noPaidReactionsForUsersCount,
onReset,
onScreenSelect,
}: OwnProps & StateProps) {
const { updateGlobalPrivacySettings, openPremiumModal } = getActions();
const { updateGlobalPrivacySettings } = getActions();
const oldLang = useOldLang();
const lang = useLang();
@ -131,68 +119,6 @@ function PrivacyMessages({
updateGlobalPrivacySettingsWithDebounced(value);
}, [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() {
const itemSubtitle = !noPaidReactionsForUsersCount ? lang('SubtitlePrivacyAddUsers')
: oldLang('Users', noPaidReactionsForUsersCount, 'i');
@ -248,7 +174,15 @@ function PrivacyMessages({
{privacyDescription}
</p>
</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()}
{!isCurrentUserPremium && selectedValue !== 'charge_for_messages'
&& <PremiumStatusItem premiumSection="message_privacy" />}
@ -259,12 +193,6 @@ function PrivacyMessages({
export default memo(withGlobal<OwnProps>((global): StateProps => {
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;
return {
@ -274,9 +202,6 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
isCurrentUserPremium: selectIsCurrentUserPremium(global),
canLimitNewMessagesWithoutPremium: global.appConfig?.canLimitNewMessagesWithoutPremium,
canChargeForMessages: global.appConfig?.starsPaidMessagesAvailable,
starsPaidMessageAmountMax: global.appConfig?.starsPaidMessageAmountMax || DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
starsPaidMessageCommissionPermille,
starsUsdWithdrawRate,
noPaidReactionsForUsersCount,
};
})(PrivacyMessages));

View File

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

View File

@ -3,7 +3,11 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
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 {
getMessageInvoice, getMessageText, isChatChannel,
} from '../../../global/helpers';

View File

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

View File

@ -1,25 +1,38 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useMemo, useState,
memo, useCallback, useEffect,
useMemo, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
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 useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useManagePermissions from '../hooks/useManagePermissions';
import Icon from '../../common/icons/Icon';
import PaidMessagePrice from '../../common/paidMessage/PaidMessagePrice';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem from '../../ui/ListItem';
import Spinner from '../../ui/Spinner';
import Switcher from '../../ui/Switcher';
type OwnProps = {
chatId: string;
@ -31,9 +44,13 @@ type OwnProps = {
type StateProps = {
chat?: ApiChat;
progress?: ManagementProgress;
currentUserId?: string;
removedUsersCount: number;
members?: ApiChatMember[];
arePaidMessagesAvailable?: boolean;
groupPeersPaidStars: number;
canChargeForMessages?: boolean;
};
const ITEM_HEIGHT = 48;
@ -83,18 +100,23 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
onScreenSelect,
onChatMemberSelect,
chat,
progress,
currentUserId,
removedUsersCount,
members,
onClose,
isActive,
arePaidMessagesAvailable,
canChargeForMessages,
groupPeersPaidStars,
}) => {
const { updateChatDefaultBannedRights } = getActions();
const { updateChatDefaultBannedRights, updatePaidMessagesPrice } = getActions();
const {
permissions, havePermissionChanged, isLoading, handlePermissionChange, setIsLoading,
} = useManagePermissions(chat?.defaultBannedRights);
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
useHistoryBack({
isActive,
@ -116,14 +138,41 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
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) {
return;
}
setIsLoading(true);
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(() => {
if (!members) {
@ -157,11 +206,24 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
return result;
}
const translatedString = lang(langKey);
const translatedString = oldLang(langKey);
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 (
<div
@ -187,6 +249,43 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
/>
</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
className={buildClassName(
'section',
@ -237,12 +336,12 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
</div>
<FloatingActionButton
isShown={havePermissionChanged}
onClick={handleSavePermissions}
isShown={arePermissionsChanged}
onClick={handleUpdatePermissions}
ariaLabel={lang('Save')}
disabled={isLoading}
disabled={arePermissionsLoading}
>
{isLoading ? (
{arePermissionsLoading ? (
<Spinner color="white" />
) : (
<Icon name="check" />
@ -256,12 +355,20 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const chat = selectChat(global, chatId);
const fullInfo = selectChatFullInfo(global, chatId);
const { progress } = selectTabState(global).management;
const paidMessagesStars = chat?.paidMessagesStars;
const configStarsPaidMessageCommissionPermille = global.appConfig?.starsPaidMessageCommissionPermille;
return {
chat,
progress,
currentUserId: global.currentUserId,
removedUsersCount: fullInfo?.kickedMembers?.length || 0,
members: fullInfo?.members,
arePaidMessagesAvailable: Boolean(fullInfo?.arePaidMessagesAvailable && configStarsPaidMessageCommissionPermille),
canChargeForMessages: Boolean(paidMessagesStars && configStarsPaidMessageCommissionPermille),
groupPeersPaidStars: paidMessagesStars || DEFAULT_CHARGE_FOR_MESSAGES,
};
},
)(ManageGroupPermissions));

View File

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

View File

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

View File

@ -2896,6 +2896,27 @@ addActionHandler('toggleChannelRecommendations', (global, actions, payload): Act
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> => {
const { slug, tabId = getCurrentTabId() } = payload;
const result = await callApi('resolveBusinessChatLink', { slug });

View File

@ -1058,6 +1058,10 @@ export interface ActionPayloads {
chatId: string;
isEnabled: boolean;
};
updatePaidMessagesPrice: {
chatId: string;
paidMessagesStars: number;
} & WithTabId;
updateChat: {
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.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.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.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;

View File

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

View File

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