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:**
|
- **Code Style:**
|
||||||
- Early returns.
|
- Early returns.
|
||||||
- Prefix boolean variables with primary or modal auxiliaries (e.q. `isOpen`, `willUpdate`, `shouldRender`).
|
- Prefix boolean variables with primary or modal auxiliaries (e.g. `isOpen`, `willUpdate`, `shouldRender`).
|
||||||
- Functions should start with a verb (e.q. `openModal`, `closeDialog`, `handleClick`).
|
- 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.
|
- 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.
|
- Only leave comments for complex logic.
|
||||||
- Do not use `null`. There's linter rule to enforce it.
|
- Do not use `null`. There's linter rule to enforce it.
|
||||||
@ -160,7 +160,7 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
|
|||||||
### 1. Basics & Imports
|
### 1. Basics & Imports
|
||||||
|
|
||||||
* All components use JSX and render with Teact.
|
* 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.
|
* Built-in hooks live in Teact library. Import them from there.
|
||||||
|
|
||||||
### 2. Props & Types
|
### 2. Props & Types
|
||||||
@ -171,6 +171,7 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
|
|||||||
* Merge them as `OwnProps & StateProps` when defining your component.
|
* Merge them as `OwnProps & StateProps` when defining your component.
|
||||||
* You can skip one or both if they are not used.
|
* You can skip one or both if they are not used.
|
||||||
* **Order rule**: list any function types *last* in your props definitions.
|
* **Order rule**: list any function types *last* in your props definitions.
|
||||||
|
* Do not pass unmemoized objects as props into memo() components.
|
||||||
|
|
||||||
### 3. Hooks
|
### 3. Hooks
|
||||||
* **useLastCallback** is your go-to for callbacks, since it won't trigger re-renders and always uses the latest scope.
|
* **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 { int2hex } from '../../../util/colors';
|
||||||
import { toJSNumber } from '../../../util/numbers';
|
import { toJSNumber } from '../../../util/numbers';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import { addDocumentToLocalDb } from '../helpers/localDb';
|
|
||||||
import { buildApiFormattedText } from './common';
|
import { buildApiFormattedText } from './common';
|
||||||
import { buildApiCurrencyAmount } from './payments';
|
import { buildApiCurrencyAmount } from './payments';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||||
@ -73,8 +72,6 @@ export function buildApiStarGift(starGift: GramJs.TypeStarGift): ApiStarGift {
|
|||||||
background,
|
background,
|
||||||
} = starGift;
|
} = starGift;
|
||||||
|
|
||||||
addDocumentToLocalDb(starGift.sticker);
|
|
||||||
|
|
||||||
const sticker = buildStickerFromDocument(starGift.sticker)!;
|
const sticker = buildStickerFromDocument(starGift.sticker)!;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -138,8 +135,6 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
addDocumentToLocalDb(attribute.document);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'model',
|
type: 'model',
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
@ -154,8 +149,6 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
addDocumentToLocalDb(attribute.document);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'pattern',
|
type: 'pattern',
|
||||||
name: attribute.name,
|
name: attribute.name,
|
||||||
@ -349,10 +342,6 @@ export function buildApiStarGiftCollection(collection: GramJs.StarGiftCollection
|
|||||||
|
|
||||||
const { collectionId, title, icon, giftsCount, hash } = collection;
|
const { collectionId, title, icon, giftsCount, hash } = collection;
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
addDocumentToLocalDb(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
collectionId,
|
collectionId,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { toJSNumber } from '../../../util/numbers';
|
|||||||
import { buildApiBotApp } from './bots';
|
import { buildApiBotApp } from './bots';
|
||||||
import { buildApiFormattedText, buildApiPhoto } from './common';
|
import { buildApiFormattedText, buildApiPhoto } from './common';
|
||||||
import { buildApiStarGift } from './gifts';
|
import { buildApiStarGift } from './gifts';
|
||||||
import { buildTodoItem } from './messageContent';
|
import { buildPollAnswer, buildTodoItem } from './messageContent';
|
||||||
import { buildApiCurrencyAmount } from './payments';
|
import { buildApiCurrencyAmount } from './payments';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||||
|
|
||||||
@ -525,6 +525,26 @@ export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMess
|
|||||||
items: list.map(buildTodoItem),
|
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) {
|
if (action instanceof GramJs.MessageActionStarGiftPurchaseOffer) {
|
||||||
const {
|
const {
|
||||||
accepted, declined, gift, price, expiresAt,
|
accepted, declined, gift, price, expiresAt,
|
||||||
|
|||||||
@ -13,11 +13,14 @@ import type {
|
|||||||
ApiMediaExtendedPreview,
|
ApiMediaExtendedPreview,
|
||||||
ApiMediaInvoice,
|
ApiMediaInvoice,
|
||||||
ApiMediaTodo,
|
ApiMediaTodo,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiMessageStoryData,
|
ApiMessageStoryData,
|
||||||
ApiMessageWebPage,
|
ApiMessageWebPage,
|
||||||
ApiPaidMedia,
|
ApiPaidMedia,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
ApiPoll,
|
ApiPoll,
|
||||||
|
ApiPollAnswer,
|
||||||
|
ApiPollResults,
|
||||||
ApiStarGiftUnique,
|
ApiStarGiftUnique,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiTodoItem,
|
ApiTodoItem,
|
||||||
@ -43,7 +46,7 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
||||||
import { generateWaveform } from '../../../util/generateWaveform';
|
import { generateWaveform } from '../../../util/generateWaveform';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { buildCollectionByKey, pick } from '../../../util/iteratees';
|
||||||
import { toJSNumber } from '../../../util/numbers';
|
import { toJSNumber } from '../../../util/numbers';
|
||||||
import {
|
import {
|
||||||
addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext,
|
addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext,
|
||||||
@ -77,7 +80,7 @@ export function buildMessageContent(
|
|||||||
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
||||||
|
|
||||||
if (mtpMessage.message && !hasUnsupportedMedia
|
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 text = buildMessageTextContent(mtpMessage.message, mtpMessage.entities);
|
||||||
const textWithTimestamps = addTimestampEntities(text);
|
const textWithTimestamps = addTimestampEntities(text);
|
||||||
content = {
|
content = {
|
||||||
@ -559,12 +562,12 @@ function buildPollIdFromMedia(media: GramJs.TypeMessageMedia): string | undefine
|
|||||||
return media.poll.id.toString();
|
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)) {
|
if (!(media instanceof GramJs.MessageMediaPoll)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildPoll(media.poll, media.results);
|
return buildMessagePoll(media);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTodoFromMedia(media: GramJs.TypeMessageMedia): ApiMediaTodo | undefined {
|
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 {
|
export function buildMessagePoll(media: GramJs.MessageMediaPoll): ApiMessagePoll {
|
||||||
const { id, answers: rawAnswers } = poll;
|
const { poll, results, attachedMedia } = media;
|
||||||
const answers = rawAnswers
|
|
||||||
.filter((answer): answer is GramJs.PollAnswer => answer instanceof GramJs.PollAnswer)
|
|
||||||
.map((answer) => ({
|
|
||||||
text: buildApiFormattedText(answer.text),
|
|
||||||
option: serializeBytes(answer.option),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mediaType: 'poll',
|
mediaType: 'poll',
|
||||||
id: String(id),
|
summary: buildPoll(poll),
|
||||||
summary: {
|
results: buildPollResults(results),
|
||||||
isPublic: poll.publicVoters,
|
attachedMedia: attachedMedia ? buildMessageMediaContent(attachedMedia) : undefined,
|
||||||
question: buildApiFormattedText(poll.question),
|
};
|
||||||
...pick(poll, [
|
}
|
||||||
'closed',
|
|
||||||
'multipleChoice',
|
export function buildPollAnswer(answer: GramJs.TypePollAnswer): ApiPollAnswer | undefined {
|
||||||
'quiz',
|
if (!(answer instanceof GramJs.PollAnswer)) return undefined;
|
||||||
'closePeriod',
|
const { text, option, media, addedBy, date } = answer;
|
||||||
'closeDate',
|
|
||||||
]),
|
return {
|
||||||
answers,
|
text: buildApiFormattedText(text),
|
||||||
},
|
option: serializeBytes(option),
|
||||||
results: buildPollResults(pollResults),
|
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 {
|
const {
|
||||||
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min,
|
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min, solutionMedia,
|
||||||
} = pollResults;
|
} = pollResults;
|
||||||
const results = rawResults?.map(({
|
const results = rawResults?.map(({
|
||||||
option, chosen, correct, voters,
|
option, chosen, correct, voters, recentVoters: recentAnswerVoters,
|
||||||
}) => ({
|
}) => ({
|
||||||
isChosen: chosen,
|
isChosen: chosen,
|
||||||
isCorrect: correct,
|
isCorrect: correct,
|
||||||
option: serializeBytes(option),
|
option: serializeBytes(option),
|
||||||
votersCount: voters ?? 0,
|
votersCount: voters ?? 0,
|
||||||
|
recentVoterIds: recentAnswerVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (solutionMedia) {
|
||||||
|
addMediaToLocalDb(solutionMedia);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMin: min,
|
isMin: min,
|
||||||
totalVoters,
|
totalVoters,
|
||||||
recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
|
recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
|
||||||
results,
|
resultByOption: results && buildCollectionByKey(results, 'option'),
|
||||||
solution,
|
solution,
|
||||||
...(entities && { solutionEntities: entities.map(buildApiMessageEntity) }),
|
solutionEntities: entities?.map(buildApiMessageEntity),
|
||||||
|
solutionMedia: solutionMedia ? buildMessageMediaContent(solutionMedia) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,13 +16,14 @@ import type {
|
|||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
ApiMessageForwardInfo,
|
ApiMessageForwardInfo,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiMessageReportResult,
|
ApiMessageReportResult,
|
||||||
ApiMessageThreadInfo,
|
ApiMessageThreadInfo,
|
||||||
ApiNewMediaTodo,
|
ApiNewMediaTodo,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
ApiPeer,
|
ApiPeer,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
ApiPoll,
|
ApiPollResult,
|
||||||
ApiPreparedInlineMessage,
|
ApiPreparedInlineMessage,
|
||||||
ApiQuickReply,
|
ApiQuickReply,
|
||||||
ApiReplyInfo,
|
ApiReplyInfo,
|
||||||
@ -49,7 +50,7 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
||||||
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
import { addTimestampEntities } from '../../../util/dates/timestamp';
|
||||||
import { omitUndefined, pick } from '../../../util/iteratees';
|
import { omitUndefined } from '../../../util/iteratees';
|
||||||
import { toJSNumber } from '../../../util/numbers';
|
import { toJSNumber } from '../../../util/numbers';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { interpolateArray } from '../../../util/waveform';
|
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 {
|
return {
|
||||||
mediaType: 'poll',
|
mediaType: 'poll',
|
||||||
id: String(localId),
|
summary: poll.summary,
|
||||||
summary: pick(poll.summary, ['question', 'answers']),
|
results: {
|
||||||
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 {
|
return {
|
||||||
mediaType: 'todo',
|
mediaType: 'todo',
|
||||||
todo: todo.todo,
|
todo: todo.todo,
|
||||||
@ -487,8 +507,8 @@ export function buildLocalMessage({
|
|||||||
|
|
||||||
const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum);
|
const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum);
|
||||||
|
|
||||||
const localPoll = poll && buildNewPoll(poll, localId);
|
const localPoll = poll && buildNewLocalPoll(poll);
|
||||||
const localTodo = todo && buildNewTodo(todo);
|
const localTodo = todo && buildNewLocalTodo(todo);
|
||||||
|
|
||||||
const localDice = dice ? {
|
const localDice = dice ? {
|
||||||
mediaType: 'dice',
|
mediaType: 'dice',
|
||||||
@ -510,7 +530,7 @@ export function buildLocalMessage({
|
|||||||
video: gif || media?.video,
|
video: gif || media?.video,
|
||||||
contact,
|
contact,
|
||||||
storyData: story && { mediaType: 'storyData', ...story },
|
storyData: story && { mediaType: 'storyData', ...story },
|
||||||
pollId: localPoll?.id,
|
pollId: localPoll?.summary.id,
|
||||||
todo: localTodo,
|
todo: localTodo,
|
||||||
dice: localDice,
|
dice: localDice,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type {
|
|||||||
|
|
||||||
import { LOTTIE_STICKER_MIME_TYPE, VIDEO_STICKER_MIME_TYPE } from '../../../config';
|
import { LOTTIE_STICKER_MIME_TYPE, VIDEO_STICKER_MIME_TYPE } from '../../../config';
|
||||||
import { compact } from '../../../util/iteratees';
|
import { compact } from '../../../util/iteratees';
|
||||||
|
import { addDocumentToLocalDb } from '../helpers/localDb';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiPhotoPreviewSizes, buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
import { buildApiPhotoPreviewSizes, buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
||||||
|
|
||||||
@ -15,6 +16,8 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument,
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addDocumentToLocalDb(document);
|
||||||
|
|
||||||
const { mimeType, videoThumbs } = document;
|
const { mimeType, videoThumbs } = document;
|
||||||
const stickerAttribute = document.attributes
|
const stickerAttribute = document.attributes
|
||||||
.find((attr: any): attr is GramJs.DocumentAttributeSticker => (
|
.find((attr: any): attr is GramJs.DocumentAttributeSticker => (
|
||||||
@ -202,12 +205,7 @@ export function processStickerResult(stickers: GramJs.TypeDocument[]) {
|
|||||||
return stickers
|
return stickers
|
||||||
.map((document) => {
|
.map((document) => {
|
||||||
if (document instanceof GramJs.Document) {
|
if (document instanceof GramJs.Document) {
|
||||||
const sticker = buildStickerFromDocument(document);
|
return buildStickerFromDocument(document);
|
||||||
if (sticker) {
|
|
||||||
localDb.documents[String(document.id)] = document;
|
|
||||||
|
|
||||||
return sticker;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
ApiChatFolder,
|
ApiChatFolder,
|
||||||
ApiChatReactions,
|
ApiChatReactions,
|
||||||
ApiDisallowedGiftsSettings,
|
ApiDisallowedGiftsSettings,
|
||||||
|
ApiDocument,
|
||||||
ApiEmojiStatusType,
|
ApiEmojiStatusType,
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
@ -15,12 +16,13 @@ import type {
|
|||||||
ApiInputReplyInfo,
|
ApiInputReplyInfo,
|
||||||
ApiInputStorePaymentPurpose,
|
ApiInputStorePaymentPurpose,
|
||||||
ApiInputSuggestedPostInfo,
|
ApiInputSuggestedPostInfo,
|
||||||
|
ApiLocation,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiNewMediaTodo,
|
ApiNewMediaTodo,
|
||||||
ApiNewPoll,
|
ApiNewPoll,
|
||||||
ApiPhoneCall,
|
ApiPhoneCall,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
ApiPoll,
|
|
||||||
ApiPremiumGiftCodeOption,
|
ApiPremiumGiftCodeOption,
|
||||||
ApiPrivacyKey,
|
ApiPrivacyKey,
|
||||||
ApiProfileTab,
|
ApiProfileTab,
|
||||||
@ -35,6 +37,7 @@ import type {
|
|||||||
ApiThemeParameters,
|
ApiThemeParameters,
|
||||||
ApiTypeCurrencyAmount,
|
ApiTypeCurrencyAmount,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
|
MediaContent,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
ApiMessageEntityTypes,
|
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];
|
const document = localDb.documents[media.id];
|
||||||
|
|
||||||
if (!document) {
|
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);
|
const inputDocument = buildInputDocument(media);
|
||||||
|
|
||||||
if (!inputDocument) {
|
if (!inputDocument) {
|
||||||
@ -207,48 +214,47 @@ export function buildInputMediaDocument(media: ApiSticker | ApiVideo, spoiler?:
|
|||||||
return new GramJs.InputMediaDocument({ id: inputDocument, spoiler });
|
return new GramJs.InputMediaDocument({ id: inputDocument, spoiler });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildInputPoll(pollParams: ApiNewPoll, randomId: bigint) {
|
export function buildInputPoll(
|
||||||
const { summary, quiz } = pollParams;
|
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,
|
id: randomId,
|
||||||
publicVoters: summary.isPublic,
|
publicVoters: poll.isPublic,
|
||||||
question: buildInputTextWithEntities(summary.question),
|
question: buildInputTextWithEntities(poll.question),
|
||||||
answers: summary.answers.map(({ text, option }) => {
|
answers: poll.answers.map(({ text, option }) => {
|
||||||
return new GramJs.PollAnswer({
|
return new GramJs.PollAnswer({
|
||||||
text: buildInputTextWithEntities(text),
|
text: buildInputTextWithEntities(text),
|
||||||
option: deserializeBytes(option),
|
option: deserializeBytes(option),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
quiz: summary.quiz,
|
quiz: poll.isQuiz,
|
||||||
multipleChoice: summary.multipleChoice,
|
multipleChoice: poll.isMultipleChoice,
|
||||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
hash: DEFAULT_PRIMITIVES.BIGINT,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!quiz) {
|
const inputSolutionEntities = solutionEntities?.map(buildMtpMessageEntity);
|
||||||
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) : [];
|
|
||||||
|
|
||||||
return new GramJs.InputMediaPoll({
|
return new GramJs.InputMediaPoll({
|
||||||
poll,
|
poll: inputPoll,
|
||||||
correctAnswers,
|
correctAnswers,
|
||||||
...(solution && {
|
attachedMedia: media?.attachedMedia,
|
||||||
solution,
|
solution,
|
||||||
solutionEntities,
|
solutionEntities: inputSolutionEntities,
|
||||||
}),
|
solutionMedia: media?.solutionMedia,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
|
export function buildInputPollFromExisting(poll: ApiMessagePoll, shouldClose = false) {
|
||||||
return new GramJs.InputMediaPoll({
|
return new GramJs.InputMediaPoll({
|
||||||
poll: new GramJs.Poll({
|
poll: new GramJs.Poll({
|
||||||
id: BigInt(poll.id),
|
id: BigInt(poll.summary.id),
|
||||||
publicVoters: poll.summary.isPublic,
|
publicVoters: poll.summary.isPublic,
|
||||||
question: buildInputTextWithEntities(poll.summary.question),
|
question: buildInputTextWithEntities(poll.summary.question),
|
||||||
answers: poll.summary.answers.map(({ text, option }) => {
|
answers: poll.summary.answers.map(({ text, option }) => {
|
||||||
@ -257,18 +263,95 @@ export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
|
|||||||
option: deserializeBytes(option),
|
option: deserializeBytes(option),
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
quiz: poll.summary.quiz,
|
quiz: poll.summary.isQuiz,
|
||||||
multipleChoice: poll.summary.multipleChoice,
|
multipleChoice: poll.summary.isMultipleChoice,
|
||||||
closeDate: poll.summary.closeDate,
|
closeDate: poll.summary.closeDate,
|
||||||
closePeriod: poll.summary.closePeriod,
|
closePeriod: poll.summary.closePeriod,
|
||||||
closed: shouldClose ? true : poll.summary.closed,
|
closed: shouldClose ? true : poll.summary.isClosed,
|
||||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
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
|
correctAnswers: poll.summary.answers.map((answer, index) => {
|
||||||
?.map((result, index) => (result.isCorrect ? index : -1))
|
const result = poll.results.resultByOption?.[answer.option];
|
||||||
.filter((i) => i !== -1),
|
return result?.isCorrect ? index : -1;
|
||||||
|
}).filter((i) => i !== -1),
|
||||||
|
attachedMedia: buildInputMediaFromContent(poll.attachedMedia),
|
||||||
solution: poll.results.solution,
|
solution: poll.results.solution,
|
||||||
solutionEntities: poll.results.solutionEntities?.map(buildMtpMessageEntity),
|
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) {
|
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) {
|
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,
|
ApiInputSuggestedPostInfo,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiMessageSearchContext,
|
ApiMessageSearchContext,
|
||||||
ApiMessageSearchType,
|
ApiMessageSearchType,
|
||||||
ApiNewMediaTodo,
|
ApiNewMediaTodo,
|
||||||
ApiOnProgress,
|
ApiOnProgress,
|
||||||
ApiPeer,
|
ApiPeer,
|
||||||
ApiPoll,
|
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiSearchPostsFlood,
|
ApiSearchPostsFlood,
|
||||||
ApiSendMessageAction,
|
ApiSendMessageAction,
|
||||||
@ -64,7 +64,7 @@ import {
|
|||||||
import { buildApiComposedMessageWithAI, buildApiFormattedText } from '../apiBuilders/common';
|
import { buildApiComposedMessageWithAI, buildApiFormattedText } from '../apiBuilders/common';
|
||||||
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
||||||
import {
|
import {
|
||||||
buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia,
|
buildMessageMediaContent, buildMessagePollFromMedia, buildMessageTextContent,
|
||||||
buildWebPageFromMedia,
|
buildWebPageFromMedia,
|
||||||
} from '../apiBuilders/messageContent';
|
} from '../apiBuilders/messageContent';
|
||||||
import {
|
import {
|
||||||
@ -461,7 +461,28 @@ export function sendApiMessage(
|
|||||||
} else if (gif) {
|
} else if (gif) {
|
||||||
media = buildInputMediaDocument(gif);
|
media = buildInputMediaDocument(gif);
|
||||||
} else if (poll) {
|
} 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) {
|
} else if (todo) {
|
||||||
media = buildInputTodo(todo);
|
media = buildInputTodo(todo);
|
||||||
} else if (story) {
|
} else if (story) {
|
||||||
@ -1856,7 +1877,7 @@ export async function closePoll({
|
|||||||
}: {
|
}: {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
poll: ApiPoll;
|
poll: ApiMessagePoll;
|
||||||
}) {
|
}) {
|
||||||
const { id, accessHash } = chat;
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
@ -2498,7 +2519,7 @@ function handleLocalMessageUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newContent: MediaContent | undefined;
|
let newContent: MediaContent | undefined;
|
||||||
let poll: ApiPoll | undefined;
|
let poll: ApiMessagePoll | undefined;
|
||||||
let webPage: ApiWebPage | undefined;
|
let webPage: ApiWebPage | undefined;
|
||||||
if (messageUpdate instanceof GramJs.UpdateShortSentMessage) {
|
if (messageUpdate instanceof GramJs.UpdateShortSentMessage) {
|
||||||
if (localMessage.content.text && messageUpdate.entities) {
|
if (localMessage.content.text && messageUpdate.entities) {
|
||||||
@ -2513,7 +2534,7 @@ function handleLocalMessageUpdate(
|
|||||||
peerId: buildPeer(localMessage.chatId), id: messageUpdate.id,
|
peerId: buildPeer(localMessage.chatId), id: messageUpdate.id,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
poll = buildPollFromMedia(messageUpdate.media);
|
poll = buildMessagePollFromMedia(messageUpdate.media);
|
||||||
webPage = buildWebPageFromMedia(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]));
|
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 effects = result.effects.map(buildApiAvailableEffect);
|
||||||
|
|
||||||
const stickers: ApiSticker[] = [];
|
const stickers: ApiSticker[] = [];
|
||||||
@ -127,12 +121,12 @@ export async function fetchAvailableEffects() {
|
|||||||
|
|
||||||
for (const effect of effects) {
|
for (const effect of effects) {
|
||||||
if (effect.effectAnimationId) {
|
if (effect.effectAnimationId) {
|
||||||
const document = documentsMap.get(effect.effectStickerId);
|
const document = documentsMap.get(effect.effectAnimationId);
|
||||||
const emoji = document && buildStickerFromDocument(document, false, effect.isPremium);
|
const emoji = document && buildStickerFromDocument(document, false, effect.isPremium);
|
||||||
if (emoji) emojis.push(emoji);
|
if (emoji) emojis.push(emoji);
|
||||||
} else {
|
} else {
|
||||||
const document = localDb.documents[effect.effectStickerId];
|
const document = documentsMap.get(effect.effectStickerId);
|
||||||
const sticker = buildStickerFromDocument(document);
|
const sticker = document && buildStickerFromDocument(document);
|
||||||
if (sticker) {
|
if (sticker) {
|
||||||
stickers.push(sticker);
|
stickers.push(sticker);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiPoll, ApiThreadInfo, ApiUser,
|
ApiChat, ApiMessagePoll, ApiThreadInfo, ApiUser,
|
||||||
ApiWebPage,
|
ApiWebPage,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import { buildPollFromMedia, buildWebPageFromMedia } from '../apiBuilders/messageContent';
|
import { buildMessagePollFromMedia, buildWebPageFromMedia } from '../apiBuilders/messageContent';
|
||||||
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
|
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers/localDb';
|
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 userById: Record<string, ApiUser> | undefined;
|
||||||
let chatById: Record<string, ApiChat> | undefined;
|
let chatById: Record<string, ApiChat> | undefined;
|
||||||
const threadInfos: ApiThreadInfo[] | undefined = [];
|
const threadInfos: ApiThreadInfo[] | undefined = [];
|
||||||
const polls: ApiPoll[] | undefined = [];
|
const polls: ApiMessagePoll[] | undefined = [];
|
||||||
const webPages: ApiWebPage[] | undefined = [];
|
const webPages: ApiWebPage[] | undefined = [];
|
||||||
|
|
||||||
if ('users' in response && Array.isArray(response.users) && TYPE_USER.has(response.users[0]?.className)) {
|
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) {
|
if ('media' in message && message.media) {
|
||||||
const poll = buildPollFromMedia(message.media);
|
const poll = buildMessagePollFromMedia(message.media);
|
||||||
if (poll) {
|
if (poll) {
|
||||||
polls.push(poll);
|
polls.push(poll);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gram
|
|||||||
|
|
||||||
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
|
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
|
||||||
import {
|
import {
|
||||||
type ApiMessage, type ApiPoll, type ApiStory, type ApiStorySkipped,
|
type ApiMessage,
|
||||||
|
type ApiMessagePoll,
|
||||||
|
type ApiStory,
|
||||||
|
type ApiStorySkipped,
|
||||||
type ApiUpdateConnectionStateType,
|
type ApiUpdateConnectionStateType,
|
||||||
type ApiWebPage,
|
type ApiWebPage,
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
@ -11,7 +14,7 @@ import {
|
|||||||
|
|
||||||
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
|
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
|
||||||
import {
|
import {
|
||||||
omit, pick,
|
omit, omitUndefined, pick,
|
||||||
} from '../../../util/iteratees';
|
} from '../../../util/iteratees';
|
||||||
import { getServerTimeOffset, setServerTimeOffset } from '../../../util/serverTime';
|
import { getServerTimeOffset, setServerTimeOffset } from '../../../util/serverTime';
|
||||||
import { buildApiBotCommand, buildApiBotMenuButton } from '../apiBuilders/bots';
|
import { buildApiBotCommand, buildApiBotMenuButton } from '../apiBuilders/bots';
|
||||||
@ -38,8 +41,8 @@ import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
|||||||
import {
|
import {
|
||||||
buildApiMessageExtendedMediaPreview,
|
buildApiMessageExtendedMediaPreview,
|
||||||
buildBoughtMediaContent,
|
buildBoughtMediaContent,
|
||||||
|
buildMessagePollFromMedia,
|
||||||
buildPoll,
|
buildPoll,
|
||||||
buildPollFromMedia,
|
|
||||||
buildPollResults,
|
buildPollResults,
|
||||||
buildWebPage,
|
buildWebPage,
|
||||||
buildWebPageFromMedia,
|
buildWebPageFromMedia,
|
||||||
@ -134,7 +137,7 @@ export function updater(update: Update) {
|
|||||||
|| update instanceof GramJs.UpdateShortMessage
|
|| update instanceof GramJs.UpdateShortMessage
|
||||||
) {
|
) {
|
||||||
let message: ApiMessage | undefined;
|
let message: ApiMessage | undefined;
|
||||||
let poll: ApiPoll | undefined;
|
let poll: ApiMessagePoll | undefined;
|
||||||
let webPage: ApiWebPage | undefined;
|
let webPage: ApiWebPage | undefined;
|
||||||
let shouldForceReply: boolean | undefined;
|
let shouldForceReply: boolean | undefined;
|
||||||
|
|
||||||
@ -159,7 +162,7 @@ export function updater(update: Update) {
|
|||||||
message = buildApiMessage(mtpMessage)!;
|
message = buildApiMessage(mtpMessage)!;
|
||||||
|
|
||||||
if (mtpMessage instanceof GramJs.Message) {
|
if (mtpMessage instanceof GramJs.Message) {
|
||||||
poll = mtpMessage.media && buildPollFromMedia(mtpMessage.media);
|
poll = mtpMessage.media && buildMessagePollFromMedia(mtpMessage.media);
|
||||||
webPage = mtpMessage.media && buildWebPageFromMedia(mtpMessage.media);
|
webPage = mtpMessage.media && buildWebPageFromMedia(mtpMessage.media);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,7 +306,7 @@ export function updater(update: Update) {
|
|||||||
if (!message) return;
|
if (!message) return;
|
||||||
|
|
||||||
const poll = update.message instanceof GramJs.Message && update.message.media
|
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
|
const webPage = update.message instanceof GramJs.Message && update.message.media
|
||||||
? buildWebPageFromMedia(update.message.media) : undefined;
|
? buildWebPageFromMedia(update.message.media) : undefined;
|
||||||
|
|
||||||
@ -358,7 +361,7 @@ export function updater(update: Update) {
|
|||||||
const message = omit(buildApiMessage(mtpMessage)!, ['isOutgoing']) as ApiMessage;
|
const message = omit(buildApiMessage(mtpMessage)!, ['isOutgoing']) as ApiMessage;
|
||||||
|
|
||||||
const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
||||||
? buildPollFromMedia(mtpMessage.media) : undefined;
|
? buildMessagePollFromMedia(mtpMessage.media) : undefined;
|
||||||
|
|
||||||
const webPage = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
const webPage = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
||||||
? buildWebPageFromMedia(mtpMessage.media) : undefined;
|
? buildWebPageFromMedia(mtpMessage.media) : undefined;
|
||||||
@ -472,22 +475,14 @@ export function updater(update: Update) {
|
|||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdateMessagePoll) {
|
} else if (update instanceof GramJs.UpdateMessagePoll) {
|
||||||
const { pollId, poll, results } = update;
|
const { pollId, poll, results } = update;
|
||||||
if (poll) {
|
const apiPoll = poll && buildPoll(poll);
|
||||||
const apiPoll = buildPoll(poll, results);
|
const pollResults = buildPollResults(results);
|
||||||
|
|
||||||
sendApiUpdate({
|
sendApiUpdate({
|
||||||
'@type': 'updateMessagePoll',
|
'@type': 'updateMessagePoll',
|
||||||
pollId: String(pollId),
|
pollId: pollId.toString(),
|
||||||
pollUpdate: apiPoll,
|
pollUpdate: omitUndefined({ summary: apiPoll, results: pollResults }),
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
const pollResults = buildPollResults(results);
|
|
||||||
sendApiUpdate({
|
|
||||||
'@type': 'updateMessagePoll',
|
|
||||||
pollId: String(pollId),
|
|
||||||
pollUpdate: { results: pollResults },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (update instanceof GramJs.UpdateMessagePollVote) {
|
} else if (update instanceof GramJs.UpdateMessagePollVote) {
|
||||||
sendApiUpdate({
|
sendApiUpdate({
|
||||||
'@type': 'updateMessagePollVote',
|
'@type': 'updateMessagePollVote',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls';
|
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';
|
import type { ApiStarGiftRegular, ApiStarGiftUnique, ApiTypeCurrencyAmount } from './stars';
|
||||||
|
|
||||||
interface ActionMediaType {
|
interface ActionMediaType {
|
||||||
@ -330,6 +330,16 @@ export interface ApiMessageActionTodoAppendTasks extends ActionMediaType {
|
|||||||
items: ApiTodoItem[];
|
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 {
|
export interface ApiMessageActionStarGiftPurchaseOffer extends ActionMediaType {
|
||||||
type: 'starGiftPurchaseOffer';
|
type: 'starGiftPurchaseOffer';
|
||||||
isAccepted?: true;
|
isAccepted?: true;
|
||||||
@ -388,6 +398,7 @@ export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionCha
|
|||||||
| ApiMessageActionGiftTon | ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique
|
| ApiMessageActionGiftTon | ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique
|
||||||
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
|
| ApiMessageActionPaidMessagesRefunded | ApiMessageActionPaidMessagesPrice | ApiMessageActionSuggestedPostApproval
|
||||||
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
|
| ApiMessageActionSuggestedPostSuccess | ApiMessageActionSuggestedPostRefund | ApiMessageActionTodoCompletions
|
||||||
| ApiMessageActionTodoAppendTasks | ApiMessageActionStarGiftPurchaseOffer
|
| ApiMessageActionTodoAppendTasks | ApiMessageActionPollAppendAnswer | ApiMessageActionPollDeleteAnswer
|
||||||
|
| ApiMessageActionStarGiftPurchaseOffer
|
||||||
| ApiMessageActionStarGiftPurchaseOfferDeclined | ApiMessageActionNewCreatorPending
|
| ApiMessageActionStarGiftPurchaseOfferDeclined | ApiMessageActionNewCreatorPending
|
||||||
| ApiMessageActionChangeCreator | ApiMessageActionNoForwardsToggle | ApiMessageActionNoForwardsRequest;
|
| ApiMessageActionChangeCreator | ApiMessageActionNoForwardsToggle | ApiMessageActionNoForwardsRequest;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type {
|
|||||||
ApiWebDocument,
|
ApiWebDocument,
|
||||||
} from './bots';
|
} from './bots';
|
||||||
import type { ApiMessageAction } from './messageActions';
|
import type { ApiMessageAction } from './messageActions';
|
||||||
import type { ApiPeerNotifySettings, ApiRestrictionReason } from './misc';
|
import type { ApiAttachment, ApiPeerNotifySettings, ApiRestrictionReason } from './misc';
|
||||||
import type {
|
import type {
|
||||||
ApiLabeledPrice,
|
ApiLabeledPrice,
|
||||||
} from './payments';
|
} from './payments';
|
||||||
@ -186,6 +186,9 @@ export type ApiPaidMedia = {
|
|||||||
export interface ApiPollAnswer {
|
export interface ApiPollAnswer {
|
||||||
text: ApiFormattedText;
|
text: ApiFormattedText;
|
||||||
option: string;
|
option: string;
|
||||||
|
media?: MediaContent;
|
||||||
|
addedByPeerId?: string;
|
||||||
|
date?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiPollResult {
|
export interface ApiPollResult {
|
||||||
@ -193,29 +196,42 @@ export interface ApiPollResult {
|
|||||||
isCorrect?: true;
|
isCorrect?: true;
|
||||||
option: string;
|
option: string;
|
||||||
votersCount: number;
|
votersCount: number;
|
||||||
|
recentVoterIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiPoll {
|
export interface ApiPoll {
|
||||||
mediaType: 'poll';
|
|
||||||
id: string;
|
id: string;
|
||||||
summary: {
|
hash: string;
|
||||||
closed?: true;
|
isClosed?: true;
|
||||||
isPublic?: true;
|
isPublic?: true;
|
||||||
multipleChoice?: true;
|
isMultipleChoice?: true;
|
||||||
quiz?: true;
|
isQuiz?: true;
|
||||||
question: ApiFormattedText;
|
canAddAnswers?: true;
|
||||||
answers: ApiPollAnswer[];
|
isRevoteDisabled?: true;
|
||||||
closePeriod?: number;
|
shouldShuffleAnswers?: true;
|
||||||
closeDate?: number;
|
shouldHideResultsUntilClose?: true;
|
||||||
};
|
isCreator?: true;
|
||||||
results: {
|
question: ApiFormattedText;
|
||||||
isMin?: true;
|
answers: ApiPollAnswer[];
|
||||||
results?: ApiPollResult[];
|
closePeriod?: number;
|
||||||
totalVoters?: number;
|
closeDate?: number;
|
||||||
recentVoterIds?: string[];
|
}
|
||||||
solution?: string;
|
|
||||||
solutionEntities?: ApiMessageEntity[];
|
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 {
|
export interface ApiInvoice {
|
||||||
@ -339,12 +355,12 @@ export type ApiGiveawayResults = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ApiNewPoll = {
|
export type ApiNewPoll = {
|
||||||
summary: ApiPoll['summary'];
|
summary: ApiPoll;
|
||||||
quiz?: {
|
correctAnswers?: number[];
|
||||||
correctAnswers: string[];
|
solution?: string;
|
||||||
solution?: string;
|
solutionEntities?: ApiMessageEntity[];
|
||||||
solutionEntities?: ApiMessageEntity[];
|
attachedMedia?: ApiAttachment;
|
||||||
};
|
solutionMedia?: ApiAttachment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ApiTodoItem {
|
export interface ApiTodoItem {
|
||||||
@ -666,7 +682,7 @@ export type MediaContainer = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type StatefulMediaContent = {
|
export type StatefulMediaContent = {
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
story?: ApiStory;
|
story?: ApiStory;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,9 +24,9 @@ import type {
|
|||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
ApiMediaExtendedPreview,
|
ApiMediaExtendedPreview,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiPaidReactionPrivacyType,
|
ApiPaidReactionPrivacyType,
|
||||||
ApiPhoto,
|
ApiPhoto,
|
||||||
ApiPoll,
|
|
||||||
ApiQuickReply,
|
ApiQuickReply,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiReactions,
|
ApiReactions,
|
||||||
@ -237,7 +237,7 @@ export type ApiUpdateNewScheduledMessage = {
|
|||||||
id: number;
|
id: number;
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
wasDrafted?: boolean;
|
wasDrafted?: boolean;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ export type ApiUpdateNewMessage = {
|
|||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
shouldForceReply?: boolean;
|
shouldForceReply?: boolean;
|
||||||
wasDrafted?: boolean;
|
wasDrafted?: boolean;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -256,7 +256,7 @@ export type ApiUpdateMessage = {
|
|||||||
'@type': 'updateMessage';
|
'@type': 'updateMessage';
|
||||||
chatId: string;
|
chatId: string;
|
||||||
id: number;
|
id: number;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
shouldForceReply?: boolean;
|
shouldForceReply?: boolean;
|
||||||
isFromNew?: true;
|
isFromNew?: true;
|
||||||
@ -274,7 +274,7 @@ export type ApiUpdateScheduledMessage = {
|
|||||||
'@type': 'updateScheduledMessage';
|
'@type': 'updateScheduledMessage';
|
||||||
chatId: string;
|
chatId: string;
|
||||||
id: number;
|
id: number;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
isFromNew?: true;
|
isFromNew?: true;
|
||||||
} & (
|
} & (
|
||||||
@ -291,7 +291,7 @@ export type ApiUpdateQuickReplyMessage = {
|
|||||||
'@type': 'updateQuickReplyMessage';
|
'@type': 'updateQuickReplyMessage';
|
||||||
id: number;
|
id: number;
|
||||||
message: Partial<ApiMessage>;
|
message: Partial<ApiMessage>;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -335,7 +335,7 @@ export type ApiUpdateScheduledMessageSendSucceeded = {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
localId: number;
|
localId: number;
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -344,7 +344,7 @@ export type ApiUpdateMessageSendSucceeded = {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
localId: number;
|
localId: number;
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -385,7 +385,7 @@ export type ApiUpdateChannelMessages = {
|
|||||||
export type ApiUpdateMessagePoll = {
|
export type ApiUpdateMessagePoll = {
|
||||||
'@type': 'updateMessagePoll';
|
'@type': 'updateMessagePoll';
|
||||||
pollId: string;
|
pollId: string;
|
||||||
pollUpdate: Partial<ApiPoll>;
|
pollUpdate: Partial<ApiMessagePoll>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiUpdateMessagePollVote = {
|
export type ApiUpdateMessagePollVote = {
|
||||||
@ -895,7 +895,7 @@ export type ApiUpdateEntities = {
|
|||||||
users?: Record<string, ApiUser>;
|
users?: Record<string, ApiUser>;
|
||||||
chats?: Record<string, ApiChat>;
|
chats?: Record<string, ApiChat>;
|
||||||
threadInfos?: ApiThreadInfo[];
|
threadInfos?: ApiThreadInfo[];
|
||||||
polls?: ApiPoll[];
|
polls?: ApiMessagePoll[];
|
||||||
webPages?: ApiWebPage[];
|
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";
|
"CallAgain" = "Call Again";
|
||||||
"CallBack" = "Call Back";
|
"CallBack" = "Call Back";
|
||||||
"CallMessageWithDuration" = "{time} ({duration})";
|
"CallMessageWithDuration" = "{time} ({duration})";
|
||||||
"PollSubmitVotes" = "VOTE";
|
"PollSubmitVotes" = "Vote";
|
||||||
"PollViewResults" = "VIEW RESULTS";
|
"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";
|
"ChatQuizTotalVotesEmpty" = "No answers yet";
|
||||||
"ChatPollTotalVotesResultEmpty" = "No votes";
|
"ChatPollTotalVotesResultEmpty" = "No votes";
|
||||||
"Answer_one" = "{count} answer";
|
"Answer_one" = "{count} answer";
|
||||||
"Answer_other" = "{count} answers";
|
"Answer_other" = "{count} answers";
|
||||||
|
"PollAnsweredCount_one" = "{count} answered";
|
||||||
|
"PollAnsweredCount_other" = "{count} answered";
|
||||||
"Vote" = "Vote";
|
"Vote" = "Vote";
|
||||||
|
"VoteCount_one" = "{count} vote";
|
||||||
|
"VoteCount_other" = "{count} votes";
|
||||||
|
"TimeIn" = "in {time}";
|
||||||
"MessageRecommendedLabel" = "recommended";
|
"MessageRecommendedLabel" = "recommended";
|
||||||
"SponsoredMessageAd" = "Ad";
|
"SponsoredMessageAd" = "Ad";
|
||||||
"SponsoredMessageAdWhatIsThis" = "what's this?";
|
"SponsoredMessageAdWhatIsThis" = "what's this?";
|
||||||
@ -1726,6 +1740,7 @@
|
|||||||
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
|
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
|
||||||
"StarsPerMonth" = "⭐️{amount}/month";
|
"StarsPerMonth" = "⭐️{amount}/month";
|
||||||
"StarsBalance" = "Balance";
|
"StarsBalance" = "Balance";
|
||||||
|
"OpenMapWith" = "Open Map in";
|
||||||
"OpenApp" = "Open App";
|
"OpenApp" = "Open App";
|
||||||
"PopularApps" = "Popular Apps";
|
"PopularApps" = "Popular Apps";
|
||||||
"SearchApps" = "Search Apps";
|
"SearchApps" = "Search Apps";
|
||||||
@ -2376,6 +2391,10 @@
|
|||||||
"MessageActionAppendTodoYou" = "You added a new task \"{task}\" to {list}";
|
"MessageActionAppendTodoYou" = "You added a new task \"{task}\" to {list}";
|
||||||
"MessageActionAppendTodoMultiple" = "{peer} added {tasks} to {list}";
|
"MessageActionAppendTodoMultiple" = "{peer} added {tasks} to {list}";
|
||||||
"MessageActionAppendTodoMultipleYou" = "You 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";
|
"PremiumMore" = "More";
|
||||||
"SubscribeToTelegramPremiumForToggleTask" = "Subscribe to **Telegram Premium** to toggle tasks";
|
"SubscribeToTelegramPremiumForToggleTask" = "Subscribe to **Telegram Premium** to toggle tasks";
|
||||||
"SubscribeToTelegramPremiumForCreateToDo" = "Subscribe to **Telegram Premium** to create Checklists";
|
"SubscribeToTelegramPremiumForCreateToDo" = "Subscribe to **Telegram Premium** to create Checklists";
|
||||||
|
|||||||
@ -5,6 +5,7 @@ $animation-time: 200ms;
|
|||||||
|
|
||||||
.root {
|
.root {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[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 {
|
import {
|
||||||
memo, useEffect, useRef, useState,
|
memo, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions } from '../../global';
|
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 type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDocumentMediaHash,
|
getDocumentMediaHash,
|
||||||
getMediaFormat,
|
getMediaFormat,
|
||||||
getMediaThumbUri,
|
|
||||||
getMediaTransferState,
|
getMediaTransferState,
|
||||||
isDocumentVideo,
|
isDocumentVideo,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
@ -20,7 +19,6 @@ import { preloadDocumentMedia } from './helpers/preloadDocumentMedia';
|
|||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useMedia from '../../hooks/useMedia';
|
|
||||||
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
@ -117,9 +115,10 @@ const Document = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasPreview = getDocumentHasPreview(document);
|
const hasPreview = getDocumentHasPreview(document);
|
||||||
const thumbDataUri = hasPreview ? getMediaThumbUri(document) : undefined;
|
const previewMedia = useMemo<MediaContent | undefined>(
|
||||||
const localBlobUrl = hasPreview ? document.previewBlobUrl : undefined;
|
() => (hasPreview ? { document } : undefined),
|
||||||
const previewData = useMedia(getDocumentMediaHash(document, 'pictogram'), !isIntersecting);
|
[document, hasPreview],
|
||||||
|
);
|
||||||
|
|
||||||
const shouldForceDownload = document.innerMediaType === 'photo' && document.mediaSize
|
const shouldForceDownload = document.innerMediaType === 'photo' && document.mediaSize
|
||||||
&& !document.mediaSize.fromDocumentAttribute && !document.mediaSize.fromPreload;
|
&& !document.mediaSize.fromDocumentAttribute && !document.mediaSize.fromPreload;
|
||||||
@ -199,8 +198,8 @@ const Document = ({
|
|||||||
extension={extension}
|
extension={extension}
|
||||||
size={size}
|
size={size}
|
||||||
timestamp={datetime}
|
timestamp={datetime}
|
||||||
thumbnailDataUri={thumbDataUri}
|
previewMedia={previewMedia}
|
||||||
previewData={localBlobUrl || previewData}
|
observeIntersection={observeIntersection}
|
||||||
previewSize={fileSize}
|
previewSize={fileSize}
|
||||||
isTransferring={isTransferring}
|
isTransferring={isTransferring}
|
||||||
isUploading={isUploading}
|
isUploading={isUploading}
|
||||||
|
|||||||
@ -1,27 +1,26 @@
|
|||||||
import type { ElementRef } from '../../lib/teact/teact';
|
import type { ElementRef } from '../../lib/teact/teact';
|
||||||
import {
|
import {
|
||||||
memo, useRef, useState,
|
memo, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiAttachment, MediaContent } from '../../api/types';
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import type { IconName } from '../../types/icons';
|
import type { IconName } from '../../types/icons';
|
||||||
|
|
||||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/browser/windowEnvironment';
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/oldDateFormat';
|
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/oldDateFormat';
|
||||||
import { getColorFromExtension } from './helpers/documentInfo';
|
import { getColorFromExtension } from './helpers/documentInfo';
|
||||||
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
|
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
|
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
|
||||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||||
|
|
||||||
import Link from '../ui/Link';
|
import Link from '../ui/Link';
|
||||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||||
import AnimatedFileSize from './AnimatedFileSize';
|
import AnimatedFileSize from './AnimatedFileSize';
|
||||||
|
import CompactMediaPreview, { canRenderCompactMediaPreview } from './CompactMediaPreview';
|
||||||
import Icon from './icons/Icon';
|
import Icon from './icons/Icon';
|
||||||
|
|
||||||
import './File.scss';
|
import './File.scss';
|
||||||
@ -36,8 +35,9 @@ type OwnProps = {
|
|||||||
size: number;
|
size: number;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
sender?: string;
|
sender?: string;
|
||||||
thumbnailDataUri?: string;
|
previewMedia?: MediaContent;
|
||||||
previewData?: string;
|
previewAttachment?: ApiAttachment;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
className?: string;
|
className?: string;
|
||||||
previewSize?: FileSize;
|
previewSize?: FileSize;
|
||||||
isTransferring?: boolean;
|
isTransferring?: boolean;
|
||||||
@ -58,8 +58,8 @@ const File = ({
|
|||||||
extension = '',
|
extension = '',
|
||||||
timestamp,
|
timestamp,
|
||||||
sender,
|
sender,
|
||||||
thumbnailDataUri,
|
previewMedia,
|
||||||
previewData,
|
previewAttachment,
|
||||||
className,
|
className,
|
||||||
previewSize = 'medium',
|
previewSize = 'medium',
|
||||||
isTransferring,
|
isTransferring,
|
||||||
@ -68,6 +68,7 @@ const File = ({
|
|||||||
isSelected,
|
isSelected,
|
||||||
transferProgress,
|
transferProgress,
|
||||||
actionIcon,
|
actionIcon,
|
||||||
|
observeIntersection,
|
||||||
onClick,
|
onClick,
|
||||||
onDateClick,
|
onDateClick,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
@ -78,12 +79,6 @@ const File = ({
|
|||||||
elementRef = ref;
|
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 {
|
const {
|
||||||
shouldRender: shouldSpinnerRender,
|
shouldRender: shouldSpinnerRender,
|
||||||
transitionClassNames: spinnerClassNames,
|
transitionClassNames: spinnerClassNames,
|
||||||
@ -91,7 +86,8 @@ const File = ({
|
|||||||
|
|
||||||
const color = getColorFromExtension(extension);
|
const color = getColorFromExtension(extension);
|
||||||
|
|
||||||
const { width, height } = getDocumentThumbnailDimensions(previewSize);
|
const { width } = getDocumentThumbnailDimensions(previewSize);
|
||||||
|
const shouldRenderPreview = canRenderCompactMediaPreview(previewMedia, previewAttachment);
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'File',
|
'File',
|
||||||
@ -109,23 +105,14 @@ const File = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="file-icon-container" onClick={isUploading ? undefined : onClick}>
|
<div className="file-icon-container" onClick={isUploading ? undefined : onClick}>
|
||||||
{thumbnailDataUri || previewData ? (
|
{shouldRenderPreview ? (
|
||||||
<div className="file-preview media-inner">
|
<CompactMediaPreview
|
||||||
<img
|
className="file-preview media-inner"
|
||||||
src={previewData}
|
media={previewMedia}
|
||||||
className="full-media"
|
attachment={previewAttachment}
|
||||||
width={width}
|
size={width}
|
||||||
height={height}
|
observeIntersectionForLoading={observeIntersection}
|
||||||
draggable={false}
|
/>
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
{withThumb && (
|
|
||||||
<canvas
|
|
||||||
ref={thumbRef}
|
|
||||||
className={buildClassName('thumbnail', thumbClassNames)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={`file-icon ${color}`}>
|
<div className={`file-icon ${color}`}>
|
||||||
{extension.length <= 4 && (
|
{extension.length <= 4 && (
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { memo } from '../../lib/teact/teact';
|
|||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiFormattedText, ApiMessage, ApiPoll, ApiTypeStory,
|
ApiFormattedText, ApiMessage, ApiMessagePoll, ApiTypeStory,
|
||||||
ApiWebPage,
|
ApiWebPage,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
@ -42,7 +42,7 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
story?: ApiTypeStory;
|
story?: ApiTypeStory;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -238,58 +238,6 @@
|
|||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
border-radius: 0.25rem;
|
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 {
|
&.inside-input {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef } from '../../../lib/teact/teact';
|
import { useMemo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat,
|
ApiChat,
|
||||||
@ -23,21 +23,16 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import { formatScheduledDateTime } from '../../../util/dates/oldDateFormat';
|
import { formatScheduledDateTime } from '../../../util/dates/oldDateFormat';
|
||||||
import { isUserId } from '../../../util/entities/ids';
|
import { isUserId } from '../../../util/entities/ids';
|
||||||
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
|
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
|
||||||
import { getPictogramDimensions } from '../helpers/mediaDimensions';
|
|
||||||
import renderText from '../helpers/renderText';
|
import renderText from '../helpers/renderText';
|
||||||
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
|
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 useLang from '../../../hooks/useLang';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
|
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
|
||||||
|
|
||||||
import RippleEffect from '../../ui/RippleEffect';
|
import RippleEffect from '../../ui/RippleEffect';
|
||||||
|
import CompactMediaPreview, { canRenderCompactMediaPreview } from '../CompactMediaPreview';
|
||||||
import Icon from '../icons/Icon';
|
import Icon from '../icons/Icon';
|
||||||
import MediaSpoiler from '../MediaSpoiler';
|
|
||||||
import MessageSummary from '../MessageSummary';
|
import MessageSummary from '../MessageSummary';
|
||||||
import PeerColorWrapper from '../PeerColorWrapper';
|
import PeerColorWrapper from '../PeerColorWrapper';
|
||||||
|
|
||||||
@ -96,9 +91,6 @@ const EmbeddedMessage = ({
|
|||||||
onClick,
|
onClick,
|
||||||
onPictogramClick,
|
onPictogramClick,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const ref = useRef<HTMLDivElement>();
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
|
|
||||||
|
|
||||||
const containedMedia: MediaContainer | undefined = useMemo(() => {
|
const containedMedia: MediaContainer | undefined = useMemo(() => {
|
||||||
const media = (replyInfo?.type === 'message' && replyInfo.replyMedia) || message?.content;
|
const media = (replyInfo?.type === 'message' && replyInfo.replyMedia) || message?.content;
|
||||||
if (!media) {
|
if (!media) {
|
||||||
@ -109,13 +101,7 @@ const EmbeddedMessage = ({
|
|||||||
content: media,
|
content: media,
|
||||||
};
|
};
|
||||||
}, [message, replyInfo]);
|
}, [message, replyInfo]);
|
||||||
|
const hasPictogram = canRenderCompactMediaPreview(containedMedia?.content);
|
||||||
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 isRoundVideo = Boolean(containedMedia && getMessageRoundVideo(containedMedia));
|
const isRoundVideo = Boolean(containedMedia && getMessageRoundVideo(containedMedia));
|
||||||
const isSpoiler = Boolean(containedMedia && getMessageIsSpoiler(containedMedia)) || isMediaNsfw;
|
const isSpoiler = Boolean(containedMedia && getMessageIsSpoiler(containedMedia)) || isMediaNsfw;
|
||||||
@ -206,7 +192,7 @@ const EmbeddedMessage = ({
|
|||||||
return (
|
return (
|
||||||
<MessageSummary
|
<MessageSummary
|
||||||
message={message}
|
message={message}
|
||||||
noEmoji={Boolean(mediaThumbnail)}
|
noEmoji={hasPictogram}
|
||||||
forcedText={translatedText}
|
forcedText={translatedText}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
@ -301,7 +287,6 @@ const EmbeddedMessage = ({
|
|||||||
<PeerColorWrapper
|
<PeerColorWrapper
|
||||||
peer={sender}
|
peer={sender}
|
||||||
emojiIconClassName="EmbeddedMessage--background-icons"
|
emojiIconClassName="EmbeddedMessage--background-icons"
|
||||||
ref={ref}
|
|
||||||
shouldReset
|
shouldReset
|
||||||
isReply={Boolean(replyInfo)}
|
isReply={Boolean(replyInfo)}
|
||||||
noUserColors={noUserColors}
|
noUserColors={noUserColors}
|
||||||
@ -309,7 +294,7 @@ const EmbeddedMessage = ({
|
|||||||
'EmbeddedMessage',
|
'EmbeddedMessage',
|
||||||
className,
|
className,
|
||||||
isQuote && 'is-quote',
|
isQuote && 'is-quote',
|
||||||
mediaThumbnail && 'with-thumb',
|
hasPictogram && 'with-thumb',
|
||||||
'no-selection',
|
'no-selection',
|
||||||
composerForwardSenders && 'is-input-forward',
|
composerForwardSenders && 'is-input-forward',
|
||||||
suggestedPostInfo && 'is-suggested-post',
|
suggestedPostInfo && 'is-suggested-post',
|
||||||
@ -319,16 +304,20 @@ const EmbeddedMessage = ({
|
|||||||
>
|
>
|
||||||
<div className="hover-effect" />
|
<div className="hover-effect" />
|
||||||
<RippleEffect />
|
<RippleEffect />
|
||||||
{mediaThumbnail && renderPictogram({
|
{hasPictogram && (
|
||||||
thumbDataUri: mediaThumbnail,
|
<CompactMediaPreview
|
||||||
blobUrl: mediaBlobUrl,
|
media={containedMedia?.content}
|
||||||
isFullVideo: isVideoThumbnail,
|
className="embedded-thumb"
|
||||||
isRoundVideo,
|
isPictogram
|
||||||
isProtected,
|
isRound={isRoundVideo}
|
||||||
isSpoiler,
|
isProtected={isProtected}
|
||||||
pictogramActionIcon,
|
isSpoiler={isSpoiler}
|
||||||
onPictogramClick,
|
actionIcon={pictogramActionIcon}
|
||||||
})}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
onClick={onPictogramClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="message-text">
|
<div className="message-text">
|
||||||
<p className={buildClassName('embedded-text-wrapper', isQuote && 'multiline')}>
|
<p className={buildClassName('embedded-text-wrapper', isQuote && 'multiline')}>
|
||||||
{renderTextContent()}
|
{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;
|
export default EmbeddedMessage;
|
||||||
|
|||||||
@ -1,24 +1,18 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import { useRef } from '../../../lib/teact/teact';
|
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { ApiPeer, ApiTypeStory } from '../../../api/types';
|
import type { ApiPeer, ApiTypeStory } from '../../../api/types';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import {
|
|
||||||
getStoryMediaHash,
|
|
||||||
} from '../../../global/helpers';
|
|
||||||
import { getPeerTitle } from '../../../global/helpers/peers';
|
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { getPictogramDimensions } from '../helpers/mediaDimensions';
|
|
||||||
import renderText from '../helpers/renderText';
|
import renderText from '../helpers/renderText';
|
||||||
|
|
||||||
import { useFastClick } from '../../../hooks/useFastClick';
|
import { useFastClick } from '../../../hooks/useFastClick';
|
||||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import CompactMediaPreview, { canRenderCompactMediaPreview } from '../CompactMediaPreview';
|
||||||
import Icon from '../icons/Icon';
|
import Icon from '../icons/Icon';
|
||||||
import PeerColorWrapper from '../PeerColorWrapper';
|
import PeerColorWrapper from '../PeerColorWrapper';
|
||||||
|
|
||||||
@ -30,6 +24,7 @@ type OwnProps = {
|
|||||||
noUserColors?: boolean;
|
noUserColors?: boolean;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
onClick: NoneToVoidFunction;
|
onClick: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,22 +36,17 @@ const EmbeddedStory: FC<OwnProps> = ({
|
|||||||
noUserColors,
|
noUserColors,
|
||||||
isProtected,
|
isProtected,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification } = getActions();
|
const { showNotification } = getActions();
|
||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>();
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading);
|
|
||||||
const isFullStory = story && 'content' in story;
|
const isFullStory = story && 'content' in story;
|
||||||
const isExpiredStory = story && 'isDeleted' in story;
|
const isExpiredStory = story && 'isDeleted' in story;
|
||||||
const isVideoStory = isFullStory && Boolean(story.content.video);
|
|
||||||
const title = isFullStory ? 'Story' : (isExpiredStory ? 'ExpiredStory' : 'Loading');
|
const title = isFullStory ? 'Story' : (isExpiredStory ? 'ExpiredStory' : 'Loading');
|
||||||
|
|
||||||
const mediaBlobUrl = useMedia(isFullStory && getStoryMediaHash(story, 'pictogram'), !isIntersecting);
|
const hasPictogram = isFullStory && canRenderCompactMediaPreview(story.content);
|
||||||
const mediaThumbnail = isVideoStory ? story.content.video!.thumbnail?.dataUri : undefined;
|
|
||||||
const pictogramUrl = mediaBlobUrl || mediaThumbnail;
|
|
||||||
|
|
||||||
const senderTitle = sender ? getPeerTitle(lang, sender) : undefined;
|
const senderTitle = sender ? getPeerTitle(lang, sender) : undefined;
|
||||||
const handleFastClick = useLastCallback(() => {
|
const handleFastClick = useLastCallback(() => {
|
||||||
@ -73,18 +63,26 @@ const EmbeddedStory: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PeerColorWrapper
|
<PeerColorWrapper
|
||||||
ref={ref}
|
|
||||||
peerColor={sender?.color}
|
peerColor={sender?.color}
|
||||||
noUserColors={noUserColors}
|
noUserColors={noUserColors}
|
||||||
shouldReset
|
shouldReset
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'EmbeddedMessage',
|
'EmbeddedMessage',
|
||||||
pictogramUrl && 'with-thumb',
|
hasPictogram && 'with-thumb',
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseDown={handleMouseDown}
|
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">
|
<div className="message-text with-message-color">
|
||||||
<p className="embedded-text-wrapper">
|
<p className="embedded-text-wrapper">
|
||||||
{isExpiredStory && (
|
{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;
|
export default EmbeddedStory;
|
||||||
|
|||||||
@ -60,11 +60,11 @@ function getMaxMessageWidthRem(fromOwnMessage?: boolean, noAvatars?: boolean, is
|
|||||||
|
|
||||||
export function getAvailableWidth(
|
export function getAvailableWidth(
|
||||||
fromOwnMessage?: boolean,
|
fromOwnMessage?: boolean,
|
||||||
isWebPageMedia?: boolean,
|
isNestedMedia?: boolean,
|
||||||
noAvatars?: boolean,
|
noAvatars?: boolean,
|
||||||
isMobile?: boolean,
|
isMobile?: boolean,
|
||||||
) {
|
) {
|
||||||
const extraPaddingRem = isWebPageMedia ? 1.625 : 0;
|
const extraPaddingRem = isNestedMedia ? 2.125 : 0;
|
||||||
const availableWidthRem = getMaxMessageWidthRem(fromOwnMessage, noAvatars, isMobile) - extraPaddingRem;
|
const availableWidthRem = getMaxMessageWidthRem(fromOwnMessage, noAvatars, isMobile) - extraPaddingRem;
|
||||||
|
|
||||||
return availableWidthRem * REM;
|
return availableWidthRem * REM;
|
||||||
@ -85,7 +85,7 @@ export function calculateDimensionsForMessageMedia({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fromOwnMessage,
|
fromOwnMessage,
|
||||||
isWebPageMedia,
|
isNestedMedia,
|
||||||
isGif,
|
isGif,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
isMobile,
|
isMobile,
|
||||||
@ -94,13 +94,13 @@ export function calculateDimensionsForMessageMedia({
|
|||||||
height: number;
|
height: number;
|
||||||
fromOwnMessage?: boolean;
|
fromOwnMessage?: boolean;
|
||||||
asForwarded?: boolean;
|
asForwarded?: boolean;
|
||||||
isWebPageMedia?: boolean;
|
isNestedMedia?: boolean;
|
||||||
isGif?: boolean;
|
isGif?: boolean;
|
||||||
noAvatars?: boolean;
|
noAvatars?: boolean;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
}): ApiDimensions {
|
}): ApiDimensions {
|
||||||
const aspectRatio = height / width;
|
const aspectRatio = height / width;
|
||||||
const availableWidth = getAvailableWidth(fromOwnMessage, isWebPageMedia, noAvatars, isMobile);
|
const availableWidth = getAvailableWidth(fromOwnMessage, isNestedMedia, noAvatars, isMobile);
|
||||||
const availableHeight = getAvailableHeight(isGif, aspectRatio);
|
const availableHeight = getAvailableHeight(isGif, aspectRatio);
|
||||||
const mediaWidth = isGif ? Math.max(GIF_MIN_WIDTH, width) : width;
|
const mediaWidth = isGif ? Math.max(GIF_MIN_WIDTH, width) : width;
|
||||||
const mediaHeight = isGif ? height * (mediaWidth / width) : height;
|
const mediaHeight = isGif ? height * (mediaWidth / width) : height;
|
||||||
@ -125,7 +125,7 @@ export function calculateInlineImageDimensions(
|
|||||||
photo: ApiPhoto,
|
photo: ApiPhoto,
|
||||||
fromOwnMessage?: boolean,
|
fromOwnMessage?: boolean,
|
||||||
asForwarded?: boolean,
|
asForwarded?: boolean,
|
||||||
isWebPageMedia?: boolean,
|
isNestedMedia?: boolean,
|
||||||
noAvatars?: boolean,
|
noAvatars?: boolean,
|
||||||
isMobile?: boolean,
|
isMobile?: boolean,
|
||||||
) {
|
) {
|
||||||
@ -136,7 +136,7 @@ export function calculateInlineImageDimensions(
|
|||||||
height,
|
height,
|
||||||
fromOwnMessage,
|
fromOwnMessage,
|
||||||
asForwarded,
|
asForwarded,
|
||||||
isWebPageMedia,
|
isNestedMedia,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
isMobile,
|
isMobile,
|
||||||
});
|
});
|
||||||
@ -146,7 +146,7 @@ export function calculateVideoDimensions(
|
|||||||
video: ApiVideo,
|
video: ApiVideo,
|
||||||
fromOwnMessage?: boolean,
|
fromOwnMessage?: boolean,
|
||||||
asForwarded?: boolean,
|
asForwarded?: boolean,
|
||||||
isWebPageMedia?: boolean,
|
isNestedMedia?: boolean,
|
||||||
noAvatars?: boolean,
|
noAvatars?: boolean,
|
||||||
isMobile?: boolean,
|
isMobile?: boolean,
|
||||||
) {
|
) {
|
||||||
@ -157,7 +157,7 @@ export function calculateVideoDimensions(
|
|||||||
height,
|
height,
|
||||||
fromOwnMessage,
|
fromOwnMessage,
|
||||||
asForwarded,
|
asForwarded,
|
||||||
isWebPageMedia,
|
isNestedMedia,
|
||||||
isGif: video.isGif,
|
isGif: video.isGif,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
isMobile,
|
isMobile,
|
||||||
@ -168,7 +168,7 @@ export function calculateExtendedPreviewDimensions(
|
|||||||
preview: ApiMediaExtendedPreview,
|
preview: ApiMediaExtendedPreview,
|
||||||
fromOwnMessage?: boolean,
|
fromOwnMessage?: boolean,
|
||||||
asForwarded?: boolean,
|
asForwarded?: boolean,
|
||||||
isWebPageMedia?: boolean,
|
isNestedMedia?: boolean,
|
||||||
noAvatars?: boolean,
|
noAvatars?: boolean,
|
||||||
isMobile?: boolean,
|
isMobile?: boolean,
|
||||||
) {
|
) {
|
||||||
@ -179,19 +179,12 @@ export function calculateExtendedPreviewDimensions(
|
|||||||
height,
|
height,
|
||||||
fromOwnMessage,
|
fromOwnMessage,
|
||||||
asForwarded,
|
asForwarded,
|
||||||
isWebPageMedia,
|
isNestedMedia,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
isMobile,
|
isMobile,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPictogramDimensions(): ApiDimensions {
|
|
||||||
return {
|
|
||||||
width: 2 * REM,
|
|
||||||
height: 2 * REM,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDocumentThumbnailDimensions(
|
export function getDocumentThumbnailDimensions(
|
||||||
size: 'small' | 'medium' | 'large' = 'medium',
|
size: 'small' | 'medium' | 'large' = 'medium',
|
||||||
): ApiDimensions {
|
): ApiDimensions {
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import {
|
|||||||
FRAGMENT_PHONE_CODE, FRAGMENT_PHONE_LENGTH, MUTE_INDEFINITE_TIMESTAMP, UNMUTE_TIMESTAMP,
|
FRAGMENT_PHONE_CODE, FRAGMENT_PHONE_LENGTH, MUTE_INDEFINITE_TIMESTAMP, UNMUTE_TIMESTAMP,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import {
|
import {
|
||||||
buildStaticMapHash,
|
|
||||||
getChatLink,
|
getChatLink,
|
||||||
getHasAdminRight,
|
getHasAdminRight,
|
||||||
isChatAdmin,
|
isChatAdmin,
|
||||||
@ -56,15 +55,13 @@ import useCollapsibleLines from '../../../hooks/element/useCollapsibleLines';
|
|||||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
|
|
||||||
|
|
||||||
import Chat from '../../left/main/Chat';
|
import Chat from '../../left/main/Chat';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
|
||||||
import Switcher from '../../ui/Switcher';
|
import Switcher from '../../ui/Switcher';
|
||||||
|
import CompactMapPreview from '../CompactMapPreview';
|
||||||
import CustomEmoji from '../CustomEmoji';
|
import CustomEmoji from '../CustomEmoji';
|
||||||
import Icon from '../icons/Icon';
|
import Icon from '../icons/Icon';
|
||||||
import SafeLink from '../SafeLink';
|
import SafeLink from '../SafeLink';
|
||||||
@ -191,19 +188,18 @@ const ChatExtra = ({
|
|||||||
}, [peerId, chat, user]);
|
}, [peerId, chat, user]);
|
||||||
|
|
||||||
const { width, height, zoom } = DEFAULT_MAP_CONFIG;
|
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(() => {
|
const locationRightComponent = useMemo(() => {
|
||||||
if (!businessLocation?.geo) return undefined;
|
if (!businessLocation?.geo) return undefined;
|
||||||
if (locationBlobUrl) {
|
return (
|
||||||
return <img src={locationBlobUrl} alt="" className={styles.businessLocation} />;
|
<CompactMapPreview
|
||||||
}
|
className={styles.businessLocation}
|
||||||
|
geo={businessLocation.geo}
|
||||||
return <Skeleton className={styles.businessLocation} animation="wave" />;
|
width={width}
|
||||||
}, [businessLocation, locationBlobUrl]);
|
height={height}
|
||||||
|
zoom={zoom}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [businessLocation, width, height, zoom]);
|
||||||
|
|
||||||
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
|
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
|
||||||
const shouldRenderAllLinks = (chat && isChatChannel(chat)) || user?.isPremium;
|
const shouldRenderAllLinks = (chat && isChatChannel(chat)) || user?.isPremium;
|
||||||
|
|||||||
@ -2,34 +2,72 @@
|
|||||||
.root {
|
.root {
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
margin: 0;
|
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;
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: var(--color-background);
|
background-color: var(--ui-bg-color, transparent);
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
background-size: 0;
|
|
||||||
|
|
||||||
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 {
|
&:checked {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--ui-accent-color, var(--color-primary));
|
||||||
background-color: var(--color-primary);
|
background-color: var(--ui-accent-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;
|
&::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 {
|
&:indeterminate {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--ui-accent-color, var(--color-primary));
|
||||||
background-color: var(--color-primary);
|
background-color: var(--ui-accent-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;
|
&::before {
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
width: 0.625rem;
|
||||||
|
height: 0.125rem;
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: 9999px;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--ui-check-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@ -38,7 +76,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,13 +15,13 @@ type InputProps = React.DetailedHTMLProps<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
checked: boolean;
|
checked?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isRound?: boolean;
|
isRound?: boolean;
|
||||||
indeterminate?: boolean;
|
indeterminate?: boolean;
|
||||||
isInvalid?: boolean;
|
isInvalid?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
||||||
@ -50,7 +50,7 @@ const Checkbox = ({
|
|||||||
}, [indeterminate]);
|
}, [indeterminate]);
|
||||||
|
|
||||||
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange(e.currentTarget.checked);
|
onChange?.(e.currentTarget.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (interactive?.isLoading) return undefined;
|
if (interactive?.isLoading) return undefined;
|
||||||
|
|||||||
@ -7,18 +7,21 @@
|
|||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
margin: 0;
|
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%;
|
border-radius: 50%;
|
||||||
|
|
||||||
appearance: none;
|
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 {
|
&:checked {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--ui-accent-color, var(--color-primary));
|
||||||
background-color: var(--color-primary);
|
background-size: 0.5rem 0.5rem;
|
||||||
box-shadow: inset 0 0 0 0.1875rem var(--color-background);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
@ -27,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,10 @@ type InputProps = React.DetailedHTMLProps<
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
value: string;
|
value: string;
|
||||||
checked: boolean;
|
checked?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
||||||
@ -40,7 +40,7 @@ const Radio = ({
|
|||||||
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
||||||
|
|
||||||
const handleChange = useLastCallback(() => {
|
const handleChange = useLastCallback(() => {
|
||||||
onChange(value);
|
onChange?.(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (interactive?.isLoading) return undefined;
|
if (interactive?.isLoading) return undefined;
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
|
|
||||||
appearance: none;
|
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;
|
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||||
|
|
||||||
@ -25,21 +25,21 @@
|
|||||||
|
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 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%;
|
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;
|
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked {
|
&:checked {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--ui-accent-color, var(--color-primary));
|
||||||
background-color: var(--color-primary);
|
background-color: var(--ui-accent-color, var(--color-primary));
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
transform: translateX(0.75rem);
|
transform: translateX(0.75rem);
|
||||||
border-color: var(--color-primary);
|
border-color: var(--ui-accent-color, var(--color-primary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { getChatAvatarHash } from '../../../global/helpers';
|
|||||||
import { selectTabState, selectUser, selectUserFullInfo } from '../../../global/selectors';
|
import { selectTabState, selectUser, selectUserFullInfo } from '../../../global/selectors';
|
||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
import { formatDateToString } from '../../../util/dates/oldDateFormat';
|
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 { throttle } from '../../../util/schedulers';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
@ -284,7 +284,7 @@ const SettingsEditProfile = ({
|
|||||||
link: (
|
link: (
|
||||||
<Link isPrimary onClick={handleBirthdayPrivacyClick}>
|
<Link isPrimary onClick={handleBirthdayPrivacyClick}>
|
||||||
{lang('BirthdayPrivacySuggestionLink',
|
{lang('BirthdayPrivacySuggestionLink',
|
||||||
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}, { withNodes: true })}
|
}, { withNodes: true })}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { IS_WEBAUTHN_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
|||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { type LangFn } from '../../../util/localization';
|
import { type LangFn } from '../../../util/localization';
|
||||||
import { formatDateTime, getCalendarDayDiff, secondsToDate } from '../../../util/localization/dateFormat';
|
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 { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
import { REM } from '../../common/helpers/mediaDimensions';
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ const SettingsPasskeys = ({
|
|||||||
link: (
|
link: (
|
||||||
<Link isPrimary onClick={handleOpenPasskeyModal}>
|
<Link isPrimary onClick={handleOpenPasskeyModal}>
|
||||||
{lang('SettingsPasskeysFooterLink', undefined,
|
{lang('SettingsPasskeysFooterLink', undefined,
|
||||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}, { withNodes: true })}
|
}, { 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');
|
const ghost = document.createElement('div');
|
||||||
ghost.classList.add('ghost');
|
ghost.classList.add('ghost');
|
||||||
|
|
||||||
@ -221,6 +224,8 @@ function createGhost(source: string | HTMLImageElement | HTMLVideoElement, origi
|
|||||||
|
|
||||||
if (typeof source === 'string') {
|
if (typeof source === 'string') {
|
||||||
img.src = source;
|
img.src = source;
|
||||||
|
} else if (source instanceof HTMLCanvasElement) {
|
||||||
|
img.src = source.toDataURL();
|
||||||
} else if (source instanceof HTMLVideoElement) {
|
} else if (source instanceof HTMLVideoElement) {
|
||||||
img.src = source.poster;
|
img.src = source.poster;
|
||||||
} else {
|
} else {
|
||||||
@ -305,6 +310,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
|||||||
mediaSelector = 'img';
|
mediaSelector = 'img';
|
||||||
break;
|
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:
|
case MediaViewerOrigin.SharedMedia:
|
||||||
containerSelector = `#shared-media${getMessageHtmlId(message!.id, index)}`;
|
containerSelector = `#shared-media${getMessageHtmlId(message!.id, index)}`;
|
||||||
mediaSelector = 'img';
|
mediaSelector = 'img';
|
||||||
@ -343,14 +353,18 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
|||||||
|
|
||||||
case MediaViewerOrigin.SponsoredMessage:
|
case MediaViewerOrigin.SponsoredMessage:
|
||||||
containerSelector = '.Transition_slide-active > .MessageList .sponsored-media-preview';
|
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;
|
break;
|
||||||
|
|
||||||
case MediaViewerOrigin.ScheduledInline:
|
case MediaViewerOrigin.ScheduledInline:
|
||||||
case MediaViewerOrigin.Inline:
|
case MediaViewerOrigin.Inline:
|
||||||
default:
|
default:
|
||||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id, index)}`;
|
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)!;
|
const container = document.querySelector<HTMLElement>(containerSelector)!;
|
||||||
@ -371,6 +385,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
|||||||
case MediaViewerOrigin.ScheduledInline:
|
case MediaViewerOrigin.ScheduledInline:
|
||||||
case MediaViewerOrigin.StarsTransaction:
|
case MediaViewerOrigin.StarsTransaction:
|
||||||
case MediaViewerOrigin.PreviewMedia:
|
case MediaViewerOrigin.PreviewMedia:
|
||||||
|
case MediaViewerOrigin.PollPreview:
|
||||||
ghost.classList.add('rounded-corners');
|
ghost.classList.add('rounded-corners');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@ -84,14 +84,13 @@ const AttachmentModalItem = ({
|
|||||||
);
|
);
|
||||||
default: {
|
default: {
|
||||||
const canEdit = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType) && !isMobile;
|
const canEdit = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType) && !isMobile;
|
||||||
const isPhoto = SUPPORTED_PHOTO_CONTENT_TYPES.has(attachment.mimeType);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<File
|
<File
|
||||||
className={styles.file}
|
className={styles.file}
|
||||||
name={attachment.filename}
|
name={attachment.filename}
|
||||||
extension={getFileExtension(attachment.filename, attachment.mimeType)}
|
extension={getFileExtension(attachment.filename, attachment.mimeType)}
|
||||||
previewData={isPhoto && attachment.blobUrl ? attachment.blobUrl : attachment.previewBlobUrl}
|
previewAttachment={attachment}
|
||||||
size={attachment.size}
|
size={attachment.size}
|
||||||
previewSize="large"
|
previewSize="large"
|
||||||
onClick={canEdit ? handleEditClick : undefined}
|
onClick={canEdit ? handleEditClick : undefined}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type { ApiNewPoll } from '../../../api/types';
|
|||||||
|
|
||||||
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
import { generateUniqueNumberId } from '../../../util/generateUniqueId';
|
||||||
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||||
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
@ -147,25 +148,26 @@ const PollModal = ({
|
|||||||
|
|
||||||
const payload: ApiNewPoll = {
|
const payload: ApiNewPoll = {
|
||||||
summary: {
|
summary: {
|
||||||
|
id: generateUniqueNumberId().toString(),
|
||||||
|
hash: '0',
|
||||||
question: {
|
question: {
|
||||||
text: questionTrimmed,
|
text: questionTrimmed,
|
||||||
},
|
},
|
||||||
answers,
|
answers,
|
||||||
...(!isAnonymous && { isPublic: true }),
|
isPublic: !isAnonymous || undefined,
|
||||||
...(isMultipleAnswers && { multipleChoice: true }),
|
isMultipleChoice: isMultipleAnswers || undefined,
|
||||||
...(isQuizMode && { quiz: true }),
|
isQuiz: isQuizMode || undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isQuizMode) {
|
if (isQuizMode) {
|
||||||
const { text, entities } = (solution && parseHtmlAsFormattedText(solution.substring(0, MAX_SOLUTION_LENGTH)))
|
const { text, entities } = (solution && parseHtmlAsFormattedText(solution.substring(0, MAX_SOLUTION_LENGTH)))
|
||||||
|| {};
|
|| {};
|
||||||
|
const correctAnswerIndex = answers.findIndex((answer) => answer.option === String(correctOption!));
|
||||||
|
|
||||||
payload.quiz = {
|
payload.correctAnswers = [correctAnswerIndex];
|
||||||
correctAnswers: [String(correctOption)],
|
payload.solution = text;
|
||||||
...(text && { solution: text }),
|
payload.solutionEntities = entities;
|
||||||
...(entities && { solutionEntities: entities }),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSend(payload);
|
onSend(payload);
|
||||||
|
|||||||
@ -114,6 +114,8 @@ const SINGLE_LINE_ACTIONS = new Set<ApiMessageAction['type']>([
|
|||||||
'chatDeletePhoto',
|
'chatDeletePhoto',
|
||||||
'todoCompletions',
|
'todoCompletions',
|
||||||
'todoAppendTasks',
|
'todoAppendTasks',
|
||||||
|
'pollAppendAnswer',
|
||||||
|
'pollDeleteAnswer',
|
||||||
'unsupported',
|
'unsupported',
|
||||||
]);
|
]);
|
||||||
const HIDDEN_TEXT_ACTIONS = new Set<ApiMessageAction['type']>(['giftCode', 'prizeStars',
|
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
|
case 'phoneCall': // Rendered as a regular message, but considered an action for the summary
|
||||||
return lang(getCallMessageKey(action, isOutgoing));
|
return lang(getCallMessageKey(action, isOutgoing));
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import type {
|
|||||||
ApiChat,
|
ApiChat,
|
||||||
ApiChatReactions,
|
ApiChatReactions,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiPoll,
|
ApiMessagePoll,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiStickerSet,
|
ApiStickerSet,
|
||||||
ApiStickerSetInfo,
|
ApiStickerSetInfo,
|
||||||
@ -108,7 +108,7 @@ export type OwnProps = {
|
|||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
threadId?: ThreadId;
|
threadId?: ThreadId;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
story?: ApiTypeStory;
|
story?: ApiTypeStory;
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
|
|||||||
@ -666,7 +666,7 @@
|
|||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
&.has-replies:not(.custom-shape),
|
&.has-replies:not(.custom-shape),
|
||||||
&.has-footer:not(.web-page) {
|
&.has-footer:not(.web-page):not(.poll) {
|
||||||
.media-inner,
|
.media-inner,
|
||||||
.Album {
|
.Album {
|
||||||
--border-bottom-left-radius: 0;
|
--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 {
|
&.text.is-inverted-media {
|
||||||
.Album,
|
.Album,
|
||||||
.media-inner {
|
.media-inner {
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import type {
|
|||||||
ApiKeyboardButton,
|
ApiKeyboardButton,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageOutgoingStatus,
|
ApiMessageOutgoingStatus,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiPeer,
|
ApiPeer,
|
||||||
ApiPoll,
|
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiReactionKey,
|
ApiReactionKey,
|
||||||
ApiSavedReactionTag,
|
ApiSavedReactionTag,
|
||||||
@ -202,7 +202,7 @@ import MessageMeta from './MessageMeta';
|
|||||||
import MessagePhoneCall from './MessagePhoneCall';
|
import MessagePhoneCall from './MessagePhoneCall';
|
||||||
import PaidMediaOverlay from './PaidMediaOverlay';
|
import PaidMediaOverlay from './PaidMediaOverlay';
|
||||||
import Photo from './Photo';
|
import Photo from './Photo';
|
||||||
import Poll from './Poll';
|
import Poll from './poll/Poll';
|
||||||
import Reactions from './reactions/Reactions';
|
import Reactions from './reactions/Reactions';
|
||||||
import RoundVideo from './RoundVideo';
|
import RoundVideo from './RoundVideo';
|
||||||
import Sticker from './Sticker';
|
import Sticker from './Sticker';
|
||||||
@ -323,7 +323,7 @@ type StateProps = {
|
|||||||
canTranscribeVoice?: boolean;
|
canTranscribeVoice?: boolean;
|
||||||
viaBusinessBot?: ApiUser;
|
viaBusinessBot?: ApiUser;
|
||||||
effect?: ApiAvailableEffect;
|
effect?: ApiAvailableEffect;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
maxTimestamp?: number;
|
maxTimestamp?: number;
|
||||||
lastPlaybackTimestamp?: number;
|
lastPlaybackTimestamp?: number;
|
||||||
@ -680,7 +680,6 @@ const Message = ({
|
|||||||
handleOpenThread,
|
handleOpenThread,
|
||||||
handleReadMedia,
|
handleReadMedia,
|
||||||
handleCancelUpload,
|
handleCancelUpload,
|
||||||
handleVoteSend,
|
|
||||||
handleGroupForward,
|
handleGroupForward,
|
||||||
handleForward,
|
handleForward,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
@ -1221,6 +1220,7 @@ const Message = ({
|
|||||||
noUserColors={noUserColors}
|
noUserColors={noUserColors}
|
||||||
isProtected={isProtected}
|
isProtected={isProtected}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
onClick={handleStoryClick}
|
onClick={handleStoryClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -1358,7 +1358,17 @@ const Message = ({
|
|||||||
<Contact contact={contact} noUserColors={isOwn} />
|
<Contact contact={contact} noUserColors={isOwn} />
|
||||||
)}
|
)}
|
||||||
{poll && (
|
{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 && (
|
{todo && (
|
||||||
<TodoList message={message} todoList={todo} />
|
<TodoList message={message} todoList={todo} />
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import type {
|
|||||||
ApiChat,
|
ApiChat,
|
||||||
ApiChatReactions,
|
ApiChatReactions,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiPeer,
|
ApiPeer,
|
||||||
ApiPoll,
|
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiStickerSet,
|
ApiStickerSet,
|
||||||
ApiThreadInfo,
|
ApiThreadInfo,
|
||||||
@ -57,7 +57,7 @@ type OwnProps = {
|
|||||||
anchor: IAnchorPosition;
|
anchor: IAnchorPosition;
|
||||||
targetHref?: string;
|
targetHref?: string;
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
story?: ApiTypeStory;
|
story?: ApiTypeStory;
|
||||||
canSendNow?: boolean;
|
canSendNow?: boolean;
|
||||||
|
|||||||
@ -37,7 +37,7 @@ import ProgressSpinner from '../../ui/ProgressSpinner';
|
|||||||
export type OwnProps<T> = {
|
export type OwnProps<T> = {
|
||||||
id?: string;
|
id?: string;
|
||||||
photo: ApiPhoto | ApiMediaExtendedPreview;
|
photo: ApiPhoto | ApiMediaExtendedPreview;
|
||||||
isInWebPage?: boolean;
|
isNestedMedia?: boolean;
|
||||||
messageText?: string;
|
messageText?: string;
|
||||||
isOwn?: boolean;
|
isOwn?: boolean;
|
||||||
noAvatars?: boolean;
|
noAvatars?: boolean;
|
||||||
@ -85,7 +85,7 @@ const Photo = <T,>({
|
|||||||
isDownloading,
|
isDownloading,
|
||||||
isProtected,
|
isProtected,
|
||||||
theme,
|
theme,
|
||||||
isInWebPage,
|
isNestedMedia,
|
||||||
clickArg,
|
clickArg,
|
||||||
className,
|
className,
|
||||||
isMediaNsfw,
|
isMediaNsfw,
|
||||||
@ -245,7 +245,7 @@ const Photo = <T,>({
|
|||||||
noAvatars,
|
noAvatars,
|
||||||
isMobile,
|
isMobile,
|
||||||
messageText,
|
messageText,
|
||||||
isInWebPage,
|
isNestedMedia,
|
||||||
});
|
});
|
||||||
|
|
||||||
const componentClassName = buildClassName(
|
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;
|
video: ApiVideo | ApiMediaExtendedPreview;
|
||||||
lastPlaybackTimestamp?: number;
|
lastPlaybackTimestamp?: number;
|
||||||
isOwn?: boolean;
|
isOwn?: boolean;
|
||||||
isInWebPage?: boolean;
|
isNestedMedia?: boolean;
|
||||||
noAvatars?: boolean;
|
noAvatars?: boolean;
|
||||||
canAutoLoad?: boolean;
|
canAutoLoad?: boolean;
|
||||||
canAutoPlay?: boolean;
|
canAutoPlay?: boolean;
|
||||||
@ -65,7 +65,7 @@ const Video = <T,>({
|
|||||||
id,
|
id,
|
||||||
video,
|
video,
|
||||||
isOwn,
|
isOwn,
|
||||||
isInWebPage,
|
isNestedMedia,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
canAutoLoad,
|
canAutoLoad,
|
||||||
canAutoPlay,
|
canAutoPlay,
|
||||||
@ -209,8 +209,8 @@ const Video = <T,>({
|
|||||||
width, height,
|
width, height,
|
||||||
} = dimensions || (
|
} = dimensions || (
|
||||||
isPaidPreview
|
isPaidPreview
|
||||||
? calculateExtendedPreviewDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile)
|
? calculateExtendedPreviewDimensions(video, Boolean(isOwn), asForwarded, isNestedMedia, noAvatars, isMobile)
|
||||||
: calculateVideoDimensions(video, Boolean(isOwn), asForwarded, isInWebPage, noAvatars, isMobile)
|
: calculateVideoDimensions(video, Boolean(isOwn), asForwarded, isNestedMedia, noAvatars, isMobile)
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>, isFromSpinner?: boolean) => {
|
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>, isFromSpinner?: boolean) => {
|
||||||
|
|||||||
@ -247,7 +247,7 @@ const WebPage = ({
|
|||||||
<Photo
|
<Photo
|
||||||
photo={photo}
|
photo={photo}
|
||||||
isOwn={message?.isOutgoing}
|
isOwn={message?.isOutgoing}
|
||||||
isInWebPage
|
isNestedMedia
|
||||||
observeIntersection={observeIntersectionForLoading}
|
observeIntersection={observeIntersectionForLoading}
|
||||||
noAvatars={noAvatars}
|
noAvatars={noAvatars}
|
||||||
canAutoLoad={canAutoLoad}
|
canAutoLoad={canAutoLoad}
|
||||||
@ -265,7 +265,7 @@ const WebPage = ({
|
|||||||
<Video
|
<Video
|
||||||
video={video}
|
video={video}
|
||||||
isOwn={message?.isOutgoing}
|
isOwn={message?.isOutgoing}
|
||||||
isInWebPage
|
isNestedMedia
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
noAvatars={noAvatars}
|
noAvatars={noAvatars}
|
||||||
canAutoLoad={canAutoLoad}
|
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 type { IAlbum } from '../../../../types';
|
||||||
|
|
||||||
import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
|
import { EMOJI_SIZES, MESSAGE_CONTENT_CLASS_NAME } from '../../../../config';
|
||||||
@ -26,7 +26,7 @@ export function buildContentClassName(
|
|||||||
peerColorClass,
|
peerColorClass,
|
||||||
hasOutsideReactions,
|
hasOutsideReactions,
|
||||||
}: {
|
}: {
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
hasSubheader?: boolean;
|
hasSubheader?: boolean;
|
||||||
isCustomShape?: boolean | number;
|
isCustomShape?: boolean | number;
|
||||||
@ -61,8 +61,10 @@ export function buildContentClassName(
|
|||||||
const isInvertibleMedia = photo || (video && !isRoundVideo) || album || webPage;
|
const isInvertibleMedia = photo || (video && !isRoundVideo) || album || webPage;
|
||||||
|
|
||||||
const classNames = [MESSAGE_CONTENT_CLASS_NAME];
|
const classNames = [MESSAGE_CONTENT_CLASS_NAME];
|
||||||
const isMedia = storyData || photo || video || location || invoice?.extendedMedia || paidMedia;
|
const pollMedia = poll?.attachedMedia;
|
||||||
const hasText = text || location?.mediaType === 'venue' || isGeoLiveActive || hasFactCheck;
|
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 isMediaWithNoText = isMedia && !hasText;
|
||||||
const hasInlineKeyboard = Boolean(message.inlineButtons);
|
const hasInlineKeyboard = Boolean(message.inlineButtons);
|
||||||
const isViaBot = Boolean(message.viaBotId);
|
const isViaBot = Boolean(message.viaBotId);
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export function calculateMediaDimensions({
|
|||||||
media,
|
media,
|
||||||
messageText,
|
messageText,
|
||||||
isOwn,
|
isOwn,
|
||||||
isInWebPage,
|
isNestedMedia,
|
||||||
asForwarded,
|
asForwarded,
|
||||||
noAvatars,
|
noAvatars,
|
||||||
isMobile,
|
isMobile,
|
||||||
@ -34,19 +34,19 @@ export function calculateMediaDimensions({
|
|||||||
media: ApiPhoto | ApiVideo | ApiMediaExtendedPreview;
|
media: ApiPhoto | ApiVideo | ApiMediaExtendedPreview;
|
||||||
messageText?: string;
|
messageText?: string;
|
||||||
isOwn?: boolean;
|
isOwn?: boolean;
|
||||||
isInWebPage?: boolean;
|
isNestedMedia?: boolean;
|
||||||
asForwarded?: boolean;
|
asForwarded?: boolean;
|
||||||
noAvatars?: boolean;
|
noAvatars?: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
}) {
|
}) {
|
||||||
const isPhoto = media.mediaType === 'photo';
|
const isPhoto = media.mediaType === 'photo';
|
||||||
const isVideo = media.mediaType === 'video';
|
const isVideo = media.mediaType === 'video';
|
||||||
const isWebPagePhoto = isPhoto && isInWebPage;
|
const isWebPagePhoto = isPhoto && isNestedMedia;
|
||||||
const isWebPageVideo = isVideo && isInWebPage;
|
const isWebPageVideo = isVideo && isNestedMedia;
|
||||||
const { width, height } = isPhoto
|
const { width, height } = isPhoto
|
||||||
? calculateInlineImageDimensions(media, isOwn, asForwarded, isWebPagePhoto, noAvatars, isMobile)
|
? calculateInlineImageDimensions(media, isOwn, asForwarded, isWebPagePhoto, noAvatars, isMobile)
|
||||||
: isVideo ? calculateVideoDimensions(media, isOwn, asForwarded, isWebPageVideo, 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);
|
const minMediaWidth = getMinMediaWidth(messageText, isMobile);
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export default function useInnerHandlers({
|
|||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
openChat, openChatWithDraft, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
openChat, openChatWithDraft, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
|
||||||
markMessagesRead, cancelUploadMedia, sendPollVote, openForwardMenu,
|
markMessagesRead, cancelUploadMedia, openForwardMenu,
|
||||||
openChatLanguageModal, openThread, openStoryViewer, searchChatMediaMessages,
|
openChatLanguageModal, openThread, openStoryViewer, searchChatMediaMessages,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
@ -198,10 +198,6 @@ export default function useInnerHandlers({
|
|||||||
cancelUploadMedia({ chatId, messageId });
|
cancelUploadMedia({ chatId, messageId });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVoteSend = useLastCallback((options: string[]) => {
|
|
||||||
sendPollVote({ chatId, messageId, options });
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleGroupForward = useLastCallback(() => {
|
const handleGroupForward = useLastCallback(() => {
|
||||||
openForwardMenu({ fromChatId: chatId, groupedId });
|
openForwardMenu({ fromChatId: chatId, groupedId });
|
||||||
});
|
});
|
||||||
@ -305,7 +301,6 @@ export default function useInnerHandlers({
|
|||||||
handleOpenThread,
|
handleOpenThread,
|
||||||
handleReadMedia,
|
handleReadMedia,
|
||||||
handleCancelUpload,
|
handleCancelUpload,
|
||||||
handleVoteSend,
|
|
||||||
handleGroupForward,
|
handleGroupForward,
|
||||||
handleForward,
|
handleForward,
|
||||||
handleFocus,
|
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) {
|
@media (max-width: 600px) {
|
||||||
.pinnedMessage {
|
.pinnedMessage {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
getIsSavedDialog,
|
getIsSavedDialog,
|
||||||
getMessageIsSpoiler,
|
getMessageIsSpoiler,
|
||||||
getMessageSingleInlineButton,
|
getMessageSingleInlineButton,
|
||||||
getMessageVideo,
|
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import { getPeerTitle } from '../../../global/helpers/peers';
|
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||||
import {
|
import {
|
||||||
@ -25,12 +24,10 @@ import {
|
|||||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import cycleRestrict from '../../../util/cycleRestrict';
|
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 renderText from '../../common/helpers/renderText';
|
||||||
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
|
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
|
||||||
|
|
||||||
import useMessageMediaHash from '../../../hooks/media/useMessageMediaHash';
|
|
||||||
import useThumbnail from '../../../hooks/media/useThumbnail';
|
|
||||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
import useDerivedState from '../../../hooks/useDerivedState';
|
import useDerivedState from '../../../hooks/useDerivedState';
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||||
@ -38,14 +35,13 @@ import { useFastClick } from '../../../hooks/useFastClick';
|
|||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
||||||
|
|
||||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||||
|
import CompactMediaPreview, { canRenderCompactMediaPreview } from '../../common/CompactMediaPreview';
|
||||||
import Icon from '../../common/icons/Icon';
|
import Icon from '../../common/icons/Icon';
|
||||||
import MediaSpoiler from '../../common/MediaSpoiler';
|
|
||||||
import MessageSummary from '../../common/MessageSummary';
|
import MessageSummary from '../../common/MessageSummary';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
@ -114,21 +110,15 @@ const HeaderPinnedMessage = ({
|
|||||||
|
|
||||||
const topMessageTitle = topMessageSender ? getPeerTitle(lang, topMessageSender) : undefined;
|
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 isLoading = Boolean(useDerivedState(getLoadingPinnedId));
|
||||||
const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
|
const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
|
||||||
const shouldShowLoader = canRenderLoader && isLoading;
|
const shouldShowLoader = canRenderLoader && isLoading;
|
||||||
|
|
||||||
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
|
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
|
||||||
|
const hasPictogram = Boolean(
|
||||||
|
renderingPinnedMessage && canRenderCompactMediaPreview(renderingPinnedMessage.content),
|
||||||
|
);
|
||||||
|
const isSpoiler = renderingPinnedMessage && getMessageIsSpoiler(renderingPinnedMessage);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) {
|
if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) {
|
||||||
@ -190,39 +180,6 @@ const HeaderPinnedMessage = ({
|
|||||||
|
|
||||||
const { handleClick, handleMouseDown } = useFastClick(handleMessageClick);
|
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;
|
if (!shouldRender || !renderingPinnedMessage) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -284,12 +241,12 @@ const HeaderPinnedMessage = ({
|
|||||||
index={currentPinnedIndex}
|
index={currentPinnedIndex}
|
||||||
/>
|
/>
|
||||||
<Transition activeKey={renderingPinnedMessage.id} name="slideVertical" className={styles.pictogramTransition}>
|
<Transition activeKey={renderingPinnedMessage.id} name="slideVertical" className={styles.pictogramTransition}>
|
||||||
{renderPictogram(
|
<CompactMediaPreview
|
||||||
mediaThumbnail,
|
media={renderingPinnedMessage.content}
|
||||||
mediaBlobUrl,
|
className={styles.pinnedThumb}
|
||||||
isVideoThumbnail,
|
isPictogram
|
||||||
isSpoiler,
|
isSpoiler={isSpoiler}
|
||||||
)}
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
<div
|
<div
|
||||||
className={buildClassName(styles.messageText, hasPictogram && styles.withMedia)}
|
className={buildClassName(styles.messageText, hasPictogram && styles.withMedia)}
|
||||||
@ -315,7 +272,7 @@ const HeaderPinnedMessage = ({
|
|||||||
<MessageSummary
|
<MessageSummary
|
||||||
message={renderingPinnedMessage}
|
message={renderingPinnedMessage}
|
||||||
truncateLength={MAX_LENGTH}
|
truncateLength={MAX_LENGTH}
|
||||||
noEmoji={Boolean(mediaThumbnail)}
|
noEmoji={hasPictogram}
|
||||||
emojiSize={EMOJI_SIZE}
|
emojiSize={EMOJI_SIZE}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { TabState } from '../../../global/types';
|
|||||||
import { SettingsScreens } from '../../../types';
|
import { SettingsScreens } from '../../../types';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
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 { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
@ -210,7 +210,7 @@ const BirthdaySetupModal = ({ modal }: OwnProps) => {
|
|||||||
link: (
|
link: (
|
||||||
<Link isPrimary onClick={handlePrivacyClick}>
|
<Link isPrimary onClick={handlePrivacyClick}>
|
||||||
{lang('BirthdayPrivacySuggestionLink', undefined,
|
{lang('BirthdayPrivacySuggestionLink', undefined,
|
||||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}, { withNodes: true })}
|
}, { withNodes: true })}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import buildStyle from '../../../util/buildStyle';
|
|||||||
import { formatCountdown } from '../../../util/dates/oldDateFormat';
|
import { formatCountdown } from '../../../util/dates/oldDateFormat';
|
||||||
import { HOUR } from '../../../util/dates/units';
|
import { HOUR } from '../../../util/dates/units';
|
||||||
import { formatCurrency } from '../../../util/formatCurrency';
|
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 { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
import useCustomBackground from '../../../hooks/useCustomBackground';
|
import useCustomBackground from '../../../hooks/useCustomBackground';
|
||||||
@ -293,7 +293,7 @@ function GiftComposer({
|
|||||||
<Link isPrimary onClick={handleGetMoreStars}>
|
<Link isPrimary onClick={handleGetMoreStars}>
|
||||||
{lang('GetMoreStarsLinkText', undefined, {
|
{lang('GetMoreStarsLinkText', undefined, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
specialReplacement: getNextArrowReplacement(),
|
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||||
})}
|
})}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@ -332,7 +332,7 @@ function GiftComposer({
|
|||||||
link: (
|
link: (
|
||||||
<Link isPrimary onClick={handleOpenUpgradePreview}>
|
<Link isPrimary onClick={handleOpenUpgradePreview}>
|
||||||
{lang('GiftMakeUniqueLink', undefined, { withNodes: true,
|
{lang('GiftMakeUniqueLink', undefined, { withNodes: true,
|
||||||
specialReplacement: getNextArrowReplacement() })}
|
specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
@ -344,7 +344,7 @@ function GiftComposer({
|
|||||||
<Link isPrimary onClick={handleOpenUpgradePreview}>
|
<Link isPrimary onClick={handleOpenUpgradePreview}>
|
||||||
{lang('GiftMakeUniqueLink', undefined, {
|
{lang('GiftMakeUniqueLink', undefined, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
specialReplacement: getNextArrowReplacement() })}
|
specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { getUserFullName } from '../../../global/helpers';
|
|||||||
import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers';
|
import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers';
|
||||||
import { selectPeer, selectTabState, selectUserFullInfo } from '../../../global/selectors';
|
import { selectPeer, selectTabState, selectUserFullInfo } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
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 { throttle } from '../../../util/schedulers';
|
||||||
import { REM } from '../../common/helpers/mediaDimensions';
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
@ -245,7 +245,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{lang('GiftPremiumDescriptionLinkCaption', undefined, {
|
{lang('GiftPremiumDescriptionLinkCaption', undefined, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
specialReplacement: getNextArrowReplacement(),
|
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||||
})}
|
})}
|
||||||
</SafeLink>
|
</SafeLink>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import type { TabState } from '../../../../global/types';
|
|||||||
import { requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
import { requestMeasure, requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||||
import { VTT_CRAFT_ATTRIBUTES } from '../../../../util/animations/viewTransitionTypes';
|
import { VTT_CRAFT_ATTRIBUTES } from '../../../../util/animations/viewTransitionTypes';
|
||||||
import buildClassName from '../../../../util/buildClassName';
|
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 { formatPercent } from '../../../../util/textFormat';
|
||||||
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||||
import { getGiftAttributes } from '../../../common/helpers/gifts';
|
import { getGiftAttributes } from '../../../common/helpers/gifts';
|
||||||
@ -1392,7 +1392,7 @@ const GiftCraftModal = ({ modal, craftAttributePermilles }: OwnProps & StateProp
|
|||||||
<span className={styles.viewAllText}>
|
<span className={styles.viewAllText}>
|
||||||
{lang('GiftCraftViewAll', undefined, {
|
{lang('GiftCraftViewAll', undefined, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
specialReplacement: getNextArrowReplacement(),
|
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { formatDateTimeToString } from '../../../../util/dates/oldDateFormat';
|
|||||||
import { formatCurrency, formatCurrencyAsString } from '../../../../util/formatCurrency';
|
import { formatCurrency, formatCurrencyAsString } from '../../../../util/formatCurrency';
|
||||||
import {
|
import {
|
||||||
formatStarsAsIcon, formatStarsAsText, formatTonAsIcon, formatTonAsText,
|
formatStarsAsIcon, formatStarsAsText, formatTonAsIcon, formatTonAsText,
|
||||||
getNextArrowReplacement,
|
NEXT_ARROW_REPLACEMENT,
|
||||||
} from '../../../../util/localization/format';
|
} from '../../../../util/localization/format';
|
||||||
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
|
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
|
||||||
import { getServerTime } from '../../../../util/serverTime';
|
import { getServerTime } from '../../../../util/serverTime';
|
||||||
@ -786,7 +786,7 @@ const GiftInfoModal = ({
|
|||||||
link: (
|
link: (
|
||||||
<SafeLink url={tonLink} shouldSkipModal text={lang('GiftInfoTonLinkText')}>
|
<SafeLink url={tonLink} shouldSkipModal text={lang('GiftInfoTonLinkText')}>
|
||||||
{lang('GiftInfoTonLinkText', undefined,
|
{lang('GiftInfoTonLinkText', undefined,
|
||||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</SafeLink>
|
</SafeLink>
|
||||||
),
|
),
|
||||||
}, { withNodes: true })}
|
}, { withNodes: true })}
|
||||||
@ -798,7 +798,7 @@ const GiftInfoModal = ({
|
|||||||
link: (
|
link: (
|
||||||
<Link isPrimary onClick={handleTriggerVisibility}>
|
<Link isPrimary onClick={handleTriggerVisibility}>
|
||||||
{lang(`GiftInfoSaved${isUnsaved ? 'Show' : 'Hide'}`, undefined,
|
{lang(`GiftInfoSaved${isUnsaved ? 'Show' : 'Hide'}`, undefined,
|
||||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import type { AnimationLevel } from '../../../../types';
|
|||||||
|
|
||||||
import { selectAnimationLevel } from '../../../../global/selectors/sharedState';
|
import { selectAnimationLevel } from '../../../../global/selectors/sharedState';
|
||||||
import buildClassName from '../../../../util/buildClassName';
|
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 { resolveTransitionName } from '../../../../util/resolveTransitionName';
|
||||||
import { getGiftAttributes, getRandomGiftPreviewAttributes } from '../../../common/helpers/gifts';
|
import { getGiftAttributes, getRandomGiftPreviewAttributes } from '../../../common/helpers/gifts';
|
||||||
|
|
||||||
@ -359,7 +359,7 @@ const GiftPreviewModal = ({ modal, animationLevel }: OwnProps & StateProps) => {
|
|||||||
{lang(
|
{lang(
|
||||||
isCraftableModelsMode ? 'GiftPreviewToggleRegularModels' : 'GiftPreviewToggleCraftableModels',
|
isCraftableModelsMode ? 'GiftPreviewToggleRegularModels' : 'GiftPreviewToggleCraftableModels',
|
||||||
undefined,
|
undefined,
|
||||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() },
|
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT },
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type { TabState } from '../../../../global/types';
|
|||||||
|
|
||||||
import { getPeerTitle } from '../../../../global/helpers/peers';
|
import { getPeerTitle } from '../../../../global/helpers/peers';
|
||||||
import { selectPeer } from '../../../../global/selectors';
|
import { selectPeer } from '../../../../global/selectors';
|
||||||
import { getNextArrowReplacement } from '../../../../util/localization/format';
|
import { NEXT_ARROW_REPLACEMENT } from '../../../../util/localization/format';
|
||||||
import {
|
import {
|
||||||
getRandomGiftPreviewAttributes, type GiftPreviewAttributes,
|
getRandomGiftPreviewAttributes, type GiftPreviewAttributes,
|
||||||
preloadGiftAttributeStickers } from '../../../common/helpers/gifts';
|
preloadGiftAttributeStickers } from '../../../common/helpers/gifts';
|
||||||
@ -193,7 +193,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<span className={styles.viewAllText}>
|
<span className={styles.viewAllText}>
|
||||||
{lang('GiftUpgradeViewAll', undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
{lang('GiftUpgradeViewAll', undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -255,7 +255,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
|||||||
onClick={handleOpenPriceInfo}
|
onClick={handleOpenPriceInfo}
|
||||||
>
|
>
|
||||||
{lang('StarGiftPriceDecreaseInfoLink', undefined,
|
{lang('StarGiftPriceDecreaseInfoLink', undefined,
|
||||||
{ withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -3,11 +3,10 @@ import { getActions } from '../../../global';
|
|||||||
|
|
||||||
import type { TabState } from '../../../global/types';
|
import type { TabState } from '../../../global/types';
|
||||||
|
|
||||||
import { IS_IOS, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
|
||||||
import { prepareMapUrl } from '../../../util/map';
|
import { prepareMapUrl } from '../../../util/map';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
@ -23,7 +22,7 @@ const OpenMapModal = ({ modal }: OwnProps) => {
|
|||||||
|
|
||||||
const { point: geoPoint, zoom } = modal || {};
|
const { point: geoPoint, zoom } = modal || {};
|
||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const isOpen = Boolean(geoPoint);
|
const isOpen = Boolean(geoPoint);
|
||||||
|
|
||||||
@ -38,8 +37,8 @@ const OpenMapModal = ({ modal }: OwnProps) => {
|
|||||||
|
|
||||||
const google = prepareMapUrl('google', geoPoint, zoom);
|
const google = prepareMapUrl('google', geoPoint, zoom);
|
||||||
const bing = prepareMapUrl('bing', geoPoint, zoom);
|
const bing = prepareMapUrl('bing', geoPoint, zoom);
|
||||||
const osm = prepareMapUrl('osm', geoPoint, zoom);
|
|
||||||
const apple = prepareMapUrl('apple', geoPoint, zoom);
|
const apple = prepareMapUrl('apple', geoPoint, zoom);
|
||||||
|
const osm = prepareMapUrl('osm', geoPoint, zoom);
|
||||||
|
|
||||||
return [google, bing, apple, osm];
|
return [google, bing, apple, osm];
|
||||||
}, [geoPoint, zoom]);
|
}, [geoPoint, zoom]);
|
||||||
@ -74,18 +73,16 @@ const OpenMapModal = ({ modal }: OwnProps) => {
|
|||||||
isSlim
|
isSlim
|
||||||
>
|
>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
{(IS_IOS || IS_MAC_OS) && (
|
<Button noForcedUpperCase fluid size="smaller" onClick={handleGoogleClick}>
|
||||||
<Button fluid size="smaller" onClick={handleAppleClick}>
|
|
||||||
Apple Maps
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button fluid size="smaller" onClick={handleGoogleClick}>
|
|
||||||
Google Maps
|
Google Maps
|
||||||
</Button>
|
</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
|
Bing Maps
|
||||||
</Button>
|
</Button>
|
||||||
<Button fluid size="smaller" onClick={handleOsmClick}>
|
<Button noForcedUpperCase fluid size="smaller" onClick={handleOsmClick}>
|
||||||
OpenStreetMap
|
OpenStreetMap
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { getPeerTitle } from '../../../global/helpers/peers';
|
|||||||
import { selectUser, selectUserFullInfo } from '../../../global/selectors';
|
import { selectUser, selectUserFullInfo } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatShortDuration } from '../../../util/dates/oldDateFormat';
|
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 { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
@ -132,7 +132,7 @@ const ProfileRatingModal = ({
|
|||||||
link: (
|
link: (
|
||||||
<span className={styles.backLink} onClick={handleShowCurrent}>
|
<span className={styles.backLink} onClick={handleShowCurrent}>
|
||||||
{lang('LinkDescriptionRatingBack',
|
{lang('LinkDescriptionRatingBack',
|
||||||
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
@ -148,7 +148,7 @@ const ProfileRatingModal = ({
|
|||||||
link: (
|
link: (
|
||||||
<span className={styles.previewLink} onClick={handleShowFuture}>
|
<span className={styles.previewLink} onClick={handleShowFuture}>
|
||||||
{lang('LinkDescriptionRatingPreview',
|
{lang('LinkDescriptionRatingPreview',
|
||||||
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
|
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { memo } from '../../lib/teact/teact';
|
import { memo } from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiMessage, ApiPoll } from '../../api/types';
|
import type { ApiChat, ApiMessage, ApiMessagePoll } from '../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectChat, selectChatMessage, selectPollFromMessage, selectTabState,
|
selectChat, selectChatMessage, selectPollFromMessage, selectTabState,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { buildCollectionByKey } from '../../util/iteratees';
|
|
||||||
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
|
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
|
||||||
|
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
@ -25,7 +24,7 @@ export type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
message?: ApiMessage;
|
message?: ApiMessage;
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PollResults = ({
|
const PollResults = ({
|
||||||
@ -47,12 +46,10 @@ const PollResults = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { summary, results } = poll;
|
const { summary, results } = poll;
|
||||||
if (!results.results) {
|
if (!results.resultByOption) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultsByOption = buildCollectionByKey(results.results, 'option');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="PollResults" dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className="PollResults" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<h3 className="poll-question" dir="auto">
|
<h3 className="poll-question" dir="auto">
|
||||||
@ -64,11 +61,11 @@ const PollResults = ({
|
|||||||
<div className="poll-results-list custom-scroll">
|
<div className="poll-results-list custom-scroll">
|
||||||
{summary.answers.map((answer) => (
|
{summary.answers.map((answer) => (
|
||||||
<PollAnswerResults
|
<PollAnswerResults
|
||||||
key={`${poll.id}-${answer.option}`}
|
key={`${poll.summary.id}-${answer.option}`}
|
||||||
chat={chat}
|
chat={chat}
|
||||||
message={message}
|
message={message}
|
||||||
answer={answer}
|
answer={answer}
|
||||||
answerVote={resultsByOption[answer.option]}
|
answerVote={results.resultByOption![answer.option]}
|
||||||
totalVoters={results.totalVoters!}
|
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 {
|
&.faded {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|
||||||
@ -532,5 +499,38 @@
|
|||||||
margin-inline-start: 0.25rem;
|
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 { formatStarsAmount } from '../../global/helpers/payments';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { convertTonFromNanos, convertTonToUsd, formatCurrencyAsString } from '../../util/formatCurrency';
|
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 useIsTopmostBalanceBarModal from '../../hooks/element/useIsTopmostBalanceBarModal';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
@ -101,7 +101,7 @@ function ModalStarBalanceBar({
|
|||||||
<Link className={styles.getMoreStarsLink} isPrimary onClick={handleGetMoreStars}>
|
<Link className={styles.getMoreStarsLink} isPrimary onClick={handleGetMoreStars}>
|
||||||
{lang('GetMoreStarsLinkText', undefined, {
|
{lang('GetMoreStarsLinkText', undefined, {
|
||||||
withNodes: true,
|
withNodes: true,
|
||||||
specialReplacement: getNextArrowReplacement(),
|
specialReplacement: NEXT_ARROW_REPLACEMENT,
|
||||||
})}
|
})}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,28 +1,46 @@
|
|||||||
import { useEffect } from '../../lib/teact/teact';
|
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 { getServerTime } from '../../util/serverTime';
|
||||||
|
|
||||||
import useInterval from '../../hooks/schedulers/useInterval';
|
import useInterval from '../../hooks/schedulers/useInterval';
|
||||||
|
import useTimeout from '../../hooks/schedulers/useTimeout';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import AnimatedCounter from '../common/AnimatedCounter';
|
import AnimatedCounter from '../common/AnimatedCounter';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
endsAt: number;
|
endsAt: number;
|
||||||
|
mode?: 'clock' | 'countdown';
|
||||||
shouldShowZeroOnEnd?: boolean;
|
shouldShowZeroOnEnd?: boolean;
|
||||||
onEnd?: NoneToVoidFunction;
|
onEnd?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DAY_IN_SECONDS = 24 * 60 * 60;
|
||||||
const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000
|
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 forceUpdate = useForceUpdate();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const serverTime = getServerTime();
|
const serverTime = getServerTime();
|
||||||
const isActive = serverTime < endsAt;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
@ -32,18 +50,35 @@ const TextTimer = ({ className, endsAt, shouldShowZeroOnEnd, onEnd }: OwnProps)
|
|||||||
|
|
||||||
if (!isActive && !shouldShowZeroOnEnd) return undefined;
|
if (!isActive && !shouldShowZeroOnEnd) return undefined;
|
||||||
|
|
||||||
const timeLeft = Math.max(0, endsAt - serverTime);
|
if (mode === 'countdown' && !shouldUseClock) {
|
||||||
const time = formatMediaDuration(timeLeft);
|
return (
|
||||||
|
<span className={className}>
|
||||||
|
{formatCountdownDateTime(lang, secondsToDate(endsAt), {
|
||||||
|
anchorDate: secondsToDate(serverTime),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = formatClockDuration(timeLeft);
|
||||||
|
|
||||||
const timeParts = time.split(':');
|
const timeParts = time.split(':');
|
||||||
|
const clockNode = (
|
||||||
|
<>
|
||||||
|
{timeParts.map((part, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{index > 0 && ':'}
|
||||||
|
<AnimatedCounter text={part} />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={className} style="font-variant-numeric: tabular-nums;">
|
<span className={className} style="font-variant-numeric: tabular-nums;">
|
||||||
{timeParts.map((part, index) => (
|
{mode === 'countdown'
|
||||||
<>
|
? lang('TimeIn', { time: clockNode }, { withNodes: true })
|
||||||
{index > 0 && ':'}
|
: clockNode}
|
||||||
<AnimatedCounter key={index} text={part} />
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -196,7 +196,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webPage) {
|
if (webPage) {
|
||||||
@ -320,7 +320,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webPage) {
|
if (webPage) {
|
||||||
@ -368,7 +368,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webPage) {
|
if (webPage) {
|
||||||
@ -398,7 +398,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webPage) {
|
if (webPage) {
|
||||||
@ -444,7 +444,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
global = updateQuickReplyMessage(global, id, message);
|
global = updateQuickReplyMessage(global, id, message);
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webPage) {
|
if (webPage) {
|
||||||
@ -547,7 +547,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
global = {
|
global = {
|
||||||
@ -624,7 +624,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (poll) {
|
if (poll) {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|||||||
@ -46,7 +46,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
}
|
}
|
||||||
if (polls) {
|
if (polls) {
|
||||||
polls.forEach((poll) => {
|
polls.forEach((poll) => {
|
||||||
global = updatePoll(global, poll.id, poll);
|
global = updatePoll(global, poll.summary.id, poll);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (webPages) {
|
if (webPages) {
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import type {
|
|||||||
ApiTypeStory,
|
ApiTypeStory,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type {
|
import type {
|
||||||
ApiFormattedText,
|
ApiFormattedText, ApiMessagePoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
|
||||||
ApiPoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
|
|
||||||
} from '../../api/types/messages';
|
} from '../../api/types/messages';
|
||||||
import type { ThreadId } from '../../types';
|
import type { ThreadId } from '../../types';
|
||||||
import type { LangFn } from '../../util/localization';
|
import type { LangFn } from '../../util/localization';
|
||||||
@ -74,6 +73,8 @@ export function hasMessageText(message: MediaContainer) {
|
|||||||
webPage, contact, invoice, location, game, storyData, giveaway, giveawayResults, paidMedia,
|
webPage, contact, invoice, location, game, storyData, giveaway, giveawayResults, paidMedia,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
|
if (pollId) return false;
|
||||||
|
|
||||||
return Boolean(text) || !(
|
return Boolean(text) || !(
|
||||||
sticker || photo || video || audio || voice || document || contact || pollId || todo || webPage
|
sticker || photo || video || audio || voice || document || contact || pollId || todo || webPage
|
||||||
|| invoice || location || game || storyData || giveaway || giveawayResults || dice
|
|| invoice || location || game || storyData || giveaway || giveawayResults || dice
|
||||||
@ -96,7 +97,7 @@ export function groupStatefulContent({
|
|||||||
story,
|
story,
|
||||||
webPage,
|
webPage,
|
||||||
}: {
|
}: {
|
||||||
poll?: ApiPoll;
|
poll?: ApiMessagePoll;
|
||||||
story?: ApiTypeStory;
|
story?: ApiTypeStory;
|
||||||
webPage?: ApiWebPage;
|
webPage?: ApiWebPage;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage,
|
ApiMessage, ApiMessagePoll, ApiPollResult, ApiPollResults, ApiQuickReply, ApiSponsoredMessage,
|
||||||
ApiWebPage,
|
ApiWebPage,
|
||||||
ApiWebPageFull,
|
ApiWebPageFull,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
@ -871,34 +871,21 @@ export function replaceWebPage<T extends GlobalState>(
|
|||||||
export function updatePoll<T extends GlobalState>(
|
export function updatePoll<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
pollId: string,
|
pollId: string,
|
||||||
pollUpdate: Partial<ApiPoll>,
|
pollUpdate: Partial<ApiMessagePoll>,
|
||||||
) {
|
) {
|
||||||
const poll = selectPoll(global, pollId);
|
const poll = selectPoll(global, pollId);
|
||||||
|
const results = mergePollResults(poll?.results, pollUpdate.results);
|
||||||
|
|
||||||
const oldResults = poll?.results;
|
if (!results) {
|
||||||
let newResults = oldResults || pollUpdate.results;
|
return global;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedPoll = {
|
const updatedPoll = {
|
||||||
...poll,
|
...poll,
|
||||||
...pollUpdate,
|
...pollUpdate,
|
||||||
results: newResults,
|
results,
|
||||||
} satisfies ApiPoll;
|
} satisfies ApiMessagePoll;
|
||||||
if (!updatedPoll.id) {
|
if (!updatedPoll.summary?.id) {
|
||||||
return global;
|
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>(
|
export function updatePollVote<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
pollId: string,
|
pollId: string,
|
||||||
@ -925,28 +964,26 @@ export function updatePollVote<T extends GlobalState>(
|
|||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { recentVoterIds, totalVoters, results } = poll.results;
|
const { recentVoterIds, totalVoters, resultByOption } = poll.results;
|
||||||
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
|
const newRecentVoterIds = recentVoterIds ? [...recentVoterIds] : [];
|
||||||
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
|
const newTotalVoters = totalVoters ? totalVoters + 1 : 1;
|
||||||
const newResults = results ? [...results] : [];
|
const newResultByOption = { ...resultByOption };
|
||||||
|
|
||||||
newRecentVoterIds.push(peerId);
|
newRecentVoterIds.push(peerId);
|
||||||
|
|
||||||
options.forEach((option) => {
|
options.forEach((option) => {
|
||||||
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
|
const targetOption = resultByOption?.[option];
|
||||||
const targetOption = newResults[targetOptionIndex];
|
|
||||||
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
|
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
|
||||||
|
const recentOptionVoterIds = updatedOption.recentVoterIds ? [...updatedOption.recentVoterIds] : [];
|
||||||
|
|
||||||
updatedOption.votersCount += 1;
|
updatedOption.votersCount += 1;
|
||||||
|
recentOptionVoterIds.push(peerId);
|
||||||
|
updatedOption.recentVoterIds = recentOptionVoterIds;
|
||||||
if (peerId === global.currentUserId) {
|
if (peerId === global.currentUserId) {
|
||||||
updatedOption.isChosen = true;
|
updatedOption.isChosen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetOptionIndex) {
|
newResultByOption[option] = updatedOption;
|
||||||
newResults[targetOptionIndex] = updatedOption;
|
|
||||||
} else {
|
|
||||||
newResults.push(updatedOption);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatePoll(global, pollId, {
|
return updatePoll(global, pollId, {
|
||||||
@ -954,7 +991,7 @@ export function updatePollVote<T extends GlobalState>(
|
|||||||
...poll.results,
|
...poll.results,
|
||||||
recentVoterIds: newRecentVoterIds,
|
recentVoterIds: newRecentVoterIds,
|
||||||
totalVoters: newTotalVoters,
|
totalVoters: newTotalVoters,
|
||||||
results: newResults,
|
resultByOption: newResultByOption,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -647,8 +647,12 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
|
|||||||
const canSaveGif = message.content.video?.isGif;
|
const canSaveGif = message.content.video?.isGif;
|
||||||
|
|
||||||
const poll = content.pollId ? selectPoll(global, content.pollId) : undefined;
|
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 hasChosenPollAnswer = Boolean(
|
||||||
const canClosePoll = hasMessageEditRight && poll && !poll.summary.closed && !isForwarded;
|
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 = [
|
const noOptions = [
|
||||||
canReply,
|
canReply,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import type {
|
|||||||
ApiEmojiStatusType,
|
ApiEmojiStatusType,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
|
ApiMessagePoll,
|
||||||
ApiNotifyPeerType,
|
ApiNotifyPeerType,
|
||||||
ApiPaidReactionPrivacyType,
|
ApiPaidReactionPrivacyType,
|
||||||
ApiPasskey,
|
ApiPasskey,
|
||||||
@ -23,7 +24,6 @@ import type {
|
|||||||
ApiPeerPhotos,
|
ApiPeerPhotos,
|
||||||
ApiPeerStories,
|
ApiPeerStories,
|
||||||
ApiPhoneCall,
|
ApiPhoneCall,
|
||||||
ApiPoll,
|
|
||||||
ApiPrivacyKey,
|
ApiPrivacyKey,
|
||||||
ApiPrivacySettings,
|
ApiPrivacySettings,
|
||||||
ApiPromoData,
|
ApiPromoData,
|
||||||
@ -253,7 +253,7 @@ export type GlobalState = {
|
|||||||
byId: Record<number, number>;
|
byId: Record<number, number>;
|
||||||
}>;
|
}>;
|
||||||
sponsoredByChatId: Record<string, ApiSponsoredMessage>;
|
sponsoredByChatId: Record<string, ApiSponsoredMessage>;
|
||||||
pollById: Record<string, ApiPoll>;
|
pollById: Record<string, ApiMessagePoll>;
|
||||||
webPageById: Record<string, ApiWebPage>;
|
webPageById: Record<string, ApiWebPage>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -16,337 +16,338 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$icons-map: (
|
$icons-map: (
|
||||||
"zoom-out": "\f101",
|
"active-sessions": "\f101",
|
||||||
"zoom-in": "\f102",
|
"add-caption": "\f102",
|
||||||
"word-wrap": "\f103",
|
"add-filled": "\f103",
|
||||||
"webapp": "\f104",
|
"add-one-badge": "\f104",
|
||||||
"web": "\f105",
|
"add-user-filled": "\f105",
|
||||||
"warning": "\f106",
|
"add-user": "\f106",
|
||||||
"volume-3": "\f107",
|
"add": "\f107",
|
||||||
"volume-2": "\f108",
|
"admin": "\f108",
|
||||||
"volume-1": "\f109",
|
"ai-edit": "\f109",
|
||||||
"voice-chat": "\f10a",
|
"ai-fix": "\f10a",
|
||||||
"view-once": "\f10b",
|
"ai": "\f10b",
|
||||||
"video": "\f10c",
|
"allow-share": "\f10c",
|
||||||
"video-stop": "\f10d",
|
"allow-speak": "\f10d",
|
||||||
"video-outlined": "\f10e",
|
"animals": "\f10e",
|
||||||
"user": "\f10f",
|
"animations": "\f10f",
|
||||||
"user-tag": "\f110",
|
"archive-filled": "\f110",
|
||||||
"user-stars": "\f111",
|
"archive-from-main": "\f111",
|
||||||
"user-online": "\f112",
|
"archive-to-main": "\f112",
|
||||||
"user-filled": "\f113",
|
"archive": "\f113",
|
||||||
"up": "\f114",
|
"arrow-down-circle": "\f114",
|
||||||
"unread": "\f115",
|
"arrow-down": "\f115",
|
||||||
"unpin": "\f116",
|
"arrow-left": "\f116",
|
||||||
"unmute": "\f117",
|
"arrow-right": "\f117",
|
||||||
"unlock": "\f118",
|
"ask-support": "\f118",
|
||||||
"unlock-badge": "\f119",
|
"attach": "\f119",
|
||||||
"unlist": "\f11a",
|
"auction-drop": "\f11a",
|
||||||
"unlist-outline": "\f11b",
|
"auction-filled": "\f11b",
|
||||||
"unique-profile": "\f11c",
|
"auction-next-round": "\f11c",
|
||||||
"undo": "\f11d",
|
"auction": "\f11d",
|
||||||
"understood": "\f11e",
|
"author-hidden": "\f11e",
|
||||||
"underlined": "\f11f",
|
"avatar-archived-chats": "\f11f",
|
||||||
"unarchive": "\f120",
|
"avatar-deleted-account": "\f120",
|
||||||
"truck": "\f121",
|
"avatar-saved-messages": "\f121",
|
||||||
"transcribe": "\f122",
|
"bold": "\f122",
|
||||||
"trade": "\f123",
|
"boost-craft-chance": "\f123",
|
||||||
"topic-new": "\f124",
|
"boost-outline": "\f124",
|
||||||
"tools": "\f125",
|
"boost": "\f125",
|
||||||
"toncoin": "\f126",
|
"boostcircle": "\f126",
|
||||||
"timer": "\f127",
|
"boosts": "\f127",
|
||||||
"tag": "\f128",
|
"bot-command": "\f128",
|
||||||
"tag-name": "\f129",
|
"bot-commands-filled": "\f129",
|
||||||
"tag-filter": "\f12a",
|
"bots": "\f12a",
|
||||||
"tag-crossed": "\f12b",
|
"brush": "\f12b",
|
||||||
"tag-add": "\f12c",
|
"bug": "\f12c",
|
||||||
"strikethrough": "\f12d",
|
"calendar-filter": "\f12d",
|
||||||
"story-reply": "\f12e",
|
"calendar": "\f12e",
|
||||||
"story-priority": "\f12f",
|
"camera-add": "\f12f",
|
||||||
"story-expired": "\f130",
|
"camera": "\f130",
|
||||||
"story-caption": "\f131",
|
"car": "\f131",
|
||||||
"stop": "\f132",
|
"card": "\f132",
|
||||||
"stop-raising-hand": "\f133",
|
"cash-circle": "\f133",
|
||||||
"stickers": "\f134",
|
"channel-filled": "\f134",
|
||||||
"stealth-past": "\f135",
|
"channel": "\f135",
|
||||||
"stealth-future": "\f136",
|
"channelviews": "\f136",
|
||||||
"stats": "\f137",
|
"chat-badge": "\f137",
|
||||||
"stars-refund": "\f138",
|
"chats-badge": "\f138",
|
||||||
"stars-lock": "\f139",
|
"check-bold": "\f139",
|
||||||
"star": "\f13a",
|
"check": "\f13a",
|
||||||
"sport": "\f13b",
|
"clock-edit": "\f13b",
|
||||||
"spoiler": "\f13c",
|
"clock": "\f13c",
|
||||||
"spoiler-disable": "\f13d",
|
"close-circle": "\f13d",
|
||||||
"speaker": "\f13e",
|
"close-topic": "\f13e",
|
||||||
"speaker-story": "\f13f",
|
"close": "\f13f",
|
||||||
"speaker-outline": "\f140",
|
"closed-gift": "\f140",
|
||||||
"speaker-muted-story": "\f141",
|
"cloud-download": "\f141",
|
||||||
"sort": "\f142",
|
"collapse-modal": "\f142",
|
||||||
"sort-by-price": "\f143",
|
"collapse": "\f143",
|
||||||
"sort-by-number": "\f144",
|
"colorize": "\f144",
|
||||||
"sort-by-date": "\f145",
|
"combine-craft": "\f145",
|
||||||
"smile": "\f146",
|
"comments-sticker": "\f146",
|
||||||
"smallscreen": "\f147",
|
"comments": "\f147",
|
||||||
"skip-previous": "\f148",
|
"copy-media": "\f148",
|
||||||
"skip-next": "\f149",
|
"copy": "\f149",
|
||||||
"sidebar": "\f14a",
|
"craft": "\f14a",
|
||||||
"show-message": "\f14b",
|
"crop": "\f14b",
|
||||||
"share-screen": "\f14c",
|
"crown-take-off-outline": "\f14c",
|
||||||
"share-screen-stop": "\f14d",
|
"crown-take-off": "\f14d",
|
||||||
"share-screen-outlined": "\f14e",
|
"crown-wear-outline": "\f14e",
|
||||||
"share-filled": "\f14f",
|
"crown-wear": "\f14f",
|
||||||
"settings": "\f150",
|
"darkmode": "\f150",
|
||||||
"settings-filled": "\f151",
|
"data": "\f151",
|
||||||
"send": "\f152",
|
"delete-filled": "\f152",
|
||||||
"send-outline": "\f153",
|
"delete-left": "\f153",
|
||||||
"sell": "\f154",
|
"delete-user": "\f154",
|
||||||
"sell-outline": "\f155",
|
"delete": "\f155",
|
||||||
"select": "\f156",
|
"diamond": "\f156",
|
||||||
"select-filled": "\f157",
|
"document": "\f157",
|
||||||
"search": "\f158",
|
"double-badge": "\f158",
|
||||||
"sd-photo": "\f159",
|
"down": "\f159",
|
||||||
"scheduled": "\f15a",
|
"download": "\f15a",
|
||||||
"schedule": "\f15b",
|
"dropdown-arrows": "\f15b",
|
||||||
"saved-messages": "\f15c",
|
"eats": "\f15c",
|
||||||
"save-story": "\f15d",
|
"edit": "\f15d",
|
||||||
"rotate": "\f15e",
|
"email": "\f15e",
|
||||||
"revote": "\f15f",
|
"enter": "\f15f",
|
||||||
"revenue-split": "\f160",
|
"expand-modal": "\f160",
|
||||||
"reply": "\f161",
|
"expand": "\f161",
|
||||||
"reply-filled": "\f162",
|
"eye-crossed-outline": "\f162",
|
||||||
"replies": "\f163",
|
"eye-crossed": "\f163",
|
||||||
"replace": "\f164",
|
"eye-outline": "\f164",
|
||||||
"reorder-tabs": "\f165",
|
"eye": "\f165",
|
||||||
"reopen-topic": "\f166",
|
"favorite-filled": "\f166",
|
||||||
"remove": "\f167",
|
"favorite": "\f167",
|
||||||
"remove-quote": "\f168",
|
"file-badge": "\f168",
|
||||||
"reload": "\f169",
|
"flag": "\f169",
|
||||||
"refund": "\f16a",
|
"flip": "\f16a",
|
||||||
"redo": "\f16b",
|
"folder-badge": "\f16b",
|
||||||
"recent": "\f16c",
|
"folder-tabs-bot": "\f16c",
|
||||||
"readchats": "\f16d",
|
"folder-tabs-channel": "\f16d",
|
||||||
"radial-badge": "\f16e",
|
"folder-tabs-chat": "\f16e",
|
||||||
"quote": "\f16f",
|
"folder-tabs-chats": "\f16f",
|
||||||
"quote-text": "\f170",
|
"folder-tabs-folder": "\f170",
|
||||||
"proof-of-ownership": "\f171",
|
"folder-tabs-group": "\f171",
|
||||||
"privacy-policy": "\f172",
|
"folder-tabs-star": "\f172",
|
||||||
"previous": "\f173",
|
"folder-tabs-user": "\f173",
|
||||||
"poll": "\f174",
|
"folder": "\f174",
|
||||||
"play": "\f175",
|
"fontsize": "\f175",
|
||||||
"play-story": "\f176",
|
"forums": "\f176",
|
||||||
"pip": "\f177",
|
"forward": "\f177",
|
||||||
"pinned-message": "\f178",
|
"fragment": "\f178",
|
||||||
"pinned-chat": "\f179",
|
"frozen-time": "\f179",
|
||||||
"pin": "\f17a",
|
"fullscreen": "\f17a",
|
||||||
"pin-list": "\f17b",
|
"gifs": "\f17b",
|
||||||
"pin-badge": "\f17c",
|
"gift-transfer-inline": "\f17c",
|
||||||
"photo": "\f17d",
|
"gift": "\f17d",
|
||||||
"phone": "\f17e",
|
"group-filled": "\f17e",
|
||||||
"phone-discard": "\f17f",
|
"group": "\f17f",
|
||||||
"phone-discard-outline": "\f180",
|
"grouped-disable": "\f180",
|
||||||
"permissions": "\f181",
|
"grouped": "\f181",
|
||||||
"pause": "\f182",
|
"hand-stop-filled": "\f182",
|
||||||
"password-off": "\f183",
|
"hand-stop": "\f183",
|
||||||
"open-in-new-tab": "\f184",
|
"hashtag": "\f184",
|
||||||
"one-filled": "\f185",
|
"hd-photo": "\f185",
|
||||||
"note": "\f186",
|
"heart-outline": "\f186",
|
||||||
"non-contacts": "\f187",
|
"heart": "\f187",
|
||||||
"noise-suppression": "\f188",
|
"help": "\f188",
|
||||||
"nochannel": "\f189",
|
"info-filled": "\f189",
|
||||||
"no-share": "\f18a",
|
"info": "\f18a",
|
||||||
"no-download": "\f18b",
|
"install": "\f18b",
|
||||||
"next": "\f18c",
|
"italic": "\f18c",
|
||||||
"next-link": "\f18d",
|
"key": "\f18d",
|
||||||
"new-send": "\f18e",
|
"keyboard": "\f18e",
|
||||||
"new-chat-filled": "\f18f",
|
"lamp": "\f18f",
|
||||||
"my-notes": "\f190",
|
"language": "\f190",
|
||||||
"muted": "\f191",
|
"large-pause": "\f191",
|
||||||
"mute": "\f192",
|
"large-play": "\f192",
|
||||||
"move-caption-up": "\f193",
|
"link-badge": "\f193",
|
||||||
"move-caption-down": "\f194",
|
"link-broken": "\f194",
|
||||||
"more": "\f195",
|
"link": "\f195",
|
||||||
"more-circle": "\f196",
|
"location": "\f196",
|
||||||
"monospace": "\f197",
|
"lock-badge": "\f197",
|
||||||
"microphone": "\f198",
|
"lock": "\f198",
|
||||||
"microphone-alt": "\f199",
|
"logout": "\f199",
|
||||||
"message": "\f19a",
|
"loop": "\f19a",
|
||||||
"message-succeeded": "\f19b",
|
"mention": "\f19b",
|
||||||
"message-read": "\f19c",
|
"menu": "\f19c",
|
||||||
"message-pending": "\f19d",
|
"message-failed": "\f19d",
|
||||||
"message-failed": "\f19e",
|
"message-pending": "\f19e",
|
||||||
"menu": "\f19f",
|
"message-read": "\f19f",
|
||||||
"mention": "\f1a0",
|
"message-succeeded": "\f1a0",
|
||||||
"loop": "\f1a1",
|
"message": "\f1a1",
|
||||||
"logout": "\f1a2",
|
"microphone-alt": "\f1a2",
|
||||||
"lock": "\f1a3",
|
"microphone": "\f1a3",
|
||||||
"lock-badge": "\f1a4",
|
"monospace": "\f1a4",
|
||||||
"location": "\f1a5",
|
"more-circle": "\f1a5",
|
||||||
"link": "\f1a6",
|
"more": "\f1a6",
|
||||||
"link-broken": "\f1a7",
|
"move-caption-down": "\f1a7",
|
||||||
"link-badge": "\f1a8",
|
"move-caption-up": "\f1a8",
|
||||||
"large-play": "\f1a9",
|
"mute": "\f1a9",
|
||||||
"large-pause": "\f1aa",
|
"muted": "\f1aa",
|
||||||
"language": "\f1ab",
|
"my-notes": "\f1ab",
|
||||||
"lamp": "\f1ac",
|
"new-chat-filled": "\f1ac",
|
||||||
"keyboard": "\f1ad",
|
"new-send": "\f1ad",
|
||||||
"key": "\f1ae",
|
"next-link": "\f1ae",
|
||||||
"italic": "\f1af",
|
"next": "\f1af",
|
||||||
"install": "\f1b0",
|
"no-download": "\f1b0",
|
||||||
"info": "\f1b1",
|
"no-share": "\f1b1",
|
||||||
"info-filled": "\f1b2",
|
"nochannel": "\f1b2",
|
||||||
"help": "\f1b3",
|
"noise-suppression": "\f1b3",
|
||||||
"heart": "\f1b4",
|
"non-contacts": "\f1b4",
|
||||||
"heart-outline": "\f1b5",
|
"note": "\f1b5",
|
||||||
"hd-photo": "\f1b6",
|
"one-filled": "\f1b6",
|
||||||
"hashtag": "\f1b7",
|
"open-in-new-tab": "\f1b7",
|
||||||
"hand-stop": "\f1b8",
|
"password-off": "\f1b8",
|
||||||
"hand-stop-filled": "\f1b9",
|
"pause": "\f1b9",
|
||||||
"grouped": "\f1ba",
|
"permissions": "\f1ba",
|
||||||
"grouped-disable": "\f1bb",
|
"phone-discard-outline": "\f1bb",
|
||||||
"group": "\f1bc",
|
"phone-discard": "\f1bc",
|
||||||
"group-filled": "\f1bd",
|
"phone": "\f1bd",
|
||||||
"gift": "\f1be",
|
"photo": "\f1be",
|
||||||
"gift-transfer-inline": "\f1bf",
|
"pin-badge": "\f1bf",
|
||||||
"gifs": "\f1c0",
|
"pin-list": "\f1c0",
|
||||||
"fullscreen": "\f1c1",
|
"pin": "\f1c1",
|
||||||
"frozen-time": "\f1c2",
|
"pinned-chat": "\f1c2",
|
||||||
"fragment": "\f1c3",
|
"pinned-message": "\f1c3",
|
||||||
"forward": "\f1c4",
|
"pip": "\f1c4",
|
||||||
"forums": "\f1c5",
|
"play-story": "\f1c5",
|
||||||
"fontsize": "\f1c6",
|
"play": "\f1c6",
|
||||||
"folder": "\f1c7",
|
"poll": "\f1c7",
|
||||||
"folder-badge": "\f1c8",
|
"previous-link": "\f1c8",
|
||||||
"flip": "\f1c9",
|
"previous": "\f1c9",
|
||||||
"flag": "\f1ca",
|
"privacy-policy": "\f1ca",
|
||||||
"file-badge": "\f1cb",
|
"proof-of-ownership": "\f1cb",
|
||||||
"favorite": "\f1cc",
|
"quote-text": "\f1cc",
|
||||||
"favorite-filled": "\f1cd",
|
"quote": "\f1cd",
|
||||||
"eye": "\f1ce",
|
"radial-badge": "\f1ce",
|
||||||
"eye-outline": "\f1cf",
|
"rating-icons-level1": "\f1cf",
|
||||||
"eye-crossed": "\f1d0",
|
"rating-icons-level10": "\f1d0",
|
||||||
"eye-crossed-outline": "\f1d1",
|
"rating-icons-level2": "\f1d1",
|
||||||
"expand": "\f1d2",
|
"rating-icons-level20": "\f1d2",
|
||||||
"expand-modal": "\f1d3",
|
"rating-icons-level3": "\f1d3",
|
||||||
"enter": "\f1d4",
|
"rating-icons-level30": "\f1d4",
|
||||||
"email": "\f1d5",
|
"rating-icons-level4": "\f1d5",
|
||||||
"edit": "\f1d6",
|
"rating-icons-level40": "\f1d6",
|
||||||
"eats": "\f1d7",
|
"rating-icons-level5": "\f1d7",
|
||||||
"dropdown-arrows": "\f1d8",
|
"rating-icons-level50": "\f1d8",
|
||||||
"download": "\f1d9",
|
"rating-icons-level6": "\f1d9",
|
||||||
"down": "\f1da",
|
"rating-icons-level60": "\f1da",
|
||||||
"double-badge": "\f1db",
|
"rating-icons-level7": "\f1db",
|
||||||
"document": "\f1dc",
|
"rating-icons-level70": "\f1dc",
|
||||||
"diamond": "\f1dd",
|
"rating-icons-level8": "\f1dd",
|
||||||
"delete": "\f1de",
|
"rating-icons-level80": "\f1de",
|
||||||
"delete-user": "\f1df",
|
"rating-icons-level9": "\f1df",
|
||||||
"delete-left": "\f1e0",
|
"rating-icons-level90": "\f1e0",
|
||||||
"delete-filled": "\f1e1",
|
"rating-icons-negative": "\f1e1",
|
||||||
"data": "\f1e2",
|
"readchats": "\f1e2",
|
||||||
"darkmode": "\f1e3",
|
"recent": "\f1e3",
|
||||||
"crown-wear": "\f1e4",
|
"redo": "\f1e4",
|
||||||
"crown-wear-outline": "\f1e5",
|
"refund": "\f1e5",
|
||||||
"crown-take-off": "\f1e6",
|
"reload": "\f1e6",
|
||||||
"crown-take-off-outline": "\f1e7",
|
"remove-quote": "\f1e7",
|
||||||
"crop": "\f1e8",
|
"remove": "\f1e8",
|
||||||
"craft": "\f1e9",
|
"reopen-topic": "\f1e9",
|
||||||
"copy": "\f1ea",
|
"reorder-tabs": "\f1ea",
|
||||||
"copy-media": "\f1eb",
|
"replace": "\f1eb",
|
||||||
"comments": "\f1ec",
|
"replies": "\f1ec",
|
||||||
"comments-sticker": "\f1ed",
|
"reply-filled": "\f1ed",
|
||||||
"combine-craft": "\f1ee",
|
"reply": "\f1ee",
|
||||||
"colorize": "\f1ef",
|
"revenue-split": "\f1ef",
|
||||||
"collapse": "\f1f0",
|
"revote": "\f1f0",
|
||||||
"collapse-modal": "\f1f1",
|
"rotate": "\f1f1",
|
||||||
"cloud-download": "\f1f2",
|
"save-story": "\f1f2",
|
||||||
"closed-gift": "\f1f3",
|
"saved-messages": "\f1f3",
|
||||||
"close": "\f1f4",
|
"schedule": "\f1f4",
|
||||||
"close-topic": "\f1f5",
|
"scheduled": "\f1f5",
|
||||||
"close-circle": "\f1f6",
|
"sd-photo": "\f1f6",
|
||||||
"clock": "\f1f7",
|
"search": "\f1f7",
|
||||||
"clock-edit": "\f1f8",
|
"select-filled": "\f1f8",
|
||||||
"check": "\f1f9",
|
"select": "\f1f9",
|
||||||
"check-bold": "\f1fa",
|
"sell-outline": "\f1fa",
|
||||||
"chats-badge": "\f1fb",
|
"sell": "\f1fb",
|
||||||
"chat-badge": "\f1fc",
|
"send-outline": "\f1fc",
|
||||||
"channelviews": "\f1fd",
|
"send": "\f1fd",
|
||||||
"channel": "\f1fe",
|
"settings-filled": "\f1fe",
|
||||||
"channel-filled": "\f1ff",
|
"settings": "\f1ff",
|
||||||
"cash-circle": "\f200",
|
"share-filled": "\f200",
|
||||||
"card": "\f201",
|
"share-screen-outlined": "\f201",
|
||||||
"car": "\f202",
|
"share-screen-stop": "\f202",
|
||||||
"camera": "\f203",
|
"share-screen": "\f203",
|
||||||
"camera-add": "\f204",
|
"show-message": "\f204",
|
||||||
"calendar": "\f205",
|
"sidebar": "\f205",
|
||||||
"calendar-filter": "\f206",
|
"skip-next": "\f206",
|
||||||
"bug": "\f207",
|
"skip-previous": "\f207",
|
||||||
"brush": "\f208",
|
"smallscreen": "\f208",
|
||||||
"bots": "\f209",
|
"smile": "\f209",
|
||||||
"bot-commands-filled": "\f20a",
|
"sort-by-date": "\f20a",
|
||||||
"bot-command": "\f20b",
|
"sort-by-number": "\f20b",
|
||||||
"boosts": "\f20c",
|
"sort-by-price": "\f20c",
|
||||||
"boostcircle": "\f20d",
|
"sort": "\f20d",
|
||||||
"boost": "\f20e",
|
"speaker-muted-story": "\f20e",
|
||||||
"boost-outline": "\f20f",
|
"speaker-outline": "\f20f",
|
||||||
"boost-craft-chance": "\f210",
|
"speaker-story": "\f210",
|
||||||
"bold": "\f211",
|
"speaker": "\f211",
|
||||||
"avatar-saved-messages": "\f212",
|
"spoiler-disable": "\f212",
|
||||||
"avatar-deleted-account": "\f213",
|
"spoiler": "\f213",
|
||||||
"avatar-archived-chats": "\f214",
|
"sport": "\f214",
|
||||||
"author-hidden": "\f215",
|
"star": "\f215",
|
||||||
"auction": "\f216",
|
"stars-lock": "\f216",
|
||||||
"auction-next-round": "\f217",
|
"stars-refund": "\f217",
|
||||||
"auction-filled": "\f218",
|
"stats": "\f218",
|
||||||
"auction-drop": "\f219",
|
"stealth-future": "\f219",
|
||||||
"attach": "\f21a",
|
"stealth-past": "\f21a",
|
||||||
"ask-support": "\f21b",
|
"stickers": "\f21b",
|
||||||
"arrow-right": "\f21c",
|
"stop-raising-hand": "\f21c",
|
||||||
"arrow-left": "\f21d",
|
"stop": "\f21d",
|
||||||
"arrow-down": "\f21e",
|
"story-caption": "\f21e",
|
||||||
"arrow-down-circle": "\f21f",
|
"story-expired": "\f21f",
|
||||||
"archive": "\f220",
|
"story-priority": "\f220",
|
||||||
"archive-to-main": "\f221",
|
"story-reply": "\f221",
|
||||||
"archive-from-main": "\f222",
|
"strikethrough": "\f222",
|
||||||
"archive-filled": "\f223",
|
"tag-add": "\f223",
|
||||||
"animations": "\f224",
|
"tag-crossed": "\f224",
|
||||||
"animals": "\f225",
|
"tag-filter": "\f225",
|
||||||
"allow-speak": "\f226",
|
"tag-name": "\f226",
|
||||||
"allow-share": "\f227",
|
"tag": "\f227",
|
||||||
"ai": "\f228",
|
"timer": "\f228",
|
||||||
"ai-fix": "\f229",
|
"toncoin": "\f229",
|
||||||
"ai-edit": "\f22a",
|
"tools": "\f22a",
|
||||||
"admin": "\f22b",
|
"topic-new": "\f22b",
|
||||||
"add": "\f22c",
|
"trade": "\f22c",
|
||||||
"add-user": "\f22d",
|
"transcribe": "\f22d",
|
||||||
"add-user-filled": "\f22e",
|
"truck": "\f22e",
|
||||||
"add-one-badge": "\f22f",
|
"unarchive": "\f22f",
|
||||||
"add-filled": "\f230",
|
"underlined": "\f230",
|
||||||
"add-caption": "\f231",
|
"understood": "\f231",
|
||||||
"active-sessions": "\f232",
|
"undo": "\f232",
|
||||||
"rating-icons-negative": "\f233",
|
"unique-profile": "\f233",
|
||||||
"rating-icons-level90": "\f234",
|
"unlist-outline": "\f234",
|
||||||
"rating-icons-level9": "\f235",
|
"unlist": "\f235",
|
||||||
"rating-icons-level80": "\f236",
|
"unlock-badge": "\f236",
|
||||||
"rating-icons-level8": "\f237",
|
"unlock": "\f237",
|
||||||
"rating-icons-level70": "\f238",
|
"unmute": "\f238",
|
||||||
"rating-icons-level7": "\f239",
|
"unpin": "\f239",
|
||||||
"rating-icons-level60": "\f23a",
|
"unread": "\f23a",
|
||||||
"rating-icons-level6": "\f23b",
|
"up": "\f23b",
|
||||||
"rating-icons-level50": "\f23c",
|
"user-filled": "\f23c",
|
||||||
"rating-icons-level5": "\f23d",
|
"user-online": "\f23d",
|
||||||
"rating-icons-level40": "\f23e",
|
"user-stars": "\f23e",
|
||||||
"rating-icons-level4": "\f23f",
|
"user-tag": "\f23f",
|
||||||
"rating-icons-level30": "\f240",
|
"user": "\f240",
|
||||||
"rating-icons-level3": "\f241",
|
"video-outlined": "\f241",
|
||||||
"rating-icons-level20": "\f242",
|
"video-stop": "\f242",
|
||||||
"rating-icons-level2": "\f243",
|
"video": "\f243",
|
||||||
"rating-icons-level10": "\f244",
|
"view-once": "\f244",
|
||||||
"rating-icons-level1": "\f245",
|
"voice-chat": "\f245",
|
||||||
"folder-tabs-user": "\f246",
|
"volume-1": "\f246",
|
||||||
"folder-tabs-star": "\f247",
|
"volume-2": "\f247",
|
||||||
"folder-tabs-group": "\f248",
|
"volume-3": "\f248",
|
||||||
"folder-tabs-folder": "\f249",
|
"warning": "\f249",
|
||||||
"folder-tabs-chats": "\f24a",
|
"web": "\f24a",
|
||||||
"folder-tabs-chat": "\f24b",
|
"webapp": "\f24b",
|
||||||
"folder-tabs-channel": "\f24c",
|
"word-wrap": "\f24c",
|
||||||
"folder-tabs-bot": "\f24d",
|
"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;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.next-arrow-icon {
|
.link-arrow-icon {
|
||||||
font-size: var(--next-arrow-size, 0.5625em);
|
font-size: var(--next-arrow-size, 0.5625em);
|
||||||
font-weight: var(--next-arrow-weight, inherit);
|
font-weight: var(--next-arrow-weight, inherit);
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-canvas-container {
|
.shared-canvas-container {
|
||||||
|
|||||||
@ -1,334 +1,335 @@
|
|||||||
export type FontIconName =
|
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'
|
| 'active-sessions'
|
||||||
| 'rating-icons-negative'
|
| 'add-caption'
|
||||||
| 'rating-icons-level90'
|
| 'add-filled'
|
||||||
| 'rating-icons-level9'
|
| 'add-one-badge'
|
||||||
| 'rating-icons-level80'
|
| 'add-user-filled'
|
||||||
| 'rating-icons-level8'
|
| 'add-user'
|
||||||
| 'rating-icons-level70'
|
| 'add'
|
||||||
| 'rating-icons-level7'
|
| 'admin'
|
||||||
| 'rating-icons-level60'
|
| 'ai-edit'
|
||||||
| 'rating-icons-level6'
|
| 'ai-fix'
|
||||||
| 'rating-icons-level50'
|
| 'ai'
|
||||||
| 'rating-icons-level5'
|
| 'allow-share'
|
||||||
| 'rating-icons-level40'
|
| 'allow-speak'
|
||||||
| 'rating-icons-level4'
|
| 'animals'
|
||||||
| 'rating-icons-level30'
|
| 'animations'
|
||||||
| 'rating-icons-level3'
|
| 'archive-filled'
|
||||||
| 'rating-icons-level20'
|
| 'archive-from-main'
|
||||||
| 'rating-icons-level2'
|
| 'archive-to-main'
|
||||||
| 'rating-icons-level10'
|
| 'archive'
|
||||||
| 'rating-icons-level1'
|
| 'arrow-down-circle'
|
||||||
| 'folder-tabs-user'
|
| 'arrow-down'
|
||||||
| 'folder-tabs-star'
|
| 'arrow-left'
|
||||||
| 'folder-tabs-group'
|
| 'arrow-right'
|
||||||
| 'folder-tabs-folder'
|
| 'ask-support'
|
||||||
| 'folder-tabs-chats'
|
| 'attach'
|
||||||
| 'folder-tabs-chat'
|
| '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-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,
|
StarsTransaction,
|
||||||
PreviewMedia,
|
PreviewMedia,
|
||||||
SponsoredMessage,
|
SponsoredMessage,
|
||||||
|
PollPreview,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StoryViewerOrigin {
|
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;
|
'CallAgain': undefined;
|
||||||
'CallBack': undefined;
|
'CallBack': undefined;
|
||||||
'PollSubmitVotes': undefined;
|
'PollSubmitVotes': undefined;
|
||||||
|
'PollSubmitAnswers': undefined;
|
||||||
'PollViewResults': undefined;
|
'PollViewResults': undefined;
|
||||||
|
'PollBackToVote': undefined;
|
||||||
|
'PollBackToAnswer': undefined;
|
||||||
'ChatQuizTotalVotesEmpty': undefined;
|
'ChatQuizTotalVotesEmpty': undefined;
|
||||||
'ChatPollTotalVotesResultEmpty': undefined;
|
'ChatPollTotalVotesResultEmpty': undefined;
|
||||||
'Vote': undefined;
|
'Vote': undefined;
|
||||||
@ -1403,6 +1406,7 @@ export interface LangPair {
|
|||||||
'StarsSubscribeInfoLinkText': undefined;
|
'StarsSubscribeInfoLinkText': undefined;
|
||||||
'StarsSubscribeInfoLink': undefined;
|
'StarsSubscribeInfoLink': undefined;
|
||||||
'StarsBalance': undefined;
|
'StarsBalance': undefined;
|
||||||
|
'OpenMapWith': undefined;
|
||||||
'OpenApp': undefined;
|
'OpenApp': undefined;
|
||||||
'PopularApps': undefined;
|
'PopularApps': undefined;
|
||||||
'SearchApps': undefined;
|
'SearchApps': undefined;
|
||||||
@ -2284,6 +2288,15 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'time': V;
|
'time': V;
|
||||||
'duration': V;
|
'duration': V;
|
||||||
};
|
};
|
||||||
|
'PollEndsTime': {
|
||||||
|
'time': V;
|
||||||
|
};
|
||||||
|
'PollResultsTime': {
|
||||||
|
'time': V;
|
||||||
|
};
|
||||||
|
'TimeIn': {
|
||||||
|
'time': V;
|
||||||
|
};
|
||||||
'MessageScheduledOn': {
|
'MessageScheduledOn': {
|
||||||
'date': V;
|
'date': V;
|
||||||
};
|
};
|
||||||
@ -3428,6 +3441,20 @@ export interface LangPairWithVariables<V = LangVariable> {
|
|||||||
'tasks': V;
|
'tasks': V;
|
||||||
'list': V;
|
'list': V;
|
||||||
};
|
};
|
||||||
|
'MessageActionPollAppendAnswer': {
|
||||||
|
'peer': V;
|
||||||
|
'option': V;
|
||||||
|
};
|
||||||
|
'MessageActionPollAppendAnswerYou': {
|
||||||
|
'option': V;
|
||||||
|
};
|
||||||
|
'MessageActionPollDeleteAnswer': {
|
||||||
|
'peer': V;
|
||||||
|
'option': V;
|
||||||
|
};
|
||||||
|
'MessageActionPollDeleteAnswerYou': {
|
||||||
|
'option': V;
|
||||||
|
};
|
||||||
'GiftInfoCollectibleBy': {
|
'GiftInfoCollectibleBy': {
|
||||||
'number': V;
|
'number': V;
|
||||||
'owner': V;
|
'owner': V;
|
||||||
@ -3724,9 +3751,21 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
|||||||
'ConversationContextMenuSeen': {
|
'ConversationContextMenuSeen': {
|
||||||
'count': V;
|
'count': V;
|
||||||
};
|
};
|
||||||
|
'PollVoteCountButton': {
|
||||||
|
'count': V;
|
||||||
|
};
|
||||||
|
'PollAnswerCountButton': {
|
||||||
|
'count': V;
|
||||||
|
};
|
||||||
'Answer': {
|
'Answer': {
|
||||||
'count': V;
|
'count': V;
|
||||||
};
|
};
|
||||||
|
'PollAnsweredCount': {
|
||||||
|
'count': V;
|
||||||
|
};
|
||||||
|
'VoteCount': {
|
||||||
|
'count': V;
|
||||||
|
};
|
||||||
'VoiceOverChatMessagesSelected': {
|
'VoiceOverChatMessagesSelected': {
|
||||||
'count': V;
|
'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[] {
|
export function unique<T>(array: T[]): T[] {
|
||||||
return Array.from(new Set(array));
|
return Array.from(new Set(array));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -153,6 +153,24 @@ export function formatClockDuration(duration: number) {
|
|||||||
return string;
|
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) {
|
function buildAbsoluteFormatterOptions(lang: LangFn, options: FormatDateTimeOptions) {
|
||||||
const dateStyle = options.date ?? false;
|
const dateStyle = options.date ?? false;
|
||||||
const timeStyle = options.time ?? false;
|
const timeStyle = options.time ?? false;
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import buildClassName from '../buildClassName';
|
|||||||
import Icon from '../../components/common/icons/Icon';
|
import Icon from '../../components/common/icons/Icon';
|
||||||
import StarIcon from '../../components/common/icons/StarIcon';
|
import StarIcon from '../../components/common/icons/StarIcon';
|
||||||
|
|
||||||
export function getNextArrowReplacement() {
|
export const NEXT_ARROW_REPLACEMENT = {
|
||||||
return {
|
'>': <Icon name="next-link" className="link-arrow-icon" />,
|
||||||
'>': <Icon name="next-link" className="next-arrow-icon" />,
|
};
|
||||||
};
|
export const PREVIOUS_ARROW_REPLACEMENT = {
|
||||||
}
|
'<': <Icon name="previous-link" className="link-arrow-icon" />,
|
||||||
|
};
|
||||||
|
|
||||||
export function formatStarsAsText(lang: LangFn, amount: number) {
|
export function formatStarsAsText(lang: LangFn, amount: number) {
|
||||||
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
||||||
|
|||||||
@ -24,7 +24,8 @@
|
|||||||
"jsxImportSource": "@teact",
|
"jsxImportSource": "@teact",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@teact": ["./src/lib/teact/teact.ts"],
|
"@teact": ["./src/lib/teact/teact.ts"],
|
||||||
"@teact/*": ["./src/lib/teact/*"]
|
"@teact/*": ["./src/lib/teact/*"],
|
||||||
|
"@gili/*": ["./src/components/gili/*"]
|
||||||
},
|
},
|
||||||
"types": [
|
"types": [
|
||||||
"dom-chromium-ai",
|
"dom-chromium-ai",
|
||||||
|
|||||||
@ -183,6 +183,7 @@ export default function createConfig(
|
|||||||
alias: {
|
alias: {
|
||||||
'@teact$': path.resolve(__dirname, './src/lib/teact/teact.ts'),
|
'@teact$': path.resolve(__dirname, './src/lib/teact/teact.ts'),
|
||||||
'@teact': path.resolve(__dirname, './src/lib/teact'),
|
'@teact': path.resolve(__dirname, './src/lib/teact'),
|
||||||
|
'@gili': path.resolve(__dirname, './src/components/gili'),
|
||||||
},
|
},
|
||||||
fallback: {
|
fallback: {
|
||||||
path: require.resolve('path-browserify'),
|
path: require.resolve('path-browserify'),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user