Polls: Redesign modal (#6886)
This commit is contained in:
parent
6ceb7b6573
commit
a2d6d63853
@ -27,6 +27,7 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
|
|||||||
- **Always extract styles to files** - avoid inline styles unless absolutely necessary.
|
- **Always extract styles to files** - avoid inline styles unless absolutely necessary.
|
||||||
- **If file already imports styles**, check where they come from and add new styles there - don't create new style files.
|
- **If file already imports styles**, check where they come from and add new styles there - don't create new style files.
|
||||||
- Prefer rem units for all measurements. Exceptions are possible, but usually rare.
|
- Prefer rem units for all measurements. Exceptions are possible, but usually rare.
|
||||||
|
- No complex or broad selectors. Prefer basic classes.
|
||||||
|
|
||||||
- **Code Style:**
|
- **Code Style:**
|
||||||
- Early returns.
|
- Early returns.
|
||||||
@ -176,7 +177,7 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
|
|||||||
* **StateProps**: data injected by `withGlobal` HOC
|
* **StateProps**: data injected by `withGlobal` HOC
|
||||||
* 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 handlers or functions *last* in your props definitions.
|
||||||
* Do not pass unmemoized objects as props into memo() components.
|
* Do not pass unmemoized objects as props into memo() components.
|
||||||
|
|
||||||
### 3. Hooks
|
### 3. Hooks
|
||||||
|
|||||||
@ -41,8 +41,8 @@
|
|||||||
"npm": "^10.8 || ^11"
|
"npm": "^10.8 || ^11"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js}": "eslint --fix",
|
"*.{ts,tsx,js}": "eslint --cache --cache-location .cache/.eslintcache --fix",
|
||||||
"*.{css,scss}": "stylelint --fix"
|
"*.{css,scss}": "stylelint --cache --cache-location .cache/.stylelintcache --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.29.0",
|
"@babel/core": "^7.29.0",
|
||||||
|
|||||||
@ -118,6 +118,7 @@ export interface GramJsAppConfig extends LimitsConfig {
|
|||||||
ton_usd_rate?: number;
|
ton_usd_rate?: number;
|
||||||
ton_topup_url?: string;
|
ton_topup_url?: string;
|
||||||
poll_answers_max?: number;
|
poll_answers_max?: number;
|
||||||
|
poll_close_period_max?: number;
|
||||||
todo_items_max?: number;
|
todo_items_max?: number;
|
||||||
todo_title_length_max?: number;
|
todo_title_length_max?: number;
|
||||||
todo_item_length_max?: number;
|
todo_item_length_max?: number;
|
||||||
@ -266,6 +267,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
|||||||
tonUsdRate: appConfig.ton_usd_rate,
|
tonUsdRate: appConfig.ton_usd_rate,
|
||||||
tonTopupUrl: appConfig.ton_topup_url,
|
tonTopupUrl: appConfig.ton_topup_url,
|
||||||
pollMaxAnswers: appConfig.poll_answers_max,
|
pollMaxAnswers: appConfig.poll_answers_max,
|
||||||
|
pollClosePeriodMax: appConfig.poll_close_period_max,
|
||||||
todoItemsMax: appConfig.todo_items_max,
|
todoItemsMax: appConfig.todo_items_max,
|
||||||
todoTitleLengthMax: appConfig.todo_title_length_max,
|
todoTitleLengthMax: appConfig.todo_title_length_max,
|
||||||
todoItemLengthMax: appConfig.todo_item_length_max,
|
todoItemLengthMax: appConfig.todo_item_length_max,
|
||||||
|
|||||||
@ -235,18 +235,24 @@ export function buildInputPoll(
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
quiz: poll.isQuiz,
|
quiz: poll.isQuiz,
|
||||||
|
closeDate: poll.closeDate,
|
||||||
|
closePeriod: poll.closePeriod,
|
||||||
|
hideResultsUntilClose: poll.shouldHideResultsUntilClose,
|
||||||
|
revotingDisabled: poll.isRevoteDisabled,
|
||||||
|
shuffleAnswers: poll.shouldShuffleAnswers,
|
||||||
|
openAnswers: poll.canAddAnswers,
|
||||||
multipleChoice: poll.isMultipleChoice,
|
multipleChoice: poll.isMultipleChoice,
|
||||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
hash: DEFAULT_PRIMITIVES.BIGINT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputSolutionEntities = solutionEntities?.map(buildMtpMessageEntity);
|
const inputSolutionEntities = solutionEntities?.map(buildMtpMessageEntity) || [];
|
||||||
|
|
||||||
return new GramJs.InputMediaPoll({
|
return new GramJs.InputMediaPoll({
|
||||||
poll: inputPoll,
|
poll: inputPoll,
|
||||||
correctAnswers,
|
correctAnswers,
|
||||||
attachedMedia: media?.attachedMedia,
|
attachedMedia: media?.attachedMedia,
|
||||||
solution,
|
solution,
|
||||||
solutionEntities: inputSolutionEntities,
|
solutionEntities: solution ? inputSolutionEntities : undefined,
|
||||||
solutionMedia: media?.solutionMedia,
|
solutionMedia: media?.solutionMedia,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1877,6 +1877,24 @@ export async function sendPollVote({
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function appendPollAnswer({
|
||||||
|
chat, messageId, text,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
messageId: number;
|
||||||
|
text: string;
|
||||||
|
}) {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
await invokeRequest(new GramJs.messages.AddPollAnswer({
|
||||||
|
peer: buildInputPeer(id, accessHash),
|
||||||
|
msgId: messageId,
|
||||||
|
answer: new GramJs.InputPollAnswer({
|
||||||
|
text: buildInputTextWithEntities({ text }),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export async function toggleTodoCompleted({
|
export async function toggleTodoCompleted({
|
||||||
chat, messageId, completedIds, incompletedIds,
|
chat, messageId, completedIds, incompletedIds,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -320,7 +320,8 @@ export interface ApiAppConfig {
|
|||||||
tonStargiftResaleCommissionPermille?: number;
|
tonStargiftResaleCommissionPermille?: number;
|
||||||
tonUsdRate?: number;
|
tonUsdRate?: number;
|
||||||
tonTopupUrl: string;
|
tonTopupUrl: string;
|
||||||
pollMaxAnswers?: number;
|
pollMaxAnswers: number;
|
||||||
|
pollClosePeriodMax: number;
|
||||||
todoItemsMax: number;
|
todoItemsMax: number;
|
||||||
todoTitleLengthMax: number;
|
todoTitleLengthMax: number;
|
||||||
todoItemLengthMax: number;
|
todoItemLengthMax: number;
|
||||||
|
|||||||
1
src/assets/font-icons/check-filled.svg
Normal file
1
src/assets/font-icons/check-filled.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M22.622 1.003H9.381A8.38 8.38 0 0 0 1 9.383v13.24a8.38 8.38 0 0 0 8.38 8.38h13.24a8.38 8.38 0 0 0 8.38-8.38V9.38A8.38 8.38 0 0 0 22.62 1m.94 9.8-8.668 12.386a.792.792 0 0 1-1.209.104l-5.078-5.077a1.478 1.478 0 1 1 2.09-2.091l3.254 3.256 7.19-10.272a1.478 1.478 0 1 1 2.42 1.695"/></svg>
|
||||||
|
After Width: | Height: | Size: 359 B |
1
src/assets/font-icons/choice-selected.svg
Normal file
1
src/assets/font-icons/choice-selected.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="11.497" height="11.497" x="1" y="4.834" rx="1.791" ry="1.791"/><rect width="8.069" height="8.069" x="4.428" y="19.272" rx="1.791" ry="1.791"/><rect width="9.798" height="9.798" x="15.67" y="19.272" rx="1.791" ry="1.791"/><path d="M30.21 0H17.46c-.984 0-1.79.806-1.79 1.79v12.748c0 .985.806 1.79 1.79 1.79h12.75c.984 0 1.79-.805 1.79-1.79V1.791C32 .806 31.194 0 30.21 0m-.804 4.857-5.993 8.56a.94.94 0 0 1-1.435.127L18.43 9.997a1.492 1.492 0 1 1 2.11-2.11l1.826 1.823 4.596-6.564a1.492 1.492 0 1 1 2.444 1.711"/></svg>
|
||||||
|
After Width: | Height: | Size: 593 B |
1
src/assets/font-icons/replace-round.svg
Normal file
1
src/assets/font-icons/replace-round.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><circle cx="7.87" cy="7.87" r="6.87"/><path d="M21.206 4.129a2.56 2.56 0 0 1 2.557 2.558v3.061l-2.237-2.237a1.05 1.05 0 0 0-1.49 0l-.453.454a1.05 1.05 0 0 0 0 1.49l4.847 4.847a1.003 1.003 0 0 0 1.415 0l4.846-4.848a1.05 1.05 0 0 0 0-1.49l-.454-.453a1.05 1.05 0 0 0-1.489 0l-2.237 2.237V6.687a5.304 5.304 0 0 0-5.305-5.305h-3.443a1.054 1.054 0 0 0-1.054 1.052v.642c0 .583.471 1.054 1.054 1.054z"/><circle cx="24.13" cy="24.212" r="6.87"/><path d="M14.237 27.954h-3.443a2.556 2.556 0 0 1-2.557-2.559v-3.06l2.237 2.236a1.05 1.05 0 0 0 1.49 0l.453-.453a1.05 1.05 0 0 0 0-1.49L7.57 17.781a1.003 1.003 0 0 0-1.415 0L1.31 22.628a1.05 1.05 0 0 0 0 1.49l.454.453a1.05 1.05 0 0 0 1.489 0l2.237-2.237v3.061a5.304 5.304 0 0 0 5.305 5.305h3.443a1.054 1.054 0 0 0 1.054-1.052v-.642a1.054 1.054 0 0 0-1.054-1.054"/></svg>
|
||||||
|
After Width: | Height: | Size: 868 B |
1
src/assets/font-icons/timer-filled.svg
Normal file
1
src/assets/font-icons/timer-filled.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="m26.432 9.472 1.757-1.757a1.362 1.362 0 1 0-1.926-1.925l-1.724 1.724a13.2 13.2 0 0 0-7.177-3.04v-2.11a1.362 1.362 0 1 0-2.724 0v2.11c-6.704.68-11.937 6.345-11.937 13.23C2.701 25.045 8.655 31 16 31s13.299-5.954 13.299-13.297c0-3.112-1.078-5.965-2.867-8.231M16 20.154a2.451 2.451 0 0 1-1.362-4.488v-6.04a1.362 1.362 0 1 1 2.724 0v6.038A2.451 2.451 0 0 1 16 20.154"/></svg>
|
||||||
|
After Width: | Height: | Size: 442 B |
@ -685,17 +685,30 @@
|
|||||||
"Message" = "Message";
|
"Message" = "Message";
|
||||||
"RecentStickers" = "Recently Used";
|
"RecentStickers" = "Recently Used";
|
||||||
"PollsChooseQuestion" = "Please enter a question.";
|
"PollsChooseQuestion" = "Please enter a question.";
|
||||||
"PollsChooseAnswers" = "Please enter at least two options.";
|
"PollsChooseAnswers" = "Please enter at least one option.";
|
||||||
"NewPoll" = "New Poll";
|
"NewPoll" = "New Poll";
|
||||||
"Create" = "Create";
|
"Create" = "Create";
|
||||||
"OptionHint" = "Option";
|
"OptionHint" = "Option";
|
||||||
"CreatePollAddOption" = "Add an Option";
|
"CreatePollAddOption" = "Add an Option";
|
||||||
"PollsChooseCorrect" = "Please choose the correct answer.";
|
"PollsChooseCorrect" = "Please choose the correct answer.";
|
||||||
"AskAQuestion" = "Ask a Question";
|
"AskAQuestion" = "Ask a Question";
|
||||||
"PollOptions" = "Poll options";
|
"PollAnswersVisible" = "Show Who Voted";
|
||||||
"PollAnonymous" = "Anonymous Poll";
|
"PollAnswersVisibleDescription" = "Display voter name on each option";
|
||||||
"PollMultiple" = "Multiple Answers";
|
"PollMultiple" = "Allow Multiple Answers";
|
||||||
"PollQuiz" = "Quiz Mode";
|
"PollMultipleDescription" = "Voters can select more than one option";
|
||||||
|
"PollQuiz" = "Set Correct Answer";
|
||||||
|
"PollQuizDescription" = "Mark one or more options as the right answer";
|
||||||
|
"PollAllowAddingAnswers" = "Allow Adding Options";
|
||||||
|
"PollAllowAddingAnswersDescription" = "Participants can suggest new options";
|
||||||
|
"PollAllowVoteChanges" = "Allow Revoting";
|
||||||
|
"PollAllowVoteChangesDescription" = "Voters can change their vote";
|
||||||
|
"PollRandomOrder" = "Shuffle Options";
|
||||||
|
"PollRandomOrderDescription" = "Answers appear in random order for each voter";
|
||||||
|
"PollLimitedDuration" = "Limited Duration";
|
||||||
|
"PollLimitedDurationDescription" = "Automatically close the poll at a set time";
|
||||||
|
"PollDuration" = "Duration";
|
||||||
|
"PollHideResultsUntilClose" = "Hide Results Until Close";
|
||||||
|
"PollSelectCloseDate" = "Select Date";
|
||||||
"PollsSolutionTitle" = "Explanation";
|
"PollsSolutionTitle" = "Explanation";
|
||||||
"CreatePollExplanationInfo" = "Users will see this comment after choosing a wrong answer, good for educational purposes.";
|
"CreatePollExplanationInfo" = "Users will see this comment after choosing a wrong answer, good for educational purposes.";
|
||||||
"VoipGroupPersonalAccount" = "personal account";
|
"VoipGroupPersonalAccount" = "personal account";
|
||||||
@ -2888,3 +2901,10 @@
|
|||||||
"AiMessageEditorTo" = "To";
|
"AiMessageEditorTo" = "To";
|
||||||
"ButtonHelp" = "Help";
|
"ButtonHelp" = "Help";
|
||||||
"UnofficialSecurityRisk" = "{peer} uses an unofficial Telegram client — messages to this user may be less secure.";
|
"UnofficialSecurityRisk" = "{peer} uses an unofficial Telegram client — messages to this user may be less secure.";
|
||||||
|
"PollModalQuestionTitle" = "Question";
|
||||||
|
"PollModalOptionsTitle" = "Options";
|
||||||
|
"PollModalSettingsTitle" = "Settings";
|
||||||
|
"PollModalAddMoreText_one" = "You can add {count} more option.";
|
||||||
|
"PollModalAddMoreText_other" = "You can add {count} more options.";
|
||||||
|
"PollModalAddNoMore" = "You have added the maximum number of options.";
|
||||||
|
"PollDurationOther" = "Other";
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export { default as AiMessageEditorModal }
|
|||||||
from '../components/middle/composer/AiMessageEditorModal/AiMessageEditorModal';
|
from '../components/middle/composer/AiMessageEditorModal/AiMessageEditorModal';
|
||||||
|
|
||||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||||
export { default as PollModal } from '../components/middle/composer/PollModal';
|
export { default as PollModal } from '../components/modals/poll/PollModal';
|
||||||
export { default as ToDoListModal } from '../components/middle/composer/ToDoListModal';
|
export { default as ToDoListModal } from '../components/middle/composer/ToDoListModal';
|
||||||
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
|
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
|
||||||
export { default as ChatCommandTooltip } from '../components/middle/composer/ChatCommandTooltip';
|
export { default as ChatCommandTooltip } from '../components/middle/composer/ChatCommandTooltip';
|
||||||
|
|||||||
@ -382,6 +382,7 @@ const CalendarModal = ({
|
|||||||
onEnter={handleSubmit}
|
onEnter={handleSubmit}
|
||||||
dialogRef={dialogRef}
|
dialogRef={dialogRef}
|
||||||
hasAbsoluteCloseButton
|
hasAbsoluteCloseButton
|
||||||
|
isNativeDialog
|
||||||
>
|
>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="month-selector">
|
<div className="month-selector">
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import type {
|
|||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
ApiNewMediaTodo,
|
ApiNewMediaTodo,
|
||||||
ApiNewPoll,
|
|
||||||
ApiPeer,
|
ApiPeer,
|
||||||
ApiQuickReply,
|
ApiQuickReply,
|
||||||
ApiReaction,
|
ApiReaction,
|
||||||
@ -189,7 +188,6 @@ import EmojiTooltip from '../middle/composer/EmojiTooltip.async';
|
|||||||
import InlineBotTooltip from '../middle/composer/InlineBotTooltip.async';
|
import InlineBotTooltip from '../middle/composer/InlineBotTooltip.async';
|
||||||
import MentionTooltip from '../middle/composer/MentionTooltip.async';
|
import MentionTooltip from '../middle/composer/MentionTooltip.async';
|
||||||
import MessageInput from '../middle/composer/MessageInput';
|
import MessageInput from '../middle/composer/MessageInput';
|
||||||
import PollModal from '../middle/composer/PollModal.async';
|
|
||||||
import SendAsMenu from '../middle/composer/SendAsMenu.async';
|
import SendAsMenu from '../middle/composer/SendAsMenu.async';
|
||||||
import StickerTooltip from '../middle/composer/StickerTooltip.async';
|
import StickerTooltip from '../middle/composer/StickerTooltip.async';
|
||||||
import SymbolMenuButton from '../middle/composer/SymbolMenuButton';
|
import SymbolMenuButton from '../middle/composer/SymbolMenuButton';
|
||||||
@ -253,7 +251,6 @@ type StateProps = {
|
|||||||
isReplying?: boolean;
|
isReplying?: boolean;
|
||||||
hasSuggestedPost?: boolean;
|
hasSuggestedPost?: boolean;
|
||||||
forwardedMessagesCount?: number;
|
forwardedMessagesCount?: number;
|
||||||
pollModal: TabState['pollModal'];
|
|
||||||
todoListModal: TabState['todoListModal'];
|
todoListModal: TabState['todoListModal'];
|
||||||
aiMessageEditorPendingResult: TabState['aiMessageEditorPendingResult'];
|
aiMessageEditorPendingResult: TabState['aiMessageEditorPendingResult'];
|
||||||
botKeyboardMessageId?: number;
|
botKeyboardMessageId?: number;
|
||||||
@ -321,7 +318,6 @@ type StateProps = {
|
|||||||
isAccountFrozen?: boolean;
|
isAccountFrozen?: boolean;
|
||||||
isAppConfigLoaded?: boolean;
|
isAppConfigLoaded?: boolean;
|
||||||
insertingPeerIdMention?: string;
|
insertingPeerIdMention?: string;
|
||||||
pollMaxAnswers?: number;
|
|
||||||
replyToMessage?: ApiMessage;
|
replyToMessage?: ApiMessage;
|
||||||
shouldOpenMessageMediaEditor?: TabState['shouldOpenMessageMediaEditor'];
|
shouldOpenMessageMediaEditor?: TabState['shouldOpenMessageMediaEditor'];
|
||||||
};
|
};
|
||||||
@ -382,7 +378,6 @@ const Composer = ({
|
|||||||
isReplying,
|
isReplying,
|
||||||
hasSuggestedPost,
|
hasSuggestedPost,
|
||||||
forwardedMessagesCount,
|
forwardedMessagesCount,
|
||||||
pollModal,
|
|
||||||
todoListModal,
|
todoListModal,
|
||||||
aiMessageEditorPendingResult,
|
aiMessageEditorPendingResult,
|
||||||
botKeyboardMessageId,
|
botKeyboardMessageId,
|
||||||
@ -449,7 +444,6 @@ const Composer = ({
|
|||||||
isAccountFrozen,
|
isAccountFrozen,
|
||||||
isAppConfigLoaded,
|
isAppConfigLoaded,
|
||||||
insertingPeerIdMention,
|
insertingPeerIdMention,
|
||||||
pollMaxAnswers,
|
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
shouldOpenMessageMediaEditor,
|
shouldOpenMessageMediaEditor,
|
||||||
onDropHide,
|
onDropHide,
|
||||||
@ -462,8 +456,6 @@ const Composer = ({
|
|||||||
clearDraft,
|
clearDraft,
|
||||||
saveDraft,
|
saveDraft,
|
||||||
showDialog,
|
showDialog,
|
||||||
openPollModal,
|
|
||||||
closePollModal,
|
|
||||||
openTodoListModal,
|
openTodoListModal,
|
||||||
closeTodoListModal,
|
closeTodoListModal,
|
||||||
openAiMessageEditorModal,
|
openAiMessageEditorModal,
|
||||||
@ -1654,31 +1646,6 @@ const Composer = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePollSend = useLastCallback((poll: ApiNewPoll) => {
|
|
||||||
if (!currentMessageList) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInScheduledList) {
|
|
||||||
requestCalendar((scheduledAt, scheduleRepeatPeriod) => {
|
|
||||||
handleActionWithPaymentConfirmation(
|
|
||||||
handleMessageSchedule,
|
|
||||||
{ poll },
|
|
||||||
scheduledAt,
|
|
||||||
scheduleRepeatPeriod,
|
|
||||||
currentMessageList,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
closePollModal();
|
|
||||||
} else {
|
|
||||||
handleActionWithPaymentConfirmation(
|
|
||||||
sendMessage,
|
|
||||||
{ messageList: currentMessageList, poll, isSilent: isSilentPosting },
|
|
||||||
);
|
|
||||||
closePollModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleToDoListSend = useLastCallback((todo: ApiNewMediaTodo) => {
|
const handleToDoListSend = useLastCallback((todo: ApiNewMediaTodo) => {
|
||||||
if (!currentMessageList) {
|
if (!currentMessageList) {
|
||||||
return;
|
return;
|
||||||
@ -2180,14 +2147,6 @@ const Composer = ({
|
|||||||
canScheduleUntilOnline={canScheduleUntilOnline && !isViewOnceEnabled}
|
canScheduleUntilOnline={canScheduleUntilOnline && !isViewOnceEnabled}
|
||||||
paidMessagesStars={paidMessagesStars}
|
paidMessagesStars={paidMessagesStars}
|
||||||
/>
|
/>
|
||||||
<PollModal
|
|
||||||
isOpen={pollModal.isOpen}
|
|
||||||
isQuiz={pollModal.isQuiz}
|
|
||||||
shouldBeAnonymous={isChannel}
|
|
||||||
maxOptionsCount={pollMaxAnswers}
|
|
||||||
onClear={closePollModal}
|
|
||||||
onSend={handlePollSend}
|
|
||||||
/>
|
|
||||||
<ToDoListModal
|
<ToDoListModal
|
||||||
modal={todoListModal}
|
modal={todoListModal}
|
||||||
onClear={closeTodoListModal}
|
onClear={closeTodoListModal}
|
||||||
@ -2505,7 +2464,6 @@ const Composer = ({
|
|||||||
canInsertDate={!isComposerBlocked}
|
canInsertDate={!isComposerBlocked}
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
onDateInsert={handleFormattedDateInsert}
|
onDateInsert={handleFormattedDateInsert}
|
||||||
onPollCreate={openPollModal}
|
|
||||||
onTodoListCreate={handleTodoListCreate}
|
onTodoListCreate={handleTodoListCreate}
|
||||||
isScheduled={isInScheduledList}
|
isScheduled={isInScheduledList}
|
||||||
attachBots={isInMessageList ? attachBots : undefined}
|
attachBots={isInMessageList ? attachBots : undefined}
|
||||||
@ -2709,7 +2667,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global, {
|
(global, {
|
||||||
chatId, threadId, storyId, messageListType, isMobile, type,
|
chatId, threadId, storyId, messageListType, isMobile, type,
|
||||||
}): Complete<StateProps> => {
|
}): Complete<StateProps> => {
|
||||||
const appConfig = global.appConfig;
|
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const chatBot = !isSystemBot(chatId) ? selectBot(global, chatId) : undefined;
|
const chatBot = !isSystemBot(chatId) ? selectBot(global, chatId) : undefined;
|
||||||
const isChatWithBot = Boolean(chatBot);
|
const isChatWithBot = Boolean(chatBot);
|
||||||
@ -2824,7 +2781,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isReplying,
|
isReplying,
|
||||||
hasSuggestedPost,
|
hasSuggestedPost,
|
||||||
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
|
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
|
||||||
pollModal: tabState.pollModal,
|
|
||||||
todoListModal: tabState.todoListModal,
|
todoListModal: tabState.todoListModal,
|
||||||
aiMessageEditorPendingResult: tabState.aiMessageEditorPendingResult,
|
aiMessageEditorPendingResult: tabState.aiMessageEditorPendingResult,
|
||||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||||
@ -2883,7 +2839,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
shouldPaidMessageAutoApprove,
|
shouldPaidMessageAutoApprove,
|
||||||
isSilentPosting,
|
isSilentPosting,
|
||||||
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen
|
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen
|
||||||
&& !tabState.aiMessageEditorModal,
|
&& !tabState.aiMessageEditorModal
|
||||||
|
&& !tabState.pollModal
|
||||||
|
&& !tabState.sharePreparedMessageModal,
|
||||||
starsBalance,
|
starsBalance,
|
||||||
isStarsBalanceModalOpen,
|
isStarsBalanceModalOpen,
|
||||||
shouldDisplayGiftsButton: userFullInfo?.shouldDisplayGiftsButton,
|
shouldDisplayGiftsButton: userFullInfo?.shouldDisplayGiftsButton,
|
||||||
@ -2891,7 +2849,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isAccountFrozen,
|
isAccountFrozen,
|
||||||
isAppConfigLoaded,
|
isAppConfigLoaded,
|
||||||
insertingPeerIdMention,
|
insertingPeerIdMention,
|
||||||
pollMaxAnswers: appConfig.pollMaxAnswers,
|
|
||||||
shouldOpenMessageMediaEditor,
|
shouldOpenMessageMediaEditor,
|
||||||
replyToMessage,
|
replyToMessage,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,89 +1,27 @@
|
|||||||
@layer ui.layout {
|
@layer ui.layout {
|
||||||
.control {
|
.control {
|
||||||
|
--control-grid-template-areas: "input before label after";
|
||||||
|
--control-grid-columns: auto auto 1fr auto;
|
||||||
|
--control-gap: 1rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "input label";
|
grid-template-areas: var(--control-grid-template-areas);
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: var(--control-grid-columns);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
column-gap: 1rem;
|
column-gap: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "input label" "input desc";
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlAfter) {
|
|
||||||
grid-template-areas: "input label after";
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "input label after" "input desc after";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlBefore) {
|
|
||||||
grid-template-areas: "input before label";
|
|
||||||
grid-template-columns: auto auto 1fr;
|
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "input before label" "input before desc";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlBefore):has(> .controlAfter) {
|
|
||||||
grid-template-areas: "input before label after";
|
|
||||||
grid-template-columns: auto auto 1fr auto;
|
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "input before label after" "input before desc after";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- inputEnd: input at end ---
|
|
||||||
&.inputEnd {
|
&.inputEnd {
|
||||||
grid-template-areas: "label input";
|
--control-grid-template-areas: "before label after input";
|
||||||
grid-template-columns: 1fr auto;
|
--control-grid-columns: auto 1fr auto auto;
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "label input" "desc input";
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlAfter) {
|
|
||||||
grid-template-areas: "label after input";
|
|
||||||
grid-template-columns: 1fr auto auto;
|
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "label after input" "desc after input";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlBefore) {
|
|
||||||
grid-template-areas: "before label input";
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "before label input" "before desc input";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlBefore):has(> .controlAfter) {
|
|
||||||
grid-template-areas: "before label after input";
|
|
||||||
grid-template-columns: auto 1fr auto auto;
|
|
||||||
|
|
||||||
&:has(> .controlDescription) {
|
|
||||||
grid-template-areas: "before label after input" "before desc after input";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:has(> .controlDescription) > .controlLabel {
|
&:has(> .controlDescription) {
|
||||||
align-self: end;
|
--control-grid-template-areas: "input before label after" "input before desc after";
|
||||||
}
|
|
||||||
|
|
||||||
&:has(> .controlDescription) > .input,
|
&.inputEnd {
|
||||||
&:has(> .controlDescription) > .spinner {
|
--control-grid-template-areas: "before label after input" "before desc after input";
|
||||||
transform: translateY(50%);
|
}
|
||||||
grid-row: 1;
|
|
||||||
align-self: end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(label) {
|
:global(label) {
|
||||||
@ -94,6 +32,12 @@
|
|||||||
.input {
|
.input {
|
||||||
grid-area: input;
|
grid-area: input;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin-inline-end: var(--control-gap);
|
||||||
|
|
||||||
|
.inputEnd > & {
|
||||||
|
margin-inline-start: var(--control-gap);
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
@ -101,6 +45,12 @@
|
|||||||
|
|
||||||
grid-area: input;
|
grid-area: input;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin-inline-end: var(--control-gap);
|
||||||
|
|
||||||
|
.inputEnd > & {
|
||||||
|
margin-inline-start: var(--control-gap);
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.controlLabel {
|
.controlLabel {
|
||||||
@ -130,10 +80,38 @@
|
|||||||
.controlBefore {
|
.controlBefore {
|
||||||
grid-area: before;
|
grid-area: before;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin-inline-end: var(--control-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.controlAfter {
|
.controlAfter {
|
||||||
grid-area: after;
|
grid-area: after;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin-inline-start: var(--control-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlIcon {
|
||||||
|
--control-icon-background-color: transparent;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
background-color: var(--control-icon-background-color);
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlIconFilled {
|
||||||
|
color: white;
|
||||||
|
background-image: linear-gradient(180deg, rgb(255 255 255 / 30%) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlIconGlyph {
|
||||||
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import type { TeactNode } from '../../../lib/teact/teact';
|
import type { TeactNode } from '../../../lib/teact/teact';
|
||||||
import { createContext, memo, useMemo } from '../../../lib/teact/teact';
|
import { createContext, memo, useMemo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { IconName } from '../../../types/icons';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
import useContext from '../../../hooks/data/useContext';
|
import useContext from '../../../hooks/data/useContext';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useUniqueId from '../../../hooks/useUniqueId';
|
import useUniqueId from '../../../hooks/useUniqueId';
|
||||||
|
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
import { useInteractiveContext } from './Interactive';
|
import { useInteractiveContext } from './Interactive';
|
||||||
|
|
||||||
@ -123,6 +126,12 @@ type ControlSlotProps = {
|
|||||||
children: TeactNode;
|
children: TeactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ControlIconProps = {
|
||||||
|
iconName?: IconName;
|
||||||
|
className?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const ControlBefore = ({ className, children }: ControlSlotProps) => {
|
const ControlBefore = ({ className, children }: ControlSlotProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName(styles.controlBefore, className)}>
|
<div className={buildClassName(styles.controlBefore, className)}>
|
||||||
@ -139,6 +148,22 @@ const ControlAfter = ({ className, children }: ControlSlotProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ControlIcon = ({ iconName, className, backgroundColor }: ControlIconProps) => {
|
||||||
|
return (
|
||||||
|
<ControlBefore className={className}>
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.controlIcon,
|
||||||
|
backgroundColor && styles.controlIconFilled,
|
||||||
|
)}
|
||||||
|
style={backgroundColor ? `--control-icon-background-color: ${backgroundColor};` : undefined}
|
||||||
|
>
|
||||||
|
{iconName && <Icon name={iconName} className={styles.controlIconGlyph} />}
|
||||||
|
</div>
|
||||||
|
</ControlBefore>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
export default memo(Control);
|
export default memo(Control);
|
||||||
@ -147,4 +172,5 @@ export {
|
|||||||
ControlDescription,
|
ControlDescription,
|
||||||
ControlBefore,
|
ControlBefore,
|
||||||
ControlAfter,
|
ControlAfter,
|
||||||
|
ControlIcon,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
border-radius: var(--border-radius-default);
|
border-radius: var(--border-radius-default);
|
||||||
|
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
|
|||||||
@ -17,6 +17,18 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
@ -35,4 +47,13 @@
|
|||||||
.description + .island {
|
.description + .island {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title + .island {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description + .title,
|
||||||
|
.island + .title {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
|
import type { ElementRef } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
import styles from './Island.module.scss';
|
import styles from './Island.module.scss';
|
||||||
|
|
||||||
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
|
type OwnProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
ref?: ElementRef<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Island = ({ className, children, ...otherProps }: OwnProps) => {
|
const Island = ({ ref, className, children, ...otherProps }: OwnProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className={buildClassName(styles.island, className)}
|
className={buildClassName(styles.island, className)}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@ -28,6 +32,17 @@ const IslandDescription = ({ className, children, ...otherProps }: OwnProps) =>
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IslandTitle = ({ className, children, ...otherProps }: OwnProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.title, className)}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const IslandText = ({ className, children, ...otherProps }: OwnProps) => {
|
const IslandText = ({ className, children, ...otherProps }: OwnProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -42,5 +57,6 @@ const IslandText = ({ className, children, ...otherProps }: OwnProps) => {
|
|||||||
export default Island;
|
export default Island;
|
||||||
export {
|
export {
|
||||||
IslandDescription,
|
IslandDescription,
|
||||||
|
IslandTitle,
|
||||||
IslandText,
|
IslandText,
|
||||||
};
|
};
|
||||||
|
|||||||
281
src/components/gili/modal/Modal.module.scss
Normal file
281
src/components/gili/modal/Modal.module.scss
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
@layer ui.modal {
|
||||||
|
.dialog {
|
||||||
|
--modal-max-width: 35rem;
|
||||||
|
--modal-max-height: min(92dvh, 50rem);
|
||||||
|
--modal-header-height: 3.5rem;
|
||||||
|
--modal-content-block-padding: 1rem;
|
||||||
|
--modal-scroll-fade-size: 1rem;
|
||||||
|
--modal-border-radius: 1.75rem;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contained {
|
||||||
|
contain: content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open .panel {
|
||||||
|
animation: panelOpen 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open:not(.noBackdrop)::backdrop {
|
||||||
|
animation: backdropOpen 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closing .panel {
|
||||||
|
animation: panelClose 200ms ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closing:not(.noBackdrop)::backdrop {
|
||||||
|
animation: backdropClose 200ms ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noBackdrop::backdrop {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen {
|
||||||
|
--_dialog-padding: 0;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.widthSlim {
|
||||||
|
--modal-max-width: 26.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widthRegular {
|
||||||
|
--modal-max-width: 35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widthWide {
|
||||||
|
--modal-max-width: 44rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widthFullscreen {
|
||||||
|
--modal-max-width: 100dvw;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heightAuto {
|
||||||
|
.panel {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.heightRegular {
|
||||||
|
--modal-max-height: min(92dvh, 50rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heightTall {
|
||||||
|
--modal-max-height: min(96dvh, 62rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heightFullscreen {
|
||||||
|
--modal-max-height: 100dvh;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: min(100%, var(--modal-max-width));
|
||||||
|
max-width: var(--modal-max-width);
|
||||||
|
max-height: min(100%, var(--modal-max-height));
|
||||||
|
border-radius: var(--modal-border-radius);
|
||||||
|
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
box-shadow: 0 0.625rem 2rem rgb(0 0 0 / 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerSlot {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.withHeader {
|
||||||
|
mask-image:
|
||||||
|
linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0,
|
||||||
|
transparent calc(var(--modal-header-height) - var(--modal-scroll-fade-size)),
|
||||||
|
black calc(var(--modal-header-height) + var(--modal-scroll-fade-size)),
|
||||||
|
black 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
min-height: 100%;
|
||||||
|
padding-block: var(--modal-content-block-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.withHeader .body {
|
||||||
|
padding-top: calc(var(--modal-header-height) + var(--modal-content-block-padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(2.25rem, 1fr) auto minmax(2.25rem, 1fr);
|
||||||
|
column-gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
min-height: var(--modal-header-height);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerWithSubtitle {
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
row-gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerWithSubtitle .title {
|
||||||
|
align-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
align-self: start;
|
||||||
|
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerAction {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1 / -1;
|
||||||
|
align-items: center;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / -1;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButtonAbsolute {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0.75rem;
|
||||||
|
inset-inline-start: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes panelOpen {
|
||||||
|
from {
|
||||||
|
transform: translateY(1rem) scale(0.98);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes panelClose {
|
||||||
|
from {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(1rem) scale(0.98);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdropOpen {
|
||||||
|
from {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes backdropClose {
|
||||||
|
from {
|
||||||
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.no-page-transitions) {
|
||||||
|
.dialog::backdrop, .panel {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
477
src/components/gili/modal/Modal.tsx
Normal file
477
src/components/gili/modal/Modal.tsx
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
import type { TeactNode } from '../../../lib/teact/teact';
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
memo,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { waitForAnimationEnd } from '../../../util/cssAnimationEndListeners';
|
||||||
|
|
||||||
|
import useContext from '../../../hooks/data/useContext';
|
||||||
|
import useFrozenProps from '../../../hooks/useFrozenProps';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useUniqueId from '../../../hooks/useUniqueId';
|
||||||
|
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import Portal from '../../ui/Portal';
|
||||||
|
import Surface from '../layout/Surface';
|
||||||
|
|
||||||
|
import styles from './Modal.module.scss';
|
||||||
|
|
||||||
|
const CLOSE_ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
let openModalCount = 0;
|
||||||
|
|
||||||
|
export type ModalWidth = 'slim' | 'regular' | 'wide' | 'fullscreen';
|
||||||
|
export type ModalHeight = 'auto' | 'regular' | 'tall' | 'fullscreen';
|
||||||
|
|
||||||
|
export type ModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
children: TeactNode;
|
||||||
|
header?: TeactNode;
|
||||||
|
dialogClassName?: string;
|
||||||
|
contentClassName?: string;
|
||||||
|
width?: ModalWidth;
|
||||||
|
height?: ModalHeight;
|
||||||
|
noBackdrop?: boolean;
|
||||||
|
noLightDismiss?: boolean;
|
||||||
|
ariaLabel?: string;
|
||||||
|
noContainment?: boolean;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModalContextType = {
|
||||||
|
titleId: string;
|
||||||
|
subtitleId: string;
|
||||||
|
hasSubtitle: boolean;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
registerTitle: (isPresent: boolean) => void;
|
||||||
|
registerSubtitle: (isPresent: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModalSlotProps = {
|
||||||
|
className?: string;
|
||||||
|
children?: TeactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModalCloseButtonProps = {
|
||||||
|
asAbsolute?: boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalContext = createContext<ModalContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
const WIDTH_CLASS_NAME: Record<ModalWidth, string> = {
|
||||||
|
slim: styles.widthSlim,
|
||||||
|
regular: styles.widthRegular,
|
||||||
|
wide: styles.widthWide,
|
||||||
|
fullscreen: styles.widthFullscreen,
|
||||||
|
};
|
||||||
|
|
||||||
|
const HEIGHT_CLASS_NAME: Record<ModalHeight, string> = {
|
||||||
|
auto: styles.heightAuto,
|
||||||
|
regular: styles.heightRegular,
|
||||||
|
tall: styles.heightTall,
|
||||||
|
fullscreen: styles.heightFullscreen,
|
||||||
|
};
|
||||||
|
|
||||||
|
function useModalContext() {
|
||||||
|
return useContext(ModalContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBodyDialogClass() {
|
||||||
|
openModalCount += 1;
|
||||||
|
requestMutation(() => {
|
||||||
|
document.body.classList.add('has-open-dialog');
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
openModalCount = Math.max(0, openModalCount - 1);
|
||||||
|
|
||||||
|
if (!openModalCount) {
|
||||||
|
requestMutation(() => {
|
||||||
|
document.body.classList.remove('has-open-dialog');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = ({
|
||||||
|
isOpen,
|
||||||
|
children,
|
||||||
|
header,
|
||||||
|
dialogClassName,
|
||||||
|
contentClassName,
|
||||||
|
width = 'regular',
|
||||||
|
height = 'regular',
|
||||||
|
noBackdrop,
|
||||||
|
noLightDismiss,
|
||||||
|
ariaLabel,
|
||||||
|
noContainment,
|
||||||
|
onClose,
|
||||||
|
}: ModalProps) => {
|
||||||
|
const [shouldRender, setShouldRender] = useState(Boolean(isOpen));
|
||||||
|
const [isClosing, setIsClosing] = useState(false);
|
||||||
|
const [hasTitle, setHasTitle] = useState(false);
|
||||||
|
const [hasSubtitle, setHasSubtitle] = useState(false);
|
||||||
|
|
||||||
|
const dialogRef = useRef<HTMLDialogElement>();
|
||||||
|
const panelRef = useRef<HTMLDivElement>();
|
||||||
|
const closeAnimationCleanupRef = useRef<NoneToVoidFunction>();
|
||||||
|
|
||||||
|
const uniqueId = useUniqueId();
|
||||||
|
const titleId = `modal-title-${uniqueId}`;
|
||||||
|
const subtitleId = `modal-subtitle-${uniqueId}`;
|
||||||
|
|
||||||
|
const frozenProps = useFrozenProps({
|
||||||
|
header,
|
||||||
|
children,
|
||||||
|
dialogClassName,
|
||||||
|
contentClassName,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
noBackdrop,
|
||||||
|
ariaLabel,
|
||||||
|
noContainment,
|
||||||
|
}, !isOpen);
|
||||||
|
|
||||||
|
const shouldShowHeader = Boolean(frozenProps.header);
|
||||||
|
|
||||||
|
const cleanupCloseAnimation = useLastCallback(() => {
|
||||||
|
closeAnimationCleanupRef.current?.();
|
||||||
|
closeAnimationCleanupRef.current = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const finishClose = useLastCallback(() => {
|
||||||
|
cleanupCloseAnimation();
|
||||||
|
|
||||||
|
const dialogElement = dialogRef.current;
|
||||||
|
|
||||||
|
if (dialogElement?.open) {
|
||||||
|
dialogElement.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsClosing(false);
|
||||||
|
setShouldRender(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRequestClose = useLastCallback(() => {
|
||||||
|
if (isClosing) return;
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerTitle = useLastCallback((isPresent: boolean) => {
|
||||||
|
setHasTitle(isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerSubtitle = useLastCallback((isPresent: boolean) => {
|
||||||
|
setHasSubtitle(isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextValue = useMemo<ModalContextType>(() => ({
|
||||||
|
onClose: handleRequestClose,
|
||||||
|
titleId,
|
||||||
|
subtitleId,
|
||||||
|
hasSubtitle,
|
||||||
|
registerTitle,
|
||||||
|
registerSubtitle,
|
||||||
|
}), [
|
||||||
|
handleRequestClose,
|
||||||
|
hasSubtitle,
|
||||||
|
registerSubtitle,
|
||||||
|
registerTitle,
|
||||||
|
subtitleId,
|
||||||
|
titleId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
cleanupCloseAnimation();
|
||||||
|
|
||||||
|
if (!shouldRender) {
|
||||||
|
setShouldRender(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isClosing) {
|
||||||
|
setIsClosing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRender || isClosing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsClosing(true);
|
||||||
|
}, [isClosing, isOpen, shouldRender]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isClosing) {
|
||||||
|
cleanupCloseAnimation();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelElement = panelRef.current;
|
||||||
|
|
||||||
|
if (!panelElement) {
|
||||||
|
finishClose();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body.classList.contains('no-page-transitions')) {
|
||||||
|
finishClose();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAnimationCleanupRef.current = waitForAnimationEnd(
|
||||||
|
panelElement,
|
||||||
|
finishClose,
|
||||||
|
undefined,
|
||||||
|
CLOSE_ANIMATION_DURATION + 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
return cleanupCloseAnimation;
|
||||||
|
}, [isClosing]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogElement = dialogRef.current;
|
||||||
|
|
||||||
|
if (!dialogElement) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dialogElement.open) {
|
||||||
|
dialogElement.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanupCloseAnimation();
|
||||||
|
|
||||||
|
if (dialogElement.open) {
|
||||||
|
dialogElement.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [shouldRender]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return addBodyDialogClass();
|
||||||
|
}, [shouldRender]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogElement = dialogRef.current;
|
||||||
|
|
||||||
|
if (!dialogElement) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (noLightDismiss || !isOpen || isClosing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequestClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
dialogElement.addEventListener('cancel', handleCancel);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dialogElement.removeEventListener('cancel', handleCancel);
|
||||||
|
};
|
||||||
|
}, [isClosing, isOpen, noLightDismiss, shouldRender]);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive: Boolean(isOpen && !noLightDismiss),
|
||||||
|
onBack: handleRequestClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDialogClick = useLastCallback((event: React.MouseEvent<HTMLDialogElement>) => {
|
||||||
|
if (
|
||||||
|
event.target !== event.currentTarget
|
||||||
|
|| noLightDismiss
|
||||||
|
|| !isOpen
|
||||||
|
|| isClosing
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequestClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<ModalContext.Provider value={contextValue}>
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.dialog,
|
||||||
|
WIDTH_CLASS_NAME[frozenProps.width],
|
||||||
|
HEIGHT_CLASS_NAME[frozenProps.height],
|
||||||
|
frozenProps.noBackdrop && styles.noBackdrop,
|
||||||
|
(frozenProps.width === 'fullscreen' || frozenProps.height === 'fullscreen') && styles.fullscreen,
|
||||||
|
isClosing ? styles.closing : styles.open,
|
||||||
|
frozenProps.dialogClassName,
|
||||||
|
!frozenProps.noContainment && styles.contained,
|
||||||
|
)}
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={!hasTitle ? frozenProps.ariaLabel : undefined}
|
||||||
|
aria-labelledby={hasTitle ? titleId : undefined}
|
||||||
|
aria-describedby={hasSubtitle ? subtitleId : undefined}
|
||||||
|
onClick={handleDialogClick}
|
||||||
|
>
|
||||||
|
<div ref={panelRef} className={styles.panel}>
|
||||||
|
{shouldShowHeader && (
|
||||||
|
<div className={styles.headerSlot}>
|
||||||
|
{frozenProps.header}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Surface
|
||||||
|
scrollable
|
||||||
|
className={buildClassName(
|
||||||
|
styles.content,
|
||||||
|
shouldShowHeader && styles.withHeader,
|
||||||
|
frozenProps.contentClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.body}>
|
||||||
|
{frozenProps.children}
|
||||||
|
</div>
|
||||||
|
</Surface>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</ModalContext.Provider>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalHeader = ({ className, children }: ModalSlotProps) => {
|
||||||
|
const modalContext = useModalContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.header,
|
||||||
|
modalContext?.hasSubtitle && styles.headerWithSubtitle,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalHeaderAction = ({ className, children }: ModalSlotProps) => {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.headerAction, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalTitle = ({ className, children }: ModalSlotProps) => {
|
||||||
|
const modalContext = useModalContext();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
modalContext?.registerTitle(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modalContext?.registerTitle(false);
|
||||||
|
};
|
||||||
|
}, [modalContext]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={modalContext?.titleId}
|
||||||
|
className={buildClassName(styles.title, className)}
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalSubtitle = ({ className, children }: ModalSlotProps) => {
|
||||||
|
const modalContext = useModalContext();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
modalContext?.registerSubtitle(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
modalContext?.registerSubtitle(false);
|
||||||
|
};
|
||||||
|
}, [modalContext]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={modalContext?.subtitleId}
|
||||||
|
className={buildClassName(styles.subtitle, className)}
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalCloseButton = ({ asAbsolute, className }: ModalCloseButtonProps) => {
|
||||||
|
const lang = useLang();
|
||||||
|
const modalContext = useModalContext();
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
modalContext?.onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="tiny"
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.closeButton,
|
||||||
|
asAbsolute && styles.closeButtonAbsolute,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div className="animated-close-icon" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Modal);
|
||||||
|
export {
|
||||||
|
ModalHeader,
|
||||||
|
ModalHeaderAction,
|
||||||
|
ModalTitle,
|
||||||
|
ModalSubtitle,
|
||||||
|
ModalCloseButton,
|
||||||
|
};
|
||||||
@ -2,79 +2,119 @@
|
|||||||
.root {
|
.root {
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
width: 1.875rem;
|
width: 1.875rem;
|
||||||
height: 0.875rem;
|
height: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
cursor: inherit;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track {
|
||||||
|
--switch-track-color: var(--ui-border-color, var(--color-borders-input));
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
border-radius: 0.625rem;
|
border-radius: 0.625rem;
|
||||||
|
|
||||||
appearance: none;
|
background-color: var(--switch-track-color);
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
.thumb {
|
||||||
content: "";
|
transform: translateX(-0.125rem);
|
||||||
|
|
||||||
transform: translateX(-0.125rem);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
display: block;
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border: 0.125rem solid var(--switch-track-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
width: 1.25rem;
|
color: var(--switch-track-color);
|
||||||
height: 1.25rem;
|
|
||||||
border: 0.125rem solid var(--ui-border-color, var(--color-borders-input));
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
background-color: var(--ui-bg-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 {
|
.input {
|
||||||
border-color: var(--ui-accent-color, var(--color-primary));
|
&:checked + .track {
|
||||||
background-color: var(--ui-accent-color, var(--color-primary));
|
--switch-track-color: var(--ui-accent-color, var(--color-primary));
|
||||||
|
|
||||||
&::before {
|
.thumb {
|
||||||
transform: translateX(0.75rem);
|
transform: translateX(0.75rem);
|
||||||
border-color: var(--ui-accent-color, var(--color-primary));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:focus-visible + .track {
|
||||||
cursor: default;
|
|
||||||
opacity: var(--input-disabled-opacity, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
|
||||||
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
outline: 2px solid var(--ui-accent-color, var(--color-primary));
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: var(--input-disabled-opacity, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locked {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lockIconTransition {
|
||||||
|
transform: scale(0.5);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
|
||||||
|
&:global(.open) {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lockIcon {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
.permissionColors {
|
.permissionColors {
|
||||||
background-color: var(--color-error);
|
.track {
|
||||||
|
--switch-track-color: var(--color-error);
|
||||||
&::before {
|
|
||||||
border-color: var(--color-error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked {
|
.input:checked + .track {
|
||||||
border-color: var(--color-green);
|
--switch-track-color: var(--color-green);
|
||||||
background-color: var(--color-green);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
border-color: var(--color-green);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
.input:focus-visible + .track {
|
||||||
outline-color: var(--color-error);
|
outline-color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:checked:focus-visible {
|
.input:checked:focus-visible + .track {
|
||||||
outline-color: var(--color-green);
|
outline-color: var(--color-green);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
|
import ShowTransition from '../../ui/ShowTransition';
|
||||||
import { useControlContext } from '../layout/Control';
|
import { useControlContext } from '../layout/Control';
|
||||||
import { useInteractiveContext } from '../layout/Interactive';
|
import { useInteractiveContext } from '../layout/Interactive';
|
||||||
|
|
||||||
@ -17,6 +19,7 @@ type InputProps = React.DetailedHTMLProps<
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
locked?: boolean;
|
||||||
withPermissionColors?: boolean;
|
withPermissionColors?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
@ -27,6 +30,7 @@ type Props = OwnProps & Omit<InputProps, keyof OwnProps | 'type'>;
|
|||||||
const Switch = ({
|
const Switch = ({
|
||||||
checked,
|
checked,
|
||||||
disabled,
|
disabled,
|
||||||
|
locked,
|
||||||
withPermissionColors,
|
withPermissionColors,
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
@ -40,27 +44,42 @@ const Switch = ({
|
|||||||
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
const isDisabled = disabled || interactive?.isDisabled || interactive?.isLoading;
|
||||||
|
|
||||||
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (locked) return;
|
||||||
|
|
||||||
onChange?.(e.currentTarget.checked);
|
onChange?.(e.currentTarget.checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (interactive?.isLoading) return undefined;
|
if (interactive?.isLoading) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<span
|
||||||
{...restProps}
|
|
||||||
type="checkbox"
|
|
||||||
role="switch"
|
|
||||||
id={resolvedId}
|
|
||||||
checked={checked}
|
|
||||||
disabled={isDisabled}
|
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
styles.root,
|
||||||
|
isDisabled && styles.disabled,
|
||||||
|
locked && styles.locked,
|
||||||
withPermissionColors && styles.permissionColors,
|
withPermissionColors && styles.permissionColors,
|
||||||
control?.inputClassName,
|
control?.inputClassName,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onChange={handleChange}
|
>
|
||||||
/>
|
<input
|
||||||
|
{...restProps}
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
id={resolvedId}
|
||||||
|
checked={checked}
|
||||||
|
disabled={isDisabled || locked}
|
||||||
|
className={styles.input}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<span className={styles.track} aria-hidden>
|
||||||
|
<span className={styles.thumb}>
|
||||||
|
<ShowTransition isOpen={Boolean(locked)} className={styles.lockIconTransition}>
|
||||||
|
<Icon name="lock-badge" className={styles.lockIcon} />
|
||||||
|
</ShowTransition>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -226,7 +226,10 @@ const MediaViewer = ({
|
|||||||
const prevOrigin = usePrevious(origin);
|
const prevOrigin = usePrevious(origin);
|
||||||
const prevItem = usePrevious(currentItem);
|
const prevItem = usePrevious(currentItem);
|
||||||
const prevBestImageData = usePrevious(bestImageData);
|
const prevBestImageData = usePrevious(bestImageData);
|
||||||
const textParts = message ? renderMessageText({ message, forcePlayback: true, isForMediaViewer: true }) : undefined;
|
const textMessage = currentItem?.type === 'message' ? currentItem.message : undefined;
|
||||||
|
const textParts = textMessage
|
||||||
|
? renderMessageText({ message: textMessage, forcePlayback: true, isForMediaViewer: true })
|
||||||
|
: undefined;
|
||||||
const hasFooter = Boolean(textParts);
|
const hasFooter = Boolean(textParts);
|
||||||
|
|
||||||
useEffectWithPrevDeps(([prevIsOpen, prevIsHidden]) => {
|
useEffectWithPrevDeps(([prevIsOpen, prevIsHidden]) => {
|
||||||
|
|||||||
@ -57,12 +57,11 @@ export type OwnProps = {
|
|||||||
theme: ThemeKey;
|
theme: ThemeKey;
|
||||||
canEditMedia?: boolean;
|
canEditMedia?: boolean;
|
||||||
editingMessage?: ApiMessage;
|
editingMessage?: ApiMessage;
|
||||||
messageListType?: MessageListType;
|
messageListType: MessageListType;
|
||||||
paidMessagesStars?: number;
|
paidMessagesStars?: number;
|
||||||
canInsertDate?: boolean;
|
canInsertDate?: boolean;
|
||||||
onFileSelect: (files: File[]) => void;
|
onFileSelect: (files: File[]) => void;
|
||||||
onDateInsert: (text: ApiFormattedText) => void;
|
onDateInsert: (text: ApiFormattedText) => void;
|
||||||
onPollCreate: NoneToVoidFunction;
|
|
||||||
onTodoListCreate: NoneToVoidFunction;
|
onTodoListCreate: NoneToVoidFunction;
|
||||||
onMenuOpen: NoneToVoidFunction;
|
onMenuOpen: NoneToVoidFunction;
|
||||||
onMenuClose: NoneToVoidFunction;
|
onMenuClose: NoneToVoidFunction;
|
||||||
@ -93,10 +92,10 @@ const AttachMenu = ({
|
|||||||
onDateInsert,
|
onDateInsert,
|
||||||
onMenuOpen,
|
onMenuOpen,
|
||||||
onMenuClose,
|
onMenuClose,
|
||||||
onPollCreate,
|
|
||||||
onTodoListCreate,
|
onTodoListCreate,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const {
|
const {
|
||||||
|
openPollModal,
|
||||||
updateAttachmentSettings,
|
updateAttachmentSettings,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
|
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
|
||||||
@ -190,6 +189,11 @@ const AttachMenu = ({
|
|||||||
openDateModal();
|
openDateModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handlePollCreate = useLastCallback(() => {
|
||||||
|
closeAttachMenu();
|
||||||
|
openPollModal({ chatId, threadId, messageListType });
|
||||||
|
});
|
||||||
|
|
||||||
if (!isButtonVisible && !isDateModalOpen) {
|
if (!isButtonVisible && !isDateModalOpen) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -275,7 +279,7 @@ const AttachMenu = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canAttachPolls && !editingMessage && (
|
{canAttachPolls && !editingMessage && (
|
||||||
<MenuItem icon="poll" onClick={onPollCreate}>{oldLang('Poll')}</MenuItem>
|
<MenuItem icon="poll" onClick={handlePollCreate}>{lang('Poll')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
{canAttachToDoLists && !editingMessage && (
|
{canAttachToDoLists && !editingMessage && (
|
||||||
<MenuItem icon="select" onClick={onTodoListCreate}>{lang('TitleToDoList')}</MenuItem>
|
<MenuItem icon="select" onClick={onTodoListCreate}>{lang('TitleToDoList')}</MenuItem>
|
||||||
|
|||||||
@ -1,107 +0,0 @@
|
|||||||
@use '../../../styles/mixins';
|
|
||||||
|
|
||||||
.PollModal {
|
|
||||||
.modal-dialog {
|
|
||||||
max-width: 26.25rem;
|
|
||||||
max-height: calc(100vh - 5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
min-height: 4.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header-condensed {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-header {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-list {
|
|
||||||
overflow: auto;
|
|
||||||
overflow-y: scroll;
|
|
||||||
|
|
||||||
max-height: 20rem;
|
|
||||||
margin: 1rem -0.75rem -0.5rem;
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
border-top: 1px solid var(--color-chat-hover);
|
|
||||||
|
|
||||||
@include mixins.adapt-padding-to-scrollbar(0.75rem);
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
overflow: hidden;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-wrapper {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
padding-right: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-remove-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.125rem;
|
|
||||||
right: 0.3125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.quiz-mode {
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
|
|
||||||
.dialog-checkbox-group {
|
|
||||||
margin: 0 -1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-header {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
margin-top: -1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-error {
|
|
||||||
margin: -1rem 0 1rem 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-group:last-child {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.8125rem;
|
|
||||||
margin-left: -1.125rem;
|
|
||||||
|
|
||||||
.Radio, &:hover {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Checkbox,
|
|
||||||
.Radio {
|
|
||||||
.Checkbox-main,
|
|
||||||
.Radio-main {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,385 +0,0 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
|
||||||
import type { ElementRef } from '../../../lib/teact/teact';
|
|
||||||
import {
|
|
||||||
memo, useEffect, useRef, useState,
|
|
||||||
} from '../../../lib/teact/teact';
|
|
||||||
|
|
||||||
import type { ApiNewPoll } from '../../../api/types';
|
|
||||||
|
|
||||||
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
|
||||||
import { generateUniqueNumberId } from '../../../util/generateUniqueId';
|
|
||||||
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
|
||||||
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
|
||||||
import Checkbox from '../../ui/Checkbox';
|
|
||||||
import InputText from '../../ui/InputText';
|
|
||||||
import Modal from '../../ui/Modal';
|
|
||||||
import RadioGroup from '../../ui/RadioGroup';
|
|
||||||
import TextArea from '../../ui/TextArea';
|
|
||||||
|
|
||||||
import './PollModal.scss';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
shouldBeAnonymous?: boolean;
|
|
||||||
isQuiz?: boolean;
|
|
||||||
maxOptionsCount?: number;
|
|
||||||
onSend: (pollSummary: ApiNewPoll) => void;
|
|
||||||
onClear: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_LIST_HEIGHT = 320;
|
|
||||||
const FALLBACK_MAX_OPTIONS_COUNT = 12;
|
|
||||||
const MAX_OPTION_LENGTH = 100;
|
|
||||||
const MAX_QUESTION_LENGTH = 255;
|
|
||||||
const MAX_SOLUTION_LENGTH = 200;
|
|
||||||
|
|
||||||
const PollModal = ({
|
|
||||||
isOpen,
|
|
||||||
isQuiz,
|
|
||||||
shouldBeAnonymous,
|
|
||||||
maxOptionsCount = FALLBACK_MAX_OPTIONS_COUNT,
|
|
||||||
onSend,
|
|
||||||
onClear,
|
|
||||||
}: OwnProps) => {
|
|
||||||
const questionInputRef = useRef<HTMLInputElement>();
|
|
||||||
const optionsListRef = useRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
const [question, setQuestion] = useState<string>('');
|
|
||||||
const [options, setOptions] = useState<string[]>(['']);
|
|
||||||
const [isAnonymous, setIsAnonymous] = useState(true);
|
|
||||||
const [isMultipleAnswers, setIsMultipleAnswers] = useState(false);
|
|
||||||
const [isQuizMode, setIsQuizMode] = useState(isQuiz || false);
|
|
||||||
const [solution, setSolution] = useState<string>('');
|
|
||||||
const [correctOption, setCorrectOption] = useState<number | undefined>();
|
|
||||||
const [hasErrors, setHasErrors] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const lang = useOldLang();
|
|
||||||
|
|
||||||
const handleSolutionChange = useLastCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setSolution(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const focusInput = useLastCallback((ref: ElementRef<HTMLInputElement>) => {
|
|
||||||
if (isOpen && ref.current) {
|
|
||||||
ref.current.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setQuestion('');
|
|
||||||
setOptions(['']);
|
|
||||||
setIsAnonymous(true);
|
|
||||||
setIsMultipleAnswers(false);
|
|
||||||
setIsQuizMode(isQuiz || false);
|
|
||||||
setSolution('');
|
|
||||||
setCorrectOption(undefined);
|
|
||||||
setHasErrors(false);
|
|
||||||
}
|
|
||||||
}, [isQuiz, isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => focusInput(questionInputRef), [focusInput, isOpen]);
|
|
||||||
|
|
||||||
const addNewOption = useLastCallback((newOptions: string[] = []) => {
|
|
||||||
setOptions([...newOptions, '']);
|
|
||||||
|
|
||||||
requestNextMutation(() => {
|
|
||||||
const list = optionsListRef.current;
|
|
||||||
if (!list) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestMeasure(() => {
|
|
||||||
list.scrollTo({ top: list.scrollHeight, behavior: 'smooth' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCreate = useLastCallback(() => {
|
|
||||||
setHasErrors(false);
|
|
||||||
if (!isOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isNoCorrectOptionError = isQuizMode && (correctOption === undefined || !options[correctOption].trim());
|
|
||||||
|
|
||||||
const answers = options
|
|
||||||
.map((text, index) => {
|
|
||||||
text = text.trim();
|
|
||||||
|
|
||||||
if (!text) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: {
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
option: String(index),
|
|
||||||
...(index === correctOption && { correct: true }),
|
|
||||||
};
|
|
||||||
}).filter(Boolean);
|
|
||||||
|
|
||||||
const questionTrimmed = question.trim().substring(0, MAX_QUESTION_LENGTH);
|
|
||||||
if (!questionTrimmed || answers.length < 2) {
|
|
||||||
setQuestion(questionTrimmed);
|
|
||||||
if (answers.length) {
|
|
||||||
const optionsTrimmed = options.map((o) => o.trim().substring(0, MAX_OPTION_LENGTH)).filter(Boolean);
|
|
||||||
if (optionsTrimmed.length < 2) {
|
|
||||||
addNewOption(optionsTrimmed);
|
|
||||||
} else {
|
|
||||||
setOptions(optionsTrimmed);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addNewOption();
|
|
||||||
}
|
|
||||||
setHasErrors(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNoCorrectOptionError) {
|
|
||||||
setHasErrors(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: ApiNewPoll = {
|
|
||||||
summary: {
|
|
||||||
id: generateUniqueNumberId().toString(),
|
|
||||||
hash: '0',
|
|
||||||
question: {
|
|
||||||
text: questionTrimmed,
|
|
||||||
},
|
|
||||||
answers,
|
|
||||||
isPublic: !isAnonymous || undefined,
|
|
||||||
isMultipleChoice: isMultipleAnswers || undefined,
|
|
||||||
isQuiz: isQuizMode || undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isQuizMode) {
|
|
||||||
const { text, entities } = (solution && parseHtmlAsFormattedText(solution.substring(0, MAX_SOLUTION_LENGTH)))
|
|
||||||
|| {};
|
|
||||||
const correctAnswerIndex = answers.findIndex((answer) => answer.option === String(correctOption!));
|
|
||||||
|
|
||||||
payload.correctAnswers = [correctAnswerIndex];
|
|
||||||
payload.solution = text;
|
|
||||||
payload.solutionEntities = entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSend(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateOption = useLastCallback((index: number, text: string) => {
|
|
||||||
const newOptions = [...options];
|
|
||||||
newOptions[index] = text;
|
|
||||||
if (newOptions[newOptions.length - 1].trim().length && newOptions.length < maxOptionsCount) {
|
|
||||||
addNewOption(newOptions);
|
|
||||||
} else {
|
|
||||||
setOptions(newOptions);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeOption = useLastCallback((index: number) => {
|
|
||||||
const newOptions = [...options];
|
|
||||||
newOptions.splice(index, 1);
|
|
||||||
setOptions(newOptions);
|
|
||||||
|
|
||||||
if (correctOption !== undefined) {
|
|
||||||
if (correctOption === index) {
|
|
||||||
setCorrectOption(undefined);
|
|
||||||
} else if (index < correctOption) {
|
|
||||||
setCorrectOption(correctOption - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestNextMutation(() => {
|
|
||||||
if (!optionsListRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsListRef.current.classList.toggle('overflown', optionsListRef.current.scrollHeight > MAX_LIST_HEIGHT);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCorrectOptionChange = useLastCallback((newValue: string) => {
|
|
||||||
setCorrectOption(Number(newValue));
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleIsAnonymousChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setIsAnonymous(e.target.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleMultipleAnswersChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setIsMultipleAnswers(e.target.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleQuizModeChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setIsQuizMode(e.target.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleKeyPress = useLastCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
handleCreate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleQuestionChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setQuestion(e.target.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const getQuestionError = useLastCallback(() => {
|
|
||||||
if (hasErrors && !question.trim().length) {
|
|
||||||
return lang('lng_polls_choose_question');
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOptionsError = useLastCallback((index: number) => {
|
|
||||||
const optionsTrimmed = options.map((o) => o.trim()).filter((o) => o.length);
|
|
||||||
if (hasErrors && optionsTrimmed.length < 2 && !options[index].trim().length) {
|
|
||||||
return lang('lng_polls_choose_answers');
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderHeader() {
|
|
||||||
return (
|
|
||||||
<div className="modal-header-condensed">
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
color="translucent"
|
|
||||||
size="tiny"
|
|
||||||
ariaLabel="Cancel poll creation"
|
|
||||||
onClick={onClear}
|
|
||||||
iconName="close"
|
|
||||||
/>
|
|
||||||
<div className="modal-title">{lang('NewPoll')}</div>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
size="smaller"
|
|
||||||
className="modal-action-button"
|
|
||||||
onClick={handleCreate}
|
|
||||||
>
|
|
||||||
{lang('Create')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOptions() {
|
|
||||||
return options.map((option, index) => (
|
|
||||||
<div className="option-wrapper">
|
|
||||||
<InputText
|
|
||||||
maxLength={MAX_OPTION_LENGTH}
|
|
||||||
label={index !== options.length - 1 || options.length === maxOptionsCount
|
|
||||||
? lang('OptionHint')
|
|
||||||
: lang('CreatePoll.AddOption')}
|
|
||||||
error={getOptionsError(index)}
|
|
||||||
value={option}
|
|
||||||
|
|
||||||
onChange={(e) => updateOption(index, e.currentTarget.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
/>
|
|
||||||
{index !== options.length - 1 && (
|
|
||||||
<Button
|
|
||||||
className="option-remove-button"
|
|
||||||
round
|
|
||||||
color="translucent"
|
|
||||||
size="smaller"
|
|
||||||
ariaLabel={lang('Delete')}
|
|
||||||
onClick={() => removeOption(index)}
|
|
||||||
iconName="close"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRadioOptions() {
|
|
||||||
return renderOptions()
|
|
||||||
.map((label, index) => ({ value: String(index), label, hidden: !options[index].trim() }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderQuizNoOptionError() {
|
|
||||||
const optionsTrimmed = options.map((o) => o.trim()).filter((o) => o.length);
|
|
||||||
|
|
||||||
return isQuizMode && (correctOption === undefined || !optionsTrimmed[correctOption]) && (
|
|
||||||
<p className="poll-error">{lang('lng_polls_choose_correct')}</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClear} header={renderHeader()} className="PollModal">
|
|
||||||
<InputText
|
|
||||||
ref={questionInputRef}
|
|
||||||
label={lang('AskAQuestion')}
|
|
||||||
value={question}
|
|
||||||
error={getQuestionError()}
|
|
||||||
onChange={handleQuestionChange}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
/>
|
|
||||||
<div className="options-divider" />
|
|
||||||
|
|
||||||
<div className="options-list custom-scroll" ref={optionsListRef}>
|
|
||||||
<h3 className="options-header">{lang('PollOptions')}</h3>
|
|
||||||
|
|
||||||
{hasErrors && renderQuizNoOptionError()}
|
|
||||||
{isQuizMode ? (
|
|
||||||
<RadioGroup
|
|
||||||
name="correctOption"
|
|
||||||
options={renderRadioOptions()}
|
|
||||||
selected={String(correctOption)}
|
|
||||||
onChange={handleCorrectOptionChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
renderOptions()
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="options-divider" />
|
|
||||||
|
|
||||||
<div className="quiz-mode">
|
|
||||||
<div className="dialog-checkbox-group">
|
|
||||||
{!shouldBeAnonymous && (
|
|
||||||
<Checkbox
|
|
||||||
label={lang('PollAnonymous')}
|
|
||||||
checked={isAnonymous}
|
|
||||||
onChange={handleIsAnonymousChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Checkbox
|
|
||||||
label={lang('PollMultiple')}
|
|
||||||
checked={isMultipleAnswers}
|
|
||||||
disabled={isQuizMode}
|
|
||||||
onChange={handleMultipleAnswersChange}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={lang('PollQuiz')}
|
|
||||||
checked={isQuizMode}
|
|
||||||
disabled={isMultipleAnswers || isQuiz !== undefined}
|
|
||||||
onChange={handleQuizModeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isQuizMode && (
|
|
||||||
<>
|
|
||||||
<h3 className="options-header">{lang('lng_polls_solution_title')}</h3>
|
|
||||||
<TextArea
|
|
||||||
value={solution}
|
|
||||||
onChange={handleSolutionChange}
|
|
||||||
noReplaceNewlines
|
|
||||||
/>
|
|
||||||
<div className="note">{lang('CreatePoll.ExplanationInfo')}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(PollModal);
|
|
||||||
@ -62,6 +62,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question {
|
.question {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from '../../../../lib/teact/teact';
|
} from '../../../../lib/teact/teact';
|
||||||
import { getActions, getGlobal } from '../../../../global';
|
import { getActions, getGlobal, getPromiseActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiFormattedText,
|
ApiFormattedText,
|
||||||
@ -34,6 +34,7 @@ import useLastCallback from '../../../../hooks/useLastCallback';
|
|||||||
import AvatarList from '../../../common/AvatarList';
|
import AvatarList from '../../../common/AvatarList';
|
||||||
import CompactMapPreview from '../../../common/CompactMapPreview';
|
import CompactMapPreview from '../../../common/CompactMapPreview';
|
||||||
import Document from '../../../common/Document';
|
import Document from '../../../common/Document';
|
||||||
|
import Icon from '../../../common/icons/Icon';
|
||||||
import PeerColorWrapper from '../../../common/PeerColorWrapper';
|
import PeerColorWrapper from '../../../common/PeerColorWrapper';
|
||||||
import StickerView from '../../../common/StickerView';
|
import StickerView from '../../../common/StickerView';
|
||||||
import Button from '../../../ui/Button';
|
import Button from '../../../ui/Button';
|
||||||
@ -44,6 +45,7 @@ import Video from '../Video';
|
|||||||
import PollOption from './PollOption';
|
import PollOption from './PollOption';
|
||||||
|
|
||||||
import styles from './Poll.module.scss';
|
import styles from './Poll.module.scss';
|
||||||
|
import optionStyles from './PollOption.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -56,11 +58,16 @@ type OwnProps = {
|
|||||||
observeIntersectionForPlaying?: ObserveFn;
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
pollMaxAnswers: number;
|
||||||
|
};
|
||||||
|
|
||||||
const ATTACHED_MAP_WIDTH = 350;
|
const ATTACHED_MAP_WIDTH = 350;
|
||||||
const ATTACHED_MAP_HEIGHT = 200;
|
const ATTACHED_MAP_HEIGHT = 200;
|
||||||
const ATTACHED_MAP_ZOOM = 15;
|
const ATTACHED_MAP_ZOOM = 15;
|
||||||
const STICKER_PREVIEW_SIZE = 96;
|
const STICKER_PREVIEW_SIZE = 96;
|
||||||
const VOTE_TIMEOUT = 5000;
|
const VOTE_TIMEOUT = 5000;
|
||||||
|
const MAX_OPTION_LENGTH = 100;
|
||||||
|
|
||||||
const Poll = ({
|
const Poll = ({
|
||||||
chatId,
|
chatId,
|
||||||
@ -71,7 +78,8 @@ const Poll = ({
|
|||||||
isInScheduled,
|
isInScheduled,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
}: OwnProps) => {
|
pollMaxAnswers,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
const {
|
const {
|
||||||
openMapModal,
|
openMapModal,
|
||||||
openMediaViewer,
|
openMediaViewer,
|
||||||
@ -80,6 +88,7 @@ const Poll = ({
|
|||||||
sendPollVote,
|
sendPollVote,
|
||||||
loadMessage,
|
loadMessage,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
const { appendPollAnswer } = getPromiseActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const serverTime = getServerTime();
|
const serverTime = getServerTime();
|
||||||
|
|
||||||
@ -88,6 +97,8 @@ const Poll = ({
|
|||||||
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
|
||||||
const [isExplanationOpen, setIsExplanationOpen] = useState(false);
|
const [isExplanationOpen, setIsExplanationOpen] = useState(false);
|
||||||
const [isSendingVote, setIsSendingVote] = useState(false);
|
const [isSendingVote, setIsSendingVote] = useState(false);
|
||||||
|
const [isAppendingAnswer, setIsAppendingAnswer] = useState(false);
|
||||||
|
const [newAnswerText, setNewAnswerText] = useState('');
|
||||||
const [isViewingAuthorResults, setIsViewingAuthorResults] = useState(false);
|
const [isViewingAuthorResults, setIsViewingAuthorResults] = useState(false);
|
||||||
const [answerOrder] = useState<string[]>(() => (
|
const [answerOrder] = useState<string[]>(() => (
|
||||||
buildAnswerOrder(answers, summary.shouldShuffleAnswers)
|
buildAnswerOrder(answers, summary.shouldShuffleAnswers)
|
||||||
@ -118,6 +129,11 @@ const Poll = ({
|
|||||||
(!canVote && !areResultsHiddenForCurrentUser) || isViewingAuthorResults
|
(!canVote && !areResultsHiddenForCurrentUser) || isViewingAuthorResults
|
||||||
);
|
);
|
||||||
const canShowResultsPanel = hasChosenAnswer && summary.isPublic && hasResultData && !areResultsHiddenForCurrentUser;
|
const canShowResultsPanel = hasChosenAnswer && summary.isPublic && hasResultData && !areResultsHiddenForCurrentUser;
|
||||||
|
const canShowExplanation = hasExplanation && hasChosenAnswer;
|
||||||
|
const canAppendAnswer = Boolean(
|
||||||
|
summary.canAddAnswers && !summary.isClosed && !isInScheduled && answers.length < pollMaxAnswers,
|
||||||
|
);
|
||||||
|
const trimmedNewAnswerText = newAnswerText.trim().substring(0, MAX_OPTION_LENGTH);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canVote) {
|
if (!canVote) {
|
||||||
@ -162,10 +178,19 @@ const Poll = ({
|
|||||||
}, [canToggleAuthorResults]);
|
}, [canToggleAuthorResults]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasExplanation) {
|
if (!canShowExplanation) {
|
||||||
setIsExplanationOpen(false);
|
setIsExplanationOpen(false);
|
||||||
}
|
}
|
||||||
}, [hasExplanation]);
|
}, [canShowExplanation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canAppendAnswer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewAnswerText('');
|
||||||
|
setIsAppendingAnswer(false);
|
||||||
|
}, [canAppendAnswer]);
|
||||||
|
|
||||||
const answersByOption = useMemo(() => buildCollectionByKey(answers, 'option'), [answers]);
|
const answersByOption = useMemo(() => buildCollectionByKey(answers, 'option'), [answers]);
|
||||||
|
|
||||||
@ -261,6 +286,37 @@ const Poll = ({
|
|||||||
submitVote(selectedOptions);
|
submitVote(selectedOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleNewAnswerChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNewAnswerText(e.currentTarget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAppendAnswer = useLastCallback(async () => {
|
||||||
|
if (!canAppendAnswer || !trimmedNewAnswerText || isAppendingAnswer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAppendingAnswer(true);
|
||||||
|
try {
|
||||||
|
await appendPollAnswer({
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
text: trimmedNewAnswerText,
|
||||||
|
});
|
||||||
|
setNewAnswerText('');
|
||||||
|
} finally {
|
||||||
|
setIsAppendingAnswer(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNewAnswerKeyDown = useLastCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
handleAppendAnswer();
|
||||||
|
});
|
||||||
|
|
||||||
const handleOpenPreview = useLastCallback((previewIndex: number) => {
|
const handleOpenPreview = useLastCallback((previewIndex: number) => {
|
||||||
if (!standaloneMedia.length) {
|
if (!standaloneMedia.length) {
|
||||||
return;
|
return;
|
||||||
@ -372,6 +428,23 @@ const Poll = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (trimmedNewAnswerText) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={styles.footerButton}
|
||||||
|
disabled={isAppendingAnswer || !canAppendAnswer}
|
||||||
|
noForcedUpperCase
|
||||||
|
isText
|
||||||
|
inline
|
||||||
|
size="smaller"
|
||||||
|
color="adaptive"
|
||||||
|
onClick={handleAppendAnswer}
|
||||||
|
>
|
||||||
|
{renderFooterBody(lang('Save'))}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (canVote && isMultipleChoice && selectedOptions.length) {
|
if (canVote && isMultipleChoice && selectedOptions.length) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -479,12 +552,16 @@ const Poll = ({
|
|||||||
canShowResultsPanel,
|
canShowResultsPanel,
|
||||||
canToggleAuthorResults,
|
canToggleAuthorResults,
|
||||||
canVote,
|
canVote,
|
||||||
|
canAppendAnswer,
|
||||||
|
handleAppendAnswer,
|
||||||
|
isAppendingAnswer,
|
||||||
isMultipleChoice,
|
isMultipleChoice,
|
||||||
isSendingVote,
|
isSendingVote,
|
||||||
isViewingAuthorResults,
|
isViewingAuthorResults,
|
||||||
lang,
|
lang,
|
||||||
selectedOptions.length,
|
selectedOptions.length,
|
||||||
summary.isQuiz,
|
summary.isQuiz,
|
||||||
|
trimmedNewAnswerText,
|
||||||
footerSubtext,
|
footerSubtext,
|
||||||
totalVoters,
|
totalVoters,
|
||||||
]);
|
]);
|
||||||
@ -493,7 +570,7 @@ const Poll = ({
|
|||||||
<>
|
<>
|
||||||
{attachedMediaEl}
|
{attachedMediaEl}
|
||||||
<div className={styles.root} dir={lang.isRtl ? 'rtl' : undefined}>
|
<div className={styles.root} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{isExplanationOpen && hasExplanation && (
|
{isExplanationOpen && canShowExplanation && (
|
||||||
<PeerColorWrapper className={styles.explanation} shouldReset>
|
<PeerColorWrapper className={styles.explanation} shouldReset>
|
||||||
<div className={styles.explanationHeader}>
|
<div className={styles.explanationHeader}>
|
||||||
<span className={styles.explanationTitle}>
|
<span className={styles.explanationTitle}>
|
||||||
@ -531,7 +608,7 @@ const Poll = ({
|
|||||||
<div className={styles.question} dir="auto">
|
<div className={styles.question} dir="auto">
|
||||||
{questionText}
|
{questionText}
|
||||||
</div>
|
</div>
|
||||||
{hasExplanation && !isExplanationOpen && (
|
{canShowExplanation && !isExplanationOpen && (
|
||||||
<div className={styles.explanationToggleButton}>
|
<div className={styles.explanationToggleButton}>
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
@ -595,6 +672,31 @@ const Poll = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{canAppendAnswer && (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
optionStyles.root,
|
||||||
|
optionStyles.addAnswer,
|
||||||
|
hasOptionMedia && optionStyles.hasMediaColumn,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={optionStyles.selector}>
|
||||||
|
<Icon name="add" className={optionStyles.addAnswerIcon} />
|
||||||
|
</div>
|
||||||
|
<div className={buildClassName(optionStyles.answer, optionStyles.addAnswerContent)}>
|
||||||
|
<input
|
||||||
|
className={optionStyles.addAnswerInput}
|
||||||
|
value={newAnswerText}
|
||||||
|
placeholder={lang('CreatePollAddOption')}
|
||||||
|
maxLength={MAX_OPTION_LENGTH}
|
||||||
|
dir="auto"
|
||||||
|
disabled={isAppendingAnswer}
|
||||||
|
onChange={handleNewAnswerChange}
|
||||||
|
onKeyDown={handleNewAnswerKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{footerContent && (
|
{footerContent && (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
@ -761,4 +863,8 @@ function onPreviewClick(onOpenPreview: (previewIndex: number) => void) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Poll);
|
export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
|
||||||
|
return {
|
||||||
|
pollMaxAnswers: global.appConfig.pollMaxAnswers,
|
||||||
|
};
|
||||||
|
})(Poll));
|
||||||
|
|||||||
@ -29,6 +29,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
}
|
||||||
|
|
||||||
.hasMediaColumn {
|
.hasMediaColumn {
|
||||||
--poll-option-media-width: 3rem;
|
--poll-option-media-width: 3rem;
|
||||||
}
|
}
|
||||||
@ -48,6 +52,31 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addAnswerIcon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addAnswerInput {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
font: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
outline: 0.125rem solid var(--accent-color);
|
||||||
|
outline-offset: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
--spinner-size: 1.25rem;
|
--spinner-size: 1.25rem;
|
||||||
}
|
}
|
||||||
@ -79,6 +108,15 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.addAnswer {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addAnswerContent {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
justify-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.sideMeta {
|
.sideMeta {
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
|
|||||||
@ -314,8 +314,9 @@ const PollOption = ({
|
|||||||
hasResults && !hasMaskedResults && styles.hasResults,
|
hasResults && !hasMaskedResults && styles.hasResults,
|
||||||
shouldReserveMediaEndColumn && styles.hasMediaColumn,
|
shouldReserveMediaEndColumn && styles.hasMediaColumn,
|
||||||
hasResults && !hasMaskedResults && isQuiz && !result?.isCorrect && styles.incorrect,
|
hasResults && !hasMaskedResults && isQuiz && !result?.isCorrect && styles.incorrect,
|
||||||
|
!hasResults && !isInScheduled && styles.clickable,
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={!isInScheduled ? handleClick : undefined}
|
||||||
>
|
>
|
||||||
<Transition
|
<Transition
|
||||||
name="fade"
|
name="fade"
|
||||||
|
|||||||
@ -3,9 +3,12 @@ import { withGlobal } from '../../global';
|
|||||||
|
|
||||||
import type { TabState } from '../../global/types';
|
import type { TabState } from '../../global/types';
|
||||||
|
|
||||||
import { selectTabState } from '../../global/selectors';
|
import { selectCanAnimateInterface, selectTabState } from '../../global/selectors';
|
||||||
import { pick } from '../../util/iteratees';
|
import { pick } from '../../util/iteratees';
|
||||||
|
|
||||||
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
|
||||||
import VerificationMonetizationModal from '../common/VerificationMonetizationModal.async';
|
import VerificationMonetizationModal from '../common/VerificationMonetizationModal.async';
|
||||||
import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async';
|
import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async';
|
||||||
import AiMessageEditorModal from '../middle/composer/AiMessageEditorModal/AiMessageEditorModal.async';
|
import AiMessageEditorModal from '../middle/composer/AiMessageEditorModal/AiMessageEditorModal.async';
|
||||||
@ -55,6 +58,7 @@ import MapModal from './map/MapModal.async';
|
|||||||
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
||||||
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
||||||
import PasskeyModal from './passkey/PasskeyModal.async';
|
import PasskeyModal from './passkey/PasskeyModal.async';
|
||||||
|
import PollModal from './poll/PollModal.async';
|
||||||
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
|
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
|
||||||
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
|
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
|
||||||
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
|
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
|
||||||
@ -97,6 +101,7 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'starsPayment' |
|
'starsPayment' |
|
||||||
'starsTransactionModal' |
|
'starsTransactionModal' |
|
||||||
'paidReactionModal' |
|
'paidReactionModal' |
|
||||||
|
'pollModal' |
|
||||||
'suggestMessageModal' |
|
'suggestMessageModal' |
|
||||||
'suggestedPostApprovalModal' |
|
'suggestedPostApprovalModal' |
|
||||||
'webApps' |
|
'webApps' |
|
||||||
@ -154,20 +159,68 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'editRankModal' |
|
'editRankModal' |
|
||||||
'rankModal'
|
'rankModal'
|
||||||
>;
|
>;
|
||||||
|
type WrappedModalKey = 'pollModal';
|
||||||
|
type LegacyModalKey = Exclude<ModalKey, WrappedModalKey>;
|
||||||
|
|
||||||
type StateProps = {
|
type ModalStateProps = {
|
||||||
[K in ModalKey]?: TabState[K];
|
[K in ModalKey]?: TabState[K];
|
||||||
};
|
};
|
||||||
type ModalRegistry = {
|
type StateProps = ModalStateProps & {
|
||||||
[K in ModalKey]: FC<{
|
shouldAnimateInterface: boolean;
|
||||||
|
};
|
||||||
|
type LegacyModalRegistry = {
|
||||||
|
[K in LegacyModalKey]: FC<{
|
||||||
modal: TabState[K];
|
modal: TabState[K];
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
type WrappedModalRegistry = {
|
||||||
|
[K in WrappedModalKey]: FC<{
|
||||||
|
modal: NonNullable<TabState[K]>;
|
||||||
|
isOpen: boolean;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
type Entries<T> = {
|
type Entries<T> = {
|
||||||
[K in keyof T]: [K, T[K]];
|
[K in keyof T]: [K, T[K]];
|
||||||
}[keyof T][];
|
}[keyof T][];
|
||||||
|
|
||||||
const MODALS: ModalRegistry = {
|
const POLL_MODAL_CLOSE_DURATION = 200;
|
||||||
|
const WRAPPED_MODAL_CLOSE_DURATIONS: Record<WrappedModalKey, number> = {
|
||||||
|
pollModal: POLL_MODAL_CLOSE_DURATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
type WrappedModalBoundaryProps<T> = {
|
||||||
|
modal: T;
|
||||||
|
ModalComponent: FC<{
|
||||||
|
modal: NonNullable<T>;
|
||||||
|
isOpen: boolean;
|
||||||
|
}>;
|
||||||
|
closeDuration: number;
|
||||||
|
shouldAnimateInterface: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WrappedModalBoundary = <T,>({
|
||||||
|
modal,
|
||||||
|
ModalComponent,
|
||||||
|
closeDuration,
|
||||||
|
shouldAnimateInterface,
|
||||||
|
}: WrappedModalBoundaryProps<T>) => {
|
||||||
|
const isOpen = Boolean(modal);
|
||||||
|
const renderingModal = useCurrentOrPrev(modal, true);
|
||||||
|
const { shouldRender } = useShowTransition({
|
||||||
|
isOpen,
|
||||||
|
withShouldRender: true,
|
||||||
|
closeDuration,
|
||||||
|
noCloseTransition: !shouldAnimateInterface,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!shouldRender || !renderingModal) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ModalComponent modal={renderingModal} isOpen={isOpen} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEGACY_MODALS: LegacyModalRegistry = {
|
||||||
aiMessageEditorModal: AiMessageEditorModal,
|
aiMessageEditorModal: AiMessageEditorModal,
|
||||||
giftCodeModal: GiftCodeModal,
|
giftCodeModal: GiftCodeModal,
|
||||||
boostModal: BoostModal,
|
boostModal: BoostModal,
|
||||||
@ -241,18 +294,41 @@ const MODALS: ModalRegistry = {
|
|||||||
editRankModal: EditRankModal,
|
editRankModal: EditRankModal,
|
||||||
rankModal: RankModal,
|
rankModal: RankModal,
|
||||||
};
|
};
|
||||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
const WRAPPED_MODALS: WrappedModalRegistry = {
|
||||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
pollModal: PollModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEGACY_MODAL_KEYS = Object.keys(LEGACY_MODALS) as LegacyModalKey[];
|
||||||
|
const WRAPPED_MODAL_KEYS = Object.keys(WRAPPED_MODALS) as WrappedModalKey[];
|
||||||
|
const MODAL_KEYS = [...LEGACY_MODAL_KEYS, ...WRAPPED_MODAL_KEYS] as ModalKey[];
|
||||||
|
|
||||||
|
const LEGACY_MODAL_ENTRIES = Object.entries(LEGACY_MODALS) as Entries<LegacyModalRegistry>;
|
||||||
|
const WRAPPED_MODAL_ENTRIES = Object.entries(WRAPPED_MODALS) as Entries<WrappedModalRegistry>;
|
||||||
|
|
||||||
const ModalContainer = (modalProps: StateProps) => {
|
const ModalContainer = (modalProps: StateProps) => {
|
||||||
return MODAL_ENTRIES.map(([key, ModalComponent]) => (
|
const { shouldAnimateInterface } = modalProps;
|
||||||
// @ts-ignore -- TS does not preserve tuple types in `map` callbacks
|
|
||||||
<ModalComponent key={key} modal={modalProps[key]} />
|
return [
|
||||||
));
|
...LEGACY_MODAL_ENTRIES.map(([key, ModalComponent]) => (
|
||||||
|
// @ts-ignore -- TS does not preserve tuple types in `map` callbacks
|
||||||
|
<ModalComponent key={key} modal={modalProps[key]} />
|
||||||
|
)),
|
||||||
|
...WRAPPED_MODAL_ENTRIES.map(([key, ModalComponent]) => (
|
||||||
|
// @ts-ignore -- TS does not preserve tuple types in `map` callbacks
|
||||||
|
<WrappedModalBoundary
|
||||||
|
key={key}
|
||||||
|
modal={modalProps[key]}
|
||||||
|
ModalComponent={ModalComponent}
|
||||||
|
closeDuration={WRAPPED_MODAL_CLOSE_DURATIONS[key]}
|
||||||
|
shouldAnimateInterface={shouldAnimateInterface}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal(
|
export default memo(withGlobal(
|
||||||
(global): Complete<StateProps> => (
|
(global): Complete<StateProps> => ({
|
||||||
pick(selectTabState(global), MODAL_KEYS) as Complete<StateProps>
|
...(pick(selectTabState(global), MODAL_KEYS) as Complete<ModalStateProps>),
|
||||||
),
|
shouldAnimateInterface: selectCanAnimateInterface(global),
|
||||||
|
}),
|
||||||
)(ModalContainer));
|
)(ModalContainer));
|
||||||
|
|||||||
141
src/components/modals/poll/PollModal.module.scss
Normal file
141
src/components/modals/poll/PollModal.module.scss
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
.input,
|
||||||
|
.optionInput,
|
||||||
|
.textArea {
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionRowFrame,
|
||||||
|
.durationMenuWrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionRowDragging {
|
||||||
|
--custom-cursor: grabbing;
|
||||||
|
|
||||||
|
will-change: transform;
|
||||||
|
cursor: var(--custom-cursor);
|
||||||
|
|
||||||
|
z-index: var(--z-modal-menu);
|
||||||
|
|
||||||
|
contain: content;
|
||||||
|
|
||||||
|
opacity: 0.95;
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 0 0.25rem 1rem var(--color-default-shadow);
|
||||||
|
|
||||||
|
input {
|
||||||
|
cursor: var(--custom-cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionRowAdd {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionLeadingIcon {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 1.5rem;
|
||||||
|
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionLeadingIconAdd {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionLeadingIconGlyph {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionDragHandle {
|
||||||
|
cursor: grab;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 0.125rem solid var(--color-primary);
|
||||||
|
outline-offset: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionSelector {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-inline-start: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionRadioInvalid {
|
||||||
|
--ui-border-color: var(--color-error);
|
||||||
|
--ui-accent-color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionInput {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&:global(.input-group) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionTextInput {
|
||||||
|
:global(.form-control) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionInputAdd {
|
||||||
|
:global(.form-control)::placeholder {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionRemove {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowValue {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorDescription {
|
||||||
|
padding: 0.75rem 0 0;
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.durationMenu :global(.backdrop) {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
949
src/components/modals/poll/PollModal.tsx
Normal file
949
src/components/modals/poll/PollModal.tsx
Normal file
@ -0,0 +1,949 @@
|
|||||||
|
import type { TeactNode } from '../../../lib/teact/teact';
|
||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiNewPoll } from '../../../api/types';
|
||||||
|
import type { TabState } from '../../../global/types';
|
||||||
|
import type { MessageList } from '../../../types';
|
||||||
|
import type { IconName } from '../../../types/icons';
|
||||||
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
|
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||||
|
import { isChatChannel } from '../../../global/helpers';
|
||||||
|
import { getChatNotifySettings } from '../../../global/helpers/notifications';
|
||||||
|
import { getPeerTitle } from '../../../global/helpers/peers';
|
||||||
|
import {
|
||||||
|
selectChat,
|
||||||
|
selectNotifyDefaults,
|
||||||
|
selectNotifyException,
|
||||||
|
selectPeerPaidMessagesStars,
|
||||||
|
selectTabState,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatDateTimeToString, formatShortDuration } from '../../../util/dates/oldDateFormat';
|
||||||
|
import { DAY, HOUR } from '../../../util/dates/units';
|
||||||
|
import { generateUniqueNumberId } from '../../../util/generateUniqueId';
|
||||||
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useReorderableList from '../../../hooks/useReorderableList';
|
||||||
|
import useSchedule from '../../../hooks/useSchedule';
|
||||||
|
import usePaidMessageConfirmation from '../../middle/composer/hooks/usePaidMessageConfirmation';
|
||||||
|
|
||||||
|
import CalendarModal from '../../common/CalendarModal.async';
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
|
import PaymentMessageConfirmDialog from '../../common/PaymentMessageConfirmDialog';
|
||||||
|
import CustomSendMenu from '../../middle/composer/CustomSendMenu.async';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import InputText from '../../ui/InputText';
|
||||||
|
import Menu from '../../ui/Menu';
|
||||||
|
import MenuItem from '../../ui/MenuItem';
|
||||||
|
import TextArea from '../../ui/TextArea';
|
||||||
|
import Control, {
|
||||||
|
ControlAfter,
|
||||||
|
ControlDescription,
|
||||||
|
ControlIcon,
|
||||||
|
ControlLabel,
|
||||||
|
} from '@gili/layout/Control';
|
||||||
|
import Interactive from '@gili/layout/Interactive';
|
||||||
|
import Island, {
|
||||||
|
IslandDescription,
|
||||||
|
IslandTitle,
|
||||||
|
} from '@gili/layout/Island';
|
||||||
|
import Modal, {
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalHeader,
|
||||||
|
ModalHeaderAction,
|
||||||
|
ModalTitle,
|
||||||
|
} from '@gili/modal/Modal';
|
||||||
|
import Checkbox from '@gili/primitives/Checkbox';
|
||||||
|
import Radio from '@gili/primitives/Radio';
|
||||||
|
import Switch from '@gili/primitives/Switch';
|
||||||
|
|
||||||
|
import styles from './PollModal.module.scss';
|
||||||
|
|
||||||
|
const MAX_OPTION_LENGTH = 100;
|
||||||
|
const MAX_QUESTION_LENGTH = 255;
|
||||||
|
const MAX_SOLUTION_LENGTH = 200;
|
||||||
|
|
||||||
|
const CLOSE_PERIOD_OPTIONS = [
|
||||||
|
HOUR,
|
||||||
|
3 * HOUR,
|
||||||
|
8 * HOUR,
|
||||||
|
DAY,
|
||||||
|
3 * DAY,
|
||||||
|
];
|
||||||
|
|
||||||
|
const ICON_COLORS = {
|
||||||
|
anonymous: '#0a84ff',
|
||||||
|
multiple: '#ffb300',
|
||||||
|
quiz: '#34c759',
|
||||||
|
addAnswers: '#2faeff',
|
||||||
|
revote: '#6a5cff',
|
||||||
|
shuffle: '#b75bff',
|
||||||
|
duration: '#ff5e3a',
|
||||||
|
results: '#3a8cff',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
modal: NonNullable<TabState['pollModal']>;
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
isChannel?: boolean;
|
||||||
|
pollMaxAnswers: number;
|
||||||
|
pollClosePeriodMax: number;
|
||||||
|
paidMessagesStars?: number;
|
||||||
|
isPaymentMessageConfirmDialogOpen: boolean;
|
||||||
|
starsBalance: number;
|
||||||
|
isStarsBalanceModalOpen: boolean;
|
||||||
|
isSilentPosting?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SettingRowProps = {
|
||||||
|
iconName?: IconName;
|
||||||
|
iconBackgroundColor?: string;
|
||||||
|
label: TeactNode;
|
||||||
|
description?: TeactNode;
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
locked?: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValueRowProps = {
|
||||||
|
iconName?: IconName;
|
||||||
|
iconBackgroundColor?: string;
|
||||||
|
label: TeactNode;
|
||||||
|
value: TeactNode;
|
||||||
|
onClick: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PollOption = {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PollModal = ({
|
||||||
|
modal,
|
||||||
|
isOpen,
|
||||||
|
chat,
|
||||||
|
isChannel,
|
||||||
|
pollMaxAnswers,
|
||||||
|
pollClosePeriodMax,
|
||||||
|
paidMessagesStars,
|
||||||
|
isPaymentMessageConfirmDialogOpen,
|
||||||
|
starsBalance,
|
||||||
|
isStarsBalanceModalOpen,
|
||||||
|
isSilentPosting,
|
||||||
|
}: OwnProps & StateProps) => {
|
||||||
|
const {
|
||||||
|
closePollModal,
|
||||||
|
sendMessage,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const questionInputRef = useRef<HTMLInputElement>();
|
||||||
|
const mainButtonRef = useRef<HTMLButtonElement>();
|
||||||
|
const optionListRef = useRef<HTMLDivElement>();
|
||||||
|
const durationMenuRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const [question, setQuestion] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [options, setOptions] = useState<PollOption[]>(() => [createPollOption()]);
|
||||||
|
const [isPublic, setIsPublic] = useState(true);
|
||||||
|
const [isMultipleAnswers, setIsMultipleAnswers] = useState(true);
|
||||||
|
const [isQuizMode, setIsQuizMode] = useState(Boolean(modal.isQuiz));
|
||||||
|
const [correctAnswerIds, setCorrectAnswerIds] = useState<string[]>([]);
|
||||||
|
const [solution, setSolution] = useState('');
|
||||||
|
const [canAddAnswers, setCanAddAnswers] = useState(true);
|
||||||
|
const [canRevote, setCanRevote] = useState(true);
|
||||||
|
const [shouldShuffleAnswers, setShouldShuffleAnswers] = useState(false);
|
||||||
|
const [closePeriod, setClosePeriod] = useState<number | undefined>();
|
||||||
|
const [closeDate, setCloseDate] = useState<number | undefined>();
|
||||||
|
const [durationAnchorAt, setDurationAnchorAt] = useState(() => getServerTime());
|
||||||
|
const [shouldHideResultsUntilClose, setShouldHideResultsUntilClose] = useState(false);
|
||||||
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
|
const [isCloseDatePickerOpen, setIsCloseDatePickerOpen] = useState(false);
|
||||||
|
|
||||||
|
const [requestCalendar, calendar] = useSchedule();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isContextMenuOpen: isCustomSendMenuOpen,
|
||||||
|
handleContextMenu,
|
||||||
|
handleContextMenuClose,
|
||||||
|
handleContextMenuHide,
|
||||||
|
} = useContextMenuHandlers(mainButtonRef, !isOpen || modal.messageListType === 'scheduled');
|
||||||
|
|
||||||
|
const {
|
||||||
|
isContextMenuOpen: isDurationMenuOpen,
|
||||||
|
handleContextMenu: handleDurationMenuOpen,
|
||||||
|
handleContextMenuClose: handleDurationMenuClose,
|
||||||
|
handleContextMenuHide: handleDurationMenuHide,
|
||||||
|
} = useContextMenuHandlers(durationMenuRef, !isOpen);
|
||||||
|
|
||||||
|
const {
|
||||||
|
closeConfirmDialog,
|
||||||
|
dialogHandler,
|
||||||
|
shouldAutoApprove,
|
||||||
|
setAutoApprove,
|
||||||
|
handleWithConfirmation,
|
||||||
|
} = usePaidMessageConfirmation(
|
||||||
|
paidMessagesStars || 0,
|
||||||
|
isStarsBalanceModalOpen,
|
||||||
|
starsBalance,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
questionInputRef.current?.focus();
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isChannel) {
|
||||||
|
setIsPublic(false);
|
||||||
|
setCanAddAnswers(false);
|
||||||
|
}
|
||||||
|
}, [isChannel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isQuizMode || !isPublic) {
|
||||||
|
setCanAddAnswers(false);
|
||||||
|
}
|
||||||
|
}, [isPublic, isQuizMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (closePeriod || closeDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setShouldHideResultsUntilClose(false);
|
||||||
|
}, [closeDate, closePeriod]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMultipleAnswers && correctAnswerIds.length > 1) {
|
||||||
|
setCorrectAnswerIds(correctAnswerIds.slice(0, 1));
|
||||||
|
}
|
||||||
|
}, [correctAnswerIds, isMultipleAnswers]);
|
||||||
|
|
||||||
|
const filledOptions = useMemo(() => {
|
||||||
|
return options.map((option) => ({
|
||||||
|
id: option.id,
|
||||||
|
text: option.text.trim().substring(0, MAX_OPTION_LENGTH),
|
||||||
|
})).filter(({ text }) => Boolean(text));
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
|
const correctAnswerPositions = useMemo(() => {
|
||||||
|
return correctAnswerIds.reduce<number[]>((result, id) => {
|
||||||
|
const answerIndex = filledOptions.findIndex((option) => option.id === id);
|
||||||
|
|
||||||
|
if (answerIndex >= 0) {
|
||||||
|
result.push(answerIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
}, [correctAnswerIds, filledOptions]);
|
||||||
|
|
||||||
|
const reorderableOptionIds = useMemo(() => {
|
||||||
|
return filledOptions.map(({ id }) => id);
|
||||||
|
}, [filledOptions]);
|
||||||
|
|
||||||
|
const trimmedQuestion = useMemo(() => question.trim().substring(0, MAX_QUESTION_LENGTH), [question]);
|
||||||
|
const isInScheduledList = modal.messageListType === 'scheduled';
|
||||||
|
const canSchedule = Boolean(!paidMessagesStars && !chat?.isMonoforum);
|
||||||
|
const isCorrectAnswerInvalid = hasSubmitted && isQuizMode && !correctAnswerPositions.length;
|
||||||
|
const isAddAnswersDisabled = isQuizMode || !isPublic;
|
||||||
|
const remainingOptionsCount = Math.max(pollMaxAnswers - filledOptions.length, 0);
|
||||||
|
const isSendDisabled = !trimmedQuestion
|
||||||
|
|| filledOptions.length < 1
|
||||||
|
|| (isQuizMode && !correctAnswerPositions.length);
|
||||||
|
|
||||||
|
const hasLimitedDuration = closePeriod !== undefined || closeDate !== undefined;
|
||||||
|
const closeDateLabel = closeDate !== undefined
|
||||||
|
? formatDateTimeToString(closeDate * 1000, lang.code, true)
|
||||||
|
: closePeriod !== undefined
|
||||||
|
? formatShortDuration(lang, closePeriod)
|
||||||
|
: lang('PollSelectCloseDate');
|
||||||
|
const maxCloseDateAt = (durationAnchorAt + pollClosePeriodMax) * 1000;
|
||||||
|
const closeDatePickerSelectedAt = closeDate !== undefined
|
||||||
|
? closeDate * 1000
|
||||||
|
: (durationAnchorAt + (closePeriod || DAY)) * 1000;
|
||||||
|
const messageList: MessageList = {
|
||||||
|
chatId: modal.chatId,
|
||||||
|
threadId: modal.threadId || MAIN_THREAD_ID,
|
||||||
|
type: modal.messageListType === 'scheduled' ? 'scheduled' : 'thread',
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
closePollModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleReorderOptions = useLastCallback((optionIds: string[]) => {
|
||||||
|
setOptions((currentOptions) => {
|
||||||
|
const optionsById = new Map(currentOptions.map((option) => [option.id, option]));
|
||||||
|
const nextOptions = optionIds.reduce<PollOption[]>((result, id) => {
|
||||||
|
const option = optionsById.get(id);
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
result.push(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return normalizeOptions(nextOptions, pollMaxAnswers);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
draggedId: draggedOptionId,
|
||||||
|
getRowProps: getReorderableRowProps,
|
||||||
|
getDragElementProps: getReorderableDragElementProps,
|
||||||
|
getHandleProps: getReorderableHandleProps,
|
||||||
|
getPlaceholderStyle: getReorderablePlaceholderStyle,
|
||||||
|
getDragStyle: getReorderableDragStyle,
|
||||||
|
} = useReorderableList({
|
||||||
|
itemIds: reorderableOptionIds,
|
||||||
|
withAutoscroll: true,
|
||||||
|
onReorder: handleReorderOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOption = useLastCallback((id: string, value: string) => {
|
||||||
|
const nextOptions = options.map((option) => (
|
||||||
|
option.id === id ? { ...option, text: value } : option
|
||||||
|
));
|
||||||
|
|
||||||
|
setOptions(normalizeOptions(nextOptions, pollMaxAnswers));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemoveOption = useLastCallback((id: string) => {
|
||||||
|
const nextOptions = normalizeOptions(options.filter((option) => option.id !== id), pollMaxAnswers);
|
||||||
|
setOptions(nextOptions);
|
||||||
|
setCorrectAnswerIds(correctAnswerIds.filter((correctAnswerId) => correctAnswerId !== id));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleCorrectAnswer = useLastCallback((id: string) => {
|
||||||
|
if (!isMultipleAnswers) {
|
||||||
|
setCorrectAnswerIds([id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCorrectAnswerIds((prevCorrectAnswers) => (
|
||||||
|
prevCorrectAnswers.includes(id)
|
||||||
|
? prevCorrectAnswers.filter((currentId) => currentId !== id)
|
||||||
|
: [...prevCorrectAnswers, id]
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOptionKeyDown = useLastCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!optionListRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = optionListRef.current.querySelectorAll<HTMLInputElement>(`.${styles.optionTextInput} input`);
|
||||||
|
const lastInput = inputs[inputs.length - 1];
|
||||||
|
|
||||||
|
if (!lastInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMeasure(() => {
|
||||||
|
lastInput.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleQuestionChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setQuestion(e.currentTarget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDescriptionChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setDescription(e.currentTarget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSolutionChange = useLastCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setSolution(e.currentTarget.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleQuizModeChange = useLastCallback((checked: boolean) => {
|
||||||
|
setIsQuizMode(checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMultipleAnswersChange = useLastCallback((checked: boolean) => {
|
||||||
|
setIsMultipleAnswers(checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleLimitedDurationChange = useLastCallback((checked: boolean) => {
|
||||||
|
if (!checked) {
|
||||||
|
setClosePeriod(undefined);
|
||||||
|
setCloseDate(undefined);
|
||||||
|
setShouldHideResultsUntilClose(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowAt = Math.floor(Date.now() / 1000);
|
||||||
|
setDurationAnchorAt(nowAt);
|
||||||
|
setClosePeriod(DAY);
|
||||||
|
setCloseDate(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCloseDateSave = useLastCallback((date: Date) => {
|
||||||
|
setClosePeriod(undefined);
|
||||||
|
setCloseDate(Math.round(date.getTime() / 1000));
|
||||||
|
setIsCloseDatePickerOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCloseCloseDatePicker = useLastCallback(() => {
|
||||||
|
setIsCloseDatePickerOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenCloseDatePicker = useLastCallback(() => {
|
||||||
|
setDurationAnchorAt(Math.floor(Date.now() / 1000));
|
||||||
|
setIsCloseDatePickerOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelectClosePeriod = useLastCallback((period: number) => {
|
||||||
|
setClosePeriod(period);
|
||||||
|
setCloseDate(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildPoll = useLastCallback((): ApiNewPoll | undefined => {
|
||||||
|
const normalizedOptions = normalizeOptions(
|
||||||
|
options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
text: option.text.trim().substring(0, MAX_OPTION_LENGTH),
|
||||||
|
})),
|
||||||
|
pollMaxAnswers,
|
||||||
|
);
|
||||||
|
|
||||||
|
setQuestion(trimmedQuestion);
|
||||||
|
setOptions(normalizedOptions);
|
||||||
|
setHasSubmitted(true);
|
||||||
|
|
||||||
|
if (!trimmedQuestion || filledOptions.length < 1) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQuizMode && !correctAnswerPositions.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answers = filledOptions.map(({ text }, index) => ({
|
||||||
|
text: { text },
|
||||||
|
option: String(index),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const payload: ApiNewPoll = {
|
||||||
|
summary: {
|
||||||
|
id: generateUniqueNumberId().toString(),
|
||||||
|
hash: '0',
|
||||||
|
question: {
|
||||||
|
text: trimmedQuestion,
|
||||||
|
},
|
||||||
|
answers,
|
||||||
|
isPublic: !isChannel && isPublic ? true : undefined,
|
||||||
|
isMultipleChoice: isMultipleAnswers ? true : undefined,
|
||||||
|
isQuiz: isQuizMode ? true : undefined,
|
||||||
|
canAddAnswers: !isChannel && isPublic && canAddAnswers ? true : undefined,
|
||||||
|
isRevoteDisabled: !canRevote ? true : undefined,
|
||||||
|
shouldShuffleAnswers: shouldShuffleAnswers ? true : undefined,
|
||||||
|
shouldHideResultsUntilClose: shouldHideResultsUntilClose ? true : undefined,
|
||||||
|
closePeriod,
|
||||||
|
closeDate,
|
||||||
|
isCreator: true,
|
||||||
|
},
|
||||||
|
correctAnswers: isQuizMode ? correctAnswerPositions : undefined,
|
||||||
|
solution: isQuizMode ? solution.trim().substring(0, MAX_SOLUTION_LENGTH) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitPoll = useLastCallback((
|
||||||
|
poll: ApiNewPoll,
|
||||||
|
isSilent?: boolean,
|
||||||
|
scheduledAt?: number,
|
||||||
|
scheduleRepeatPeriod?: number,
|
||||||
|
) => {
|
||||||
|
sendMessage({
|
||||||
|
messageList,
|
||||||
|
text: description,
|
||||||
|
poll,
|
||||||
|
isSilent: scheduledAt ? undefined : (isSilent || isSilentPosting),
|
||||||
|
scheduledAt,
|
||||||
|
scheduleRepeatPeriod,
|
||||||
|
});
|
||||||
|
closePollModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendNow = useLastCallback((isSilent?: boolean) => {
|
||||||
|
const poll = buildPoll();
|
||||||
|
|
||||||
|
if (!poll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWithConfirmation(submitPoll, poll, isSilent);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSendScheduled = useLastCallback((scheduledAt: number, scheduleRepeatPeriod?: number) => {
|
||||||
|
const poll = buildPoll();
|
||||||
|
|
||||||
|
if (!poll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWithConfirmation(submitPoll, poll, undefined, scheduledAt, scheduleRepeatPeriod);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePrimarySend = useLastCallback(() => {
|
||||||
|
if (isInScheduledList) {
|
||||||
|
requestCalendar(handleSendScheduled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSendNow();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSilentSend = useLastCallback(() => {
|
||||||
|
handleSendNow(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleScheduleSend = useLastCallback(() => {
|
||||||
|
requestCalendar(handleSendScheduled);
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderHeader = useMemo(() => (
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalTitle>{lang('NewPoll')}</ModalTitle>
|
||||||
|
<ModalHeaderAction>
|
||||||
|
<Button
|
||||||
|
ref={mainButtonRef}
|
||||||
|
color="primary"
|
||||||
|
pill
|
||||||
|
disabled={isSendDisabled}
|
||||||
|
noForcedUpperCase
|
||||||
|
size="smaller"
|
||||||
|
onClick={handlePrimarySend}
|
||||||
|
onContextMenu={!isInScheduledList ? handleContextMenu : undefined}
|
||||||
|
>
|
||||||
|
{lang('Send')}
|
||||||
|
</Button>
|
||||||
|
{!isInScheduledList && (
|
||||||
|
<CustomSendMenu
|
||||||
|
isOpen={isCustomSendMenuOpen}
|
||||||
|
canSchedule={canSchedule}
|
||||||
|
onSendSilent={handleSilentSend}
|
||||||
|
onSendSchedule={handleScheduleSend}
|
||||||
|
onClose={handleContextMenuClose}
|
||||||
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalHeaderAction>
|
||||||
|
</ModalHeader>
|
||||||
|
), [canSchedule, handleContextMenu, handleContextMenuClose, handleContextMenuHide,
|
||||||
|
isCustomSendMenuOpen, isInScheduledList, isSendDisabled, lang]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
header={renderHeader}
|
||||||
|
ariaLabel={lang('NewPoll')}
|
||||||
|
width="slim"
|
||||||
|
>
|
||||||
|
<IslandTitle>{lang('PollModalQuestionTitle')}</IslandTitle>
|
||||||
|
<Island>
|
||||||
|
<InputText
|
||||||
|
ref={questionInputRef}
|
||||||
|
className={styles.input}
|
||||||
|
label={lang('AskAQuestion')}
|
||||||
|
value={question}
|
||||||
|
maxLength={MAX_QUESTION_LENGTH}
|
||||||
|
error={hasSubmitted && !trimmedQuestion ? lang('PollsChooseQuestion') : undefined}
|
||||||
|
onChange={handleQuestionChange}
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
className={styles.input}
|
||||||
|
label={lang('DescriptionOptionalPlaceholder')}
|
||||||
|
value={description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
|
||||||
|
<IslandTitle>{lang('PollModalOptionsTitle')}</IslandTitle>
|
||||||
|
<Island ref={optionListRef} className={styles.optionList} teactFastList>
|
||||||
|
{options.map((option, index) => {
|
||||||
|
const isFilledOption = Boolean(option.text.trim());
|
||||||
|
const isAddOptionRow = !isFilledOption && index === options.length - 1;
|
||||||
|
const isCorrectAnswerChecked = correctAnswerIds.includes(option.id);
|
||||||
|
const shouldShowRemoveButton = options.length > 1 && !isAddOptionRow;
|
||||||
|
const shouldShowOptionError = hasSubmitted && !filledOptions.length && index === 0;
|
||||||
|
const rowProps = isFilledOption ? getReorderableRowProps(option.id) : undefined;
|
||||||
|
const handleProps = isFilledOption ? getReorderableHandleProps(option.id) : undefined;
|
||||||
|
const dragElementProps = isFilledOption ? getReorderableDragElementProps(option.id) : undefined;
|
||||||
|
const placeholderStyle = isFilledOption ? getReorderablePlaceholderStyle(option.id) : undefined;
|
||||||
|
const dragStyle = isFilledOption ? getReorderableDragStyle(option.id) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.id}
|
||||||
|
ref={rowProps?.ref}
|
||||||
|
className={styles.optionRowFrame}
|
||||||
|
style={placeholderStyle}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={dragElementProps?.ref}
|
||||||
|
style={dragStyle}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.optionRow,
|
||||||
|
isAddOptionRow && styles.optionRowAdd,
|
||||||
|
draggedOptionId === option.id && styles.optionRowDragging,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.optionLeadingIcon,
|
||||||
|
isAddOptionRow && styles.optionLeadingIconAdd,
|
||||||
|
isFilledOption && styles.optionDragHandle,
|
||||||
|
)}
|
||||||
|
role={handleProps?.role}
|
||||||
|
tabIndex={handleProps?.tabIndex}
|
||||||
|
aria-label={isFilledOption ? lang('DragToSortAria') : undefined}
|
||||||
|
onMouseDown={handleProps?.onMouseDown}
|
||||||
|
onTouchStart={handleProps?.onTouchStart}
|
||||||
|
onKeyDown={handleProps?.onKeyDown}
|
||||||
|
ref={handleProps?.ref}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={isAddOptionRow ? 'add' : 'sort'}
|
||||||
|
className={styles.optionLeadingIconGlyph}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isQuizMode && (
|
||||||
|
<div className={styles.optionSelector}>
|
||||||
|
{isMultipleAnswers ? (
|
||||||
|
<Checkbox
|
||||||
|
checked={isCorrectAnswerChecked}
|
||||||
|
disabled={isAddOptionRow}
|
||||||
|
isInvalid={isCorrectAnswerInvalid && !isCorrectAnswerChecked && !isAddOptionRow}
|
||||||
|
onChange={() => handleToggleCorrectAnswer(option.id)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Radio
|
||||||
|
value={option.id}
|
||||||
|
checked={isCorrectAnswerChecked}
|
||||||
|
disabled={isAddOptionRow}
|
||||||
|
className={isCorrectAnswerInvalid && !isCorrectAnswerChecked && !isAddOptionRow
|
||||||
|
? styles.optionRadioInvalid
|
||||||
|
: undefined}
|
||||||
|
onChange={() => handleToggleCorrectAnswer(option.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<InputText
|
||||||
|
className={buildClassName(
|
||||||
|
styles.optionInput,
|
||||||
|
styles.optionTextInput,
|
||||||
|
isAddOptionRow && styles.optionInputAdd,
|
||||||
|
)}
|
||||||
|
placeholder={isAddOptionRow ? lang('CreatePollAddOption') : lang('OptionHint')}
|
||||||
|
value={option.text}
|
||||||
|
maxLength={MAX_OPTION_LENGTH}
|
||||||
|
error={shouldShowOptionError ? lang('PollsChooseAnswers') : undefined}
|
||||||
|
onChange={(e) => updateOption(option.id, e.currentTarget.value)}
|
||||||
|
onKeyDown={handleOptionKeyDown}
|
||||||
|
/>
|
||||||
|
{shouldShowRemoveButton && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent"
|
||||||
|
className={styles.optionRemove}
|
||||||
|
ariaLabel={lang('Delete')}
|
||||||
|
iconName="close"
|
||||||
|
onClick={() => handleRemoveOption(option.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{isCorrectAnswerInvalid && (
|
||||||
|
<IslandDescription key="correct-answer-error" className={styles.errorDescription}>
|
||||||
|
{lang('PollsChooseCorrect')}
|
||||||
|
</IslandDescription>
|
||||||
|
)}
|
||||||
|
</Island>
|
||||||
|
<IslandDescription>
|
||||||
|
{remainingOptionsCount > 0 ? (
|
||||||
|
lang('PollModalAddMoreText', { count: remainingOptionsCount }, { pluralValue: remainingOptionsCount })
|
||||||
|
) : lang('PollModalAddNoMore')}
|
||||||
|
</IslandDescription>
|
||||||
|
|
||||||
|
<IslandTitle>{lang('PollModalSettingsTitle')}</IslandTitle>
|
||||||
|
<Island>
|
||||||
|
{!isChannel && (
|
||||||
|
<SettingRow
|
||||||
|
iconName="eye"
|
||||||
|
iconBackgroundColor={ICON_COLORS.anonymous}
|
||||||
|
label={lang('PollAnswersVisible')}
|
||||||
|
description={lang('PollAnswersVisibleDescription')}
|
||||||
|
checked={isPublic}
|
||||||
|
onChange={setIsPublic}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingRow
|
||||||
|
iconName="choice-selected"
|
||||||
|
iconBackgroundColor={ICON_COLORS.multiple}
|
||||||
|
label={lang('PollMultiple')}
|
||||||
|
description={lang('PollMultipleDescription')}
|
||||||
|
checked={isMultipleAnswers}
|
||||||
|
onChange={handleMultipleAnswersChange}
|
||||||
|
/>
|
||||||
|
{!isChannel && (
|
||||||
|
<SettingRow
|
||||||
|
iconName="add-filled"
|
||||||
|
iconBackgroundColor={ICON_COLORS.addAnswers}
|
||||||
|
label={lang('PollAllowAddingAnswers')}
|
||||||
|
description={lang('PollAllowAddingAnswersDescription')}
|
||||||
|
checked={canAddAnswers}
|
||||||
|
disabled={isAddAnswersDisabled}
|
||||||
|
locked={isAddAnswersDisabled}
|
||||||
|
onChange={setCanAddAnswers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SettingRow
|
||||||
|
iconName="reload"
|
||||||
|
iconBackgroundColor={ICON_COLORS.revote}
|
||||||
|
label={lang('PollAllowVoteChanges')}
|
||||||
|
description={lang('PollAllowVoteChangesDescription')}
|
||||||
|
checked={canRevote}
|
||||||
|
onChange={setCanRevote}
|
||||||
|
/>
|
||||||
|
<SettingRow
|
||||||
|
iconName="replace-round"
|
||||||
|
iconBackgroundColor={ICON_COLORS.shuffle}
|
||||||
|
label={lang('PollRandomOrder')}
|
||||||
|
description={lang('PollRandomOrderDescription')}
|
||||||
|
checked={shouldShuffleAnswers}
|
||||||
|
onChange={setShouldShuffleAnswers}
|
||||||
|
/>
|
||||||
|
<SettingRow
|
||||||
|
iconName="check-filled"
|
||||||
|
iconBackgroundColor={ICON_COLORS.quiz}
|
||||||
|
label={lang('PollQuiz')}
|
||||||
|
description={lang('PollQuizDescription')}
|
||||||
|
checked={isQuizMode}
|
||||||
|
onChange={handleQuizModeChange}
|
||||||
|
/>
|
||||||
|
<SettingRow
|
||||||
|
iconName="timer-filled"
|
||||||
|
iconBackgroundColor={ICON_COLORS.duration}
|
||||||
|
label={lang('PollLimitedDuration')}
|
||||||
|
description={lang('PollLimitedDurationDescription')}
|
||||||
|
checked={hasLimitedDuration}
|
||||||
|
onChange={handleLimitedDurationChange}
|
||||||
|
/>
|
||||||
|
{hasLimitedDuration ? (
|
||||||
|
<>
|
||||||
|
<div ref={durationMenuRef} className={styles.durationMenuWrapper}>
|
||||||
|
<ValueRow
|
||||||
|
label={lang('PollDuration')}
|
||||||
|
value={closeDateLabel}
|
||||||
|
onClick={handleDurationMenuOpen}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
isOpen={isDurationMenuOpen}
|
||||||
|
className={buildClassName('with-menu-transitions', styles.durationMenu)}
|
||||||
|
positionX="right"
|
||||||
|
positionY="bottom"
|
||||||
|
autoClose
|
||||||
|
onClose={handleDurationMenuClose}
|
||||||
|
onCloseAnimationEnd={handleDurationMenuHide}
|
||||||
|
>
|
||||||
|
{CLOSE_PERIOD_OPTIONS.map((period) => (
|
||||||
|
<MenuItem
|
||||||
|
key={period}
|
||||||
|
disabled={period > pollClosePeriodMax}
|
||||||
|
onClick={() => handleSelectClosePeriod(period)}
|
||||||
|
>
|
||||||
|
{formatShortDuration(lang, period)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
<MenuItem onClick={handleOpenCloseDatePicker}>
|
||||||
|
{lang('PollDurationOther')}
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
<SettingRow
|
||||||
|
label={lang('PollHideResultsUntilClose')}
|
||||||
|
checked={shouldHideResultsUntilClose}
|
||||||
|
onChange={setShouldHideResultsUntilClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : undefined}
|
||||||
|
</Island>
|
||||||
|
|
||||||
|
{isQuizMode && (
|
||||||
|
<>
|
||||||
|
<IslandTitle>{lang('PollsSolutionTitle')}</IslandTitle>
|
||||||
|
<Island>
|
||||||
|
<TextArea
|
||||||
|
className={styles.textArea}
|
||||||
|
value={solution}
|
||||||
|
label={lang('PollsSolutionTitle')}
|
||||||
|
maxLength={MAX_SOLUTION_LENGTH}
|
||||||
|
noReplaceNewlines
|
||||||
|
onChange={handleSolutionChange}
|
||||||
|
/>
|
||||||
|
</Island>
|
||||||
|
<IslandDescription>{lang('CreatePollExplanationInfo')}</IslandDescription>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
{calendar}
|
||||||
|
<CalendarModal
|
||||||
|
isOpen={isCloseDatePickerOpen}
|
||||||
|
selectedAt={closeDatePickerSelectedAt}
|
||||||
|
maxAt={maxCloseDateAt}
|
||||||
|
isFutureMode
|
||||||
|
withTimePicker
|
||||||
|
submitButtonLabel={lang('Save')}
|
||||||
|
onClose={handleCloseCloseDatePicker}
|
||||||
|
onSubmit={handleCloseDateSave}
|
||||||
|
/>
|
||||||
|
<PaymentMessageConfirmDialog
|
||||||
|
isOpen={isPaymentMessageConfirmDialogOpen}
|
||||||
|
onClose={closeConfirmDialog}
|
||||||
|
userName={chat ? getPeerTitle(lang, chat) : undefined}
|
||||||
|
messagePriceInStars={paidMessagesStars || 0}
|
||||||
|
messagesCount={1}
|
||||||
|
shouldAutoApprove={shouldAutoApprove}
|
||||||
|
setAutoApprove={setAutoApprove}
|
||||||
|
confirmHandler={dialogHandler}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingRow = ({
|
||||||
|
iconName,
|
||||||
|
iconBackgroundColor,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
locked,
|
||||||
|
onChange,
|
||||||
|
}: SettingRowProps) => {
|
||||||
|
return (
|
||||||
|
<Interactive asLabel clickable disabled={disabled}>
|
||||||
|
<Control inputEnd>
|
||||||
|
<Switch checked={checked} disabled={disabled} locked={locked} onChange={onChange} />
|
||||||
|
<ControlIcon iconName={iconName} backgroundColor={iconBackgroundColor} />
|
||||||
|
<ControlLabel>{label}</ControlLabel>
|
||||||
|
{description !== undefined ? (
|
||||||
|
<ControlDescription>{description}</ControlDescription>
|
||||||
|
) : undefined}
|
||||||
|
</Control>
|
||||||
|
</Interactive>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ValueRow = ({
|
||||||
|
iconName,
|
||||||
|
iconBackgroundColor,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onClick,
|
||||||
|
}: ValueRowProps) => {
|
||||||
|
return (
|
||||||
|
<Interactive clickable onClick={onClick}>
|
||||||
|
<Control>
|
||||||
|
<ControlIcon iconName={iconName} backgroundColor={iconBackgroundColor} />
|
||||||
|
<ControlLabel>{label}</ControlLabel>
|
||||||
|
<ControlAfter className={styles.rowValue}>
|
||||||
|
{value}
|
||||||
|
</ControlAfter>
|
||||||
|
</Control>
|
||||||
|
</Interactive>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPollOption(text = ''): PollOption {
|
||||||
|
return {
|
||||||
|
id: generateUniqueNumberId().toString(),
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptions(options: PollOption[], maxOptionsCount: number) {
|
||||||
|
const nextOptions = [...options];
|
||||||
|
|
||||||
|
while (
|
||||||
|
nextOptions.length > 1
|
||||||
|
&& !nextOptions[nextOptions.length - 1].text.trim()
|
||||||
|
&& !nextOptions[nextOptions.length - 2].text.trim()
|
||||||
|
) {
|
||||||
|
nextOptions.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextOptions.length) {
|
||||||
|
nextOptions.push(createPollOption());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextOptions.length < maxOptionsCount && nextOptions[nextOptions.length - 1].text.trim()) {
|
||||||
|
nextOptions.push(createPollOption());
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { modal }): Complete<StateProps> => {
|
||||||
|
const tabState = selectTabState(global);
|
||||||
|
const { chatId } = modal;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
isChannel: chat ? isChatChannel(chat) : undefined,
|
||||||
|
pollMaxAnswers: global.appConfig.pollMaxAnswers,
|
||||||
|
pollClosePeriodMax: global.appConfig.pollClosePeriodMax,
|
||||||
|
paidMessagesStars: selectPeerPaidMessagesStars(global, chatId),
|
||||||
|
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen,
|
||||||
|
starsBalance: global.stars?.balance.amount || 0,
|
||||||
|
isStarsBalanceModalOpen: Boolean(tabState.starsBalanceModal),
|
||||||
|
isSilentPosting: chat ? getChatNotifySettings(
|
||||||
|
chat,
|
||||||
|
selectNotifyDefaults(global),
|
||||||
|
selectNotifyException(global, chat.id),
|
||||||
|
)?.isSilentPosting : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(PollModal));
|
||||||
@ -66,6 +66,7 @@ const ConfirmDialog: FC<OwnProps> = ({
|
|||||||
header={header}
|
header={header}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
isNativeDialog
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
{text && text.split('\\n').map((textPart) => (
|
{text && text.split('\\n').map((textPart) => (
|
||||||
|
|||||||
@ -4,6 +4,10 @@
|
|||||||
z-index: var(--z-modal);
|
z-index: var(--z-modal);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
&.confirm,
|
&.confirm,
|
||||||
&.pin {
|
&.pin {
|
||||||
z-index: var(--z-modal-confirm);
|
z-index: var(--z-modal-confirm);
|
||||||
@ -295,4 +299,23 @@
|
|||||||
right: 0.875rem;
|
right: 0.875rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog.Modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
import type { ElementRef, TeactNode } from '../../lib/teact/teact';
|
||||||
import type React from '../../lib/teact/teact';
|
import type React from '../../lib/teact/teact';
|
||||||
import { beginHeavyAnimation, useEffect, useRef } from '../../lib/teact/teact';
|
import {
|
||||||
|
beginHeavyAnimation, useEffect, useLayoutEffect, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { TextPart } from '../../types';
|
import type { TextPart } from '../../types';
|
||||||
|
|
||||||
@ -41,6 +43,7 @@ export type OwnProps = {
|
|||||||
isBackButton?: boolean;
|
isBackButton?: boolean;
|
||||||
noBackdrop?: boolean;
|
noBackdrop?: boolean;
|
||||||
noBackdropClose?: boolean;
|
noBackdropClose?: boolean;
|
||||||
|
isNativeDialog?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
style?: string;
|
style?: string;
|
||||||
dialogStyle?: string;
|
dialogStyle?: string;
|
||||||
@ -64,6 +67,7 @@ const Modal = (props: OwnProps) => {
|
|||||||
isOpen,
|
isOpen,
|
||||||
noBackdropClose,
|
noBackdropClose,
|
||||||
noFreezeOnClose,
|
noFreezeOnClose,
|
||||||
|
isNativeDialog,
|
||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
onEnter,
|
onEnter,
|
||||||
@ -72,7 +76,7 @@ const Modal = (props: OwnProps) => {
|
|||||||
const {
|
const {
|
||||||
ref: modalRef,
|
ref: modalRef,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
} = useShowTransition({
|
} = useShowTransition<HTMLElement>({
|
||||||
isOpen,
|
isOpen,
|
||||||
withShouldRender: true,
|
withShouldRender: true,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
@ -117,6 +121,8 @@ const Modal = (props: OwnProps) => {
|
|||||||
} = useContextMenuHandlers(moreButtonRef);
|
} = useContextMenuHandlers(moreButtonRef);
|
||||||
|
|
||||||
const actualDialogRef = dialogRef || localDialogRef;
|
const actualDialogRef = dialogRef || localDialogRef;
|
||||||
|
const divModalRef = modalRef as ElementRef<HTMLDivElement>;
|
||||||
|
const nativeDialogRef = modalRef as ElementRef<HTMLDialogElement>;
|
||||||
|
|
||||||
const getRootElement = useLastCallback(() => actualDialogRef.current);
|
const getRootElement = useLastCallback(() => actualDialogRef.current);
|
||||||
const getTriggerElement = useLastCallback(() => moreButtonRef.current);
|
const getTriggerElement = useLastCallback(() => moreButtonRef.current);
|
||||||
@ -150,6 +156,52 @@ const Modal = (props: OwnProps) => {
|
|||||||
), [isOpen, onClose, handleEnter]);
|
), [isOpen, onClose, handleEnter]);
|
||||||
useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen, modalRef]);
|
useEffect(() => (isOpen && modalRef.current ? trapFocus(modalRef.current) : undefined), [isOpen, modalRef]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isNativeDialog || !shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = nativeDialogRef.current;
|
||||||
|
if (!dialog) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dialog.open) {
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (dialog.open) {
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isNativeDialog, nativeDialogRef, shouldRender]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isNativeDialog || !shouldRender) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = nativeDialogRef.current;
|
||||||
|
if (!dialog) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.addEventListener('cancel', handleCancel);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dialog.removeEventListener('cancel', handleCancel);
|
||||||
|
};
|
||||||
|
}, [isNativeDialog, isOpen, nativeDialogRef, onClose, shouldRender]);
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive: isOpen,
|
isActive: isOpen,
|
||||||
onBack: onClose,
|
onBack: onClose,
|
||||||
@ -218,14 +270,9 @@ const Modal = (props: OwnProps) => {
|
|||||||
dialogClassName,
|
dialogClassName,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
function renderContent() {
|
||||||
<Portal>
|
return (
|
||||||
<div
|
<>
|
||||||
ref={modalRef}
|
|
||||||
className={fullClassName}
|
|
||||||
tabIndex={-1}
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div className="modal-container">
|
<div className="modal-container">
|
||||||
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
||||||
{withBalanceBar && (
|
{withBalanceBar && (
|
||||||
@ -274,7 +321,30 @@ const Modal = (props: OwnProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
{isNativeDialog ? (
|
||||||
|
<dialog
|
||||||
|
ref={nativeDialogRef}
|
||||||
|
className={fullClassName}
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</dialog>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
ref={divModalRef}
|
||||||
|
className={fullClassName}
|
||||||
|
tabIndex={-1}
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -126,9 +126,16 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'requestPoll':
|
case 'requestPoll': {
|
||||||
actions.openPollModal({ isQuiz: button.isQuiz, tabId });
|
actions.openPollModal({
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
|
messageListType: 'thread',
|
||||||
|
isQuiz: button.isQuiz,
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'requestPhone': {
|
case 'requestPhone': {
|
||||||
const user = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
|
const user = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
|
||||||
|
|||||||
@ -1382,6 +1382,19 @@ addActionHandler('sendPollVote', (global, actions, payload): ActionReturnType =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('appendPollAnswer', async (global, actions, payload): Promise<void> => {
|
||||||
|
const {
|
||||||
|
chatId, messageId, text,
|
||||||
|
} = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
if (!chat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await callApi('appendPollAnswer', { chat, messageId, text });
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('toggleTodoCompleted', (global, actions, payload): ActionReturnType => {
|
addActionHandler('toggleTodoCompleted', (global, actions, payload): ActionReturnType => {
|
||||||
const { chatId, messageId, completedIds, incompletedIds } = payload;
|
const { chatId, messageId, completedIds, incompletedIds } = payload;
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
|
|||||||
@ -754,25 +754,24 @@ addActionHandler('exitMessageSelectMode', (global, actions, payload): ActionRetu
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('openPollModal', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openPollModal', (global, actions, payload): ActionReturnType => {
|
||||||
const { isQuiz, tabId = getCurrentTabId() } = payload || {};
|
const {
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
|
messageListType,
|
||||||
|
isQuiz,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
pollModal: {
|
pollModal: {
|
||||||
isOpen: true,
|
chatId,
|
||||||
|
threadId,
|
||||||
|
messageListType,
|
||||||
isQuiz,
|
isQuiz,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
addTabStateResetterAction('closePollModal', 'pollModal');
|
||||||
addActionHandler('closePollModal', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
|
||||||
|
|
||||||
return updateTabState(global, {
|
|
||||||
pollModal: {
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
}, tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('openTodoListModal', (global, actions, payload): ActionReturnType => {
|
addActionHandler('openTodoListModal', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -464,10 +464,6 @@ export const INITIAL_TAB_STATE: TabState = {
|
|||||||
byChatId: {},
|
byChatId: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
pollModal: {
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
requestedTranslations: {
|
requestedTranslations: {
|
||||||
byChatId: {},
|
byChatId: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1528,6 +1528,11 @@ export interface ActionPayloads {
|
|||||||
messageId: number;
|
messageId: number;
|
||||||
options: string[];
|
options: string[];
|
||||||
};
|
};
|
||||||
|
appendPollAnswer: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
toggleTodoCompleted: {
|
toggleTodoCompleted: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
@ -2421,9 +2426,12 @@ export interface ActionPayloads {
|
|||||||
refreshLangPackFromCache: {
|
refreshLangPackFromCache: {
|
||||||
langCode: string;
|
langCode: string;
|
||||||
};
|
};
|
||||||
openPollModal: ({
|
openPollModal: {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
messageListType: MessageListType;
|
||||||
isQuiz?: boolean;
|
isQuiz?: boolean;
|
||||||
} & WithTabId) | undefined;
|
} & WithTabId;
|
||||||
closePollModal: WithTabId | undefined;
|
closePollModal: WithTabId | undefined;
|
||||||
openTodoListModal: {
|
openTodoListModal: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
|||||||
@ -82,6 +82,7 @@ import type {
|
|||||||
MediaViewerMedia,
|
MediaViewerMedia,
|
||||||
MediaViewerOrigin,
|
MediaViewerOrigin,
|
||||||
MessageList,
|
MessageList,
|
||||||
|
MessageListType,
|
||||||
MiddleSearchParams,
|
MiddleSearchParams,
|
||||||
NewChatMembersProgress,
|
NewChatMembersProgress,
|
||||||
PaymentStep,
|
PaymentStep,
|
||||||
@ -567,8 +568,10 @@ export type TabState = {
|
|||||||
filter?: ApiChatType[];
|
filter?: ApiChatType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
pollModal: {
|
pollModal?: {
|
||||||
isOpen: boolean;
|
chatId: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
messageListType: MessageListType;
|
||||||
isQuiz?: boolean;
|
isQuiz?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
545
src/hooks/useReorderableList.ts
Normal file
545
src/hooks/useReorderableList.ts
Normal file
@ -0,0 +1,545 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useSignal,
|
||||||
|
useState,
|
||||||
|
} from '../lib/teact/teact';
|
||||||
|
|
||||||
|
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
|
||||||
|
import { areArraysShallowEqual } from '../util/areShallowEqual';
|
||||||
|
import buildStyle from '../util/buildStyle';
|
||||||
|
import getPointerPosition from '../util/events/getPointerPosition';
|
||||||
|
import { REM } from '../components/common/helpers/mediaDimensions';
|
||||||
|
import useLastCallback from './useLastCallback';
|
||||||
|
|
||||||
|
type ReorderableId = number | string;
|
||||||
|
|
||||||
|
type ReorderableListOptions<T extends ReorderableId> = {
|
||||||
|
itemIds: T[];
|
||||||
|
isDisabled?: boolean;
|
||||||
|
withAutoscroll?: boolean;
|
||||||
|
onReorder: (itemIds: T[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DragState<T extends ReorderableId> = {
|
||||||
|
id: T;
|
||||||
|
offsetY: number;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
centerY: number;
|
||||||
|
translateY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowProps = {
|
||||||
|
ref: (element?: HTMLElement) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DragElementProps = {
|
||||||
|
ref: (element?: HTMLElement) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HandleProps = {
|
||||||
|
ref: (element?: HTMLElement) => void;
|
||||||
|
role: 'button';
|
||||||
|
tabIndex: number;
|
||||||
|
onMouseDown: (event: React.MouseEvent) => void;
|
||||||
|
onTouchStart: (event: React.TouchEvent) => void;
|
||||||
|
onKeyDown: (event: React.KeyboardEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUTOSCROLL_EDGE_DISTANCE = 4 * REM;
|
||||||
|
const AUTOSCROLL_MAX_DELTA = 0.5 * REM;
|
||||||
|
|
||||||
|
export default function useReorderableList<T extends ReorderableId>({
|
||||||
|
itemIds,
|
||||||
|
isDisabled,
|
||||||
|
withAutoscroll,
|
||||||
|
onReorder,
|
||||||
|
}: ReorderableListOptions<T>) {
|
||||||
|
const rowElementsRef = useRef(new Map<T, HTMLElement>());
|
||||||
|
const dragElementsRef = useRef(new Map<T, HTMLElement>());
|
||||||
|
const handleElementsRef = useRef(new Map<T, HTMLElement>());
|
||||||
|
const itemIdsRef = useRef(itemIds);
|
||||||
|
const pendingFocusIdRef = useRef<T | undefined>();
|
||||||
|
const isAutoscrollScheduledRef = useRef(false);
|
||||||
|
const scrollContainerRef = useRef<HTMLElement | undefined>();
|
||||||
|
const lastPointerYRef = useRef<number | undefined>();
|
||||||
|
const [getDragState, setDragState] = useSignal<DragState<T> | undefined>();
|
||||||
|
const [draggedId, setDraggedId] = useState<T | undefined>();
|
||||||
|
const [draggedHeight, setDraggedHeight] = useState<number | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
itemIdsRef.current = itemIds;
|
||||||
|
|
||||||
|
const pendingFocusId = pendingFocusIdRef.current;
|
||||||
|
if (pendingFocusId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFocusIdRef.current = undefined;
|
||||||
|
handleElementsRef.current?.get(pendingFocusId)?.focus();
|
||||||
|
}, [itemIds]);
|
||||||
|
|
||||||
|
const reorder = useLastCallback((fromIndex: number, toIndex: number) => {
|
||||||
|
if (fromIndex === toIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextItemIds = moveItem(itemIdsRef.current, fromIndex, toIndex);
|
||||||
|
itemIdsRef.current = nextItemIds;
|
||||||
|
onReorder(nextItemIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reorderDraggedItem = useLastCallback((itemId: T, centerY: number, isMovingDown: boolean) => {
|
||||||
|
const currentItemIds = itemIdsRef.current;
|
||||||
|
const currentIndex = currentItemIds.indexOf(itemId);
|
||||||
|
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withoutDraggedItemIds = currentItemIds.filter((currentId) => currentId !== itemId);
|
||||||
|
const targetIndex = getTargetIndex(withoutDraggedItemIds, rowElementsRef.current, centerY, isMovingDown);
|
||||||
|
const nextItemIds = [...withoutDraggedItemIds];
|
||||||
|
nextItemIds.splice(targetIndex, 0, itemId);
|
||||||
|
|
||||||
|
if (areArraysShallowEqual(currentItemIds, nextItemIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemIdsRef.current = nextItemIds;
|
||||||
|
onReorder(nextItemIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDragFromPointer = useLastCallback((y: number, isMovingDown?: boolean) => {
|
||||||
|
const dragState = getDragState();
|
||||||
|
|
||||||
|
if (!dragState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = getVerticalBounds(itemIdsRef.current, rowElementsRef.current);
|
||||||
|
const nextTop = y - dragState.offsetY;
|
||||||
|
const clampedTop = bounds
|
||||||
|
? clamp(nextTop, bounds.top, Math.max(bounds.top, bounds.bottom - dragState.height))
|
||||||
|
: nextTop;
|
||||||
|
const translateY = clampedTop - dragState.top;
|
||||||
|
const centerY = clampedTop + dragState.height / 2;
|
||||||
|
const nextDragState = {
|
||||||
|
...dragState,
|
||||||
|
centerY,
|
||||||
|
translateY,
|
||||||
|
};
|
||||||
|
|
||||||
|
setDragState(nextDragState);
|
||||||
|
requestMutation(() => {
|
||||||
|
const currentDragState = getDragState();
|
||||||
|
const dragElement = dragElementsRef.current.get(dragState.id);
|
||||||
|
|
||||||
|
if (currentDragState !== nextDragState || !dragElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragElement.style.transform = `translateY(${nextDragState.translateY}px)`;
|
||||||
|
});
|
||||||
|
reorderDraggedItem(dragState.id, centerY, isMovingDown ?? centerY > dragState.centerY);
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduleAutoscroll = useLastCallback(() => {
|
||||||
|
isAutoscrollScheduledRef.current = true;
|
||||||
|
|
||||||
|
requestMeasure(() => {
|
||||||
|
const dragState = getDragState();
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
const pointerY = lastPointerYRef.current;
|
||||||
|
|
||||||
|
if (!dragState || !scrollContainer || pointerY === undefined) {
|
||||||
|
isAutoscrollScheduledRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoscrollState = getAutoscrollState(scrollContainer, pointerY);
|
||||||
|
|
||||||
|
if (!autoscrollState) {
|
||||||
|
isAutoscrollScheduledRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMutation(() => {
|
||||||
|
const currentDragState = getDragState();
|
||||||
|
|
||||||
|
if (currentDragState !== dragState || scrollContainerRef.current !== scrollContainer) {
|
||||||
|
if (!currentDragState || scrollContainerRef.current !== scrollContainer) {
|
||||||
|
isAutoscrollScheduledRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollContainer.scrollTop = autoscrollState.targetScrollTop;
|
||||||
|
|
||||||
|
if (autoscrollState.delta) {
|
||||||
|
requestMeasure(() => {
|
||||||
|
updateDragFromPointer(pointerY, autoscrollState.delta > 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleAutoscroll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const restartAutoscrollIfNeeded = useLastCallback((pointerY: number) => {
|
||||||
|
const scrollContainer = scrollContainerRef.current;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isAutoscrollScheduledRef.current
|
||||||
|
|| !scrollContainer
|
||||||
|
|| !getAutoscrollState(scrollContainer, pointerY)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleAutoscroll();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDrag = useLastCallback((event: MouseEvent | TouchEvent) => {
|
||||||
|
if (event.cancelable) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { y } = getPointerPosition(event);
|
||||||
|
lastPointerYRef.current = y;
|
||||||
|
|
||||||
|
updateDragFromPointer(y);
|
||||||
|
restartAutoscrollIfNeeded(y);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRelease = useLastCallback(() => {
|
||||||
|
const dragState = getDragState();
|
||||||
|
const dragElement = dragState ? dragElementsRef.current.get(dragState.id) : undefined;
|
||||||
|
|
||||||
|
if (dragState && dragElement) {
|
||||||
|
const releasedId = dragState.id;
|
||||||
|
|
||||||
|
requestMutation(() => {
|
||||||
|
if (
|
||||||
|
getDragState()?.id === releasedId
|
||||||
|
|| dragElementsRef.current.get(releasedId) !== dragElement
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragElement.style.transform = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoscrollScheduledRef.current = false;
|
||||||
|
scrollContainerRef.current = undefined;
|
||||||
|
lastPointerYRef.current = undefined;
|
||||||
|
setDragState(undefined);
|
||||||
|
setDraggedId(undefined);
|
||||||
|
setDraggedHeight(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (draggedId === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleDrag);
|
||||||
|
document.addEventListener('mouseup', handleRelease);
|
||||||
|
document.addEventListener('touchmove', handleDrag, { passive: true });
|
||||||
|
document.addEventListener('touchend', handleRelease);
|
||||||
|
document.addEventListener('touchcancel', handleRelease);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleDrag);
|
||||||
|
document.removeEventListener('mouseup', handleRelease);
|
||||||
|
document.removeEventListener('touchmove', handleDrag);
|
||||||
|
document.removeEventListener('touchend', handleRelease);
|
||||||
|
document.removeEventListener('touchcancel', handleRelease);
|
||||||
|
};
|
||||||
|
}, [draggedId]);
|
||||||
|
|
||||||
|
const startDrag = useLastCallback((event: React.MouseEvent | React.TouchEvent, itemId: T) => {
|
||||||
|
if (isDisabled || itemIdsRef.current.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('button' in event && event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const rowElement = rowElementsRef.current.get(itemId);
|
||||||
|
if (!rowElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { y } = getPointerPosition(event);
|
||||||
|
lastPointerYRef.current = y;
|
||||||
|
const {
|
||||||
|
left, top, width, height,
|
||||||
|
} = rowElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
const nextDragState = {
|
||||||
|
id: itemId,
|
||||||
|
offsetY: y - top,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
centerY: top + height / 2,
|
||||||
|
translateY: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
setDragState(nextDragState);
|
||||||
|
setDraggedId(itemId);
|
||||||
|
setDraggedHeight(height);
|
||||||
|
scrollContainerRef.current = withAutoscroll ? findScrollableContainer(rowElement) : undefined;
|
||||||
|
|
||||||
|
restartAutoscrollIfNeeded(y);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKeyboardReorder = useLastCallback((event: React.KeyboardEvent, itemId: T) => {
|
||||||
|
if (isDisabled || itemIdsRef.current.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = itemIdsRef.current.indexOf(itemId);
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = event.key === 'ArrowUp' ? -1 : event.key === 'ArrowDown' ? 1 : 0;
|
||||||
|
if (!delta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = currentIndex + delta;
|
||||||
|
if (nextIndex < 0 || nextIndex >= itemIdsRef.current.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
pendingFocusIdRef.current = itemId;
|
||||||
|
reorder(currentIndex, nextIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRowProps = useLastCallback((itemId: T): RowProps => {
|
||||||
|
return {
|
||||||
|
ref: (element?: HTMLElement) => {
|
||||||
|
if (element) {
|
||||||
|
rowElementsRef.current?.set(itemId, element);
|
||||||
|
} else {
|
||||||
|
rowElementsRef.current?.delete(itemId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getHandleProps = useLastCallback((itemId: T): HandleProps => {
|
||||||
|
return {
|
||||||
|
ref: (element?: HTMLElement) => {
|
||||||
|
if (element) {
|
||||||
|
handleElementsRef.current?.set(itemId, element);
|
||||||
|
} else {
|
||||||
|
handleElementsRef.current?.delete(itemId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
role: 'button',
|
||||||
|
tabIndex: isDisabled || itemIds.length < 2 ? -1 : 0,
|
||||||
|
onMouseDown: (event) => startDrag(event, itemId),
|
||||||
|
onTouchStart: (event) => startDrag(event, itemId),
|
||||||
|
onKeyDown: (event) => handleKeyboardReorder(event, itemId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDragElementProps = useLastCallback((itemId: T): DragElementProps => {
|
||||||
|
return {
|
||||||
|
ref: (element?: HTMLElement) => {
|
||||||
|
if (element) {
|
||||||
|
dragElementsRef.current?.set(itemId, element);
|
||||||
|
|
||||||
|
const dragState = getDragState();
|
||||||
|
if (dragState?.id === itemId) {
|
||||||
|
element.style.transform = `translateY(${dragState.translateY}px)`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dragElementsRef.current?.delete(itemId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlaceholderStyle = useLastCallback((itemId: T) => {
|
||||||
|
if (draggedId !== itemId || draggedHeight === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `height: ${draggedHeight}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getDragStyle = useLastCallback((itemId: T) => {
|
||||||
|
const dragState = getDragState();
|
||||||
|
if (dragState?.id !== itemId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildStyle(
|
||||||
|
'position: fixed',
|
||||||
|
`left: ${dragState.left}px`,
|
||||||
|
`top: ${dragState.top}px`,
|
||||||
|
`transform: translateY(${dragState.translateY}px)`,
|
||||||
|
`width: ${dragState.width}px`,
|
||||||
|
`height: ${dragState.height}px`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
draggedId,
|
||||||
|
getRowProps,
|
||||||
|
getDragElementProps,
|
||||||
|
getHandleProps,
|
||||||
|
getPlaceholderStyle,
|
||||||
|
getDragStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveItem<T>(items: T[], fromIndex: number, toIndex: number) {
|
||||||
|
const nextItems = [...items];
|
||||||
|
const [item] = nextItems.splice(fromIndex, 1);
|
||||||
|
nextItems.splice(toIndex, 0, item);
|
||||||
|
|
||||||
|
return nextItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetIndex<T extends ReorderableId>(
|
||||||
|
itemIds: T[],
|
||||||
|
rowElements: Map<T, HTMLElement>,
|
||||||
|
centerY: number,
|
||||||
|
isMovingDown: boolean,
|
||||||
|
) {
|
||||||
|
const measuredItems = itemIds.reduce<{ id: T; top: number; height: number }[]>((result, id) => {
|
||||||
|
const element = rowElements.get(id);
|
||||||
|
if (!element) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { top, height } = element.getBoundingClientRect();
|
||||||
|
result.push({ id, top, height });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
measuredItems.sort((a, b) => a.top - b.top);
|
||||||
|
|
||||||
|
if (isMovingDown) {
|
||||||
|
let targetIndex = 0;
|
||||||
|
|
||||||
|
for (const { id, top } of measuredItems) {
|
||||||
|
if (centerY >= top) {
|
||||||
|
targetIndex = itemIds.indexOf(id) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { id, top, height } of measuredItems) {
|
||||||
|
if (centerY <= top + height) {
|
||||||
|
return itemIds.indexOf(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVerticalBounds<T extends ReorderableId>(itemIds: T[], rowElements: Map<T, HTMLElement>) {
|
||||||
|
return itemIds.reduce<{ top: number; bottom: number } | undefined>((bounds, id) => {
|
||||||
|
const element = rowElements.get(id);
|
||||||
|
if (!element) {
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { top, bottom } = element.getBoundingClientRect();
|
||||||
|
if (!bounds) {
|
||||||
|
return { top, bottom };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: Math.min(bounds.top, top),
|
||||||
|
bottom: Math.max(bounds.bottom, bottom),
|
||||||
|
};
|
||||||
|
}, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findScrollableContainer(element: HTMLElement) {
|
||||||
|
let currentElement = element.parentElement;
|
||||||
|
|
||||||
|
while (currentElement) {
|
||||||
|
const isScrollable = currentElement.scrollHeight > currentElement.clientHeight;
|
||||||
|
|
||||||
|
if (isScrollable) {
|
||||||
|
return currentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoscrollState(scrollContainer: HTMLElement, pointerY: number) {
|
||||||
|
const { top, bottom } = scrollContainer.getBoundingClientRect();
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
||||||
|
const distanceToTop = pointerY - top;
|
||||||
|
const distanceToBottom = bottom - pointerY;
|
||||||
|
const maxScrollTop = scrollHeight - clientHeight;
|
||||||
|
|
||||||
|
if (distanceToTop < AUTOSCROLL_EDGE_DISTANCE && scrollTop > 0) {
|
||||||
|
const distanceIntoEdge = AUTOSCROLL_EDGE_DISTANCE - Math.max(distanceToTop, 0);
|
||||||
|
const delta = -Math.min(
|
||||||
|
getAutoscrollSpeed(distanceIntoEdge),
|
||||||
|
scrollTop,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
delta,
|
||||||
|
targetScrollTop: scrollTop + delta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceToBottom < AUTOSCROLL_EDGE_DISTANCE && scrollTop < maxScrollTop) {
|
||||||
|
const distanceIntoEdge = AUTOSCROLL_EDGE_DISTANCE - Math.max(distanceToBottom, 0);
|
||||||
|
const delta = Math.min(
|
||||||
|
getAutoscrollSpeed(distanceIntoEdge),
|
||||||
|
maxScrollTop - scrollTop,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
delta,
|
||||||
|
targetScrollTop: scrollTop + delta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoscrollSpeed(distanceIntoEdge: number) {
|
||||||
|
return clamp(
|
||||||
|
(distanceIntoEdge / AUTOSCROLL_EDGE_DISTANCE) * AUTOSCROLL_MAX_DELTA,
|
||||||
|
1,
|
||||||
|
AUTOSCROLL_MAX_DELTA,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
@ -1812,6 +1812,7 @@ messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer r
|
|||||||
messages.declineUrlAuth#35436bbc url:string = Bool;
|
messages.declineUrlAuth#35436bbc url:string = Bool;
|
||||||
messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;
|
messages.checkUrlAuthMatchCode#c9a47b0b url:string match_code:string = Bool;
|
||||||
messages.composeMessageWithAI#fd426afe flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string change_tone:flags.2?string = messages.ComposedMessageWithAI;
|
messages.composeMessageWithAI#fd426afe flags:# proofread:flags.0?true emojify:flags.3?true text:TextWithEntities translate_to_lang:flags.1?string change_tone:flags.2?string = messages.ComposedMessageWithAI;
|
||||||
|
messages.addPollAnswer#19bc4b6d peer:InputPeer msg_id:int answer:PollAnswer = Updates;
|
||||||
messages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
messages.getUnreadPollVotes#43286cf2 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
|
||||||
messages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;
|
messages.readPollVotes#1720b4d8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
|
|||||||
@ -250,6 +250,7 @@
|
|||||||
"messages.deleteTopicHistory",
|
"messages.deleteTopicHistory",
|
||||||
"messages.toggleTodoCompleted",
|
"messages.toggleTodoCompleted",
|
||||||
"messages.appendTodoList",
|
"messages.appendTodoList",
|
||||||
|
"messages.addPollAnswer",
|
||||||
"messages.summarizeText",
|
"messages.summarizeText",
|
||||||
"messages.editChatCreator",
|
"messages.editChatCreator",
|
||||||
"messages.getFutureChatCreatorAfterLeave",
|
"messages.getFutureChatCreatorAfterLeave",
|
||||||
|
|||||||
@ -484,7 +484,7 @@ function renderChildren(
|
|||||||
forceMoveToEnd = false,
|
forceMoveToEnd = false,
|
||||||
namespace?: string,
|
namespace?: string,
|
||||||
) {
|
) {
|
||||||
if (('props' in $new) && $new.props.teactFastList) {
|
if ($new.type === VirtualType.Tag && $new.props.teactFastList) {
|
||||||
renderFastListChildren($current, $new, currentContext, currentEl, namespace);
|
renderFastListChildren($current, $new, currentContext, currentEl, namespace);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -965,7 +965,7 @@ function getChildKeysByIndex(children: VirtualElementChildren) {
|
|||||||
if (isNullable(key)) {
|
if (isNullable(key)) {
|
||||||
if (DEBUG && isParentElement($child)) {
|
if (DEBUG && isParentElement($child)) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('Missing `key` in `teactFastList`');
|
console.warn('Missing `key` in `teactFastList`', $child);
|
||||||
}
|
}
|
||||||
|
|
||||||
key = `${INDEX_KEY_PREFIX}${index}`;
|
key = `${INDEX_KEY_PREFIX}${index}`;
|
||||||
|
|||||||
@ -130,6 +130,8 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = {
|
|||||||
starsSuggestedPostFutureMin: 300,
|
starsSuggestedPostFutureMin: 300,
|
||||||
starsSuggestedPostFutureMax: 2678400,
|
starsSuggestedPostFutureMax: 2678400,
|
||||||
starsSuggestedPostCommissionPermille: 850,
|
starsSuggestedPostCommissionPermille: 850,
|
||||||
|
pollMaxAnswers: 12,
|
||||||
|
pollClosePeriodMax: 2628000,
|
||||||
noForwardsRequestExpirePeriod: 86400,
|
noForwardsRequestExpirePeriod: 86400,
|
||||||
tonSuggestedPostCommissionPermille: 850,
|
tonSuggestedPostCommissionPermille: 850,
|
||||||
todoItemLengthMax: 64,
|
todoItemLengthMax: 64,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -74,282 +74,286 @@ $icons-map: (
|
|||||||
"chats-badge": "\f138",
|
"chats-badge": "\f138",
|
||||||
"check": "\f139",
|
"check": "\f139",
|
||||||
"check-bold": "\f13a",
|
"check-bold": "\f13a",
|
||||||
"clock": "\f13b",
|
"check-filled": "\f13b",
|
||||||
"clock-edit": "\f13c",
|
"choice-selected": "\f13c",
|
||||||
"close": "\f13d",
|
"clock": "\f13d",
|
||||||
"close-circle": "\f13e",
|
"clock-edit": "\f13e",
|
||||||
"close-topic": "\f13f",
|
"close": "\f13f",
|
||||||
"closed-gift": "\f140",
|
"close-circle": "\f140",
|
||||||
"cloud-download": "\f141",
|
"close-topic": "\f141",
|
||||||
"collapse": "\f142",
|
"closed-gift": "\f142",
|
||||||
"collapse-modal": "\f143",
|
"cloud-download": "\f143",
|
||||||
"colorize": "\f144",
|
"collapse": "\f144",
|
||||||
"combine-craft": "\f145",
|
"collapse-modal": "\f145",
|
||||||
"comments": "\f146",
|
"colorize": "\f146",
|
||||||
"comments-sticker": "\f147",
|
"combine-craft": "\f147",
|
||||||
"copy": "\f148",
|
"comments": "\f148",
|
||||||
"copy-media": "\f149",
|
"comments-sticker": "\f149",
|
||||||
"craft": "\f14a",
|
"copy": "\f14a",
|
||||||
"crop": "\f14b",
|
"copy-media": "\f14b",
|
||||||
"crown-take-off": "\f14c",
|
"craft": "\f14c",
|
||||||
"crown-take-off-outline": "\f14d",
|
"crop": "\f14d",
|
||||||
"crown-wear": "\f14e",
|
"crown-take-off": "\f14e",
|
||||||
"crown-wear-outline": "\f14f",
|
"crown-take-off-outline": "\f14f",
|
||||||
"darkmode": "\f150",
|
"crown-wear": "\f150",
|
||||||
"data": "\f151",
|
"crown-wear-outline": "\f151",
|
||||||
"delete": "\f152",
|
"darkmode": "\f152",
|
||||||
"delete-filled": "\f153",
|
"data": "\f153",
|
||||||
"delete-left": "\f154",
|
"delete": "\f154",
|
||||||
"delete-user": "\f155",
|
"delete-filled": "\f155",
|
||||||
"diamond": "\f156",
|
"delete-left": "\f156",
|
||||||
"document": "\f157",
|
"delete-user": "\f157",
|
||||||
"double-badge": "\f158",
|
"diamond": "\f158",
|
||||||
"down": "\f159",
|
"document": "\f159",
|
||||||
"download": "\f15a",
|
"double-badge": "\f15a",
|
||||||
"dropdown-arrows": "\f15b",
|
"down": "\f15b",
|
||||||
"eats": "\f15c",
|
"download": "\f15c",
|
||||||
"edit": "\f15d",
|
"dropdown-arrows": "\f15d",
|
||||||
"email": "\f15e",
|
"eats": "\f15e",
|
||||||
"enter": "\f15f",
|
"edit": "\f15f",
|
||||||
"expand": "\f160",
|
"email": "\f160",
|
||||||
"expand-modal": "\f161",
|
"enter": "\f161",
|
||||||
"eye": "\f162",
|
"expand": "\f162",
|
||||||
"eye-crossed": "\f163",
|
"expand-modal": "\f163",
|
||||||
"eye-crossed-outline": "\f164",
|
"eye": "\f164",
|
||||||
"eye-outline": "\f165",
|
"eye-crossed": "\f165",
|
||||||
"favorite": "\f166",
|
"eye-crossed-outline": "\f166",
|
||||||
"favorite-filled": "\f167",
|
"eye-outline": "\f167",
|
||||||
"file-badge": "\f168",
|
"favorite": "\f168",
|
||||||
"flag": "\f169",
|
"favorite-filled": "\f169",
|
||||||
"flip": "\f16a",
|
"file-badge": "\f16a",
|
||||||
"folder": "\f16b",
|
"flag": "\f16b",
|
||||||
"folder-badge": "\f16c",
|
"flip": "\f16c",
|
||||||
"folder-tabs-bot": "\f16d",
|
"folder": "\f16d",
|
||||||
"folder-tabs-channel": "\f16e",
|
"folder-badge": "\f16e",
|
||||||
"folder-tabs-chat": "\f16f",
|
"folder-tabs-bot": "\f16f",
|
||||||
"folder-tabs-chats": "\f170",
|
"folder-tabs-channel": "\f170",
|
||||||
"folder-tabs-folder": "\f171",
|
"folder-tabs-chat": "\f171",
|
||||||
"folder-tabs-group": "\f172",
|
"folder-tabs-chats": "\f172",
|
||||||
"folder-tabs-star": "\f173",
|
"folder-tabs-folder": "\f173",
|
||||||
"folder-tabs-user": "\f174",
|
"folder-tabs-group": "\f174",
|
||||||
"fontsize": "\f175",
|
"folder-tabs-star": "\f175",
|
||||||
"forums": "\f176",
|
"folder-tabs-user": "\f176",
|
||||||
"forward": "\f177",
|
"fontsize": "\f177",
|
||||||
"fragment": "\f178",
|
"forums": "\f178",
|
||||||
"frozen-time": "\f179",
|
"forward": "\f179",
|
||||||
"fullscreen": "\f17a",
|
"fragment": "\f17a",
|
||||||
"gifs": "\f17b",
|
"frozen-time": "\f17b",
|
||||||
"gift": "\f17c",
|
"fullscreen": "\f17c",
|
||||||
"gift-transfer-inline": "\f17d",
|
"gifs": "\f17d",
|
||||||
"group": "\f17e",
|
"gift": "\f17e",
|
||||||
"group-filled": "\f17f",
|
"gift-transfer-inline": "\f17f",
|
||||||
"grouped": "\f180",
|
"group": "\f180",
|
||||||
"grouped-disable": "\f181",
|
"group-filled": "\f181",
|
||||||
"hand-stop": "\f182",
|
"grouped": "\f182",
|
||||||
"hand-stop-filled": "\f183",
|
"grouped-disable": "\f183",
|
||||||
"hashtag": "\f184",
|
"hand-stop": "\f184",
|
||||||
"hd-photo": "\f185",
|
"hand-stop-filled": "\f185",
|
||||||
"heart": "\f186",
|
"hashtag": "\f186",
|
||||||
"heart-outline": "\f187",
|
"hd-photo": "\f187",
|
||||||
"help": "\f188",
|
"heart": "\f188",
|
||||||
"info": "\f189",
|
"heart-outline": "\f189",
|
||||||
"info-filled": "\f18a",
|
"help": "\f18a",
|
||||||
"install": "\f18b",
|
"info": "\f18b",
|
||||||
"italic": "\f18c",
|
"info-filled": "\f18c",
|
||||||
"key": "\f18d",
|
"install": "\f18d",
|
||||||
"keyboard": "\f18e",
|
"italic": "\f18e",
|
||||||
"lamp": "\f18f",
|
"key": "\f18f",
|
||||||
"language": "\f190",
|
"keyboard": "\f190",
|
||||||
"large-pause": "\f191",
|
"lamp": "\f191",
|
||||||
"large-play": "\f192",
|
"language": "\f192",
|
||||||
"link": "\f193",
|
"large-pause": "\f193",
|
||||||
"link-badge": "\f194",
|
"large-play": "\f194",
|
||||||
"link-broken": "\f195",
|
"link": "\f195",
|
||||||
"location": "\f196",
|
"link-badge": "\f196",
|
||||||
"lock": "\f197",
|
"link-broken": "\f197",
|
||||||
"lock-badge": "\f198",
|
"location": "\f198",
|
||||||
"logout": "\f199",
|
"lock": "\f199",
|
||||||
"loop": "\f19a",
|
"lock-badge": "\f19a",
|
||||||
"mention": "\f19b",
|
"logout": "\f19b",
|
||||||
"menu": "\f19c",
|
"loop": "\f19c",
|
||||||
"message": "\f19d",
|
"mention": "\f19d",
|
||||||
"message-failed": "\f19e",
|
"menu": "\f19e",
|
||||||
"message-pending": "\f19f",
|
"message": "\f19f",
|
||||||
"message-read": "\f1a0",
|
"message-failed": "\f1a0",
|
||||||
"message-succeeded": "\f1a1",
|
"message-pending": "\f1a1",
|
||||||
"microphone": "\f1a2",
|
"message-read": "\f1a2",
|
||||||
"microphone-alt": "\f1a3",
|
"message-succeeded": "\f1a3",
|
||||||
"monospace": "\f1a4",
|
"microphone": "\f1a4",
|
||||||
"more": "\f1a5",
|
"microphone-alt": "\f1a5",
|
||||||
"more-circle": "\f1a6",
|
"monospace": "\f1a6",
|
||||||
"move-caption-down": "\f1a7",
|
"more": "\f1a7",
|
||||||
"move-caption-up": "\f1a8",
|
"more-circle": "\f1a8",
|
||||||
"mute": "\f1a9",
|
"move-caption-down": "\f1a9",
|
||||||
"muted": "\f1aa",
|
"move-caption-up": "\f1aa",
|
||||||
"my-notes": "\f1ab",
|
"mute": "\f1ab",
|
||||||
"new-chat-filled": "\f1ac",
|
"muted": "\f1ac",
|
||||||
"new-send": "\f1ad",
|
"my-notes": "\f1ad",
|
||||||
"next": "\f1ae",
|
"new-chat-filled": "\f1ae",
|
||||||
"next-link": "\f1af",
|
"new-send": "\f1af",
|
||||||
"no-download": "\f1b0",
|
"next": "\f1b0",
|
||||||
"no-share": "\f1b1",
|
"next-link": "\f1b1",
|
||||||
"nochannel": "\f1b2",
|
"no-download": "\f1b2",
|
||||||
"noise-suppression": "\f1b3",
|
"no-share": "\f1b3",
|
||||||
"non-contacts": "\f1b4",
|
"nochannel": "\f1b4",
|
||||||
"note": "\f1b5",
|
"noise-suppression": "\f1b5",
|
||||||
"one-filled": "\f1b6",
|
"non-contacts": "\f1b6",
|
||||||
"open-in-new-tab": "\f1b7",
|
"note": "\f1b7",
|
||||||
"password-off": "\f1b8",
|
"one-filled": "\f1b8",
|
||||||
"pause": "\f1b9",
|
"open-in-new-tab": "\f1b9",
|
||||||
"permissions": "\f1ba",
|
"password-off": "\f1ba",
|
||||||
"phone": "\f1bb",
|
"pause": "\f1bb",
|
||||||
"phone-discard": "\f1bc",
|
"permissions": "\f1bc",
|
||||||
"phone-discard-outline": "\f1bd",
|
"phone": "\f1bd",
|
||||||
"photo": "\f1be",
|
"phone-discard": "\f1be",
|
||||||
"pin": "\f1bf",
|
"phone-discard-outline": "\f1bf",
|
||||||
"pin-badge": "\f1c0",
|
"photo": "\f1c0",
|
||||||
"pin-list": "\f1c1",
|
"pin": "\f1c1",
|
||||||
"pinned-chat": "\f1c2",
|
"pin-badge": "\f1c2",
|
||||||
"pinned-message": "\f1c3",
|
"pin-list": "\f1c3",
|
||||||
"pip": "\f1c4",
|
"pinned-chat": "\f1c4",
|
||||||
"play": "\f1c5",
|
"pinned-message": "\f1c5",
|
||||||
"play-story": "\f1c6",
|
"pip": "\f1c6",
|
||||||
"poll": "\f1c7",
|
"play": "\f1c7",
|
||||||
"poll-badge": "\f1c8",
|
"play-story": "\f1c8",
|
||||||
"previous": "\f1c9",
|
"poll": "\f1c9",
|
||||||
"previous-link": "\f1ca",
|
"poll-badge": "\f1ca",
|
||||||
"privacy-policy": "\f1cb",
|
"previous": "\f1cb",
|
||||||
"proof-of-ownership": "\f1cc",
|
"previous-link": "\f1cc",
|
||||||
"quote": "\f1cd",
|
"privacy-policy": "\f1cd",
|
||||||
"quote-text": "\f1ce",
|
"proof-of-ownership": "\f1ce",
|
||||||
"radial-badge": "\f1cf",
|
"quote": "\f1cf",
|
||||||
"rating-icons-level1": "\f1d0",
|
"quote-text": "\f1d0",
|
||||||
"rating-icons-level10": "\f1d1",
|
"radial-badge": "\f1d1",
|
||||||
"rating-icons-level2": "\f1d2",
|
"rating-icons-level1": "\f1d2",
|
||||||
"rating-icons-level20": "\f1d3",
|
"rating-icons-level10": "\f1d3",
|
||||||
"rating-icons-level3": "\f1d4",
|
"rating-icons-level2": "\f1d4",
|
||||||
"rating-icons-level30": "\f1d5",
|
"rating-icons-level20": "\f1d5",
|
||||||
"rating-icons-level4": "\f1d6",
|
"rating-icons-level3": "\f1d6",
|
||||||
"rating-icons-level40": "\f1d7",
|
"rating-icons-level30": "\f1d7",
|
||||||
"rating-icons-level5": "\f1d8",
|
"rating-icons-level4": "\f1d8",
|
||||||
"rating-icons-level50": "\f1d9",
|
"rating-icons-level40": "\f1d9",
|
||||||
"rating-icons-level6": "\f1da",
|
"rating-icons-level5": "\f1da",
|
||||||
"rating-icons-level60": "\f1db",
|
"rating-icons-level50": "\f1db",
|
||||||
"rating-icons-level7": "\f1dc",
|
"rating-icons-level6": "\f1dc",
|
||||||
"rating-icons-level70": "\f1dd",
|
"rating-icons-level60": "\f1dd",
|
||||||
"rating-icons-level8": "\f1de",
|
"rating-icons-level7": "\f1de",
|
||||||
"rating-icons-level80": "\f1df",
|
"rating-icons-level70": "\f1df",
|
||||||
"rating-icons-level9": "\f1e0",
|
"rating-icons-level8": "\f1e0",
|
||||||
"rating-icons-level90": "\f1e1",
|
"rating-icons-level80": "\f1e1",
|
||||||
"rating-icons-negative": "\f1e2",
|
"rating-icons-level9": "\f1e2",
|
||||||
"readchats": "\f1e3",
|
"rating-icons-level90": "\f1e3",
|
||||||
"recent": "\f1e4",
|
"rating-icons-negative": "\f1e4",
|
||||||
"redo": "\f1e5",
|
"readchats": "\f1e5",
|
||||||
"refund": "\f1e6",
|
"recent": "\f1e6",
|
||||||
"reload": "\f1e7",
|
"redo": "\f1e7",
|
||||||
"remove": "\f1e8",
|
"refund": "\f1e8",
|
||||||
"remove-quote": "\f1e9",
|
"reload": "\f1e9",
|
||||||
"reopen-topic": "\f1ea",
|
"remove": "\f1ea",
|
||||||
"reorder-tabs": "\f1eb",
|
"remove-quote": "\f1eb",
|
||||||
"replace": "\f1ec",
|
"reopen-topic": "\f1ec",
|
||||||
"replies": "\f1ed",
|
"reorder-tabs": "\f1ed",
|
||||||
"reply": "\f1ee",
|
"replace": "\f1ee",
|
||||||
"reply-filled": "\f1ef",
|
"replace-round": "\f1ef",
|
||||||
"revenue-split": "\f1f0",
|
"replies": "\f1f0",
|
||||||
"revote": "\f1f1",
|
"reply": "\f1f1",
|
||||||
"rotate": "\f1f2",
|
"reply-filled": "\f1f2",
|
||||||
"save-story": "\f1f3",
|
"revenue-split": "\f1f3",
|
||||||
"saved-messages": "\f1f4",
|
"revote": "\f1f4",
|
||||||
"schedule": "\f1f5",
|
"rotate": "\f1f5",
|
||||||
"scheduled": "\f1f6",
|
"save-story": "\f1f6",
|
||||||
"sd-photo": "\f1f7",
|
"saved-messages": "\f1f7",
|
||||||
"search": "\f1f8",
|
"schedule": "\f1f8",
|
||||||
"select": "\f1f9",
|
"scheduled": "\f1f9",
|
||||||
"select-filled": "\f1fa",
|
"sd-photo": "\f1fa",
|
||||||
"sell": "\f1fb",
|
"search": "\f1fb",
|
||||||
"sell-outline": "\f1fc",
|
"select": "\f1fc",
|
||||||
"send": "\f1fd",
|
"select-filled": "\f1fd",
|
||||||
"send-outline": "\f1fe",
|
"sell": "\f1fe",
|
||||||
"settings": "\f1ff",
|
"sell-outline": "\f1ff",
|
||||||
"settings-filled": "\f200",
|
"send": "\f200",
|
||||||
"share-filled": "\f201",
|
"send-outline": "\f201",
|
||||||
"share-screen": "\f202",
|
"settings": "\f202",
|
||||||
"share-screen-outlined": "\f203",
|
"settings-filled": "\f203",
|
||||||
"share-screen-stop": "\f204",
|
"share-filled": "\f204",
|
||||||
"show-message": "\f205",
|
"share-screen": "\f205",
|
||||||
"sidebar": "\f206",
|
"share-screen-outlined": "\f206",
|
||||||
"skip-next": "\f207",
|
"share-screen-stop": "\f207",
|
||||||
"skip-previous": "\f208",
|
"show-message": "\f208",
|
||||||
"smallscreen": "\f209",
|
"sidebar": "\f209",
|
||||||
"smile": "\f20a",
|
"skip-next": "\f20a",
|
||||||
"sort": "\f20b",
|
"skip-previous": "\f20b",
|
||||||
"sort-by-date": "\f20c",
|
"smallscreen": "\f20c",
|
||||||
"sort-by-number": "\f20d",
|
"smile": "\f20d",
|
||||||
"sort-by-price": "\f20e",
|
"sort": "\f20e",
|
||||||
"speaker": "\f20f",
|
"sort-by-date": "\f20f",
|
||||||
"speaker-muted-story": "\f210",
|
"sort-by-number": "\f210",
|
||||||
"speaker-outline": "\f211",
|
"sort-by-price": "\f211",
|
||||||
"speaker-story": "\f212",
|
"speaker": "\f212",
|
||||||
"spoiler": "\f213",
|
"speaker-muted-story": "\f213",
|
||||||
"spoiler-disable": "\f214",
|
"speaker-outline": "\f214",
|
||||||
"sport": "\f215",
|
"speaker-story": "\f215",
|
||||||
"star": "\f216",
|
"spoiler": "\f216",
|
||||||
"stars-lock": "\f217",
|
"spoiler-disable": "\f217",
|
||||||
"stars-refund": "\f218",
|
"sport": "\f218",
|
||||||
"stats": "\f219",
|
"star": "\f219",
|
||||||
"stealth-future": "\f21a",
|
"stars-lock": "\f21a",
|
||||||
"stealth-past": "\f21b",
|
"stars-refund": "\f21b",
|
||||||
"stickers": "\f21c",
|
"stats": "\f21c",
|
||||||
"stop": "\f21d",
|
"stealth-future": "\f21d",
|
||||||
"stop-raising-hand": "\f21e",
|
"stealth-past": "\f21e",
|
||||||
"story-caption": "\f21f",
|
"stickers": "\f21f",
|
||||||
"story-expired": "\f220",
|
"stop": "\f220",
|
||||||
"story-priority": "\f221",
|
"stop-raising-hand": "\f221",
|
||||||
"story-reply": "\f222",
|
"story-caption": "\f222",
|
||||||
"strikethrough": "\f223",
|
"story-expired": "\f223",
|
||||||
"tag": "\f224",
|
"story-priority": "\f224",
|
||||||
"tag-add": "\f225",
|
"story-reply": "\f225",
|
||||||
"tag-crossed": "\f226",
|
"strikethrough": "\f226",
|
||||||
"tag-filter": "\f227",
|
"tag": "\f227",
|
||||||
"tag-name": "\f228",
|
"tag-add": "\f228",
|
||||||
"timer": "\f229",
|
"tag-crossed": "\f229",
|
||||||
"toncoin": "\f22a",
|
"tag-filter": "\f22a",
|
||||||
"tone": "\f22b",
|
"tag-name": "\f22b",
|
||||||
"tools": "\f22c",
|
"timer": "\f22c",
|
||||||
"topic-new": "\f22d",
|
"timer-filled": "\f22d",
|
||||||
"trade": "\f22e",
|
"toncoin": "\f22e",
|
||||||
"transcribe": "\f22f",
|
"tone": "\f22f",
|
||||||
"truck": "\f230",
|
"tools": "\f230",
|
||||||
"unarchive": "\f231",
|
"topic-new": "\f231",
|
||||||
"underlined": "\f232",
|
"trade": "\f232",
|
||||||
"understood": "\f233",
|
"transcribe": "\f233",
|
||||||
"undo": "\f234",
|
"truck": "\f234",
|
||||||
"unique-profile": "\f235",
|
"unarchive": "\f235",
|
||||||
"unlist": "\f236",
|
"underlined": "\f236",
|
||||||
"unlist-outline": "\f237",
|
"understood": "\f237",
|
||||||
"unlock": "\f238",
|
"undo": "\f238",
|
||||||
"unlock-badge": "\f239",
|
"unique-profile": "\f239",
|
||||||
"unmute": "\f23a",
|
"unlist": "\f23a",
|
||||||
"unpin": "\f23b",
|
"unlist-outline": "\f23b",
|
||||||
"unread": "\f23c",
|
"unlock": "\f23c",
|
||||||
"up": "\f23d",
|
"unlock-badge": "\f23d",
|
||||||
"user": "\f23e",
|
"unmute": "\f23e",
|
||||||
"user-filled": "\f23f",
|
"unpin": "\f23f",
|
||||||
"user-online": "\f240",
|
"unread": "\f240",
|
||||||
"user-stars": "\f241",
|
"up": "\f241",
|
||||||
"user-tag": "\f242",
|
"user": "\f242",
|
||||||
"video": "\f243",
|
"user-filled": "\f243",
|
||||||
"video-outlined": "\f244",
|
"user-online": "\f244",
|
||||||
"video-stop": "\f245",
|
"user-stars": "\f245",
|
||||||
"view-once": "\f246",
|
"user-tag": "\f246",
|
||||||
"voice-chat": "\f247",
|
"video": "\f247",
|
||||||
"volume-1": "\f248",
|
"video-outlined": "\f248",
|
||||||
"volume-2": "\f249",
|
"video-stop": "\f249",
|
||||||
"volume-3": "\f24a",
|
"view-once": "\f24a",
|
||||||
"warning": "\f24b",
|
"voice-chat": "\f24b",
|
||||||
"web": "\f24c",
|
"volume-1": "\f24c",
|
||||||
"webapp": "\f24d",
|
"volume-2": "\f24d",
|
||||||
"word-wrap": "\f24e",
|
"volume-3": "\f24e",
|
||||||
"zoom-in": "\f24f",
|
"warning": "\f24f",
|
||||||
"zoom-out": "\f250",
|
"web": "\f250",
|
||||||
|
"webapp": "\f251",
|
||||||
|
"word-wrap": "\f252",
|
||||||
|
"zoom-in": "\f253",
|
||||||
|
"zoom-out": "\f254",
|
||||||
);
|
);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -57,6 +57,8 @@ export type FontIconName =
|
|||||||
| 'chats-badge'
|
| 'chats-badge'
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'check-bold'
|
| 'check-bold'
|
||||||
|
| 'check-filled'
|
||||||
|
| 'choice-selected'
|
||||||
| 'clock'
|
| 'clock'
|
||||||
| 'clock-edit'
|
| 'clock-edit'
|
||||||
| 'close'
|
| 'close'
|
||||||
@ -235,6 +237,7 @@ export type FontIconName =
|
|||||||
| 'reopen-topic'
|
| 'reopen-topic'
|
||||||
| 'reorder-tabs'
|
| 'reorder-tabs'
|
||||||
| 'replace'
|
| 'replace'
|
||||||
|
| 'replace-round'
|
||||||
| 'replies'
|
| 'replies'
|
||||||
| 'reply'
|
| 'reply'
|
||||||
| 'reply-filled'
|
| 'reply-filled'
|
||||||
@ -296,6 +299,7 @@ export type FontIconName =
|
|||||||
| 'tag-filter'
|
| 'tag-filter'
|
||||||
| 'tag-name'
|
| 'tag-name'
|
||||||
| 'timer'
|
| 'timer'
|
||||||
|
| 'timer-filled'
|
||||||
| 'toncoin'
|
| 'toncoin'
|
||||||
| 'tone'
|
| 'tone'
|
||||||
| 'tools'
|
| 'tools'
|
||||||
|
|||||||
25
src/types/language.d.ts
vendored
25
src/types/language.d.ts
vendored
@ -597,10 +597,23 @@ export interface LangPair {
|
|||||||
'CreatePollAddOption': undefined;
|
'CreatePollAddOption': undefined;
|
||||||
'PollsChooseCorrect': undefined;
|
'PollsChooseCorrect': undefined;
|
||||||
'AskAQuestion': undefined;
|
'AskAQuestion': undefined;
|
||||||
'PollOptions': undefined;
|
'PollAnswersVisible': undefined;
|
||||||
'PollAnonymous': undefined;
|
'PollAnswersVisibleDescription': undefined;
|
||||||
'PollMultiple': undefined;
|
'PollMultiple': undefined;
|
||||||
|
'PollMultipleDescription': undefined;
|
||||||
'PollQuiz': undefined;
|
'PollQuiz': undefined;
|
||||||
|
'PollQuizDescription': undefined;
|
||||||
|
'PollAllowAddingAnswers': undefined;
|
||||||
|
'PollAllowAddingAnswersDescription': undefined;
|
||||||
|
'PollAllowVoteChanges': undefined;
|
||||||
|
'PollAllowVoteChangesDescription': undefined;
|
||||||
|
'PollRandomOrder': undefined;
|
||||||
|
'PollRandomOrderDescription': undefined;
|
||||||
|
'PollLimitedDuration': undefined;
|
||||||
|
'PollLimitedDurationDescription': undefined;
|
||||||
|
'PollDuration': undefined;
|
||||||
|
'PollHideResultsUntilClose': undefined;
|
||||||
|
'PollSelectCloseDate': undefined;
|
||||||
'PollsSolutionTitle': undefined;
|
'PollsSolutionTitle': undefined;
|
||||||
'CreatePollExplanationInfo': undefined;
|
'CreatePollExplanationInfo': undefined;
|
||||||
'VoipGroupPersonalAccount': undefined;
|
'VoipGroupPersonalAccount': undefined;
|
||||||
@ -2106,6 +2119,11 @@ export interface LangPair {
|
|||||||
'AiMessageEditorFrom': undefined;
|
'AiMessageEditorFrom': undefined;
|
||||||
'AiMessageEditorTo': undefined;
|
'AiMessageEditorTo': undefined;
|
||||||
'ButtonHelp': undefined;
|
'ButtonHelp': undefined;
|
||||||
|
'PollModalQuestionTitle': undefined;
|
||||||
|
'PollModalOptionsTitle': undefined;
|
||||||
|
'PollModalSettingsTitle': undefined;
|
||||||
|
'PollModalAddNoMore': undefined;
|
||||||
|
'PollDurationOther': undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LangPairWithVariables<V = LangVariable> {
|
export interface LangPairWithVariables<V = LangVariable> {
|
||||||
@ -4224,6 +4242,9 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
|
|||||||
'FwdMessagesToChats': {
|
'FwdMessagesToChats': {
|
||||||
'count': V;
|
'count': V;
|
||||||
};
|
};
|
||||||
|
'PollModalAddMoreText': {
|
||||||
|
'count': V;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type RegularLangKey = keyof LangPair;
|
export type RegularLangKey = keyof LangPair;
|
||||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user