Layer: Support layer 206 and 207 (#6038)

This commit is contained in:
Alexander Zinchuk 2025-07-25 19:34:40 +02:00
parent d76356da1f
commit 1e12d4f628
67 changed files with 3136 additions and 310 deletions

399
CLAUDE.md Normal file
View File

@ -0,0 +1,399 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Instructions
You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep experience in our project's simplified React-like API. You are working on a modern web app for Telegram.
- **Be concise.** Only change code directly related to the current task; leave unrelated parts untouched.
- **Reuse** existing types, functions and components. Search before creating a new one.
- **No new libraries.** Use existing dependencies only. If a task truly can't be done without a new library, stop and explain why.
- **Do not** write tests.
- **SCSS modules:**
- Name classes in camelCase.
- Import as `styles` in your component:
```scss
/* Component.module.scss */
.myWrapper { /*…*/ }
```
```tsx
/* Component.tsx */
import styles from "./Component.module.scss";
<div className={buildClassName(styles.myWrapper, "legacy-class")} />
```
- Use [buildClassName.ts](mdc:src/util/buildClassName.ts) to merge multiple class names.
- **Always extract styles to files** - avoid inline styles unless absolutely necessary.
- **If file already imports styles**, check where they come from and add new styles there - don't create new style files.
- Use rem units for all measurements.
- **Code Style:**
- Early returns.
- Prefix boolean variables with primary or modal auxiliaries (e.q. `isOpen`, `willUpdate`, `shouldRender`).
- Functions should start with a verb (e.q. `openModal`, `closeDialog`, `handleClick`).
- Prefer checking required parameter before calling a function, avoid making it optinal and checking at the beginning of the function.
- Only leave comments for complex logic.
- **IMPORTANT: Avoid conditional spread operators** - TypeScript doesn't check if spread fields match the target type.
```typescript
// ❌ BAD - No type checking
{ ...condition && { field: value } }
// ✅ GOOD - Full type checking
{ field: condition ? value : undefined }
```
- **Localization & Text Rules:**
- **ALWAYS** use `lang()` for all text content - never hardcode strings.
- `lang()` can accept parameters: `lang('Key', { param: value })`.
- Add new translations to end of `src/assets/localization/fallback.strings`.
- **After your solution:**
1. Critique it—identify any shortcomings.
2. Fix those issues, do more planning.
3. Present the improved result.
- **When deeper debugging is needed:**
1. Outline clear, step-by-step debugging instructions for the operator.
2. Remove any temporary debug code once the issue is resolved.
- **Lint errors you can't fix manually:**
Suggest running `eslint --fix <filename>`.
# Telegram Web API Guide
## 1. API Definition
- The master file is `src/lib/gramjs/tl/static/api.tl` (TL syntax).
- **Don't edit** this autogenerated file. TypeScript types live in `api.d.ts`.
- We use GramJS inside a web worker; UI code uses plain objects (`Api*` types) in `src/api/types`.
## 2. Generating Code
1. Make sure to include the method name in `api.json`.
2. Run:
```bash
npm run gramjs:tl
```
to regenerate `api.d.ts`.
3. In `src/api/gramjs/methods/`, pick a file for your method, then:
* Name fetchers `fetch*` if the TL method starts with `get`.
* Use a destructured parameter object.
* Call the API via:
```ts
const result = await invokeRequest(
new GramJs.namespace.MethodName({ /* params */ })
);
```
* If `result` is `undefined`, return `undefined` to signal an error.
* Convert any returned GramJS classes into plain `Api*` objects.
Convesion from and to Api* objects is done by `apiBuilders` (function name starts with `buildApi*`) and `gramjsBuilders` (function name `buildInput*`).
## 3. Using the API
* In your actions, call:
```ts
const result = await callApi('methodName', { /* params */ });
```
* Always check for `undefined` before proceeding.
## 4. Example
```ts
// src/api/gramjs/methods/users.ts
export async function fetchUsers({ users }: { users: ApiUser[] }) {
const result = await invokeRequest(new GramJs.users.GetUsers({
id: users.map(({ id, accessHash }) => buildInputUser(id, accessHash)),
}));
if (!result || !result.length) {
return undefined;
}
const apiUsers = result.map(buildApiUser).filter(Boolean);
const userStatusesById = buildApiUserStatuses(result);
return {
users: apiUsers,
userStatusesById,
};
}
// src/global/actions/api/users.ts
addActionHandler('loadUser', async (global, actions, { userId }) => {
const user = selectUser(global, userId);
if (!user) return;
const res = await callApi('fetchUsers', { users: [user] });
if (!res) return;
// update global state...
});
```
## 5. Handling Updates
* Updates come in via `mtpUpdateHandler.ts`.
* They're routed through `src/global/actions/apiUpdaters` to merge into global state.
* Types are defined in `src/api/types/updates.ts`.
## Component Style Guide
### 1. Basics & Imports
* All components use JSX and render with Teact.
* **Always** import React from teact library, for JSX compatibility reasons. Only import from `'react'` when you need React **types** that are not provided in Teact.
* Built-in hooks live in `src/lib/teact/teact`. Import them from there.
### 2. Props & Types
* Split your props into two types:
* **OwnProps**: data passed in by the parent
* **StateProps**: data injected by `withGlobal` HOC
* Merge them as `OwnProps & StateProps` when defining your component.
* You can skip one or both if they are not used.
* **Order rule**: list any function types *last* in your props definitions.
### 3. Hooks
* **useLastCallback** is your go-to for callbacks, since it won't trigger re-renders and always uses the latest scope.
* Only use **useCallback** when you really need to memoize a render function.
* Prefer **useFlag()** over `useState<boolean>()` for simple boolean toggles.
* Check the `hooks/` folders for additional utilities.
### 4. Component Signature
> **Migrate** any old `FC` syntax to the new form.
```ts
// Before
const OldComp: FC<OwnProps & StateProps> = ({ … }) => { … }
// After
const NewComp = (props: OwnProps & StateProps) => { … }
```
### 5. Memoization
* Wrap most components with `memo()` to avoid unnecessary updates.
* Don't pass freshly created objects or arrays as props to memoized components.
* **Exceptions** (no memo): `ListItem`, `Button`, `MenuItem`, etc.
### 6. Localization
* Call `const lang = useLang()` at the top of your component.
* Look up the localization guide for how to add new language keys.
---
### Example
```ts
import React, { useFlag } from '../../lib/teact/teact';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import styles from './Component.module.scss';
type OwnProps = {
id: string;
className?: string;
onClick?: NoneToVoidFunction;
};
type StateProps = {
stateValue?: string;
};
// Constants first
const MAX_ITEMS = 10
const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps) => {
const { someAction } = getActions(); // Should always be first, if actions are used
const ref = useRef<HTMLDivElement>(null);
const [color, setColor] = useState('#FF00FF');
const [isOpen, open, close] = useFlag();
const lang = useLang();
const handleClick = useLastCallback(() => {
if (!ref.current) return;
const el = ref.current;
setColor(el.value);
close();
onClick?.();
someAction(el.value);
});
return (
<div ref={ref} className={styles.root + (className ? ` ${className}` : '')}>
<button onClick={handleClick}>{lang('ButtonKey')}</button>
<p>{stateValue}</p>
</div>
);
}
export default memo(withGlobal<OwnProps>((global, { id }): StateProps => {
const stateValue = selectValue(global, id);
return {
stateValue,
};
})(Component);
)
```
## Global State Overview
Global State is our single, app-wide store, similar to Redux or Zustand. All its code lives under `src/global/`, with subfolders grouping related functionality (for example, `selectors/users.ts` holds all user-related selectors).
### 1. Folder Structure
* **`actions/`**: Actions that are used to update global from any point in the app
* **`selectors/`**: Pure functions that read data (e.g. `selectors/users.ts`).
* **`reducers/`**: Functions that update global state.
* **`types/`**: All TypeScript types live in `src/global/types`.
* **`cache.ts`**: Manages saving a slimmed-down copy of global to IndexedDB.
### 2. Actions
1. **Preffered** way to update global. When inside action, use `setGlobal`, or simple `return` if sync.
2. **Sync actions** return type should be `ActionReturnType`.
3. **Async actions** return type should be `Promise<void>`.
4. If you add or remove an action, update `actions.ts` accordingly.
5. Actions in `ui` folder should be only sync.
### 3. Multi-Tab Support
* Actions and selectors can accept a `tabId` parameter, so we don't lose tab context when working with multiple tabs.
* **`tabId` is required** if calling an action or selector that can accept it.
* **Exception**: UI components may call without `tabId` (they receive it automatically).
### 4. Selectors & Reducers
* If logic takes more than one line, create a new selector or reducer in the appropriate folder and file.
* **Selectors must be pure**: only use their inputs and global. Don't allocate new objects or arrays, as that breaks memoization.
### 5. Data Constraints
* Global may only store serializable primitives (strings, numbers, booleans).
* When you change a type that's cached in `cache.ts`, add a migration to avoid errors from new selectors.
---
## Component Guidelines
### 1. Accessing Global in Components
* **Use** `withGlobal` (a `mapStateToProps` helper) to pull in state.
* **Avoid** the experimental `useSelector` API.
* **Use** `getGlobal` **only** inside hooks for one-off reads (it's non-reactive).
### 2. Performance
* Wrap `withGlobal` in `memo` so the component re-renders only on real data changes.
* **Don't** return new arrays or objects inside `withGlobal`; that defeats memoization.
* If you need to filter or map a list, **pass IDs as props** and do the heavy work in a `useMemo` hook.
### 3. Example Component
```ts
type OwnProps = { id: string };
type StateProps = {
someValue?: string;
otherValue?: number;
thirdValue: boolean;
};
const Component = ({
id,
someValue,
otherValue,
thirdValue,
}: OwnProps & StateProps) => {
// component logic...
};
export default memo(
withGlobal<OwnProps>((global, { id }) => {
const { otherValue } = selectTabState(global);
const someValue = selectSomeValue(global, id);
const thirdValue = Boolean(global.rawValue);
return {
someValue,
otherValue,
thirdValue,
};
})(Component);
);
```
# Localization Guide
**1. Setup & Fallback**
* Translations live on [Translation Platform](https://translations.telegram.org/).
* Fallback file: `src/assets/localization/fallback.strings`.
**2. Getting Strings**
```ts
const lang = useLang();
// Simple
lang('SimpleKey');
// Plurals
lang('PluralKey', undefined, { pluralValue: 3 });
// String replacements
lang('ReplKey', { name: 'Amy' });
// JSX nodes (e.g. links)
lang('LinkKey', { link: <Link /> }, { withNodes: true });
// Markdown
lang('MarkdownKey', undefined, { withNodes: true, withMarkdown: true });
```
**3. Adding a New Key**
1. Search Translation Platform for similar strings to get the correct wording.
2. Add it to `fallback.strings`.
3. If it's plural, include `_one` and `_other`.
4. Run `npm run lang:ts`.
**4. Naming Rules**
* **PascalCase** (no dots).
* Use short, clear prefixes for context (e.g. `Acc` for accessibility).
* Keep names under ~30 chars, shorten consistently if needed.
**5. API & Options**
* **Basic**: `lang(key, vars?, options?) → string`
* **Advanced** (`withNodes`): returns `TeactNode[]` so you can inject JSX.
* **Other options**:
* `withMarkdown` (for simple markdown + emojis)
* `renderTextFilters` (custom filters)
* `specialReplacement` (for replacing substrings, e.g. icons)
* **Object syntax**:
Simple form that returns string can be used in some actions.
```ts
actions.showNotification({ key: 'LangKey' });
lang.with({ key: 'hello', vars: { name }, options: { withNodes: true } });
```
**6. Handy Extensions**
* `lang.region(code)` → country name
* `lang.conjunction(['a','b','c'])` → "a, b, and c"
* `lang.disjunction(['x','y'])` → "x or y"
* `lang.number(1234)` → locale-formatted number
* Flags: `lang.isRtl`, `lang.code`, `lang.rawCode`
**7. Beyond React**
Use `getTranslationFn()` to grab the same `lang` function in non-component code. Discouraged, use object syntax.

View File

@ -101,6 +101,13 @@ export interface GramJsAppConfig extends LimitsConfig {
stars_stargift_resale_amount_max?: number;
stars_stargift_resale_amount_min?: number;
stars_stargift_resale_commission_permille?: number;
stars_suggested_post_amount_max?: number;
stars_suggested_post_amount_min?: number;
stars_suggested_post_commission_permille?: number;
stars_suggested_post_age_min?: number;
stars_suggested_post_future_max?: number;
stars_suggested_post_future_min?: number;
ton_suggested_post_commission_permille?: number;
poll_answers_max?: number;
todo_items_max?: number;
todo_title_length_max?: number;
@ -205,6 +212,13 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
starsStargiftResaleAmountMin: appConfig.stars_stargift_resale_amount_min,
starsStargiftResaleAmountMax: appConfig.stars_stargift_resale_amount_max,
starsStargiftResaleCommissionPermille: appConfig.stars_stargift_resale_commission_permille,
starsSuggestedPostAmountMax: appConfig.stars_suggested_post_amount_max,
starsSuggestedPostAmountMin: appConfig.stars_suggested_post_amount_min,
starsSuggestedPostCommissionPermille: appConfig.stars_suggested_post_commission_permille,
starsSuggestedPostAgeMin: appConfig.stars_suggested_post_age_min,
starsSuggestedPostFutureMax: appConfig.stars_suggested_post_future_max,
starsSuggestedPostFutureMin: appConfig.stars_suggested_post_future_min,
tonSuggestedPostCommissionPermille: appConfig.ton_suggested_post_commission_permille,
pollMaxAnswers: appConfig.poll_answers_max,
todoItemsMax: appConfig.todo_items_max ?? TODO_ITEMS_LIMIT,
todoTitleLengthMax: appConfig.todo_title_length_max ?? TODO_TITLE_LENGTH_LIMIT,

View File

@ -24,7 +24,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
if (starGift instanceof GramJs.StarGiftUnique) {
const {
id, num, ownerId, ownerName, title, attributes, availabilityIssued, availabilityTotal, slug, ownerAddress,
giftAddress, resellStars,
giftAddress, resellStars, releasedBy,
} = starGift;
return {
@ -41,12 +41,13 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
slug,
giftAddress,
resellPriceInStars: resellStars?.toJSNumber(),
releasedByPeerId: releasedBy && getApiChatIdFromMtpPeer(releasedBy),
};
}
const {
id, limited, stars, availabilityRemains, availabilityTotal, convertStars, firstSaleDate, lastSaleDate, soldOut,
birthday, upgradeStars, resellMinStars, title, availabilityResale,
birthday, upgradeStars, resellMinStars, title, availabilityResale, releasedBy,
} = starGift;
addDocumentToLocalDb(starGift.sticker);
@ -69,6 +70,7 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
upgradeStars: upgradeStars?.toJSNumber(),
title,
resellMinStars: resellMinStars?.toJSNumber(),
releasedByPeerId: releasedBy && getApiChatIdFromMtpPeer(releasedBy),
availabilityResale: availabilityResale?.toJSNumber(),
};
}

View File

@ -7,6 +7,7 @@ import { buildApiBotApp } from './bots';
import { buildApiFormattedText, buildApiPhoto } from './common';
import { buildApiStarGift } from './gifts';
import { buildTodoItem } from './messageContent';
import { buildApiStarsAmount } from './payments';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
const UNSUPPORTED_ACTION: ApiMessageAction = {
@ -447,6 +448,36 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
count,
};
}
if (action instanceof GramJs.MessageActionSuggestedPostApproval) {
const {
rejected, balanceTooLow, rejectComment, scheduleDate, price,
} = action;
return {
mediaType: 'action',
type: 'suggestedPostApproval',
isRejected: Boolean(rejected),
isBalanceTooLow: Boolean(balanceTooLow),
rejectComment,
scheduleDate,
amount: price ? buildApiStarsAmount(price) : undefined,
};
}
if (action instanceof GramJs.MessageActionSuggestedPostSuccess) {
const { price } = action;
return {
mediaType: 'action',
type: 'suggestedPostSuccess',
amount: buildApiStarsAmount(price),
};
}
if (action instanceof GramJs.MessageActionSuggestedPostRefund) {
const { payerInitiated } = action;
return {
mediaType: 'action',
type: 'suggestedPostRefund',
payerInitiated: Boolean(payerInitiated),
};
}
if (action instanceof GramJs.MessageActionTodoCompletions) {
const {
completed, incompleted,

View File

@ -8,6 +8,7 @@ import type {
ApiFactCheck,
ApiInputMessageReplyInfo,
ApiInputReplyInfo,
ApiInputSuggestedPostInfo,
ApiMediaTodo,
ApiMessage,
ApiMessageEntity,
@ -25,6 +26,7 @@ import type {
ApiSticker,
ApiStory,
ApiStorySkipped,
ApiSuggestedPost,
ApiThreadInfo,
ApiVideo,
MediaContent,
@ -44,6 +46,9 @@ import { addTimestampEntities } from '../../../util/dates/timestamp';
import { omitUndefined, pick } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform';
import {
buildApiStarsAmount,
} from '../apiBuilders/payments';
import { buildPeer } from '../gramjsBuilders';
import {
addDocumentToLocalDb,
@ -241,6 +246,7 @@ export function buildApiMessageWithChatId(
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
emojiOnlyCount,
...(mtpMessage.replyTo && { replyInfo: buildApiReplyInfo(mtpMessage.replyTo, mtpMessage) }),
...(mtpMessage.suggestedPost && { suggestedPostInfo: buildApiSuggestedPost(mtpMessage.suggestedPost) }),
forwardInfo,
isEdited,
editDate: mtpMessage.editDate,
@ -280,7 +286,7 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un
}
const {
message, entities, replyTo, date, effect,
message, entities, replyTo, date, effect, suggestedPost,
} = draft;
const replyInfo = replyTo instanceof GramJs.InputReplyToMessage ? {
@ -293,14 +299,31 @@ export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | un
quoteOffset: replyTo.quoteOffset,
} satisfies ApiInputMessageReplyInfo : undefined;
const suggestedPostInfo = suggestedPost instanceof GramJs.SuggestedPost ? {
isAccepted: suggestedPost.accepted,
isRejected: suggestedPost.rejected,
price: suggestedPost.price ? buildApiStarsAmount(suggestedPost.price) : undefined,
scheduleDate: suggestedPost.scheduleDate,
} satisfies ApiInputSuggestedPostInfo : undefined;
return {
text: message ? buildMessageTextContent(message, entities) : undefined,
replyInfo,
suggestedPostInfo,
date,
effectId: effect?.toString(),
};
}
function buildApiSuggestedPost(suggestedPost: GramJs.SuggestedPost): ApiSuggestedPost {
return {
isAccepted: suggestedPost.accepted,
isRejected: suggestedPost.rejected,
price: suggestedPost.price ? buildApiStarsAmount(suggestedPost.price) : undefined,
scheduleDate: suggestedPost.scheduleDate,
};
}
function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWithSelf = false): ApiMessageForwardInfo {
const savedFromPeerId = fwdFrom.savedFromPeer && getApiChatIdFromMtpPeer(fwdFrom.savedFromPeer);
const fromId = fwdFrom.fromId && getApiChatIdFromMtpPeer(fwdFrom.fromId);
@ -396,6 +419,7 @@ export function buildLocalMessage(
text?: string,
entities?: ApiMessageEntity[],
replyInfo?: ApiInputReplyInfo,
suggestedPostInfo?: ApiInputSuggestedPostInfo,
attachment?: ApiAttachment,
sticker?: ApiSticker,
gif?: ApiVideo,
@ -439,6 +463,7 @@ export function buildLocalMessage(
isOutgoing: !isChannel,
senderId: chat.type !== 'chatTypePrivate' ? (sendAs?.id || currentUserId) : undefined,
replyInfo: resultReplyInfo,
suggestedPostInfo,
...(groupedId && {
groupedId,
...(media && (media.photo || media.video) && { isInAlbum: true }),

View File

@ -461,11 +461,26 @@ export function buildApiStarsGiftOptions(option: GramJs.StarsGiftOption): ApiSta
};
}
export function buildApiStarsAmount(amount: GramJs.StarsAmount): ApiStarsAmount {
return {
amount: amount.amount.toJSNumber(),
export function buildApiStarsAmount(amount: GramJs.TypeStarsAmount): ApiStarsAmount | undefined {
if (amount instanceof GramJs.StarsAmount) {
return {
amount: amount.amount.toJSNumber(),
nanos: amount.nanos,
};
}
if (amount instanceof GramJs.StarsTonAmount) {
return undefined;
}
return undefined;
}
export function buildInputStarsAmount(amount: ApiStarsAmount): GramJs.TypeStarsAmount {
return new GramJs.StarsAmount({
amount: bigInt(amount.amount),
nanos: amount.nanos,
};
});
}
export function buildApiStarsGiveawayWinnersOption(
@ -532,9 +547,9 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
return { type: 'unsupported' };
}
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction | undefined {
const {
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
date, id, peer, amount, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages,
stargiftResale,
} = transaction;
@ -548,11 +563,16 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
const starRefCommision = starrefCommissionPermille ? starrefCommissionPermille / 10 : undefined;
const starsAmount = buildApiStarsAmount(amount);
if (!starsAmount) {
return undefined;
}
return {
id,
date,
peer: buildApiStarsTransactionPeer(peer),
stars: buildApiStarsAmount(stars),
stars: starsAmount,
title,
description,
photo: photo && buildApiWebDocument(photo),

View File

@ -54,15 +54,15 @@ export function buildChannelStatistics(stats: GramJs.stats.BroadcastStats): ApiC
}
export function buildChannelMonetizationStatistics(
stats: GramJs.stats.BroadcastRevenueStats,
stats: GramJs.payments.StarsRevenueStats,
): ApiChannelMonetizationStatistics {
return {
// Graphs
topHoursGraph: buildGraph(stats.topHoursGraph),
topHoursGraph: stats.topHoursGraph ? buildGraph(stats.topHoursGraph) : undefined,
revenueGraph: buildGraph(stats.revenueGraph, undefined, true, stats.usdRate),
// Statistics overview
balances: buildChannelMonetizationBalances(stats.balances),
balances: buildChannelMonetizationBalances(stats.status),
usdRate: stats.usdRate,
};
}
@ -269,7 +269,7 @@ function buildChannelMonetizationBalances({
availableBalance,
overallRevenue,
withdrawalEnabled,
}: GramJs.BroadcastRevenueBalances): ChannelMonetizationBalances {
}: GramJs.StarsRevenueStatus): ChannelMonetizationBalances {
return {
currentBalance: Number(currentBalance) / DECIMALS,
availableBalance: Number(availableBalance) / DECIMALS,

View File

@ -15,6 +15,7 @@ import type {
ApiInputPrivacyRules,
ApiInputReplyInfo,
ApiInputStorePaymentPurpose,
ApiInputSuggestedPostInfo,
ApiMessageEntity,
ApiNewMediaTodo,
ApiNewPoll,
@ -40,6 +41,7 @@ import {
import { CHANNEL_ID_BASE, DEFAULT_STATUS_ICON_ID } from '../../../config';
import { pick } from '../../../util/iteratees';
import { buildInputStarsAmount } from '../apiBuilders/payments';
import { deserializeBytes } from '../helpers/misc';
import localDb from '../localDb';
@ -888,6 +890,16 @@ export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
return undefined;
}
export function buildInputSuggestedPost(suggestedPostInfo: ApiInputSuggestedPostInfo): GramJs.SuggestedPost {
const isPaid = Boolean(suggestedPostInfo.price)
&& Boolean((suggestedPostInfo.price.amount || suggestedPostInfo.price.nanos));
return new GramJs.SuggestedPost({
price: isPaid ? buildInputStarsAmount(suggestedPostInfo.price!) : undefined,
scheduleDate: suggestedPostInfo.scheduleDate,
});
}
export function buildInputPrivacyRules(
rules: ApiInputPrivacyRules,
) {

View File

@ -68,6 +68,7 @@ import {
buildInputPeer,
buildInputPhoto,
buildInputReplyTo,
buildInputSuggestedPost,
buildInputUser,
buildMtpMessageEntity,
DEFAULT_PRIMITIVES,
@ -526,6 +527,7 @@ export function saveDraft({
message: draft?.text?.text || DEFAULT_PRIMITIVES.STRING,
entities: draft?.text?.entities?.map(buildMtpMessageEntity),
replyTo: draft?.replyInfo && buildInputReplyTo(draft.replyInfo),
suggestedPost: draft?.suggestedPostInfo && buildInputSuggestedPost(draft.suggestedPostInfo),
}));
}

View File

@ -14,6 +14,7 @@ import type {
ApiFormattedText,
ApiGlobalMessageSearchType,
ApiInputReplyInfo,
ApiInputSuggestedPostInfo,
ApiMessage,
ApiMessageEntity,
ApiMessageSearchContext,
@ -76,13 +77,16 @@ import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
import {
buildInputChannel,
buildInputDocument,
buildInputMediaDocument,
buildInputPeer,
buildInputPhoto,
buildInputPoll,
buildInputPollFromExisting,
buildInputReaction,
buildInputReplyTo,
buildInputStory,
buildInputSuggestedPost,
buildInputTextWithEntities,
buildInputTodo,
buildInputUser,
@ -98,6 +102,7 @@ import {
deserializeBytes,
resolveMessageApiChatId,
} from '../helpers/misc';
import localDb from '../localDb';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { processMessageAndUpdateThreadInfo } from '../updates/entityProcessor';
import { processAffectedHistory, updateChannelState } from '../updates/updateManager';
@ -266,8 +271,8 @@ export function sendMessageLocal(
params: SendMessageParams,
) {
const {
chat, lastMessageId, text, entities, replyInfo, attachment, sticker, story, gif, poll, todo, contact,
scheduledAt, groupedId, sendAs, wasDrafted, isInvertedMedia, effectId, isPending, messagePriceInStars,
chat, lastMessageId, text, entities, replyInfo, suggestedPostInfo, attachment, sticker, story, gif, poll, todo,
contact, scheduledAt, groupedId, sendAs, wasDrafted, isInvertedMedia, effectId, isPending, messagePriceInStars,
} = params;
if (!chat) return undefined;
@ -281,6 +286,7 @@ export function sendMessageLocal(
text,
entities,
replyInfo,
suggestedPostInfo,
attachment,
sticker,
gif,
@ -315,7 +321,9 @@ export function sendApiMessage(
onProgress?: ApiOnProgress,
) {
const {
chat, text, entities, replyInfo, attachment, sticker, story, gif, poll, todo, contact,
chat, text, entities, replyInfo, suggestedPostInfo, suggestedMedia,
attachment, sticker, story, gif, poll, todo, contact,
isSilent, scheduledAt, groupedId, noWebPage, sendAs, shouldUpdateStickerSetOrder,
isInvertedMedia, effectId, webPageMediaSize, webPageUrl, messagePriceInStars,
} = params;
@ -343,6 +351,7 @@ export function sendApiMessage(
text,
entities,
replyInfo,
suggestedPostInfo,
attachment: attachment!,
groupedId,
isSilent,
@ -353,7 +362,43 @@ export function sendApiMessage(
const messagePromise = (async () => {
let media: GramJs.TypeInputMedia | undefined;
if (attachment) {
if (suggestedPostInfo && suggestedMedia && !attachment) {
if (suggestedMedia.photo) {
const inputPhoto = buildInputPhoto(suggestedMedia.photo);
if (inputPhoto) {
media = new GramJs.InputMediaPhoto({
id: inputPhoto,
spoiler: suggestedMedia.photo.isSpoiler || undefined,
});
}
} else if (suggestedMedia.video) {
const inputDocument = buildInputDocument(suggestedMedia.video);
if (inputDocument) {
media = new GramJs.InputMediaDocument({
id: inputDocument,
spoiler: suggestedMedia.video.isSpoiler || undefined,
});
}
} else if (suggestedMedia.document) {
const document = suggestedMedia.document;
if (document.id) {
const localDocument = localDb.documents[document.id];
if (localDocument) {
const inputDocument = new GramJs.InputDocument({
id: localDocument.id,
accessHash: localDocument.accessHash,
fileReference: localDocument.fileReference,
});
media = new GramJs.InputMediaDocument({
id: inputDocument,
});
}
}
}
}
if (!media && attachment) {
try {
media = await uploadMedia(localMessage, attachment, onProgress!);
} catch (err) {
@ -416,6 +461,7 @@ export function sendApiMessage(
invertMedia: isInvertedMedia || undefined,
effect: effectId ? BigInt(effectId) : undefined,
allowPaidStars: messagePriceInStars ? BigInt(messagePriceInStars) : undefined,
suggestedPost: suggestedPostInfo && buildInputSuggestedPost(suggestedPostInfo),
};
try {
@ -477,6 +523,7 @@ function sendGroupedMedia(
text = DEFAULT_PRIMITIVES.STRING,
entities,
replyInfo,
suggestedPostInfo,
attachment,
groupedId,
isSilent,
@ -488,6 +535,7 @@ function sendGroupedMedia(
text?: string;
entities?: ApiMessageEntity[];
replyInfo?: ApiInputReplyInfo;
suggestedPostInfo?: ApiInputSuggestedPostInfo;
attachment: ApiAttachment;
groupedId: string;
isSilent?: boolean;
@ -571,6 +619,7 @@ function sendGroupedMedia(
...(scheduledAt && { scheduleDate: scheduledAt }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
...(messagePriceInStars && { allowPaidStars: BigInt(messagePriceInStars * count) }),
...(suggestedPostInfo && { suggestedPost: buildInputSuggestedPost(suggestedPostInfo) }),
}), {
shouldIgnoreUpdates: true,
});
@ -1075,6 +1124,30 @@ export async function deleteSavedHistory({
});
}
export async function toggleSuggestedPostApproval({
chat,
messageId,
reject,
scheduleDate,
rejectComment,
}: {
chat: ApiChat;
messageId: number;
reject?: boolean;
scheduleDate?: number;
rejectComment?: string;
}) {
const result = await invokeRequest(new GramJs.messages.ToggleSuggestedPostApproval({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: messageId,
reject: reject || undefined,
scheduleDate,
rejectComment,
}));
return result;
}
export async function reportMessages({
peer, messageIds, description, option,
}: {

View File

@ -10,6 +10,7 @@ import type {
ApiStarGiftRegular,
} from '../../types';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildApiResaleGifts, buildApiSavedStarGift, buildApiStarGift,
buildApiStarGiftAttribute, buildInputResaleGiftsAttributes } from '../apiBuilders/gifts';
import {
@ -20,6 +21,7 @@ import {
buildApiStarsTransaction,
buildApiStarTopupOption,
} from '../apiBuilders/payments';
import { buildApiUser } from '../apiBuilders/users';
import { buildInputPeer, buildInputSavedStarGift, buildInputUser, DEFAULT_PRIMITIVES } from '../gramjsBuilders';
import { checkErrorType, wrapError } from '../helpers/misc';
import { invokeRequest } from './client';
@ -44,8 +46,18 @@ export async function fetchStarGifts() {
return undefined;
}
const chats = result.chats?.map((chat) => buildApiChatFromPreview(chat)).filter(Boolean);
const users = result.users?.map(buildApiUser).filter(Boolean);
// Right now, only regular star gifts can be bought, but API are not specific
return result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift');
const gifts
= result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift');
return {
gifts,
chats,
users,
};
}
export async function fetchResaleGifts({
@ -179,12 +191,18 @@ export async function fetchStarsStatus() {
return undefined;
}
const balance = buildApiStarsAmount(result.balance);
if (!balance) {
// For now, skip if balance is in TON
return undefined;
}
return {
nextHistoryOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction),
history: result.history?.map(buildApiStarsTransaction).filter(Boolean),
nextSubscriptionOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions?.map(buildApiStarsSubscription),
balance: buildApiStarsAmount(result.balance),
balance,
};
}
@ -214,10 +232,16 @@ export async function fetchStarsTransactions({
return undefined;
}
const balance = buildApiStarsAmount(result.balance);
if (!balance) {
// For now, skip if balance is in TON
return undefined;
}
return {
nextOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction),
balance: buildApiStarsAmount(result.balance),
history: result.history?.map(buildApiStarsTransaction).filter(Boolean),
balance,
};
}
@ -261,10 +285,16 @@ export async function fetchStarsSubscriptions({
return undefined;
}
const balance = buildApiStarsAmount(result.balance);
if (!balance) {
// For now, skip if balance is in TON
return undefined;
}
return {
nextOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions.map(buildApiStarsSubscription),
balance: buildApiStarsAmount(result.balance),
balance,
};
}

View File

@ -45,7 +45,7 @@ export async function fetchChannelMonetizationStatistics({
peer: ApiPeer;
dcId?: number;
}) {
const result = await invokeRequest(new GramJs.stats.GetBroadcastRevenueStats({
const result = await invokeRequest(new GramJs.payments.GetStarsRevenueStats({
peer: buildInputPeer(peer.id, peer.accessHash),
}), {
dcId,
@ -234,7 +234,7 @@ export async function fetchMonetizationRevenueWithdrawalUrl({
return password;
}
const result = await invokeRequest(new GramJs.stats.GetBroadcastRevenueWithdrawalUrl({
const result = await invokeRequest(new GramJs.payments.GetStarsRevenueWithdrawalUrl({
peer: buildInputPeer(peer.id, peer.accessHash),
password,
}), {

View File

@ -1042,9 +1042,14 @@ export function updater(update: Update) {
isEnabled: update.enabled ? true : undefined,
});
} else if (update instanceof GramJs.UpdateStarsBalance) {
const balance = buildApiStarsAmount(update.balance);
if (!balance) {
// Skip TON balance updates for now
return;
}
sendApiUpdate({
'@type': 'updateStarsBalance',
balance: buildApiStarsAmount(update.balance),
balance,
});
} else if (update instanceof GramJs.UpdatePaidReactionPrivacy) {
sendApiUpdate({

View File

@ -1,6 +1,6 @@
import type { ApiBotCommand } from './bots';
import type {
ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiPhoto, ApiStickerSet,
ApiChatReactions, ApiFormattedText, ApiInputMessageReplyInfo, ApiInputSuggestedPostInfo, ApiPhoto, ApiStickerSet,
} from './messages';
import type { ApiBotVerification, ApiChatInviteImporter, ApiPeerNotifySettings } from './misc';
import type {
@ -318,6 +318,7 @@ export interface ApiChatLink {
export type ApiDraft = {
text?: ApiFormattedText;
replyInfo?: ApiInputMessageReplyInfo;
suggestedPostInfo?: ApiInputSuggestedPostInfo;
date?: number;
effectId?: string;
isLocal?: boolean;

View File

@ -1,7 +1,7 @@
import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls';
import type { ApiBotApp, ApiFormattedText, ApiPhoto } from './messages';
import type { ApiTodoItem } from './messages';
import type { ApiStarGiftRegular, ApiStarGiftUnique } from './stars';
import type { ApiStarGiftRegular, ApiStarGiftUnique, ApiStarsAmount } from './stars';
interface ActionMediaType {
mediaType: 'action';
@ -282,6 +282,25 @@ export interface ApiMessageActionPaidMessagesPrice extends ActionMediaType {
isAllowedInChannel?: boolean;
}
export interface ApiMessageActionSuggestedPostApproval extends ActionMediaType {
type: 'suggestedPostApproval';
isRejected?: boolean;
isBalanceTooLow?: boolean;
rejectComment?: string;
scheduleDate?: number;
amount?: ApiStarsAmount;
}
export interface ApiMessageActionSuggestedPostSuccess extends ActionMediaType {
type: 'suggestedPostSuccess';
amount?: ApiStarsAmount;
}
export interface ApiMessageActionSuggestedPostRefund extends ActionMediaType {
type: 'suggestedPostRefund';
payerInitiated: boolean;
}
export interface ApiMessageActionTodoCompletions extends ActionMediaType {
type: 'todoCompletions';
completedIds: number[];
@ -310,5 +329,6 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
| ApiMessageActionChannelJoined | ApiMessageActionGiftCode | ApiMessageActionGiveawayLaunch
| ApiMessageActionGiveawayResults | ApiMessageActionPaymentRefunded | ApiMessageActionGiftStars
| ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionTodoCompletions
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
| ApiMessageActionTodoAppendTasks;

View File

@ -9,7 +9,7 @@ import type { ApiMessageAction } from './messageActions';
import type {
ApiLabeledPrice,
} from './payments';
import type { ApiStarGiftUnique } from './stars';
import type { ApiStarGiftUnique, ApiStarsAmount } from './stars';
import type {
ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData,
} from './stories';
@ -412,12 +412,26 @@ export interface ApiInputMessageReplyInfo {
quoteOffset?: number;
}
export interface ApiSuggestedPost {
isAccepted?: true;
isRejected?: true;
price?: ApiStarsAmount;
scheduleDate?: number;
}
export interface ApiInputStoryReplyInfo {
type: 'story';
peerId: string;
storyId: number;
}
export interface ApiInputSuggestedPostInfo {
price?: ApiStarsAmount;
scheduleDate?: number;
isAccepted?: true;
isRejected?: true;
}
export type ApiInputReplyInfo = ApiInputMessageReplyInfo | ApiInputStoryReplyInfo;
export interface ApiMessageForwardInfo {
@ -577,6 +591,7 @@ export interface ApiMessage {
isOutgoing: boolean;
senderId?: string;
replyInfo?: ApiReplyInfo;
suggestedPostInfo?: ApiInputSuggestedPostInfo;
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
forwardInfo?: ApiMessageForwardInfo;
isDeleting?: boolean;
@ -866,6 +881,13 @@ interface ApiKeyboardButtonCopy {
copyText: string;
}
export interface ApiKeyboardButtonSuggestedMessage {
type: 'suggestedMessage';
text: string;
buttonType: 'approve' | 'decline' | 'suggestChanges';
disabled?: boolean;
}
export type ApiKeyboardButton = (
ApiKeyboardButtonSimple
| ApiKeyboardButtonReceipt
@ -878,6 +900,7 @@ export type ApiKeyboardButton = (
| ApiKeyboardButtonSimpleWebView
| ApiKeyboardButtonUrlAuth
| ApiKeyboardButtonCopy
| ApiKeyboardButtonSuggestedMessage
);
export type ApiKeyboardButtons = ApiKeyboardButton[][];

View File

@ -252,6 +252,13 @@ export interface ApiAppConfig {
starsStargiftResaleAmountMin?: number;
starsStargiftResaleAmountMax?: number;
starsStargiftResaleCommissionPermille?: number;
starsSuggestedPostAmountMax?: number;
starsSuggestedPostAmountMin?: number;
starsSuggestedPostCommissionPermille?: number;
starsSuggestedPostAgeMin?: number;
starsSuggestedPostFutureMax?: number;
starsSuggestedPostFutureMin?: number;
tonSuggestedPostCommissionPermille?: number;
pollMaxAnswers?: number;
todoItemsMax?: number;
todoTitleLengthMax?: number;

View File

@ -19,6 +19,7 @@ export interface ApiStarGiftRegular {
isBirthday?: true;
upgradeStars?: number;
resellMinStars?: number;
releasedByPeerId?: string;
title?: string;
}
@ -36,6 +37,7 @@ export interface ApiStarGiftUnique {
slug: string;
giftAddress?: string;
resellPriceInStars?: number;
releasedByPeerId?: string;
}
export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique;

View File

@ -2025,6 +2025,70 @@
"MonoforumComposerPlaceholder" = "Choose a message to reply";
"ChannelSendMessage" = "Direct Messages";
"AutomaticTranslation" = "Automatic Translation";
"ComposerEmbeddedMessageSuggestedPostTitle" = "Suggest a Post Below";
"ComposerEmbeddedMessageSuggestedPostDescription" = "Tap to offer a price for publishing";
"TitleSuggestedPostAmountForAnyTime" = "{amount} for publishing anytime";
"ActionSuggestedPostOutgoing" = "**You** suggest to post this message.";
"ActionSuggestedPostIncoming" = "**{user}** suggest to post this message.";
"ActionSuggestedChangesPrice" = "price";
"ActionSuggestedChangesText" = "text";
"ActionSuggestedChangesTime" = "time";
"ActionSuggestedChangesMedia" = "media";
"ActionSuggestedChangesOutgoing" = "**You** suggested a new {changes} for this post.";
"ActionSuggestedChangesIncoming" = "**{user}** suggested a new {changes} for this post.";
"TitlePrice" = "Price";
"TitleTime" = "Time";
"TitleSuggestMessage" = "Suggest a Message";
"TitleSuggestedChanges" = "Suggest Changes";
"EnterPriceInStars" = "Enter Price In Stars";
"SuggestMessagePriceDescription" = "Choose how many {currency} to pay to publish this post.";
"SuggestMessageDateTimeHint" = "Select the date and time you want the post to be published.";
"SuggestMessageTimeDescription" = "{hint} The post will remain available for at least {duration} from this date.";
"TitleAnytime" = "Anytime";
"ButtonOfferAmount" = "Offer {amount}";
"ButtonOfferFree" = "Offer Free";
"ButtonUpdateTerms" = "Update Terms";
"InputPlaceholderPrice" = "Enter Price";
"SuggestedPostApprove" = "Approve";
"SuggestedPostDecline" = "Decline";
"SuggestedPostSuggestChanges" = "Suggest Changes";
"InputTitleSuggestMessageTime" = "Publishing Time";
"SuggestedPostApproved" = "Suggested post approved";
"SuggestedPostRejectedNotification" = "Suggested post rejected";
"SuggestedPostAgreementReached" = "🤝 Agreement reached!";
"SuggestedPostPublishSchedule" = "📅 This post will be automatically published on {peer} **{date}**.";
"SuggestedPostPublishScheduleYou" = "📅 Your post will be automatically published on {peer} **{date}**.";
"SuggestedPostPublished" = "📅 This post was automatically published on {peer} **{date}**.";
"SuggestedPostPublishedYou" = "📅 Your post was automatically published on {peer} **{date}**.";
"SuggestedPostCharged" = "💰 {user} has been charged {amount}.";
"SuggestedPostChargedYou" = "💰 You have been charged {amount}.";
"SuggestedPostReceiveAmount" = "⏳ {peer} will receive the {currency} once the post has been live for {duration}.";
"SuggestedPostReceiveAmountYou" = "⏳ {peer} will receive your {currency} once the post has been live for {duration}.";
"SuggestedPostRefund" = "🔄 If {peer} removes the post before it has been live for {duration}, payment will be refunded.";
"SuggestedPostRefundYou" = "🔄 If {peer} removes the post before it has been live for {duration}, your {currency} will be refunded.";
"SuggestedPostBalanceTooLow" = "⚠️ **Transaction failed** because {peer} didn't have enough {currency}.";
"SuggestedPostRefundedByUser" = "{channel} will not receive {amount} because {user} requested a refund.";
"SuggestedPostRefundedByChannel" = "{amount} was returned to {peer} because {channel} deleted the message.";
"CurrencyStars" = "Stars";
"DeclineReasonPlaceholder" = "Add a reason (optional)";
"DeclinePostDialogQuestion" = "Do you want to decline this post from **{sender}**?";
"SuggestedPostRejected" = "**{peer}** rejected this message.";
"SuggestedPostRejectedYou" = "You rejected this message.";
"SuggestedPostRejectedWithReason" = "**{peer}** rejected this message with the comment.";
"SuggestedPostRejectedWithReasonYou" = "You rejected this message with the comment.";
"SuggestedPostRejectedComment" = "\"{comment}\"";
"ActionSuggestedPostSuccess" = "{channel} has received {amount} for publishing post";
"ComposerPlaceholderCaption" = "Caption";
"DescriptionSuggestedPostMinimumOffer" = "Minimum offer is **{amount}**.";
"SuggestedPostConfirmTitle" = "Accept Terms";
"SuggestedPostConfirmMessage" = "Do you want to publish this post from **{peer}**?";
"SuggestedPostConfirmDetailsAdmin" = "You will receive **{amount}** ({commission}%) for publishing this post. It must remain visible for at least {duration} after publication.";
"SuggestedPostConfirmDetailsUser" = "You will pay **{amount}** ({commission}%) for publishing this post. It must remain visible for at least {duration} after publication.";
"SuggestedPostConfirmDetailsWithTimeAdmin" = "You will receive **{amount}** ({commission}%) for publishing this post **{time}**. It must remain visible for at least {duration} after publication.";
"SuggestedPostConfirmDetailsWithTimeUser" = "You will pay **{amount}** ({commission}%) for publishing this post **{time}**. It must remain visible for at least {duration} after publication.";
"ButtonPublish" = "Publish";
"ButtonPublishAtTime" = "Publish {time}";
"PublishNow" = "Publish Now";
"TitleNewToDoList" = "New Checklist";
"TitleEditToDoList" = "Edit Checklist";
"TitleAppendToDoList" = "Append Checklist";
@ -2061,4 +2125,5 @@
"HintTodoListTasksCount" = "You can add {count} more tasks";
"ToDoListErrorChooseTitle" = "Please enter a title.";
"ToDoListErrorChooseTasks" = "Please enter at least one task.";
"PremiumPreviewTodo" = "Checklists";
"GiftInfoCollectibleBy" = "Collectible #{number} by **{owner}**";
"PremiumPreviewTodo" = "Checklists";

View File

@ -37,6 +37,9 @@ export { default as ReportModal } from '../components/modals/reportModal/ReportM
export { default as PreparedMessageModal } from '../components/modals/preparedMessage/PreparedMessageModal';
export { default as SharePreparedMessageModal }
from '../components/modals/sharePreparedMessage/SharePreparedMessageModal';
export { default as SuggestMessageModal } from '../components/modals/suggestMessage/SuggestMessageModal';
export { default as SuggestedPostApprovalModal }
from '../components/modals/suggestedPostApproval/SuggestedPostApprovalModal';
export { default as CalendarModal } from '../components/common/CalendarModal';
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';
export { default as PinMessageModal } from '../components/common/PinMessageModal';

View File

@ -30,6 +30,15 @@
flex-direction: column;
justify-content: flex-end;
.description {
min-height: 2.75rem;
margin-bottom: 1rem;
margin-left: 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.Button {
text-transform: none;

View File

@ -33,8 +33,10 @@ export type OwnProps = {
withTimePicker?: boolean;
submitButtonLabel?: string;
secondButtonLabel?: string;
description?: string;
onClose: () => void;
onSubmit: (date: Date) => void;
onDateChange?: (date: Date) => void;
onSecondButtonClick?: NoneToVoidFunction;
};
@ -58,8 +60,10 @@ const CalendarModal: FC<OwnProps> = ({
withTimePicker,
submitButtonLabel,
secondButtonLabel,
description,
onClose,
onSubmit,
onDateChange,
onSecondButtonClick,
}) => {
const lang = useOldLang();
@ -169,6 +173,7 @@ const CalendarModal: FC<OwnProps> = ({
dateCopy.setMonth(currentMonth);
dateCopy.setFullYear(currentYear);
onDateChange?.(dateCopy);
return dateCopy;
});
}
@ -196,11 +201,12 @@ const CalendarModal: FC<OwnProps> = ({
const date = new Date(selectedDate.getTime());
date.setHours(hours);
setSelectedDate(date);
onDateChange?.(date);
const hoursStr = formatInputTime(hours);
setSelectedHours(hoursStr);
e.target.value = hoursStr;
}, [selectedDate]);
}, [selectedDate, onDateChange]);
const handleChangeMinutes = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/[^\d]+/g, '');
@ -215,11 +221,12 @@ const CalendarModal: FC<OwnProps> = ({
const date = new Date(selectedDate.getTime());
date.setMinutes(minutes);
setSelectedDate(date);
onDateChange?.(date);
const minutesStr = formatInputTime(minutes);
setSelectedMinutes(minutesStr);
e.target.value = minutesStr;
}, [selectedDate]);
}, [selectedDate, onDateChange]);
function renderTimePicker() {
return (
@ -331,6 +338,11 @@ const CalendarModal: FC<OwnProps> = ({
{withTimePicker && renderTimePicker()}
<div className="footer">
{description && (
<div className="description">
{description}
</div>
)}
<div className="footer">
<Button
onClick={handleSubmit}

View File

@ -235,6 +235,8 @@ type StateProps =
isReactionPickerOpen?: boolean;
shouldDisplayGiftsButton?: boolean;
isForwarding?: boolean;
isReplying?: boolean;
hasSuggestedPost?: boolean;
forwardedMessagesCount?: number;
pollModal: TabState['pollModal'];
todoListModal: TabState['todoListModal'];
@ -358,6 +360,8 @@ const Composer: FC<OwnProps & StateProps> = ({
isReactionPickerOpen,
shouldDisplayGiftsButton,
isForwarding,
isReplying,
hasSuggestedPost,
forwardedMessagesCount,
pollModal,
todoListModal,
@ -460,6 +464,7 @@ const Composer: FC<OwnProps & StateProps> = ({
hideEffectInComposer,
updateChatSilentPosting,
updateInsertingPeerIdMention,
updateDraftSuggestedPostInfo,
} = getActions();
const oldLang = useOldLang();
@ -816,7 +821,7 @@ const Composer: FC<OwnProps & StateProps> = ({
getHtml,
setHtml,
editedMessage: editingMessage,
isDisabled: isInStoryViewer || Boolean(requestedDraft) || isMonoforum,
isDisabled: isInStoryViewer || Boolean(requestedDraft) || (!hasSuggestedPost && isMonoforum),
});
const resetComposer = useLastCallback((shouldPreserveInput = false) => {
@ -872,6 +877,8 @@ const Composer: FC<OwnProps & StateProps> = ({
}, [disallowedGifts]);
const shouldShowGiftButton = Boolean(!isChatWithSelf && shouldDisplayGiftsButton && !areAllGiftsDisallowed);
const shouldShowSuggestedPostButton = isMonoforum && !editingMessage
&& !isForwarding && !isReplying && !draft?.suggestedPostInfo;
const showCustomEmojiPremiumNotification = useLastCallback(() => {
const notificationNumber = customEmojiNotificationNumber.current;
@ -1567,6 +1574,11 @@ const Composer: FC<OwnProps & StateProps> = ({
const handleGiftClick = useLastCallback(() => {
openGiftModal({ forUserId: chatId });
});
const handleSuggestPostClick = useLastCallback(() => {
updateDraftSuggestedPostInfo({
price: { amount: 0, nanos: 0 },
});
});
const handleToggleSilentPosting = useLastCallback(() => {
const newValue = !isSilentPosting;
@ -1638,6 +1650,10 @@ const Composer: FC<OwnProps & StateProps> = ({
});
}
if (isReplying && hasSuggestedPost) {
return lang('ComposerPlaceholderCaption');
}
if (chat?.adminRights?.anonymous) {
return lang('ComposerPlaceholderAnonymous');
}
@ -1658,7 +1674,8 @@ const Composer: FC<OwnProps & StateProps> = ({
return lang('ComposerPlaceholderNoText');
}, [
activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked,
isInStoryViewer, isSilentPosting, lang, replyToTopic, threadId, windowWidth, paidMessagesStars,
isInStoryViewer, isSilentPosting, lang, replyToTopic, isReplying, threadId, windowWidth, paidMessagesStars,
hasSuggestedPost,
]);
useEffect(() => {
@ -2166,6 +2183,17 @@ const Composer: FC<OwnProps & StateProps> = ({
<Icon name="gift" />
</Button>
)}
{shouldShowSuggestedPostButton && (
<Button
round
faded
className="composer-action-button"
color="translucent"
onClick={handleSuggestPostClick}
>
<Icon name="cash-circle" />
</Button>
)}
{Boolean(botKeyboardMessageId) && !activeVoiceRecording && !editingMessage && (
<ResponsiveHoverButton
className={buildClassName('composer-action-button', isBotKeyboardOpen && 'activated')}
@ -2479,6 +2507,8 @@ export default memo(withGlobal<OwnProps>(
const maxMessageLength = global.config?.maxMessageLength || DEFAULT_MAX_MESSAGE_LENGTH;
const isForwarding = chatId === tabState.forwardMessages.toChatId;
const isReplying = Boolean(draft?.replyInfo);
const hasSuggestedPost = Boolean(draft?.suggestedPostInfo);
const starsBalance = global.stars?.balance.amount || 0;
const isStarsBalanceModalOpen = Boolean(tabState.starsBalanceModal);
const isAccountFrozen = selectIsCurrentUserFrozen(global);
@ -2507,6 +2537,8 @@ export default memo(withGlobal<OwnProps>(
botKeyboardMessageId,
botKeyboardPlaceholder: keyboardMessage?.keyboardPlaceholder,
isForwarding,
isReplying,
hasSuggestedPost,
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
pollModal: tabState.pollModal,
todoListModal: tabState.todoListModal,

View File

@ -63,6 +63,11 @@
}
}
.suggested-price-star-icon {
margin-left: 0rem;
text-indent: 0rem;
}
&--background-icons {
margin: -0.1875rem -0.375rem -0.1875rem -0.1875rem;
}

View File

@ -4,6 +4,7 @@ import { useMemo, useRef } from '../../../lib/teact/teact';
import type {
ApiChat,
ApiInputSuggestedPostInfo,
ApiMessage, ApiPeer, ApiReplyInfo, MediaContainer,
} from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
@ -22,8 +23,10 @@ import {
import { getMediaContentTypeDescription } from '../../../global/helpers/messageSummary';
import { getPeerTitle } from '../../../global/helpers/peers';
import buildClassName from '../../../util/buildClassName';
import { formatScheduledDateTime } from '../../../util/dates/dateFormat';
import { isUserId } from '../../../util/entities/ids';
import freezeWhenClosed from '../../../util/hoc/freezeWhenClosed';
import { formatStarsAsIcon } from '../../../util/localization/format';
import { getPictogramDimensions } from '../helpers/mediaDimensions';
import renderText from '../helpers/renderText';
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
@ -47,6 +50,7 @@ import './EmbeddedMessage.scss';
type OwnProps = {
className?: string;
replyInfo?: ApiReplyInfo;
suggestedPostInfo?: ApiInputSuggestedPostInfo;
message?: ApiMessage;
sender?: ApiPeer;
senderChat?: ApiChat;
@ -72,6 +76,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
className,
message,
replyInfo,
suggestedPostInfo,
sender,
senderChat,
forwardSender,
@ -139,6 +144,35 @@ const EmbeddedMessage: FC<OwnProps> = ({
const { handleClick, handleMouseDown } = useFastClick(onClick);
function renderTextContent() {
const isFree = !(suggestedPostInfo?.price?.amount);
if (suggestedPostInfo) {
if (isFree && !suggestedPostInfo.scheduleDate) {
return lang('ComposerEmbeddedMessageSuggestedPostDescription');
}
const priceText = suggestedPostInfo.price
? formatStarsAsIcon(lang, suggestedPostInfo.price.amount, {
className: 'suggested-price-star-icon',
})
: '';
const scheduleText = suggestedPostInfo.scheduleDate
? formatScheduledDateTime(suggestedPostInfo.scheduleDate, lang, oldLang)
: '';
if (priceText && !scheduleText) {
return lang('TitleSuggestedPostAmountForAnyTime',
{ amount: priceText },
{
withNodes: true,
withMarkdown: true,
});
}
return (
<span>
{priceText}
{scheduleText ? `${scheduleText}` : ''}
</span>
);
}
if (replyInfo?.type === 'message' && replyInfo.quoteText) {
return renderTextWithEntities({
text: replyInfo.quoteText.text,
@ -186,6 +220,14 @@ const EmbeddedMessage: FC<OwnProps> = ({
return renderText(title);
}
if (suggestedPostInfo && replyInfo) {
return lang('TitleSuggestedChanges');
}
if (suggestedPostInfo) {
return lang('ComposerEmbeddedMessageSuggestedPostTitle');
}
if (!senderTitle && !forwardSendersTitle) {
return NBSP;
}
@ -252,6 +294,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
mediaThumbnail && 'with-thumb',
'no-selection',
composerForwardSenders && 'is-input-forward',
suggestedPostInfo && 'is-suggested-post',
)}
dir={lang.isRtl ? 'rtl' : undefined}
onClick={handleClick}

View File

@ -13,18 +13,21 @@ import { SCHEDULED_WHEN_ONLINE } from '../../config';
import {
getMessageHtmlId,
getMessageOriginalId,
getSuggestedChangesActionText,
getSuggestedChangesInfo,
isActionMessage,
isOwnMessage,
isServiceNotificationMessage,
} from '../../global/helpers';
import { getPeerTitle } from '../../global/helpers/peers';
import { selectSender } from '../../global/selectors';
import { selectChatMessage, selectSender } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { formatHumanDate } from '../../util/dates/dateFormat';
import { formatHumanDate, formatScheduledDateTime } from '../../util/dates/dateFormat';
import { compact } from '../../util/iteratees';
import { formatStarsAsText } from '../../util/localization/format';
import { isAlbum } from './helpers/groupMessages';
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
import { renderPeerLink } from './message/helpers/messageActions';
import useDerivedSignal from '../../hooks/useDerivedSignal';
import useLang from '../../hooks/useLang';
@ -33,12 +36,15 @@ import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useMessageObservers from './hooks/useMessageObservers';
import useScrollHooks from './hooks/useScrollHooks';
import MiniTable, { type TableEntry } from '../common/MiniTable';
import ActionMessage from './message/ActionMessage';
import Message from './message/Message';
import SenderGroupContainer from './message/SenderGroupContainer';
import SponsoredMessage from './message/SponsoredMessage';
import MessageListAccountInfo from './MessageListAccountInfo';
import actionMessageStyles from './message/ActionMessage.module.scss';
interface OwnProps {
canShowAds?: boolean;
chatId: string;
@ -176,6 +182,54 @@ const MessageListContent: FC<OwnProps> = ({
}
return undefined;
};
const renderSuggestedPostInfoAction = (message: ApiMessage) => {
if (message.suggestedPostInfo) {
const { price, scheduleDate } = message.suggestedPostInfo;
const sender = selectSender(getGlobal(), message);
const userTitle = sender ? getPeerTitle(lang, sender) : '';
const userLink = renderPeerLink(sender?.id, userTitle || lang('ActionFallbackUser'));
const originalMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
? selectChatMessage(getGlobal(), message.chatId, message.replyInfo.replyToMsgId)
: undefined;
const changesInfo = getSuggestedChangesInfo(message, originalMessage);
const titleText = changesInfo
? getSuggestedChangesActionText(lang, message, originalMessage, message.isOutgoing, userLink)
: message.isOutgoing
? lang('ActionSuggestedPostOutgoing', undefined, { withNodes: true, withMarkdown: true })
: lang('ActionSuggestedPostIncoming', { user: userLink }, { withNodes: true, withMarkdown: true });
const tableData: TableEntry[] = compact([
price && [lang('TitlePrice'), formatStarsAsText(lang, price.amount)],
Boolean(scheduleDate) && [lang('TitleTime'), formatScheduledDateTime(scheduleDate, lang, oldLang)],
]);
return (
<div
className={buildClassName('local-action-message')}
key={`suggested-post-action-${message.id}`}
>
<span className={actionMessageStyles.suggestedPostContainer}>
<div
className={actionMessageStyles.suggestedPostTitle}
>
{titleText}
</div>
{Boolean(tableData.length) && (
<MiniTable
className={actionMessageStyles.suggestedPostInfo}
data={tableData}
/>
)}
</span>
</div>
);
}
return undefined;
};
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
return acc + messageGroup.senderGroups.flat().length;
}, 0);
@ -268,6 +322,7 @@ const MessageListContent: FC<OwnProps> = ({
return compact([
message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
message.paidMessageStars && !withUsers && renderPaidMessageAction(message, album),
message.suggestedPostInfo && renderSuggestedPostInfoAction(message),
<Message
key={key}
message={message}

View File

@ -6,7 +6,7 @@ import {
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiChat, ApiInputMessageReplyInfo, ApiMessage, ApiPeer,
ApiChat, ApiInputMessageReplyInfo, ApiInputSuggestedPostInfo, ApiMessage, ApiPeer,
} from '../../../api/types';
import type { MessageListType, ThreadId } from '../../../types/index';
@ -48,6 +48,7 @@ import './ComposerEmbeddedMessage.scss';
type StateProps = {
replyInfo?: ApiInputMessageReplyInfo;
suggestedPostInfo?: ApiInputSuggestedPostInfo;
editingId?: number;
message?: ApiMessage;
sender?: ApiPeer;
@ -80,6 +81,7 @@ const CLOSE_DURATION = 350;
const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
replyInfo,
suggestedPostInfo,
editingId,
message,
sender,
@ -104,6 +106,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
}) => {
const {
resetDraftReplyInfo,
resetDraftSuggestedPostInfo,
updateDraftReplyInfo,
setEditingId,
focusMessage,
@ -113,6 +116,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
setForwardNoCaptions,
exitForwardMode,
setShouldPreventComposerAnimation,
openSuggestMessageModal,
} = getActions();
const ref = useRef<HTMLDivElement>();
const oldLang = useOldLang();
@ -121,6 +125,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
const isReplyToTopicStart = message?.content.action?.type === 'topicCreate';
const isShowingReply = replyInfo && !shouldForceShowEditing;
const isReplyWithQuote = Boolean(replyInfo?.quoteText);
const isShowingSuggestedPost = Boolean(suggestedPostInfo) && !shouldForceShowEditing;
const isForwarding = Boolean(forwardedMessagesCount);
@ -145,6 +150,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
if (isInChangingRecipientMode) return false;
if (message && (replyInfo || editingId)) return true;
if (forwardSenders && isForwarding) return true;
if (isShowingSuggestedPost) return true;
return false;
})();
@ -170,6 +176,9 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
setEditingId({ messageId: undefined });
} else if (forwardedMessagesCount) {
exitForwardMode();
} else if (isShowingSuggestedPost) {
resetDraftSuggestedPostInfo();
resetDraftReplyInfo();
} else if (replyInfo && !shouldForceShowEditing) {
resetDraftReplyInfo();
}
@ -187,6 +196,10 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
focusMessage({ chatId: message!.chatId, messageId: message!.id, noForumTopicPanel: true });
};
const handleMessageClick = useLastCallback((e: React.MouseEvent): void => {
if (suggestedPostInfo) {
openSuggestMessageModal({ chatId });
return;
}
handleContextMenu(e);
});
@ -236,6 +249,9 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
if (editingId) {
return 'edit';
}
if (isShowingSuggestedPost) {
return 'cash-circle';
}
if (isForwarding) {
return 'forward';
}
@ -244,7 +260,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
}
return undefined;
}, [editingId, isForwarding, isShowingReply]);
}, [editingId, isForwarding, isShowingReply, isShowingSuggestedPost]);
const customText = forwardedMessagesCount && forwardedMessagesCount > 1
? oldLang('ForwardedMessageCount', forwardedMessagesCount)
@ -284,6 +300,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
isOpen={isShown}
className="inside-input"
replyInfo={replyInfo}
suggestedPostInfo={suggestedPostInfo}
isInComposer
message={strippedMessage}
sender={!noAuthors ? sender : undefined}
@ -425,6 +442,7 @@ export default memo(withGlobal<OwnProps>(
const draft = selectDraft(global, chatId, threadId);
const replyInfo = draft?.replyInfo;
const suggestedPostInfo = draft?.suggestedPostInfo;
const replyToPeerId = replyInfo?.replyToPeerId;
const senderChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined;
@ -479,6 +497,7 @@ export default memo(withGlobal<OwnProps>(
return {
replyInfo,
suggestedPostInfo,
editingId,
message,
sender,

View File

@ -6,10 +6,11 @@ import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types';
import { DRAFT_DEBOUNCE } from '../../../../config';
import { DRAFT_DEBOUNCE, EDITABLE_INPUT_CSS_SELECTOR } from '../../../../config';
import {
requestMeasure,
requestMeasure, requestNextMutation,
} from '../../../../lib/fasterdom/fasterdom';
import focusEditableElement from '../../../../util/focusEditableElement';
import parseHtmlAsFormattedText from '../../../../util/parseHtmlAsFormattedText';
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
@ -84,6 +85,7 @@ const useDraft = ({
chatId: prevState.chatId ?? chatId,
threadId: prevState.threadId ?? threadId,
shouldKeepReply: true,
shouldKeepSuggestedPost: true,
});
}
});
@ -96,6 +98,7 @@ const useDraft = ({
return;
}
const isTouched = isTouchedRef.current;
const shouldUpdateSuggestedPost = draft?.suggestedPostInfo && !prevDraft?.suggestedPostInfo;
if (chatId === prevChatId && threadId === prevThreadId) {
if (isTouched && !draft) return; // Prevent reset from other client if we have local edits
@ -103,7 +106,7 @@ const useDraft = ({
setHtml('');
}
if (isTouched) return;
if (isTouched && !shouldUpdateSuggestedPost) return;
}
if (editedMessage || !draft) {
@ -111,6 +114,14 @@ const useDraft = ({
}
setHtml(getTextWithEntitiesAsHtml(draft.text));
if (shouldUpdateSuggestedPost) {
requestNextMutation(() => {
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
if (messageInput) {
focusEditableElement(messageInput, true);
}
});
}
const customEmojiIds = draft.text?.entities
?.map((entity) => entity.type === ApiMessageEntityTypes.CustomEmoji && entity.documentId)

View File

@ -91,6 +91,7 @@ export function groupMessages(
nextMessage.id === firstUnreadId
|| message.senderId !== nextMessage.senderId
|| (!withUsers && message.paidMessageStars)
|| (nextMessage.suggestedPostInfo)
|| message.isOutgoing !== nextMessage.isOutgoing
|| message.postAuthorTitle !== nextMessage.postAuthorTitle
|| (isActionMessage(message) && message.content.action?.type !== 'phoneCall')

View File

@ -201,3 +201,84 @@
.uniqueValue {
color: white;
}
.suggestedPostContainer {
padding: 0.5rem 0.75rem !important;
}
.suggestedPostTitle {
width: 100%;
max-width: 10rem;
font-weight: var(--font-weight-normal);
text-align: center;
}
.suggestedPostInfo {
width: fit-content;
margin: 0.75rem auto 0;
}
.suggestedPostBalanceTooLowBox,
.suggestedPostRejectedContentBox,
.suggestedPostContentBox,
.hoverable .textContent {
cursor: var(--custom-cursor, pointer);
transition: opacity 0.15s;
&:hover {
opacity: 0.8;
}
}
.suggestedPostContentBox {
max-width: 20rem !important;
}
.suggestedPostBalanceTooLowBox {
max-width: 20rem !important;
text-align: center;
}
.suggestedPostBalanceTooLowTitle {
margin-bottom: 0.25rem;
}
.suggestedPostRejectedContentBox {
max-width: 15rem !important;
}
.suggestedPostApprovalTitle {
display: flex;
justify-content: center;
width: 100%;
margin-bottom: 0.375rem;
font-weight: var(--font-weight-medium);
}
.suggestedPostApprovalSection {
width: 100%;
margin-bottom: 0.375rem;
text-align: left;
&:last-child {
margin-bottom: 0;
}
}
.suggestedPostRejectedComment {
width: 100%;
text-align: center;
}
.suggestedPostRejectedTitle {
display: inline-block;
text-align: center;
}
.rejectedIcon {
margin-right: 0.125rem;
font-size: 1rem;
vertical-align: middle;
}

View File

@ -1,4 +1,3 @@
import type React from '../../../lib/teact/teact';
import {
memo, useEffect, useMemo, useRef, useUnmountCleanup,
} from '../../../lib/teact/teact';
@ -53,6 +52,9 @@ import GiveawayPrize from './actions/GiveawayPrize';
import StarGift from './actions/StarGift';
import StarGiftUnique from './actions/StarGiftUnique';
import SuggestedPhoto from './actions/SuggestedPhoto';
import SuggestedPostApproval from './actions/SuggestedPostApproval';
import SuggestedPostBalanceTooLow from './actions/SuggestedPostBalanceTooLow';
import SuggestedPostRejected from './actions/SuggestedPostRejected';
import ContextMenuContainer from './ContextMenuContainer';
import Reactions from './reactions/Reactions';
import SimilarChannels from './SimilarChannels';
@ -98,7 +100,8 @@ const SINGLE_LINE_ACTIONS = new Set<ApiMessageAction['type']>([
'todoAppendTasks',
'unsupported',
]);
const HIDDEN_TEXT_ACTIONS = new Set<ApiMessageAction['type']>(['giftCode', 'prizeStars', 'suggestProfilePhoto']);
const HIDDEN_TEXT_ACTIONS = new Set<ApiMessageAction['type']>(['giftCode', 'prizeStars',
'suggestProfilePhoto', 'suggestedPostApproval']);
const ActionMessage = ({
message,
@ -139,6 +142,7 @@ const ActionMessage = ({
toggleChannelRecommendations,
animateUnreadReaction,
markMentionsRead,
focusMessage,
} = getActions();
const ref = useRef<HTMLDivElement>();
@ -150,6 +154,7 @@ const ActionMessage = ({
const isTextHidden = HIDDEN_TEXT_ACTIONS.has(action.type);
const isSingleLine = SINGLE_LINE_ACTIONS.has(action.type);
const isFluidMultiline = IS_FLUID_BACKGROUND_SUPPORTED && !isSingleLine;
const isClickableText = action.type === 'suggestedPostSuccess';
const messageReplyInfo = getMessageReplyInfo(message);
const { replyToMsgId, replyToPeerId } = messageReplyInfo || {};
@ -310,6 +315,30 @@ const ActionMessage = ({
toggleChannelRecommendations({ chatId });
break;
}
case 'suggestedPostApproval': {
const replyInfo = getMessageReplyInfo(message);
if (replyInfo?.type === 'message' && replyInfo.replyToMsgId) {
focusMessage({
chatId: message.chatId,
threadId,
messageId: replyInfo.replyToMsgId,
});
}
break;
}
case 'suggestedPostSuccess': {
const replyInfo = getMessageReplyInfo(message);
if (replyInfo?.type === 'message' && replyInfo.replyToMsgId) {
focusMessage({
chatId: message.chatId,
threadId,
messageId: replyInfo.replyToMsgId,
});
}
break;
}
}
});
@ -387,6 +416,30 @@ const ActionMessage = ({
/>
);
case 'suggestedPostApproval':
if (action.isBalanceTooLow) {
return (
<SuggestedPostBalanceTooLow
message={message}
action={action}
onClick={handleClick}
/>
);
}
return action.isRejected ? (
<SuggestedPostRejected
message={message}
action={action}
onClick={handleClick}
/>
) : (
<SuggestedPostApproval
message={message}
action={action}
onClick={handleClick}
/>
);
default:
return undefined;
}
@ -421,13 +474,13 @@ const ActionMessage = ({
{!isTextHidden && (
<>
{isFluidMultiline && (
<div className={styles.inlineWrapper}>
<div className={buildClassName(styles.inlineWrapper, isClickableText && styles.hoverable)}>
<span className={styles.fluidBackground} style={fluidBackgroundStyle}>
<ActionMessageText message={message} isInsideTopic={isInsideTopic} />
</span>
</div>
)}
<div className={styles.inlineWrapper}>
<div className={buildClassName(styles.inlineWrapper, isClickableText && styles.hoverable)}>
<span className={styles.textContent} onClick={handleClick}>
<ActionMessageText message={message} isInsideTopic={isInsideTopic} />
</span>

View File

@ -18,16 +18,18 @@ import { getMessageReplyInfo } from '../../../global/helpers/replies';
import {
selectChat,
selectChatMessage,
selectMonoforumChannel,
selectPeer,
selectSender,
selectThreadIdFromMessage,
selectTopic,
} from '../../../global/selectors';
import { ensureProtocol } from '../../../util/browser/url';
import { formatDateTimeToString, formatShortDuration } from '../../../util/dates/dateFormat';
import { formatDateTimeToString, formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat';
import { formatCurrency } from '../../../util/formatCurrency';
import { formatStarsAsText } from '../../../util/localization/format';
import { conjuctionWithNodes } from '../../../util/localization/utils';
import { getServerTime } from '../../../util/serverTime';
import renderText from '../../common/helpers/renderText';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import {
@ -40,6 +42,7 @@ import {
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import CustomEmoji from '../../common/CustomEmoji';
import TopicDefaultIcon from '../../common/TopicDefaultIcon';
@ -83,6 +86,7 @@ const ActionMessageText = ({
const action = message.content.action!;
const lang = useLang();
const oldLang = useOldLang();
function renderStrong(text: TeactNode) {
if (asPreview) return text;
@ -92,10 +96,10 @@ const ActionMessageText = ({
const renderActionText = useLastCallback(() => {
const global = getGlobal();
const isChannel = chat && isChatChannel(chat);
const isServiceNotificationsChat = chatId === SERVICE_NOTIFICATIONS_USER_ID;
const isSavedMessages = chatId === currentUserId;
const isChannel = chat && isChatChannel(chat);
const senderTitle = sender && getPeerTitle(lang, sender);
const chatTitle = chat && getPeerTitle(lang, chat);
@ -750,6 +754,97 @@ const ActionMessageText = ({
}, { withNodes: true, withMarkdown: true });
}
case 'suggestedPostSuccess': {
const { amount: stars } = action;
const channel = chat?.isMonoforum ? selectMonoforumChannel(global, chatId) : chat;
const channelTitle = channel && getPeerTitle(lang, channel);
const channelLink = renderPeerLink(channel?.id, channelTitle || channelFallbackText, asPreview);
return lang('ActionSuggestedPostSuccess', {
channel: channelLink,
amount: formatStarsAsText(lang, stars?.amount || 0),
}, { withNodes: true });
}
case 'suggestedPostRefund': {
const { payerInitiated } = action;
const replyMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
? selectChatMessage(global, chatId, message.replyInfo.replyToMsgId)
: undefined;
const postSender = replyMessage ? selectSender(global, replyMessage) : sender;
const postSenderTitle = postSender && getPeerTitle(lang, postSender);
const postSenderLink = renderPeerLink(postSender?.id, postSenderTitle || userFallbackText, asPreview);
const starsAmount = replyMessage?.suggestedPostInfo?.price?.amount || 0;
const channel = chat?.isMonoforum ? selectMonoforumChannel(global, chatId) : chat;
const channelTitle = channel && getPeerTitle(lang, channel);
const channelLink = renderPeerLink(channel?.id, channelTitle || channelFallbackText, asPreview);
if (payerInitiated) {
return lang('SuggestedPostRefundedByUser', {
amount: formatStarsAsText(lang, starsAmount),
user: postSenderLink,
channel: channelLink,
}, { withNodes: true, withMarkdown: true });
}
return lang('SuggestedPostRefundedByChannel', {
amount: formatStarsAsText(lang, starsAmount),
peer: postSenderLink,
channel: channelLink,
}, { withNodes: true, withMarkdown: true });
}
case 'suggestedPostApproval': {
const { isRejected, isBalanceTooLow, rejectComment } = action;
if (isRejected) {
const senderTitle = sender && getPeerTitle(lang, sender);
const senderLink = renderPeerLink(sender?.id, senderTitle || userFallbackText, asPreview);
return translateWithYou(
lang,
rejectComment ? 'SuggestedPostRejectedWithReason' : 'SuggestedPostRejected',
isOutgoing,
{ peer: senderLink },
{ withMarkdown: true },
);
}
if (isBalanceTooLow) {
const replyMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
? selectChatMessage(global, chatId, message.replyInfo.replyToMsgId)
: undefined;
const replyMessageSender = replyMessage ? selectSender(global, replyMessage) : sender;
const replyPeerTitle = replyMessageSender && getPeerTitle(lang, replyMessageSender);
const userLink = renderPeerLink(replyMessageSender?.id, replyPeerTitle || userFallbackText, asPreview);
return lang('SuggestedPostBalanceTooLow', {
peer: userLink,
currency: lang('CurrencyStars'),
}, { withNodes: true, withMarkdown: true });
}
const channel = chat?.isMonoforum ? selectMonoforumChannel(global, chatId) : chat;
const channelTitle = channel && getPeerTitle(lang, channel);
const channelLink = renderPeerLink(channel?.id, channelTitle || channelFallbackText, asPreview);
const { scheduleDate } = action;
if (scheduleDate) {
const publishDate = formatScheduledDateTime(scheduleDate, lang, oldLang);
const isPostPublished = scheduleDate <= getServerTime();
return translateWithYou(
lang,
isPostPublished ? 'SuggestedPostPublished' : 'SuggestedPostPublishSchedule',
isOutgoing,
{ peer: channelLink, date: publishDate },
{ withMarkdown: true },
);
}
return lang(UNSUPPORTED_LANG_KEY);
}
case 'todoCompletions': {
const { completedIds, incompletedIds } = action;

View File

@ -59,6 +59,10 @@
}
}
.left-icon {
margin-right: 0.25rem;
}
.inline-button-text {
overflow: hidden;
text-overflow: ellipsis;

View File

@ -43,6 +43,17 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
return <Icon className="corner-icon" name="webapp" />;
case 'copy':
return <Icon className="corner-icon" name="copy" />;
case 'suggestedMessage':
if (button.buttonType === 'suggestChanges') {
return <Icon className="left-icon" name="edit" />;
}
if (button.buttonType === 'approve') {
return <Icon className="left-icon" name="check" />;
}
if (button.buttonType === 'decline') {
return <Icon className="left-icon" name="close" />;
}
break;
}
return undefined;
};
@ -63,14 +74,14 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
<Button
size="tiny"
ripple
disabled={button.type === 'unsupported'}
disabled={button.type === 'unsupported' || (button.type === 'suggestedMessage' && button.disabled)}
onClick={() => onClick({ chatId: message.chatId, messageId: message.id, button })}
>
{renderIcon(button)}
<span className="inline-button-text">
{buttonTexts[i][j]}
</span>
{renderIcon(button)}
</Button>
))}
</div>

View File

@ -28,6 +28,7 @@ import type {
ApiTypeStory,
ApiUser,
} from '../../../api/types';
import type { ActionPayloads } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type {
ActiveEmojiInteraction,
@ -44,7 +45,7 @@ import type { OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../../api/types';
import { AudioOrigin } from '../../../types';
import { EMOJI_STATUS_LOOP_LIMIT, MESSAGE_APPEARANCE_DELAY } from '../../../config';
import { EMOJI_STATUS_LOOP_LIMIT, MESSAGE_APPEARANCE_DELAY, STARS_SUGGESTED_POST_FUTURE_MIN } from '../../../config';
import {
areReactionsEmpty,
getIsDownloading,
@ -120,6 +121,7 @@ import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util
import buildClassName from '../../../util/buildClassName';
import { isUserId } from '../../../util/entities/ids';
import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTime } from '../../../util/serverTime';
import stopEvent from '../../../util/stopEvent';
import { isElementInViewport } from '../../../util/visibility/isElementInViewport';
import { calculateDimensionsForMessageMedia, getStickerDimensions, REM } from '../../common/helpers/mediaDimensions';
@ -137,6 +139,7 @@ import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useEnsureStory from '../../../hooks/useEnsureStory';
import useFlag from '../../../hooks/useFlag';
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
@ -164,6 +167,8 @@ import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
import TopicChip from '../../common/TopicChip';
import { animateSnap } from '../../main/visualEffects/SnapEffectContainer';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import InputText from '../../ui/InputText';
import Album from './Album';
import AnimatedCustomEmoji from './AnimatedCustomEmoji';
import AnimatedEmoji from './AnimatedEmoji';
@ -310,6 +315,7 @@ type StateProps = {
paidMessageStars?: number;
isChatWithUser?: boolean;
isAccountFrozen?: boolean;
minFutureTime?: number;
};
type MetaPosition =
@ -328,6 +334,7 @@ const NBSP = '\u00A0';
const NO_MEDIA_CORNERS_THRESHOLD = 18;
const QUICK_REACTION_SIZE = 1.75 * REM;
const EXTRA_SPACE_FOR_REACTIONS = 2.25 * REM;
const MAX_REASON_LENGTH = 200;
const Message: FC<OwnProps & StateProps> = ({
message,
@ -434,10 +441,14 @@ const Message: FC<OwnProps & StateProps> = ({
paidMessageStars,
isChatWithUser,
isAccountFrozen,
minFutureTime,
}) => {
const {
toggleMessageSelection,
clickBotInlineButton,
clickSuggestedMessageButton,
rejectSuggestedPost,
openSuggestedPostApprovalModal,
disableContextMenuHint,
animateUnreadReaction,
focusLastMessage,
@ -448,12 +459,15 @@ const Message: FC<OwnProps & StateProps> = ({
const bottomMarkerRef = useRef<HTMLDivElement>();
const quickReactionRef = useRef<HTMLDivElement>();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const [isTranscriptionHidden, setTranscriptionHidden] = useState(false);
const [isPlayingSnapAnimation, setIsPlayingSnapAnimation] = useState(false);
const [isPlayingDeleteAnimation, setIsPlayingDeleteAnimation] = useState(false);
const [shouldPlayEffect, requestEffect, hideEffect] = useFlag();
const [isDeclineDialogOpen, openDeclineDialog, closeDeclineDialog] = useFlag();
const [declineReason, setDeclineReason] = useState('');
const { isMobile, isTouchScreen } = useAppLayout();
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
@ -646,7 +660,7 @@ const Message: FC<OwnProps & StateProps> = ({
handleTopicChipClick,
handleStoryClick,
} = useInnerHandlers({
lang,
lang: oldLang,
selectMessage,
message,
chatId,
@ -857,7 +871,7 @@ const Message: FC<OwnProps & StateProps> = ({
scrollTargetPosition,
});
const viaBusinessBotTitle = viaBusinessBot ? getPeerFullTitle(lang, viaBusinessBot) : undefined;
const viaBusinessBotTitle = viaBusinessBot ? getPeerFullTitle(oldLang, viaBusinessBot) : undefined;
const canShowPostAuthor = !message.senderId;
const signature = viaBusinessBotTitle || (canShowPostAuthor && message.postAuthorTitle)
@ -1269,7 +1283,7 @@ const Message: FC<OwnProps & StateProps> = ({
)}
dir="auto"
>
{(isTranscriptionError ? lang('NoWordsRecognized') : (
{(isTranscriptionError ? oldLang('NoWordsRecognized') : (
isTranscribing && transcribedText ? <DotAnimation content={transcribedText} /> : transcribedText
))}
</p>
@ -1482,24 +1496,59 @@ const Message: FC<OwnProps & StateProps> = ({
)}
{asForwarded && (
<span className="forward-title">
{lang('ForwardedFrom')}
{oldLang('ForwardedFrom')}
</span>
)}
</span>
);
}
const handleSuggestedMessageButton = useLastCallback((payload: ActionPayloads['clickBotInlineButton']) => {
if (payload.button.type !== 'suggestedMessage') return;
if (payload.button.buttonType === 'approve') {
openSuggestedPostApprovalModal({
chatId,
messageId: message.id,
});
return;
}
if (payload.button.buttonType === 'decline') {
openDeclineDialog();
return;
}
clickSuggestedMessageButton({
...payload,
button: payload.button,
});
});
const handleDeclineReasonChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setDeclineReason(e.target.value);
});
const handleDeclineConfirm = useLastCallback(() => {
rejectSuggestedPost({
chatId,
messageId: message.id,
rejectComment: declineReason.trim() || undefined,
});
closeDeclineDialog();
setDeclineReason('');
});
function renderSenderName(
shouldSkipRenderForwardTitle: boolean = false, shouldSkipRenderAdminTitle: boolean = false,
) {
let senderTitle;
let senderColor;
if (senderPeer && !(isCustomShape && viaBotId)) {
senderTitle = getPeerFullTitle(lang, senderPeer);
senderTitle = getPeerFullTitle(oldLang, senderPeer);
} else if (forwardInfo?.hiddenUserName) {
senderTitle = forwardInfo.hiddenUserName;
} else if (storyData && originSender) {
senderTitle = getPeerFullTitle(lang, originSender);
senderTitle = getPeerFullTitle(oldLang, originSender);
}
const senderEmojiStatus = senderPeer && 'emojiStatus' in senderPeer && senderPeer.emojiStatus;
const senderIsPremium = senderPeer && 'isPremium' in senderPeer && senderPeer.isPremium;
@ -1551,7 +1600,7 @@ const Message: FC<OwnProps & StateProps> = ({
) : undefined}
{botSender?.hasUsername && (
<span className="interactive">
<span className="via">{lang('ViaBot')}</span>
<span className="via">{oldLang('ViaBot')}</span>
<span
className="sender-title"
onClick={handleViaBotClick}
@ -1562,12 +1611,12 @@ const Message: FC<OwnProps & StateProps> = ({
)}
<div className="title-spacer" />
{!shouldSkipRenderAdminTitle && !hasBotSenderUsername ? (forwardInfo?.isLinkedChannelPost ? (
<span className="admin-title" dir="auto">{lang('DiscussChannel')}</span>
<span className="admin-title" dir="auto">{oldLang('DiscussChannel')}</span>
) : message.postAuthorTitle && isGroup && !asForwarded ? (
<span className="admin-title" dir="auto">{message.postAuthorTitle}</span>
) : senderAdminMember && !asForwarded && !viaBotId ? (
<span className="admin-title" dir="auto">
{senderAdminMember.customTitle || lang(
{senderAdminMember.customTitle || oldLang(
senderAdminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin',
)}
</span>
@ -1583,6 +1632,14 @@ const Message: FC<OwnProps & StateProps> = ({
}
const forwardAuthor = isGroup && asForwarded ? message.postAuthorTitle : undefined;
const shouldRenderSuggestedPostButtons = message.suggestedPostInfo
&& !message.isOutgoing && !message.suggestedPostInfo.isAccepted && !message.suggestedPostInfo.isRejected;
const isSuggestedPostExpired = useMemo(() => {
if (!message.suggestedPostInfo?.scheduleDate || !minFutureTime) return false;
const now = getServerTime();
return message.suggestedPostInfo.scheduleDate <= now + minFutureTime;
}, [message.suggestedPostInfo, minFutureTime]);
return (
<div
@ -1666,7 +1723,7 @@ const Message: FC<OwnProps & StateProps> = ({
color="translucent-white"
round
size="tiny"
ariaLabel={lang('lng_context_forward_msg')}
ariaLabel={oldLang('lng_context_forward_msg')}
onClick={isLastInDocumentGroup ? handleGroupForward : handleForward}
>
<Icon name="share-filled" />
@ -1699,6 +1756,36 @@ const Message: FC<OwnProps & StateProps> = ({
{message.inlineButtons && (
<InlineButtons message={message} onClick={clickBotInlineButton} />
)}
{shouldRenderSuggestedPostButtons && (
<InlineButtons
message={{
...message,
inlineButtons: [
[
{
type: 'suggestedMessage',
buttonType: 'decline',
text: lang('SuggestedPostDecline'),
},
{
type: 'suggestedMessage',
buttonType: 'approve',
text: lang('SuggestedPostApprove'),
disabled: isSuggestedPostExpired,
},
],
[
{
type: 'suggestedMessage',
buttonType: 'suggestChanges',
text: lang('SuggestedPostSuggestChanges'),
},
],
],
}}
onClick={handleSuggestedMessageButton}
/>
)}
{reactionsPosition === 'outside' && !isStoryMention && (
<Reactions
message={reactionMessage!}
@ -1728,6 +1815,28 @@ const Message: FC<OwnProps & StateProps> = ({
detectedLanguage={detectedLanguage}
/>
)}
{isDeclineDialogOpen && (
<ConfirmDialog
isOpen={isDeclineDialogOpen}
onClose={closeDeclineDialog}
title={lang('SuggestedPostDecline')}
confirmLabel={lang('SuggestedPostDecline')}
confirmHandler={handleDeclineConfirm}
confirmIsDestructive
>
<div className="decline-dialog-question">
{renderText(lang('DeclinePostDialogQuestion', {
sender: sender ? getPeerFullTitle(oldLang, sender) : '',
}, { withNodes: true, withMarkdown: true }))}
</div>
<InputText
placeholder={lang('DeclineReasonPlaceholder')}
value={declineReason}
onChange={handleDeclineReasonChange}
maxLength={MAX_REASON_LENGTH}
/>
</ConfirmDialog>
)}
</div>
);
};
@ -1871,6 +1980,8 @@ export default memo(withGlobal<OwnProps>(
const lastPlaybackTimestamp = selectMessageLastPlaybackTimestamp(global, chatId, message.id);
const isAccountFrozen = selectIsCurrentUserFrozen(global);
const minFutureTime = global.appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN;
return {
theme: selectTheme(global),
forceSenderName,
@ -1957,6 +2068,7 @@ export default memo(withGlobal<OwnProps>(
tags: global.savedReactionTags?.byKey,
canTranscribeVoice,
viaBusinessBot,
minFutureTime,
effect,
poll,
maxTimestamp,

View File

@ -20,6 +20,10 @@
}
}
.decline-dialog-question {
margin-bottom: 1rem;
}
.message-content {
position: relative;
max-width: var(--max-width);

View File

@ -0,0 +1,148 @@
import { memo } from '../../../../lib/teact/teact';
import { withGlobal } from '../../../../global';
import type { ApiMessage, ApiPeer } from '../../../../api/types';
import type { ApiMessageActionSuggestedPostApproval } from '../../../../api/types/messageActions';
import { STARS_SUGGESTED_POST_AGE_MIN } from '../../../../config';
import { getPeerFullTitle } from '../../../../global/helpers/peers';
import { getMessageReplyInfo } from '../../../../global/helpers/replies';
import { selectIsMonoforumAdmin, selectMonoforumChannel,
selectReplyMessage,
selectSender } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatScheduledDateTime, formatShortDuration } from '../../../../util/dates/dateFormat';
import { formatStarsAsText } from '../../../../util/localization/format';
import { getServerTime } from '../../../../util/serverTime';
import renderText from '../../../common/helpers/renderText';
import { renderPeerLink, translateWithYou } from '../helpers/messageActions';
import useLang from '../../../../hooks/useLang';
import useOldLang from '../../../../hooks/useOldLang';
import styles from '../ActionMessage.module.scss';
type OwnProps = {
message: ApiMessage;
action: ApiMessageActionSuggestedPostApproval;
onClick?: NoneToVoidFunction;
};
type StateProps = {
sender?: ApiPeer;
chat?: ApiPeer;
originalSender?: ApiPeer;
ageMinSeconds: number;
isAdmin: boolean;
};
const SuggestedPostApproval = ({
message,
action,
sender,
chat,
originalSender,
ageMinSeconds,
isAdmin,
onClick,
}: OwnProps & StateProps) => {
const lang = useLang();
const oldLang = useOldLang();
const { scheduleDate, amount } = action;
const chatTitle = chat && getPeerFullTitle(lang, chat);
const renderChatLink = () => renderPeerLink(chat?.id, chatTitle || lang('ActionFallbackChat'));
const originalSenderTitle = originalSender && getPeerFullTitle(lang, originalSender);
const originalSenderLink = renderPeerLink(originalSender?.id, originalSenderTitle || lang('ActionFallbackUser'));
const publishDate = scheduleDate
? formatScheduledDateTime(scheduleDate, lang, oldLang)
: lang('TitleAnytime');
const isPostPublished = scheduleDate ? scheduleDate <= getServerTime() : false;
const starsText = amount?.amount ? formatStarsAsText(lang, amount.amount) : undefined;
const duration = formatShortDuration(lang, ageMinSeconds, true);
return (
<div
className={buildClassName(styles.contentBox, styles.suggestedPostContentBox)}
onClick={onClick}
>
<div className={styles.suggestedPostApprovalTitle}>
{renderText(lang('SuggestedPostAgreementReached'))}
</div>
<div className={styles.suggestedPostApprovalSection}>
{translateWithYou(
lang,
isPostPublished ? 'SuggestedPostPublished' : 'SuggestedPostPublishSchedule',
!isAdmin,
{ peer: renderChatLink(), date: publishDate },
{ withMarkdown: true },
)}
</div>
{starsText && (
<div className={styles.suggestedPostApprovalSection}>
{translateWithYou(lang,
'SuggestedPostCharged',
!isAdmin,
{
user: originalSenderLink,
amount: starsText,
},
{ withMarkdown: true },
)}
</div>
)}
{isPostPublished && starsText && (
<>
<div className={styles.suggestedPostApprovalSection}>
{translateWithYou(lang, 'SuggestedPostReceiveAmount', !isAdmin, {
peer: renderChatLink(), duration, currency: lang('CurrencyStars'),
}, { withMarkdown: true })}
</div>
<div className={styles.suggestedPostApprovalSection}>
{translateWithYou(lang, 'SuggestedPostRefund', !isAdmin, {
peer: renderChatLink(), duration, currency: lang('CurrencyStars'),
}, { withMarkdown: true })}
</div>
</>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
const chat = selectMonoforumChannel(global, message.chatId);
const replyInfo = getMessageReplyInfo(message);
let originalSender: ApiPeer | undefined;
if (replyInfo?.type === 'message' && replyInfo.replyToMsgId) {
const replyMessage = selectReplyMessage(global, message);
if (replyMessage) {
originalSender = selectSender(global, replyMessage);
}
}
const { appConfig } = global;
const ageMinSeconds = appConfig?.starsSuggestedPostAgeMin || STARS_SUGGESTED_POST_AGE_MIN;
const isAdmin = chat ? Boolean(selectIsMonoforumAdmin(global, message.chatId)) : false;
return {
sender,
chat,
originalSender,
ageMinSeconds,
isAdmin,
};
},
)(SuggestedPostApproval));

View File

@ -0,0 +1,86 @@
import { memo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiMessage, ApiPeer } from '../../../../api/types';
import type { ApiMessageActionSuggestedPostApproval } from '../../../../api/types/messageActions';
import { getPeerFullTitle } from '../../../../global/helpers/peers';
import { selectChatMessage, selectSender } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { renderPeerLink } from '../helpers/messageActions';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import Sparkles from '../../../common/Sparkles';
import styles from '../ActionMessage.module.scss';
type OwnProps = {
message: ApiMessage;
action: ApiMessageActionSuggestedPostApproval;
onClick?: NoneToVoidFunction;
};
type StateProps = {
sender?: ApiPeer;
replyMessageSender?: ApiPeer;
};
const SuggestedPostBalanceTooLow = ({
onClick,
message,
sender,
replyMessageSender,
}: OwnProps & StateProps) => {
const { openStarsBalanceModal } = getActions();
const lang = useLang();
const handleGetMoreStars = useLastCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
openStarsBalanceModal({});
});
const targetPeer = replyMessageSender || sender;
const peerTitle = targetPeer && getPeerFullTitle(lang, targetPeer);
const peerLink = renderPeerLink(targetPeer?.id, peerTitle || lang('ActionFallbackUser'));
return (
<div
className={buildClassName(styles.contentBox, styles.suggestedPostBalanceTooLowBox)}
onClick={onClick}
>
<div className={styles.suggestedPostBalanceTooLowTitle}>
{lang('SuggestedPostBalanceTooLow', {
peer: peerLink,
currency: lang('CurrencyStars'),
}, { withNodes: true, withMarkdown: true })}
</div>
{!message.isOutgoing && (
<div className={styles.actionButton} onClick={handleGetMoreStars}>
<Sparkles preset="button" />
{lang('ButtonBuyStars')}
</div>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
const replyMessage = message.replyInfo?.type === 'message' && message.replyInfo.replyToMsgId
? selectChatMessage(global, message.chatId, message.replyInfo.replyToMsgId)
: undefined;
const replyMessageSender = replyMessage ? selectSender(global, replyMessage) : undefined;
return {
sender,
replyMessageSender,
};
},
)(SuggestedPostBalanceTooLow));

View File

@ -0,0 +1,74 @@
import { memo } from '../../../../lib/teact/teact';
import { withGlobal } from '../../../../global';
import type { ApiMessage, ApiPeer } from '../../../../api/types';
import type { ApiMessageActionSuggestedPostApproval } from '../../../../api/types/messageActions';
import { getPeerTitle } from '../../../../global/helpers/peers';
import { selectSender } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { renderPeerLink, translateWithYou } from '../helpers/messageActions';
import useLang from '../../../../hooks/useLang';
import Icon from '../../../common/icons/Icon';
import styles from '../ActionMessage.module.scss';
type OwnProps = {
message: ApiMessage;
action: ApiMessageActionSuggestedPostApproval;
onClick?: NoneToVoidFunction;
};
type StateProps = {
sender?: ApiPeer;
};
const SuggestedPostRejected = ({
message,
action,
sender,
onClick,
}: OwnProps & StateProps) => {
const lang = useLang();
const { isOutgoing } = message;
const { rejectComment } = action;
const senderTitle = sender && getPeerTitle(lang, sender);
const senderLink = renderPeerLink(sender?.id, senderTitle || lang('ActionFallbackUser'));
return (
<div
className={buildClassName(styles.contentBox, styles.suggestedPostRejectedContentBox)}
onClick={onClick}
>
<div className={styles.suggestedPostRejectedTitle}>
<Icon className={styles.rejectedIcon} name="close" />
{translateWithYou(
lang,
rejectComment ? 'SuggestedPostRejectedWithReason' : 'SuggestedPostRejected',
isOutgoing,
{ peer: senderLink },
{ withMarkdown: true },
)}
</div>
{rejectComment && (
<div className={styles.suggestedPostRejectedComment}>
{lang('SuggestedPostRejectedComment', { comment: rejectComment })}
</div>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
return {
sender,
};
},
)(SuggestedPostRejected));

View File

@ -42,7 +42,9 @@ import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async';
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
import SuggestedPostApprovalModal from './suggestedPostApproval/SuggestedPostApprovalModal.async';
import SuggestedStatusModal from './suggestedStatus/SuggestedStatusModal.async';
import SuggestMessageModal from './suggestMessage/SuggestMessageModal.async';
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
import WebAppModal from './webApp/WebAppModal.async';
@ -63,6 +65,8 @@ type ModalKey = keyof Pick<TabState,
'starsPayment' |
'starsTransactionModal' |
'paidReactionModal' |
'suggestMessageModal' |
'suggestedPostApprovalModal' |
'webApps' |
'chatInviteModal' |
'starsSubscriptionModal' |
@ -118,6 +122,8 @@ const MODALS: ModalRegistry = {
starsTransactionModal: StarsTransactionInfoModal,
chatInviteModal: ChatInviteModal,
paidReactionModal: PaidReactionModal,
suggestMessageModal: SuggestMessageModal,
suggestedPostApprovalModal: SuggestedPostApprovalModal,
starsSubscriptionModal: StarsSubscriptionModal,
starsGiftModal: StarsGiftModal,
giftModal: PremiumGiftModal,

View File

@ -11,6 +11,23 @@
padding-bottom: 1rem;
}
.subtitleBadge {
margin-top: 0.25rem;
padding: 0.75rem;
padding-block: 0.125rem;
border-radius: 1rem;
background-color: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(50px);
transition: color 150ms ease-in, background-color 0.15s !important;
&:hover {
cursor: pointer;
background-color: rgba(0, 0, 0, 0.15);
}
}
.radialPattern {
position: absolute;
z-index: -1;

View File

@ -1,9 +1,11 @@
import type { TeactNode } from '../../../lib/teact/teact';
import { memo, useMemo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type {
ApiPeer,
ApiStarGiftAttributeBackdrop, ApiStarGiftAttributeModel, ApiStarGiftAttributePattern,
ApiStarsAmount,
} from '../../../api/types';
ApiStarsAmount } from '../../../api/types';
import {
formatStarsTransactionAmount,
@ -26,7 +28,8 @@ type OwnProps = {
backdropAttribute: ApiStarGiftAttributeBackdrop;
patternAttribute: ApiStarGiftAttributePattern;
title?: string;
subtitle?: string;
subtitle?: TeactNode;
subtitlePeer?: ApiPeer;
className?: string;
resellPrice?: ApiStarsAmount;
};
@ -39,9 +42,14 @@ const UniqueGiftHeader = ({
patternAttribute,
title,
subtitle,
subtitlePeer,
className,
resellPrice,
}: OwnProps) => {
const {
openChat,
} = getActions();
const lang = useLang();
const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]);
const subtitleColor = backdropAttribute?.textColor;
@ -77,10 +85,18 @@ const UniqueGiftHeader = ({
/>
</Transition>
{title && <h1 className={styles.title}>{title}</h1>}
{subtitle && (
<p className={styles.subtitle} style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}>
{Boolean(subtitle) && (
<div
className={buildClassName(styles.subtitle, subtitlePeer && styles.subtitleBadge)}
style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}
onClick={() => {
if (subtitlePeer) {
openChat({ id: subtitlePeer.id });
}
}}
>
{subtitle}
</p>
</div>
)}
{resellPrice && (
<p className={styles.amount}>

View File

@ -10,7 +10,8 @@ import type {
import type { TabState } from '../../../../global/types';
import { getHasAdminRight } from '../../../../global/helpers';
import { getPeerTitle, isApiPeerChat } from '../../../../global/helpers/peers';
import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../../global/helpers/peers';
import { getMainUsername } from '../../../../global/helpers/users';
import { selectPeer, selectUser } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { copyTextToClipboard } from '../../../../util/clipboard';
@ -51,6 +52,7 @@ export type OwnProps = {
type StateProps = {
fromPeer?: ApiPeer;
targetPeer?: ApiPeer;
releasedByPeer?: ApiPeer;
currentUserId?: string;
starGiftMaxConvertPeriod?: number;
hasAdminRights?: boolean;
@ -67,6 +69,7 @@ const GiftInfoModal = ({
modal,
fromPeer,
targetPeer,
releasedByPeer,
currentUserId,
starGiftMaxConvertPeriod,
hasAdminRights,
@ -117,6 +120,26 @@ const GiftInfoModal = ({
const isGiftUnique = gift && gift.type === 'starGiftUnique';
const uniqueGift = isGiftUnique ? gift : undefined;
const giftSubtitle = useMemo(() => {
if (!gift || gift.type !== 'starGiftUnique') return undefined;
if (releasedByPeer) {
const releasedByUsername = `@${getMainUsername(releasedByPeer)}`;
const ownerTitle = releasedByUsername || getPeerTitle(lang, releasedByPeer);
const fallbackText = isApiPeerUser(releasedByPeer)
? lang('ActionFallbackUser')
: lang('ActionFallbackChannel');
return lang('GiftInfoCollectibleBy', {
number: gift.number, owner: ownerTitle || fallbackText }, {
withNodes: true,
withMarkdown: true,
});
}
return lang('GiftInfoCollectible', { number: gift.number });
}, [gift, releasedByPeer, lang]);
const canFocusUpgrade = Boolean(savedGift?.upgradeMsgId);
const canManage = !canFocusUpgrade && savedGift?.inputGift && (
isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId
@ -406,7 +429,8 @@ const GiftInfoModal = ({
patternAttribute={giftAttributes!.pattern!}
modelAttribute={giftAttributes!.model!}
title={gift.title}
subtitle={lang('GiftInfoCollectible', { number: gift.number })}
subtitle={giftSubtitle}
subtitlePeer={releasedByPeer}
/>
</div>
);
@ -512,6 +536,7 @@ const GiftInfoModal = ({
if (isGiftUnique) {
const { ownerName, ownerAddress, ownerId } = gift;
const ownerPeer = ownerId ? selectPeer(getGlobal(), ownerId) : undefined;
const {
model, backdrop, pattern, originalDetails,
} = giftAttributes || {};
@ -533,7 +558,7 @@ const GiftInfoModal = ({
<Icon className={styles.copyIcon} name="copy" />
</span>,
]);
} else {
} else if (ownerPeer || ownerName) {
tableData.push([
lang('GiftInfoOwner'),
ownerId ? { chatId: ownerId, withEmojiStatus: true } : ownerName || '',
@ -702,7 +727,8 @@ const GiftInfoModal = ({
gift, giftAttributes, renderFooterButton, isTargetChat,
SettingsMenuButton, isGiftUnique, renderingModal,
collectibleEmojiStatuses, currentUserEmojiStatus, saleDateInfo,
canBuyGift, giftOwnerTitle, isOpen, resellPriceInStars,
canBuyGift, giftOwnerTitle, isOpen, resellPriceInStars, giftSubtitle,
releasedByPeer,
]);
return (
@ -812,9 +838,14 @@ export default memo(withGlobal<OwnProps>(
const currentUserEmojiStatus = currentUser?.emojiStatus;
const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses;
const gift = isSavedGift ? typeGift.gift : typeGift;
const releasedByPeerId = gift?.type === 'starGiftUnique' && gift.releasedByPeerId;
const releasedByPeer = releasedByPeerId ? selectPeer(global, releasedByPeerId) : undefined;
return {
fromPeer,
targetPeer,
releasedByPeer,
currentUserId,
starGiftMaxConvertPeriod: global.appConfig?.starGiftMaxConvertPeriod,
tonExplorerUrl: global.appConfig?.tonExplorerUrl,

View File

@ -40,6 +40,7 @@ const ChatRefundModal = ({ modal, user }: OwnProps & StateProps) => {
const handleConfirmRemoveFee = useLastCallback(() => {
closeChatRefundModal();
if (!userId) return;
toggleNoPaidMessagesException ({ userId, shouldRefundCharged: shouldRefundStars });
});

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../lib/teact/teact';
import type { OwnProps } from './SuggestMessageModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
import Loading from '../../ui/Loading';
const SuggestMessageModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const SuggestMessageModal = useModuleLoader(Bundles.Extra, 'SuggestMessageModal', !modal);
return SuggestMessageModal ? <SuggestMessageModal {...props} /> : <Loading />;
};
export default SuggestMessageModalAsync;

View File

@ -0,0 +1,55 @@
.content {
display: flex;
flex-direction: column;
max-height: min(92vh, 32rem) !important;
}
.modalHeader {
padding-top: 0.25rem !important;
}
.section,
.form {
display: flex;
flex-direction: column;
}
.section {
margin-bottom: 1rem;
}
.label {
margin-block: 0.5rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
}
.input {
margin-bottom: 0.5rem;
}
.description {
min-height: 2.75rem;
margin-bottom: 0.5rem;
margin-left: 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.timeInputIcon {
position: absolute;
top: 50%;
right: 1rem;
transform: translateY(-50%);
font-size: 1.25rem;
color: var(--color-text-secondary);
transition: color 0.15s ease;
}
.offerButton {
font-weight: var(--font-weight-medium);
}

View File

@ -0,0 +1,256 @@
import type React from '../../../lib/teact/teact';
import {
memo, useEffect,
useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiDraft, ApiStarsAmount } from '../../../api/types';
import type { ApiPeer } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { MAIN_THREAD_ID } from '../../../api/types';
import {
STARS_SUGGESTED_POST_AGE_MIN,
STARS_SUGGESTED_POST_AMOUNT_MAX,
STARS_SUGGESTED_POST_AMOUNT_MIN,
STARS_SUGGESTED_POST_FUTURE_MAX,
STARS_SUGGESTED_POST_FUTURE_MIN } from '../../../config';
import { selectPeer } from '../../../global/selectors';
import { selectDraft } from '../../../global/selectors/messages';
import buildClassName from '../../../util/buildClassName';
import { formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat';
import { formatStarsAsIcon, formatStarsAsText } from '../../../util/localization/format';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import CalendarModal from '../../common/CalendarModal';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import InputText from '../../ui/InputText';
import Modal from '../../ui/Modal';
import styles from './SuggestMessageModal.module.scss';
export type OwnProps = {
modal: TabState['suggestMessageModal'];
};
import useFlag from '../../../hooks/useFlag';
type StateProps = {
starBalance?: ApiStarsAmount;
peer?: ApiPeer;
currentDraft?: ApiDraft;
maxAmount: number;
minAmount: number;
ageMinSeconds: number;
futureMin: number;
futureMax: number;
};
const SuggestMessageModal = ({
modal,
starBalance,
peer,
currentDraft,
maxAmount,
minAmount,
ageMinSeconds,
futureMin,
futureMax,
}: OwnProps & StateProps) => {
const { closeSuggestMessageModal, updateDraftSuggestedPostInfo, openStarsBalanceModal } = getActions();
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
const currentSuggestedPostInfo = currentDraft?.suggestedPostInfo;
const currentReplyInfo = currentDraft?.replyInfo;
const isInSuggestChangesMode = Boolean(currentReplyInfo);
const [starsAmount, setStarsAmount] = useState<number | undefined>(
currentSuggestedPostInfo?.price?.amount || undefined,
);
const [scheduleDate, setScheduleDate] = useState<number | undefined>(
currentSuggestedPostInfo?.scheduleDate
? currentSuggestedPostInfo.scheduleDate * 1000
: undefined,
);
useEffect(() => {
setStarsAmount(currentSuggestedPostInfo?.price?.amount || undefined);
setScheduleDate(currentSuggestedPostInfo?.scheduleDate
? currentSuggestedPostInfo.scheduleDate * 1000
: undefined);
}, [currentSuggestedPostInfo]);
const lang = useLang();
const oldLang = useOldLang();
const now = Math.floor(Date.now() / 1000);
const minAt = (now + futureMin) * 1000;
const maxAt = (now + futureMax) * 1000;
const defaultSelectedTime = (now + futureMin * 2) * 1000;
const handleAmountChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
const number = parseFloat(value);
const result = value === '' || Number.isNaN(number) ? undefined
: Math.min(Math.max(number, 0), maxAmount);
setStarsAmount(result);
});
const handleExpireDateChange = useLastCallback((date: Date) => {
setScheduleDate(date.getTime());
closeCalendar();
});
const handleAnytimeClick = useLastCallback(() => {
setScheduleDate(undefined);
closeCalendar();
});
const isDisabled = Boolean(starsAmount) && starsAmount < minAmount;
const handleOffer = useLastCallback(() => {
const neededAmount = starsAmount || 0;
if (isDisabled) {
return;
}
const currentBalance = starBalance?.amount || 0;
if (neededAmount > currentBalance) {
openStarsBalanceModal({
topup: {
balanceNeeded: neededAmount,
},
});
return;
}
updateDraftSuggestedPostInfo({
price: { amount: neededAmount, nanos: 0 },
scheduleDate: scheduleDate ? scheduleDate / 1000 : undefined,
});
closeSuggestMessageModal();
});
return (
<Modal
headerClassName={styles.modalHeader}
isOpen={Boolean(modal)}
onClose={closeSuggestMessageModal}
isSlim
isLowStackPriority
hasCloseButton
contentClassName={styles.content}
title={isInSuggestChangesMode ? lang('TitleSuggestedChanges') : lang('TitleSuggestMessage')}
>
<div className={styles.form}>
<div className={styles.section}>
<InputText
label={lang('InputPlaceholderPrice')}
className={buildClassName(styles.input)}
value={starsAmount?.toString()}
onChange={handleAmountChange}
inputMode="numeric"
tabIndex={0}
teactExperimentControlled
/>
<div className={styles.description}>
{starsAmount !== undefined && starsAmount > 0 && starsAmount < minAmount
? lang('DescriptionSuggestedPostMinimumOffer', {
amount: formatStarsAsText(lang, minAmount) },
{ withNodes: true, withMarkdown: true })
: lang('SuggestMessagePriceDescription', {
currency: lang('CurrencyStars'),
})}
</div>
</div>
<div className={styles.section}>
<div className={buildClassName('input-group', 'touched')}>
<input
type="text"
className={buildClassName('form-control', isCalendarOpened && 'focus')}
value={scheduleDate ? formatScheduledDateTime(scheduleDate / 1000, lang, oldLang) : lang('TitleAnytime')}
autoComplete="off"
onClick={openCalendar}
onFocus={openCalendar}
readOnly
/>
<label>{lang('InputTitleSuggestMessageTime')}</label>
<Icon name="down" className={styles.timeInputIcon} />
</div>
<div className={styles.description}>
{lang('SuggestMessageTimeDescription', {
hint: lang('SuggestMessageDateTimeHint'),
duration: formatShortDuration(lang, ageMinSeconds, true),
})}
</div>
</div>
<CalendarModal
isOpen={isCalendarOpened}
isFutureMode
withTimePicker
minAt={minAt}
maxAt={maxAt}
onClose={closeCalendar}
onSubmit={handleExpireDateChange}
selectedAt={scheduleDate || defaultSelectedTime}
submitButtonLabel={lang('Save')}
secondButtonLabel={lang('TitleAnytime')}
onSecondButtonClick={handleAnytimeClick}
description={lang('SuggestMessageDateTimeHint')}
/>
<Button
className={styles.offerButton}
onClick={handleOffer}
size="smaller"
disabled={isDisabled}
>
{isInSuggestChangesMode ? lang('ButtonUpdateTerms')
: starsAmount ? lang('ButtonOfferAmount', {
amount: formatStarsAsIcon(lang, starsAmount, { asFont: true }),
}, {
withNodes: true,
withMarkdown: true,
}) : lang('ButtonOfferFree')}
</Button>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const starBalance = global.stars?.balance;
const peer = modal ? selectPeer(global, modal.chatId) : undefined;
const currentDraft = modal ? selectDraft(global, modal.chatId, MAIN_THREAD_ID) : undefined;
const { appConfig } = global;
const maxAmount = appConfig?.starsSuggestedPostAmountMax || STARS_SUGGESTED_POST_AMOUNT_MAX;
const minAmount = appConfig?.starsSuggestedPostAmountMin || STARS_SUGGESTED_POST_AMOUNT_MIN;
const ageMinSeconds = appConfig?.starsSuggestedPostAgeMin || STARS_SUGGESTED_POST_AGE_MIN;
const futureMin = appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN;
const futureMax = appConfig?.starsSuggestedPostFutureMax || STARS_SUGGESTED_POST_FUTURE_MAX;
return {
peer,
starBalance,
currentDraft,
maxAmount,
minAmount,
ageMinSeconds,
futureMin,
futureMax,
};
},
)(SuggestMessageModal));

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../lib/teact/teact';
import type { OwnProps } from './SuggestedPostApprovalModal';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
import Loading from '../../ui/Loading';
const SuggestedPostApprovalModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const SuggestedPostApprovalModal = useModuleLoader(Bundles.Extra, 'SuggestedPostApprovalModal', !modal);
return SuggestedPostApprovalModal ? <SuggestedPostApprovalModal {...props} /> : <Loading />;
};
export default SuggestedPostApprovalModalAsync;

View File

@ -0,0 +1,3 @@
.details {
margin-top: 0.5rem;
}

View File

@ -0,0 +1,234 @@
import { memo, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiMessage, ApiPeer } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { STARS_SUGGESTED_POST_AGE_MIN,
STARS_SUGGESTED_POST_COMMISSION_PERMILLE,
STARS_SUGGESTED_POST_FUTURE_MAX,
STARS_SUGGESTED_POST_FUTURE_MIN,
} from '../../../config';
import { getPeerFullTitle } from '../../../global/helpers/peers';
import { selectChatMessage, selectIsMonoforumAdmin, selectSender } from '../../../global/selectors';
import { formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat';
import { formatStarsAsText } from '../../../util/localization/format';
import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import CalendarModal from '../../common/CalendarModal';
import ConfirmDialog from '../../ui/ConfirmDialog';
import styles from './SuggestedPostApprovalModal.module.scss';
export type OwnProps = {
modal: TabState['suggestedPostApprovalModal'];
};
type StateProps = {
commissionPermille: number;
minAge: number;
futureMin: number;
futureMax: number;
message?: ApiMessage;
sender?: ApiPeer;
isAdmin?: boolean;
scheduleDate?: number;
};
const SuggestedPostApprovalModal = ({
modal,
message,
sender,
isAdmin,
commissionPermille,
minAge,
futureMin,
futureMax,
scheduleDate,
}: OwnProps & StateProps) => {
const {
closeSuggestedPostApprovalModal,
approveSuggestedPost,
} = getActions();
const lang = useLang();
const oldLang = useOldLang();
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
const now = Math.floor(Date.now() / 1000);
const minAt = (now + futureMin) * 1000;
const maxAt = (now + futureMax) * 1000;
const defaultSelectedTime = now + futureMin * 2;
const [selectedScheduleDate, setSelectedScheduleDate] = useState<number>(defaultSelectedTime);
const handleClose = useLastCallback(() => {
closeSuggestedPostApprovalModal();
});
const handleApprove = useLastCallback((date?: number) => {
if (!modal) return;
approveSuggestedPost({
chatId: modal.chatId,
messageId: modal.messageId,
scheduleDate: date,
});
closeSuggestedPostApprovalModal();
});
const handleCalendarDateChange = useLastCallback((date: Date) => {
const time = Math.floor(date.getTime() / 1000);
setSelectedScheduleDate(time);
});
const handleCalendarSubmit = useLastCallback((date: Date) => {
const time = Math.floor(date.getTime() / 1000);
closeCalendar();
handleApprove(time);
});
const handlePublishNow = useLastCallback(() => {
closeCalendar();
handleApprove();
});
const handleNext = useLastCallback(() => {
if (scheduleDate) {
handleApprove(scheduleDate);
} else {
openCalendar();
}
});
if (!modal || !message) {
return undefined;
}
const senderName = sender ? getPeerFullTitle(oldLang, sender) : '';
const renderContent = () => {
const amount = message?.suggestedPostInfo?.price?.amount;
const question = lang(
'SuggestedPostConfirmMessage',
{ peer: senderName },
{ withNodes: true, withMarkdown: true });
const questionText = renderText(question);
if (!amount) {
return questionText;
}
const commission = (commissionPermille / 10);
const amountWithCommission = amount / 100 * commission;
const starsText = formatStarsAsText(lang, amountWithCommission);
const ageMinSeconds = minAge;
const duration = formatShortDuration(lang, ageMinSeconds, true);
if (scheduleDate) {
const time = formatScheduledDateTime(scheduleDate, lang, oldLang);
const key
= isAdmin ? 'SuggestedPostConfirmDetailsWithTimeAdmin' : 'SuggestedPostConfirmDetailsWithTimeUser';
return (
<>
<div>
{questionText}
</div>
<div className={styles.details}>
{renderText(lang(key, {
amount: starsText,
commission,
duration,
time,
}, { withNodes: true, withMarkdown: true }))}
</div>
</>
);
}
const key = isAdmin ? 'SuggestedPostConfirmDetailsAdmin' : 'SuggestedPostConfirmDetailsUser';
return (
<>
<div>
{questionText}
</div>
<div className={styles.details}>
{renderText(lang(key, {
amount: starsText,
commission,
duration,
}, { withNodes: true, withMarkdown: true }))}
</div>
</>
);
};
return (
<>
<ConfirmDialog
isOpen={Boolean(modal) && !isCalendarOpened}
onClose={handleClose}
title={lang('SuggestedPostConfirmTitle')}
confirmHandler={handleNext}
confirmLabel={scheduleDate ? lang('ButtonPublish') : lang('Next')}
>
{renderContent()}
</ConfirmDialog>
<CalendarModal
isOpen={isCalendarOpened}
isFutureMode
withTimePicker
minAt={minAt}
maxAt={maxAt}
onClose={closeCalendar}
onSubmit={handleCalendarSubmit}
onDateChange={handleCalendarDateChange}
selectedAt={selectedScheduleDate * 1000}
submitButtonLabel={lang('ButtonPublishAtTime', {
time: formatScheduledDateTime(selectedScheduleDate, lang, oldLang),
})}
secondButtonLabel={lang('PublishNow')}
onSecondButtonClick={handlePublishNow}
description={lang('SuggestMessageDateTimeHint')}
/>
</>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const message = modal && selectChatMessage(global, modal.chatId, modal.messageId);
const sender = message ? selectSender(global, message) : undefined;
const isAdmin = modal && selectIsMonoforumAdmin(global, modal.chatId);
const { appConfig } = global;
const commissionPermille = appConfig?.starsSuggestedPostCommissionPermille
|| STARS_SUGGESTED_POST_COMMISSION_PERMILLE;
const minAge = appConfig?.starsSuggestedPostAgeMin || STARS_SUGGESTED_POST_AGE_MIN;
const futureMin = (appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN) * 2;
const futureMax = appConfig?.starsSuggestedPostFutureMax || STARS_SUGGESTED_POST_FUTURE_MAX;
const scheduleDate = message?.suggestedPostInfo?.scheduleDate;
return {
minAge,
futureMin,
futureMax,
message,
sender,
isAdmin,
commissionPermille,
scheduleDate,
};
},
)(SuggestedPostApprovalModal));

View File

@ -110,6 +110,15 @@ export const TODO_ITEMS_LIMIT = 30;
export const TODO_TITLE_LENGTH_LIMIT = 32;
export const TODO_ITEM_LENGTH_LIMIT = 64;
// Suggested Posts defaults
export const STARS_SUGGESTED_POST_AMOUNT_MAX = 100000;
export const STARS_SUGGESTED_POST_AMOUNT_MIN = 5;
export const STARS_SUGGESTED_POST_COMMISSION_PERMILLE = 850;
export const STARS_SUGGESTED_POST_AGE_MIN = 86400; // 24 hours in seconds
export const STARS_SUGGESTED_POST_FUTURE_MAX = 2678400; // 31 days in seconds
export const STARS_SUGGESTED_POST_FUTURE_MIN = 300; // 5 minutes in seconds
export const TON_SUGGESTED_POST_COMMISSION_PERMILLE = 850;
export const STORY_VIEWS_MIN_SEARCH = 15;
export const STORY_MIN_REACTIONS_SORT = 10;
export const STORY_VIEWS_MIN_CONTACTS_FILTER = 20;

View File

@ -78,6 +78,23 @@ const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
let botFatherId: string | null;
addActionHandler('clickSuggestedMessageButton', (global, actions, payload): ActionReturnType => {
const {
chatId, messageId, button, tabId = getCurrentTabId(),
} = payload;
const { buttonType } = button;
const message = selectChatMessage(global, chatId, messageId);
switch (buttonType) {
case 'suggestChanges':
if (!message) break;
actions.initDraftFromSuggestedMessage({ chatId, messageId, tabId });
break;
}
});
addActionHandler('clickBotInlineButton', (global, actions, payload): ActionReturnType => {
const {
chatId, messageId, button, tabId = getCurrentTabId(),

View File

@ -6,10 +6,12 @@ import type {
ApiError,
ApiInputMessageReplyInfo,
ApiInputStoryReplyInfo,
ApiInputSuggestedPostInfo,
ApiMessage,
ApiOnProgress,
ApiStory,
ApiUser,
MediaContent,
} from '../../../api/types';
import type {
ForwardMessagesParams,
@ -31,6 +33,7 @@ import {
MESSAGE_LIST_SLICE,
RE_TELEGRAM_LINK,
SERVICE_NOTIFICATIONS_USER_ID,
STARS_SUGGESTED_POST_FUTURE_MIN,
SUPPORTED_AUDIO_CONTENT_TYPES,
SUPPORTED_PHOTO_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
@ -124,6 +127,7 @@ import {
selectIsChatWithSelf,
selectIsCurrentUserFrozen,
selectIsCurrentUserPremium,
selectIsMonoforumAdmin,
selectLanguageCode,
selectListedIds,
selectMessageReplyInfo,
@ -135,6 +139,7 @@ import {
selectPollFromMessage,
selectRealLastReadId,
selectReplyCanBeSentToChat,
selectSavedDialogIdFromMessage,
selectScheduledMessage,
selectSendAs,
selectTabState,
@ -337,6 +342,8 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
const isForwarding = selectTabState(global, tabId).forwardMessages?.messageIds?.length;
const draftReplyInfo = !isForwarding && !isStoryReply ? draft?.replyInfo : undefined;
const draftSuggestedPostInfo = !isForwarding && !isStoryReply
? draft?.suggestedPostInfo : undefined;
const storyReplyInfo = isStoryReply ? {
type: 'story',
@ -354,16 +361,41 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
const messagePriceInStars = await getPeerStarsForMessage(global, chatId!);
const suggestedPostPrice = draftSuggestedPostInfo?.price?.amount || 0;
if (suggestedPostPrice && !draftReplyInfo) {
const currentBalance = global.stars?.balance?.amount || 0;
if (suggestedPostPrice > currentBalance) {
actions.openStarsBalanceModal({
topup: {
balanceNeeded: suggestedPostPrice,
},
tabId,
});
return;
}
}
const suggestedMessage = draftReplyInfo && draftSuggestedPostInfo
? selectChatMessage(global, chatId!, draftReplyInfo.replyToMsgId) : undefined;
let suggestedMedia: MediaContent | undefined;
if (draftSuggestedPostInfo && suggestedMessage?.content) {
suggestedMedia = suggestedMessage.content;
}
const params: SendMessageParams = {
...payload,
chat,
replyInfo,
suggestedPostInfo: draftSuggestedPostInfo,
suggestedMedia,
noWebPage: selectNoWebPage(global, chatId!, threadId!),
sendAs: selectSendAs(global, chatId!),
lastMessageId,
messagePriceInStars,
isStoryReply,
isPending: messagePriceInStars ? true : undefined,
...suggestedMessage && { isInvertedMedia: suggestedMessage?.isInvertedMedia },
};
if (!isStoryReply) {
@ -608,7 +640,7 @@ addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
const currentDraft = selectDraft(global, chatId, threadId);
if (chat.isMonoforum && !currentDraft?.replyInfo) {
if (chat.isMonoforum && !currentDraft?.replyInfo && !currentDraft?.suggestedPostInfo) {
return; // Monoforum doesn't support drafts outside threads
}
@ -616,6 +648,7 @@ addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
text,
replyInfo: currentDraft?.replyInfo,
effectId: currentDraft?.effectId,
suggestedPostInfo: currentDraft?.suggestedPostInfo,
};
saveDraft({
@ -625,7 +658,7 @@ addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId = MAIN_THREAD_ID, isLocalOnly, shouldKeepReply,
chatId, threadId = MAIN_THREAD_ID, isLocalOnly, shouldKeepReply, shouldKeepSuggestedPost,
} = payload;
const currentDraft = selectDraft(global, chatId, threadId);
if (!currentDraft) {
@ -634,8 +667,9 @@ addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
const currentReplyInfo = currentDraft.replyInfo;
const newDraft: ApiDraft | undefined = shouldKeepReply && currentReplyInfo ? {
replyInfo: currentReplyInfo,
const newDraft: ApiDraft | undefined = (shouldKeepReply || shouldKeepSuggestedPost) ? {
replyInfo: shouldKeepReply ? currentReplyInfo : undefined,
suggestedPostInfo: shouldKeepSuggestedPost ? currentDraft.suggestedPostInfo : undefined,
} : undefined;
saveDraft({
@ -665,6 +699,7 @@ addActionHandler('updateDraftReplyInfo', (global, actions, payload): ActionRetur
const newDraft: ApiDraft = {
...currentDraft,
replyInfo: updatedReplyInfo,
suggestedPostInfo: undefined,
};
saveDraft({
@ -682,7 +717,7 @@ addActionHandler('resetDraftReplyInfo', (global, actions, payload): ActionReturn
const chat = selectChat(global, chatId);
const currentDraft = selectDraft(global, chatId, threadId);
if (chat?.isMonoforum && !currentDraft?.replyInfo) {
if (chat?.isMonoforum && !currentDraft?.replyInfo && !currentDraft?.suggestedPostInfo) {
return; // Monoforum doesn't support drafts outside threads
}
const newDraft: ApiDraft | undefined = !currentDraft?.text ? undefined : {
@ -695,6 +730,92 @@ addActionHandler('resetDraftReplyInfo', (global, actions, payload): ActionReturn
});
});
addActionHandler('updateDraftSuggestedPostInfo', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), ...update } = payload;
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { chatId, threadId } = currentMessageList;
const currentDraft = selectDraft(global, chatId, threadId);
const updatedSuggestedPostInfo = {
...currentDraft?.suggestedPostInfo,
...update,
} as ApiInputSuggestedPostInfo;
const newDraft: ApiDraft = {
...currentDraft,
suggestedPostInfo: updatedSuggestedPostInfo,
};
saveDraft({
global, chatId, threadId, draft: newDraft, isLocalOnly: true, noLocalTimeUpdate: true,
});
});
addActionHandler('resetDraftSuggestedPostInfo', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { chatId, threadId } = currentMessageList;
saveDraft({
global, chatId, threadId, draft: undefined, isLocalOnly: false,
});
});
addActionHandler('initDraftFromSuggestedMessage', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
const message = selectChatMessage(global, chatId, messageId);
if (!message) {
return;
}
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { threadId } = currentMessageList;
actions.clearDraft({
chatId,
threadId,
isLocalOnly: true,
});
actions.updateDraftReplyInfo({
replyToMsgId: messageId,
monoforumPeerId: selectSavedDialogIdFromMessage(global, message),
tabId,
});
if (message.suggestedPostInfo) {
const { scheduleDate, ...messageSuggestedPost } = message.suggestedPostInfo;
const now = getServerTime();
const futureMin = global.appConfig?.starsSuggestedPostFutureMin || STARS_SUGGESTED_POST_FUTURE_MIN;
const validScheduleDate = scheduleDate && scheduleDate > now + futureMin ? scheduleDate : undefined;
actions.updateDraftSuggestedPostInfo({
...messageSuggestedPost,
scheduleDate: validScheduleDate,
tabId,
});
}
actions.saveDraft({
chatId,
threadId,
text: message.content.text,
});
});
addActionHandler('saveEffectInDraft', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId, effectId,
@ -702,7 +823,7 @@ addActionHandler('saveEffectInDraft', (global, actions, payload): ActionReturnTy
const chat = selectChat(global, chatId);
const currentDraft = selectDraft(global, chatId, threadId);
if (chat?.isMonoforum && !currentDraft?.replyInfo) {
if (chat?.isMonoforum && !currentDraft?.replyInfo && !currentDraft?.suggestedPostInfo) {
return; // Monoforum doesn't support drafts outside threads
}
@ -1713,6 +1834,9 @@ export async function getPeerStarsForMessage<T extends GlobalState>(
if (!peer) return undefined;
if (isApiPeerChat(peer)) {
if (selectIsMonoforumAdmin(global, peerId)) {
return undefined;
}
return peer.paidMessagesStars;
}
@ -2068,6 +2192,65 @@ addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promis
await fetchUnreadMentions(global, chatId, offsetId);
});
addActionHandler('approveSuggestedPost', async (global, actions, payload): Promise<void> => {
const { chatId, messageId, scheduleDate, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const message = selectChatMessage(global, chatId, messageId);
const isAdmin = selectIsMonoforumAdmin(global, chatId);
if (!isAdmin && message?.suggestedPostInfo?.price?.amount) {
const neededAmount = message.suggestedPostInfo.price.amount;
const currentBalance = global.stars?.balance?.amount || 0;
if (neededAmount > currentBalance) {
actions.openStarsBalanceModal({
topup: {
balanceNeeded: neededAmount,
},
tabId,
});
return;
}
}
const result = await callApi('toggleSuggestedPostApproval', {
chat,
messageId,
reject: false,
scheduleDate,
});
if (!result) return;
actions.showNotification({
message: { key: 'SuggestedPostApproved' },
tabId,
});
});
addActionHandler('rejectSuggestedPost', async (global, actions, payload): Promise<void> => {
const { chatId, messageId, rejectComment, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('toggleSuggestedPostApproval', {
chat,
messageId,
reject: true,
rejectComment,
});
if (!result) return;
actions.showNotification({
message: { key: 'SuggestedPostRejectedNotification' },
tabId,
});
});
async function fetchUnreadMentions<T extends GlobalState>(global: T, chatId: string, offsetId?: number) {
const chat = selectChat(global, chatId);
if (!chat) return;

View File

@ -99,7 +99,9 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
return;
}
const byId = buildCollectionByKey(result, 'id');
global = getGlobal();
const byId = buildCollectionByKey(result.gifts, 'id');
const idsByCategoryName: Record<StarGiftCategory, string[]> = {
all: [],
@ -134,7 +136,6 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
idsByCategoryName[starsCategory].push(gift.id);
});
global = getGlobal();
global = {
...global,
starGifts: {

View File

@ -41,6 +41,7 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
actions.closeStoryViewer({ tabId });
actions.closeStarsBalanceModal({ tabId });
actions.closeStarsTransactionModal({ tabId });
actions.closeGiftInfoModal({ tabId });
if (!currentMessageList || (
currentMessageList.chatId !== chatId

View File

@ -1020,6 +1020,34 @@ addActionHandler('closePaidReactionModal', (global, actions, payload): ActionRet
}, tabId);
});
addActionHandler('openSuggestMessageModal', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
suggestMessageModal: { chatId, messageId },
}, tabId);
});
addActionHandler('closeSuggestMessageModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
suggestMessageModal: undefined,
}, tabId);
});
addActionHandler('openSuggestedPostApprovalModal', (global, actions, payload): ActionReturnType => {
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
suggestedPostApprovalModal: { chatId, messageId },
}, tabId);
});
addActionHandler('closeSuggestedPostApprovalModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
suggestedPostApprovalModal: undefined,
}, tabId);
});
function copyTextForMessages(global: GlobalState, chatId: string, messageIds: number[]) {
const { type: messageListType, threadId } = selectCurrentMessageList(global) || {};
const lang = langProvider.oldTranslate;

View File

@ -1,3 +1,5 @@
import type { TeactNode } from '../../lib/teact/teact';
import type {
ApiAttachment,
ApiMessage,
@ -10,6 +12,7 @@ import type {
ApiPoll, MediaContainer, StatefulMediaContent,
} from '../../api/types/messages';
import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization';
import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
@ -25,6 +28,7 @@ import {
VERIFICATION_CODES_USER_ID,
VIDEO_STICKER_MIME_TYPE,
} from '../../config';
import { areDeepEqual } from '../../util/areDeepEqual';
import { getCleanPeerId, isUserId } from '../../util/entities/ids';
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { isLocalMessageId } from '../../util/keys/messageKey';
@ -409,3 +413,84 @@ export function splitMessagesForForwarding(messages: ApiMessage[], limit: number
return result;
}
export interface SuggestedChangesInfo {
isNewText: boolean;
isNewPrice: boolean;
isNewTime: boolean;
isNewMedia: boolean;
}
export function getSuggestedChangesInfo(
message: ApiMessage,
originalMessage?: ApiMessage,
): SuggestedChangesInfo | undefined {
if (!message.suggestedPostInfo || message.replyInfo?.type !== 'message'
|| !message.replyInfo?.replyToMsgId || !originalMessage) {
return undefined;
}
if (!originalMessage.suggestedPostInfo) {
return undefined;
}
const original = originalMessage.suggestedPostInfo;
const suggested = message.suggestedPostInfo;
const originalContent = originalMessage.content;
const suggestedContent = message.content;
const { text: originalText, ...originalMediaContent } = originalContent;
const { text: suggestedText, ...suggestedMediaContent } = suggestedContent;
const isNewText = !areDeepEqual(originalText, suggestedText);
const isNewMedia = !areDeepEqual(originalMediaContent, suggestedMediaContent);
const originalPrice = original.price?.amount;
const suggestedPrice = suggested.price?.amount;
const isNewPrice = originalPrice !== suggestedPrice;
const originalTime = original.scheduleDate;
const suggestedTime = suggested.scheduleDate;
const isNewTime = originalTime !== suggestedTime;
if (!isNewText && !isNewPrice && !isNewTime && !isNewMedia) {
return undefined;
}
return {
isNewText,
isNewPrice,
isNewTime,
isNewMedia,
};
}
export function getSuggestedChangesActionText(
lang: LangFn,
message: ApiMessage,
originalMessage?: ApiMessage,
isOutgoing?: boolean,
senderLink?: TeactNode,
): TeactNode | undefined {
const changesInfo = getSuggestedChangesInfo(message, originalMessage);
if (!changesInfo) {
return undefined;
}
const changesParts: string[] = [];
if (changesInfo.isNewPrice) changesParts.push(lang('ActionSuggestedChangesPrice'));
if (changesInfo.isNewTime) changesParts.push(lang('ActionSuggestedChangesTime'));
if (changesInfo.isNewText) changesParts.push(lang('ActionSuggestedChangesText'));
if (changesInfo.isNewMedia) changesParts.push(lang('ActionSuggestedChangesMedia'));
const changesText = lang.conjunction(changesParts);
const langKey = isOutgoing ? 'ActionSuggestedChangesOutgoing' : 'ActionSuggestedChangesIncoming';
return lang(langKey, {
changes: changesText,
user: senderLink,
}, {
withNodes: true,
withMarkdown: true,
});
}

View File

@ -18,7 +18,9 @@ import type {
ApiInputInvoiceStarGift,
ApiInputMessageReplyInfo,
ApiInputSavedStarGift,
ApiInputSuggestedPostInfo,
ApiKeyboardButton,
ApiKeyboardButtonSuggestedMessage,
ApiLimitTypeWithModal,
ApiMessage,
ApiMessageEntity,
@ -607,6 +609,21 @@ export interface ActionPayloads {
description?: string;
option?: string;
} & WithTabId;
approveSuggestedPost: {
chatId: string;
messageId: number;
scheduleDate?: number;
} & WithTabId;
confirmApproveSuggestedPost: {
chatId: string;
messageId: number;
} & WithTabId;
rejectSuggestedPost: {
chatId: string;
messageId: number;
rejectComment?: string;
} & WithTabId;
sendMessageAction: {
action: ApiSendMessageAction;
chatId: string;
@ -672,6 +689,7 @@ export interface ActionPayloads {
threadId?: ThreadId;
isLocalOnly?: boolean;
shouldKeepReply?: boolean;
shouldKeepSuggestedPost?: boolean;
};
loadPinnedMessages: {
chatId: string;
@ -985,6 +1003,12 @@ export interface ActionPayloads {
focusLastMessage: WithTabId | undefined;
updateDraftReplyInfo: Partial<ApiInputMessageReplyInfo> & WithTabId;
resetDraftReplyInfo: WithTabId | undefined;
updateDraftSuggestedPostInfo: Partial<ApiInputSuggestedPostInfo> & WithTabId;
resetDraftSuggestedPostInfo: WithTabId | undefined;
initDraftFromSuggestedMessage: {
chatId: string;
messageId: number;
} & WithTabId;
updateInsertingPeerIdMention: {
peerId?: string;
} & WithTabId;
@ -1997,6 +2021,11 @@ export interface ActionPayloads {
messageId: number;
button: ApiKeyboardButton;
} & WithTabId;
clickSuggestedMessageButton: {
chatId: string;
messageId: number;
button: ApiKeyboardButtonSuggestedMessage;
} & WithTabId;
switchBotInline: {
messageId?: number;
@ -2428,6 +2457,18 @@ export interface ActionPayloads {
} & WithTabId;
closePaidReactionModal: WithTabId | undefined;
openSuggestMessageModal: {
chatId: string;
messageId?: number;
} & WithTabId;
closeSuggestMessageModal: WithTabId | undefined;
openSuggestedPostApprovalModal: {
chatId: string;
messageId: number;
} & WithTabId;
closeSuggestedPostApprovalModal: WithTabId | undefined;
openDeleteMessageModal: ({
chatId: string;
messageIds: number[];

View File

@ -758,6 +758,16 @@ export type TabState = {
messageId: number;
};
suggestMessageModal?: {
chatId: string;
messageId?: number;
};
suggestedPostApprovalModal?: {
chatId: string;
messageId: number;
};
inviteViaLinkModal?: {
missingUsers: ApiMissingInvitedUser[];
chatId: string;

View File

@ -12,5 +12,6 @@ for (const tl of Object.values(Api)) {
}
}
export const LAYER = 205;
export const LAYER = 207;
export { tlobjects };

File diff suppressed because one or more lines are too long

View File

@ -93,7 +93,7 @@ chatParticipants#3cbc93f8 chat_id:long participants:Vector<ChatParticipant> vers
chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto;
messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;
message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message;
message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message;
messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;
messageMediaEmpty#3ded6320 = MessageMedia;
messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia;
@ -166,6 +166,10 @@ messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags
messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction;
messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction;
messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction;
messageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction;
messageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction;
messageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction;
messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction;
dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
photoEmpty#2331b22d id:long = Photo;
@ -376,7 +380,6 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag
updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update;
updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector<int> qts:int = Update;
updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update;
updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update;
updateStarsBalance#4e80a379 balance:StarsAmount = Update;
updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update;
updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update;
@ -386,6 +389,7 @@ updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update;
updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector<bytes> next_offset:int = Update;
updateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update;
updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update;
updateMonoForumNoPaidException#9f812b08 flags:# exception:flags.0?true channel_id:long saved_peer_id:Peer = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference;
updates.difference#f49ca0 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> state:updates.State = updates.Difference;
@ -549,6 +553,7 @@ inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet;
inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet;
inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet;
inputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet;
inputStickerSetTonGifts#1cf671a0 = InputStickerSet;
stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true creator:flags.11?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector<PhotoSize> thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet;
messages.stickerSet#6e153f16 set:StickerSet packs:Vector<StickerPack> keywords:Vector<StickerKeyword> documents:Vector<Document> = messages.StickerSet;
messages.stickerSetNotModified#d3f924eb = messages.StickerSet;
@ -689,7 +694,7 @@ contacts.topPeersNotModified#de266ef5 = contacts.TopPeers;
contacts.topPeers#70b772a8 categories:Vector<TopPeerCategoryPeers> chats:Vector<Chat> users:Vector<User> = contacts.TopPeers;
contacts.topPeersDisabled#b52c939d = contacts.TopPeers;
draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage;
draftMessage#2d65321f flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long = DraftMessage;
draftMessage#96eaa5eb flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost = DraftMessage;
messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers;
messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;
messages.recentStickersNotModified#b17f890 = messages.RecentStickers;
@ -966,7 +971,7 @@ pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true optio
pollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<Peer> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> = PollResults;
chatOnlines#f041e250 onlines:int = ChatOnlines;
statsURL#47a971e0 url:string = StatsURL;
chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights;
chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true = ChatAdminRights;
chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights;
inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper;
inputWallPaperSlug#72091c80 slug:string = InputWallPaper;
@ -1346,16 +1351,9 @@ sponsoredMessageReportOption#430d3150 text:string option:bytes = SponsoredMessag
channels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options:Vector<SponsoredMessageReportOption> = channels.SponsoredMessageReportResult;
channels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult;
channels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult;
stats.broadcastRevenueStats#5407e297 top_hours_graph:StatsGraph revenue_graph:StatsGraph balances:BroadcastRevenueBalances usd_rate:double = stats.BroadcastRevenueStats;
stats.broadcastRevenueWithdrawalUrl#ec659737 url:string = stats.BroadcastRevenueWithdrawalUrl;
broadcastRevenueTransactionProceeds#557e2cc4 amount:long from_date:int to_date:int = BroadcastRevenueTransaction;
broadcastRevenueTransactionWithdrawal#5a590978 flags:# pending:flags.0?true failed:flags.2?true amount:long date:int provider:string transaction_date:flags.1?int transaction_url:flags.1?string = BroadcastRevenueTransaction;
broadcastRevenueTransactionRefund#42d30d2e amount:long date:int provider:string = BroadcastRevenueTransaction;
stats.broadcastRevenueTransactions#87158466 count:int transactions:Vector<BroadcastRevenueTransaction> = stats.BroadcastRevenueTransactions;
reactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom;
reactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom;
reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings;
broadcastRevenueBalances#c3ff71e7 flags:# withdrawal_enabled:flags.0?true current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances;
availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect;
messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects;
messages.availableEffects#bddb616e hash:int effects:Vector<AvailableEffect> documents:Vector<Document> = messages.AvailableEffects;
@ -1369,13 +1367,13 @@ starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer;
starsTransactionPeerAds#60682812 = StarsTransactionPeer;
starsTransactionPeerAPI#f9677aad = StarsTransactionPeer;
starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption;
starsTransaction#a39fd94a flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int = StarsTransaction;
starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction;
payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;
foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory;
stories.foundStories#e2de7737 flags:# count:int stories:Vector<FoundStory> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = stories.FoundStories;
geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress;
starsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus;
payments.starsRevenueStats#c92bb73b revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats;
payments.starsRevenueStats#6c207376 flags:# top_hours_graph:flags.0?StatsGraph revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats;
payments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl;
payments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAdsAccountUrl;
inputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction;
@ -1388,10 +1386,10 @@ starsSubscription#2e6eab1a flags:# canceled:flags.0?true can_refulfill:flags.1?t
messageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor;
starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true stars:long yearly_boosts:int store_product:flags.2?string currency:string amount:long winners:Vector<StarsGiveawayWinnersOption> = StarsGiveawayOption;
starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption;
starGift#c62aca28 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string = StarGift;
starGiftUnique#6411db89 flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector<StarGiftAttribute> availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long = StarGift;
starGift#7f853c12 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer = StarGift;
starGiftUnique#f63778ae flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector<StarGiftAttribute> availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift;
payments.starGiftsNotModified#a388a368 = payments.StarGifts;
payments.starGifts#901689ea hash:int gifts:Vector<StarGift> = payments.StarGifts;
payments.starGifts#2ed82995 hash:int gifts:Vector<StarGift> chats:Vector<Chat> users:Vector<User> = payments.StarGifts;
messageReportOption#7903e3d9 text:string option:bytes = MessageReportOption;
reportResultChooseOption#f0e4e0b6 title:string options:Vector<MessageReportOption> = ReportResult;
reportResultAddComment#6f09ac31 flags:# optional:flags.0?true option:bytes = ReportResult;
@ -1404,6 +1402,7 @@ connectedBotStarRef#19a13f71 flags:# revoked:flags.1?true url:string date:int bo
payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector<ConnectedBotStarRef> users:Vector<User> = payments.ConnectedStarRefBots;
payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector<StarRefProgram> users:Vector<User> next_offset:flags.0?string = payments.SuggestedStarRefBots;
starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount;
starsTonAmount#74aee3e0 amount:long = StarsAmount;
messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers;
messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector<Document> = messages.FoundStickers;
botVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true icon:long company:string custom_description:flags.0?string = BotVerifierSettings;
@ -1445,6 +1444,7 @@ pendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:
todoItem#cba9a52f id:int title:TextWithEntities = TodoItem;
todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList;
todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion;
suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
@ -1511,6 +1511,7 @@ account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessC
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
account.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;
account.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;
@ -1535,9 +1536,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;
messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool;
messages.sendMessage#fbf2340a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates;
messages.sendMedia#a550cd78 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates;
messages.forwardMessages#38f0188c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates;
messages.sendMessage#fe05dc9a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;
messages.sendMedia#ac55d9c1 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;
messages.forwardMessages#978928ca flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates;
messages.reportSpam#cf1592db peer:InputPeer = Bool;
messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings;
messages.report#fc78af9b peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult;
@ -1571,7 +1572,7 @@ messages.sendInlineBotResult#c0cf7646 flags:# silent:flags.5?true background:fla
messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates;
messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
messages.saveDraft#d372c5ce flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long = Bool;
messages.saveDraft#54ae308e flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost = Bool;
messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers;
messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;
messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers;
@ -1674,6 +1675,7 @@ messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.Pr
messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool;
messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates;
messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates;
messages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
@ -1760,11 +1762,11 @@ payments.applyGiftCode#f6e26854 slug:string = Updates;
payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo;
payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates;
payments.getStarsTopupOptions#c00ec7d3 = Vector<StarsTopupOption>;
payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus;
payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;
payments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus;
payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;
payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult;
payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;
payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;
payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus;
payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool;
@ -1814,8 +1816,6 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats;
stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards;
stats.getBroadcastRevenueStats#f788ee19 flags:# dark:flags.0?true peer:InputPeer = stats.BroadcastRevenueStats;
stats.getBroadcastRevenueWithdrawalUrl#9df4faad peer:InputPeer password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl;
chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;
chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;
chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;

View File

@ -61,6 +61,7 @@
"account.resolveBusinessChatLink",
"account.toggleSponsoredMessages",
"account.getCollectibleEmojiStatuses",
"account.toggleNoPaidMessagesException",
"account.toggleNoPaidMessagesException ",
"account.getPaidMessagesRevenue",
"account.getAccountTTL",
@ -225,6 +226,7 @@
"messages.getSponsoredMessages",
"messages.reportMessagesDelivery",
"messages.getPreparedInlineMessage",
"messages.toggleSuggestedPostApproval",
"messages.toggleTodoCompleted",
"messages.appendTodoList",
"updates.getState",

View File

@ -117,7 +117,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto;
messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;
message#eabcdd4d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long = Message;
message#9815cec8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector<RestrictionReason> ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost = Message;
messageService#7a800e0a flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;
messageMediaEmpty#3ded6320 = MessageMedia;
@ -192,6 +192,10 @@ messageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags
messageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction;
messageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction;
messageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction;
messageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction;
messageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction;
messageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction;
messageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction;
dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog;
dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog;
@ -429,7 +433,6 @@ updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Messag
updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update;
updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector<int> qts:int = Update;
updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update;
updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update;
updateStarsBalance#4e80a379 balance:StarsAmount = Update;
updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update;
updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update;
@ -439,6 +442,7 @@ updateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update;
updateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector<bytes> next_offset:int = Update;
updateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update;
updateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update;
updateMonoForumNoPaidException#9f812b08 flags:# exception:flags.0?true channel_id:long saved_peer_id:Peer = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -648,6 +652,7 @@ inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet;
inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet;
inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet;
inputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet;
inputStickerSetTonGifts#1cf671a0 = InputStickerSet;
stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true creator:flags.11?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector<PhotoSize> thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet;
@ -826,7 +831,7 @@ contacts.topPeers#70b772a8 categories:Vector<TopPeerCategoryPeers> chats:Vector<
contacts.topPeersDisabled#b52c939d = contacts.TopPeers;
draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage;
draftMessage#2d65321f flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long = DraftMessage;
draftMessage#96eaa5eb flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost = DraftMessage;
messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers;
messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;
@ -1206,7 +1211,7 @@ chatOnlines#f041e250 onlines:int = ChatOnlines;
statsURL#47a971e0 url:string = StatsURL;
chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights;
chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true manage_direct_messages:flags.17?true = ChatAdminRights;
chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights;
@ -1824,23 +1829,11 @@ channels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options:
channels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult;
channels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult;
stats.broadcastRevenueStats#5407e297 top_hours_graph:StatsGraph revenue_graph:StatsGraph balances:BroadcastRevenueBalances usd_rate:double = stats.BroadcastRevenueStats;
stats.broadcastRevenueWithdrawalUrl#ec659737 url:string = stats.BroadcastRevenueWithdrawalUrl;
broadcastRevenueTransactionProceeds#557e2cc4 amount:long from_date:int to_date:int = BroadcastRevenueTransaction;
broadcastRevenueTransactionWithdrawal#5a590978 flags:# pending:flags.0?true failed:flags.2?true amount:long date:int provider:string transaction_date:flags.1?int transaction_url:flags.1?string = BroadcastRevenueTransaction;
broadcastRevenueTransactionRefund#42d30d2e amount:long date:int provider:string = BroadcastRevenueTransaction;
stats.broadcastRevenueTransactions#87158466 count:int transactions:Vector<BroadcastRevenueTransaction> = stats.BroadcastRevenueTransactions;
reactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom;
reactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom;
reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings;
broadcastRevenueBalances#c3ff71e7 flags:# withdrawal_enabled:flags.0?true current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances;
availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect;
messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects;
@ -1859,7 +1852,7 @@ starsTransactionPeerAPI#f9677aad = StarsTransactionPeer;
starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption;
starsTransaction#a39fd94a flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int = StarsTransaction;
starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector<MessageMedia> subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction;
payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;
@ -1871,7 +1864,7 @@ geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:f
starsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus;
payments.starsRevenueStats#c92bb73b revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats;
payments.starsRevenueStats#6c207376 flags:# top_hours_graph:flags.0?StatsGraph revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats;
payments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl;
@ -1897,11 +1890,11 @@ starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true
starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption;
starGift#c62aca28 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string = StarGift;
starGiftUnique#6411db89 flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector<StarGiftAttribute> availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long = StarGift;
starGift#7f853c12 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer = StarGift;
starGiftUnique#f63778ae flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector<StarGiftAttribute> availability_issued:int availability_total:int gift_address:flags.3?string resell_stars:flags.4?long released_by:flags.5?Peer = StarGift;
payments.starGiftsNotModified#a388a368 = payments.StarGifts;
payments.starGifts#901689ea hash:int gifts:Vector<StarGift> = payments.StarGifts;
payments.starGifts#2ed82995 hash:int gifts:Vector<StarGift> chats:Vector<Chat> users:Vector<User> = payments.StarGifts;
messageReportOption#7903e3d9 text:string option:bytes = MessageReportOption;
@ -1924,6 +1917,7 @@ payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector<Connected
payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector<StarRefProgram> users:Vector<User> next_offset:flags.0?string = payments.SuggestedStarRefBots;
starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount;
starsTonAmount#74aee3e0 amount:long = StarsAmount;
messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers;
messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector<Document> = messages.FoundStickers;
@ -1993,6 +1987,8 @@ todoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:fla
todoCompletion#4cc120b7 id:int completed_by:long date:int = TodoCompletion;
suggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -2004,7 +2000,7 @@ invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;
invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X;
invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X;
invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X;
invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X;
invokeWithApnsSecret#dae54f8 {X:Type} nonce:string secret:string query:!X = X;
invokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X;
auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode;
@ -2189,9 +2185,9 @@ messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?t
messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;
messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;
messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool;
messages.sendMessage#fbf2340a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates;
messages.sendMedia#a550cd78 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates;
messages.forwardMessages#38f0188c flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long = Updates;
messages.sendMessage#fe05dc9a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;
messages.sendMedia#ac55d9c1 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;
messages.forwardMessages#978928ca flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates;
messages.reportSpam#cf1592db peer:InputPeer = Bool;
messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings;
messages.report#fc78af9b peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult;
@ -2241,7 +2237,7 @@ messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_me
messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;
messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;
messages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;
messages.saveDraft#d372c5ce flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long = Bool;
messages.saveDraft#54ae308e flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost = Bool;
messages.getAllDrafts#6a3f8d65 = Updates;
messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers;
messages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;
@ -2409,6 +2405,7 @@ messages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:
messages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool;
messages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates;
messages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates;
messages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
@ -2567,14 +2564,14 @@ payments.applyGiftCode#f6e26854 slug:string = Updates;
payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo;
payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates;
payments.getStarsTopupOptions#c00ec7d3 = Vector<StarsTopupOption>;
payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus;
payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;
payments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus;
payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;
payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult;
payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;
payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer = payments.StarsRevenueStats;
payments.getStarsRevenueWithdrawalUrl#13bbe8b3 peer:InputPeer stars:long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl;
payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true ton:flags.1?true peer:InputPeer = payments.StarsRevenueStats;
payments.getStarsRevenueWithdrawalUrl#2433dc92 flags:# ton:flags.0?true peer:InputPeer amount:flags.1?long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl;
payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl;
payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;
payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus;
payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool;
@ -2667,9 +2664,6 @@ stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:s
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats;
stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards;
stats.getBroadcastRevenueStats#f788ee19 flags:# dark:flags.0?true peer:InputPeer = stats.BroadcastRevenueStats;
stats.getBroadcastRevenueWithdrawalUrl#9df4faad peer:InputPeer password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl;
stats.getBroadcastRevenueTransactions#70990b6d peer:InputPeer offset:int limit:int = stats.BroadcastRevenueTransactions;
chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;
chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;

View File

@ -16,6 +16,7 @@ import type {
ApiFakeType,
ApiFormattedText,
ApiInputReplyInfo,
ApiInputSuggestedPostInfo,
ApiLabeledPrice,
ApiLanguage,
ApiMediaFormat,
@ -40,6 +41,7 @@ import type {
ApiTopic,
ApiTypingStatus,
ApiVideo,
MediaContent,
StarGiftAttributeIdModel,
} from '../api/types';
import type { DC_IDS } from '../config';
@ -726,6 +728,7 @@ export type SendMessageParams = {
text?: string;
entities?: ApiMessageEntity[];
replyInfo?: ApiInputReplyInfo;
suggestedPostInfo?: ApiInputSuggestedPostInfo;
attachment?: ApiAttachment;
sticker?: ApiSticker;
story?: ApiStory | ApiStorySkipped;
@ -755,6 +758,7 @@ export type SendMessageParams = {
isForwarding?: boolean;
forwardParams?: ForwardMessagesParams;
isStoryReply?: boolean;
suggestedMedia?: MediaContent;
};
export type ForwardedLocalMessagesSlice = {

View File

@ -1538,6 +1538,38 @@ export interface LangPair {
'MonoforumComposerPlaceholder': undefined;
'ChannelSendMessage': undefined;
'AutomaticTranslation': undefined;
'ComposerEmbeddedMessageSuggestedPostTitle': undefined;
'ComposerEmbeddedMessageSuggestedPostDescription': undefined;
'ActionSuggestedPostOutgoing': undefined;
'ActionSuggestedChangesPrice': undefined;
'ActionSuggestedChangesText': undefined;
'ActionSuggestedChangesTime': undefined;
'ActionSuggestedChangesMedia': undefined;
'TitlePrice': undefined;
'TitleTime': undefined;
'TitleSuggestMessage': undefined;
'TitleSuggestedChanges': undefined;
'EnterPriceInStars': undefined;
'SuggestMessageDateTimeHint': undefined;
'TitleAnytime': undefined;
'ButtonOfferFree': undefined;
'ButtonUpdateTerms': undefined;
'InputPlaceholderPrice': undefined;
'SuggestedPostApprove': undefined;
'SuggestedPostDecline': undefined;
'SuggestedPostSuggestChanges': undefined;
'InputTitleSuggestMessageTime': undefined;
'SuggestedPostApproved': undefined;
'SuggestedPostRejectedNotification': undefined;
'SuggestedPostAgreementReached': undefined;
'CurrencyStars': undefined;
'DeclineReasonPlaceholder': undefined;
'SuggestedPostRejectedYou': undefined;
'SuggestedPostRejectedWithReasonYou': undefined;
'ComposerPlaceholderCaption': undefined;
'SuggestedPostConfirmTitle': undefined;
'ButtonPublish': undefined;
'PublishNow': undefined;
'TitleNewToDoList': undefined;
'TitleEditToDoList': undefined;
'TitleAppendToDoList': undefined;
@ -2539,6 +2571,132 @@ export interface LangPairWithVariables<V = LangVariable> {
'ComposerTitleForwardFrom': {
'users': V;
};
'TitleSuggestedPostAmountForAnyTime': {
'amount': V;
};
'ActionSuggestedPostIncoming': {
'user': V;
};
'ActionSuggestedChangesOutgoing': {
'changes': V;
};
'ActionSuggestedChangesIncoming': {
'user': V;
'changes': V;
};
'SuggestMessagePriceDescription': {
'currency': V;
};
'SuggestMessageTimeDescription': {
'hint': V;
'duration': V;
};
'ButtonOfferAmount': {
'amount': V;
};
'SuggestedPostPublishSchedule': {
'peer': V;
'date': V;
};
'SuggestedPostPublishScheduleYou': {
'peer': V;
'date': V;
};
'SuggestedPostPublished': {
'peer': V;
'date': V;
};
'SuggestedPostPublishedYou': {
'peer': V;
'date': V;
};
'SuggestedPostCharged': {
'user': V;
'amount': V;
};
'SuggestedPostChargedYou': {
'amount': V;
};
'SuggestedPostReceiveAmount': {
'peer': V;
'currency': V;
'duration': V;
};
'SuggestedPostReceiveAmountYou': {
'peer': V;
'currency': V;
'duration': V;
};
'SuggestedPostRefund': {
'peer': V;
'duration': V;
};
'SuggestedPostRefundYou': {
'peer': V;
'duration': V;
'currency': V;
};
'SuggestedPostBalanceTooLow': {
'peer': V;
'currency': V;
};
'SuggestedPostRefundedByUser': {
'channel': V;
'amount': V;
'user': V;
};
'SuggestedPostRefundedByChannel': {
'amount': V;
'peer': V;
'channel': V;
};
'DeclinePostDialogQuestion': {
'sender': V;
};
'SuggestedPostRejected': {
'peer': V;
};
'SuggestedPostRejectedWithReason': {
'peer': V;
};
'SuggestedPostRejectedComment': {
'comment': V;
};
'ActionSuggestedPostSuccess': {
'channel': V;
'amount': V;
};
'DescriptionSuggestedPostMinimumOffer': {
'amount': V;
};
'SuggestedPostConfirmMessage': {
'peer': V;
};
'SuggestedPostConfirmDetailsAdmin': {
'amount': V;
'commission': V;
'duration': V;
};
'SuggestedPostConfirmDetailsUser': {
'amount': V;
'commission': V;
'duration': V;
};
'SuggestedPostConfirmDetailsWithTimeAdmin': {
'amount': V;
'commission': V;
'time': V;
'duration': V;
};
'SuggestedPostConfirmDetailsWithTimeUser': {
'amount': V;
'commission': V;
'time': V;
'duration': V;
};
'ButtonPublishAtTime': {
'time': V;
};
'TitleUserToDoList': {
'peer': V;
};
@ -2595,6 +2753,10 @@ export interface LangPairWithVariables<V = LangVariable> {
'HintTodoListTasksCount': {
'count': V;
};
'GiftInfoCollectibleBy': {
'number': V;
'owner': V;
};
}
export interface LangPairPlural {

View File

@ -227,6 +227,21 @@ export function formatTimeDuration(lang: OldLangFn, duration: number, showLast =
return out.map((part) => lang(part.type, part.duration, 'i')).join(', ');
}
export function formatScheduledDateTime(
scheduleDateTimestamp: number,
lang: LangFn,
oldLang: OldLangFn,
): string {
const scheduleDate = new Date(scheduleDateTimestamp * 1000);
return lang('FormatDateAtTime', {
date: isToday(scheduleDate)
? lang('WeekdayToday')
: formatHumanDate(oldLang, scheduleDateTimestamp * 1000, true, false, true),
time: formatTime(oldLang, scheduleDateTimestamp * 1000),
});
}
export function formatHumanDate(
lang: OldLangFn,
datetime: number | Date,
@ -407,7 +422,7 @@ export function formatDateAtTime(
return lang('formatDateAtTime', [formattedDate, time]);
}
export function formatShortDuration(lang: LangFn, duration: number) {
export function formatShortDuration(lang: LangFn, duration: number, hoursPriority?: boolean) {
if (duration < 0) {
return lang('RightNow');
}
@ -422,7 +437,7 @@ export function formatShortDuration(lang: LangFn, duration: number) {
return lang('Minutes', { count }, { pluralValue: count });
}
if (duration < 60 * 60 * 24) {
if (duration < 60 * 60 * 24 || (hoursPriority && duration <= 60 * 60 * 24 * 2)) {
const count = Math.ceil(duration / (60 * 60));
return lang('Hours', { count }, { pluralValue: count });
}