diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 0037f710e..0a114fb25 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -742,7 +742,7 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice { export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['results'] { const { - results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, + results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min, } = pollResults; const results = rawResults && rawResults.map(({ option, chosen, correct, voters, @@ -754,6 +754,7 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu })); return { + isMin: min, totalVoters, recentVoterIds: recentVoters?.map((id) => buildApiPeerId(id, 'user')), results, diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 7900a4e69..a43510120 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -18,6 +18,7 @@ import type { ApiSticker, ApiVideo, ApiThemeParameters, + ApiPoll, } from '../../types'; import { 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 { const { emoticon, diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 115371ca6..a2802b919 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -28,7 +28,7 @@ export { fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate, fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, - saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, + saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, closePoll, } from './messages'; export { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 17d49ae9d..4e22d3f4a 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -17,6 +17,7 @@ import type { ApiSponsoredMessage, ApiSendMessageAction, ApiContact, + ApiPoll, } from '../../types'; import { MAIN_THREAD_ID, @@ -51,6 +52,7 @@ import { isMessageWithMedia, isServiceMessageWithMedia, buildSendMessageAction, + buildInputPollFromExisting, } from '../gramjsBuilders'; import localDb from '../localDb'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; @@ -1051,6 +1053,22 @@ export async function sendPollVote({ }), 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({ chat, messageId, option, offset, limit, shouldResetVoters, }: { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 05d759c4b..fd8daac6a 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -125,6 +125,7 @@ export interface ApiPoll { closeDate?: number; }; results: { + isMin?: true; results?: ApiPollResult[]; totalVoters?: number; recentVoterIds?: string[]; diff --git a/src/assets/tgs/settings/Experimental.tgs b/src/assets/tgs/settings/Experimental.tgs new file mode 100644 index 000000000..b75e15b68 Binary files /dev/null and b/src/assets/tgs/settings/Experimental.tgs differ diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 58a4d977f..390eb9df4 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -8,6 +8,7 @@ import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs'; import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; import Lock from '../../../assets/tgs/settings/Lock.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 HandFilled from '../../../assets/tgs/calls/HandFilled.tgs'; @@ -51,4 +52,5 @@ export const LOCAL_TGS_URLS = { Invite, QrPlane, Congratulations, + Experimental, }; diff --git a/src/components/left/LeftColumn.scss b/src/components/left/LeftColumn.scss index d8edb72af..0e266f6cb 100644 --- a/src/components/left/LeftColumn.scss +++ b/src/components/left/LeftColumn.scss @@ -16,6 +16,7 @@ font-weight: 500; margin-left: 1.375rem; margin-right: auto; + user-select: none; } .SearchInput { diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index 7bb24ec1d..e834e200d 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -142,6 +142,7 @@ const LeftColumn: FC = ({ case SettingsScreens.Privacy: case SettingsScreens.ActiveSessions: case SettingsScreens.Language: + case SettingsScreens.Experimental: setSettingsScreen(SettingsScreens.Main); return; diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index 6be8f2bfa..0484f9de1 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -27,6 +27,7 @@ import SettingsTwoFa from './twoFa/SettingsTwoFa'; import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList'; import SettingsQuickReaction from './SettingsQuickReaction'; import SettingsPasscode from './passcode/SettingsPasscode'; +import SettingsExperimental from './SettingsExperimental'; import './Settings.scss'; @@ -237,6 +238,10 @@ const Settings: FC = ({ return ( ); + case SettingsScreens.Experimental: + return ( + + ); case SettingsScreens.GeneralChatBackground: return ( void; +}; + +const SettingsExperimental: FC = ({ + isActive, + onReset, +}) => { + const { requestConfetti } = getActions(); + const lang = useLang(); + + useHistoryBack({ + isActive, + onBack: onReset, + }); + + return ( +
+
+ +

{lang('lng_settings_experimental_about')}

+
+
+ requestConfetti()} + icon="animations" + > +
Launch some confetti!
+
+
+
+ ); +}; + +export default memo(SettingsExperimental); diff --git a/src/components/left/settings/SettingsHeader.tsx b/src/components/left/settings/SettingsHeader.tsx index 8871464e1..0e4fc3637 100644 --- a/src/components/left/settings/SettingsHeader.tsx +++ b/src/components/left/settings/SettingsHeader.tsx @@ -8,6 +8,7 @@ import { SettingsScreens } from '../../../types'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import useLang from '../../../hooks/useLang'; +import useMultiClick from '../../../hooks/useMultiClick'; import DropdownMenu from '../../ui/DropdownMenu'; import MenuItem from '../../ui/MenuItem'; @@ -37,6 +38,10 @@ const SettingsHeader: FC = ({ const [isSignOutDialogOpen, setIsSignOutDialogOpen] = useState(false); const [isDeleteFolderDialogOpen, setIsDeleteFolderDialogOpen] = useState(false); + const handleMultiClick = useMultiClick(5, () => { + onScreenSelect(SettingsScreens.Experimental); + }); + const openSignOutConfirmation = useCallback(() => { setIsSignOutDialogOpen(true); }, []); @@ -98,6 +103,8 @@ const SettingsHeader: FC = ({ return

{lang('PrivacySettings')}

; case SettingsScreens.Language: return

{lang('Language')}

; + case SettingsScreens.Experimental: + return

{lang('lng_settings_experimental')}

; case SettingsScreens.GeneralChatBackground: return

{lang('ChatBackground')}

; @@ -228,7 +235,10 @@ const SettingsHeader: FC = ({ default: return (
-

{lang('SETTINGS')}

+ {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +

+ {lang('SETTINGS')} +