Poll: Support new features (#6819)
This commit is contained in:
parent
d4138b0ebd
commit
fc0e52e908
@ -30,8 +30,8 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
|
||||
|
||||
- **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`).
|
||||
- Prefix boolean variables with primary or modal auxiliaries (e.g. `isOpen`, `willUpdate`, `shouldRender`).
|
||||
- Functions should start with a verb (e.g. `openModal`, `closeDialog`, `handleClick`).
|
||||
- Prefer checking required parameter before calling a function, avoid making it optional and checking at the beginning of the function.
|
||||
- Only leave comments for complex logic.
|
||||
- Do not use `null`. There's linter rule to enforce it.
|
||||
@ -160,7 +160,7 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
|
||||
### 1. Basics & Imports
|
||||
|
||||
* All components use JSX and render with Teact.
|
||||
* Only import from `'react'` when you need React **types** that are not provided in Teact.
|
||||
* Do not import "react". React types are available globally in React namespace (e.g. React.MouseEvent).
|
||||
* Built-in hooks live in Teact library. Import them from there.
|
||||
|
||||
### 2. Props & Types
|
||||
@ -171,6 +171,7 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
|
||||
* 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.
|
||||
* Do not pass unmemoized objects as props into memo() components.
|
||||
|
||||
### 3. Hooks
|
||||
* **useLastCallback** is your go-to for callbacks, since it won't trigger re-renders and always uses the latest scope.
|
||||
|
||||
@ -23,7 +23,6 @@ import type {
|
||||
import { int2hex } from '../../../util/colors';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { addDocumentToLocalDb } from '../helpers/localDb';
|
||||
import { buildApiFormattedText } from './common';
|
||||
import { buildApiCurrencyAmount } from './payments';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
@ -73,8 +72,6 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
||||
background,
|
||||
} = starGift;
|
||||
|
||||
addDocumentToLocalDb(starGift.sticker);
|
||||
|
||||
const sticker = buildStickerFromDocument(starGift.sticker)!;
|
||||
|
||||
return {
|
||||
@ -138,8 +135,6 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addDocumentToLocalDb(attribute.document);
|
||||
|
||||
return {
|
||||
type: 'model',
|
||||
name: attribute.name,
|
||||
@ -154,8 +149,6 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addDocumentToLocalDb(attribute.document);
|
||||
|
||||
return {
|
||||
type: 'pattern',
|
||||
name: attribute.name,
|
||||
@ -349,10 +342,6 @@ export function buildApiStarGiftCollection(collection: GramJs.StarGiftCollection
|
||||
|
||||
const { collectionId, title, icon, giftsCount, hash } = collection;
|
||||
|
||||
if (icon) {
|
||||
addDocumentToLocalDb(icon);
|
||||
}
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
title,
|
||||
|
||||
@ -7,7 +7,7 @@ import { toJSNumber } from '../../../util/numbers';
|
||||
import { buildApiBotApp } from './bots';
|
||||
import { buildApiFormattedText, buildApiPhoto } from './common';
|
||||
import { buildApiStarGift } from './gifts';
|
||||
import { buildTodoItem } from './messageContent';
|
||||
import { buildPollAnswer, buildTodoItem } from './messageContent';
|
||||
import { buildApiCurrencyAmount } from './payments';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
|
||||
@ -525,6 +525,26 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
|
||||
items: list.map(buildTodoItem),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPollAppendAnswer) {
|
||||
const answer = buildPollAnswer(action.answer);
|
||||
if (!answer) return UNSUPPORTED_ACTION;
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'pollAppendAnswer',
|
||||
answer,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPollDeleteAnswer) {
|
||||
const answer = buildPollAnswer(action.answer);
|
||||
if (!answer) return UNSUPPORTED_ACTION;
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'pollDeleteAnswer',
|
||||
answer,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionStarGiftPurchaseOffer) {
|
||||
const {
|
||||
accepted, declined, gift, price, expiresAt,
|
||||
|
||||
@ -13,11 +13,14 @@ import type {
|
||||
ApiMediaExtendedPreview,
|
||||
ApiMediaInvoice,
|
||||
ApiMediaTodo,
|
||||
ApiMessagePoll,
|
||||
ApiMessageStoryData,
|
||||
ApiMessageWebPage,
|
||||
ApiPaidMedia,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiPollAnswer,
|
||||
ApiPollResults,
|
||||
ApiStarGiftUnique,
|
||||
ApiSticker,
|
||||
ApiTodoItem,
|
||||
@ -43,7 +46,7 @@ import {
|
||||
} from '../../../config';
|
||||
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
||||
import { generateWaveform } from '../../../util/generateWaveform';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { buildCollectionByKey, pick } from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import {
|
||||
addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext,
|
||||
@ -77,7 +80,7 @@ export function buildMessageContent(
|
||||
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
||||
|
||||
if (mtpMessage.message && !hasUnsupportedMedia
|
||||
&& !content.sticker && !content.pollId && !content.todo && !content.contact && !content.video?.isRound) {
|
||||
&& !content.sticker && !content.todo && !content.contact && !content.video?.isRound) {
|
||||
const text = buildMessageTextContent(mtpMessage.message, mtpMessage.entities);
|
||||
const textWithTimestamps = addTimestampEntities(text);
|
||||
content = {
|
||||
@ -559,12 +562,12 @@ function buildPollIdFromMedia(media: GramJs.TypeMessageMedia): string | undefine
|
||||
return media.poll.id.toString();
|
||||
}
|
||||
|
||||
export function buildPollFromMedia(media: GramJs.TypeMessageMedia): ApiPoll | undefined {
|
||||
export function buildMessagePollFromMedia(media: GramJs.TypeMessageMedia): ApiMessagePoll | undefined {
|
||||
if (!(media instanceof GramJs.MessageMediaPoll)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildPoll(media.poll, media.results);
|
||||
return buildMessagePoll(media);
|
||||
}
|
||||
|
||||
function buildTodoFromMedia(media: GramJs.TypeMessageMedia): ApiMediaTodo | undefined {
|
||||
@ -764,31 +767,53 @@ export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessag
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): ApiPoll {
|
||||
const { id, answers: rawAnswers } = poll;
|
||||
const answers = rawAnswers
|
||||
.filter((answer): answer is GramJs.PollAnswer => answer instanceof GramJs.PollAnswer)
|
||||
.map((answer) => ({
|
||||
text: buildApiFormattedText(answer.text),
|
||||
option: serializeBytes(answer.option),
|
||||
}));
|
||||
export function buildMessagePoll(media: GramJs.MessageMediaPoll): ApiMessagePoll {
|
||||
const { poll, results, attachedMedia } = media;
|
||||
|
||||
return {
|
||||
mediaType: 'poll',
|
||||
id: String(id),
|
||||
summary: {
|
||||
isPublic: poll.publicVoters,
|
||||
question: buildApiFormattedText(poll.question),
|
||||
...pick(poll, [
|
||||
'closed',
|
||||
'multipleChoice',
|
||||
'quiz',
|
||||
'closePeriod',
|
||||
'closeDate',
|
||||
]),
|
||||
answers,
|
||||
},
|
||||
results: buildPollResults(pollResults),
|
||||
summary: buildPoll(poll),
|
||||
results: buildPollResults(results),
|
||||
attachedMedia: attachedMedia ? buildMessageMediaContent(attachedMedia) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPollAnswer(answer: GramJs.TypePollAnswer): ApiPollAnswer | undefined {
|
||||
if (!(answer instanceof GramJs.PollAnswer)) return undefined;
|
||||
const { text, option, media, addedBy, date } = answer;
|
||||
|
||||
return {
|
||||
text: buildApiFormattedText(text),
|
||||
option: serializeBytes(option),
|
||||
media: media ? buildMessageMediaContent(media) : undefined,
|
||||
addedByPeerId: addedBy ? getApiChatIdFromMtpPeer(addedBy) : undefined,
|
||||
date,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPoll(poll: GramJs.Poll): ApiPoll {
|
||||
const {
|
||||
id, closed, publicVoters, multipleChoice, quiz, closePeriod, closeDate, answers, question, creator,
|
||||
hideResultsUntilClose, revotingDisabled, shuffleAnswers, openAnswers, hash,
|
||||
} = poll;
|
||||
const apiAnswers = answers.map(buildPollAnswer).filter(Boolean);
|
||||
|
||||
return {
|
||||
id: id.toString(),
|
||||
isClosed: closed,
|
||||
isPublic: publicVoters,
|
||||
isMultipleChoice: multipleChoice,
|
||||
isQuiz: quiz,
|
||||
closePeriod,
|
||||
closeDate,
|
||||
isCreator: creator,
|
||||
shouldHideResultsUntilClose: hideResultsUntilClose,
|
||||
isRevoteDisabled: revotingDisabled,
|
||||
shouldShuffleAnswers: shuffleAnswers,
|
||||
question: buildApiFormattedText(question),
|
||||
answers: apiAnswers,
|
||||
hash: hash.toString(),
|
||||
canAddAnswers: openAnswers,
|
||||
};
|
||||
}
|
||||
|
||||
@ -843,26 +868,32 @@ export function buildMediaInvoice(media: GramJs.MessageMediaInvoice): ApiMediaIn
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] {
|
||||
export function buildPollResults(pollResults: GramJs.PollResults): ApiPollResults {
|
||||
const {
|
||||
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min,
|
||||
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min, solutionMedia,
|
||||
} = pollResults;
|
||||
const results = rawResults?.map(({
|
||||
option, chosen, correct, voters,
|
||||
option, chosen, correct, voters, recentVoters: recentAnswerVoters,
|
||||
}) => ({
|
||||
isChosen: chosen,
|
||||
isCorrect: correct,
|
||||
option: serializeBytes(option),
|
||||
votersCount: voters ?? 0,
|
||||
recentVoterIds: recentAnswerVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
|
||||
}));
|
||||
|
||||
if (solutionMedia) {
|
||||
addMediaToLocalDb(solutionMedia);
|
||||
}
|
||||
|
||||
return {
|
||||
isMin: min,
|
||||
totalVoters,
|
||||
recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
|
||||
results,
|
||||
resultByOption: results && buildCollectionByKey(results, 'option'),
|
||||
solution,
|
||||
...(entities && { solutionEntities: entities.map(buildApiMessageEntity) }),
|
||||
solutionEntities: entities?.map(buildApiMessageEntity),
|
||||
solutionMedia: solutionMedia ? buildMessageMediaContent(solutionMedia) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -16,13 +16,14 @@ import type {
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiMessageForwardInfo,
|
||||
ApiMessagePoll,
|
||||
ApiMessageReportResult,
|
||||
ApiMessageThreadInfo,
|
||||
ApiNewMediaTodo,
|
||||
ApiNewPoll,
|
||||
ApiPeer,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiPollResult,
|
||||
ApiPreparedInlineMessage,
|
||||
ApiQuickReply,
|
||||
ApiReplyInfo,
|
||||
@ -49,7 +50,7 @@ import {
|
||||
} from '../../../config';
|
||||
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
||||
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
||||
import { omitUndefined, pick } from '../../../util/iteratees';
|
||||
import { omitUndefined } from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { interpolateArray } from '../../../util/waveform';
|
||||
@ -418,16 +419,35 @@ export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck {
|
||||
};
|
||||
}
|
||||
|
||||
function buildNewPoll(poll: ApiNewPoll, localId: number): ApiPoll {
|
||||
function buildNewLocalPoll(poll: ApiNewPoll): ApiMessagePoll {
|
||||
const resultByOption = poll.correctAnswers?.length
|
||||
? poll.summary.answers.reduce((acc, answer, index) => {
|
||||
const isCorrect = poll.correctAnswers?.includes(index);
|
||||
|
||||
acc[answer.option] = {
|
||||
option: answer.option,
|
||||
votersCount: 0,
|
||||
isCorrect: isCorrect ? true : undefined,
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, ApiPollResult>)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
mediaType: 'poll',
|
||||
id: String(localId),
|
||||
summary: pick(poll.summary, ['question', 'answers']),
|
||||
results: {},
|
||||
summary: poll.summary,
|
||||
results: {
|
||||
resultByOption,
|
||||
solution: poll.solution,
|
||||
solutionEntities: poll.solutionEntities,
|
||||
solutionMedia: poll.solutionMedia ? buildUploadingMedia(poll.solutionMedia) : undefined,
|
||||
},
|
||||
attachedMedia: poll.attachedMedia ? buildUploadingMedia(poll.attachedMedia) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function buildNewTodo(todo: ApiNewMediaTodo): ApiMediaTodo {
|
||||
function buildNewLocalTodo(todo: ApiNewMediaTodo): ApiMediaTodo {
|
||||
return {
|
||||
mediaType: 'todo',
|
||||
todo: todo.todo,
|
||||
@ -487,8 +507,8 @@ export function buildLocalMessage({
|
||||
|
||||
const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum);
|
||||
|
||||
const localPoll = poll && buildNewPoll(poll, localId);
|
||||
const localTodo = todo && buildNewTodo(todo);
|
||||
const localPoll = poll && buildNewLocalPoll(poll);
|
||||
const localTodo = todo && buildNewLocalTodo(todo);
|
||||
|
||||
const localDice = dice ? {
|
||||
mediaType: 'dice',
|
||||
@ -510,7 +530,7 @@ export function buildLocalMessage({
|
||||
video: gif || media?.video,
|
||||
contact,
|
||||
storyData: story && { mediaType: 'storyData', ...story },
|
||||
pollId: localPoll?.id,
|
||||
pollId: localPoll?.summary.id,
|
||||
todo: localTodo,
|
||||
dice: localDice,
|
||||
}),
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
|
||||
import { LOTTIE_STICKER_MIME_TYPE, VIDEO_STICKER_MIME_TYPE } from '../../../config';
|
||||
import { compact } from '../../../util/iteratees';
|
||||
import { addDocumentToLocalDb } from '../helpers/localDb';
|
||||
import localDb from '../localDb';
|
||||
import { buildApiPhotoPreviewSizes, buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
||||
|
||||
@ -15,6 +16,8 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument,
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addDocumentToLocalDb(document);
|
||||
|
||||
const { mimeType, videoThumbs } = document;
|
||||
const stickerAttribute = document.attributes
|
||||
.find((attr: any): attr is GramJs.DocumentAttributeSticker => (
|
||||
@ -202,12 +205,7 @@ export function processStickerResult(stickers: GramJs.TypeDocument[]) {
|
||||
return stickers
|
||||
.map((document) => {
|
||||
if (document instanceof GramJs.Document) {
|
||||
const sticker = buildStickerFromDocument(document);
|
||||
if (sticker) {
|
||||
localDb.documents[String(document.id)] = document;
|
||||
|
||||
return sticker;
|
||||
}
|
||||
return buildStickerFromDocument(document);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
ApiChatFolder,
|
||||
ApiChatReactions,
|
||||
ApiDisallowedGiftsSettings,
|
||||
ApiDocument,
|
||||
ApiEmojiStatusType,
|
||||
ApiFormattedText,
|
||||
ApiGroupCall,
|
||||
@ -15,12 +16,13 @@ import type {
|
||||
ApiInputReplyInfo,
|
||||
ApiInputStorePaymentPurpose,
|
||||
ApiInputSuggestedPostInfo,
|
||||
ApiLocation,
|
||||
ApiMessageEntity,
|
||||
ApiMessagePoll,
|
||||
ApiNewMediaTodo,
|
||||
ApiNewPoll,
|
||||
ApiPhoneCall,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiPremiumGiftCodeOption,
|
||||
ApiPrivacyKey,
|
||||
ApiProfileTab,
|
||||
@ -35,6 +37,7 @@ import type {
|
||||
ApiThemeParameters,
|
||||
ApiTypeCurrencyAmount,
|
||||
ApiVideo,
|
||||
MediaContent,
|
||||
} from '../../types';
|
||||
import {
|
||||
ApiMessageEntityTypes,
|
||||
@ -183,7 +186,11 @@ export function buildInputStickerSetShortName(shortName: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInputDocument(media: ApiSticker | ApiVideo) {
|
||||
export function buildInputDocument(media: ApiSticker | ApiVideo | ApiDocument) {
|
||||
if (!media.id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const document = localDb.documents[media.id];
|
||||
|
||||
if (!document) {
|
||||
@ -197,7 +204,7 @@ export function buildInputDocument(media: ApiSticker | ApiVideo) {
|
||||
]));
|
||||
}
|
||||
|
||||
export function buildInputMediaDocument(media: ApiSticker | ApiVideo, spoiler?: true) {
|
||||
export function buildInputMediaDocument(media: ApiSticker | ApiVideo | ApiDocument, spoiler?: true) {
|
||||
const inputDocument = buildInputDocument(media);
|
||||
|
||||
if (!inputDocument) {
|
||||
@ -207,48 +214,47 @@ export function buildInputMediaDocument(media: ApiSticker | ApiVideo, spoiler?:
|
||||
return new GramJs.InputMediaDocument({ id: inputDocument, spoiler });
|
||||
}
|
||||
|
||||
export function buildInputPoll(pollParams: ApiNewPoll, randomId: bigint) {
|
||||
const { summary, quiz } = pollParams;
|
||||
export function buildInputPoll(
|
||||
pollParams: ApiNewPoll,
|
||||
randomId: bigint,
|
||||
media?: {
|
||||
attachedMedia?: GramJs.TypeInputMedia;
|
||||
solutionMedia?: GramJs.TypeInputMedia;
|
||||
},
|
||||
) {
|
||||
const { summary: poll, correctAnswers, solution, solutionEntities } = pollParams;
|
||||
|
||||
const poll = new GramJs.Poll({
|
||||
const inputPoll = new GramJs.Poll({
|
||||
id: randomId,
|
||||
publicVoters: summary.isPublic,
|
||||
question: buildInputTextWithEntities(summary.question),
|
||||
answers: summary.answers.map(({ text, option }) => {
|
||||
publicVoters: poll.isPublic,
|
||||
question: buildInputTextWithEntities(poll.question),
|
||||
answers: poll.answers.map(({ text, option }) => {
|
||||
return new GramJs.PollAnswer({
|
||||
text: buildInputTextWithEntities(text),
|
||||
option: deserializeBytes(option),
|
||||
});
|
||||
}),
|
||||
quiz: summary.quiz,
|
||||
multipleChoice: summary.multipleChoice,
|
||||
quiz: poll.isQuiz,
|
||||
multipleChoice: poll.isMultipleChoice,
|
||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
||||
});
|
||||
|
||||
if (!quiz) {
|
||||
return new GramJs.InputMediaPoll({ poll });
|
||||
}
|
||||
|
||||
const correctAnswers = quiz.correctAnswers.map((correctOption) => {
|
||||
return summary.answers.findIndex((a) => a.option === correctOption);
|
||||
}).filter((i) => i !== -1);
|
||||
const { solution } = quiz;
|
||||
const solutionEntities = quiz.solutionEntities ? quiz.solutionEntities.map(buildMtpMessageEntity) : [];
|
||||
const inputSolutionEntities = solutionEntities?.map(buildMtpMessageEntity);
|
||||
|
||||
return new GramJs.InputMediaPoll({
|
||||
poll,
|
||||
poll: inputPoll,
|
||||
correctAnswers,
|
||||
...(solution && {
|
||||
solution,
|
||||
solutionEntities,
|
||||
}),
|
||||
attachedMedia: media?.attachedMedia,
|
||||
solution,
|
||||
solutionEntities: inputSolutionEntities,
|
||||
solutionMedia: media?.solutionMedia,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
|
||||
export function buildInputPollFromExisting(poll: ApiMessagePoll, shouldClose = false) {
|
||||
return new GramJs.InputMediaPoll({
|
||||
poll: new GramJs.Poll({
|
||||
id: BigInt(poll.id),
|
||||
id: BigInt(poll.summary.id),
|
||||
publicVoters: poll.summary.isPublic,
|
||||
question: buildInputTextWithEntities(poll.summary.question),
|
||||
answers: poll.summary.answers.map(({ text, option }) => {
|
||||
@ -257,18 +263,95 @@ export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
|
||||
option: deserializeBytes(option),
|
||||
});
|
||||
}),
|
||||
quiz: poll.summary.quiz,
|
||||
multipleChoice: poll.summary.multipleChoice,
|
||||
quiz: poll.summary.isQuiz,
|
||||
multipleChoice: poll.summary.isMultipleChoice,
|
||||
closeDate: poll.summary.closeDate,
|
||||
closePeriod: poll.summary.closePeriod,
|
||||
closed: shouldClose ? true : poll.summary.closed,
|
||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
||||
closed: shouldClose ? true : poll.summary.isClosed,
|
||||
creator: poll.summary.isCreator,
|
||||
revotingDisabled: poll.summary.isRevoteDisabled,
|
||||
shuffleAnswers: poll.summary.shouldShuffleAnswers,
|
||||
hideResultsUntilClose: poll.summary.shouldHideResultsUntilClose,
|
||||
openAnswers: poll.summary.canAddAnswers,
|
||||
hash: BigInt(poll.summary.hash),
|
||||
}),
|
||||
correctAnswers: poll.results.results
|
||||
?.map((result, index) => (result.isCorrect ? index : -1))
|
||||
.filter((i) => i !== -1),
|
||||
correctAnswers: poll.summary.answers.map((answer, index) => {
|
||||
const result = poll.results.resultByOption?.[answer.option];
|
||||
return result?.isCorrect ? index : -1;
|
||||
}).filter((i) => i !== -1),
|
||||
attachedMedia: buildInputMediaFromContent(poll.attachedMedia),
|
||||
solution: poll.results.solution,
|
||||
solutionEntities: poll.results.solutionEntities?.map(buildMtpMessageEntity),
|
||||
solutionMedia: buildInputMediaFromContent(poll.results.solutionMedia),
|
||||
});
|
||||
}
|
||||
|
||||
function buildInputMediaFromContent(content?: MediaContent) {
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (content.photo) {
|
||||
const inputPhoto = buildInputPhoto(content.photo);
|
||||
return inputPhoto ? new GramJs.InputMediaPhoto({
|
||||
id: inputPhoto,
|
||||
spoiler: content.photo.isSpoiler || undefined,
|
||||
}) : undefined;
|
||||
}
|
||||
|
||||
if (content.video) {
|
||||
return buildInputMediaDocument(content.video, content.video.isSpoiler || undefined);
|
||||
}
|
||||
|
||||
if (content.document) {
|
||||
return buildInputMediaDocument(content.document);
|
||||
}
|
||||
|
||||
if (content.location) {
|
||||
return buildInputLocationMedia(content.location);
|
||||
}
|
||||
|
||||
if (content.sticker) {
|
||||
return buildInputMediaDocument(content.sticker);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildInputLocationMedia(location: ApiLocation) {
|
||||
const geoPoint = buildInputGeoPoint(location.geo);
|
||||
|
||||
if (!geoPoint) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (location.mediaType === 'venue') {
|
||||
return new GramJs.InputMediaVenue({
|
||||
geoPoint,
|
||||
title: location.title,
|
||||
address: location.address,
|
||||
provider: location.provider,
|
||||
venueId: location.venueId,
|
||||
venueType: location.venueType,
|
||||
});
|
||||
}
|
||||
|
||||
if (location.mediaType === 'geoLive') {
|
||||
return new GramJs.InputMediaGeoLive({
|
||||
geoPoint,
|
||||
heading: location.heading,
|
||||
period: location.period,
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.InputMediaGeoPoint({ geoPoint });
|
||||
}
|
||||
|
||||
function buildInputGeoPoint(geo: ApiLocation['geo']) {
|
||||
return new GramJs.InputGeoPoint({
|
||||
lat: geo.lat,
|
||||
long: geo.long,
|
||||
accuracyRadius: geo.accuracyRadius,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -84,6 +84,12 @@ export function addMediaToLocalDb(media: GramJs.TypeMessageMedia, context?: Medi
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (media instanceof GramJs.MessageMediaPoll) {
|
||||
if (media.attachedMedia) {
|
||||
addMediaToLocalDb(media.attachedMedia, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) {
|
||||
@ -117,9 +123,16 @@ export function addPhotoToLocalDb(photo: GramJs.TypePhoto) {
|
||||
}
|
||||
}
|
||||
|
||||
export function addDocumentToLocalDb(document: GramJs.TypeDocument) {
|
||||
export function addDocumentToLocalDb(document: GramJs.TypeDocument & RepairInfo) {
|
||||
if (document instanceof GramJs.Document) {
|
||||
localDb.documents[String(document.id)] = document;
|
||||
const id = String(document.id);
|
||||
const current = localDb.documents[id];
|
||||
if (current && document.accessHash === current.accessHash && document.fileReference === current.fileReference
|
||||
&& !document.localRepairInfo
|
||||
) {
|
||||
return;
|
||||
}
|
||||
localDb.documents[id] = document;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,12 +18,12 @@ import type {
|
||||
ApiInputSuggestedPostInfo,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiMessagePoll,
|
||||
ApiMessageSearchContext,
|
||||
ApiMessageSearchType,
|
||||
ApiNewMediaTodo,
|
||||
ApiOnProgress,
|
||||
ApiPeer,
|
||||
ApiPoll,
|
||||
ApiReaction,
|
||||
ApiSearchPostsFlood,
|
||||
ApiSendMessageAction,
|
||||
@ -64,7 +64,7 @@ import {
|
||||
import { buildApiComposedMessageWithAI, buildApiFormattedText } from '../apiBuilders/common';
|
||||
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
||||
import {
|
||||
buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia,
|
||||
buildMessageMediaContent, buildMessagePollFromMedia, buildMessageTextContent,
|
||||
buildWebPageFromMedia,
|
||||
} from '../apiBuilders/messageContent';
|
||||
import {
|
||||
@ -461,7 +461,28 @@ export function sendApiMessage(
|
||||
} else if (gif) {
|
||||
media = buildInputMediaDocument(gif);
|
||||
} else if (poll) {
|
||||
media = buildInputPoll(poll, randomId);
|
||||
try {
|
||||
const attachedMedia = poll.attachedMedia
|
||||
? await uploadMedia(localMessage, poll.attachedMedia, onProgress!)
|
||||
: undefined;
|
||||
const solutionMedia = poll.solutionMedia
|
||||
? await uploadMedia(localMessage, poll.solutionMedia, onProgress!)
|
||||
: undefined;
|
||||
|
||||
media = buildInputPoll(poll, randomId, {
|
||||
attachedMedia,
|
||||
solutionMedia,
|
||||
});
|
||||
} catch (err) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
await mediaQueue;
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (todo) {
|
||||
media = buildInputTodo(todo);
|
||||
} else if (story) {
|
||||
@ -1856,7 +1877,7 @@ export async function closePoll({
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
poll: ApiPoll;
|
||||
poll: ApiMessagePoll;
|
||||
}) {
|
||||
const { id, accessHash } = chat;
|
||||
|
||||
@ -2498,7 +2519,7 @@ function handleLocalMessageUpdate(
|
||||
}
|
||||
|
||||
let newContent: MediaContent | undefined;
|
||||
let poll: ApiPoll | undefined;
|
||||
let poll: ApiMessagePoll | undefined;
|
||||
let webPage: ApiWebPage | undefined;
|
||||
if (messageUpdate instanceof GramJs.UpdateShortSentMessage) {
|
||||
if (localMessage.content.text && messageUpdate.entities) {
|
||||
@ -2513,7 +2534,7 @@ function handleLocalMessageUpdate(
|
||||
peerId: buildPeer(localMessage.chatId), id: messageUpdate.id,
|
||||
}),
|
||||
};
|
||||
poll = buildPollFromMedia(messageUpdate.media);
|
||||
poll = buildMessagePollFromMedia(messageUpdate.media);
|
||||
webPage = buildWebPageFromMedia(messageUpdate.media);
|
||||
}
|
||||
|
||||
|
||||
@ -114,12 +114,6 @@ export async function fetchAvailableEffects() {
|
||||
|
||||
const documentsMap = new Map(result.documents.map((doc) => [String(doc.id), doc]));
|
||||
|
||||
result.documents.forEach((document) => {
|
||||
if (document instanceof GramJs.Document) {
|
||||
localDb.documents[String(document.id)] = document;
|
||||
}
|
||||
});
|
||||
|
||||
const effects = result.effects.map(buildApiAvailableEffect);
|
||||
|
||||
const stickers: ApiSticker[] = [];
|
||||
@ -127,12 +121,12 @@ export async function fetchAvailableEffects() {
|
||||
|
||||
for (const effect of effects) {
|
||||
if (effect.effectAnimationId) {
|
||||
const document = documentsMap.get(effect.effectStickerId);
|
||||
const document = documentsMap.get(effect.effectAnimationId);
|
||||
const emoji = document && buildStickerFromDocument(document, false, effect.isPremium);
|
||||
if (emoji) emojis.push(emoji);
|
||||
} else {
|
||||
const document = localDb.documents[effect.effectStickerId];
|
||||
const sticker = buildStickerFromDocument(document);
|
||||
const document = documentsMap.get(effect.effectStickerId);
|
||||
const sticker = document && buildStickerFromDocument(document);
|
||||
if (sticker) {
|
||||
stickers.push(sticker);
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiPoll, ApiThreadInfo, ApiUser,
|
||||
ApiChat, ApiMessagePoll, ApiThreadInfo, ApiUser,
|
||||
ApiWebPage,
|
||||
} from '../../types';
|
||||
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { buildPollFromMedia, buildWebPageFromMedia } from '../apiBuilders/messageContent';
|
||||
import { buildMessagePollFromMedia, buildWebPageFromMedia } from '../apiBuilders/messageContent';
|
||||
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers/localDb';
|
||||
@ -24,7 +24,7 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
|
||||
let userById: Record<string, ApiUser> | undefined;
|
||||
let chatById: Record<string, ApiChat> | undefined;
|
||||
const threadInfos: ApiThreadInfo[] | undefined = [];
|
||||
const polls: ApiPoll[] | undefined = [];
|
||||
const polls: ApiMessagePoll[] | undefined = [];
|
||||
const webPages: ApiWebPage[] | undefined = [];
|
||||
|
||||
if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) {
|
||||
@ -57,7 +57,7 @@ export function processAndUpdateEntities(response?: GramJs.AnyRequest['__respons
|
||||
}
|
||||
|
||||
if ('media' in message && message.media) {
|
||||
const poll = buildPollFromMedia(message.media);
|
||||
const poll = buildMessagePollFromMedia(message.media);
|
||||
if (poll) {
|
||||
polls.push(poll);
|
||||
}
|
||||
|
||||
@ -3,7 +3,10 @@ import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gram
|
||||
|
||||
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
|
||||
import {
|
||||
type ApiMessage, type ApiPoll, type ApiStory, type ApiStorySkipped,
|
||||
type ApiMessage,
|
||||
type ApiMessagePoll,
|
||||
type ApiStory,
|
||||
type ApiStorySkipped,
|
||||
type ApiUpdateConnectionStateType,
|
||||
type ApiWebPage,
|
||||
MAIN_THREAD_ID,
|
||||
@ -11,7 +14,7 @@ import {
|
||||
|
||||
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
|
||||
import {
|
||||
omit, pick,
|
||||
omit, omitUndefined, pick,
|
||||
} from '../../../util/iteratees';
|
||||
import { getServerTimeOffset, setServerTimeOffset } from '../../../util/serverTime';
|
||||
import { buildApiBotCommand, buildApiBotMenuButton } from '../apiBuilders/bots';
|
||||
@ -38,8 +41,8 @@ import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||
import {
|
||||
buildApiMessageExtendedMediaPreview,
|
||||
buildBoughtMediaContent,
|
||||
buildMessagePollFromMedia,
|
||||
buildPoll,
|
||||
buildPollFromMedia,
|
||||
buildPollResults,
|
||||
buildWebPage,
|
||||
buildWebPageFromMedia,
|
||||
@ -134,7 +137,7 @@ export function updater(update: Update) {
|
||||
|| update instanceof GramJs.UpdateShortMessage
|
||||
) {
|
||||
let message: ApiMessage | undefined;
|
||||
let poll: ApiPoll | undefined;
|
||||
let poll: ApiMessagePoll | undefined;
|
||||
let webPage: ApiWebPage | undefined;
|
||||
let shouldForceReply: boolean | undefined;
|
||||
|
||||
@ -159,7 +162,7 @@ export function updater(update: Update) {
|
||||
message = buildApiMessage(mtpMessage)!;
|
||||
|
||||
if (mtpMessage instanceof GramJs.Message) {
|
||||
poll = mtpMessage.media && buildPollFromMedia(mtpMessage.media);
|
||||
poll = mtpMessage.media && buildMessagePollFromMedia(mtpMessage.media);
|
||||
webPage = mtpMessage.media && buildWebPageFromMedia(mtpMessage.media);
|
||||
}
|
||||
|
||||
@ -303,7 +306,7 @@ export function updater(update: Update) {
|
||||
if (!message) return;
|
||||
|
||||
const poll = update.message instanceof GramJs.Message && update.message.media
|
||||
? buildPollFromMedia(update.message.media) : undefined;
|
||||
? buildMessagePollFromMedia(update.message.media) : undefined;
|
||||
const webPage = update.message instanceof GramJs.Message && update.message.media
|
||||
? buildWebPageFromMedia(update.message.media) : undefined;
|
||||
|
||||
@ -358,7 +361,7 @@ export function updater(update: Update) {
|
||||
const message = omit(buildApiMessage(mtpMessage)!, ['isOutgoing']) as ApiMessage;
|
||||
|
||||
const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
||||
? buildPollFromMedia(mtpMessage.media) : undefined;
|
||||
? buildMessagePollFromMedia(mtpMessage.media) : undefined;
|
||||
|
||||
const webPage = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
||||
? buildWebPageFromMedia(mtpMessage.media) : undefined;
|
||||
@ -472,22 +475,14 @@ export function updater(update: Update) {
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateMessagePoll) {
|
||||
const { pollId, poll, results } = update;
|
||||
if (poll) {
|
||||
const apiPoll = buildPoll(poll, results);
|
||||
const apiPoll = poll && buildPoll(poll);
|
||||
const pollResults = buildPollResults(results);
|
||||
|
||||
sendApiUpdate({
|
||||
'@type': 'updateMessagePoll',
|
||||
pollId: String(pollId),
|
||||
pollUpdate: apiPoll,
|
||||
});
|
||||
} else {
|
||||
const pollResults = buildPollResults(results);
|
||||
sendApiUpdate({
|
||||
'@type': 'updateMessagePoll',
|
||||
pollId: String(pollId),
|
||||
pollUpdate: { results: pollResults },
|
||||
});
|
||||
}
|
||||
sendApiUpdate({
|
||||
'@type': 'updateMessagePoll',
|
||||
pollId: pollId.toString(),
|
||||
pollUpdate: omitUndefined({ summary: apiPoll, results: pollResults }),
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateMessagePollVote) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateMessagePollVote',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls';
|
||||
import type { ApiBotApp, ApiFormattedText, ApiPhoto, ApiTodoItem } from './messages';
|
||||
import type { ApiBotApp, ApiFormattedText, ApiPhoto, ApiPollAnswer, ApiTodoItem } from './messages';
|
||||
import type { ApiStarGiftRegular, ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars';
|
||||
|
||||
interface ActionMediaType {
|
||||
@ -330,6 +330,16 @@ export interface ApiMessageActionTodoAppendTasks extends ActionMediaType {
|
||||
items: ApiTodoItem[];
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPollAppendAnswer extends ActionMediaType {
|
||||
type: 'pollAppendAnswer';
|
||||
answer: ApiPollAnswer;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPollDeleteAnswer extends ActionMediaType {
|
||||
type: 'pollDeleteAnswer';
|
||||
answer: ApiPollAnswer;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionStarGiftPurchaseOffer extends ActionMediaType {
|
||||
type: 'starGiftPurchaseOffer';
|
||||
isAccepted?: true;
|
||||
@ -388,6 +398,7 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
|
||||
| ApiMessageActionGiftTon | ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique
|
||||
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
|
||||
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
|
||||
| ApiMessageActionTodoAppendTasks | ApiMessageActionStarGiftPurchaseOffer
|
||||
| ApiMessageActionTodoAppendTasks | ApiMessageActionPollAppendAnswer | ApiMessageActionPollDeleteAnswer
|
||||
| ApiMessageActionStarGiftPurchaseOffer
|
||||
| ApiMessageActionStarGiftPurchaseOfferDeclined | ApiMessageActionNewCreatorPending
|
||||
| ApiMessageActionChangeCreator | ApiMessageActionNoForwardsToggle | ApiMessageActionNoForwardsRequest;
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
ApiWebDocument,
|
||||
} from './bots';
|
||||
import type { ApiMessageAction } from './messageActions';
|
||||
import type { ApiPeerNotifySettings, ApiRestrictionReason } from './misc';
|
||||
import type { ApiAttachment, ApiPeerNotifySettings, ApiRestrictionReason } from './misc';
|
||||
import type {
|
||||
ApiLabeledPrice,
|
||||
} from './payments';
|
||||
@ -186,6 +186,9 @@ export type ApiPaidMedia = {
|
||||
export interface ApiPollAnswer {
|
||||
text: ApiFormattedText;
|
||||
option: string;
|
||||
media?: MediaContent;
|
||||
addedByPeerId?: string;
|
||||
date?: number;
|
||||
}
|
||||
|
||||
export interface ApiPollResult {
|
||||
@ -193,29 +196,42 @@ export interface ApiPollResult {
|
||||
isCorrect?: true;
|
||||
option: string;
|
||||
votersCount: number;
|
||||
recentVoterIds?: string[];
|
||||
}
|
||||
|
||||
export interface ApiPoll {
|
||||
mediaType: 'poll';
|
||||
id: string;
|
||||
summary: {
|
||||
closed?: true;
|
||||
isPublic?: true;
|
||||
multipleChoice?: true;
|
||||
quiz?: true;
|
||||
question: ApiFormattedText;
|
||||
answers: ApiPollAnswer[];
|
||||
closePeriod?: number;
|
||||
closeDate?: number;
|
||||
};
|
||||
results: {
|
||||
isMin?: true;
|
||||
results?: ApiPollResult[];
|
||||
totalVoters?: number;
|
||||
recentVoterIds?: string[];
|
||||
solution?: string;
|
||||
solutionEntities?: ApiMessageEntity[];
|
||||
};
|
||||
hash: string;
|
||||
isClosed?: true;
|
||||
isPublic?: true;
|
||||
isMultipleChoice?: true;
|
||||
isQuiz?: true;
|
||||
canAddAnswers?: true;
|
||||
isRevoteDisabled?: true;
|
||||
shouldShuffleAnswers?: true;
|
||||
shouldHideResultsUntilClose?: true;
|
||||
isCreator?: true;
|
||||
question: ApiFormattedText;
|
||||
answers: ApiPollAnswer[];
|
||||
closePeriod?: number;
|
||||
closeDate?: number;
|
||||
}
|
||||
|
||||
export interface ApiPollResults {
|
||||
isMin?: true;
|
||||
resultByOption?: Record<string, ApiPollResult>;
|
||||
totalVoters?: number;
|
||||
recentVoterIds?: string[];
|
||||
solution?: string;
|
||||
solutionEntities?: ApiMessageEntity[];
|
||||
solutionMedia?: MediaContent;
|
||||
}
|
||||
|
||||
export interface ApiMessagePoll {
|
||||
mediaType: 'poll';
|
||||
summary: ApiPoll;
|
||||
results: ApiPollResults;
|
||||
attachedMedia?: MediaContent;
|
||||
}
|
||||
|
||||
export interface ApiInvoice {
|
||||
@ -339,12 +355,12 @@ export type ApiGiveawayResults = {
|
||||
};
|
||||
|
||||
export type ApiNewPoll = {
|
||||
summary: ApiPoll['summary'];
|
||||
quiz?: {
|
||||
correctAnswers: string[];
|
||||
solution?: string;
|
||||
solutionEntities?: ApiMessageEntity[];
|
||||
};
|
||||
summary: ApiPoll;
|
||||
correctAnswers?: number[];
|
||||
solution?: string;
|
||||
solutionEntities?: ApiMessageEntity[];
|
||||
attachedMedia?: ApiAttachment;
|
||||
solutionMedia?: ApiAttachment;
|
||||
};
|
||||
|
||||
export interface ApiTodoItem {
|
||||
@ -666,7 +682,7 @@ export type MediaContainer = {
|
||||
};
|
||||
|
||||
export type StatefulMediaContent = {
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
story?: ApiStory;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -24,9 +24,9 @@ import type {
|
||||
ApiFormattedText,
|
||||
ApiMediaExtendedPreview,
|
||||
ApiMessage,
|
||||
ApiMessagePoll,
|
||||
ApiPaidReactionPrivacyType,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiQuickReply,
|
||||
ApiReaction,
|
||||
ApiReactions,
|
||||
@ -237,7 +237,7 @@ export type ApiUpdateNewScheduledMessage = {
|
||||
id: number;
|
||||
message: ApiMessage;
|
||||
wasDrafted?: boolean;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -248,7 +248,7 @@ export type ApiUpdateNewMessage = {
|
||||
message: ApiMessage;
|
||||
shouldForceReply?: boolean;
|
||||
wasDrafted?: boolean;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -256,7 +256,7 @@ export type ApiUpdateMessage = {
|
||||
'@type': 'updateMessage';
|
||||
chatId: string;
|
||||
id: number;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
shouldForceReply?: boolean;
|
||||
isFromNew?: true;
|
||||
@ -274,7 +274,7 @@ export type ApiUpdateScheduledMessage = {
|
||||
'@type': 'updateScheduledMessage';
|
||||
chatId: string;
|
||||
id: number;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
isFromNew?: true;
|
||||
} & (
|
||||
@ -291,7 +291,7 @@ export type ApiUpdateQuickReplyMessage = {
|
||||
'@type': 'updateQuickReplyMessage';
|
||||
id: number;
|
||||
message: Partial<ApiMessage>;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -335,7 +335,7 @@ export type ApiUpdateScheduledMessageSendSucceeded = {
|
||||
chatId: string;
|
||||
localId: number;
|
||||
message: ApiMessage;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -344,7 +344,7 @@ export type ApiUpdateMessageSendSucceeded = {
|
||||
chatId: string;
|
||||
localId: number;
|
||||
message: ApiMessage;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -385,7 +385,7 @@ export type ApiUpdateChannelMessages = {
|
||||
export type ApiUpdateMessagePoll = {
|
||||
'@type': 'updateMessagePoll';
|
||||
pollId: string;
|
||||
pollUpdate: Partial<ApiPoll>;
|
||||
pollUpdate: Partial<ApiMessagePoll>;
|
||||
};
|
||||
|
||||
export type ApiUpdateMessagePollVote = {
|
||||
@ -895,7 +895,7 @@ export type ApiUpdateEntities = {
|
||||
users?: Record<string, ApiUser>;
|
||||
chats?: Record<string, ApiChat>;
|
||||
threadInfos?: ApiThreadInfo[];
|
||||
polls?: ApiPoll[];
|
||||
polls?: ApiMessagePoll[];
|
||||
webPages?: ApiWebPage[];
|
||||
};
|
||||
|
||||
|
||||
1
src/assets/font-icons/previous-link.svg
Normal file
1
src/assets/font-icons/previous-link.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M7.523 14.376a2.284 2.284 0 0 0-.19 3.016l.19.216 13.71 13.71a2.284 2.284 0 0 0 3.421-3.016l-.19-.216-12.092-12.093L24.463 3.9a2.284 2.284 0 0 0 .19-3.016l-.19-.216a2.284 2.284 0 0 0-3.015-.19l-.215.19z"/></svg>
|
||||
|
After Width: | Height: | Size: 303 B |
@ -810,13 +810,27 @@
|
||||
"CallAgain" = "Call Again";
|
||||
"CallBack" = "Call Back";
|
||||
"CallMessageWithDuration" = "{time} ({duration})";
|
||||
"PollSubmitVotes" = "VOTE";
|
||||
"PollViewResults" = "VIEW RESULTS";
|
||||
"PollSubmitVotes" = "Vote";
|
||||
"PollSubmitAnswers" = "Answer";
|
||||
"PollViewResults" = "View Results";
|
||||
"PollBackToVote" = "< Vote";
|
||||
"PollBackToAnswer" = "< Answer";
|
||||
"PollVoteCountButton_one" = "{count} vote >";
|
||||
"PollVoteCountButton_other" = "{count} votes >";
|
||||
"PollAnswerCountButton_one" = "{count} answered >";
|
||||
"PollAnswerCountButton_other" = "{count} answered >";
|
||||
"PollEndsTime" = "ends {time}";
|
||||
"PollResultsTime" = "results {time}";
|
||||
"ChatQuizTotalVotesEmpty" = "No answers yet";
|
||||
"ChatPollTotalVotesResultEmpty" = "No votes";
|
||||
"Answer_one" = "{count} answer";
|
||||
"Answer_other" = "{count} answers";
|
||||
"PollAnsweredCount_one" = "{count} answered";
|
||||
"PollAnsweredCount_other" = "{count} answered";
|
||||
"Vote" = "Vote";
|
||||
"VoteCount_one" = "{count} vote";
|
||||
"VoteCount_other" = "{count} votes";
|
||||
"TimeIn" = "in {time}";
|
||||
"MessageRecommendedLabel" = "recommended";
|
||||
"SponsoredMessageAd" = "Ad";
|
||||
"SponsoredMessageAdWhatIsThis" = "what's this?";
|
||||
@ -1726,6 +1740,7 @@
|
||||
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
|
||||
"StarsPerMonth" = "⭐️{amount}/month";
|
||||
"StarsBalance" = "Balance";
|
||||
"OpenMapWith" = "Open Map in";
|
||||
"OpenApp" = "Open App";
|
||||
"PopularApps" = "Popular Apps";
|
||||
"SearchApps" = "Search Apps";
|
||||
@ -2376,6 +2391,10 @@
|
||||
"MessageActionAppendTodoYou" = "You added a new task \"{task}\" to {list}";
|
||||
"MessageActionAppendTodoMultiple" = "{peer} added {tasks} to {list}";
|
||||
"MessageActionAppendTodoMultipleYou" = "You added {tasks} to {list}";
|
||||
"MessageActionPollAppendAnswer" = "{peer} added \"{option}\" to the poll.";
|
||||
"MessageActionPollAppendAnswerYou" = "You added \"{option}\" to the poll.";
|
||||
"MessageActionPollDeleteAnswer" = "{peer} removed \"{option}\" from the poll.";
|
||||
"MessageActionPollDeleteAnswerYou" = "You removed \"{option}\" from the poll.";
|
||||
"PremiumMore" = "More";
|
||||
"SubscribeToTelegramPremiumForToggleTask" = "Subscribe to **Telegram Premium** to toggle tasks";
|
||||
"SubscribeToTelegramPremiumForCreateToDo" = "Subscribe to **Telegram Premium** to create Checklists";
|
||||
|
||||
@ -5,6 +5,7 @@ $animation-time: 200ms;
|
||||
|
||||
.root {
|
||||
display: inline-flex;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: pre;
|
||||
|
||||
&[dir="rtl"] {
|
||||
|
||||
36
src/components/common/CompactMapPreview.module.scss
Normal file
36
src/components/common/CompactMapPreview.module.scss
Normal file
@ -0,0 +1,36 @@
|
||||
.root {
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.interactive {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
}
|
||||
|
||||
.map,
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pin {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
63
src/components/common/CompactMapPreview.tsx
Normal file
63
src/components/common/CompactMapPreview.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiGeoPoint } from '../../api/types';
|
||||
|
||||
import { buildStaticMapHash } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio';
|
||||
|
||||
import Skeleton from '../ui/placeholder/Skeleton';
|
||||
|
||||
import styles from './CompactMapPreview.module.scss';
|
||||
|
||||
import mapPin from '../../assets/map-pin.svg';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
geo: ApiGeoPoint;
|
||||
width: number;
|
||||
height: number;
|
||||
zoom?: number;
|
||||
shouldShowPin?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
};
|
||||
|
||||
const DEFAULT_ZOOM = 15;
|
||||
|
||||
const CompactMapPreview = ({
|
||||
className,
|
||||
geo,
|
||||
width,
|
||||
height,
|
||||
zoom = DEFAULT_ZOOM,
|
||||
shouldShowPin = true,
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
const dpr = useDevicePixelRatio();
|
||||
const mediaHash = buildStaticMapHash(geo, width, height, zoom, dpr);
|
||||
const mapBlobUrl = useMedia(mediaHash);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(styles.root, onClick && styles.interactive, className)}
|
||||
style={`width: ${width}px; height: ${height}px;`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{mapBlobUrl ? (
|
||||
<img
|
||||
src={mapBlobUrl}
|
||||
alt=""
|
||||
className={styles.map}
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className={styles.skeleton} width={width} height={height} animation="wave" />
|
||||
)}
|
||||
{shouldShowPin && <img src={mapPin} alt="" className={styles.pin} draggable={false} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CompactMapPreview);
|
||||
82
src/components/common/CompactMediaPreview.module.scss
Normal file
82
src/components/common/CompactMediaPreview.module.scss
Normal file
@ -0,0 +1,82 @@
|
||||
.root {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pictogram {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.interactive {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.media,
|
||||
.thumb {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.withActionIcon {
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover .actionIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.protector {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
color: white;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
254
src/components/common/CompactMediaPreview.tsx
Normal file
254
src/components/common/CompactMediaPreview.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import type React from '../../lib/teact/teact';
|
||||
import { memo, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiAttachment,
|
||||
ApiDocument,
|
||||
ApiPhoto,
|
||||
ApiVideo,
|
||||
MediaContent,
|
||||
} from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { IconName } from '../../types/icons';
|
||||
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
getMediaThumbUri,
|
||||
getPhotoMediaHash,
|
||||
getVideoMediaHash,
|
||||
} from '../../global/helpers';
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
import Icon from './icons/Icon';
|
||||
import MediaSpoiler from './MediaSpoiler';
|
||||
|
||||
import styles from './CompactMediaPreview.module.scss';
|
||||
|
||||
const PICTOGRAM_SIZE = 2 * REM;
|
||||
|
||||
type OwnProps = {
|
||||
id?: string;
|
||||
className?: string;
|
||||
media?: MediaContent;
|
||||
attachment?: ApiAttachment;
|
||||
size?: number;
|
||||
isPictogram?: boolean;
|
||||
isRound?: boolean;
|
||||
isProtected?: boolean;
|
||||
isSpoiler?: boolean;
|
||||
actionIcon?: IconName;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function canRenderCompactMediaPreview(
|
||||
media?: MediaContent,
|
||||
attachment?: ApiAttachment,
|
||||
) {
|
||||
const photo = media?.photo;
|
||||
const document = media?.document;
|
||||
const previewUrl = getPreviewUrl(photo, document, media?.video, attachment);
|
||||
const video = media?.video || attachment?.gif;
|
||||
const shouldRenderPreviewAsVideo = shouldUseVideoPreview(video, previewUrl);
|
||||
const previewVideoUrl = shouldRenderPreviewAsVideo
|
||||
? (media?.video?.blobUrl || attachment?.blobUrl)
|
||||
: undefined;
|
||||
const previewHash = getPreviewHash(photo, document, video, shouldRenderPreviewAsVideo);
|
||||
|
||||
return Boolean(
|
||||
previewUrl
|
||||
|| previewVideoUrl
|
||||
|| previewHash
|
||||
|| getThumbDataUri(photo, document, media?.video, attachment),
|
||||
);
|
||||
}
|
||||
|
||||
const CompactMediaPreview = ({
|
||||
id,
|
||||
className,
|
||||
media,
|
||||
attachment,
|
||||
size,
|
||||
isPictogram,
|
||||
isRound,
|
||||
isProtected,
|
||||
isSpoiler,
|
||||
actionIcon,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const previewSize = size || (isPictogram ? PICTOGRAM_SIZE : undefined);
|
||||
const photo = media?.photo;
|
||||
const document = media?.document;
|
||||
const video = media?.video || attachment?.gif;
|
||||
const previewUrl = getPreviewUrl(photo, document, media?.video, attachment);
|
||||
const shouldRenderPreviewAsVideo = shouldUseVideoPreview(video, previewUrl);
|
||||
|
||||
const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
const isIntersectingForPlaying = (
|
||||
useIsIntersecting(ref, observeIntersectionForPlaying)
|
||||
&& isIntersectingForLoading
|
||||
);
|
||||
|
||||
const previewHash = getPreviewHash(photo, document, video, shouldRenderPreviewAsVideo);
|
||||
|
||||
const previewVideoUrl = shouldRenderPreviewAsVideo
|
||||
? (media?.video?.blobUrl || attachment?.blobUrl)
|
||||
: undefined;
|
||||
const thumbDataUri = getThumbDataUri(photo, document, media?.video, attachment);
|
||||
|
||||
const fetchedPreviewUrl = useMedia(
|
||||
previewHash,
|
||||
Boolean(!previewHash || previewUrl || previewVideoUrl || !isIntersectingForLoading),
|
||||
);
|
||||
|
||||
const resolvedPreviewUrl = previewUrl || (!shouldRenderPreviewAsVideo ? fetchedPreviewUrl : undefined);
|
||||
const resolvedPreviewVideoUrl = previewVideoUrl || (shouldRenderPreviewAsVideo ? fetchedPreviewUrl : undefined);
|
||||
const shouldShowSpoiler = isSpoiler ?? photo?.isSpoiler ?? media?.video?.isSpoiler ?? attachment?.shouldSendAsSpoiler;
|
||||
|
||||
const hasResolvedMedia = Boolean(resolvedPreviewUrl || resolvedPreviewVideoUrl);
|
||||
const shouldShowCanvasThumb = Boolean(thumbDataUri && (shouldShowSpoiler || !hasResolvedMedia));
|
||||
const canvasRef = useCanvasBlur(
|
||||
thumbDataUri,
|
||||
!thumbDataUri || !shouldShowCanvasThumb,
|
||||
isMobile && !IS_CANVAS_FILTER_SUPPORTED,
|
||||
undefined,
|
||||
previewSize,
|
||||
previewSize,
|
||||
);
|
||||
useMediaTransition({
|
||||
ref: canvasRef,
|
||||
hasMediaData: shouldShowCanvasThumb,
|
||||
});
|
||||
|
||||
const { ref: imageRef } = useMediaTransition<HTMLImageElement>({
|
||||
hasMediaData: Boolean(resolvedPreviewUrl && !shouldShowSpoiler),
|
||||
});
|
||||
const { ref: videoRef } = useMediaTransition<HTMLVideoElement>({
|
||||
hasMediaData: Boolean(resolvedPreviewVideoUrl && !shouldShowSpoiler),
|
||||
});
|
||||
|
||||
const spoilerThumbDataUri = thumbDataUri || resolvedPreviewUrl;
|
||||
const style = previewSize ? `width: ${previewSize}px; height: ${previewSize}px` : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
className,
|
||||
isPictogram && styles.pictogram,
|
||||
isRound && styles.round,
|
||||
actionIcon && styles.withActionIcon,
|
||||
onClick && styles.interactive,
|
||||
)}
|
||||
style={style}
|
||||
onClick={onClick}
|
||||
>
|
||||
{thumbDataUri && (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={buildClassName('thumbnail', styles.thumb)}
|
||||
/>
|
||||
)}
|
||||
{!shouldShowSpoiler && resolvedPreviewUrl && (
|
||||
<img
|
||||
ref={imageRef}
|
||||
src={resolvedPreviewUrl}
|
||||
alt=""
|
||||
className={buildClassName('full-media', styles.media)}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
{!shouldShowSpoiler && resolvedPreviewVideoUrl && (
|
||||
<OptimizedVideo
|
||||
ref={videoRef}
|
||||
className={buildClassName('full-media', styles.media)}
|
||||
src={resolvedPreviewVideoUrl}
|
||||
canPlay={isIntersectingForPlaying}
|
||||
poster={thumbDataUri}
|
||||
loop
|
||||
playsInline
|
||||
muted
|
||||
disablePictureInPicture
|
||||
/>
|
||||
)}
|
||||
<MediaSpoiler
|
||||
thumbDataUri={spoilerThumbDataUri}
|
||||
isVisible={Boolean(shouldShowSpoiler)}
|
||||
width={previewSize}
|
||||
height={previewSize}
|
||||
/>
|
||||
{isProtected && <span className={buildClassName('protector', styles.protector)} />}
|
||||
{actionIcon && <Icon name={actionIcon} className={styles.actionIcon} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getThumbDataUri(
|
||||
photo?: ApiPhoto,
|
||||
document?: ApiDocument,
|
||||
video?: ApiVideo,
|
||||
attachment?: ApiAttachment,
|
||||
) {
|
||||
return (photo && getMediaThumbUri(photo))
|
||||
|| (document && getMediaThumbUri(document))
|
||||
|| (video && getMediaThumbUri(video))
|
||||
|| attachment?.previewBlobUrl
|
||||
|| (attachment?.mimeType.startsWith('image/') ? attachment.blobUrl : undefined);
|
||||
}
|
||||
|
||||
function getPreviewUrl(
|
||||
photo?: ApiPhoto,
|
||||
document?: ApiDocument,
|
||||
video?: ApiVideo,
|
||||
attachment?: ApiAttachment,
|
||||
) {
|
||||
return photo?.blobUrl
|
||||
|| document?.previewBlobUrl
|
||||
|| video?.previewBlobUrl
|
||||
|| attachment?.previewBlobUrl
|
||||
|| (attachment?.mimeType.startsWith('image/') ? attachment.blobUrl : undefined);
|
||||
}
|
||||
|
||||
function shouldUseVideoPreview(video: ApiVideo | undefined, previewUrl?: string) {
|
||||
return Boolean(video?.isGif && !video.previewPhotoSizes?.length && !previewUrl);
|
||||
}
|
||||
|
||||
function getPreviewHash(
|
||||
photo?: ApiPhoto,
|
||||
document?: ApiDocument,
|
||||
video?: ApiVideo,
|
||||
shouldRenderPreviewAsVideo?: boolean,
|
||||
) {
|
||||
if (photo) {
|
||||
return getPhotoMediaHash(photo, 'pictogram');
|
||||
}
|
||||
|
||||
if (document) {
|
||||
return getDocumentMediaHash(document, 'pictogram');
|
||||
}
|
||||
|
||||
if (video) {
|
||||
return getVideoMediaHash(video, shouldRenderPreviewAsVideo ? 'full' : 'pictogram');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default memo(CompactMediaPreview);
|
||||
@ -1,15 +1,14 @@
|
||||
import {
|
||||
memo, useEffect, useRef, useState,
|
||||
memo, useEffect, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiDocument, ApiMessage } from '../../api/types';
|
||||
import type { ApiDocument, ApiMessage, MediaContent } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
getMediaFormat,
|
||||
getMediaThumbUri,
|
||||
getMediaTransferState,
|
||||
isDocumentVideo,
|
||||
} from '../../global/helpers';
|
||||
@ -20,7 +19,6 @@ import { preloadDocumentMedia } from './helpers/preloadDocumentMedia';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
@ -117,9 +115,10 @@ const Document = ({
|
||||
);
|
||||
|
||||
const hasPreview = getDocumentHasPreview(document);
|
||||
const thumbDataUri = hasPreview ? getMediaThumbUri(document) : undefined;
|
||||
const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined;
|
||||
const previewData = useMedia(getDocumentMediaHash(document, 'pictogram'), !isIntersecting);
|
||||
const previewMedia = useMemo<MediaContent | undefined>(
|
||||
() => (hasPreview ? { document } : undefined),
|
||||
[document, hasPreview],
|
||||
);
|
||||
|
||||
const shouldForceDownload = document.innerMediaType === 'photo' && document.mediaSize
|
||||
&& !document.mediaSize.fromDocumentAttribute && !document.mediaSize.fromPreload;
|
||||
@ -199,8 +198,8 @@ const Document = ({
|
||||
extension={extension}
|
||||
size={size}
|
||||
timestamp={datetime}
|
||||
thumbnailDataUri={thumbDataUri}
|
||||
previewData={localBlobUrl || previewData}
|
||||
previewMedia={previewMedia}
|
||||
observeIntersection={observeIntersection}
|
||||
previewSize={fileSize}
|
||||
isTransferring={isTransferring}
|
||||
isUploading={isUploading}
|
||||
|
||||
@ -1,27 +1,26 @@
|
||||
import type { ElementRef } from '../../lib/teact/teact';
|
||||
import {
|
||||
memo, useRef, useState,
|
||||
memo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiAttachment, MediaContent } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { IconName } from '../../types/icons';
|
||||
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/oldDateFormat';
|
||||
import { getColorFromExtension } from './helpers/documentInfo';
|
||||
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
|
||||
import Link from '../ui/Link';
|
||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
import AnimatedFileSize from './AnimatedFileSize';
|
||||
import CompactMediaPreview, { canRenderCompactMediaPreview } from './CompactMediaPreview';
|
||||
import Icon from './icons/Icon';
|
||||
|
||||
import './File.scss';
|
||||
@ -36,8 +35,9 @@ type OwnProps = {
|
||||
size: number;
|
||||
timestamp?: number;
|
||||
sender?: string;
|
||||
thumbnailDataUri?: string;
|
||||
previewData?: string;
|
||||
previewMedia?: MediaContent;
|
||||
previewAttachment?: ApiAttachment;
|
||||
observeIntersection?: ObserveFn;
|
||||
className?: string;
|
||||
previewSize?: FileSize;
|
||||
isTransferring?: boolean;
|
||||
@ -58,8 +58,8 @@ const File = ({
|
||||
extension = '',
|
||||
timestamp,
|
||||
sender,
|
||||
thumbnailDataUri,
|
||||
previewData,
|
||||
previewMedia,
|
||||
previewAttachment,
|
||||
className,
|
||||
previewSize = 'medium',
|
||||
isTransferring,
|
||||
@ -68,6 +68,7 @@ const File = ({
|
||||
isSelected,
|
||||
transferProgress,
|
||||
actionIcon,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
onDateClick,
|
||||
}: OwnProps) => {
|
||||
@ -78,12 +79,6 @@ const File = ({
|
||||
elementRef = ref;
|
||||
}
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const [withThumb] = useState(!previewData);
|
||||
const noThumb = Boolean(previewData);
|
||||
const thumbRef = useCanvasBlur(thumbnailDataUri, noThumb, isMobile && !IS_CANVAS_FILTER_SUPPORTED);
|
||||
const thumbClassNames = useMediaTransitionDeprecated(!noThumb);
|
||||
|
||||
const {
|
||||
shouldRender: shouldSpinnerRender,
|
||||
transitionClassNames: spinnerClassNames,
|
||||
@ -91,7 +86,8 @@ const File = ({
|
||||
|
||||
const color = getColorFromExtension(extension);
|
||||
|
||||
const { width, height } = getDocumentThumbnailDimensions(previewSize);
|
||||
const { width } = getDocumentThumbnailDimensions(previewSize);
|
||||
const shouldRenderPreview = canRenderCompactMediaPreview(previewMedia, previewAttachment);
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
'File',
|
||||
@ -109,23 +105,14 @@ const File = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="file-icon-container" onClick={isUploading ? undefined : onClick}>
|
||||
{thumbnailDataUri || previewData ? (
|
||||
<div className="file-preview media-inner">
|
||||
<img
|
||||
src={previewData}
|
||||
className="full-media"
|
||||
width={width}
|
||||
height={height}
|
||||
draggable={false}
|
||||
alt=""
|
||||
/>
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{shouldRenderPreview ? (
|
||||
<CompactMediaPreview
|
||||
className="file-preview media-inner"
|
||||
media={previewMedia}
|
||||
attachment={previewAttachment}
|
||||
size={width}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
/>
|
||||
) : (
|
||||
<div className={`file-icon ${color}`}>
|
||||
{extension.length <= 4 && (
|
||||
|
||||
@ -2,7 +2,7 @@ import { memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiFormattedText, ApiMessage, ApiPoll, ApiTypeStory,
|
||||
ApiFormattedText, ApiMessage, ApiMessagePoll, ApiTypeStory,
|
||||
ApiWebPage,
|
||||
} from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
@ -42,7 +42,7 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
story?: ApiTypeStory;
|
||||
webPage?: ApiWebPage;
|
||||
};
|
||||
|
||||
@ -238,58 +238,6 @@
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&.round {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
&.with-action-icon {
|
||||
&::after {
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover .pictogram-action-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pictogram {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pictogram-action-icon {
|
||||
pointer-events: none;
|
||||
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
color: white;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&.inside-input {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef } from '../../../lib/teact/teact';
|
||||
import { useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiChat,
|
||||
@ -23,21 +23,16 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import { formatScheduledDateTime } from '../../../util/dates/oldDateFormat';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
|
||||
import { getPictogramDimensions } from '../helpers/mediaDimensions';
|
||||
import renderText from '../helpers/renderText';
|
||||
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
|
||||
|
||||
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
|
||||
import useThumbnail from '../../../hooks/media/useThumbnail';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
|
||||
|
||||
import RippleEffect from '../../ui/RippleEffect';
|
||||
import CompactMediaPreview, { canRenderCompactMediaPreview } from '../CompactMediaPreview';
|
||||
import Icon from '../icons/Icon';
|
||||
import MediaSpoiler from '../MediaSpoiler';
|
||||
import MessageSummary from '../MessageSummary';
|
||||
import PeerColorWrapper from '../PeerColorWrapper';
|
||||
|
||||
@ -96,9 +91,6 @@ const EmbeddedMessage = ({
|
||||
onClick,
|
||||
onPictogramClick,
|
||||
}: OwnProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
|
||||
const containedMedia: MediaContainer | undefined = useMemo(() => {
|
||||
const media = (replyInfo?.type === 'message' && replyInfo.replyMedia) || message?.content;
|
||||
if (!media) {
|
||||
@ -109,13 +101,7 @@ const EmbeddedMessage = ({
|
||||
content: media,
|
||||
};
|
||||
}, [message, replyInfo]);
|
||||
|
||||
const gif = containedMedia?.content?.video?.isGif ? containedMedia.content.video : undefined;
|
||||
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
|
||||
|
||||
const mediaHash = useMessageMediaHash(containedMedia, isVideoThumbnail ? 'full' : 'pictogram');
|
||||
const mediaBlobUrl = useMedia(mediaHash, !isIntersecting);
|
||||
const mediaThumbnail = useThumbnail(containedMedia);
|
||||
const hasPictogram = canRenderCompactMediaPreview(containedMedia?.content);
|
||||
|
||||
const isRoundVideo = Boolean(containedMedia && getMessageRoundVideo(containedMedia));
|
||||
const isSpoiler = Boolean(containedMedia && getMessageIsSpoiler(containedMedia)) || isMediaNsfw;
|
||||
@ -206,7 +192,7 @@ const EmbeddedMessage = ({
|
||||
return (
|
||||
<MessageSummary
|
||||
message={message}
|
||||
noEmoji={Boolean(mediaThumbnail)}
|
||||
noEmoji={hasPictogram}
|
||||
forcedText={translatedText}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
@ -301,7 +287,6 @@ const EmbeddedMessage = ({
|
||||
<PeerColorWrapper
|
||||
peer={sender}
|
||||
emojiIconClassName="EmbeddedMessage--background-icons"
|
||||
ref={ref}
|
||||
shouldReset
|
||||
isReply={Boolean(replyInfo)}
|
||||
noUserColors={noUserColors}
|
||||
@ -309,7 +294,7 @@ const EmbeddedMessage = ({
|
||||
'EmbeddedMessage',
|
||||
className,
|
||||
isQuote && 'is-quote',
|
||||
mediaThumbnail && 'with-thumb',
|
||||
hasPictogram && 'with-thumb',
|
||||
'no-selection',
|
||||
composerForwardSenders && 'is-input-forward',
|
||||
suggestedPostInfo && 'is-suggested-post',
|
||||
@ -319,16 +304,20 @@ const EmbeddedMessage = ({
|
||||
>
|
||||
<div className="hover-effect" />
|
||||
<RippleEffect />
|
||||
{mediaThumbnail && renderPictogram({
|
||||
thumbDataUri: mediaThumbnail,
|
||||
blobUrl: mediaBlobUrl,
|
||||
isFullVideo: isVideoThumbnail,
|
||||
isRoundVideo,
|
||||
isProtected,
|
||||
isSpoiler,
|
||||
pictogramActionIcon,
|
||||
onPictogramClick,
|
||||
})}
|
||||
{hasPictogram && (
|
||||
<CompactMediaPreview
|
||||
media={containedMedia?.content}
|
||||
className="embedded-thumb"
|
||||
isPictogram
|
||||
isRound={isRoundVideo}
|
||||
isProtected={isProtected}
|
||||
isSpoiler={isSpoiler}
|
||||
actionIcon={pictogramActionIcon}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={onPictogramClick}
|
||||
/>
|
||||
)}
|
||||
<div className="message-text">
|
||||
<p className={buildClassName('embedded-text-wrapper', isQuote && 'multiline')}>
|
||||
{renderTextContent()}
|
||||
@ -342,65 +331,4 @@ const EmbeddedMessage = ({
|
||||
);
|
||||
};
|
||||
|
||||
function renderPictogram({
|
||||
thumbDataUri,
|
||||
blobUrl,
|
||||
isFullVideo,
|
||||
isRoundVideo,
|
||||
isProtected,
|
||||
isSpoiler,
|
||||
pictogramActionIcon,
|
||||
onPictogramClick,
|
||||
}: {
|
||||
thumbDataUri: string;
|
||||
blobUrl?: string;
|
||||
isFullVideo?: boolean;
|
||||
isRoundVideo?: boolean;
|
||||
isProtected?: boolean;
|
||||
isSpoiler?: boolean;
|
||||
pictogramActionIcon?: IconName;
|
||||
onPictogramClick?: ((e: React.MouseEvent) => void);
|
||||
}) {
|
||||
const { width, height } = getPictogramDimensions();
|
||||
|
||||
const srcUrl = blobUrl || thumbDataUri;
|
||||
const shouldRenderVideo = isFullVideo && blobUrl;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('embedded-thumb', isRoundVideo && 'round', pictogramActionIcon && 'with-action-icon')}
|
||||
onClick={onPictogramClick}
|
||||
>
|
||||
{!isSpoiler && !shouldRenderVideo && (
|
||||
<img
|
||||
src={srcUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
className="pictogram"
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
{!isSpoiler && shouldRenderVideo && (
|
||||
<video
|
||||
src={blobUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
className="pictogram"
|
||||
/>
|
||||
)}
|
||||
<MediaSpoiler
|
||||
thumbDataUri={shouldRenderVideo ? thumbDataUri : srcUrl}
|
||||
isVisible={Boolean(isSpoiler)}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
{isProtected && <span className="protector" />}
|
||||
{pictogramActionIcon && <Icon name={pictogramActionIcon} className="pictogram-action-icon" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmbeddedMessage;
|
||||
|
||||
@ -1,24 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { useRef } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiPeer, ApiTypeStory } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import {
|
||||
getStoryMediaHash,
|
||||
} from '../../../global/helpers';
|
||||
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getPictogramDimensions } from '../helpers/mediaDimensions';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import { useFastClick } from '../../../hooks/useFastClick';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import CompactMediaPreview, { canRenderCompactMediaPreview } from '../CompactMediaPreview';
|
||||
import Icon from '../icons/Icon';
|
||||
import PeerColorWrapper from '../PeerColorWrapper';
|
||||
|
||||
@ -30,6 +24,7 @@ type OwnProps = {
|
||||
noUserColors?: boolean;
|
||||
isProtected?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
@ -41,22 +36,17 @@ const EmbeddedStory: FC<OwnProps> = ({
|
||||
noUserColors,
|
||||
isProtected,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
const isFullStory = story && 'content' in story;
|
||||
const isExpiredStory = story && 'isDeleted' in story;
|
||||
const isVideoStory = isFullStory && Boolean(story.content.video);
|
||||
const title = isFullStory ? 'Story' : (isExpiredStory ? 'ExpiredStory' : 'Loading');
|
||||
|
||||
const mediaBlobUrl = useMedia(isFullStory && getStoryMediaHash(story, 'pictogram'), !isIntersecting);
|
||||
const mediaThumbnail = isVideoStory ? story.content.video!.thumbnail?.dataUri : undefined;
|
||||
const pictogramUrl = mediaBlobUrl || mediaThumbnail;
|
||||
const hasPictogram = isFullStory && canRenderCompactMediaPreview(story.content);
|
||||
|
||||
const senderTitle = sender ? getPeerTitle(lang, sender) : undefined;
|
||||
const handleFastClick = useLastCallback(() => {
|
||||
@ -73,18 +63,26 @@ const EmbeddedStory: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<PeerColorWrapper
|
||||
ref={ref}
|
||||
peerColor={sender?.color}
|
||||
noUserColors={noUserColors}
|
||||
shouldReset
|
||||
className={buildClassName(
|
||||
'EmbeddedMessage',
|
||||
pictogramUrl && 'with-thumb',
|
||||
hasPictogram && 'with-thumb',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
{pictogramUrl && renderPictogram(pictogramUrl, isProtected)}
|
||||
{isFullStory && hasPictogram && (
|
||||
<CompactMediaPreview
|
||||
media={story.content}
|
||||
className="embedded-thumb"
|
||||
isPictogram
|
||||
isProtected={isProtected}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
)}
|
||||
<div className="message-text with-message-color">
|
||||
<p className="embedded-text-wrapper">
|
||||
{isExpiredStory && (
|
||||
@ -101,25 +99,4 @@ const EmbeddedStory: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function renderPictogram(
|
||||
srcUrl: string,
|
||||
isProtected?: boolean,
|
||||
) {
|
||||
const { width, height } = getPictogramDimensions();
|
||||
|
||||
return (
|
||||
<div className="embedded-thumb">
|
||||
<img
|
||||
src={srcUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
className="pictogram"
|
||||
draggable={false}
|
||||
/>
|
||||
{isProtected && <span className="protector" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmbeddedStory;
|
||||
|
||||
@ -60,11 +60,11 @@ function getMaxMessageWidthRem(fromOwnMessage?: boolean, noAvatars?: boolean, is
|
||||
|
||||
export function getAvailableWidth(
|
||||
fromOwnMessage?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
isNestedMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
isMobile?: boolean,
|
||||
) {
|
||||
const extraPaddingRem = isWebPageMedia ? 1.625 : 0;
|
||||
const extraPaddingRem = isNestedMedia ? 2.125 : 0;
|
||||
const availableWidthRem = getMaxMessageWidthRem(fromOwnMessage, noAvatars, isMobile) - extraPaddingRem;
|
||||
|
||||
return availableWidthRem * REM;
|
||||
@ -85,7 +85,7 @@ export function calculateDimensionsForMessageMedia({
|
||||
width,
|
||||
height,
|
||||
fromOwnMessage,
|
||||
isWebPageMedia,
|
||||
isNestedMedia,
|
||||
isGif,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
@ -94,13 +94,13 @@ export function calculateDimensionsForMessageMedia({
|
||||
height: number;
|
||||
fromOwnMessage?: boolean;
|
||||
asForwarded?: boolean;
|
||||
isWebPageMedia?: boolean;
|
||||
isNestedMedia?: boolean;
|
||||
isGif?: boolean;
|
||||
noAvatars?: boolean;
|
||||
isMobile?: boolean;
|
||||
}): ApiDimensions {
|
||||
const aspectRatio = height / width;
|
||||
const availableWidth = getAvailableWidth(fromOwnMessage, isWebPageMedia, noAvatars, isMobile);
|
||||
const availableWidth = getAvailableWidth(fromOwnMessage, isNestedMedia, noAvatars, isMobile);
|
||||
const availableHeight = getAvailableHeight(isGif, aspectRatio);
|
||||
const mediaWidth = isGif ? Math.max(GIF_MIN_WIDTH, width) : width;
|
||||
const mediaHeight = isGif ? height * (mediaWidth / width) : height;
|
||||
@ -125,7 +125,7 @@ export function calculateInlineImageDimensions(
|
||||
photo: ApiPhoto,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
isNestedMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
isMobile?: boolean,
|
||||
) {
|
||||
@ -136,7 +136,7 @@ export function calculateInlineImageDimensions(
|
||||
height,
|
||||
fromOwnMessage,
|
||||
asForwarded,
|
||||
isWebPageMedia,
|
||||
isNestedMedia,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
});
|
||||
@ -146,7 +146,7 @@ export function calculateVideoDimensions(
|
||||
video: ApiVideo,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
isNestedMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
isMobile?: boolean,
|
||||
) {
|
||||
@ -157,7 +157,7 @@ export function calculateVideoDimensions(
|
||||
height,
|
||||
fromOwnMessage,
|
||||
asForwarded,
|
||||
isWebPageMedia,
|
||||
isNestedMedia,
|
||||
isGif: video.isGif,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
@ -168,7 +168,7 @@ export function calculateExtendedPreviewDimensions(
|
||||
preview: ApiMediaExtendedPreview,
|
||||
fromOwnMessage?: boolean,
|
||||
asForwarded?: boolean,
|
||||
isWebPageMedia?: boolean,
|
||||
isNestedMedia?: boolean,
|
||||
noAvatars?: boolean,
|
||||
isMobile?: boolean,
|
||||
) {
|
||||
@ -179,19 +179,12 @@ export function calculateExtendedPreviewDimensions(
|
||||
height,
|
||||
fromOwnMessage,
|
||||
asForwarded,
|
||||
isWebPageMedia,
|
||||
isNestedMedia,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
});
|
||||
}
|
||||
|
||||
export function getPictogramDimensions(): ApiDimensions {
|
||||
return {
|
||||
width: 2 * REM,
|
||||
height: 2 * REM,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDocumentThumbnailDimensions(
|
||||
size: 'small' | 'medium' | 'large' = 'medium',
|
||||
): ApiDimensions {
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
FRAGMENT_PHONE_CODE, FRAGMENT_PHONE_LENGTH, MUTE_INDEFINITE_TIMESTAMP, UNMUTE_TIMESTAMP,
|
||||
} from '../../../config';
|
||||
import {
|
||||
buildStaticMapHash,
|
||||
getChatLink,
|
||||
getHasAdminRight,
|
||||
isChatAdmin,
|
||||
@ -56,15 +55,13 @@ import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines';
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
|
||||
|
||||
import Chat from '../../left/main/Chat';
|
||||
import Button from '../../ui/Button';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import CompactMapPreview from '../CompactMapPreview';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import Icon from '../icons/Icon';
|
||||
import SafeLink from '../SafeLink';
|
||||
@ -191,19 +188,18 @@ const ChatExtra = ({
|
||||
}, [peerId, chat, user]);
|
||||
|
||||
const { width, height, zoom } = DEFAULT_MAP_CONFIG;
|
||||
const dpr = useDevicePixelRatio();
|
||||
const locationMediaHash = businessLocation?.geo
|
||||
&& buildStaticMapHash(businessLocation.geo, width, height, zoom, dpr);
|
||||
const locationBlobUrl = useMedia(locationMediaHash);
|
||||
|
||||
const locationRightComponent = useMemo(() => {
|
||||
if (!businessLocation?.geo) return undefined;
|
||||
if (locationBlobUrl) {
|
||||
return <img src={locationBlobUrl} alt="" className={styles.businessLocation} />;
|
||||
}
|
||||
|
||||
return <Skeleton className={styles.businessLocation} animation="wave" />;
|
||||
}, [businessLocation, locationBlobUrl]);
|
||||
return (
|
||||
<CompactMapPreview
|
||||
className={styles.businessLocation}
|
||||
geo={businessLocation.geo}
|
||||
width={width}
|
||||
height={height}
|
||||
zoom={zoom}
|
||||
/>
|
||||
);
|
||||
}, [businessLocation, width, height, zoom]);
|
||||
|
||||
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
|
||||
const shouldRenderAllLinks = (chat && isChatChannel(chat)) || user?.isPremium;
|
||||
|
||||
@ -2,34 +2,72 @@
|
||||
.root {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
position: relative;
|
||||
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
place-items: center;
|
||||
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin: 0;
|
||||
border: 0.125rem solid var(--color-borders-input);
|
||||
border: 0.125rem solid var(--ui-border-color, var(--color-borders-input));
|
||||
border-radius: 0.25rem;
|
||||
|
||||
appearance: none;
|
||||
background-color: var(--color-background);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 0;
|
||||
background-color: var(--ui-bg-color, transparent);
|
||||
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, background-size 0.15s ease;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
transform: scale(0.8);
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 0 solid transparent;
|
||||
border-radius: 0;
|
||||
|
||||
opacity: 0;
|
||||
background-color: transparent;
|
||||
|
||||
transition: opacity 0.15s ease, transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-primary);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
|
||||
background-size: 0.75rem;
|
||||
border-color: var(--ui-accent-color, var(--color-primary));
|
||||
background-color: var(--ui-accent-color, var(--color-primary));
|
||||
|
||||
&::before {
|
||||
transform: rotate(45deg) translate(-0.0625rem, -0.03125rem);
|
||||
|
||||
width: 0.375rem;
|
||||
height: 0.625rem;
|
||||
border-color: var(--ui-check-color, white);
|
||||
border-style: solid;
|
||||
border-width: 0 0.125rem 0.125rem 0;
|
||||
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:indeterminate {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-primary);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
background-size: 0.75rem;
|
||||
border-color: var(--ui-accent-color, var(--color-primary));
|
||||
background-color: var(--ui-accent-color, var(--color-primary));
|
||||
|
||||
&::before {
|
||||
transform: none;
|
||||
|
||||
width: 0.625rem;
|
||||
height: 0.125rem;
|
||||
border-width: 0;
|
||||
border-radius: 9999px;
|
||||
|
||||
opacity: 1;
|
||||
background-color: var(--ui-check-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@ -38,7 +76,7 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,13 +15,13 @@ type InputProps = React.DetailedHTMLProps<
|
||||
>;
|
||||
|
||||
type OwnProps = {
|
||||
checked: boolean;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
isRound?: boolean;
|
||||
indeterminate?: boolean;
|
||||
isInvalid?: boolean;
|
||||
className?: string;
|
||||
onChange: (checked: boolean) => void;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
||||
@ -50,7 +50,7 @@ const Checkbox = ({
|
||||
}, [indeterminate]);
|
||||
|
||||
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.currentTarget.checked);
|
||||
onChange?.(e.currentTarget.checked);
|
||||
});
|
||||
|
||||
if (interactive?.isLoading) return undefined;
|
||||
|
||||
@ -7,18 +7,21 @@
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin: 0;
|
||||
border: 0.125rem solid var(--color-borders-input);
|
||||
border: 0.125rem solid var(--ui-border-color, var(--color-borders-input));
|
||||
border-radius: 50%;
|
||||
|
||||
appearance: none;
|
||||
background-color: var(--color-background);
|
||||
background-color: var(--ui-bg-color, transparent);
|
||||
background-image: radial-gradient(circle, var(--ui-accent-color, var(--color-primary)) 0 55%, transparent 60%);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 0 0;
|
||||
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
transition: border-color 0.15s ease, background-size 0.15s ease;
|
||||
|
||||
&:checked {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-primary);
|
||||
box-shadow: inset 0 0 0 0.1875rem var(--color-background);
|
||||
border-color: var(--ui-accent-color, var(--color-primary));
|
||||
background-size: 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@ -27,7 +30,7 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,10 +16,10 @@ type InputProps = React.DetailedHTMLProps<
|
||||
|
||||
type OwnProps = {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onChange: (value: string) => void;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
||||
@ -40,7 +40,7 @@ const Radio = ({
|
||||
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
||||
|
||||
const handleChange = useLastCallback(() => {
|
||||
onChange(value);
|
||||
onChange?.(value);
|
||||
});
|
||||
|
||||
if (interactive?.isLoading) return undefined;
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
border-radius: 0.625rem;
|
||||
|
||||
appearance: none;
|
||||
background-color: var(--color-borders-input);
|
||||
background-color: var(--ui-border-color, var(--color-borders-input));
|
||||
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
@ -25,21 +25,21 @@
|
||||
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border: 0.125rem solid var(--color-borders-input);
|
||||
border: 0.125rem solid var(--ui-border-color, var(--color-borders-input));
|
||||
border-radius: 50%;
|
||||
|
||||
background-color: var(--color-background);
|
||||
background-color: var(--ui-bg-color, var(--color-background));
|
||||
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--ui-accent-color, var(--color-primary));
|
||||
background-color: var(--ui-accent-color, var(--color-primary));
|
||||
|
||||
&::before {
|
||||
transform: translateX(0.75rem);
|
||||
border-color: var(--color-primary);
|
||||
border-color: var(--ui-accent-color, var(--color-primary));
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import { getChatAvatarHash } from '../../../global/helpers';
|
||||
import { selectTabState, selectUser, selectUserFullInfo } from '../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { formatDateToString } from '../../../util/dates/oldDateFormat';
|
||||
import { getNextArrowReplacement } from '../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../util/localization/format';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
@ -284,7 +284,7 @@ const SettingsEditProfile = ({
|
||||
link: (
|
||||
<Link isPrimary onClick={handleBirthdayPrivacyClick}>
|
||||
{lang('BirthdayPrivacySuggestionLink',
|
||||
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
),
|
||||
}, { withNodes: true })}
|
||||
|
||||
@ -11,7 +11,7 @@ import { IS_WEBAUTHN_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { type LangFn } from '../../../util/localization';
|
||||
import { formatDateTime, getCalendarDayDiff, secondsToDate } from '../../../util/localization/dateFormat';
|
||||
import { getNextArrowReplacement } from '../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../util/localization/format';
|
||||
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
@ -158,7 +158,7 @@ const SettingsPasskeys = ({
|
||||
link: (
|
||||
<Link isPrimary onClick={handleOpenPasskeyModal}>
|
||||
{lang('SettingsPasskeysFooterLink', undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
),
|
||||
}, { withNodes: true })}
|
||||
|
||||
@ -211,7 +211,10 @@ export function animateClosing(
|
||||
});
|
||||
}
|
||||
|
||||
function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origin?: MediaViewerOrigin) {
|
||||
function createGhost(
|
||||
source: string | HTMLImageElement | HTMLVideoElement | HTMLCanvasElement,
|
||||
origin?: MediaViewerOrigin,
|
||||
) {
|
||||
const ghost = document.createElement('div');
|
||||
ghost.classList.add('ghost');
|
||||
|
||||
@ -221,6 +224,8 @@ function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origi
|
||||
|
||||
if (typeof source === 'string') {
|
||||
img.src = source;
|
||||
} else if (source instanceof HTMLCanvasElement) {
|
||||
img.src = source.toDataURL();
|
||||
} else if (source instanceof HTMLVideoElement) {
|
||||
img.src = source.poster;
|
||||
} else {
|
||||
@ -305,6 +310,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
||||
mediaSelector = 'img';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.PollPreview:
|
||||
containerSelector = `#poll-media${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = 'img.full-media, video.full-media, img.thumbnail:not(.blurred-bg), img, video';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SharedMedia:
|
||||
containerSelector = `#shared-media${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = 'img';
|
||||
@ -343,14 +353,18 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
||||
|
||||
case MediaViewerOrigin.SponsoredMessage:
|
||||
containerSelector = '.Transition_slide-active > .MessageList .sponsored-media-preview';
|
||||
mediaSelector = `${MESSAGE_CONTENT_SELECTOR} .full-media,${MESSAGE_CONTENT_SELECTOR} .thumbnail:not(.blurred-bg)`;
|
||||
mediaSelector = `${MESSAGE_CONTENT_SELECTOR} img.full-media,`
|
||||
+ `${MESSAGE_CONTENT_SELECTOR} video.full-media,`
|
||||
+ `${MESSAGE_CONTENT_SELECTOR} img.thumbnail:not(.blurred-bg)`;
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.Inline:
|
||||
default:
|
||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = `${MESSAGE_CONTENT_SELECTOR} .full-media,${MESSAGE_CONTENT_SELECTOR} .thumbnail:not(.blurred-bg)`;
|
||||
mediaSelector = `${MESSAGE_CONTENT_SELECTOR} img.full-media,`
|
||||
+ `${MESSAGE_CONTENT_SELECTOR} video.full-media,`
|
||||
+ `${MESSAGE_CONTENT_SELECTOR} img.thumbnail:not(.blurred-bg)`;
|
||||
}
|
||||
|
||||
const container = document.querySelector<HTMLElement>(containerSelector)!;
|
||||
@ -371,6 +385,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.StarsTransaction:
|
||||
case MediaViewerOrigin.PreviewMedia:
|
||||
case MediaViewerOrigin.PollPreview:
|
||||
ghost.classList.add('rounded-corners');
|
||||
break;
|
||||
|
||||
|
||||
@ -84,14 +84,13 @@ const AttachmentModalItem = ({
|
||||
);
|
||||
default: {
|
||||
const canEdit = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType) && !isMobile;
|
||||
const isPhoto = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType);
|
||||
return (
|
||||
<>
|
||||
<File
|
||||
className={styles.file}
|
||||
name={attachment.filename}
|
||||
extension={getFileExtension(attachment.filename, attachment.mimeType)}
|
||||
previewData={isPhoto && attachment.blobUrl ? attachment.blobUrl : attachment.previewBlobUrl}
|
||||
previewAttachment={attachment}
|
||||
size={attachment.size}
|
||||
previewSize="large"
|
||||
onClick={canEdit ? handleEditClick : undefined}
|
||||
|
||||
@ -8,6 +8,7 @@ import type { ApiNewPoll } from '../../../api/types';
|
||||
|
||||
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { generateUniqueNumberId } from '../../../util/generateUniqueId';
|
||||
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
@ -147,25 +148,26 @@ const PollModal = ({
|
||||
|
||||
const payload: ApiNewPoll = {
|
||||
summary: {
|
||||
id: generateUniqueNumberId().toString(),
|
||||
hash: '0',
|
||||
question: {
|
||||
text: questionTrimmed,
|
||||
},
|
||||
answers,
|
||||
...(!isAnonymous && { isPublic: true }),
|
||||
...(isMultipleAnswers && { multipleChoice: true }),
|
||||
...(isQuizMode && { quiz: true }),
|
||||
isPublic: !isAnonymous || undefined,
|
||||
isMultipleChoice: isMultipleAnswers || undefined,
|
||||
isQuiz: isQuizMode || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (isQuizMode) {
|
||||
const { text, entities } = (solution && parseHtmlAsFormattedText(solution.substring(0, MAX_SOLUTION_LENGTH)))
|
||||
|| {};
|
||||
const correctAnswerIndex = answers.findIndex((answer) => answer.option === String(correctOption!));
|
||||
|
||||
payload.quiz = {
|
||||
correctAnswers: [String(correctOption)],
|
||||
...(text && { solution: text }),
|
||||
...(entities && { solutionEntities: entities }),
|
||||
};
|
||||
payload.correctAnswers = [correctAnswerIndex];
|
||||
payload.solution = text;
|
||||
payload.solutionEntities = entities;
|
||||
}
|
||||
|
||||
onSend(payload);
|
||||
|
||||
@ -114,6 +114,8 @@ const SINGLE_LINE_ACTIONS = new Set<ApiMessageAction['type']>([
|
||||
'chatDeletePhoto',
|
||||
'todoCompletions',
|
||||
'todoAppendTasks',
|
||||
'pollAppendAnswer',
|
||||
'pollDeleteAnswer',
|
||||
'unsupported',
|
||||
]);
|
||||
const HIDDEN_TEXT_ACTIONS = new Set<ApiMessageAction['type']>(['giftCode', 'prizeStars',
|
||||
|
||||
@ -1057,6 +1057,29 @@ const ActionMessageText = ({
|
||||
});
|
||||
}
|
||||
|
||||
case 'pollAppendAnswer':
|
||||
case 'pollDeleteAnswer': {
|
||||
const optionLink = renderMessageLink(
|
||||
replyMessage,
|
||||
renderTextWithEntities({
|
||||
text: action.answer.text.text,
|
||||
entities: action.answer.text.entities,
|
||||
asPreview: true,
|
||||
}),
|
||||
asPreview,
|
||||
);
|
||||
|
||||
return translateWithYou(
|
||||
lang,
|
||||
action.type === 'pollAppendAnswer' ? 'MessageActionPollAppendAnswer' : 'MessageActionPollDeleteAnswer',
|
||||
isOutgoing,
|
||||
{
|
||||
peer: senderLink,
|
||||
option: optionLink,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
case 'phoneCall': // Rendered as a regular message, but considered an action for the summary
|
||||
return lang(getCallMessageKey(action, isOutgoing));
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
ApiChat,
|
||||
ApiChatReactions,
|
||||
ApiMessage,
|
||||
ApiPoll,
|
||||
ApiMessagePoll,
|
||||
ApiReaction,
|
||||
ApiStickerSet,
|
||||
ApiStickerSetInfo,
|
||||
@ -108,7 +108,7 @@ export type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
threadId?: ThreadId;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
story?: ApiTypeStory;
|
||||
chat?: ApiChat;
|
||||
|
||||
@ -666,7 +666,7 @@
|
||||
|
||||
.message-content {
|
||||
&.has-replies:not(.custom-shape),
|
||||
&.has-footer:not(.web-page) {
|
||||
&.has-footer:not(.web-page):not(.poll) {
|
||||
.media-inner,
|
||||
.Album {
|
||||
--border-bottom-left-radius: 0;
|
||||
@ -674,6 +674,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.poll > .content-inner > .media-inner {
|
||||
--border-bottom-left-radius: 0;
|
||||
--border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&.text.is-inverted-media {
|
||||
.Album,
|
||||
.media-inner {
|
||||
|
||||
@ -17,8 +17,8 @@ import type {
|
||||
ApiKeyboardButton,
|
||||
ApiMessage,
|
||||
ApiMessageOutgoingStatus,
|
||||
ApiMessagePoll,
|
||||
ApiPeer,
|
||||
ApiPoll,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
ApiSavedReactionTag,
|
||||
@ -202,7 +202,7 @@ import MessageMeta from './MessageMeta';
|
||||
import MessagePhoneCall from './MessagePhoneCall';
|
||||
import PaidMediaOverlay from './PaidMediaOverlay';
|
||||
import Photo from './Photo';
|
||||
import Poll from './Poll';
|
||||
import Poll from './poll/Poll';
|
||||
import Reactions from './reactions/Reactions';
|
||||
import RoundVideo from './RoundVideo';
|
||||
import Sticker from './Sticker';
|
||||
@ -323,7 +323,7 @@ type StateProps = {
|
||||
canTranscribeVoice?: boolean;
|
||||
viaBusinessBot?: ApiUser;
|
||||
effect?: ApiAvailableEffect;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
maxTimestamp?: number;
|
||||
lastPlaybackTimestamp?: number;
|
||||
@ -680,7 +680,6 @@ const Message = ({
|
||||
handleOpenThread,
|
||||
handleReadMedia,
|
||||
handleCancelUpload,
|
||||
handleVoteSend,
|
||||
handleGroupForward,
|
||||
handleForward,
|
||||
handleFocus,
|
||||
@ -1221,6 +1220,7 @@ const Message = ({
|
||||
noUserColors={noUserColors}
|
||||
isProtected={isProtected}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleStoryClick}
|
||||
/>
|
||||
)}
|
||||
@ -1358,7 +1358,17 @@ const Message = ({
|
||||
<Contact contact={contact} noUserColors={isOwn} />
|
||||
)}
|
||||
{poll && (
|
||||
<Poll message={message} poll={poll} onSendVote={handleVoteSend} />
|
||||
<Poll
|
||||
key={poll.summary.id}
|
||||
chatId={chatId}
|
||||
messageId={messageId}
|
||||
poll={poll}
|
||||
messageText={text}
|
||||
theme={theme}
|
||||
isInScheduled={isScheduled}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
)}
|
||||
{todo && (
|
||||
<TodoList message={message} todoList={todo} />
|
||||
|
||||
@ -9,8 +9,8 @@ import type {
|
||||
ApiChat,
|
||||
ApiChatReactions,
|
||||
ApiMessage,
|
||||
ApiMessagePoll,
|
||||
ApiPeer,
|
||||
ApiPoll,
|
||||
ApiReaction,
|
||||
ApiStickerSet,
|
||||
ApiThreadInfo,
|
||||
@ -57,7 +57,7 @@ type OwnProps = {
|
||||
anchor: IAnchorPosition;
|
||||
targetHref?: string;
|
||||
message: ApiMessage;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
story?: ApiTypeStory;
|
||||
canSendNow?: boolean;
|
||||
|
||||
@ -37,7 +37,7 @@ import ProgressSpinner from '../../ui/ProgressSpinner';
|
||||
export type OwnProps<T> = {
|
||||
id?: string;
|
||||
photo: ApiPhoto | ApiMediaExtendedPreview;
|
||||
isInWebPage?: boolean;
|
||||
isNestedMedia?: boolean;
|
||||
messageText?: string;
|
||||
isOwn?: boolean;
|
||||
noAvatars?: boolean;
|
||||
@ -85,7 +85,7 @@ const Photo = <T,>({
|
||||
isDownloading,
|
||||
isProtected,
|
||||
theme,
|
||||
isInWebPage,
|
||||
isNestedMedia,
|
||||
clickArg,
|
||||
className,
|
||||
isMediaNsfw,
|
||||
@ -245,7 +245,7 @@ const Photo = <T,>({
|
||||
noAvatars,
|
||||
isMobile,
|
||||
messageText,
|
||||
isInWebPage,
|
||||
isNestedMedia,
|
||||
});
|
||||
|
||||
const componentClassName = buildClassName(
|
||||
|
||||
@ -1,170 +0,0 @@
|
||||
.Poll {
|
||||
min-width: 15rem;
|
||||
text-align: initial;
|
||||
|
||||
.poll-question {
|
||||
margin: 0.125rem 0;
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1.25rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.poll-type,
|
||||
.poll-voters-count {
|
||||
font-size: 0.875rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.poll-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 1.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.poll-voters-count {
|
||||
margin: 0 0 1.125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Checkbox,
|
||||
.Radio {
|
||||
min-height: 2.5rem;
|
||||
padding-bottom: 1.25rem;
|
||||
padding-left: 2.25rem;
|
||||
|
||||
&.disabled {
|
||||
cursor: var(--custom-cursor, not-allowed);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.Checkbox-main,
|
||||
.Radio-main {
|
||||
|
||||
&::before {
|
||||
--color-borders-input: var(--secondary-color);
|
||||
|
||||
top: 0.6875rem;
|
||||
left: 0.125rem;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0.6875rem;
|
||||
left: 0.4375rem;
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.label {
|
||||
line-height: 1.3125rem;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked ~ .Radio-main,
|
||||
input:checked ~ .Checkbox-main {
|
||||
&::before {
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
top: 0.6875rem;
|
||||
left: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox {
|
||||
&.loading {
|
||||
.Spinner {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.Checkbox-main {
|
||||
&::after {
|
||||
left: 0.125rem;
|
||||
background-color: var(--accent-color);
|
||||
|
||||
.theme-dark .Message.own & {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Spinner .Spinner__inner {
|
||||
// gray spinner
|
||||
background-image: var(--spinner-gray-data);
|
||||
|
||||
.theme-dark & {
|
||||
background-image: var(--spinner-white-data);
|
||||
}
|
||||
|
||||
.Message.own & {
|
||||
// green spinner
|
||||
background-image: var(--spinner-green-data);
|
||||
.theme-dark & {
|
||||
background-image: var(--spinner-white-data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poll-recent-voters {
|
||||
margin-left: 0.875rem;
|
||||
}
|
||||
|
||||
.poll-countdown {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
transition: color 0.2s;
|
||||
|
||||
&.hurry-up {
|
||||
color: var(--color-error);
|
||||
|
||||
.poll-countdown-progress {
|
||||
stroke: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-countdown-progress {
|
||||
fill: transparent;
|
||||
stroke: var(--color-primary);
|
||||
stroke-linecap: round;
|
||||
stroke-width: 2;
|
||||
|
||||
transition: stroke-dashoffset 2s, stroke 0.2s;
|
||||
}
|
||||
|
||||
.poll-quiz-help {
|
||||
margin: -0.625rem 0 -0.625rem auto;
|
||||
.Message:not(.own) & {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.Button {
|
||||
font-size: 1rem;
|
||||
text-transform: none;
|
||||
|
||||
.Message.own & {
|
||||
--color-primary-shade-rgb: var(--color-accent-own);
|
||||
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
> .Button {
|
||||
margin-bottom: 0.1875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
min-width: 50vw;
|
||||
}
|
||||
}
|
||||
@ -1,357 +0,0 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type React from '../../../lib/teact/teact';
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer,
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
|
||||
import { selectPeer } from '../../../global/selectors';
|
||||
import { formatMediaDuration } from '../../../util/dates/oldDateFormat';
|
||||
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Button from '../../ui/Button';
|
||||
import CheckboxGroup from '../../ui/CheckboxGroup';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import PollOption from './PollOption';
|
||||
|
||||
import './Poll.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
poll: ApiPoll;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onSendVote: (options: string[]) => void;
|
||||
};
|
||||
|
||||
const SOLUTION_CONTAINER_ID = '#middle-column-portals';
|
||||
const SOLUTION_DURATION = 5000;
|
||||
const TIMER_RADIUS = 6;
|
||||
const TIMER_CIRCUMFERENCE = TIMER_RADIUS * 2 * Math.PI;
|
||||
const TIMER_UPDATE_INTERVAL = 1000;
|
||||
const NBSP = '\u00A0';
|
||||
|
||||
const Poll: FC<OwnProps> = ({
|
||||
message,
|
||||
poll,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onSendVote,
|
||||
}) => {
|
||||
const {
|
||||
loadMessage, openPollResults, requestConfetti, showNotification,
|
||||
} = getActions();
|
||||
|
||||
const { id: messageId, chatId } = message;
|
||||
const { summary, results } = poll;
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [chosenOptions, setChosenOptions] = useState<string[]>([]);
|
||||
const [wasSubmitted, setWasSubmitted] = useState<boolean>(false);
|
||||
const [closePeriod, setClosePeriod] = useState<number>(() => (
|
||||
!summary.closed && summary.closeDate && summary.closeDate > 0
|
||||
? Math.min(summary.closeDate - getServerTime(), summary.closePeriod!)
|
||||
: 0
|
||||
));
|
||||
const countdownRef = useRef<HTMLDivElement>();
|
||||
const timerCircleRef = useRef<SVGCircleElement>();
|
||||
const { results: voteResults, totalVoters } = results;
|
||||
const hasVoted = voteResults && voteResults.some((r) => r.isChosen);
|
||||
const canVote = !summary.closed && !hasVoted;
|
||||
const canViewResult = !canVote && summary.isPublic && Number(results.totalVoters) > 0;
|
||||
const isMultiple = canVote && summary.multipleChoice;
|
||||
const recentVoterIds = results.recentVoterIds;
|
||||
const maxVotersCount = voteResults ? Math.max(...voteResults.map((r) => r.votersCount)) : totalVoters;
|
||||
const correctResults = useMemo(() => {
|
||||
return voteResults?.filter((r) => r.isCorrect).map((r) => r.option) || [];
|
||||
}, [voteResults]);
|
||||
const answers = useMemo(() => summary.answers.map((a) => ({
|
||||
label: renderTextWithEntities({
|
||||
text: a.text.text,
|
||||
entities: a.text.entities,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}),
|
||||
value: a.option,
|
||||
hidden: Boolean(summary.quiz && summary.closePeriod && closePeriod <= 0),
|
||||
})), [
|
||||
closePeriod, observeIntersectionForLoading, observeIntersectionForPlaying,
|
||||
summary.answers, summary.closePeriod, summary.quiz,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const chosen = poll.results.results?.find((result) => result.isChosen);
|
||||
if (isSubmitting && chosen) {
|
||||
if (chosen.isCorrect) {
|
||||
requestConfetti({});
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [isSubmitting, poll.results.results, requestConfetti]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (closePeriod > 0) {
|
||||
window.setTimeout(() => setClosePeriod(closePeriod - 1), TIMER_UPDATE_INTERVAL);
|
||||
}
|
||||
if (!timerCircleRef.current) return;
|
||||
|
||||
if (closePeriod <= 5) {
|
||||
countdownRef.current!.classList.add('hurry-up');
|
||||
}
|
||||
|
||||
const strokeDashOffset = ((summary.closePeriod! - closePeriod) / summary.closePeriod!) * TIMER_CIRCUMFERENCE;
|
||||
timerCircleRef.current.setAttribute('stroke-dashoffset', `-${strokeDashOffset}`);
|
||||
}, [closePeriod, summary.closePeriod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (summary.quiz && (closePeriod <= 0 || (hasVoted && !summary.closed))) {
|
||||
loadMessage({ chatId, messageId });
|
||||
}
|
||||
}, [chatId, closePeriod, hasVoted, loadMessage, messageId, summary.closed, summary.quiz]);
|
||||
|
||||
// If the client time is not synchronized, the poll must be updated after the closePeriod time has expired.
|
||||
useEffect(() => {
|
||||
let timer: number | undefined;
|
||||
|
||||
if (summary.quiz && !summary.closed && summary.closePeriod && summary.closePeriod > 0) {
|
||||
timer = window.setTimeout(() => {
|
||||
loadMessage({ chatId, messageId });
|
||||
}, summary.closePeriod * 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [canVote, chatId, loadMessage, messageId, summary.closePeriod, summary.closed, summary.quiz]);
|
||||
|
||||
const recentVoters = useMemo(() => {
|
||||
// No need for expensive global updates on chats or users, so we avoid them
|
||||
const global = getGlobal();
|
||||
return recentVoterIds ? recentVoterIds.reduce((result: ApiPeer[], id) => {
|
||||
const peer = selectPeer(global, id);
|
||||
if (peer) {
|
||||
result.push(peer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []) : [];
|
||||
}, [recentVoterIds]);
|
||||
|
||||
const handleRadioChange = useLastCallback((option: string) => {
|
||||
setChosenOptions([option]);
|
||||
setIsSubmitting(true);
|
||||
setWasSubmitted(true);
|
||||
onSendVote([option]);
|
||||
});
|
||||
|
||||
const handleCheckboxChange = useLastCallback((options: string[]) => {
|
||||
setChosenOptions(options);
|
||||
});
|
||||
|
||||
const handleVoteClick = useLastCallback(() => {
|
||||
setIsSubmitting(true);
|
||||
setWasSubmitted(true);
|
||||
onSendVote(chosenOptions);
|
||||
});
|
||||
|
||||
const handleViewResultsClick = useLastCallback(() => {
|
||||
openPollResults({ chatId, messageId });
|
||||
});
|
||||
|
||||
const showSolution = useLastCallback(() => {
|
||||
showNotification({
|
||||
localId: getMessageKey(message),
|
||||
message: poll.results.solution!,
|
||||
messageEntities: poll.results.solutionEntities,
|
||||
duration: SOLUTION_DURATION,
|
||||
containerSelector: SOLUTION_CONTAINER_ID,
|
||||
});
|
||||
});
|
||||
|
||||
// Show the solution to quiz if the answer was incorrect
|
||||
useEffect(() => {
|
||||
if (wasSubmitted && hasVoted && summary.quiz && results.results && poll.results.solution) {
|
||||
const correctResult = results.results.find((r) => r.isChosen && r.isCorrect);
|
||||
if (!correctResult) {
|
||||
showSolution();
|
||||
}
|
||||
}
|
||||
}, [hasVoted, wasSubmitted, results.results, summary.quiz, poll.results.solution]);
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
function renderResultOption(answer: ApiPollAnswer) {
|
||||
return (
|
||||
<PollOption
|
||||
key={answer.option}
|
||||
shouldAnimate={wasSubmitted || !canVote}
|
||||
answer={answer}
|
||||
voteResults={voteResults}
|
||||
totalVoters={totalVoters}
|
||||
maxVotersCount={maxVotersCount}
|
||||
correctResults={correctResults}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRecentVoters() {
|
||||
return (
|
||||
recentVoters.length > 0 && (
|
||||
<div className="poll-recent-voters">
|
||||
<AvatarList
|
||||
size="micro"
|
||||
peers={recentVoters}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Poll" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||
<div className="poll-question">
|
||||
{renderTextWithEntities({
|
||||
text: summary.question.text,
|
||||
entities: summary.question.entities,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
})}
|
||||
</div>
|
||||
<div className="poll-type">
|
||||
{lang(getPollTypeString(summary))}
|
||||
{renderRecentVoters()}
|
||||
{closePeriod > 0 && canVote && (
|
||||
<div ref={countdownRef} className="poll-countdown">
|
||||
<span>{formatMediaDuration(closePeriod)}</span>
|
||||
<svg width="16px" height="16px">
|
||||
<circle
|
||||
ref={timerCircleRef}
|
||||
cx="8"
|
||||
cy="8"
|
||||
r={TIMER_RADIUS}
|
||||
className="poll-countdown-progress"
|
||||
transform="rotate(-90, 8, 8)"
|
||||
stroke-dasharray={TIMER_CIRCUMFERENCE}
|
||||
stroke-dashoffset="0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{summary.quiz && poll.results.solution && !canVote && (
|
||||
<Button
|
||||
round
|
||||
size="tiny"
|
||||
color="translucent"
|
||||
className="poll-quiz-help"
|
||||
onClick={showSolution}
|
||||
ariaLabel="Show Solution"
|
||||
iconName="lamp"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{canVote && (
|
||||
<div
|
||||
className="poll-answers"
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
{isMultiple
|
||||
? (
|
||||
<CheckboxGroup
|
||||
options={answers}
|
||||
selected={chosenOptions}
|
||||
onChange={handleCheckboxChange}
|
||||
disabled={message.isScheduled || isSubmitting}
|
||||
loadingOptions={isSubmitting ? chosenOptions : undefined}
|
||||
isRound
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<RadioGroup
|
||||
name={`poll-${messageId}`}
|
||||
options={answers}
|
||||
onChange={handleRadioChange}
|
||||
disabled={message.isScheduled || isSubmitting}
|
||||
loadingOption={isSubmitting ? chosenOptions[0] : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!canVote && (
|
||||
<div className="poll-results">
|
||||
{summary.answers.map(renderResultOption)}
|
||||
</div>
|
||||
)}
|
||||
{!canViewResult && !isMultiple && (
|
||||
<div className="poll-voters-count">{getReadableVotersCount(lang, summary.quiz, results.totalVoters)}</div>
|
||||
)}
|
||||
{isMultiple && (
|
||||
<Button
|
||||
isText
|
||||
disabled={chosenOptions.length === 0}
|
||||
size="tiny"
|
||||
onClick={handleVoteClick}
|
||||
>
|
||||
{lang('PollSubmitVotes')}
|
||||
</Button>
|
||||
)}
|
||||
{canViewResult && (
|
||||
<Button
|
||||
isText
|
||||
size="tiny"
|
||||
onClick={handleViewResultsClick}
|
||||
>
|
||||
{lang('PollViewResults')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getPollTypeString(summary: ApiPoll['summary']) {
|
||||
// When we just created the poll, some properties don't exist.
|
||||
if (typeof summary.isPublic === 'undefined') {
|
||||
return NBSP;
|
||||
}
|
||||
|
||||
if (summary.closed) {
|
||||
return 'FinalResults';
|
||||
}
|
||||
|
||||
if (summary.quiz) {
|
||||
return summary.isPublic ? 'QuizPoll' : 'AnonymousQuizPoll';
|
||||
}
|
||||
|
||||
return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll';
|
||||
}
|
||||
|
||||
function getReadableVotersCount(lang: OldLangFn, isQuiz: true | undefined, count?: number) {
|
||||
if (!count) {
|
||||
return lang(isQuiz ? 'Chat.Quiz.TotalVotesEmpty' : 'Chat.Poll.TotalVotesResultEmpty');
|
||||
}
|
||||
|
||||
return lang(isQuiz ? 'Answer' : 'Vote', count, 'i');
|
||||
}
|
||||
|
||||
function stopPropagation(e: React.MouseEvent<HTMLDivElement>) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
export default memo(Poll);
|
||||
@ -1,131 +0,0 @@
|
||||
.PollOption {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.poll-option-text {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.poll-option-share {
|
||||
position: relative;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 1.75rem;
|
||||
margin-top: 0.125rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: right;
|
||||
|
||||
&.limit-width {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-option-chosen {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -5px;
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
font-size: 0.75rem;
|
||||
color: var(--background-color);
|
||||
text-align: center;
|
||||
|
||||
background: var(--accent-color);
|
||||
|
||||
&.wrong {
|
||||
background: var(--color-error);
|
||||
}
|
||||
|
||||
.poll-option-icon {
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
&.animate {
|
||||
opacity: 0;
|
||||
|
||||
animation-name: PollOptionIconAnimate;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 0.09s;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-option-right {
|
||||
flex-grow: 1;
|
||||
line-height: 1.3125rem;
|
||||
}
|
||||
|
||||
.poll-option-answer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poll-option-line {
|
||||
position: relative;
|
||||
transform-origin: 0 0;
|
||||
|
||||
width: 0;
|
||||
min-width: 0.5rem;
|
||||
height: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
background: var(--accent-color);
|
||||
|
||||
transition: transform 0.3s;
|
||||
transition-delay: 0.09s;
|
||||
}
|
||||
|
||||
.poll-line {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: -27px;
|
||||
|
||||
width: 30px;
|
||||
height: 35px;
|
||||
|
||||
stroke-dasharray: 0, 200%;
|
||||
stroke-dashoffset: 0;
|
||||
|
||||
transition: stroke-dashoffset 0.3s, stroke-dasharray 0.3s;
|
||||
}
|
||||
|
||||
.poll-line path {
|
||||
fill: none;
|
||||
stroke: var(--accent-color);
|
||||
stroke-linecap: round;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.wrong {
|
||||
.poll-option-line {
|
||||
background: var(--color-error);
|
||||
}
|
||||
|
||||
.poll-line path {
|
||||
stroke: var(--color-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes PollOptionIconAnimate {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiPollAnswer, ApiPollResult } from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Icon from '../../common/icons/Icon';
|
||||
|
||||
import './PollOption.scss';
|
||||
|
||||
type OwnProps = {
|
||||
answer: ApiPollAnswer;
|
||||
voteResults?: ApiPollResult[];
|
||||
totalVoters?: number;
|
||||
maxVotersCount?: number;
|
||||
correctResults: string[];
|
||||
shouldAnimate: boolean;
|
||||
};
|
||||
|
||||
const PollOption: FC<OwnProps> = ({
|
||||
answer,
|
||||
voteResults,
|
||||
totalVoters,
|
||||
maxVotersCount,
|
||||
correctResults,
|
||||
shouldAnimate,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const result = voteResults && voteResults.find((r) => r.option === answer.option);
|
||||
const correctAnswer = correctResults.length === 0 || correctResults.indexOf(answer.option) !== -1;
|
||||
const showIcon = (correctResults.length > 0 && correctAnswer) || (result?.isChosen);
|
||||
const answerPercent = result ? getPercentage(result.votersCount, totalVoters || 0) : 0;
|
||||
const [finalPercent, setFinalPercent] = useState(shouldAnimate ? 0 : answerPercent);
|
||||
const lineWidth = result ? getPercentage(result.votersCount, maxVotersCount || 0) : 0;
|
||||
const isAnimationDoesNotStart = finalPercent !== answerPercent;
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAnimate) {
|
||||
setFinalPercent(answerPercent);
|
||||
}
|
||||
}, [shouldAnimate, answerPercent]);
|
||||
|
||||
if (!voteResults || !result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lineStyle = `width: ${lineWidth}%; transform:scaleX(${isAnimationDoesNotStart ? 0 : 1})`;
|
||||
|
||||
return (
|
||||
<div className="PollOption" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className={`poll-option-share ${answerPercent === '100' ? 'limit-width' : ''}`}>
|
||||
{answerPercent}
|
||||
%
|
||||
{showIcon && (
|
||||
<span className={buildClassName(
|
||||
'poll-option-chosen',
|
||||
!correctAnswer && 'wrong',
|
||||
shouldAnimate && 'animate',
|
||||
)}
|
||||
>
|
||||
<Icon name={correctAnswer ? 'check' : 'close'} className="poll-option-icon" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="poll-option-right">
|
||||
<div className="poll-option-text" dir="auto">
|
||||
{renderTextWithEntities({
|
||||
text: answer.text.text,
|
||||
entities: answer.text.entities,
|
||||
})}
|
||||
</div>
|
||||
<div className={buildClassName('poll-option-answer', showIcon && !correctAnswer && 'wrong')}>
|
||||
{shouldAnimate && (
|
||||
<svg
|
||||
className="poll-line"
|
||||
style={!isAnimationDoesNotStart ? 'stroke-dasharray: 100% 200%; stroke-dashoffset: -44' : ''}
|
||||
>
|
||||
<path d="M4.47 5.33v13.6a9 9 0 009 9h13" />
|
||||
</svg>
|
||||
)}
|
||||
<div
|
||||
className="poll-option-line"
|
||||
style={lineStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getPercentage(value: number, total: number) {
|
||||
return total > 0 ? ((value / total) * 100).toFixed() : 0;
|
||||
}
|
||||
|
||||
export default PollOption;
|
||||
@ -38,7 +38,7 @@ export type OwnProps<T> = {
|
||||
video: ApiVideo | ApiMediaExtendedPreview;
|
||||
lastPlaybackTimestamp?: number;
|
||||
isOwn?: boolean;
|
||||
isInWebPage?: boolean;
|
||||
isNestedMedia?: boolean;
|
||||
noAvatars?: boolean;
|
||||
canAutoLoad?: boolean;
|
||||
canAutoPlay?: boolean;
|
||||
@ -65,7 +65,7 @@ const Video = <T,>({
|
||||
id,
|
||||
video,
|
||||
isOwn,
|
||||
isInWebPage,
|
||||
isNestedMedia,
|
||||
noAvatars,
|
||||
canAutoLoad,
|
||||
canAutoPlay,
|
||||
@ -209,8 +209,8 @@ const Video = <T,>({
|
||||
width, height,
|
||||
} = dimensions || (
|
||||
isPaidPreview
|
||||
? calculateExtendedPreviewDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile)
|
||||
: calculateVideoDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile)
|
||||
? calculateExtendedPreviewDimensions(video, Boolean(isOwn), asForwarded, isNestedMedia, noAvatars, isMobile)
|
||||
: calculateVideoDimensions(video, Boolean(isOwn), asForwarded, isNestedMedia, noAvatars, isMobile)
|
||||
);
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>, isFromSpinner?: boolean) => {
|
||||
|
||||
@ -247,7 +247,7 @@ const WebPage = ({
|
||||
<Photo
|
||||
photo={photo}
|
||||
isOwn={message?.isOutgoing}
|
||||
isInWebPage
|
||||
isNestedMedia
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoad}
|
||||
@ -265,7 +265,7 @@ const WebPage = ({
|
||||
<Video
|
||||
video={video}
|
||||
isOwn={message?.isOutgoing}
|
||||
isInWebPage
|
||||
isNestedMedia
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoad}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessage, ApiPoll, ApiWebPage } from '../../../../api/types';
|
||||
import type { ApiMessage, ApiMessagePoll, ApiWebPage } from '../../../../api/types';
|
||||
import type { IAlbum } from '../../../../types';
|
||||
|
||||
import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
|
||||
@ -26,7 +26,7 @@ export function buildContentClassName(
|
||||
peerColorClass,
|
||||
hasOutsideReactions,
|
||||
}: {
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
webPage?: ApiWebPage;
|
||||
hasSubheader?: boolean;
|
||||
isCustomShape?: boolean | number;
|
||||
@ -61,8 +61,10 @@ export function buildContentClassName(
|
||||
const isInvertibleMedia = photo || (video && !isRoundVideo) || album || webPage;
|
||||
|
||||
const classNames = [MESSAGE_CONTENT_CLASS_NAME];
|
||||
const isMedia = storyData || photo || video || location || invoice?.extendedMedia || paidMedia;
|
||||
const hasText = text || location?.mediaType === 'venue' || isGeoLiveActive || hasFactCheck;
|
||||
const pollMedia = poll?.attachedMedia;
|
||||
const isMedia = storyData || photo || video || location || invoice?.extendedMedia || paidMedia
|
||||
|| pollMedia?.photo || pollMedia?.video || pollMedia?.location;
|
||||
const hasText = text || location?.mediaType === 'venue' || isGeoLiveActive || hasFactCheck || poll;
|
||||
const isMediaWithNoText = isMedia && !hasText;
|
||||
const hasInlineKeyboard = Boolean(message.inlineButtons);
|
||||
const isViaBot = Boolean(message.viaBotId);
|
||||
|
||||
@ -26,7 +26,7 @@ export function calculateMediaDimensions({
|
||||
media,
|
||||
messageText,
|
||||
isOwn,
|
||||
isInWebPage,
|
||||
isNestedMedia,
|
||||
asForwarded,
|
||||
noAvatars,
|
||||
isMobile,
|
||||
@ -34,19 +34,19 @@ export function calculateMediaDimensions({
|
||||
media: ApiPhoto | ApiVideo | ApiMediaExtendedPreview;
|
||||
messageText?: string;
|
||||
isOwn?: boolean;
|
||||
isInWebPage?: boolean;
|
||||
isNestedMedia?: boolean;
|
||||
asForwarded?: boolean;
|
||||
noAvatars?: boolean;
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
const isPhoto = media.mediaType === 'photo';
|
||||
const isVideo = media.mediaType === 'video';
|
||||
const isWebPagePhoto = isPhoto && isInWebPage;
|
||||
const isWebPageVideo = isVideo && isInWebPage;
|
||||
const isWebPagePhoto = isPhoto && isNestedMedia;
|
||||
const isWebPageVideo = isVideo && isNestedMedia;
|
||||
const { width, height } = isPhoto
|
||||
? calculateInlineImageDimensions(media, isOwn, asForwarded, isWebPagePhoto, noAvatars, isMobile)
|
||||
: isVideo ? calculateVideoDimensions(media, isOwn, asForwarded, isWebPageVideo, noAvatars, isMobile)
|
||||
: calculateExtendedPreviewDimensions(media, isOwn, asForwarded, isInWebPage, noAvatars, isMobile);
|
||||
: calculateExtendedPreviewDimensions(media, isOwn, asForwarded, isNestedMedia, noAvatars, isMobile);
|
||||
|
||||
const minMediaWidth = getMinMediaWidth(messageText, isMobile);
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ export default function useInnerHandlers({
|
||||
}) {
|
||||
const {
|
||||
openChat, openChatWithDraft, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
||||
markMessagesRead, cancelUploadMedia, sendPollVote, openForwardMenu,
|
||||
markMessagesRead, cancelUploadMedia, openForwardMenu,
|
||||
openChatLanguageModal, openThread, openStoryViewer, searchChatMediaMessages,
|
||||
} = getActions();
|
||||
|
||||
@ -198,10 +198,6 @@ export default function useInnerHandlers({
|
||||
cancelUploadMedia({ chatId, messageId });
|
||||
});
|
||||
|
||||
const handleVoteSend = useLastCallback((options: string[]) => {
|
||||
sendPollVote({ chatId, messageId, options });
|
||||
});
|
||||
|
||||
const handleGroupForward = useLastCallback(() => {
|
||||
openForwardMenu({ fromChatId: chatId, groupedId });
|
||||
});
|
||||
@ -305,7 +301,6 @@ export default function useInnerHandlers({
|
||||
handleOpenThread,
|
||||
handleReadMedia,
|
||||
handleCancelUpload,
|
||||
handleVoteSend,
|
||||
handleGroupForward,
|
||||
handleForward,
|
||||
handleFocus,
|
||||
|
||||
143
src/components/middle/message/poll/Poll.module.scss
Normal file
143
src/components/middle/message/poll/Poll.module.scss
Normal file
@ -0,0 +1,143 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
min-width: 15rem;
|
||||
margin-bottom: 0.75rem; // Space for message meta
|
||||
padding-top: 0.125rem;
|
||||
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.explanation,
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.explanation {
|
||||
padding-block: 0.375rem;
|
||||
}
|
||||
|
||||
.explanationMedia,
|
||||
.metaVoters {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.explanationMedia {
|
||||
:global(.media-inner) {
|
||||
--border-top-left-radius: var(--border-radius-messages-small);
|
||||
--border-top-right-radius: var(--border-radius-messages-small);
|
||||
--border-bottom-left-radius: var(--border-radius-messages-small);
|
||||
--border-bottom-right-radius: var(--border-radius-messages-small);
|
||||
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.explanationHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.explanationCloseButton {
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
right: 0.125rem;
|
||||
}
|
||||
|
||||
.explanationTitle {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.explanationText {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.questionContainer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.question {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
min-width: 0;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.explanationToggleButton {
|
||||
display: flex;
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metaInfo {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metaLabel {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer,
|
||||
.footerContentSlide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footerContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.footerButton {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.footerSubtext {
|
||||
display: inline;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.footerStatic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stickerPreview {
|
||||
position: relative;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
}
|
||||
764
src/components/middle/message/poll/Poll.tsx
Normal file
764
src/components/middle/message/poll/Poll.tsx
Normal file
@ -0,0 +1,764 @@
|
||||
import {
|
||||
memo,
|
||||
type TeactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiLocation,
|
||||
ApiMessagePoll,
|
||||
ApiPoll,
|
||||
ApiSticker,
|
||||
MediaContent,
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import { type MediaViewerMedia, MediaViewerOrigin, type ThemeKey } from '../../../../types';
|
||||
|
||||
import { getMessageHtmlId } from '../../../../global/helpers';
|
||||
import { selectPeer } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { buildCollectionByKey, shuffle } from '../../../../util/iteratees';
|
||||
import { NEXT_ARROW_REPLACEMENT, PREVIOUS_ARROW_REPLACEMENT } from '../../../../util/localization/format';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useTimeout from '../../../../hooks/schedulers/useTimeout';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import AvatarList from '../../../common/AvatarList';
|
||||
import CompactMapPreview from '../../../common/CompactMapPreview';
|
||||
import Document from '../../../common/Document';
|
||||
import PeerColorWrapper from '../../../common/PeerColorWrapper';
|
||||
import StickerView from '../../../common/StickerView';
|
||||
import Button from '../../../ui/Button';
|
||||
import TextTimer from '../../../ui/TextTimer';
|
||||
import Transition from '../../../ui/Transition';
|
||||
import Photo from '../Photo';
|
||||
import Video from '../Video';
|
||||
import PollOption from './PollOption';
|
||||
|
||||
import styles from './Poll.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
poll: ApiMessagePoll;
|
||||
messageText?: ApiFormattedText;
|
||||
theme: ThemeKey;
|
||||
isInScheduled?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
};
|
||||
|
||||
const ATTACHED_MAP_WIDTH = 350;
|
||||
const ATTACHED_MAP_HEIGHT = 200;
|
||||
const ATTACHED_MAP_ZOOM = 15;
|
||||
const STICKER_PREVIEW_SIZE = 96;
|
||||
const VOTE_TIMEOUT = 5000;
|
||||
|
||||
const Poll = ({
|
||||
chatId,
|
||||
messageId,
|
||||
poll,
|
||||
messageText,
|
||||
theme,
|
||||
isInScheduled,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps) => {
|
||||
const {
|
||||
openMapModal,
|
||||
openMediaViewer,
|
||||
openPollResults,
|
||||
requestConfetti,
|
||||
sendPollVote,
|
||||
loadMessage,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
const serverTime = getServerTime();
|
||||
|
||||
const { summary, results, attachedMedia } = poll;
|
||||
const { answers, question, isMultipleChoice } = summary;
|
||||
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
||||
const [isExplanationOpen, setIsExplanationOpen] = useState(false);
|
||||
const [isSendingVote, setIsSendingVote] = useState(false);
|
||||
const [isViewingAuthorResults, setIsViewingAuthorResults] = useState(false);
|
||||
const [answerOrder] = useState<string[]>(() => (
|
||||
buildAnswerOrder(answers, summary.shouldShuffleAnswers)
|
||||
));
|
||||
|
||||
const hasChosenAnswer = useMemo(
|
||||
() => Object.values(results.resultByOption || {}).some((result) => result.isChosen),
|
||||
[results.resultByOption],
|
||||
);
|
||||
const activeCloseDate = !summary.isClosed && summary.closeDate && summary.closeDate > serverTime
|
||||
? summary.closeDate
|
||||
: undefined;
|
||||
const areResultsHiddenForCurrentUser = Boolean(
|
||||
summary.shouldHideResultsUntilClose && activeCloseDate && !summary.isCreator,
|
||||
);
|
||||
const hasMaskedResults = areResultsHiddenForCurrentUser && hasChosenAnswer;
|
||||
const canVote = !summary.isClosed && !hasChosenAnswer;
|
||||
const hasExplanation = summary.isQuiz && Boolean(results.solution?.trim() || results.solutionMedia);
|
||||
const hasOptionMedia = useMemo(
|
||||
() => answers.some((answer) => hasPollOptionMedia(answer)),
|
||||
[answers],
|
||||
);
|
||||
const selectedOptionsSet = useMemo(() => new Set(selectedOptions), [selectedOptions]);
|
||||
const hasResultData = Boolean(results.resultByOption);
|
||||
const totalVoters = results.totalVoters || 0;
|
||||
const canToggleAuthorResults = summary.isCreator && canVote && hasResultData && totalVoters > 0;
|
||||
const areInlineResultsVisible = hasResultData && (
|
||||
(!canVote && !areResultsHiddenForCurrentUser) || isViewingAuthorResults
|
||||
);
|
||||
const canShowResultsPanel = hasChosenAnswer && summary.isPublic && hasResultData && !areResultsHiddenForCurrentUser;
|
||||
|
||||
useEffect(() => {
|
||||
if (!canVote) {
|
||||
setSelectedOptions([]);
|
||||
setIsSendingVote(false);
|
||||
}
|
||||
}, [canVote]);
|
||||
|
||||
useTimeout(() => {
|
||||
setIsSendingVote(false);
|
||||
}, isSendingVote ? VOTE_TIMEOUT : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!summary.isQuiz || !isSendingVote || areResultsHiddenForCurrentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resultByOption = results.resultByOption;
|
||||
if (!resultByOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pollResults = Object.values(resultByOption);
|
||||
const chosenResults = pollResults.filter((result) => result.isChosen);
|
||||
const correctResults = pollResults.filter((result) => result.isCorrect);
|
||||
|
||||
if (
|
||||
!chosenResults.length
|
||||
|| chosenResults.length !== correctResults.length
|
||||
|| !chosenResults.every((result) => result.isCorrect)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestConfetti({});
|
||||
}, [areResultsHiddenForCurrentUser, isSendingVote, results.resultByOption, summary.isQuiz]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canToggleAuthorResults) {
|
||||
setIsViewingAuthorResults(false);
|
||||
}
|
||||
}, [canToggleAuthorResults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasExplanation) {
|
||||
setIsExplanationOpen(false);
|
||||
}
|
||||
}, [hasExplanation]);
|
||||
|
||||
const answersByOption = useMemo(() => buildCollectionByKey(answers, 'option'), [answers]);
|
||||
|
||||
const orderedAnswers = useMemo(() => {
|
||||
const seen = new Set(answerOrder);
|
||||
const ordered = answerOrder
|
||||
.map((option) => answersByOption[option])
|
||||
.filter(Boolean);
|
||||
const appended = answers.filter((answer) => !seen.has(answer.option));
|
||||
|
||||
return [...ordered, ...appended];
|
||||
}, [answerOrder, answers, answersByOption]);
|
||||
|
||||
const previewItems = useMemo(() => {
|
||||
const items: {
|
||||
key: string;
|
||||
media: MediaViewerMedia;
|
||||
}[] = [];
|
||||
|
||||
if (attachedMedia?.photo) {
|
||||
items.push({ key: 'attached', media: attachedMedia.photo });
|
||||
} else if (attachedMedia?.video) {
|
||||
items.push({ key: 'attached', media: attachedMedia.video });
|
||||
} else if (attachedMedia?.document) {
|
||||
items.push({ key: 'attached', media: attachedMedia.document });
|
||||
}
|
||||
|
||||
if (results.solutionMedia?.photo) {
|
||||
items.push({ key: 'explanation', media: results.solutionMedia.photo });
|
||||
} else if (results.solutionMedia?.video) {
|
||||
items.push({ key: 'explanation', media: results.solutionMedia.video });
|
||||
} else if (results.solutionMedia?.document) {
|
||||
items.push({ key: 'explanation', media: results.solutionMedia.document });
|
||||
}
|
||||
|
||||
orderedAnswers.forEach((answer) => {
|
||||
if (answer.media?.photo) {
|
||||
items.push({ key: answer.option, media: answer.media.photo });
|
||||
} else if (answer.media?.video) {
|
||||
items.push({ key: answer.option, media: answer.media.video });
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [attachedMedia, orderedAnswers, results.solutionMedia]);
|
||||
|
||||
const previewIndexByKey = useMemo(() => {
|
||||
return previewItems.reduce((acc, item, index) => {
|
||||
acc[item.key] = index;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}, [previewItems]);
|
||||
const standaloneMedia = useMemo(() => previewItems.map((item) => item.media), [previewItems]);
|
||||
|
||||
const pollRecentVoters = useMemo(
|
||||
() => areResultsHiddenForCurrentUser ? undefined : resolvePeers(results.recentVoterIds),
|
||||
[areResultsHiddenForCurrentUser, results.recentVoterIds],
|
||||
);
|
||||
|
||||
const submitVote = useLastCallback((options: string[]) => {
|
||||
setIsSendingVote(true);
|
||||
sendPollVote({
|
||||
chatId,
|
||||
messageId,
|
||||
options,
|
||||
});
|
||||
});
|
||||
|
||||
const handleSelectOption = useLastCallback((option: string) => {
|
||||
if (!canVote || isSendingVote || isViewingAuthorResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMultipleChoice) {
|
||||
submitVote([option]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedOptions((current) => {
|
||||
if (current.includes(option)) {
|
||||
return current.filter((currentOption) => currentOption !== option);
|
||||
}
|
||||
|
||||
return [...current, option];
|
||||
});
|
||||
});
|
||||
|
||||
const handleSendVote = useLastCallback(() => {
|
||||
if (!canVote || !selectedOptions.length || isSendingVote) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitVote(selectedOptions);
|
||||
});
|
||||
|
||||
const handleOpenPreview = useLastCallback((previewIndex: number) => {
|
||||
if (!standaloneMedia.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
openMediaViewer({
|
||||
chatId,
|
||||
messageId,
|
||||
mediaIndex: previewIndex,
|
||||
standaloneMedia,
|
||||
origin: MediaViewerOrigin.PollPreview,
|
||||
});
|
||||
});
|
||||
|
||||
const handleOpenLocation = useLastCallback((location: ApiLocation) => {
|
||||
openMapModal({
|
||||
geoPoint: location.geo,
|
||||
zoom: ATTACHED_MAP_ZOOM,
|
||||
});
|
||||
});
|
||||
|
||||
const handleOpenResults = useLastCallback(() => {
|
||||
if (!canShowResultsPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
openPollResults({ chatId, messageId });
|
||||
});
|
||||
|
||||
const handleToggleAuthorResults = useLastCallback(() => {
|
||||
if (!canToggleAuthorResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsViewingAuthorResults((current) => !current);
|
||||
});
|
||||
|
||||
const handleToggleExplanation = useLastCallback(() => {
|
||||
setIsExplanationOpen((current) => !current);
|
||||
});
|
||||
|
||||
const handleCloseDateEnd = useLastCallback(() => {
|
||||
loadMessage({ chatId, messageId });
|
||||
});
|
||||
|
||||
const attachedPreviewIndex = attachedMedia && previewIndexByKey.attached;
|
||||
const explanationPreviewIndex = results.solutionMedia && previewIndexByKey.explanation;
|
||||
const attachedMediaEl = attachedMedia && renderPollMedia({
|
||||
content: attachedMedia,
|
||||
theme,
|
||||
previewIndex: attachedPreviewIndex,
|
||||
previewId: attachedPreviewIndex !== undefined ? getPollPreviewId(messageId, attachedPreviewIndex) : undefined,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onOpenLocation: handleOpenLocation,
|
||||
onOpenPreview: handleOpenPreview,
|
||||
});
|
||||
const explanationMedia = results.solutionMedia && renderPollMedia({
|
||||
content: results.solutionMedia,
|
||||
theme,
|
||||
previewIndex: explanationPreviewIndex,
|
||||
previewId: explanationPreviewIndex !== undefined ? getPollPreviewId(messageId, explanationPreviewIndex) : undefined,
|
||||
isNestedMedia: true,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onOpenLocation: handleOpenLocation,
|
||||
onOpenPreview: handleOpenPreview,
|
||||
locationWidth: ATTACHED_MAP_WIDTH,
|
||||
locationHeight: ATTACHED_MAP_HEIGHT,
|
||||
});
|
||||
|
||||
const questionText = useMemo(() => renderTextWithEntities({
|
||||
text: question.text,
|
||||
entities: question.entities,
|
||||
}), [question.entities, question.text]);
|
||||
const descriptionText = useMemo(() => messageText && renderTextWithEntities({
|
||||
text: messageText.text,
|
||||
entities: messageText.entities,
|
||||
}), [messageText]);
|
||||
const explanationText = useMemo(() => results.solution?.trim() ? renderTextWithEntities({
|
||||
text: results.solution,
|
||||
entities: results.solutionEntities,
|
||||
}) : undefined, [results.solution, results.solutionEntities]);
|
||||
const footerSubtext = activeCloseDate
|
||||
? lang(areResultsHiddenForCurrentUser ? 'PollResultsTime' : 'PollEndsTime', {
|
||||
time: (
|
||||
<TextTimer
|
||||
endsAt={activeCloseDate}
|
||||
mode="countdown"
|
||||
shouldShowZeroOnEnd
|
||||
onEnd={handleCloseDateEnd}
|
||||
/>
|
||||
),
|
||||
}, { withNodes: true })
|
||||
: undefined;
|
||||
|
||||
const footerContent = useMemo(() => {
|
||||
const renderFooterBody = (label: TeactNode) => (
|
||||
<div className={styles.footerContent}>
|
||||
<span>{label}</span>
|
||||
{Boolean(footerSubtext) && (
|
||||
<div
|
||||
className={styles.footerSubtext}
|
||||
dir="auto"
|
||||
>
|
||||
{footerSubtext}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (canVote && isMultipleChoice && selectedOptions.length) {
|
||||
return (
|
||||
<Button
|
||||
className={styles.footerButton}
|
||||
disabled={isSendingVote}
|
||||
noForcedUpperCase
|
||||
isText
|
||||
inline
|
||||
size="smaller"
|
||||
color="adaptive"
|
||||
onClick={handleSendVote}
|
||||
>
|
||||
{renderFooterBody(lang(summary.isQuiz ? 'PollSubmitAnswers' : 'PollSubmitVotes'))}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (canToggleAuthorResults) {
|
||||
const label = isViewingAuthorResults
|
||||
? lang(summary.isQuiz ? 'PollBackToAnswer' : 'PollBackToVote', undefined, {
|
||||
withNodes: true,
|
||||
specialReplacement: PREVIOUS_ARROW_REPLACEMENT,
|
||||
})
|
||||
: lang(
|
||||
summary.isQuiz ? 'PollAnswerCountButton' : 'PollVoteCountButton',
|
||||
{ count: totalVoters },
|
||||
{
|
||||
withNodes: true,
|
||||
pluralValue: totalVoters,
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={styles.footerButton}
|
||||
noForcedUpperCase
|
||||
isText
|
||||
inline
|
||||
size="smaller"
|
||||
color="adaptive"
|
||||
onClick={handleToggleAuthorResults}
|
||||
>
|
||||
<Transition
|
||||
activeKey={Number(isViewingAuthorResults)}
|
||||
name={lang.isRtl ? 'slideRtl' : 'slide'}
|
||||
shouldCleanup
|
||||
slideClassName={styles.footerContentSlide}
|
||||
>
|
||||
{renderFooterBody(label)}
|
||||
</Transition>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (canVote && isMultipleChoice) {
|
||||
return (
|
||||
<Button
|
||||
className={styles.footerButton}
|
||||
disabled={isSendingVote || !selectedOptions.length}
|
||||
noForcedUpperCase
|
||||
isText
|
||||
inline
|
||||
size="smaller"
|
||||
color="adaptive"
|
||||
onClick={handleSendVote}
|
||||
>
|
||||
{renderFooterBody(lang(summary.isQuiz ? 'PollSubmitAnswers' : 'PollSubmitVotes'))}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (canShowResultsPanel) {
|
||||
return (
|
||||
<Button
|
||||
className={styles.footerButton}
|
||||
noForcedUpperCase
|
||||
isText
|
||||
inline
|
||||
size="smaller"
|
||||
color="adaptive"
|
||||
onClick={handleOpenResults}
|
||||
>
|
||||
{renderFooterBody(lang('PollViewResults'))}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (totalVoters) {
|
||||
return (
|
||||
<div className={styles.footerStatic}>
|
||||
{renderFooterBody(lang(summary.isQuiz ? 'PollAnsweredCount' : 'VoteCount', {
|
||||
count: totalVoters,
|
||||
}, { pluralValue: totalVoters }))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.footerStatic}>
|
||||
{renderFooterBody(lang(summary.isQuiz ? 'ChatQuizTotalVotesEmpty' : 'ChatPollTotalVotesResultEmpty'))}
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
canShowResultsPanel,
|
||||
canToggleAuthorResults,
|
||||
canVote,
|
||||
isMultipleChoice,
|
||||
isSendingVote,
|
||||
isViewingAuthorResults,
|
||||
lang,
|
||||
selectedOptions.length,
|
||||
summary.isQuiz,
|
||||
footerSubtext,
|
||||
totalVoters,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{attachedMediaEl}
|
||||
<div className={styles.root} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{isExplanationOpen && hasExplanation && (
|
||||
<PeerColorWrapper className={styles.explanation} shouldReset>
|
||||
<div className={styles.explanationHeader}>
|
||||
<span className={styles.explanationTitle}>
|
||||
{lang('PollsSolutionTitle')}
|
||||
</span>
|
||||
{explanationText && (
|
||||
<div className={styles.explanationText} dir="auto">
|
||||
{explanationText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
round
|
||||
size="tiny"
|
||||
color="adaptive"
|
||||
isText
|
||||
iconName="close"
|
||||
ariaLabel={lang('Close')}
|
||||
onClick={handleToggleExplanation}
|
||||
className={styles.explanationCloseButton}
|
||||
/>
|
||||
{explanationMedia && (
|
||||
<div className={styles.explanationMedia}>
|
||||
{explanationMedia}
|
||||
</div>
|
||||
)}
|
||||
</PeerColorWrapper>
|
||||
)}
|
||||
{descriptionText && (
|
||||
<div dir="auto">
|
||||
{descriptionText}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.questionContainer}>
|
||||
<div className={styles.question} dir="auto">
|
||||
{questionText}
|
||||
</div>
|
||||
{hasExplanation && !isExplanationOpen && (
|
||||
<div className={styles.explanationToggleButton}>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="adaptive"
|
||||
isText
|
||||
iconName="lamp"
|
||||
ariaLabel={lang('MediaPollSolutionAria')}
|
||||
onClick={handleToggleExplanation}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.meta}>
|
||||
<div className={styles.metaInfo}>
|
||||
<span className={styles.metaLabel}>
|
||||
{summary.isClosed
|
||||
? lang('FinalResults')
|
||||
: summary.isQuiz
|
||||
? lang(summary.isPublic ? 'QuizPoll' : 'AnonymousQuizPoll')
|
||||
: lang(summary.isPublic ? 'PublicPoll' : 'AnonymousPoll')}
|
||||
</span>
|
||||
{Boolean(pollRecentVoters?.length) && (
|
||||
<AvatarList
|
||||
size="micro"
|
||||
peers={pollRecentVoters}
|
||||
className={styles.metaVoters}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.options}>
|
||||
{orderedAnswers.map((answer) => {
|
||||
const optionResult = results.resultByOption?.[answer.option];
|
||||
const previewIndex = previewIndexByKey[answer.option];
|
||||
|
||||
return (
|
||||
<PollOption
|
||||
key={answer.option}
|
||||
answer={answer}
|
||||
result={optionResult}
|
||||
isQuiz={summary.isQuiz}
|
||||
totalVotersCount={results.totalVoters}
|
||||
hasResults={areInlineResultsVisible || hasMaskedResults}
|
||||
hasMaskedResults={hasMaskedResults}
|
||||
isSendingVote={isSendingVote}
|
||||
isInScheduled={isInScheduled}
|
||||
isSelected={selectedOptionsSet.has(answer.option)}
|
||||
isMultipleChoice={isMultipleChoice}
|
||||
recentVoters={hasMaskedResults ? undefined : resolvePeers(optionResult?.recentVoterIds)}
|
||||
shouldReserveMediaColumn={hasOptionMedia}
|
||||
mediaPreviewId={previewIndex !== undefined
|
||||
? getPollPreviewId(messageId, previewIndex)
|
||||
: undefined}
|
||||
mediaPreviewIndex={previewIndex}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onSelect={handleSelectOption}
|
||||
onOpenMedia={handleOpenPreview}
|
||||
onOpenLocation={handleOpenLocation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{footerContent && (
|
||||
<div className={styles.footer}>
|
||||
{footerContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function buildAnswerOrder(answers: ApiPoll['answers'], shouldShuffle?: true) {
|
||||
const options = answers.map((answer) => answer.option);
|
||||
return shouldShuffle ? shuffle(options) : options;
|
||||
}
|
||||
|
||||
function resolvePeers(peerIds?: string[]) {
|
||||
if (!peerIds?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const global = getGlobal();
|
||||
|
||||
return peerIds
|
||||
.map((peerId) => selectPeer(global, peerId))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getPollPreviewId(messageId: number, previewIndex: number) {
|
||||
return `poll-media${getMessageHtmlId(messageId, previewIndex)}`;
|
||||
}
|
||||
|
||||
function renderPollMedia({
|
||||
content,
|
||||
theme,
|
||||
className,
|
||||
previewIndex,
|
||||
previewId,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onOpenLocation,
|
||||
onOpenPreview,
|
||||
locationWidth = ATTACHED_MAP_WIDTH,
|
||||
locationHeight = ATTACHED_MAP_HEIGHT,
|
||||
isNestedMedia,
|
||||
}: {
|
||||
content: MediaContent;
|
||||
theme: ThemeKey;
|
||||
className?: string;
|
||||
previewIndex?: number;
|
||||
previewId?: string;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onOpenLocation: (location: ApiLocation) => void;
|
||||
onOpenPreview: (previewIndex: number) => void;
|
||||
locationWidth?: number;
|
||||
locationHeight?: number;
|
||||
isNestedMedia?: boolean;
|
||||
}) {
|
||||
if (content.photo) {
|
||||
return (
|
||||
<Photo
|
||||
id={previewId}
|
||||
photo={content.photo}
|
||||
theme={theme}
|
||||
className={className}
|
||||
isNestedMedia={isNestedMedia}
|
||||
canAutoLoad
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
clickArg={previewIndex}
|
||||
onClick={previewIndex !== undefined ? onPreviewClick(onOpenPreview) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.video) {
|
||||
return (
|
||||
<Video
|
||||
id={previewId}
|
||||
video={content.video}
|
||||
className={className}
|
||||
isNestedMedia={isNestedMedia}
|
||||
canAutoLoad
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
clickArg={previewIndex}
|
||||
onClick={previewIndex !== undefined ? onPreviewClick(onOpenPreview) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.document) {
|
||||
return (
|
||||
<Document
|
||||
id={previewId}
|
||||
document={content.document}
|
||||
className={className}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
onMediaClick={previewIndex !== undefined ? () => onOpenPreview(previewIndex) : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.location) {
|
||||
return (
|
||||
<CompactMapPreview
|
||||
className={buildClassName(className, 'media-inner')}
|
||||
geo={content.location.geo}
|
||||
width={locationWidth}
|
||||
height={locationHeight}
|
||||
zoom={ATTACHED_MAP_ZOOM}
|
||||
shouldShowPin={false}
|
||||
onClick={() => onOpenLocation(content.location!)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (content.sticker) {
|
||||
return (
|
||||
<PollSticker
|
||||
sticker={content.sticker}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function PollSticker({
|
||||
sticker,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: {
|
||||
sticker: ApiSticker;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
|
||||
return (
|
||||
<div className={styles.stickerPreview} ref={ref}>
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
sticker={sticker}
|
||||
size={STICKER_PREVIEW_SIZE}
|
||||
shouldLoop
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function hasPollOptionMedia(answer: ApiPoll['answers'][number]) {
|
||||
return Boolean(answer.media?.photo || answer.media?.video || answer.media?.location || answer.media?.sticker);
|
||||
}
|
||||
|
||||
function onPreviewClick(onOpenPreview: (previewIndex: number) => void) {
|
||||
return (previewIndex: number, e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
onOpenPreview(previewIndex);
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(Poll);
|
||||
192
src/components/middle/message/poll/PollOption.module.scss
Normal file
192
src/components/middle/message/poll/PollOption.module.scss
Normal file
@ -0,0 +1,192 @@
|
||||
.root {
|
||||
--poll-option-media-width: 0;
|
||||
--percent-column-width: 2rem;
|
||||
--ui-border-color: var(--secondary-color);
|
||||
--ui-accent-color: var(--accent-color);
|
||||
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(var(--percent-column-width), auto)
|
||||
minmax(0, 1fr)
|
||||
minmax(2.5rem, auto)
|
||||
var(--poll-option-media-width);
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.375rem;
|
||||
align-items: start;
|
||||
|
||||
:global(.theme-dark .Message.own) & {
|
||||
--ui-check-color: var(--color-primary);
|
||||
}
|
||||
|
||||
:global(body.is-macos) & {
|
||||
--percent-column-width: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hasResults {
|
||||
.selector {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.hasMediaColumn {
|
||||
--poll-option-media-width: 3rem;
|
||||
}
|
||||
|
||||
.selector {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.selectorSlide {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
--spinner-size: 1.25rem;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
margin-inline-start: auto;
|
||||
|
||||
font-size: 0.9375rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fullPercent {
|
||||
font-size: 0.8125rem;
|
||||
:global(body.is-macos) & {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.answer {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
align-self: center;
|
||||
justify-self: flex-start;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sideMeta {
|
||||
display: flex;
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.votersCount {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.avatarList {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.media {
|
||||
display: flex;
|
||||
grid-column: 4;
|
||||
grid-row: 1 / 3;
|
||||
align-self: flex-start;
|
||||
justify-self: end;
|
||||
|
||||
width: var(--poll-option-media-width);
|
||||
}
|
||||
|
||||
.mediaPreview {
|
||||
overflow: hidden;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.stickerPreview {
|
||||
position: relative;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.chosenMarker {
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
min-height: 1rem;
|
||||
}
|
||||
|
||||
.chosenMarkerIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
color: var(--ui-check-color, var(--color-white));
|
||||
|
||||
background-color: var(--ui-accent-color);
|
||||
}
|
||||
|
||||
.square {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.incorrect {
|
||||
.chosenMarkerIcon, .progressFill {
|
||||
background-color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 2;
|
||||
align-self: center;
|
||||
|
||||
min-height: 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
background-color: var(--accent-background-color);
|
||||
}
|
||||
|
||||
.progressTrack {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
.progressFill {
|
||||
transform-origin: left center;
|
||||
transform: scaleX(var(--_progress));
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
|
||||
background-color: var(--color-accent, var(--accent-color));
|
||||
}
|
||||
|
||||
.animated {
|
||||
transition: transform 500ms ease-out;
|
||||
}
|
||||
356
src/components/middle/message/poll/PollOption.tsx
Normal file
356
src/components/middle/message/poll/PollOption.tsx
Normal file
@ -0,0 +1,356 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
||||
|
||||
import type {
|
||||
ApiLocation,
|
||||
ApiPeer,
|
||||
ApiPollAnswer,
|
||||
ApiPollResult,
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { formatPercent } from '../../../../util/textFormat';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useInterval from '../../../../hooks/schedulers/useInterval';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedCounter from '../../../common/AnimatedCounter';
|
||||
import AvatarList from '../../../common/AvatarList';
|
||||
import CompactMapPreview from '../../../common/CompactMapPreview';
|
||||
import CompactMediaPreview from '../../../common/CompactMediaPreview';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import StickerView from '../../../common/StickerView';
|
||||
import Spinner from '../../../ui/Spinner';
|
||||
import Transition from '../../../ui/Transition';
|
||||
import Checkbox from '@gili/primitives/Checkbox';
|
||||
import Radio from '@gili/primitives/Radio';
|
||||
|
||||
import styles from './PollOption.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
answer: ApiPollAnswer;
|
||||
result?: ApiPollResult;
|
||||
isSelected?: boolean;
|
||||
isQuiz?: boolean;
|
||||
totalVotersCount?: number;
|
||||
isMultipleChoice?: boolean;
|
||||
hasResults?: boolean;
|
||||
hasMaskedResults?: boolean;
|
||||
isSendingVote?: boolean;
|
||||
recentVoters?: ApiPeer[];
|
||||
shouldReserveMediaColumn?: boolean;
|
||||
mediaPreviewId?: string;
|
||||
mediaPreviewIndex?: number;
|
||||
isInScheduled?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onSelect: (option: string) => void;
|
||||
onOpenMedia: (previewIndex: number) => void;
|
||||
onOpenLocation: (location: ApiLocation) => void;
|
||||
};
|
||||
|
||||
const OPTION_MEDIA_SIZE = 48;
|
||||
const MIN_PROGRESS = 5;
|
||||
const PERCENT_STEP = 13;
|
||||
const PERCENT_STEP_MS = 60;
|
||||
|
||||
const PollOption = ({
|
||||
answer,
|
||||
result,
|
||||
isSelected,
|
||||
isQuiz,
|
||||
totalVotersCount,
|
||||
isMultipleChoice,
|
||||
hasResults,
|
||||
hasMaskedResults,
|
||||
isSendingVote,
|
||||
recentVoters,
|
||||
shouldReserveMediaColumn,
|
||||
mediaPreviewId,
|
||||
mediaPreviewIndex,
|
||||
isInScheduled,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onSelect,
|
||||
onOpenMedia,
|
||||
onOpenLocation,
|
||||
}: OwnProps) => {
|
||||
const lang = useLang();
|
||||
const stickerRef = useRef<HTMLDivElement>();
|
||||
|
||||
const media = answer.media;
|
||||
const shouldReserveMediaEndColumn = Boolean(shouldReserveMediaColumn);
|
||||
const votersCount = result?.votersCount ?? 0;
|
||||
|
||||
const percentage = totalVotersCount
|
||||
? Math.round((votersCount / totalVotersCount) * 100)
|
||||
: 0;
|
||||
const hasInitializedRef = useRef(false);
|
||||
const previousResultStateRef = useRef({
|
||||
percentage,
|
||||
votersCount,
|
||||
totalVotersCount,
|
||||
});
|
||||
const [displayedPercentage, setDisplayedPercentage] = useState(percentage);
|
||||
const [progressAnimationKey, setProgressAnimationKey] = useState(0);
|
||||
const isAnimatingPercentage = hasResults && !isSendingVote && displayedPercentage !== percentage;
|
||||
const progressPercentage = totalVotersCount !== undefined
|
||||
? Math.max(MIN_PROGRESS, percentage) : 0;
|
||||
const selectorStateKey = isSendingVote ? 0
|
||||
: hasResults && !hasMaskedResults ? 1
|
||||
: hasMaskedResults ? 2 : 3;
|
||||
|
||||
const answerText = useMemo(() => renderTextWithEntities({
|
||||
text: answer.text.text,
|
||||
entities: answer.text.entities,
|
||||
}), [answer.text.entities, answer.text.text]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousResultState = previousResultStateRef.current;
|
||||
previousResultStateRef.current = {
|
||||
percentage,
|
||||
votersCount,
|
||||
totalVotersCount,
|
||||
};
|
||||
|
||||
if (!hasInitializedRef.current) {
|
||||
hasInitializedRef.current = true;
|
||||
setProgressAnimationKey((current) => current + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasResults) {
|
||||
setDisplayedPercentage(percentage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
votersCount !== previousResultState.votersCount
|
||||
|| totalVotersCount !== previousResultState.totalVotersCount
|
||||
) {
|
||||
setDisplayedPercentage(isSendingVote ? 0
|
||||
: percentage !== previousResultState.percentage ? previousResultState.percentage : 0,
|
||||
);
|
||||
setProgressAnimationKey((current) => current + 1);
|
||||
}
|
||||
}, [hasResults, isSendingVote, percentage, totalVotersCount, votersCount]);
|
||||
|
||||
useInterval(() => {
|
||||
setDisplayedPercentage((current) => {
|
||||
if (current === percentage) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current < percentage) {
|
||||
return Math.min(percentage, current + PERCENT_STEP);
|
||||
}
|
||||
|
||||
return Math.max(percentage, current - PERCENT_STEP);
|
||||
});
|
||||
}, isAnimatingPercentage ? PERCENT_STEP_MS : undefined, true);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
onSelect(answer.option);
|
||||
});
|
||||
|
||||
const handleOpenPreview = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (mediaPreviewIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
onOpenMedia(mediaPreviewIndex);
|
||||
});
|
||||
|
||||
const handleOpenMap = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!media?.location) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
onOpenLocation(media.location);
|
||||
});
|
||||
|
||||
function renderSelector() {
|
||||
if (isSendingVote) {
|
||||
return <Spinner className={styles.spinner} color="gray" />;
|
||||
}
|
||||
|
||||
if (hasResults && !hasMaskedResults) {
|
||||
return (
|
||||
<AnimatedCounter
|
||||
className={buildClassName(styles.percentage, percentage === 100 && styles.fullPercent)}
|
||||
text={formatPercent(displayedPercentage, 0)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMultipleChoice) {
|
||||
return (
|
||||
<Checkbox
|
||||
className={styles.input}
|
||||
checked={hasMaskedResults ? result?.isChosen : isSelected}
|
||||
disabled={hasMaskedResults || isInScheduled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Radio
|
||||
className={styles.input}
|
||||
value={answer.option}
|
||||
checked={hasMaskedResults ? result?.isChosen : isSelected}
|
||||
disabled={hasMaskedResults || isInScheduled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderChosenMarker() {
|
||||
if (!hasResults || !result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasMaskedResults) {
|
||||
if (!result.isChosen) return undefined;
|
||||
} else if (!result.isChosen) {
|
||||
if (!isQuiz || !result.isCorrect) return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.chosenMarkerIcon, isMultipleChoice && styles.square)}>
|
||||
<Icon name={!hasMaskedResults && isQuiz && !result.isCorrect ? 'close' : 'check'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderResultsMeta() {
|
||||
if (!hasResults || hasMaskedResults || votersCount === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.sideMeta}>
|
||||
<span className={styles.votersCount}>{lang.number(votersCount)}</span>
|
||||
{Boolean(recentVoters?.length) && (
|
||||
<AvatarList
|
||||
size="micro"
|
||||
peers={recentVoters}
|
||||
className={styles.avatarList}
|
||||
limit={2}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderMedia() {
|
||||
if (media?.photo) {
|
||||
return (
|
||||
<CompactMediaPreview
|
||||
media={media}
|
||||
id={mediaPreviewId}
|
||||
className={styles.mediaPreview}
|
||||
size={OPTION_MEDIA_SIZE}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleOpenPreview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (media?.video) {
|
||||
return (
|
||||
<CompactMediaPreview
|
||||
media={media}
|
||||
id={mediaPreviewId}
|
||||
className={styles.mediaPreview}
|
||||
size={OPTION_MEDIA_SIZE}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleOpenPreview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (media?.location) {
|
||||
return (
|
||||
<CompactMapPreview
|
||||
className={styles.mediaPreview}
|
||||
geo={media.location.geo}
|
||||
width={OPTION_MEDIA_SIZE}
|
||||
height={OPTION_MEDIA_SIZE}
|
||||
shouldShowPin={false}
|
||||
onClick={handleOpenMap}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (media?.sticker) {
|
||||
return (
|
||||
<div className={styles.stickerPreview} ref={stickerRef} onClick={(e) => e.stopPropagation()}>
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={media.sticker}
|
||||
size={OPTION_MEDIA_SIZE}
|
||||
shouldLoop
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mediaNode = renderMedia();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
hasResults && !hasMaskedResults && styles.hasResults,
|
||||
shouldReserveMediaEndColumn && styles.hasMediaColumn,
|
||||
hasResults && !hasMaskedResults && isQuiz && !result?.isCorrect && styles.incorrect,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Transition
|
||||
name="fade"
|
||||
activeKey={selectorStateKey}
|
||||
className={styles.selector}
|
||||
slideClassName={styles.selectorSlide}
|
||||
shouldCleanup
|
||||
direction={1}
|
||||
>
|
||||
{renderSelector()}
|
||||
</Transition>
|
||||
<div className={styles.answer} dir="auto">
|
||||
{answerText}
|
||||
</div>
|
||||
{renderResultsMeta()}
|
||||
{mediaNode && (
|
||||
<div className={styles.media}>
|
||||
{mediaNode}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.chosenMarker}>
|
||||
{renderChosenMarker()}
|
||||
</div>
|
||||
<div className={styles.progress}>
|
||||
<div
|
||||
className={styles.progressTrack}
|
||||
style={`--_progress: ${hasResults && !hasMaskedResults ? progressPercentage / 100 : 0}`}
|
||||
>
|
||||
<div
|
||||
className={buildClassName(styles.progressFill, progressAnimationKey > 0 && styles.animated)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PollOption);
|
||||
@ -216,12 +216,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pinnedThumbImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.pinnedMessage {
|
||||
flex-grow: 1;
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
getIsSavedDialog,
|
||||
getMessageIsSpoiler,
|
||||
getMessageSingleInlineButton,
|
||||
getMessageVideo,
|
||||
} from '../../../global/helpers';
|
||||
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||
import {
|
||||
@ -25,12 +24,10 @@ import {
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import cycleRestrict from '../../../util/cycleRestrict';
|
||||
import { getPictogramDimensions, REM } from '../../common/helpers/mediaDimensions';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
|
||||
|
||||
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
|
||||
import useThumbnail from '../../../hooks/media/useThumbnail';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||
@ -38,14 +35,13 @@ import { useFastClick } from '../../../hooks/useFastClick';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
||||
|
||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
import CompactMediaPreview, { canRenderCompactMediaPreview } from '../../common/CompactMediaPreview';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import MediaSpoiler from '../../common/MediaSpoiler';
|
||||
import MessageSummary from '../../common/MessageSummary';
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
@ -114,21 +110,15 @@ const HeaderPinnedMessage = ({
|
||||
|
||||
const topMessageTitle = topMessageSender ? getPeerTitle(lang, topMessageSender) : undefined;
|
||||
|
||||
const video = pinnedMessage && getMessageVideo(pinnedMessage);
|
||||
const gif = video?.isGif ? video : undefined;
|
||||
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
|
||||
|
||||
const mediaThumbnail = useThumbnail(pinnedMessage);
|
||||
const mediaHash = useMessageMediaHash(pinnedMessage, isVideoThumbnail ? 'full' : 'pictogram');
|
||||
const mediaBlobUrl = useMedia(mediaHash);
|
||||
const hasPictogram = Boolean(mediaThumbnail || mediaBlobUrl);
|
||||
const isSpoiler = pinnedMessage && getMessageIsSpoiler(pinnedMessage);
|
||||
|
||||
const isLoading = Boolean(useDerivedState(getLoadingPinnedId));
|
||||
const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
|
||||
const shouldShowLoader = canRenderLoader && isLoading;
|
||||
|
||||
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
|
||||
const hasPictogram = Boolean(
|
||||
renderingPinnedMessage && canRenderCompactMediaPreview(renderingPinnedMessage.content),
|
||||
);
|
||||
const isSpoiler = renderingPinnedMessage && getMessageIsSpoiler(renderingPinnedMessage);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) {
|
||||
@ -190,39 +180,6 @@ const HeaderPinnedMessage = ({
|
||||
|
||||
const { handleClick, handleMouseDown } = useFastClick(handleMessageClick);
|
||||
|
||||
function renderPictogram(thumbDataUri?: string, blobUrl?: string, isFullVideo?: boolean, asSpoiler?: boolean) {
|
||||
const { width, height } = getPictogramDimensions();
|
||||
const srcUrl = blobUrl || thumbDataUri;
|
||||
const shouldRenderVideo = isFullVideo && blobUrl;
|
||||
|
||||
return (
|
||||
<div className={styles.pinnedThumb}>
|
||||
{thumbDataUri && !asSpoiler && !shouldRenderVideo && (
|
||||
<img
|
||||
className={styles.pinnedThumbImage}
|
||||
src={srcUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
{shouldRenderVideo && !asSpoiler && (
|
||||
<video
|
||||
src={blobUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
className={styles.pinnedThumbImage}
|
||||
/>
|
||||
)}
|
||||
{thumbDataUri
|
||||
&& <MediaSpoiler thumbDataUri={srcUrl} isVisible={Boolean(asSpoiler)} width={width} height={height} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!shouldRender || !renderingPinnedMessage) return undefined;
|
||||
|
||||
return (
|
||||
@ -284,12 +241,12 @@ const HeaderPinnedMessage = ({
|
||||
index={currentPinnedIndex}
|
||||
/>
|
||||
<Transition activeKey={renderingPinnedMessage.id} name="slideVertical" className={styles.pictogramTransition}>
|
||||
{renderPictogram(
|
||||
mediaThumbnail,
|
||||
mediaBlobUrl,
|
||||
isVideoThumbnail,
|
||||
isSpoiler,
|
||||
)}
|
||||
<CompactMediaPreview
|
||||
media={renderingPinnedMessage.content}
|
||||
className={styles.pinnedThumb}
|
||||
isPictogram
|
||||
isSpoiler={isSpoiler}
|
||||
/>
|
||||
</Transition>
|
||||
<div
|
||||
className={buildClassName(styles.messageText, hasPictogram && styles.withMedia)}
|
||||
@ -315,7 +272,7 @@ const HeaderPinnedMessage = ({
|
||||
<MessageSummary
|
||||
message={renderingPinnedMessage}
|
||||
truncateLength={MAX_LENGTH}
|
||||
noEmoji={Boolean(mediaThumbnail)}
|
||||
noEmoji={hasPictogram}
|
||||
emojiSize={EMOJI_SIZE}
|
||||
/>
|
||||
</p>
|
||||
|
||||
@ -5,7 +5,7 @@ import type { TabState } from '../../../global/types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getNextArrowReplacement } from '../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../util/localization/format';
|
||||
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
@ -210,7 +210,7 @@ const BirthdaySetupModal = ({ modal }: OwnProps) => {
|
||||
link: (
|
||||
<Link isPrimary onClick={handlePrivacyClick}>
|
||||
{lang('BirthdayPrivacySuggestionLink', undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
),
|
||||
}, { withNodes: true })}
|
||||
|
||||
@ -19,7 +19,7 @@ import buildStyle from '../../../util/buildStyle';
|
||||
import { formatCountdown } from '../../../util/dates/oldDateFormat';
|
||||
import { HOUR } from '../../../util/dates/units';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { formatStarsAsIcon, getNextArrowReplacement } from '../../../util/localization/format';
|
||||
import { formatStarsAsIcon, NEXT_ARROW_REPLACEMENT } from '../../../util/localization/format';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
|
||||
import useCustomBackground from '../../../hooks/useCustomBackground';
|
||||
@ -293,7 +293,7 @@ function GiftComposer({
|
||||
<Link isPrimary onClick={handleGetMoreStars}>
|
||||
{lang('GetMoreStarsLinkText', undefined, {
|
||||
withNodes: true,
|
||||
specialReplacement: getNextArrowReplacement(),
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||
})}
|
||||
</Link>
|
||||
),
|
||||
@ -332,7 +332,7 @@ function GiftComposer({
|
||||
link: (
|
||||
<Link isPrimary onClick={handleOpenUpgradePreview}>
|
||||
{lang('GiftMakeUniqueLink', undefined, { withNodes: true,
|
||||
specialReplacement: getNextArrowReplacement() })}
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
),
|
||||
}, {
|
||||
@ -344,7 +344,7 @@ function GiftComposer({
|
||||
<Link isPrimary onClick={handleOpenUpgradePreview}>
|
||||
{lang('GiftMakeUniqueLink', undefined, {
|
||||
withNodes: true,
|
||||
specialReplacement: getNextArrowReplacement() })}
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
),
|
||||
}, {
|
||||
|
||||
@ -22,7 +22,7 @@ import { getUserFullName } from '../../../global/helpers';
|
||||
import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers';
|
||||
import { selectPeer, selectTabState, selectUserFullInfo } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getNextArrowReplacement } from '../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../util/localization/format';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
|
||||
@ -245,7 +245,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{lang('GiftPremiumDescriptionLinkCaption', undefined, {
|
||||
withNodes: true,
|
||||
specialReplacement: getNextArrowReplacement(),
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||
})}
|
||||
</SafeLink>
|
||||
),
|
||||
|
||||
@ -16,7 +16,7 @@ import type { TabState } from '../../../../global/types';
|
||||
import { requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { VTT_CRAFT_ATTRIBUTES } from '../../../../util/animations/viewTransitionTypes';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { getNextArrowReplacement } from '../../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../../util/localization/format';
|
||||
import { formatPercent } from '../../../../util/textFormat';
|
||||
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import { getGiftAttributes } from '../../../common/helpers/gifts';
|
||||
@ -1392,7 +1392,7 @@ const GiftCraftModal = ({ modal, craftAttributePermilles }: OwnProps & StateProp
|
||||
<span className={styles.viewAllText}>
|
||||
{lang('GiftCraftViewAll', undefined, {
|
||||
withNodes: true,
|
||||
specialReplacement: getNextArrowReplacement(),
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
@ -19,7 +19,7 @@ import { formatDateTimeToString } from '../../../../util/dates/oldDateFormat';
|
||||
import { formatCurrency, formatCurrencyAsString } from '../../../../util/formatCurrency';
|
||||
import {
|
||||
formatStarsAsIcon, formatStarsAsText, formatTonAsIcon, formatTonAsText,
|
||||
getNextArrowReplacement,
|
||||
NEXT_ARROW_REPLACEMENT,
|
||||
} from '../../../../util/localization/format';
|
||||
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
@ -786,7 +786,7 @@ const GiftInfoModal = ({
|
||||
link: (
|
||||
<SafeLink url={tonLink} shouldSkipModal text={lang('GiftInfoTonLinkText')}>
|
||||
{lang('GiftInfoTonLinkText', undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</SafeLink>
|
||||
),
|
||||
}, { withNodes: true })}
|
||||
@ -798,7 +798,7 @@ const GiftInfoModal = ({
|
||||
link: (
|
||||
<Link isPrimary onClick={handleTriggerVisibility}>
|
||||
{lang(`GiftInfoSaved${isUnsaved ? 'Show' : 'Hide'}`, undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
),
|
||||
}, {
|
||||
|
||||
@ -11,7 +11,7 @@ import type { AnimationLevel } from '../../../../types';
|
||||
|
||||
import { selectAnimationLevel } from '../../../../global/selectors/sharedState';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { getNextArrowReplacement } from '../../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../../util/localization/format';
|
||||
import { resolveTransitionName } from '../../../../util/resolveTransitionName';
|
||||
import { getGiftAttributes, getRandomGiftPreviewAttributes } from '../../../common/helpers/gifts';
|
||||
|
||||
@ -359,7 +359,7 @@ const GiftPreviewModal = ({ modal, animationLevel }: OwnProps & StateProps) => {
|
||||
{lang(
|
||||
isCraftableModelsMode ? 'GiftPreviewToggleRegularModels' : 'GiftPreviewToggleCraftableModels',
|
||||
undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() },
|
||||
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT },
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@ -8,7 +8,7 @@ import type { TabState } from '../../../../global/types';
|
||||
|
||||
import { getPeerTitle } from '../../../../global/helpers/peers';
|
||||
import { selectPeer } from '../../../../global/selectors';
|
||||
import { getNextArrowReplacement } from '../../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../../util/localization/format';
|
||||
import {
|
||||
getRandomGiftPreviewAttributes, type GiftPreviewAttributes,
|
||||
preloadGiftAttributeStickers } from '../../../common/helpers/gifts';
|
||||
@ -193,7 +193,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
||||
/>
|
||||
))}
|
||||
<span className={styles.viewAllText}>
|
||||
{lang('GiftUpgradeViewAll', undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
{lang('GiftUpgradeViewAll', undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@ -255,7 +255,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
||||
onClick={handleOpenPriceInfo}
|
||||
>
|
||||
{lang('StarGiftPriceDecreaseInfoLink', undefined,
|
||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -3,11 +3,10 @@ import { getActions } from '../../../global';
|
||||
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { IS_IOS, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
||||
import { prepareMapUrl } from '../../../util/map';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
@ -23,7 +22,7 @@ const OpenMapModal = ({ modal }: OwnProps) => {
|
||||
|
||||
const { point: geoPoint, zoom } = modal || {};
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(geoPoint);
|
||||
|
||||
@ -38,8 +37,8 @@ const OpenMapModal = ({ modal }: OwnProps) => {
|
||||
|
||||
const google = prepareMapUrl('google', geoPoint, zoom);
|
||||
const bing = prepareMapUrl('bing', geoPoint, zoom);
|
||||
const osm = prepareMapUrl('osm', geoPoint, zoom);
|
||||
const apple = prepareMapUrl('apple', geoPoint, zoom);
|
||||
const osm = prepareMapUrl('osm', geoPoint, zoom);
|
||||
|
||||
return [google, bing, apple, osm];
|
||||
}, [geoPoint, zoom]);
|
||||
@ -74,18 +73,16 @@ const OpenMapModal = ({ modal }: OwnProps) => {
|
||||
isSlim
|
||||
>
|
||||
<div className={styles.buttons}>
|
||||
{(IS_IOS || IS_MAC_OS) && (
|
||||
<Button fluid size="smaller" onClick={handleAppleClick}>
|
||||
Apple Maps
|
||||
</Button>
|
||||
)}
|
||||
<Button fluid size="smaller" onClick={handleGoogleClick}>
|
||||
<Button noForcedUpperCase fluid size="smaller" onClick={handleGoogleClick}>
|
||||
Google Maps
|
||||
</Button>
|
||||
<Button fluid size="smaller" onClick={handleBingClick}>
|
||||
<Button noForcedUpperCase fluid size="smaller" onClick={handleAppleClick}>
|
||||
Apple Maps
|
||||
</Button>
|
||||
<Button noForcedUpperCase fluid size="smaller" onClick={handleBingClick}>
|
||||
Bing Maps
|
||||
</Button>
|
||||
<Button fluid size="smaller" onClick={handleOsmClick}>
|
||||
<Button noForcedUpperCase fluid size="smaller" onClick={handleOsmClick}>
|
||||
OpenStreetMap
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@ import { getPeerTitle } from '../../../global/helpers/peers';
|
||||
import { selectUser, selectUserFullInfo } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatShortDuration } from '../../../util/dates/oldDateFormat';
|
||||
import { getNextArrowReplacement } from '../../../util/localization/format';
|
||||
import { NEXT_ARROW_REPLACEMENT } from '../../../util/localization/format';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
@ -132,7 +132,7 @@ const ProfileRatingModal = ({
|
||||
link: (
|
||||
<span className={styles.backLink} onClick={handleShowCurrent}>
|
||||
{lang('LinkDescriptionRatingBack',
|
||||
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</span>
|
||||
),
|
||||
}, {
|
||||
@ -148,7 +148,7 @@ const ProfileRatingModal = ({
|
||||
link: (
|
||||
<span className={styles.previewLink} onClick={handleShowFuture}>
|
||||
{lang('LinkDescriptionRatingPreview',
|
||||
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
||||
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||
</span>
|
||||
),
|
||||
}, {
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat, ApiMessage, ApiPoll } from '../../api/types';
|
||||
import type { ApiChat, ApiMessage, ApiMessagePoll } from '../../api/types';
|
||||
|
||||
import {
|
||||
selectChat, selectChatMessage, selectPollFromMessage, selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { buildCollectionByKey } from '../../util/iteratees';
|
||||
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
@ -25,7 +24,7 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
message?: ApiMessage;
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
};
|
||||
|
||||
const PollResults = ({
|
||||
@ -47,12 +46,10 @@ const PollResults = ({
|
||||
}
|
||||
|
||||
const { summary, results } = poll;
|
||||
if (!results.results) {
|
||||
if (!results.resultByOption) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resultsByOption = buildCollectionByKey(results.results, 'option');
|
||||
|
||||
return (
|
||||
<div className="PollResults" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<h3 className="poll-question" dir="auto">
|
||||
@ -64,11 +61,11 @@ const PollResults = ({
|
||||
<div className="poll-results-list custom-scroll">
|
||||
{summary.answers.map((answer) => (
|
||||
<PollAnswerResults
|
||||
key={`${poll.id}-${answer.option}`}
|
||||
key={`${poll.summary.id}-${answer.option}`}
|
||||
chat={chat}
|
||||
message={message}
|
||||
answer={answer}
|
||||
answerVote={resultsByOption[answer.option]}
|
||||
answerVote={results.resultByOption![answer.option]}
|
||||
totalVoters={results.totalVoters!}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -150,39 +150,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.text {
|
||||
background-color: transparent;
|
||||
|
||||
&.primary {
|
||||
color: var(--color-primary);
|
||||
background-color: transparent;
|
||||
|
||||
@include active-styles() {
|
||||
background-color: rgba(var(--color-primary-shade-rgb), 0.08);
|
||||
}
|
||||
|
||||
@include no-ripple-styles() {
|
||||
background-color: rgba(var(--color-primary-shade-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: var(--color-text-secondary);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@include active-styles() {
|
||||
color: var(--color-error);
|
||||
background-color: rgba(var(--color-error-rgb), 0.08);
|
||||
}
|
||||
|
||||
@include no-ripple-styles() {
|
||||
background-color: rgba(var(--color-error-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.faded {
|
||||
opacity: 0.8;
|
||||
|
||||
@ -532,5 +499,38 @@
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.text {
|
||||
background-color: transparent;
|
||||
|
||||
&.primary {
|
||||
color: var(--color-primary);
|
||||
background-color: transparent;
|
||||
|
||||
@include active-styles() {
|
||||
background-color: rgba(var(--color-primary-shade-rgb), 0.08);
|
||||
}
|
||||
|
||||
@include no-ripple-styles() {
|
||||
background-color: rgba(var(--color-primary-shade-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: var(--color-text-secondary);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
@include active-styles() {
|
||||
color: var(--color-error);
|
||||
background-color: rgba(var(--color-error-rgb), 0.08);
|
||||
}
|
||||
|
||||
@include no-ripple-styles() {
|
||||
background-color: rgba(var(--color-error-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import type { ApiStarsAmount, ApiTonAmount } from '../../api/types';
|
||||
import { formatStarsAmount } from '../../global/helpers/payments';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { convertTonFromNanos, convertTonToUsd, formatCurrencyAsString } from '../../util/formatCurrency';
|
||||
import { formatStarsAsIcon, formatTonAsIcon, getNextArrowReplacement } from '../../util/localization/format';
|
||||
import { formatStarsAsIcon, formatTonAsIcon, NEXT_ARROW_REPLACEMENT } from '../../util/localization/format';
|
||||
|
||||
import useIsTopmostBalanceBarModal from '../../hooks/element/useIsTopmostBalanceBarModal';
|
||||
import useLang from '../../hooks/useLang';
|
||||
@ -101,7 +101,7 @@ function ModalStarBalanceBar({
|
||||
<Link className={styles.getMoreStarsLink} isPrimary onClick={handleGetMoreStars}>
|
||||
{lang('GetMoreStarsLinkText', undefined, {
|
||||
withNodes: true,
|
||||
specialReplacement: getNextArrowReplacement(),
|
||||
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||
})}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@ -1,28 +1,46 @@
|
||||
import { useEffect } from '../../lib/teact/teact';
|
||||
|
||||
import { formatMediaDuration } from '../../util/dates/oldDateFormat';
|
||||
import { formatClockDuration, formatCountdownDateTime, secondsToDate } from '../../util/localization/dateFormat';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
|
||||
import useInterval from '../../hooks/schedulers/useInterval';
|
||||
import useTimeout from '../../hooks/schedulers/useTimeout';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import AnimatedCounter from '../common/AnimatedCounter';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
endsAt: number;
|
||||
mode?: 'clock' | 'countdown';
|
||||
shouldShowZeroOnEnd?: boolean;
|
||||
onEnd?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const DAY_IN_SECONDS = 24 * 60 * 60;
|
||||
const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000
|
||||
|
||||
const TextTimer = ({ className, endsAt, shouldShowZeroOnEnd, onEnd }: OwnProps) => {
|
||||
const TextTimer = ({
|
||||
className,
|
||||
endsAt,
|
||||
mode = 'clock',
|
||||
shouldShowZeroOnEnd,
|
||||
onEnd,
|
||||
}: OwnProps) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const lang = useLang();
|
||||
|
||||
const serverTime = getServerTime();
|
||||
const isActive = serverTime < endsAt;
|
||||
useInterval(forceUpdate, isActive ? UPDATE_FREQUENCY : undefined);
|
||||
const timeLeft = Math.max(0, endsAt - serverTime);
|
||||
const shouldUseClock = mode === 'clock' || timeLeft < DAY_IN_SECONDS;
|
||||
const switchToClockDelay = isActive && mode === 'countdown' && !shouldUseClock
|
||||
? ((timeLeft - DAY_IN_SECONDS) * 1000) + UPDATE_FREQUENCY
|
||||
: undefined;
|
||||
|
||||
useTimeout(forceUpdate, switchToClockDelay);
|
||||
useInterval(forceUpdate, isActive && shouldUseClock ? UPDATE_FREQUENCY : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
@ -32,18 +50,35 @@ const TextTimer = ({ className, endsAt, shouldShowZeroOnEnd, onEnd }: OwnProps)
|
||||
|
||||
if (!isActive && !shouldShowZeroOnEnd) return undefined;
|
||||
|
||||
const timeLeft = Math.max(0, endsAt - serverTime);
|
||||
const time = formatMediaDuration(timeLeft);
|
||||
if (mode === 'countdown' && !shouldUseClock) {
|
||||
return (
|
||||
<span className={className}>
|
||||
{formatCountdownDateTime(lang, secondsToDate(endsAt), {
|
||||
anchorDate: secondsToDate(serverTime),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const time = formatClockDuration(timeLeft);
|
||||
|
||||
const timeParts = time.split(':');
|
||||
const clockNode = (
|
||||
<>
|
||||
{timeParts.map((part, index) => (
|
||||
<span key={index}>
|
||||
{index > 0 && ':'}
|
||||
<AnimatedCounter text={part} />
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={className} style="font-variant-numeric: tabular-nums;">
|
||||
{timeParts.map((part, index) => (
|
||||
<>
|
||||
{index > 0 && ':'}
|
||||
<AnimatedCounter key={index} text={part} />
|
||||
</>
|
||||
))}
|
||||
{mode === 'countdown'
|
||||
? lang('TimeIn', { time: clockNode }, { withNodes: true })
|
||||
: clockNode}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@ -196,7 +196,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
});
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
@ -320,7 +320,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
@ -368,7 +368,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
);
|
||||
}
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
@ -398,7 +398,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
@ -444,7 +444,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
global = updateQuickReplyMessage(global, id, message);
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
@ -547,7 +547,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
});
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
global = {
|
||||
@ -624,7 +624,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
});
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
@ -46,7 +46,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
if (polls) {
|
||||
polls.forEach((poll) => {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
global = updatePoll(global, poll.summary.id, poll);
|
||||
});
|
||||
}
|
||||
if (webPages) {
|
||||
|
||||
@ -10,8 +10,7 @@ import type {
|
||||
ApiTypeStory,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiPoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
|
||||
ApiFormattedText, ApiMessagePoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
|
||||
} from '../../api/types/messages';
|
||||
import type { ThreadId } from '../../types';
|
||||
import type { LangFn } from '../../util/localization';
|
||||
@ -74,6 +73,8 @@ export function hasMessageText(message: MediaContainer) {
|
||||
webPage, contact, invoice, location, game, storyData, giveaway, giveawayResults, paidMedia,
|
||||
} = message.content;
|
||||
|
||||
if (pollId) return false;
|
||||
|
||||
return Boolean(text) || !(
|
||||
sticker || photo || video || audio || voice || document || contact || pollId || todo || webPage
|
||||
|| invoice || location || game || storyData || giveaway || giveawayResults || dice
|
||||
@ -96,7 +97,7 @@ export function groupStatefulContent({
|
||||
story,
|
||||
webPage,
|
||||
}: {
|
||||
poll?: ApiPoll;
|
||||
poll?: ApiMessagePoll;
|
||||
story?: ApiTypeStory;
|
||||
webPage?: ApiWebPage;
|
||||
}) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage,
|
||||
ApiMessage, ApiMessagePoll, ApiPollResult, ApiPollResults, ApiQuickReply, ApiSponsoredMessage,
|
||||
ApiWebPage,
|
||||
ApiWebPageFull,
|
||||
} from '../../api/types';
|
||||
@ -871,34 +871,21 @@ export function replaceWebPage<T extends GlobalState>(
|
||||
export function updatePoll<T extends GlobalState>(
|
||||
global: T,
|
||||
pollId: string,
|
||||
pollUpdate: Partial<ApiPoll>,
|
||||
pollUpdate: Partial<ApiMessagePoll>,
|
||||
) {
|
||||
const poll = selectPoll(global, pollId);
|
||||
const results = mergePollResults(poll?.results, pollUpdate.results);
|
||||
|
||||
const oldResults = poll?.results;
|
||||
let newResults = oldResults || pollUpdate.results;
|
||||
if (poll && pollUpdate.results?.results) {
|
||||
if (!poll.results || !pollUpdate.results.isMin) {
|
||||
newResults = pollUpdate.results;
|
||||
} else if (oldResults.results) {
|
||||
// Update voters counts, but keep local `isChosen` values
|
||||
newResults = {
|
||||
...pollUpdate.results,
|
||||
results: pollUpdate.results.results.map((result) => ({
|
||||
...result,
|
||||
isChosen: oldResults.results!.find((r) => r.option === result.option)?.isChosen,
|
||||
})),
|
||||
isMin: undefined,
|
||||
};
|
||||
}
|
||||
if (!results) {
|
||||
return global;
|
||||
}
|
||||
|
||||
const updatedPoll = {
|
||||
...poll,
|
||||
...pollUpdate,
|
||||
results: newResults,
|
||||
} satisfies ApiPoll;
|
||||
if (!updatedPoll.id) {
|
||||
results,
|
||||
} satisfies ApiMessagePoll;
|
||||
if (!updatedPoll.summary?.id) {
|
||||
return global;
|
||||
}
|
||||
|
||||
@ -914,6 +901,58 @@ export function updatePoll<T extends GlobalState>(
|
||||
};
|
||||
}
|
||||
|
||||
function mergePollResults(
|
||||
currentResults: ApiPollResults | undefined,
|
||||
newResults: ApiPollResults | undefined,
|
||||
) {
|
||||
if (!newResults) {
|
||||
return currentResults;
|
||||
}
|
||||
|
||||
if (!currentResults) {
|
||||
return newResults;
|
||||
}
|
||||
|
||||
const mergedResults: ApiPollResults = {
|
||||
...currentResults,
|
||||
...newResults,
|
||||
};
|
||||
|
||||
if (newResults.resultByOption) {
|
||||
mergedResults.resultByOption = newResults.isMin
|
||||
? mergeMinPollResultByOption(currentResults.resultByOption, newResults.resultByOption)
|
||||
: newResults.resultByOption;
|
||||
}
|
||||
|
||||
return mergedResults;
|
||||
}
|
||||
|
||||
function mergeMinPollResultByOption(
|
||||
currentResultByOption: Record<string, ApiPollResult> | undefined,
|
||||
newResultByOption: Record<string, ApiPollResult>,
|
||||
) {
|
||||
const mergedResultByOption: Record<string, ApiPollResult> = {};
|
||||
|
||||
Object.keys(newResultByOption).forEach((option) => {
|
||||
const currentResult = currentResultByOption?.[option];
|
||||
const nextResult = newResultByOption[option];
|
||||
|
||||
if (!currentResult) {
|
||||
mergedResultByOption[option] = nextResult;
|
||||
return;
|
||||
}
|
||||
|
||||
mergedResultByOption[option] = {
|
||||
...currentResult,
|
||||
...nextResult,
|
||||
isChosen: currentResult.isChosen || nextResult.isChosen,
|
||||
isCorrect: currentResult.isCorrect || nextResult.isCorrect,
|
||||
};
|
||||
});
|
||||
|
||||
return mergedResultByOption;
|
||||
}
|
||||
|
||||
export function updatePollVote<T extends GlobalState>(
|
||||
global: T,
|
||||
pollId: string,
|
||||
@ -925,28 +964,26 @@ export function updatePollVote<T extends GlobalState>(
|
||||
return global;
|
||||
}
|
||||
|
||||
const { recentVoterIds, totalVoters, results } = poll.results;
|
||||
const { recentVoterIds, totalVoters, resultByOption } = poll.results;
|
||||
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
|
||||
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
|
||||
const newResults = results ? [...results] : [];
|
||||
const newResultByOption = { ...resultByOption };
|
||||
|
||||
newRecentVoterIds.push(peerId);
|
||||
|
||||
options.forEach((option) => {
|
||||
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
|
||||
const targetOption = newResults[targetOptionIndex];
|
||||
const targetOption = resultByOption?.[option];
|
||||
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
|
||||
const recentOptionVoterIds = updatedOption.recentVoterIds ? [...updatedOption.recentVoterIds] : [];
|
||||
|
||||
updatedOption.votersCount += 1;
|
||||
recentOptionVoterIds.push(peerId);
|
||||
updatedOption.recentVoterIds = recentOptionVoterIds;
|
||||
if (peerId === global.currentUserId) {
|
||||
updatedOption.isChosen = true;
|
||||
}
|
||||
|
||||
if (targetOptionIndex) {
|
||||
newResults[targetOptionIndex] = updatedOption;
|
||||
} else {
|
||||
newResults.push(updatedOption);
|
||||
}
|
||||
newResultByOption[option] = updatedOption;
|
||||
});
|
||||
|
||||
return updatePoll(global, pollId, {
|
||||
@ -954,7 +991,7 @@ export function updatePollVote<T extends GlobalState>(
|
||||
...poll.results,
|
||||
recentVoterIds: newRecentVoterIds,
|
||||
totalVoters: newTotalVoters,
|
||||
results: newResults,
|
||||
resultByOption: newResultByOption,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -647,8 +647,12 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
|
||||
const canSaveGif = message.content.video?.isGif;
|
||||
|
||||
const poll = content.pollId ? selectPoll(global, content.pollId) : undefined;
|
||||
const canRevote = !poll?.summary.closed && !poll?.summary.quiz && poll?.results.results?.some((r) => r.isChosen);
|
||||
const canClosePoll = hasMessageEditRight && poll && !poll.summary.closed && !isForwarded;
|
||||
const hasChosenPollAnswer = Boolean(
|
||||
poll && Object.values(poll.results.resultByOption || {}).some((result) => result.isChosen),
|
||||
);
|
||||
const canRevote = poll && !poll.summary.isClosed && !poll.summary.isRevoteDisabled
|
||||
&& hasChosenPollAnswer;
|
||||
const canClosePoll = hasMessageEditRight && poll && !poll.summary.isClosed && !isForwarded;
|
||||
|
||||
const noOptions = [
|
||||
canReply,
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
ApiEmojiStatusType,
|
||||
ApiGroupCall,
|
||||
ApiMessage,
|
||||
ApiMessagePoll,
|
||||
ApiNotifyPeerType,
|
||||
ApiPaidReactionPrivacyType,
|
||||
ApiPasskey,
|
||||
@ -23,7 +24,6 @@ import type {
|
||||
ApiPeerPhotos,
|
||||
ApiPeerStories,
|
||||
ApiPhoneCall,
|
||||
ApiPoll,
|
||||
ApiPrivacyKey,
|
||||
ApiPrivacySettings,
|
||||
ApiPromoData,
|
||||
@ -253,7 +253,7 @@ export type GlobalState = {
|
||||
byId: Record<number, number>;
|
||||
}>;
|
||||
sponsoredByChatId: Record<string, ApiSponsoredMessage>;
|
||||
pollById: Record<string, ApiPoll>;
|
||||
pollById: Record<string, ApiMessagePoll>;
|
||||
webPageById: Record<string, ApiWebPage>;
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,337 +16,338 @@
|
||||
}
|
||||
|
||||
$icons-map: (
|
||||
"zoom-out": "\f101",
|
||||
"zoom-in": "\f102",
|
||||
"word-wrap": "\f103",
|
||||
"webapp": "\f104",
|
||||
"web": "\f105",
|
||||
"warning": "\f106",
|
||||
"volume-3": "\f107",
|
||||
"volume-2": "\f108",
|
||||
"volume-1": "\f109",
|
||||
"voice-chat": "\f10a",
|
||||
"view-once": "\f10b",
|
||||
"video": "\f10c",
|
||||
"video-stop": "\f10d",
|
||||
"video-outlined": "\f10e",
|
||||
"user": "\f10f",
|
||||
"user-tag": "\f110",
|
||||
"user-stars": "\f111",
|
||||
"user-online": "\f112",
|
||||
"user-filled": "\f113",
|
||||
"up": "\f114",
|
||||
"unread": "\f115",
|
||||
"unpin": "\f116",
|
||||
"unmute": "\f117",
|
||||
"unlock": "\f118",
|
||||
"unlock-badge": "\f119",
|
||||
"unlist": "\f11a",
|
||||
"unlist-outline": "\f11b",
|
||||
"unique-profile": "\f11c",
|
||||
"undo": "\f11d",
|
||||
"understood": "\f11e",
|
||||
"underlined": "\f11f",
|
||||
"unarchive": "\f120",
|
||||
"truck": "\f121",
|
||||
"transcribe": "\f122",
|
||||
"trade": "\f123",
|
||||
"topic-new": "\f124",
|
||||
"tools": "\f125",
|
||||
"toncoin": "\f126",
|
||||
"timer": "\f127",
|
||||
"tag": "\f128",
|
||||
"tag-name": "\f129",
|
||||
"tag-filter": "\f12a",
|
||||
"tag-crossed": "\f12b",
|
||||
"tag-add": "\f12c",
|
||||
"strikethrough": "\f12d",
|
||||
"story-reply": "\f12e",
|
||||
"story-priority": "\f12f",
|
||||
"story-expired": "\f130",
|
||||
"story-caption": "\f131",
|
||||
"stop": "\f132",
|
||||
"stop-raising-hand": "\f133",
|
||||
"stickers": "\f134",
|
||||
"stealth-past": "\f135",
|
||||
"stealth-future": "\f136",
|
||||
"stats": "\f137",
|
||||
"stars-refund": "\f138",
|
||||
"stars-lock": "\f139",
|
||||
"star": "\f13a",
|
||||
"sport": "\f13b",
|
||||
"spoiler": "\f13c",
|
||||
"spoiler-disable": "\f13d",
|
||||
"speaker": "\f13e",
|
||||
"speaker-story": "\f13f",
|
||||
"speaker-outline": "\f140",
|
||||
"speaker-muted-story": "\f141",
|
||||
"sort": "\f142",
|
||||
"sort-by-price": "\f143",
|
||||
"sort-by-number": "\f144",
|
||||
"sort-by-date": "\f145",
|
||||
"smile": "\f146",
|
||||
"smallscreen": "\f147",
|
||||
"skip-previous": "\f148",
|
||||
"skip-next": "\f149",
|
||||
"sidebar": "\f14a",
|
||||
"show-message": "\f14b",
|
||||
"share-screen": "\f14c",
|
||||
"share-screen-stop": "\f14d",
|
||||
"share-screen-outlined": "\f14e",
|
||||
"share-filled": "\f14f",
|
||||
"settings": "\f150",
|
||||
"settings-filled": "\f151",
|
||||
"send": "\f152",
|
||||
"send-outline": "\f153",
|
||||
"sell": "\f154",
|
||||
"sell-outline": "\f155",
|
||||
"select": "\f156",
|
||||
"select-filled": "\f157",
|
||||
"search": "\f158",
|
||||
"sd-photo": "\f159",
|
||||
"scheduled": "\f15a",
|
||||
"schedule": "\f15b",
|
||||
"saved-messages": "\f15c",
|
||||
"save-story": "\f15d",
|
||||
"rotate": "\f15e",
|
||||
"revote": "\f15f",
|
||||
"revenue-split": "\f160",
|
||||
"reply": "\f161",
|
||||
"reply-filled": "\f162",
|
||||
"replies": "\f163",
|
||||
"replace": "\f164",
|
||||
"reorder-tabs": "\f165",
|
||||
"reopen-topic": "\f166",
|
||||
"remove": "\f167",
|
||||
"remove-quote": "\f168",
|
||||
"reload": "\f169",
|
||||
"refund": "\f16a",
|
||||
"redo": "\f16b",
|
||||
"recent": "\f16c",
|
||||
"readchats": "\f16d",
|
||||
"radial-badge": "\f16e",
|
||||
"quote": "\f16f",
|
||||
"quote-text": "\f170",
|
||||
"proof-of-ownership": "\f171",
|
||||
"privacy-policy": "\f172",
|
||||
"previous": "\f173",
|
||||
"poll": "\f174",
|
||||
"play": "\f175",
|
||||
"play-story": "\f176",
|
||||
"pip": "\f177",
|
||||
"pinned-message": "\f178",
|
||||
"pinned-chat": "\f179",
|
||||
"pin": "\f17a",
|
||||
"pin-list": "\f17b",
|
||||
"pin-badge": "\f17c",
|
||||
"photo": "\f17d",
|
||||
"phone": "\f17e",
|
||||
"phone-discard": "\f17f",
|
||||
"phone-discard-outline": "\f180",
|
||||
"permissions": "\f181",
|
||||
"pause": "\f182",
|
||||
"password-off": "\f183",
|
||||
"open-in-new-tab": "\f184",
|
||||
"one-filled": "\f185",
|
||||
"note": "\f186",
|
||||
"non-contacts": "\f187",
|
||||
"noise-suppression": "\f188",
|
||||
"nochannel": "\f189",
|
||||
"no-share": "\f18a",
|
||||
"no-download": "\f18b",
|
||||
"next": "\f18c",
|
||||
"next-link": "\f18d",
|
||||
"new-send": "\f18e",
|
||||
"new-chat-filled": "\f18f",
|
||||
"my-notes": "\f190",
|
||||
"muted": "\f191",
|
||||
"mute": "\f192",
|
||||
"move-caption-up": "\f193",
|
||||
"move-caption-down": "\f194",
|
||||
"more": "\f195",
|
||||
"more-circle": "\f196",
|
||||
"monospace": "\f197",
|
||||
"microphone": "\f198",
|
||||
"microphone-alt": "\f199",
|
||||
"message": "\f19a",
|
||||
"message-succeeded": "\f19b",
|
||||
"message-read": "\f19c",
|
||||
"message-pending": "\f19d",
|
||||
"message-failed": "\f19e",
|
||||
"menu": "\f19f",
|
||||
"mention": "\f1a0",
|
||||
"loop": "\f1a1",
|
||||
"logout": "\f1a2",
|
||||
"lock": "\f1a3",
|
||||
"lock-badge": "\f1a4",
|
||||
"location": "\f1a5",
|
||||
"link": "\f1a6",
|
||||
"link-broken": "\f1a7",
|
||||
"link-badge": "\f1a8",
|
||||
"large-play": "\f1a9",
|
||||
"large-pause": "\f1aa",
|
||||
"language": "\f1ab",
|
||||
"lamp": "\f1ac",
|
||||
"keyboard": "\f1ad",
|
||||
"key": "\f1ae",
|
||||
"italic": "\f1af",
|
||||
"install": "\f1b0",
|
||||
"info": "\f1b1",
|
||||
"info-filled": "\f1b2",
|
||||
"help": "\f1b3",
|
||||
"heart": "\f1b4",
|
||||
"heart-outline": "\f1b5",
|
||||
"hd-photo": "\f1b6",
|
||||
"hashtag": "\f1b7",
|
||||
"hand-stop": "\f1b8",
|
||||
"hand-stop-filled": "\f1b9",
|
||||
"grouped": "\f1ba",
|
||||
"grouped-disable": "\f1bb",
|
||||
"group": "\f1bc",
|
||||
"group-filled": "\f1bd",
|
||||
"gift": "\f1be",
|
||||
"gift-transfer-inline": "\f1bf",
|
||||
"gifs": "\f1c0",
|
||||
"fullscreen": "\f1c1",
|
||||
"frozen-time": "\f1c2",
|
||||
"fragment": "\f1c3",
|
||||
"forward": "\f1c4",
|
||||
"forums": "\f1c5",
|
||||
"fontsize": "\f1c6",
|
||||
"folder": "\f1c7",
|
||||
"folder-badge": "\f1c8",
|
||||
"flip": "\f1c9",
|
||||
"flag": "\f1ca",
|
||||
"file-badge": "\f1cb",
|
||||
"favorite": "\f1cc",
|
||||
"favorite-filled": "\f1cd",
|
||||
"eye": "\f1ce",
|
||||
"eye-outline": "\f1cf",
|
||||
"eye-crossed": "\f1d0",
|
||||
"eye-crossed-outline": "\f1d1",
|
||||
"expand": "\f1d2",
|
||||
"expand-modal": "\f1d3",
|
||||
"enter": "\f1d4",
|
||||
"email": "\f1d5",
|
||||
"edit": "\f1d6",
|
||||
"eats": "\f1d7",
|
||||
"dropdown-arrows": "\f1d8",
|
||||
"download": "\f1d9",
|
||||
"down": "\f1da",
|
||||
"double-badge": "\f1db",
|
||||
"document": "\f1dc",
|
||||
"diamond": "\f1dd",
|
||||
"delete": "\f1de",
|
||||
"delete-user": "\f1df",
|
||||
"delete-left": "\f1e0",
|
||||
"delete-filled": "\f1e1",
|
||||
"data": "\f1e2",
|
||||
"darkmode": "\f1e3",
|
||||
"crown-wear": "\f1e4",
|
||||
"crown-wear-outline": "\f1e5",
|
||||
"crown-take-off": "\f1e6",
|
||||
"crown-take-off-outline": "\f1e7",
|
||||
"crop": "\f1e8",
|
||||
"craft": "\f1e9",
|
||||
"copy": "\f1ea",
|
||||
"copy-media": "\f1eb",
|
||||
"comments": "\f1ec",
|
||||
"comments-sticker": "\f1ed",
|
||||
"combine-craft": "\f1ee",
|
||||
"colorize": "\f1ef",
|
||||
"collapse": "\f1f0",
|
||||
"collapse-modal": "\f1f1",
|
||||
"cloud-download": "\f1f2",
|
||||
"closed-gift": "\f1f3",
|
||||
"close": "\f1f4",
|
||||
"close-topic": "\f1f5",
|
||||
"close-circle": "\f1f6",
|
||||
"clock": "\f1f7",
|
||||
"clock-edit": "\f1f8",
|
||||
"check": "\f1f9",
|
||||
"check-bold": "\f1fa",
|
||||
"chats-badge": "\f1fb",
|
||||
"chat-badge": "\f1fc",
|
||||
"channelviews": "\f1fd",
|
||||
"channel": "\f1fe",
|
||||
"channel-filled": "\f1ff",
|
||||
"cash-circle": "\f200",
|
||||
"card": "\f201",
|
||||
"car": "\f202",
|
||||
"camera": "\f203",
|
||||
"camera-add": "\f204",
|
||||
"calendar": "\f205",
|
||||
"calendar-filter": "\f206",
|
||||
"bug": "\f207",
|
||||
"brush": "\f208",
|
||||
"bots": "\f209",
|
||||
"bot-commands-filled": "\f20a",
|
||||
"bot-command": "\f20b",
|
||||
"boosts": "\f20c",
|
||||
"boostcircle": "\f20d",
|
||||
"boost": "\f20e",
|
||||
"boost-outline": "\f20f",
|
||||
"boost-craft-chance": "\f210",
|
||||
"bold": "\f211",
|
||||
"avatar-saved-messages": "\f212",
|
||||
"avatar-deleted-account": "\f213",
|
||||
"avatar-archived-chats": "\f214",
|
||||
"author-hidden": "\f215",
|
||||
"auction": "\f216",
|
||||
"auction-next-round": "\f217",
|
||||
"auction-filled": "\f218",
|
||||
"auction-drop": "\f219",
|
||||
"attach": "\f21a",
|
||||
"ask-support": "\f21b",
|
||||
"arrow-right": "\f21c",
|
||||
"arrow-left": "\f21d",
|
||||
"arrow-down": "\f21e",
|
||||
"arrow-down-circle": "\f21f",
|
||||
"archive": "\f220",
|
||||
"archive-to-main": "\f221",
|
||||
"archive-from-main": "\f222",
|
||||
"archive-filled": "\f223",
|
||||
"animations": "\f224",
|
||||
"animals": "\f225",
|
||||
"allow-speak": "\f226",
|
||||
"allow-share": "\f227",
|
||||
"ai": "\f228",
|
||||
"ai-fix": "\f229",
|
||||
"ai-edit": "\f22a",
|
||||
"admin": "\f22b",
|
||||
"add": "\f22c",
|
||||
"add-user": "\f22d",
|
||||
"add-user-filled": "\f22e",
|
||||
"add-one-badge": "\f22f",
|
||||
"add-filled": "\f230",
|
||||
"add-caption": "\f231",
|
||||
"active-sessions": "\f232",
|
||||
"rating-icons-negative": "\f233",
|
||||
"rating-icons-level90": "\f234",
|
||||
"rating-icons-level9": "\f235",
|
||||
"rating-icons-level80": "\f236",
|
||||
"rating-icons-level8": "\f237",
|
||||
"rating-icons-level70": "\f238",
|
||||
"rating-icons-level7": "\f239",
|
||||
"rating-icons-level60": "\f23a",
|
||||
"rating-icons-level6": "\f23b",
|
||||
"rating-icons-level50": "\f23c",
|
||||
"rating-icons-level5": "\f23d",
|
||||
"rating-icons-level40": "\f23e",
|
||||
"rating-icons-level4": "\f23f",
|
||||
"rating-icons-level30": "\f240",
|
||||
"rating-icons-level3": "\f241",
|
||||
"rating-icons-level20": "\f242",
|
||||
"rating-icons-level2": "\f243",
|
||||
"rating-icons-level10": "\f244",
|
||||
"rating-icons-level1": "\f245",
|
||||
"folder-tabs-user": "\f246",
|
||||
"folder-tabs-star": "\f247",
|
||||
"folder-tabs-group": "\f248",
|
||||
"folder-tabs-folder": "\f249",
|
||||
"folder-tabs-chats": "\f24a",
|
||||
"folder-tabs-chat": "\f24b",
|
||||
"folder-tabs-channel": "\f24c",
|
||||
"folder-tabs-bot": "\f24d",
|
||||
"active-sessions": "\f101",
|
||||
"add-caption": "\f102",
|
||||
"add-filled": "\f103",
|
||||
"add-one-badge": "\f104",
|
||||
"add-user-filled": "\f105",
|
||||
"add-user": "\f106",
|
||||
"add": "\f107",
|
||||
"admin": "\f108",
|
||||
"ai-edit": "\f109",
|
||||
"ai-fix": "\f10a",
|
||||
"ai": "\f10b",
|
||||
"allow-share": "\f10c",
|
||||
"allow-speak": "\f10d",
|
||||
"animals": "\f10e",
|
||||
"animations": "\f10f",
|
||||
"archive-filled": "\f110",
|
||||
"archive-from-main": "\f111",
|
||||
"archive-to-main": "\f112",
|
||||
"archive": "\f113",
|
||||
"arrow-down-circle": "\f114",
|
||||
"arrow-down": "\f115",
|
||||
"arrow-left": "\f116",
|
||||
"arrow-right": "\f117",
|
||||
"ask-support": "\f118",
|
||||
"attach": "\f119",
|
||||
"auction-drop": "\f11a",
|
||||
"auction-filled": "\f11b",
|
||||
"auction-next-round": "\f11c",
|
||||
"auction": "\f11d",
|
||||
"author-hidden": "\f11e",
|
||||
"avatar-archived-chats": "\f11f",
|
||||
"avatar-deleted-account": "\f120",
|
||||
"avatar-saved-messages": "\f121",
|
||||
"bold": "\f122",
|
||||
"boost-craft-chance": "\f123",
|
||||
"boost-outline": "\f124",
|
||||
"boost": "\f125",
|
||||
"boostcircle": "\f126",
|
||||
"boosts": "\f127",
|
||||
"bot-command": "\f128",
|
||||
"bot-commands-filled": "\f129",
|
||||
"bots": "\f12a",
|
||||
"brush": "\f12b",
|
||||
"bug": "\f12c",
|
||||
"calendar-filter": "\f12d",
|
||||
"calendar": "\f12e",
|
||||
"camera-add": "\f12f",
|
||||
"camera": "\f130",
|
||||
"car": "\f131",
|
||||
"card": "\f132",
|
||||
"cash-circle": "\f133",
|
||||
"channel-filled": "\f134",
|
||||
"channel": "\f135",
|
||||
"channelviews": "\f136",
|
||||
"chat-badge": "\f137",
|
||||
"chats-badge": "\f138",
|
||||
"check-bold": "\f139",
|
||||
"check": "\f13a",
|
||||
"clock-edit": "\f13b",
|
||||
"clock": "\f13c",
|
||||
"close-circle": "\f13d",
|
||||
"close-topic": "\f13e",
|
||||
"close": "\f13f",
|
||||
"closed-gift": "\f140",
|
||||
"cloud-download": "\f141",
|
||||
"collapse-modal": "\f142",
|
||||
"collapse": "\f143",
|
||||
"colorize": "\f144",
|
||||
"combine-craft": "\f145",
|
||||
"comments-sticker": "\f146",
|
||||
"comments": "\f147",
|
||||
"copy-media": "\f148",
|
||||
"copy": "\f149",
|
||||
"craft": "\f14a",
|
||||
"crop": "\f14b",
|
||||
"crown-take-off-outline": "\f14c",
|
||||
"crown-take-off": "\f14d",
|
||||
"crown-wear-outline": "\f14e",
|
||||
"crown-wear": "\f14f",
|
||||
"darkmode": "\f150",
|
||||
"data": "\f151",
|
||||
"delete-filled": "\f152",
|
||||
"delete-left": "\f153",
|
||||
"delete-user": "\f154",
|
||||
"delete": "\f155",
|
||||
"diamond": "\f156",
|
||||
"document": "\f157",
|
||||
"double-badge": "\f158",
|
||||
"down": "\f159",
|
||||
"download": "\f15a",
|
||||
"dropdown-arrows": "\f15b",
|
||||
"eats": "\f15c",
|
||||
"edit": "\f15d",
|
||||
"email": "\f15e",
|
||||
"enter": "\f15f",
|
||||
"expand-modal": "\f160",
|
||||
"expand": "\f161",
|
||||
"eye-crossed-outline": "\f162",
|
||||
"eye-crossed": "\f163",
|
||||
"eye-outline": "\f164",
|
||||
"eye": "\f165",
|
||||
"favorite-filled": "\f166",
|
||||
"favorite": "\f167",
|
||||
"file-badge": "\f168",
|
||||
"flag": "\f169",
|
||||
"flip": "\f16a",
|
||||
"folder-badge": "\f16b",
|
||||
"folder-tabs-bot": "\f16c",
|
||||
"folder-tabs-channel": "\f16d",
|
||||
"folder-tabs-chat": "\f16e",
|
||||
"folder-tabs-chats": "\f16f",
|
||||
"folder-tabs-folder": "\f170",
|
||||
"folder-tabs-group": "\f171",
|
||||
"folder-tabs-star": "\f172",
|
||||
"folder-tabs-user": "\f173",
|
||||
"folder": "\f174",
|
||||
"fontsize": "\f175",
|
||||
"forums": "\f176",
|
||||
"forward": "\f177",
|
||||
"fragment": "\f178",
|
||||
"frozen-time": "\f179",
|
||||
"fullscreen": "\f17a",
|
||||
"gifs": "\f17b",
|
||||
"gift-transfer-inline": "\f17c",
|
||||
"gift": "\f17d",
|
||||
"group-filled": "\f17e",
|
||||
"group": "\f17f",
|
||||
"grouped-disable": "\f180",
|
||||
"grouped": "\f181",
|
||||
"hand-stop-filled": "\f182",
|
||||
"hand-stop": "\f183",
|
||||
"hashtag": "\f184",
|
||||
"hd-photo": "\f185",
|
||||
"heart-outline": "\f186",
|
||||
"heart": "\f187",
|
||||
"help": "\f188",
|
||||
"info-filled": "\f189",
|
||||
"info": "\f18a",
|
||||
"install": "\f18b",
|
||||
"italic": "\f18c",
|
||||
"key": "\f18d",
|
||||
"keyboard": "\f18e",
|
||||
"lamp": "\f18f",
|
||||
"language": "\f190",
|
||||
"large-pause": "\f191",
|
||||
"large-play": "\f192",
|
||||
"link-badge": "\f193",
|
||||
"link-broken": "\f194",
|
||||
"link": "\f195",
|
||||
"location": "\f196",
|
||||
"lock-badge": "\f197",
|
||||
"lock": "\f198",
|
||||
"logout": "\f199",
|
||||
"loop": "\f19a",
|
||||
"mention": "\f19b",
|
||||
"menu": "\f19c",
|
||||
"message-failed": "\f19d",
|
||||
"message-pending": "\f19e",
|
||||
"message-read": "\f19f",
|
||||
"message-succeeded": "\f1a0",
|
||||
"message": "\f1a1",
|
||||
"microphone-alt": "\f1a2",
|
||||
"microphone": "\f1a3",
|
||||
"monospace": "\f1a4",
|
||||
"more-circle": "\f1a5",
|
||||
"more": "\f1a6",
|
||||
"move-caption-down": "\f1a7",
|
||||
"move-caption-up": "\f1a8",
|
||||
"mute": "\f1a9",
|
||||
"muted": "\f1aa",
|
||||
"my-notes": "\f1ab",
|
||||
"new-chat-filled": "\f1ac",
|
||||
"new-send": "\f1ad",
|
||||
"next-link": "\f1ae",
|
||||
"next": "\f1af",
|
||||
"no-download": "\f1b0",
|
||||
"no-share": "\f1b1",
|
||||
"nochannel": "\f1b2",
|
||||
"noise-suppression": "\f1b3",
|
||||
"non-contacts": "\f1b4",
|
||||
"note": "\f1b5",
|
||||
"one-filled": "\f1b6",
|
||||
"open-in-new-tab": "\f1b7",
|
||||
"password-off": "\f1b8",
|
||||
"pause": "\f1b9",
|
||||
"permissions": "\f1ba",
|
||||
"phone-discard-outline": "\f1bb",
|
||||
"phone-discard": "\f1bc",
|
||||
"phone": "\f1bd",
|
||||
"photo": "\f1be",
|
||||
"pin-badge": "\f1bf",
|
||||
"pin-list": "\f1c0",
|
||||
"pin": "\f1c1",
|
||||
"pinned-chat": "\f1c2",
|
||||
"pinned-message": "\f1c3",
|
||||
"pip": "\f1c4",
|
||||
"play-story": "\f1c5",
|
||||
"play": "\f1c6",
|
||||
"poll": "\f1c7",
|
||||
"previous-link": "\f1c8",
|
||||
"previous": "\f1c9",
|
||||
"privacy-policy": "\f1ca",
|
||||
"proof-of-ownership": "\f1cb",
|
||||
"quote-text": "\f1cc",
|
||||
"quote": "\f1cd",
|
||||
"radial-badge": "\f1ce",
|
||||
"rating-icons-level1": "\f1cf",
|
||||
"rating-icons-level10": "\f1d0",
|
||||
"rating-icons-level2": "\f1d1",
|
||||
"rating-icons-level20": "\f1d2",
|
||||
"rating-icons-level3": "\f1d3",
|
||||
"rating-icons-level30": "\f1d4",
|
||||
"rating-icons-level4": "\f1d5",
|
||||
"rating-icons-level40": "\f1d6",
|
||||
"rating-icons-level5": "\f1d7",
|
||||
"rating-icons-level50": "\f1d8",
|
||||
"rating-icons-level6": "\f1d9",
|
||||
"rating-icons-level60": "\f1da",
|
||||
"rating-icons-level7": "\f1db",
|
||||
"rating-icons-level70": "\f1dc",
|
||||
"rating-icons-level8": "\f1dd",
|
||||
"rating-icons-level80": "\f1de",
|
||||
"rating-icons-level9": "\f1df",
|
||||
"rating-icons-level90": "\f1e0",
|
||||
"rating-icons-negative": "\f1e1",
|
||||
"readchats": "\f1e2",
|
||||
"recent": "\f1e3",
|
||||
"redo": "\f1e4",
|
||||
"refund": "\f1e5",
|
||||
"reload": "\f1e6",
|
||||
"remove-quote": "\f1e7",
|
||||
"remove": "\f1e8",
|
||||
"reopen-topic": "\f1e9",
|
||||
"reorder-tabs": "\f1ea",
|
||||
"replace": "\f1eb",
|
||||
"replies": "\f1ec",
|
||||
"reply-filled": "\f1ed",
|
||||
"reply": "\f1ee",
|
||||
"revenue-split": "\f1ef",
|
||||
"revote": "\f1f0",
|
||||
"rotate": "\f1f1",
|
||||
"save-story": "\f1f2",
|
||||
"saved-messages": "\f1f3",
|
||||
"schedule": "\f1f4",
|
||||
"scheduled": "\f1f5",
|
||||
"sd-photo": "\f1f6",
|
||||
"search": "\f1f7",
|
||||
"select-filled": "\f1f8",
|
||||
"select": "\f1f9",
|
||||
"sell-outline": "\f1fa",
|
||||
"sell": "\f1fb",
|
||||
"send-outline": "\f1fc",
|
||||
"send": "\f1fd",
|
||||
"settings-filled": "\f1fe",
|
||||
"settings": "\f1ff",
|
||||
"share-filled": "\f200",
|
||||
"share-screen-outlined": "\f201",
|
||||
"share-screen-stop": "\f202",
|
||||
"share-screen": "\f203",
|
||||
"show-message": "\f204",
|
||||
"sidebar": "\f205",
|
||||
"skip-next": "\f206",
|
||||
"skip-previous": "\f207",
|
||||
"smallscreen": "\f208",
|
||||
"smile": "\f209",
|
||||
"sort-by-date": "\f20a",
|
||||
"sort-by-number": "\f20b",
|
||||
"sort-by-price": "\f20c",
|
||||
"sort": "\f20d",
|
||||
"speaker-muted-story": "\f20e",
|
||||
"speaker-outline": "\f20f",
|
||||
"speaker-story": "\f210",
|
||||
"speaker": "\f211",
|
||||
"spoiler-disable": "\f212",
|
||||
"spoiler": "\f213",
|
||||
"sport": "\f214",
|
||||
"star": "\f215",
|
||||
"stars-lock": "\f216",
|
||||
"stars-refund": "\f217",
|
||||
"stats": "\f218",
|
||||
"stealth-future": "\f219",
|
||||
"stealth-past": "\f21a",
|
||||
"stickers": "\f21b",
|
||||
"stop-raising-hand": "\f21c",
|
||||
"stop": "\f21d",
|
||||
"story-caption": "\f21e",
|
||||
"story-expired": "\f21f",
|
||||
"story-priority": "\f220",
|
||||
"story-reply": "\f221",
|
||||
"strikethrough": "\f222",
|
||||
"tag-add": "\f223",
|
||||
"tag-crossed": "\f224",
|
||||
"tag-filter": "\f225",
|
||||
"tag-name": "\f226",
|
||||
"tag": "\f227",
|
||||
"timer": "\f228",
|
||||
"toncoin": "\f229",
|
||||
"tools": "\f22a",
|
||||
"topic-new": "\f22b",
|
||||
"trade": "\f22c",
|
||||
"transcribe": "\f22d",
|
||||
"truck": "\f22e",
|
||||
"unarchive": "\f22f",
|
||||
"underlined": "\f230",
|
||||
"understood": "\f231",
|
||||
"undo": "\f232",
|
||||
"unique-profile": "\f233",
|
||||
"unlist-outline": "\f234",
|
||||
"unlist": "\f235",
|
||||
"unlock-badge": "\f236",
|
||||
"unlock": "\f237",
|
||||
"unmute": "\f238",
|
||||
"unpin": "\f239",
|
||||
"unread": "\f23a",
|
||||
"up": "\f23b",
|
||||
"user-filled": "\f23c",
|
||||
"user-online": "\f23d",
|
||||
"user-stars": "\f23e",
|
||||
"user-tag": "\f23f",
|
||||
"user": "\f240",
|
||||
"video-outlined": "\f241",
|
||||
"video-stop": "\f242",
|
||||
"video": "\f243",
|
||||
"view-once": "\f244",
|
||||
"voice-chat": "\f245",
|
||||
"volume-1": "\f246",
|
||||
"volume-2": "\f247",
|
||||
"volume-3": "\f248",
|
||||
"warning": "\f249",
|
||||
"web": "\f24a",
|
||||
"webapp": "\f24b",
|
||||
"word-wrap": "\f24c",
|
||||
"zoom-in": "\f24d",
|
||||
"zoom-out": "\f24e",
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -352,11 +352,10 @@ body:not(.is-ios) {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.next-arrow-icon {
|
||||
.link-arrow-icon {
|
||||
font-size: var(--next-arrow-size, 0.5625em);
|
||||
font-weight: var(--next-arrow-weight, inherit);
|
||||
line-height: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.shared-canvas-container {
|
||||
|
||||
@ -1,334 +1,335 @@
|
||||
export type FontIconName =
|
||||
| 'zoom-out'
|
||||
| 'zoom-in'
|
||||
| 'word-wrap'
|
||||
| 'webapp'
|
||||
| 'web'
|
||||
| 'warning'
|
||||
| 'volume-3'
|
||||
| 'volume-2'
|
||||
| 'volume-1'
|
||||
| 'voice-chat'
|
||||
| 'view-once'
|
||||
| 'video'
|
||||
| 'video-stop'
|
||||
| 'video-outlined'
|
||||
| 'user'
|
||||
| 'user-tag'
|
||||
| 'user-stars'
|
||||
| 'user-online'
|
||||
| 'user-filled'
|
||||
| 'up'
|
||||
| 'unread'
|
||||
| 'unpin'
|
||||
| 'unmute'
|
||||
| 'unlock'
|
||||
| 'unlock-badge'
|
||||
| 'unlist'
|
||||
| 'unlist-outline'
|
||||
| 'unique-profile'
|
||||
| 'undo'
|
||||
| 'understood'
|
||||
| 'underlined'
|
||||
| 'unarchive'
|
||||
| 'truck'
|
||||
| 'transcribe'
|
||||
| 'trade'
|
||||
| 'topic-new'
|
||||
| 'tools'
|
||||
| 'toncoin'
|
||||
| 'timer'
|
||||
| 'tag'
|
||||
| 'tag-name'
|
||||
| 'tag-filter'
|
||||
| 'tag-crossed'
|
||||
| 'tag-add'
|
||||
| 'strikethrough'
|
||||
| 'story-reply'
|
||||
| 'story-priority'
|
||||
| 'story-expired'
|
||||
| 'story-caption'
|
||||
| 'stop'
|
||||
| 'stop-raising-hand'
|
||||
| 'stickers'
|
||||
| 'stealth-past'
|
||||
| 'stealth-future'
|
||||
| 'stats'
|
||||
| 'stars-refund'
|
||||
| 'stars-lock'
|
||||
| 'star'
|
||||
| 'sport'
|
||||
| 'spoiler'
|
||||
| 'spoiler-disable'
|
||||
| 'speaker'
|
||||
| 'speaker-story'
|
||||
| 'speaker-outline'
|
||||
| 'speaker-muted-story'
|
||||
| 'sort'
|
||||
| 'sort-by-price'
|
||||
| 'sort-by-number'
|
||||
| 'sort-by-date'
|
||||
| 'smile'
|
||||
| 'smallscreen'
|
||||
| 'skip-previous'
|
||||
| 'skip-next'
|
||||
| 'sidebar'
|
||||
| 'show-message'
|
||||
| 'share-screen'
|
||||
| 'share-screen-stop'
|
||||
| 'share-screen-outlined'
|
||||
| 'share-filled'
|
||||
| 'settings'
|
||||
| 'settings-filled'
|
||||
| 'send'
|
||||
| 'send-outline'
|
||||
| 'sell'
|
||||
| 'sell-outline'
|
||||
| 'select'
|
||||
| 'select-filled'
|
||||
| 'search'
|
||||
| 'sd-photo'
|
||||
| 'scheduled'
|
||||
| 'schedule'
|
||||
| 'saved-messages'
|
||||
| 'save-story'
|
||||
| 'rotate'
|
||||
| 'revote'
|
||||
| 'revenue-split'
|
||||
| 'reply'
|
||||
| 'reply-filled'
|
||||
| 'replies'
|
||||
| 'replace'
|
||||
| 'reorder-tabs'
|
||||
| 'reopen-topic'
|
||||
| 'remove'
|
||||
| 'remove-quote'
|
||||
| 'reload'
|
||||
| 'refund'
|
||||
| 'redo'
|
||||
| 'recent'
|
||||
| 'readchats'
|
||||
| 'radial-badge'
|
||||
| 'quote'
|
||||
| 'quote-text'
|
||||
| 'proof-of-ownership'
|
||||
| 'privacy-policy'
|
||||
| 'previous'
|
||||
| 'poll'
|
||||
| 'play'
|
||||
| 'play-story'
|
||||
| 'pip'
|
||||
| 'pinned-message'
|
||||
| 'pinned-chat'
|
||||
| 'pin'
|
||||
| 'pin-list'
|
||||
| 'pin-badge'
|
||||
| 'photo'
|
||||
| 'phone'
|
||||
| 'phone-discard'
|
||||
| 'phone-discard-outline'
|
||||
| 'permissions'
|
||||
| 'pause'
|
||||
| 'password-off'
|
||||
| 'open-in-new-tab'
|
||||
| 'one-filled'
|
||||
| 'note'
|
||||
| 'non-contacts'
|
||||
| 'noise-suppression'
|
||||
| 'nochannel'
|
||||
| 'no-share'
|
||||
| 'no-download'
|
||||
| 'next'
|
||||
| 'next-link'
|
||||
| 'new-send'
|
||||
| 'new-chat-filled'
|
||||
| 'my-notes'
|
||||
| 'muted'
|
||||
| 'mute'
|
||||
| 'move-caption-up'
|
||||
| 'move-caption-down'
|
||||
| 'more'
|
||||
| 'more-circle'
|
||||
| 'monospace'
|
||||
| 'microphone'
|
||||
| 'microphone-alt'
|
||||
| 'message'
|
||||
| 'message-succeeded'
|
||||
| 'message-read'
|
||||
| 'message-pending'
|
||||
| 'message-failed'
|
||||
| 'menu'
|
||||
| 'mention'
|
||||
| 'loop'
|
||||
| 'logout'
|
||||
| 'lock'
|
||||
| 'lock-badge'
|
||||
| 'location'
|
||||
| 'link'
|
||||
| 'link-broken'
|
||||
| 'link-badge'
|
||||
| 'large-play'
|
||||
| 'large-pause'
|
||||
| 'language'
|
||||
| 'lamp'
|
||||
| 'keyboard'
|
||||
| 'key'
|
||||
| 'italic'
|
||||
| 'install'
|
||||
| 'info'
|
||||
| 'info-filled'
|
||||
| 'help'
|
||||
| 'heart'
|
||||
| 'heart-outline'
|
||||
| 'hd-photo'
|
||||
| 'hashtag'
|
||||
| 'hand-stop'
|
||||
| 'hand-stop-filled'
|
||||
| 'grouped'
|
||||
| 'grouped-disable'
|
||||
| 'group'
|
||||
| 'group-filled'
|
||||
| 'gift'
|
||||
| 'gift-transfer-inline'
|
||||
| 'gifs'
|
||||
| 'fullscreen'
|
||||
| 'frozen-time'
|
||||
| 'fragment'
|
||||
| 'forward'
|
||||
| 'forums'
|
||||
| 'fontsize'
|
||||
| 'folder'
|
||||
| 'folder-badge'
|
||||
| 'flip'
|
||||
| 'flag'
|
||||
| 'file-badge'
|
||||
| 'favorite'
|
||||
| 'favorite-filled'
|
||||
| 'eye'
|
||||
| 'eye-outline'
|
||||
| 'eye-crossed'
|
||||
| 'eye-crossed-outline'
|
||||
| 'expand'
|
||||
| 'expand-modal'
|
||||
| 'enter'
|
||||
| 'email'
|
||||
| 'edit'
|
||||
| 'eats'
|
||||
| 'dropdown-arrows'
|
||||
| 'download'
|
||||
| 'down'
|
||||
| 'double-badge'
|
||||
| 'document'
|
||||
| 'diamond'
|
||||
| 'delete'
|
||||
| 'delete-user'
|
||||
| 'delete-left'
|
||||
| 'delete-filled'
|
||||
| 'data'
|
||||
| 'darkmode'
|
||||
| 'crown-wear'
|
||||
| 'crown-wear-outline'
|
||||
| 'crown-take-off'
|
||||
| 'crown-take-off-outline'
|
||||
| 'crop'
|
||||
| 'craft'
|
||||
| 'copy'
|
||||
| 'copy-media'
|
||||
| 'comments'
|
||||
| 'comments-sticker'
|
||||
| 'combine-craft'
|
||||
| 'colorize'
|
||||
| 'collapse'
|
||||
| 'collapse-modal'
|
||||
| 'cloud-download'
|
||||
| 'closed-gift'
|
||||
| 'close'
|
||||
| 'close-topic'
|
||||
| 'close-circle'
|
||||
| 'clock'
|
||||
| 'clock-edit'
|
||||
| 'check'
|
||||
| 'check-bold'
|
||||
| 'chats-badge'
|
||||
| 'chat-badge'
|
||||
| 'channelviews'
|
||||
| 'channel'
|
||||
| 'channel-filled'
|
||||
| 'cash-circle'
|
||||
| 'card'
|
||||
| 'car'
|
||||
| 'camera'
|
||||
| 'camera-add'
|
||||
| 'calendar'
|
||||
| 'calendar-filter'
|
||||
| 'bug'
|
||||
| 'brush'
|
||||
| 'bots'
|
||||
| 'bot-commands-filled'
|
||||
| 'bot-command'
|
||||
| 'boosts'
|
||||
| 'boostcircle'
|
||||
| 'boost'
|
||||
| 'boost-outline'
|
||||
| 'boost-craft-chance'
|
||||
| 'bold'
|
||||
| 'avatar-saved-messages'
|
||||
| 'avatar-deleted-account'
|
||||
| 'avatar-archived-chats'
|
||||
| 'author-hidden'
|
||||
| 'auction'
|
||||
| 'auction-next-round'
|
||||
| 'auction-filled'
|
||||
| 'auction-drop'
|
||||
| 'attach'
|
||||
| 'ask-support'
|
||||
| 'arrow-right'
|
||||
| 'arrow-left'
|
||||
| 'arrow-down'
|
||||
| 'arrow-down-circle'
|
||||
| 'archive'
|
||||
| 'archive-to-main'
|
||||
| 'archive-from-main'
|
||||
| 'archive-filled'
|
||||
| 'animations'
|
||||
| 'animals'
|
||||
| 'allow-speak'
|
||||
| 'allow-share'
|
||||
| 'ai'
|
||||
| 'ai-fix'
|
||||
| 'ai-edit'
|
||||
| 'admin'
|
||||
| 'add'
|
||||
| 'add-user'
|
||||
| 'add-user-filled'
|
||||
| 'add-one-badge'
|
||||
| 'add-filled'
|
||||
| 'add-caption'
|
||||
| 'active-sessions'
|
||||
| 'rating-icons-negative'
|
||||
| 'rating-icons-level90'
|
||||
| 'rating-icons-level9'
|
||||
| 'rating-icons-level80'
|
||||
| 'rating-icons-level8'
|
||||
| 'rating-icons-level70'
|
||||
| 'rating-icons-level7'
|
||||
| 'rating-icons-level60'
|
||||
| 'rating-icons-level6'
|
||||
| 'rating-icons-level50'
|
||||
| 'rating-icons-level5'
|
||||
| 'rating-icons-level40'
|
||||
| 'rating-icons-level4'
|
||||
| 'rating-icons-level30'
|
||||
| 'rating-icons-level3'
|
||||
| 'rating-icons-level20'
|
||||
| 'rating-icons-level2'
|
||||
| 'rating-icons-level10'
|
||||
| 'rating-icons-level1'
|
||||
| 'folder-tabs-user'
|
||||
| 'folder-tabs-star'
|
||||
| 'folder-tabs-group'
|
||||
| 'folder-tabs-folder'
|
||||
| 'folder-tabs-chats'
|
||||
| 'folder-tabs-chat'
|
||||
| 'add-caption'
|
||||
| 'add-filled'
|
||||
| 'add-one-badge'
|
||||
| 'add-user-filled'
|
||||
| 'add-user'
|
||||
| 'add'
|
||||
| 'admin'
|
||||
| 'ai-edit'
|
||||
| 'ai-fix'
|
||||
| 'ai'
|
||||
| 'allow-share'
|
||||
| 'allow-speak'
|
||||
| 'animals'
|
||||
| 'animations'
|
||||
| 'archive-filled'
|
||||
| 'archive-from-main'
|
||||
| 'archive-to-main'
|
||||
| 'archive'
|
||||
| 'arrow-down-circle'
|
||||
| 'arrow-down'
|
||||
| 'arrow-left'
|
||||
| 'arrow-right'
|
||||
| 'ask-support'
|
||||
| 'attach'
|
||||
| 'auction-drop'
|
||||
| 'auction-filled'
|
||||
| 'auction-next-round'
|
||||
| 'auction'
|
||||
| 'author-hidden'
|
||||
| 'avatar-archived-chats'
|
||||
| 'avatar-deleted-account'
|
||||
| 'avatar-saved-messages'
|
||||
| 'bold'
|
||||
| 'boost-craft-chance'
|
||||
| 'boost-outline'
|
||||
| 'boost'
|
||||
| 'boostcircle'
|
||||
| 'boosts'
|
||||
| 'bot-command'
|
||||
| 'bot-commands-filled'
|
||||
| 'bots'
|
||||
| 'brush'
|
||||
| 'bug'
|
||||
| 'calendar-filter'
|
||||
| 'calendar'
|
||||
| 'camera-add'
|
||||
| 'camera'
|
||||
| 'car'
|
||||
| 'card'
|
||||
| 'cash-circle'
|
||||
| 'channel-filled'
|
||||
| 'channel'
|
||||
| 'channelviews'
|
||||
| 'chat-badge'
|
||||
| 'chats-badge'
|
||||
| 'check-bold'
|
||||
| 'check'
|
||||
| 'clock-edit'
|
||||
| 'clock'
|
||||
| 'close-circle'
|
||||
| 'close-topic'
|
||||
| 'close'
|
||||
| 'closed-gift'
|
||||
| 'cloud-download'
|
||||
| 'collapse-modal'
|
||||
| 'collapse'
|
||||
| 'colorize'
|
||||
| 'combine-craft'
|
||||
| 'comments-sticker'
|
||||
| 'comments'
|
||||
| 'copy-media'
|
||||
| 'copy'
|
||||
| 'craft'
|
||||
| 'crop'
|
||||
| 'crown-take-off-outline'
|
||||
| 'crown-take-off'
|
||||
| 'crown-wear-outline'
|
||||
| 'crown-wear'
|
||||
| 'darkmode'
|
||||
| 'data'
|
||||
| 'delete-filled'
|
||||
| 'delete-left'
|
||||
| 'delete-user'
|
||||
| 'delete'
|
||||
| 'diamond'
|
||||
| 'document'
|
||||
| 'double-badge'
|
||||
| 'down'
|
||||
| 'download'
|
||||
| 'dropdown-arrows'
|
||||
| 'eats'
|
||||
| 'edit'
|
||||
| 'email'
|
||||
| 'enter'
|
||||
| 'expand-modal'
|
||||
| 'expand'
|
||||
| 'eye-crossed-outline'
|
||||
| 'eye-crossed'
|
||||
| 'eye-outline'
|
||||
| 'eye'
|
||||
| 'favorite-filled'
|
||||
| 'favorite'
|
||||
| 'file-badge'
|
||||
| 'flag'
|
||||
| 'flip'
|
||||
| 'folder-badge'
|
||||
| 'folder-tabs-bot'
|
||||
| 'folder-tabs-channel'
|
||||
| 'folder-tabs-bot';
|
||||
| 'folder-tabs-chat'
|
||||
| 'folder-tabs-chats'
|
||||
| 'folder-tabs-folder'
|
||||
| 'folder-tabs-group'
|
||||
| 'folder-tabs-star'
|
||||
| 'folder-tabs-user'
|
||||
| 'folder'
|
||||
| 'fontsize'
|
||||
| 'forums'
|
||||
| 'forward'
|
||||
| 'fragment'
|
||||
| 'frozen-time'
|
||||
| 'fullscreen'
|
||||
| 'gifs'
|
||||
| 'gift-transfer-inline'
|
||||
| 'gift'
|
||||
| 'group-filled'
|
||||
| 'group'
|
||||
| 'grouped-disable'
|
||||
| 'grouped'
|
||||
| 'hand-stop-filled'
|
||||
| 'hand-stop'
|
||||
| 'hashtag'
|
||||
| 'hd-photo'
|
||||
| 'heart-outline'
|
||||
| 'heart'
|
||||
| 'help'
|
||||
| 'info-filled'
|
||||
| 'info'
|
||||
| 'install'
|
||||
| 'italic'
|
||||
| 'key'
|
||||
| 'keyboard'
|
||||
| 'lamp'
|
||||
| 'language'
|
||||
| 'large-pause'
|
||||
| 'large-play'
|
||||
| 'link-badge'
|
||||
| 'link-broken'
|
||||
| 'link'
|
||||
| 'location'
|
||||
| 'lock-badge'
|
||||
| 'lock'
|
||||
| 'logout'
|
||||
| 'loop'
|
||||
| 'mention'
|
||||
| 'menu'
|
||||
| 'message-failed'
|
||||
| 'message-pending'
|
||||
| 'message-read'
|
||||
| 'message-succeeded'
|
||||
| 'message'
|
||||
| 'microphone-alt'
|
||||
| 'microphone'
|
||||
| 'monospace'
|
||||
| 'more-circle'
|
||||
| 'more'
|
||||
| 'move-caption-down'
|
||||
| 'move-caption-up'
|
||||
| 'mute'
|
||||
| 'muted'
|
||||
| 'my-notes'
|
||||
| 'new-chat-filled'
|
||||
| 'new-send'
|
||||
| 'next-link'
|
||||
| 'next'
|
||||
| 'no-download'
|
||||
| 'no-share'
|
||||
| 'nochannel'
|
||||
| 'noise-suppression'
|
||||
| 'non-contacts'
|
||||
| 'note'
|
||||
| 'one-filled'
|
||||
| 'open-in-new-tab'
|
||||
| 'password-off'
|
||||
| 'pause'
|
||||
| 'permissions'
|
||||
| 'phone-discard-outline'
|
||||
| 'phone-discard'
|
||||
| 'phone'
|
||||
| 'photo'
|
||||
| 'pin-badge'
|
||||
| 'pin-list'
|
||||
| 'pin'
|
||||
| 'pinned-chat'
|
||||
| 'pinned-message'
|
||||
| 'pip'
|
||||
| 'play-story'
|
||||
| 'play'
|
||||
| 'poll'
|
||||
| 'previous-link'
|
||||
| 'previous'
|
||||
| 'privacy-policy'
|
||||
| 'proof-of-ownership'
|
||||
| 'quote-text'
|
||||
| 'quote'
|
||||
| 'radial-badge'
|
||||
| 'rating-icons-level1'
|
||||
| 'rating-icons-level10'
|
||||
| 'rating-icons-level2'
|
||||
| 'rating-icons-level20'
|
||||
| 'rating-icons-level3'
|
||||
| 'rating-icons-level30'
|
||||
| 'rating-icons-level4'
|
||||
| 'rating-icons-level40'
|
||||
| 'rating-icons-level5'
|
||||
| 'rating-icons-level50'
|
||||
| 'rating-icons-level6'
|
||||
| 'rating-icons-level60'
|
||||
| 'rating-icons-level7'
|
||||
| 'rating-icons-level70'
|
||||
| 'rating-icons-level8'
|
||||
| 'rating-icons-level80'
|
||||
| 'rating-icons-level9'
|
||||
| 'rating-icons-level90'
|
||||
| 'rating-icons-negative'
|
||||
| 'readchats'
|
||||
| 'recent'
|
||||
| 'redo'
|
||||
| 'refund'
|
||||
| 'reload'
|
||||
| 'remove-quote'
|
||||
| 'remove'
|
||||
| 'reopen-topic'
|
||||
| 'reorder-tabs'
|
||||
| 'replace'
|
||||
| 'replies'
|
||||
| 'reply-filled'
|
||||
| 'reply'
|
||||
| 'revenue-split'
|
||||
| 'revote'
|
||||
| 'rotate'
|
||||
| 'save-story'
|
||||
| 'saved-messages'
|
||||
| 'schedule'
|
||||
| 'scheduled'
|
||||
| 'sd-photo'
|
||||
| 'search'
|
||||
| 'select-filled'
|
||||
| 'select'
|
||||
| 'sell-outline'
|
||||
| 'sell'
|
||||
| 'send-outline'
|
||||
| 'send'
|
||||
| 'settings-filled'
|
||||
| 'settings'
|
||||
| 'share-filled'
|
||||
| 'share-screen-outlined'
|
||||
| 'share-screen-stop'
|
||||
| 'share-screen'
|
||||
| 'show-message'
|
||||
| 'sidebar'
|
||||
| 'skip-next'
|
||||
| 'skip-previous'
|
||||
| 'smallscreen'
|
||||
| 'smile'
|
||||
| 'sort-by-date'
|
||||
| 'sort-by-number'
|
||||
| 'sort-by-price'
|
||||
| 'sort'
|
||||
| 'speaker-muted-story'
|
||||
| 'speaker-outline'
|
||||
| 'speaker-story'
|
||||
| 'speaker'
|
||||
| 'spoiler-disable'
|
||||
| 'spoiler'
|
||||
| 'sport'
|
||||
| 'star'
|
||||
| 'stars-lock'
|
||||
| 'stars-refund'
|
||||
| 'stats'
|
||||
| 'stealth-future'
|
||||
| 'stealth-past'
|
||||
| 'stickers'
|
||||
| 'stop-raising-hand'
|
||||
| 'stop'
|
||||
| 'story-caption'
|
||||
| 'story-expired'
|
||||
| 'story-priority'
|
||||
| 'story-reply'
|
||||
| 'strikethrough'
|
||||
| 'tag-add'
|
||||
| 'tag-crossed'
|
||||
| 'tag-filter'
|
||||
| 'tag-name'
|
||||
| 'tag'
|
||||
| 'timer'
|
||||
| 'toncoin'
|
||||
| 'tools'
|
||||
| 'topic-new'
|
||||
| 'trade'
|
||||
| 'transcribe'
|
||||
| 'truck'
|
||||
| 'unarchive'
|
||||
| 'underlined'
|
||||
| 'understood'
|
||||
| 'undo'
|
||||
| 'unique-profile'
|
||||
| 'unlist-outline'
|
||||
| 'unlist'
|
||||
| 'unlock-badge'
|
||||
| 'unlock'
|
||||
| 'unmute'
|
||||
| 'unpin'
|
||||
| 'unread'
|
||||
| 'up'
|
||||
| 'user-filled'
|
||||
| 'user-online'
|
||||
| 'user-stars'
|
||||
| 'user-tag'
|
||||
| 'user'
|
||||
| 'video-outlined'
|
||||
| 'video-stop'
|
||||
| 'video'
|
||||
| 'view-once'
|
||||
| 'voice-chat'
|
||||
| 'volume-1'
|
||||
| 'volume-2'
|
||||
| 'volume-3'
|
||||
| 'warning'
|
||||
| 'web'
|
||||
| 'webapp'
|
||||
| 'word-wrap'
|
||||
| 'zoom-in'
|
||||
| 'zoom-out';
|
||||
|
||||
@ -342,6 +342,7 @@ export enum MediaViewerOrigin {
|
||||
StarsTransaction,
|
||||
PreviewMedia,
|
||||
SponsoredMessage,
|
||||
PollPreview,
|
||||
}
|
||||
|
||||
export enum StoryViewerOrigin {
|
||||
|
||||
39
src/types/language.d.ts
vendored
39
src/types/language.d.ts
vendored
@ -713,7 +713,10 @@ export interface LangPair {
|
||||
'CallAgain': undefined;
|
||||
'CallBack': undefined;
|
||||
'PollSubmitVotes': undefined;
|
||||
'PollSubmitAnswers': undefined;
|
||||
'PollViewResults': undefined;
|
||||
'PollBackToVote': undefined;
|
||||
'PollBackToAnswer': undefined;
|
||||
'ChatQuizTotalVotesEmpty': undefined;
|
||||
'ChatPollTotalVotesResultEmpty': undefined;
|
||||
'Vote': undefined;
|
||||
@ -1403,6 +1406,7 @@ export interface LangPair {
|
||||
'StarsSubscribeInfoLinkText': undefined;
|
||||
'StarsSubscribeInfoLink': undefined;
|
||||
'StarsBalance': undefined;
|
||||
'OpenMapWith': undefined;
|
||||
'OpenApp': undefined;
|
||||
'PopularApps': undefined;
|
||||
'SearchApps': undefined;
|
||||
@ -2284,6 +2288,15 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'time': V;
|
||||
'duration': V;
|
||||
};
|
||||
'PollEndsTime': {
|
||||
'time': V;
|
||||
};
|
||||
'PollResultsTime': {
|
||||
'time': V;
|
||||
};
|
||||
'TimeIn': {
|
||||
'time': V;
|
||||
};
|
||||
'MessageScheduledOn': {
|
||||
'date': V;
|
||||
};
|
||||
@ -3428,6 +3441,20 @@ export interface LangPairWithVariables<V = LangVariable> {
|
||||
'tasks': V;
|
||||
'list': V;
|
||||
};
|
||||
'MessageActionPollAppendAnswer': {
|
||||
'peer': V;
|
||||
'option': V;
|
||||
};
|
||||
'MessageActionPollAppendAnswerYou': {
|
||||
'option': V;
|
||||
};
|
||||
'MessageActionPollDeleteAnswer': {
|
||||
'peer': V;
|
||||
'option': V;
|
||||
};
|
||||
'MessageActionPollDeleteAnswerYou': {
|
||||
'option': V;
|
||||
};
|
||||
'GiftInfoCollectibleBy': {
|
||||
'number': V;
|
||||
'owner': V;
|
||||
@ -3724,9 +3751,21 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
||||
'ConversationContextMenuSeen': {
|
||||
'count': V;
|
||||
};
|
||||
'PollVoteCountButton': {
|
||||
'count': V;
|
||||
};
|
||||
'PollAnswerCountButton': {
|
||||
'count': V;
|
||||
};
|
||||
'Answer': {
|
||||
'count': V;
|
||||
};
|
||||
'PollAnsweredCount': {
|
||||
'count': V;
|
||||
};
|
||||
'VoteCount': {
|
||||
'count': V;
|
||||
};
|
||||
'VoiceOverChatMessagesSelected': {
|
||||
'count': V;
|
||||
};
|
||||
|
||||
@ -114,6 +114,17 @@ export function orderBy<T>(
|
||||
});
|
||||
}
|
||||
|
||||
export function shuffle<T>(array: readonly T[]): T[] {
|
||||
const shuffled = [...array];
|
||||
|
||||
for (let i = shuffled.length - 1; i > 0; i -= 1) {
|
||||
const randomIndex = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]];
|
||||
}
|
||||
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
export function unique<T>(array: T[]): T[] {
|
||||
return Array.from(new Set(array));
|
||||
}
|
||||
|
||||
@ -153,6 +153,24 @@ export function formatClockDuration(duration: number) {
|
||||
return string;
|
||||
}
|
||||
|
||||
export function formatCountdownDateTime(
|
||||
lang: LangFn,
|
||||
targetDate: Date,
|
||||
options: Pick<FormatDateTimeOptions, 'anchorDate'> = {},
|
||||
) {
|
||||
const anchorDate = options.anchorDate || new Date();
|
||||
const diffInSeconds = Math.max(0, Math.trunc((targetDate.getTime() - anchorDate.getTime()) / 1000));
|
||||
|
||||
if (diffInSeconds < DAY_IN_SECONDS) {
|
||||
return lang('TimeIn', { time: formatClockDuration(diffInSeconds) });
|
||||
}
|
||||
|
||||
return formatDateTime(lang, targetDate, {
|
||||
relative: 'auto',
|
||||
anchorDate,
|
||||
});
|
||||
}
|
||||
|
||||
function buildAbsoluteFormatterOptions(lang: LangFn, options: FormatDateTimeOptions) {
|
||||
const dateStyle = options.date ?? false;
|
||||
const timeStyle = options.time ?? false;
|
||||
|
||||
@ -8,11 +8,12 @@ import buildClassName from '../buildClassName';
|
||||
import Icon from '../../components/common/icons/Icon';
|
||||
import StarIcon from '../../components/common/icons/StarIcon';
|
||||
|
||||
export function getNextArrowReplacement() {
|
||||
return {
|
||||
'>': <Icon name="next-link" className="next-arrow-icon" />,
|
||||
};
|
||||
}
|
||||
export const NEXT_ARROW_REPLACEMENT = {
|
||||
'>': <Icon name="next-link" className="link-arrow-icon" />,
|
||||
};
|
||||
export const PREVIOUS_ARROW_REPLACEMENT = {
|
||||
'<': <Icon name="previous-link" className="link-arrow-icon" />,
|
||||
};
|
||||
|
||||
export function formatStarsAsText(lang: LangFn, amount: number) {
|
||||
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
"jsxImportSource": "@teact",
|
||||
"paths": {
|
||||
"@teact": ["./src/lib/teact/teact.ts"],
|
||||
"@teact/*": ["./src/lib/teact/*"]
|
||||
"@teact/*": ["./src/lib/teact/*"],
|
||||
"@gili/*": ["./src/components/gili/*"]
|
||||
},
|
||||
"types": [
|
||||
"dom-chromium-ai",
|
||||
|
||||
@ -183,6 +183,7 @@ export default function createConfig(
|
||||
alias: {
|
||||
'@teact$': path.resolve(__dirname, './src/lib/teact/teact.ts'),
|
||||
'@teact': path.resolve(__dirname, './src/lib/teact'),
|
||||
'@gili': path.resolve(__dirname, './src/components/gili'),
|
||||
},
|
||||
fallback: {
|
||||
path: require.resolve('path-browserify'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user