Poll: Add confetti, retract vote, close poll (#1906)
This commit is contained in:
parent
25e7545680
commit
918f1cdf7b
@ -742,7 +742,7 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
|
|||||||
|
|
||||||
export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] {
|
export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] {
|
||||||
const {
|
const {
|
||||||
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities,
|
results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min,
|
||||||
} = pollResults;
|
} = pollResults;
|
||||||
const results = rawResults && rawResults.map(({
|
const results = rawResults && rawResults.map(({
|
||||||
option, chosen, correct, voters,
|
option, chosen, correct, voters,
|
||||||
@ -754,6 +754,7 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isMin: min,
|
||||||
totalVoters,
|
totalVoters,
|
||||||
recentVoterIds: recentVoters?.map((id) => buildApiPeerId(id, 'user')),
|
recentVoterIds: recentVoters?.map((id) => buildApiPeerId(id, 'user')),
|
||||||
results,
|
results,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import type {
|
|||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
ApiThemeParameters,
|
ApiThemeParameters,
|
||||||
|
ApiPoll,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
ApiMessageEntityTypes,
|
ApiMessageEntityTypes,
|
||||||
@ -197,6 +198,27 @@ export function buildInputPoll(pollParams: ApiNewPoll, randomId: BigInt.BigInteg
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
|
||||||
|
return new GramJs.InputMediaPoll({
|
||||||
|
poll: new GramJs.Poll({
|
||||||
|
id: BigInt(poll.id),
|
||||||
|
publicVoters: poll.summary.isPublic,
|
||||||
|
question: poll.summary.question,
|
||||||
|
answers: poll.summary.answers.map(({ text, option }) => {
|
||||||
|
return new GramJs.PollAnswer({ text, option: deserializeBytes(option) });
|
||||||
|
}),
|
||||||
|
quiz: poll.summary.quiz,
|
||||||
|
multipleChoice: poll.summary.multipleChoice,
|
||||||
|
closeDate: poll.summary.closeDate,
|
||||||
|
closePeriod: poll.summary.closePeriod,
|
||||||
|
closed: shouldClose ? true : poll.summary.closed,
|
||||||
|
}),
|
||||||
|
correctAnswers: poll.results.results?.filter((o) => o.isCorrect).map((o) => deserializeBytes(o.option)),
|
||||||
|
solution: poll.results.solution,
|
||||||
|
solutionEntities: poll.results.solutionEntities?.map(buildMtpMessageEntity),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFilter {
|
export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFilter {
|
||||||
const {
|
const {
|
||||||
emoticon,
|
emoticon,
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export {
|
|||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
||||||
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions,
|
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, closePoll,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import type {
|
|||||||
ApiSponsoredMessage,
|
ApiSponsoredMessage,
|
||||||
ApiSendMessageAction,
|
ApiSendMessageAction,
|
||||||
ApiContact,
|
ApiContact,
|
||||||
|
ApiPoll,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
@ -51,6 +52,7 @@ import {
|
|||||||
isMessageWithMedia,
|
isMessageWithMedia,
|
||||||
isServiceMessageWithMedia,
|
isServiceMessageWithMedia,
|
||||||
buildSendMessageAction,
|
buildSendMessageAction,
|
||||||
|
buildInputPollFromExisting,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
@ -1051,6 +1053,22 @@ export async function sendPollVote({
|
|||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function closePoll({
|
||||||
|
chat, messageId, poll,
|
||||||
|
} : {
|
||||||
|
chat: ApiChat;
|
||||||
|
messageId: number;
|
||||||
|
poll: ApiPoll;
|
||||||
|
}) {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
await invokeRequest(new GramJs.messages.EditMessage({
|
||||||
|
peer: buildInputPeer(id, accessHash),
|
||||||
|
id: messageId,
|
||||||
|
media: buildInputPollFromExisting(poll, true),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadPollOptionResults({
|
export async function loadPollOptionResults({
|
||||||
chat, messageId, option, offset, limit, shouldResetVoters,
|
chat, messageId, option, offset, limit, shouldResetVoters,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@ -125,6 +125,7 @@ export interface ApiPoll {
|
|||||||
closeDate?: number;
|
closeDate?: number;
|
||||||
};
|
};
|
||||||
results: {
|
results: {
|
||||||
|
isMin?: true;
|
||||||
results?: ApiPollResult[];
|
results?: ApiPollResult[];
|
||||||
totalVoters?: number;
|
totalVoters?: number;
|
||||||
recentVoterIds?: string[];
|
recentVoterIds?: string[];
|
||||||
|
|||||||
BIN
src/assets/tgs/settings/Experimental.tgs
Normal file
BIN
src/assets/tgs/settings/Experimental.tgs
Normal file
Binary file not shown.
@ -8,6 +8,7 @@ import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
|||||||
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
||||||
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
||||||
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
||||||
|
import Experimental from '../../../assets/tgs/settings/Experimental.tgs';
|
||||||
|
|
||||||
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
|
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
|
||||||
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
|
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
|
||||||
@ -51,4 +52,5 @@ export const LOCAL_TGS_URLS = {
|
|||||||
Invite,
|
Invite,
|
||||||
QrPlane,
|
QrPlane,
|
||||||
Congratulations,
|
Congratulations,
|
||||||
|
Experimental,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-left: 1.375rem;
|
margin-left: 1.375rem;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchInput {
|
.SearchInput {
|
||||||
|
|||||||
@ -142,6 +142,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
case SettingsScreens.Privacy:
|
case SettingsScreens.Privacy:
|
||||||
case SettingsScreens.ActiveSessions:
|
case SettingsScreens.ActiveSessions:
|
||||||
case SettingsScreens.Language:
|
case SettingsScreens.Language:
|
||||||
|
case SettingsScreens.Experimental:
|
||||||
setSettingsScreen(SettingsScreens.Main);
|
setSettingsScreen(SettingsScreens.Main);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
|||||||
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
||||||
import SettingsQuickReaction from './SettingsQuickReaction';
|
import SettingsQuickReaction from './SettingsQuickReaction';
|
||||||
import SettingsPasscode from './passcode/SettingsPasscode';
|
import SettingsPasscode from './passcode/SettingsPasscode';
|
||||||
|
import SettingsExperimental from './SettingsExperimental';
|
||||||
|
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
|
|
||||||
@ -237,6 +238,10 @@ const Settings: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SettingsLanguage isActive={isScreenActive} onReset={handleReset} />
|
<SettingsLanguage isActive={isScreenActive} onReset={handleReset} />
|
||||||
);
|
);
|
||||||
|
case SettingsScreens.Experimental:
|
||||||
|
return (
|
||||||
|
<SettingsExperimental isActive={isScreenActive} onReset={handleReset} />
|
||||||
|
);
|
||||||
case SettingsScreens.GeneralChatBackground:
|
case SettingsScreens.GeneralChatBackground:
|
||||||
return (
|
return (
|
||||||
<SettingsGeneralBackground
|
<SettingsGeneralBackground
|
||||||
|
|||||||
55
src/components/left/settings/SettingsExperimental.tsx
Normal file
55
src/components/left/settings/SettingsExperimental.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsExperimental: FC<OwnProps> = ({
|
||||||
|
isActive,
|
||||||
|
onReset,
|
||||||
|
}) => {
|
||||||
|
const { requestConfetti } = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive,
|
||||||
|
onBack: onReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content custom-scroll">
|
||||||
|
<div className="settings-content-header no-border">
|
||||||
|
<AnimatedIcon
|
||||||
|
tgsUrl={LOCAL_TGS_URLS.Experimental}
|
||||||
|
size={200}
|
||||||
|
className="experimental-duck"
|
||||||
|
nonInteractive
|
||||||
|
noLoop={false}
|
||||||
|
/>
|
||||||
|
<p className="settings-item-description" dir="auto">{lang('lng_settings_experimental_about')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="settings-item">
|
||||||
|
<ListItem
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => requestConfetti()}
|
||||||
|
icon="animations"
|
||||||
|
>
|
||||||
|
<div className="title">Launch some confetti!</div>
|
||||||
|
</ListItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(SettingsExperimental);
|
||||||
@ -8,6 +8,7 @@ import { SettingsScreens } from '../../../types';
|
|||||||
|
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useMultiClick from '../../../hooks/useMultiClick';
|
||||||
|
|
||||||
import DropdownMenu from '../../ui/DropdownMenu';
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
@ -37,6 +38,10 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false);
|
const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false);
|
||||||
const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] = useState(false);
|
const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleMultiClick = useMultiClick(5, () => {
|
||||||
|
onScreenSelect(SettingsScreens.Experimental);
|
||||||
|
});
|
||||||
|
|
||||||
const openSignOutConfirmation = useCallback(() => {
|
const openSignOutConfirmation = useCallback(() => {
|
||||||
setIsSignOutDialogOpen(true);
|
setIsSignOutDialogOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
@ -98,6 +103,8 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
return <h3>{lang('PrivacySettings')}</h3>;
|
return <h3>{lang('PrivacySettings')}</h3>;
|
||||||
case SettingsScreens.Language:
|
case SettingsScreens.Language:
|
||||||
return <h3>{lang('Language')}</h3>;
|
return <h3>{lang('Language')}</h3>;
|
||||||
|
case SettingsScreens.Experimental:
|
||||||
|
return <h3>{lang('lng_settings_experimental')}</h3>;
|
||||||
|
|
||||||
case SettingsScreens.GeneralChatBackground:
|
case SettingsScreens.GeneralChatBackground:
|
||||||
return <h3>{lang('ChatBackground')}</h3>;
|
return <h3>{lang('ChatBackground')}</h3>;
|
||||||
@ -228,7 +235,10 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="settings-main-header">
|
<div className="settings-main-header">
|
||||||
<h3>{lang('SETTINGS')}</h3>
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||||
|
<h3 onClick={handleMultiClick}>
|
||||||
|
{lang('SETTINGS')}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
|
|||||||
10
src/components/main/ConfettiContainer.module.scss
Normal file
10
src/components/main/ConfettiContainer.module.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.root {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: var(--z-confetti);
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
193
src/components/main/ConfettiContainer.tsx
Normal file
193
src/components/main/ConfettiContainer.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
|
import { pick } from '../../util/iteratees';
|
||||||
|
|
||||||
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
|
import useOnChange from '../../hooks/useOnChange';
|
||||||
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
|
|
||||||
|
import styles from './ConfettiContainer.module.scss';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
lastConfettiTime?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Confetti {
|
||||||
|
pos: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
velocity: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
size: number;
|
||||||
|
color: string;
|
||||||
|
flicker: number;
|
||||||
|
flickerFrequency: number;
|
||||||
|
rotation: number;
|
||||||
|
lastDrawnAt: number;
|
||||||
|
frameCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFETTI_FADEOUT_TIMEOUT = 10000;
|
||||||
|
const DEFAULT_CONFETTI_AMOUNT = IS_SINGLE_COLUMN_LAYOUT ? 50 : 100;
|
||||||
|
const DEFAULT_CONFETTI_SIZE = 15;
|
||||||
|
const CONFETTI_COLORS = ['#E8BC2C', '#D0049E', '#02CBFE', '#5723FD', '#FE8C27', '#6CB859'];
|
||||||
|
|
||||||
|
const ConfettiContainer: FC<StateProps> = ({ lastConfettiTime }) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const confettiRef = useRef<Confetti[]>([]);
|
||||||
|
const isRafStartedRef = useRef(false);
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
function generateConfetti(width: number, height: number, amount = DEFAULT_CONFETTI_AMOUNT) {
|
||||||
|
for (let i = 0; i < amount; i++) {
|
||||||
|
const leftSide = i % 2;
|
||||||
|
const pos = {
|
||||||
|
x: width * (leftSide ? -0.1 : 1.1),
|
||||||
|
y: height * 0.75,
|
||||||
|
};
|
||||||
|
const randomX = Math.random() * width * 0.8;
|
||||||
|
const randomY = -height / 2 - Math.random() * height * 0.5;
|
||||||
|
const velocity = {
|
||||||
|
x: leftSide ? randomX : randomX * -1,
|
||||||
|
y: randomY,
|
||||||
|
};
|
||||||
|
|
||||||
|
const randomColor = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)];
|
||||||
|
const size = DEFAULT_CONFETTI_SIZE;
|
||||||
|
confettiRef.current.push({
|
||||||
|
pos,
|
||||||
|
size,
|
||||||
|
color: randomColor,
|
||||||
|
velocity,
|
||||||
|
flicker: size,
|
||||||
|
flickerFrequency: Math.random() * 0.2,
|
||||||
|
rotation: 0,
|
||||||
|
lastDrawnAt: Date.now(),
|
||||||
|
frameCount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCanvas = () => {
|
||||||
|
if (!canvasRef.current || !isRafStartedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = canvas;
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
const confettiToRemove: Confetti[] = [];
|
||||||
|
confettiRef.current.forEach((confetti, i) => {
|
||||||
|
const {
|
||||||
|
pos,
|
||||||
|
velocity,
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
flicker,
|
||||||
|
flickerFrequency,
|
||||||
|
rotation,
|
||||||
|
lastDrawnAt,
|
||||||
|
frameCount,
|
||||||
|
} = confetti;
|
||||||
|
const diff = (Date.now() - lastDrawnAt) / 1000;
|
||||||
|
|
||||||
|
const newPos = {
|
||||||
|
x: pos.x + velocity.x * diff,
|
||||||
|
y: pos.y + velocity.y * diff,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newVelocity = {
|
||||||
|
x: velocity.x * 0.99, // Air Resistance
|
||||||
|
y: velocity.y += diff * 500, // Gravity
|
||||||
|
};
|
||||||
|
|
||||||
|
const newFlicker = size * Math.abs(Math.sin(frameCount * flickerFrequency));
|
||||||
|
const newRotation = 5 * frameCount * flickerFrequency * (Math.PI / 180);
|
||||||
|
|
||||||
|
const newFrameCount = frameCount + 1;
|
||||||
|
const newLastDrawnAt = Date.now();
|
||||||
|
|
||||||
|
const shouldRemove = newPos.y > height + confetti.size;
|
||||||
|
if (shouldRemove) {
|
||||||
|
confettiToRemove.push(confetti);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfetti = {
|
||||||
|
...confetti,
|
||||||
|
pos: newPos,
|
||||||
|
velocity: newVelocity,
|
||||||
|
flicker: newFlicker,
|
||||||
|
rotation: newRotation,
|
||||||
|
lastDrawnAt: newLastDrawnAt,
|
||||||
|
frameCount: newFrameCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
confettiRef.current[i] = newConfetti;
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.ellipse(
|
||||||
|
pos.x,
|
||||||
|
pos.y,
|
||||||
|
size,
|
||||||
|
flicker,
|
||||||
|
rotation,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
confettiRef.current = confettiRef.current.filter((confetti) => !confettiToRemove.includes(confetti));
|
||||||
|
if (confettiRef.current.length) {
|
||||||
|
requestAnimationFrame(updateCanvas);
|
||||||
|
} else {
|
||||||
|
isRafStartedRef.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useOnChange(([prevConfettiTime]) => {
|
||||||
|
let hideTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
if (prevConfettiTime !== lastConfettiTime) {
|
||||||
|
generateConfetti(windowSize.width, windowSize.height);
|
||||||
|
hideTimeout = setTimeout(forceUpdate, CONFETTI_FADEOUT_TIMEOUT);
|
||||||
|
if (!isRafStartedRef.current) {
|
||||||
|
isRafStartedRef.current = true;
|
||||||
|
requestAnimationFrame(updateCanvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (hideTimeout) {
|
||||||
|
clearTimeout(hideTimeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [lastConfettiTime, updateCanvas, windowSize]);
|
||||||
|
|
||||||
|
if (!lastConfettiTime || Date.now() - lastConfettiTime > CONFETTI_FADEOUT_TIMEOUT) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="Confetti" className={styles.root}>
|
||||||
|
<canvas ref={canvasRef} className={styles.canvas} width={windowSize.width} height={windowSize.height} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal(
|
||||||
|
(global): StateProps => pick(global, ['lastConfettiTime']),
|
||||||
|
)(ConfettiContainer));
|
||||||
@ -62,6 +62,7 @@ import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async';
|
|||||||
import WebAppModal from './WebAppModal.async';
|
import WebAppModal from './WebAppModal.async';
|
||||||
import BotTrustModal from './BotTrustModal.async';
|
import BotTrustModal from './BotTrustModal.async';
|
||||||
import BotAttachModal from './BotAttachModal.async';
|
import BotAttachModal from './BotAttachModal.async';
|
||||||
|
import ConfettiContainer from './ConfettiContainer';
|
||||||
import UrlAuthModal from './UrlAuthModal.async';
|
import UrlAuthModal from './UrlAuthModal.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
@ -392,6 +393,7 @@ const Main: FC<StateProps> = ({
|
|||||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||||
<WebAppModal webApp={webApp} />
|
<WebAppModal webApp={webApp} />
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
|
<ConfettiContainer />
|
||||||
<PhoneCall isActive={isPhoneCallActive} />
|
<PhoneCall isActive={isPhoneCallActive} />
|
||||||
<UnreadCount isForAppBadge />
|
<UnreadCount isForAppBadge />
|
||||||
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
||||||
|
|||||||
@ -992,7 +992,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
<PollModal
|
<PollModal
|
||||||
isOpen={pollModal.isOpen}
|
isOpen={pollModal.isOpen}
|
||||||
isQuiz={pollModal.isQuiz}
|
isQuiz={pollModal.isQuiz}
|
||||||
shouldBeAnonimous={isChannel}
|
shouldBeAnonymous={isChannel}
|
||||||
onClear={closePollModal}
|
onClear={closePollModal}
|
||||||
onSend={handlePollSend}
|
onSend={handlePollSend}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import './PollModal.scss';
|
|||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
shouldBeAnonimous?: boolean;
|
shouldBeAnonymous?: boolean;
|
||||||
isQuiz?: boolean;
|
isQuiz?: boolean;
|
||||||
onSend: (pollSummary: ApiNewPoll) => void;
|
onSend: (pollSummary: ApiNewPoll) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
@ -33,7 +33,7 @@ const MAX_QUESTION_LENGTH = 255;
|
|||||||
const MAX_SOLUTION_LENGTH = 200;
|
const MAX_SOLUTION_LENGTH = 200;
|
||||||
|
|
||||||
const PollModal: FC<OwnProps> = ({
|
const PollModal: FC<OwnProps> = ({
|
||||||
isOpen, isQuiz, shouldBeAnonimous, onSend, onClear,
|
isOpen, isQuiz, shouldBeAnonymous, onSend, onClear,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const questionInputRef = useRef<HTMLInputElement>(null);
|
const questionInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -324,7 +324,7 @@ const PollModal: FC<OwnProps> = ({
|
|||||||
<div className="options-divider" />
|
<div className="options-divider" />
|
||||||
|
|
||||||
<div className="quiz-mode">
|
<div className="quiz-mode">
|
||||||
{!shouldBeAnonimous && (
|
{!shouldBeAnonymous && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={lang('PollAnonymous')}
|
label={lang('PollAnonymous')}
|
||||||
checked={isAnonymous}
|
checked={isAnonymous}
|
||||||
|
|||||||
@ -23,16 +23,18 @@ import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
|||||||
import { getDayStartAt } from '../../../util/dateFormat';
|
import { getDayStartAt } from '../../../util/dateFormat';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { REM } from '../../common/helpers/mediaDimensions';
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
|
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
import DeleteMessageModal from '../../common/DeleteMessageModal';
|
||||||
import ReportModal from '../../common/ReportModal';
|
import ReportModal from '../../common/ReportModal';
|
||||||
import PinMessageModal from '../../common/PinMessageModal';
|
import PinMessageModal from '../../common/PinMessageModal';
|
||||||
import MessageContextMenu from './MessageContextMenu';
|
import MessageContextMenu from './MessageContextMenu';
|
||||||
import CalendarModal from '../../common/CalendarModal';
|
import CalendarModal from '../../common/CalendarModal';
|
||||||
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
|
||||||
const START_SIZE = 2 * REM;
|
const START_SIZE = 2 * REM;
|
||||||
|
|
||||||
@ -71,6 +73,8 @@ type StateProps = {
|
|||||||
canSelect?: boolean;
|
canSelect?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
canSaveGif?: boolean;
|
canSaveGif?: boolean;
|
||||||
|
canRevote?: boolean;
|
||||||
|
canClosePoll?: boolean;
|
||||||
activeDownloads: number[];
|
activeDownloads: number[];
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
enabledReactions?: string[];
|
enabledReactions?: string[];
|
||||||
@ -109,6 +113,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
canSaveGif,
|
canSaveGif,
|
||||||
|
canRevote,
|
||||||
|
canClosePoll,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
}) => {
|
}) => {
|
||||||
@ -132,14 +138,18 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
loadReactors,
|
loadReactors,
|
||||||
copyMessagesByIds,
|
copyMessagesByIds,
|
||||||
saveGif,
|
saveGif,
|
||||||
|
cancelPollVote,
|
||||||
|
closePoll,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
||||||
|
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canShowSeenBy && isOpen) {
|
if (canShowSeenBy && isOpen) {
|
||||||
@ -254,6 +264,16 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
unfaveSticker({ sticker: message.content.sticker });
|
unfaveSticker({ sticker: message.content.sticker });
|
||||||
}, [closeMenu, message.content.sticker, unfaveSticker]);
|
}, [closeMenu, message.content.sticker, unfaveSticker]);
|
||||||
|
|
||||||
|
const handleCancelVote = useCallback(() => {
|
||||||
|
closeMenu();
|
||||||
|
cancelPollVote({ chatId: message.chatId, messageId: message.id });
|
||||||
|
}, [closeMenu, message, cancelPollVote]);
|
||||||
|
|
||||||
|
const handlePollClose = useCallback(() => {
|
||||||
|
closeMenu();
|
||||||
|
closePoll({ chatId: message.chatId, messageId: message.id });
|
||||||
|
}, [closeMenu, message, closePoll]);
|
||||||
|
|
||||||
const handleSelectMessage = useCallback(() => {
|
const handleSelectMessage = useCallback(() => {
|
||||||
const params = album?.messages
|
const params = album?.messages
|
||||||
? {
|
? {
|
||||||
@ -373,6 +393,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canSelect={canSelect}
|
canSelect={canSelect}
|
||||||
canDownload={canDownload}
|
canDownload={canDownload}
|
||||||
canSaveGif={canSaveGif}
|
canSaveGif={canSaveGif}
|
||||||
|
canRevote={canRevote}
|
||||||
|
canClosePoll={canClosePoll}
|
||||||
canShowSeenBy={canShowSeenBy}
|
canShowSeenBy={canShowSeenBy}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
seenByRecentUsers={seenByRecentUsers}
|
seenByRecentUsers={seenByRecentUsers}
|
||||||
@ -394,6 +416,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onCopyNumber={handleCopyNumber}
|
onCopyNumber={handleCopyNumber}
|
||||||
onDownload={handleDownloadClick}
|
onDownload={handleDownloadClick}
|
||||||
onSaveGif={handleSaveGif}
|
onSaveGif={handleSaveGif}
|
||||||
|
onCancelVote={handleCancelVote}
|
||||||
|
onClosePoll={openClosePollDialog}
|
||||||
onShowSeenBy={handleOpenSeenByModal}
|
onShowSeenBy={handleOpenSeenByModal}
|
||||||
onSendReaction={handleSendReaction}
|
onSendReaction={handleSendReaction}
|
||||||
onShowReactors={handleOpenReactorListModal}
|
onShowReactors={handleOpenReactorListModal}
|
||||||
@ -416,6 +440,13 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
chatId={message.chatId}
|
chatId={message.chatId}
|
||||||
onClose={closePinModal}
|
onClose={closePinModal}
|
||||||
/>
|
/>
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isClosePollDialogOpen}
|
||||||
|
onClose={closeClosePollDialog}
|
||||||
|
text={lang('lng_polls_stop_warning')}
|
||||||
|
confirmLabel={lang('lng_polls_stop_sure')}
|
||||||
|
confirmHandler={handlePollClose}
|
||||||
|
/>
|
||||||
{canReschedule && (
|
{canReschedule && (
|
||||||
<CalendarModal
|
<CalendarModal
|
||||||
isOpen={isCalendarOpen}
|
isOpen={isCalendarOpen}
|
||||||
@ -453,6 +484,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
canSaveGif,
|
canSaveGif,
|
||||||
|
canRevote,
|
||||||
|
canClosePoll,
|
||||||
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
} = (threadId && selectAllowedMessageActions(global, message, threadId)) || {};
|
||||||
const isPinned = messageListType === 'pinned';
|
const isPinned = messageListType === 'pinned';
|
||||||
const isScheduled = messageListType === 'scheduled';
|
const isScheduled = messageListType === 'scheduled';
|
||||||
@ -494,6 +527,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload: !isProtected && canDownload,
|
canDownload: !isProtected && canDownload,
|
||||||
canSaveGif: !isProtected && canSaveGif,
|
canSaveGif: !isProtected && canSaveGif,
|
||||||
|
canRevote,
|
||||||
|
canClosePoll: !isScheduled && canClosePoll,
|
||||||
activeDownloads,
|
activeDownloads,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
enabledReactions: chat?.isForbidden ? undefined : chat?.fullInfo?.enabledReactions,
|
enabledReactions: chat?.isForbidden ? undefined : chat?.fullInfo?.enabledReactions,
|
||||||
|
|||||||
@ -50,6 +50,8 @@ type OwnProps = {
|
|||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
canDownload?: boolean;
|
canDownload?: boolean;
|
||||||
canSaveGif?: boolean;
|
canSaveGif?: boolean;
|
||||||
|
canRevote?: boolean;
|
||||||
|
canClosePoll?: boolean;
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
seenByRecentUsers?: ApiUser[];
|
seenByRecentUsers?: ApiUser[];
|
||||||
@ -72,6 +74,8 @@ type OwnProps = {
|
|||||||
onCopyNumber?: () => void;
|
onCopyNumber?: () => void;
|
||||||
onDownload?: () => void;
|
onDownload?: () => void;
|
||||||
onSaveGif?: () => void;
|
onSaveGif?: () => void;
|
||||||
|
onCancelVote?: () => void;
|
||||||
|
onClosePoll?: () => void;
|
||||||
onShowSeenBy?: () => void;
|
onShowSeenBy?: () => void;
|
||||||
onShowReactors?: () => void;
|
onShowReactors?: () => void;
|
||||||
onSendReaction: (reaction: string | undefined, x: number, y: number) => void;
|
onSendReaction: (reaction: string | undefined, x: number, y: number) => void;
|
||||||
@ -104,6 +108,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
canSaveGif,
|
canSaveGif,
|
||||||
|
canRevote,
|
||||||
|
canClosePoll,
|
||||||
isDownloading,
|
isDownloading,
|
||||||
canShowSeenBy,
|
canShowSeenBy,
|
||||||
canShowReactionsCount,
|
canShowReactionsCount,
|
||||||
@ -128,6 +134,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onCopyNumber,
|
onCopyNumber,
|
||||||
onDownload,
|
onDownload,
|
||||||
onSaveGif,
|
onSaveGif,
|
||||||
|
onCancelVote,
|
||||||
|
onClosePoll,
|
||||||
onShowSeenBy,
|
onShowSeenBy,
|
||||||
onShowReactors,
|
onShowReactors,
|
||||||
onSendReaction,
|
onSendReaction,
|
||||||
@ -260,6 +268,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
{canPin && <MenuItem icon="pin" onClick={onPin}>{lang('DialogPin')}</MenuItem>}
|
||||||
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
{canUnpin && <MenuItem icon="unpin" onClick={onUnpin}>{lang('DialogUnpin')}</MenuItem>}
|
||||||
{canSaveGif && <MenuItem icon="gifs" onClick={onSaveGif}>{lang('lng_context_save_gif')}</MenuItem>}
|
{canSaveGif && <MenuItem icon="gifs" onClick={onSaveGif}>{lang('lng_context_save_gif')}</MenuItem>}
|
||||||
|
{canRevote && <MenuItem icon="revote" onClick={onCancelVote}>{lang('lng_polls_retract')}</MenuItem>}
|
||||||
|
{canClosePoll && <MenuItem icon="stop" onClick={onClosePoll}>{lang('lng_polls_stop')}</MenuItem>}
|
||||||
{canDownload && (
|
{canDownload && (
|
||||||
<MenuItem icon="download" onClick={onDownload}>
|
<MenuItem icon="download" onClick={onDownload}>
|
||||||
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
{isDownloading ? lang('lng_context_cancel_download') : lang('lng_media_download')}
|
||||||
|
|||||||
@ -54,14 +54,7 @@
|
|||||||
&::before {
|
&::before {
|
||||||
left: 0.125rem;
|
left: 0.125rem;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
|
--color-borders-input: var(--secondary-color);
|
||||||
.theme-dark & {
|
|
||||||
--color-borders: var(--color-borders-input);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Message.own & {
|
|
||||||
--color-borders: var(--accent-color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const Poll: FC<OwnProps & StateProps> = ({
|
|||||||
onSendVote,
|
onSendVote,
|
||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadMessage, openPollResults } = getActions();
|
const { loadMessage, openPollResults, requestConfetti } = getActions();
|
||||||
|
|
||||||
const { id: messageId, chatId } = message;
|
const { id: messageId, chatId } = message;
|
||||||
const { summary, results } = poll;
|
const { summary, results } = poll;
|
||||||
@ -87,14 +87,14 @@ const Poll: FC<OwnProps & StateProps> = ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
const chosen = poll.results.results?.find((result) => result.isChosen);
|
||||||
isSubmitting
|
if (isSubmitting && chosen) {
|
||||||
&& poll.results.results
|
if (chosen.isCorrect) {
|
||||||
&& poll.results.results.some((result) => result.isChosen)
|
requestConfetti();
|
||||||
) {
|
}
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
}, [isSubmitting, poll.results.results]);
|
}, [isSubmitting, poll.results.results, requestConfetti]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (closePeriod > 0) {
|
if (closePeriod > 0) {
|
||||||
@ -216,7 +216,7 @@ const Poll: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<PollOption
|
<PollOption
|
||||||
key={answer.option}
|
key={answer.option}
|
||||||
shouldAnimate={wasSubmitted}
|
shouldAnimate={wasSubmitted || !canVote}
|
||||||
answer={answer}
|
answer={answer}
|
||||||
voteResults={voteResults}
|
voteResults={voteResults}
|
||||||
totalVoters={totalVoters}
|
totalVoters={totalVoters}
|
||||||
@ -336,14 +336,14 @@ function getPollTypeString(summary: ApiPoll['summary']) {
|
|||||||
return NBSP;
|
return NBSP;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summary.quiz) {
|
|
||||||
return summary.isPublic ? 'QuizPoll' : 'AnonymousQuizPoll';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (summary.closed) {
|
if (summary.closed) {
|
||||||
return 'FinalResults';
|
return 'FinalResults';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (summary.quiz) {
|
||||||
|
return summary.isPublic ? 'QuizPoll' : 'AnonymousQuizPoll';
|
||||||
|
}
|
||||||
|
|
||||||
return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll';
|
return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const PollOption: FC<OwnProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const lineRef = useRef<HTMLDivElement>(null);
|
const lineRef = useRef<HTMLDivElement>(null);
|
||||||
const lineWidth = result ? getPercentage(result.votersCount, maxVotersCount || 0) : 0;
|
const lineWidth = result ? getPercentage(result.votersCount, maxVotersCount || 0) : 0;
|
||||||
const isAnimationDoesNotStart = finalPercent < answerPercent;
|
const isAnimationDoesNotStart = finalPercent !== answerPercent;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldAnimate) {
|
if (shouldAnimate) {
|
||||||
|
|||||||
@ -573,6 +573,24 @@ addActionHandler('sendPollVote', (global, actions, payload) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('cancelPollVote', (global, actions, payload) => {
|
||||||
|
const { chatId, messageId } = payload!;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
if (chat) {
|
||||||
|
void callApi('sendPollVote', { chat, messageId, options: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closePoll', (global, actions, payload) => {
|
||||||
|
const { chatId, messageId } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const poll = selectChatMessage(global, chatId, messageId)?.content.poll;
|
||||||
|
if (chat && poll) {
|
||||||
|
void callApi('closePoll', { chat, messageId, poll });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('loadPollOptionResults', (global, actions, payload) => {
|
addActionHandler('loadPollOptionResults', (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
chat, messageId, option, offset, limit, shouldResetVoters,
|
chat, messageId, option, offset, limit, shouldResetVoters,
|
||||||
|
|||||||
@ -389,22 +389,23 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
const message = selectChatMessageByPollId(global, pollId);
|
const message = selectChatMessageByPollId(global, pollId);
|
||||||
|
|
||||||
if (message?.content.poll) {
|
if (message?.content.poll) {
|
||||||
const updatedPoll = { ...message.content.poll, ...pollUpdate };
|
const oldResults = message.content.poll.results;
|
||||||
|
let newResults = oldResults;
|
||||||
// Workaround for poll update bug: `chosen` option gets reset when someone votes after current user
|
if (pollUpdate.results?.results) {
|
||||||
const { results: updatedResults } = updatedPoll.results || {};
|
if (!oldResults.results || !pollUpdate.results.isMin) {
|
||||||
if (updatedResults && !updatedResults.some(((result) => result.isChosen))) {
|
newResults = pollUpdate.results;
|
||||||
const { results } = message.content.poll.results;
|
} else if (oldResults.results) {
|
||||||
const chosenAnswers = results && results.filter((result) => result.isChosen);
|
newResults = {
|
||||||
if (chosenAnswers) {
|
...pollUpdate.results,
|
||||||
chosenAnswers.forEach((chosenAnswer) => {
|
results: pollUpdate.results.results.map((result) => ({
|
||||||
const chosenAnswerIndex = updatedResults.findIndex((result) => result.option === chosenAnswer.option);
|
...result,
|
||||||
if (chosenAnswerIndex >= 0) {
|
isChosen: oldResults.results!.find((r) => r.option === result.option)?.isChosen,
|
||||||
updatedPoll.results.results![chosenAnswerIndex].isChosen = true;
|
})),
|
||||||
}
|
isMin: undefined,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const updatedPoll = { ...message.content.poll, ...pollUpdate, results: newResults };
|
||||||
|
|
||||||
setGlobal(updateChatMessage(
|
setGlobal(updateChatMessage(
|
||||||
global,
|
global,
|
||||||
@ -438,8 +439,8 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
newRecentVoterIds.push(userId);
|
newRecentVoterIds.push(userId);
|
||||||
|
|
||||||
options.forEach((option) => {
|
options.forEach((option) => {
|
||||||
const targetOption = newResults.find((result) => result.option === option);
|
|
||||||
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
|
const targetOptionIndex = newResults.findIndex((result) => result.option === option);
|
||||||
|
const targetOption = newResults[targetOptionIndex];
|
||||||
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
|
const updatedOption: ApiPollResult = targetOption ? { ...targetOption } : { option, votersCount: 0 };
|
||||||
|
|
||||||
updatedOption.votersCount += 1;
|
updatedOption.votersCount += 1;
|
||||||
|
|||||||
@ -332,3 +332,13 @@ addActionHandler('closeGame', (global) => {
|
|||||||
openedGame: undefined,
|
openedGame: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('requestConfetti', (global) => {
|
||||||
|
const { animationLevel } = global.settings.byKey;
|
||||||
|
if (animationLevel === 0) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
lastConfettiTime: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -458,6 +458,10 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
|
|
||||||
const canSaveGif = message.content.video?.isGif;
|
const canSaveGif = message.content.video?.isGif;
|
||||||
|
|
||||||
|
const poll = content.poll;
|
||||||
|
const canRevote = !poll?.summary.closed && !poll?.summary.quiz && poll?.results.results?.some((r) => r.isChosen);
|
||||||
|
const canClosePoll = isOwn && poll && !poll.summary.closed;
|
||||||
|
|
||||||
const noOptions = [
|
const noOptions = [
|
||||||
canReply,
|
canReply,
|
||||||
canEdit,
|
canEdit,
|
||||||
@ -474,6 +478,8 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
canSaveGif,
|
canSaveGif,
|
||||||
|
canRevote,
|
||||||
|
canClosePoll,
|
||||||
].every((ability) => !ability);
|
].every((ability) => !ability);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -493,6 +499,8 @@ export function selectAllowedMessageActions(global: GlobalState, message: ApiMes
|
|||||||
canSelect,
|
canSelect,
|
||||||
canDownload,
|
canDownload,
|
||||||
canSaveGif,
|
canSaveGif,
|
||||||
|
canRevote,
|
||||||
|
canClosePoll,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -596,6 +596,7 @@ export type GlobalState = {
|
|||||||
bots: Record<string, ApiAttachMenuBot>;
|
bots: Record<string, ApiAttachMenuBot>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
lastConfettiTime?: number;
|
||||||
urlAuth?: {
|
urlAuth?: {
|
||||||
button?: {
|
button?: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -685,6 +686,20 @@ export interface ActionPayloads {
|
|||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sendPollVote: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
|
cancelPollVote: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
};
|
||||||
|
closePoll: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
};
|
||||||
|
|
||||||
// Media Viewer & Audio Player
|
// Media Viewer & Audio Player
|
||||||
openMediaViewer: {
|
openMediaViewer: {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
@ -909,6 +924,7 @@ export interface ActionPayloads {
|
|||||||
isQuiz?: boolean;
|
isQuiz?: boolean;
|
||||||
};
|
};
|
||||||
closePollModal: never;
|
closePollModal: never;
|
||||||
|
requestConfetti: never;
|
||||||
|
|
||||||
openUrl: {
|
openUrl: {
|
||||||
url: string;
|
url: string;
|
||||||
@ -977,7 +993,7 @@ export type NonTypedActionNames = (
|
|||||||
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
|
'addChatMembers' | 'deleteChatMember' | 'openPreviousChat' | 'editChatFolders' | 'toggleIsProtected' |
|
||||||
// messages
|
// messages
|
||||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||||
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' |
|
||||||
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
'editMessage' | 'deleteHistory' | 'enterMessageSelectMode' | 'toggleMessageSelection' | 'exitMessageSelectMode' |
|
||||||
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
'openTelegramLink' | 'openChatByUsername' | 'requestThreadInfoUpdate' | 'setScrollOffset' | 'unpinAllMessages' |
|
||||||
'setReplyingToId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
|
|||||||
26
src/hooks/useMultiClick.ts
Normal file
26
src/hooks/useMultiClick.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { useCallback, useRef } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
const CLICK_TIMEOUT = 300;
|
||||||
|
|
||||||
|
export default function useMultiClick(amount: number, callback: NoneToVoidFunction) {
|
||||||
|
const currentAmountRef = useRef(0);
|
||||||
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
currentAmountRef.current++;
|
||||||
|
if (currentAmountRef.current === amount) {
|
||||||
|
currentAmountRef.current = 0;
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
currentAmountRef.current = 0;
|
||||||
|
}, CLICK_TIMEOUT);
|
||||||
|
}, [amount, callback]);
|
||||||
|
|
||||||
|
return handleClick;
|
||||||
|
}
|
||||||
@ -201,7 +201,8 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
|
|
||||||
--z-lock-screen: 3000;
|
--z-lock-screen: 3000;
|
||||||
--z-ui-loader-mask: 2000;
|
--z-ui-loader-mask: 2000;
|
||||||
--z-notification: 1520;
|
--z-notification: 1700;
|
||||||
|
--z-confetti: 1600;
|
||||||
--z-right-column: 900;
|
--z-right-column: 900;
|
||||||
--z-header-menu: 990;
|
--z-header-menu: 990;
|
||||||
--z-header-menu-backdrop: 980;
|
--z-header-menu-backdrop: 980;
|
||||||
|
|||||||
@ -224,6 +224,7 @@ export enum SettingsScreens {
|
|||||||
PasscodeChangePasscodeConfirm,
|
PasscodeChangePasscodeConfirm,
|
||||||
PasscodeTurnOff,
|
PasscodeTurnOff,
|
||||||
PasscodeCongratulations,
|
PasscodeCongratulations,
|
||||||
|
Experimental,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user