/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import mediaSizes from "../helpers/mediaSizes"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import appImManager from "../lib/appManagers/appImManager"; import appPollsManager from "../lib/appManagers/appPollsManager"; import serverTimeManager from "../lib/mtproto/serverTimeManager"; import { RichTextProcessor } from "../lib/richtextprocessor"; import rootScope from "../lib/rootScope"; import { ripple } from "./ripple"; import appSidebarRight from "./sidebarRight"; import AppPollResultsTab from "./sidebarRight/tabs/pollResults"; import { i18n, LangPackKey } from "../lib/langPack"; import { fastRaf } from "../helpers/schedulers"; import SetTransition from "./singleTransition"; import findUpClassName from "../helpers/dom/findUpClassName"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import { attachClickEvent, detachClickEvent } from "../helpers/dom/clickEvent"; import replaceContent from "../helpers/dom/replaceContent"; import windowSize from "../helpers/windowSize"; import { Poll, PollResults } from "../layer"; let lineTotalLength = 0; const tailLength = 9; const times = 10; const fullTime = 340; const oneTime = fullTime / times; export const roundPercents = (percents: number[]) => { //console.log('roundPercents before percents:', percents); const sum = percents.reduce((acc, p) => acc + Math.round(p), 0); if(sum > 100) { const diff = sum - 100; const length = percents.length; for(let i = 0; i < diff; ++i) { let minIndex = -1, minRemainder = 1; for(let k = 0; k < length; ++k) { let remainder = percents[k] % 1; if(remainder >= 0.5 && remainder < minRemainder) { minRemainder = remainder; minIndex = k; } } if(minIndex === -1) { //throw new Error('lol chto'); return; } percents[minIndex] -= minRemainder; } } else if(sum < 100) { const diff = 100 - sum; const length = percents.length; for(let i = 0; i < diff; ++i) { let minIndex = -1, maxRemainder = 0; for(let k = 0; k < length; ++k) { let remainder = percents[k] % 1; if(remainder < 0.5 && remainder > maxRemainder) { maxRemainder = remainder; minIndex = k; } } if(minIndex === -1) { //throw new Error('lol chto'); return; } percents[minIndex] += 1 - maxRemainder; } } //console.log('roundPercents after percents:', percents); }; /* const connectedPolls: {id: string, element: PollElement}[] = []; rootScope.on('poll_update', (e) => { const {poll, results} = e as {poll: Poll, results: PollResults}; //console.log('poll_update', poll, results); for(const connected of connectedPolls) { if(connected.id === poll.id) { const pollElement = connected.element; pollElement.isClosed = !!poll.pFlags.closed; pollElement.performResults(results, poll.chosenIndexes); } } }); */ rootScope.addEventListener('poll_update', ({poll, results}) => { const pollElements = Array.from(document.querySelectorAll(`poll-element[poll-id="${poll.id}"]`)) as PollElement[]; pollElements.forEach(pollElement => { //console.log('poll_update', poll, results); pollElement.isClosed = !!poll.pFlags.closed; pollElement.performResults(results, poll.chosenIndexes); }); }); rootScope.addEventListener('peer_changed', () => { if(prevQuizHint) { hideQuizHint(prevQuizHint, prevQuizHintOnHide, prevQuizHintTimeout); } }); mediaSizes.addEventListener('resize', () => { PollElement.setMaxLength(); PollElement.resizePolls(); }); mediaSizes.addEventListener('changeScreen', () => { PollElement.setMaxLength(); }); const hideQuizHint = (element: HTMLElement, onHide: () => void, timeout: number) => { element.classList.remove('active'); clearTimeout(timeout); setTimeout(() => { onHide(); element.remove(); if(prevQuizHint === element && prevQuizHintOnHide === onHide && prevQuizHintTimeout === timeout) { prevQuizHint = prevQuizHintOnHide = null; prevQuizHintTimeout = 0; } }, 200); }; let prevQuizHint: HTMLElement, prevQuizHintOnHide: () => void, prevQuizHintTimeout: number; const setQuizHint = (solution: string, solution_entities: any[], onHide: () => void) => { if(prevQuizHint) { hideQuizHint(prevQuizHint, prevQuizHintOnHide, prevQuizHintTimeout); } const element = document.createElement('div'); element.classList.add('quiz-hint'); const container = document.createElement('div'); container.classList.add('container', 'tgico'); const textEl = document.createElement('div'); textEl.classList.add('text'); container.append(textEl); element.append(container); textEl.innerHTML = RichTextProcessor.wrapRichText(solution, {entities: solution_entities}); appImManager.chat.bubbles.bubblesContainer.append(element); void element.offsetLeft; // reflow element.classList.add('active'); prevQuizHint = element; prevQuizHintOnHide = onHide; prevQuizHintTimeout = window.setTimeout(() => { hideQuizHint(element, onHide, prevQuizHintTimeout); }, IS_TOUCH_SUPPORTED ? 5000 : 7000); }; export default class PollElement extends HTMLElement { public static MAX_OFFSET = -46.5; public static MAX_LENGTH = 0; public svgLines: SVGSVGElement[]; private numberDivs: HTMLDivElement[]; private answerDivs: HTMLDivElement[]; private descDiv: HTMLElement; private typeDiv: HTMLElement; private avatarsDiv: HTMLElement; private viewResults: HTMLElement; private votersCountDiv: HTMLDivElement; // private maxLength: number; // private maxLengths: number[]; private maxPercents: number[]; public isClosed = false; private isQuiz = false; private isRetracted = false; private isPublic = false; private isMultiple = false; private chosenIndexes: number[] = []; private percents: number[]; public message: any; private quizInterval: number; private quizTimer: SVGSVGElement; private sendVoteBtn: HTMLElement; private chosingIndexes: number[] = []; private sendVotePromise: Promise; private sentVote = false; constructor() { super(); // элемент создан } public static setMaxLength() { const width = windowSize.windowW <= 360 ? windowSize.windowW - 120 : mediaSizes.active.poll.width; this.MAX_LENGTH = width + tailLength + this.MAX_OFFSET + -13.7; // 13 - position left } public static resizePolls() { if(!this.MAX_LENGTH) return; const pollElements = Array.from(document.querySelectorAll('poll-element.is-voted')) as PollElement[]; pollElements.forEach(pollElement => { pollElement.svgLines.forEach((svg, idx) => { //void svg.getBoundingClientRect(); // reflow pollElement.setLineProgress(idx, 1); }); }); } public render() { // браузер вызывает этот метод при добавлении элемента в документ // (может вызываться много раз, если элемент многократно добавляется/удаляется) if(!lineTotalLength) { lineTotalLength = (document.getElementById('poll-line') as any as SVGPathElement).getTotalLength(); //console.log('line total length:', lineTotalLength); PollElement.setMaxLength(); } const pollId = this.message.media.poll.id; const {poll, results} = appPollsManager.getPoll(pollId); /* const timestamp = Date.now() / 1000 | 0; if(timestamp < this.message.date) { */ if(this.message.pFlags.is_scheduled) { this.classList.add('disable-hover'); } //console.log('pollElement poll:', poll, results); let descKey: LangPackKey; if(poll.pFlags) { this.isPublic = !!poll.pFlags.public_voters; this.isQuiz = !!poll.pFlags.quiz; this.isClosed = !!poll.pFlags.closed; this.isMultiple = !!poll.pFlags.multiple_choice; if(this.isClosed) { descKey = 'Chat.Poll.Type.Closed'; this.classList.add('is-closed'); } else if(this.isQuiz) { descKey = this.isPublic ? 'Chat.Poll.Type.Quiz' : 'Chat.Poll.Type.AnonymousQuiz'; } else { descKey = this.isPublic ? 'Chat.Poll.Type.Public' : 'Chat.Poll.Type.Anonymous'; } } this.classList.toggle('is-multiple', this.isMultiple); const multipleSelect = this.isMultiple ? '' : ''; const votes = poll.answers.map((answer, idx) => { return `
${multipleSelect}
${RichTextProcessor.wrapEmojiText(answer.text)}
`; }).join(''); this.innerHTML = `
${poll.rQuestion}
${votes}`; this.descDiv = this.firstElementChild.nextElementSibling as HTMLElement; this.typeDiv = this.descDiv.firstElementChild as HTMLElement; this.avatarsDiv = this.descDiv.lastElementChild as HTMLElement; if(descKey) { this.typeDiv.append(i18n(descKey)); } if(this.isQuiz) { this.classList.add('is-quiz'); if(poll.close_period && poll.close_date) { const timeLeftDiv = document.createElement('div'); timeLeftDiv.classList.add('poll-time'); this.descDiv.append(timeLeftDiv); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); //svg.setAttributeNS(null, 'viewBox', '0 0 15 15'); svg.classList.add('poll-quiz-timer'); this.quizTimer = svg; const strokeWidth = 2; const radius = 7; const circumference = 2 * Math.PI * radius; const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.classList.add('poll-quiz-timer-circle'); circle.setAttributeNS(null, 'cx', '16'); circle.setAttributeNS(null, 'cy', '16'); circle.setAttributeNS(null, 'r', '' + radius); circle.setAttributeNS(null, 'stroke-width', '' + strokeWidth); svg.append(circle); this.descDiv.append(svg); const period = poll.close_period * 1000; const closeTime = (poll.close_date - serverTimeManager.serverTimeOffset) * 1000; //console.log('closeTime:', poll.close_date, serverTimeManager.serverTimeOffset, Date.now() / 1000 | 0); // let time = Date.now(); // let percents = (closeTime - time) / period; // timeLeftDiv.innerHTML = String((closeTime - time) / 1000 + 1 | 0).toHHMMSS(); // // @ts-ignore // circle.style.strokeDashoffset = circumference + percents * circumference; // circle.style.strokeDasharray = ${circumference} ${circumference}; this.quizInterval = window.setInterval(() => { const time = Date.now(); const percents = (closeTime - time) / period; const timeLeft = (closeTime - time) / 1000 + 1 | 0; timeLeftDiv.innerHTML = String(timeLeft).toHHMMSS(); if (timeLeft <= 5) { timeLeftDiv.style.color = '#ee545c'; circle.style.stroke = '#ee545c'; } //timeLeftDiv.style.visibility = 'visible'; // @ts-ignore circle.style.strokeDashoffset = circumference + percents * circumference; circle.style.strokeDasharray = `${circumference} ${circumference}`; if(time >= closeTime) { clearInterval(this.quizInterval); timeLeftDiv.innerHTML = ''; // @ts-ignore circle.style.strokeDashoffset = circumference; this.quizInterval = 0; setTimeout(() => { // нужно запросить апдейт чтобы опрос обновился appPollsManager.getResults(this.message); }, 3e3); } }, 1e3); } } this.answerDivs = Array.from(this.querySelectorAll('.poll-answer')) as HTMLDivElement[]; this.svgLines = Array.from(this.querySelectorAll('.poll-line')) as SVGSVGElement[]; this.numberDivs = Array.from(this.querySelectorAll('.poll-answer-percents')) as HTMLDivElement[]; const footerDiv = document.createElement('div'); footerDiv.classList.add('poll-footer'); this.viewResults = document.createElement('div'); this.viewResults.className = 'poll-footer-button poll-view-results hide'; this.viewResults.append(i18n('Chat.Poll.ViewResults')); this.votersCountDiv = document.createElement('div'); this.votersCountDiv.className = 'poll-votes-count'; footerDiv.append(this.viewResults, this.votersCountDiv); this.append(footerDiv); this.viewResults.addEventListener('click', (e) => { cancelEvent(e); if(!appSidebarRight.isTabExists(AppPollResultsTab)) { new AppPollResultsTab(appSidebarRight).open(this.message); } }); ripple(this.viewResults); if(this.isMultiple) { this.sendVoteBtn = document.createElement('div'); this.sendVoteBtn.classList.add('poll-footer-button', 'poll-send-vote'); this.sendVoteBtn.append(i18n('Chat.Poll.SubmitVote')); ripple(this.sendVoteBtn); if(!poll.chosenIndexes.length) { this.votersCountDiv.classList.add('hide'); } attachClickEvent(this.sendVoteBtn, (e) => { cancelEvent(e); /* const indexes = this.answerDivs.filter(el => el.classList.contains('is-chosing')).map(el => +el.dataset.index); if(indexes.length) { } */ if(this.chosingIndexes.length) { this.sendVotes(this.chosingIndexes).then(() => { this.chosingIndexes.length = 0; this.answerDivs.forEach(el => { el.classList.remove('is-chosing'); }); }); } }); footerDiv.append(this.sendVoteBtn); } // const width = this.getBoundingClientRect().width; // const width = mediaSizes.active.poll.width; // this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left const canVote = !(poll.chosenIndexes.length || this.isClosed); if(!canVote || this.isPublic) { this.performResults(results, poll.chosenIndexes, false); } if(canVote) { this.setVotersCount(results); attachClickEvent(this, this.clickHandler); } } initQuizHint(results: PollResults) { if(results.solution && results.solution_entities) { const toggleHint = document.createElement('div'); toggleHint.classList.add('tgico-tip', 'poll-hint'); this.descDiv.append(toggleHint); //let active = false; attachClickEvent(toggleHint, (e) => { cancelEvent(e); //active = true; toggleHint.classList.add('active'); setQuizHint(results.solution, results.solution_entities, () => { //active = false; toggleHint.classList.remove('active'); }); }); if(this.sentVote) { const correctResult = results.results.find(r => r.pFlags.correct); if(correctResult && !correctResult.pFlags.chosen) { toggleHint.click(); } } } } clickHandler(e: Event) { const target = findUpClassName(e.target, 'poll-answer') as HTMLElement; if(!target) { return; } cancelEvent(e); const answerIndex = +target.dataset.index; if(this.isMultiple) { target.classList.toggle('is-chosing'); const foundIndex = this.chosingIndexes.indexOf(answerIndex); if(foundIndex !== -1) { this.chosingIndexes.splice(foundIndex, 1); } else { this.chosingIndexes.push(answerIndex); } } else { this.sendVotes([answerIndex]); } /* target.classList.add('is-voting'); setTimeout(() => { // simulate this.setResults([100, 0], answerIndex); target.classList.remove('is-voting'); }, 1000); */ } sendVotes(indexes: number[]) { if(this.sendVotePromise) return this.sendVotePromise; const targets = this.answerDivs.filter((_, idx) => indexes.includes(idx)); targets.forEach(target => { target.classList.add('is-voting'); }); this.classList.add('disable-hover'); this.sentVote = true; return this.sendVotePromise = appPollsManager.sendVote(this.message, indexes).then(() => { targets.forEach(target => { target.classList.remove('is-voting'); }); this.classList.remove('disable-hover'); }).catch(() => { this.sentVote = false; }).finally(() => { this.sendVotePromise = null; }); } performResults(results: PollResults, chosenIndexes: number[], animate = true) { if(!rootScope.settings.animationsEnabled) { animate = false; } if(this.isQuiz && (results.results?.length || this.isClosed)) { this.answerDivs.forEach((el, idx) => { el.classList.toggle('is-correct', !!results.results[idx].pFlags.correct); }); if(this.initQuizHint) { this.initQuizHint(results); this.initQuizHint = null; } if(this.quizInterval) { clearInterval(this.quizInterval); this.quizInterval = 0; } if(this.quizTimer?.parentElement) { this.quizTimer.remove(); } const timeEl = this.descDiv.querySelector('.poll-time'); if(timeEl) { timeEl.remove(); } } if(this.isClosed) { this.classList.add('is-closed'); replaceContent(this.typeDiv, i18n('Chat.Poll.Type.Closed')); } // set chosen if(this.chosenIndexes.length !== chosenIndexes.length || this.isClosed) { // if we voted this.isRetracted = this.chosenIndexes.length && !chosenIndexes.length; this.chosenIndexes = chosenIndexes.slice(); if(this.isRetracted) { attachClickEvent(this, this.clickHandler); } else { detachClickEvent(this, this.clickHandler); } } // is need update if(this.chosenIndexes.length || this.isRetracted || this.isClosed) { const percents = results.results.map(v => results.total_voters ? v.voters / results.total_voters * 100 : 0); this.classList.toggle('no-transition', !animate); if(animate) { SetTransition(this, '', !this.isRetracted, 340); } fastRaf(() => { this.setResults(this.isRetracted ? this.percents : percents, this.chosenIndexes, animate); this.percents = percents; this.isRetracted = false; }); } this.setVotersCount(results); if(this.isPublic) { if(!this.isMultiple) { this.viewResults.classList.toggle('hide', !results.total_voters || !this.chosenIndexes.length); this.votersCountDiv.classList.toggle('hide', !!this.chosenIndexes.length); } let html = ''; /** * MACOS, ANDROID - без реверса * WINDOWS DESKTOP - реверс * все приложения накладывают аватарку первую на вторую, а в макете зато вторая на первую, ЛОЛ! */ results.recent_voters/* .slice().reverse() */.forEach((userId, idx) => { const style = idx === 0 ? '' : `style="transform: translateX(-${idx * 3}px);"`; html += ``; }); this.avatarsDiv.innerHTML = html; } if(this.isMultiple) { const isVoted = !!this.chosenIndexes.length; const hideSendVoteBtn = this.isClosed || isVoted; const hideViewResultsBtn = !this.isPublic || !results.total_voters || (!isVoted && !this.isClosed); this.sendVoteBtn.classList.toggle('hide', hideSendVoteBtn); this.viewResults.classList.toggle('hide', hideViewResultsBtn); this.votersCountDiv.classList.toggle('hide', !hideSendVoteBtn || !hideViewResultsBtn); } } setResults(percents: number[], chosenIndexes: number[], animate: boolean) { this.svgLines.forEach(svg => svg.style.display = ''); this.answerDivs.forEach((el, idx) => { el.classList.toggle('is-chosen', chosenIndexes.includes(idx)); }); const maxValue = Math.max(...percents); // this.maxLengths = percents.map(p => p / maxValue * this.maxLength); this.maxPercents = percents.map(p => p / maxValue); // line if(this.isRetracted) { this.svgLines.forEach((svg, idx) => { this.setLineProgress(idx, -1); }); } else { const cb = () => { this.svgLines.forEach((svg, idx) => { //void svg.getBoundingClientRect(); // reflow this.setLineProgress(idx, 1); }); }; animate ? fastRaf(cb) : cb(); } percents = percents.slice(); roundPercents(percents); let getPercentValue: (percents: number, index: number) => number; const iterate = (i: number) => { percents.forEach((percents, idx) => { const value = getPercentValue(percents, i); this.numberDivs[idx].innerText = value + '%'; }); }; // numbers if(this.isRetracted) { getPercentValue = (percents, index) => Math.round(percents / times * index); if(animate) { for(let i = (times - 1), k = 0; i >= 0; --i, ++k) { setTimeout(() => { iterate(i); }, oneTime * k); } } else { iterate(0); } } else { getPercentValue = (percents, index) => Math.round(percents / times * (index + 1)); if(animate) { for(let i = 0; i < times; ++i) { setTimeout(() => { iterate(i); }, oneTime * i); } } else { iterate(times - 1); } } if(this.isRetracted) { if(animate) { this.classList.add('is-retracting'); } this.classList.remove('is-voted'); const cb = () => { this.svgLines.forEach(svg => svg.style.display = 'none'); }; if(animate) { setTimeout(() => { this.classList.remove('is-retracting'); cb(); }, fullTime); } else { cb(); } } else { this.classList.add('is-voted'); } } setVotersCount(results: PollResults) { const votersCount = results.total_voters || 0; let key: LangPackKey, args = [votersCount]; if(this.isClosed) { if(this.isQuiz) key = votersCount ? 'Chat.Quiz.TotalVotes' : 'Chat.Quiz.TotalVotesResultEmpty'; else key = votersCount ? 'Chat.Poll.TotalVotes1' : 'Chat.Poll.TotalVotesResultEmpty'; } else { if(this.isQuiz) key = votersCount ? 'Chat.Quiz.TotalVotes' : 'Chat.Quiz.TotalVotesEmpty'; else key = votersCount ? 'Chat.Poll.TotalVotes1' : 'Chat.Poll.TotalVotesEmpty'; } replaceContent(this.votersCountDiv, i18n(key, args)); } setLineProgress(index: number, multiplier: number) { const svg = this.svgLines[index]; if(multiplier === -1) { svg.style.strokeDasharray = ''; svg.style.strokeDashoffset = ''; } else { // svg.style.strokeDasharray = (multiplier * this.maxLengths[index]) + ', 485.9'; svg.style.strokeDasharray = (multiplier * this.maxPercents[index] * PollElement.MAX_LENGTH) + ', 485.9'; // svg.style.strokeDasharray = (multiplier * this.maxPercents[index] * 100) + '%, 485.9'; svg.style.strokeDashoffset = '' + multiplier * PollElement.MAX_OFFSET; } } // у элемента могут быть ещё другие методы и свойства } customElements.define("poll-element", PollElement);