From 1b6fc2622e5910758b08c55dbf9e65e4d4b09789 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Tue, 13 Oct 2020 00:06:02 +0300 Subject: [PATCH] Popup changes: Closing by escape Avatar & new media are now popups Fix deleting message Edit single item from album Fix message text in album Fix overflow in pinned subtitle --- src/components/chat/contextMenu.ts | 37 +-- src/components/chat/input.ts | 272 +---------------- src/components/chat/messageRender.ts | 50 +++ src/components/inputField.ts | 13 + src/components/popup.ts | 28 +- src/components/popupAvatar.ts | 64 ++-- src/components/popupCreatePoll.ts | 31 +- src/components/popupNewMedia.ts | 285 ++++++++++++++++++ .../sidebarLeft/tabs/editProfile.ts | 14 +- src/components/sidebarLeft/tabs/newChannel.ts | 8 +- src/components/sidebarLeft/tabs/newGroup.ts | 12 +- src/index.hbs | 27 +- src/lib/appManagers/appImManager.ts | 79 +---- src/lib/rootScope.ts | 1 + src/pages/pageSignUp.ts | 8 +- src/scss/partials/_chat.scss | 22 +- 16 files changed, 502 insertions(+), 449 deletions(-) create mode 100644 src/components/chat/messageRender.ts create mode 100644 src/components/inputField.ts create mode 100644 src/components/popupNewMedia.ts diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 58a07c9f..f06eeb76 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -45,9 +45,16 @@ export class ChatContextMenu { if(!msgID) return; this.peerID = $rootScope.selectedPeerID; - this.msgID = msgID; + //this.msgID = msgID; this.target = e.target as HTMLElement; + const albumItem = findUpClassName(this.target, 'album-item'); + if(albumItem) { + this.msgID = +albumItem.dataset.mid; + } else { + this.msgID = msgID; + } + this.buttons.forEach(button => { const good = button.verify(this.peerID, this.msgID, this.target); button.element.classList.toggle('hide', !good); @@ -145,13 +152,13 @@ export class ChatContextMenu { }; private onCopyClick = () => { - let message = appMessagesManager.getMessage(this.msgID); + const message = appMessagesManager.getMessage(this.msgID); - let str = message ? message.message : ''; + const str = message ? message.message : ''; - var textArea = document.createElement("textarea"); + const textArea = document.createElement('textarea'); textArea.value = str; - textArea.style.position = "fixed"; //avoid scrolling to bottom + textArea.style.position = 'fixed'; //avoid scrolling to bottom document.body.appendChild(textArea); textArea.focus(); textArea.select(); @@ -182,22 +189,16 @@ export class ChatContextMenu { }; private onForwardClick = () => { - let msgID: number; - - const albumItem = findUpClassName(this.target, 'album-item'); - if(albumItem) { - msgID = +albumItem.dataset.mid; - } - - appSidebarRight.forwardTab.open([msgID]); + appSidebarRight.forwardTab.open([this.msgID]); }; private onDeleteClick = () => { - let peerID = $rootScope.selectedPeerID; - let firstName = appPeersManager.getPeerTitle(peerID, false, true); + const peerID = $rootScope.selectedPeerID; + const firstName = appPeersManager.getPeerTitle(peerID, false, true); - let callback = (revoke: boolean) => { - appMessagesManager.deleteMessages([this.msgID], revoke); + const msgID = this.msgID; + const callback = (revoke: boolean) => { + appMessagesManager.deleteMessages([msgID], revoke); }; let title: string, description: string, buttons: PopupButton[]; @@ -237,7 +238,7 @@ export class ChatContextMenu { isCancel: true }); - let popup = new PopupPeer('popup-delete-chat', { + const popup = new PopupPeer('popup-delete-chat', { peerID: peerID, title: title, description: description, diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index 951ce387..16c1b127 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -10,15 +10,15 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; import opusDecodeController from "../../lib/opusDecodeController"; import { RichTextProcessor } from "../../lib/richtextprocessor"; import $rootScope from '../../lib/rootScope'; -import { calcImageInBox, cancelEvent, getRichValue } from "../../lib/utils"; +import { cancelEvent, getRichValue } from "../../lib/utils"; import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; -import { Layouter, RectPart } from "../groupedLayout"; import PopupCreatePoll from "../popupCreatePoll"; +import PopupNewMedia from '../popupNewMedia'; import { ripple } from '../ripple'; import Scrollable from "../scrollable"; import { toast } from "../toast"; -import { wrapDocument, wrapReply } from "../wrappers"; +import { wrapReply } from "../wrappers"; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -40,14 +40,6 @@ export class ChatInput { public attachMenu: HTMLButtonElement; private attachMenuButtons: (ButtonMenuItemOptions & {verify: (peerID: number) => boolean})[]; - public attachMediaPopUp: { - container?: HTMLDivElement, - titleEl?: HTMLDivElement, - sendBtn?: HTMLButtonElement, - mediaContainer?: HTMLDivElement, - captionInput?: HTMLInputElement - } = {}; - public replyElements: { container?: HTMLDivElement, cancelBtn?: HTMLButtonElement, @@ -74,12 +66,14 @@ export class ChatInput { constructor() { this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement; + let willAttachType: 'document' | 'media'; this.attachMenuButtons = [{ icon: 'photo', text: 'Photo or Video', onClick: () => { + this.fileInput.value = ''; this.fileInput.setAttribute('accept', 'image/*, video/*'); - willAttach.type = 'media'; + willAttachType = 'media'; this.fileInput.click(); }, verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') @@ -87,8 +81,9 @@ export class ChatInput { icon: 'document', text: 'Document', onClick: () => { + this.fileInput.value = ''; this.fileInput.removeAttribute('accept'); - willAttach.type = 'document'; + willAttachType = 'document'; this.fileInput.click(); }, verify: (peerID: number) => peerID > 0 || appChatsManager.hasRights(peerID, 'send', 'send_media') @@ -116,12 +111,6 @@ export class ChatInput { ripple(this.attachMenu); - this.attachMediaPopUp.container = this.pageEl.querySelector('.popup-send-photo') as HTMLDivElement; - this.attachMediaPopUp.titleEl = this.attachMediaPopUp.container.querySelector('.popup-title') as HTMLDivElement; - this.attachMediaPopUp.sendBtn = this.attachMediaPopUp.container.querySelector('.btn-primary') as HTMLButtonElement; - this.attachMediaPopUp.mediaContainer = this.attachMediaPopUp.container.querySelector('.popup-photo') as HTMLDivElement; - this.attachMediaPopUp.captionInput = this.attachMediaPopUp.container.querySelector('input') as HTMLInputElement; - this.replyElements.container = this.pageEl.querySelector('.reply-wrapper') as HTMLDivElement; this.replyElements.cancelBtn = this.replyElements.container.querySelector('.reply-cancel') as HTMLButtonElement; this.replyElements.titleEl = this.replyElements.container.querySelector('.reply-title') as HTMLDivElement; @@ -281,210 +270,19 @@ export class ChatInput { window.document.execCommand('insertHTML', false, text); }); - let attachFile = (file: File) => { - return new Promise((resolve, reject) => { - let params: SendFileParams = {}; - params.file = file; - //console.log('selected file:', file, typeof(file), willAttach); - let itemDiv = document.createElement('div'); - switch(willAttach.type) { - case 'media': { - let isVideo = file.type.indexOf('video/') === 0; - - itemDiv.classList.add('popup-item-media'); - - if(isVideo) { - let video = document.createElement('video'); - let source = document.createElement('source'); - source.src = params.objectURL = URL.createObjectURL(file); - video.autoplay = false; - video.controls = false; - video.muted = true; - video.setAttribute('playsinline', ''); - - video.onloadeddata = () => { - params.width = video.videoWidth; - params.height = video.videoHeight; - params.duration = Math.floor(video.duration); - - itemDiv.append(video); - resolve(itemDiv); - }; - - video.append(source); - } else { - let img = new Image(); - img.src = params.objectURL = URL.createObjectURL(file); - img.onload = () => { - params.width = img.naturalWidth; - params.height = img.naturalHeight; - - itemDiv.append(img); - resolve(itemDiv); - }; - } - - break; - } - - case 'document': { - const isPhoto = file.type.indexOf('image/') !== -1; - if(isPhoto) { - params.objectURL = URL.createObjectURL(file); - } - - let docDiv = wrapDocument({ - file: file, - file_name: file.name || '', - size: file.size, - type: isPhoto ? 'photo' : 'doc', - url: params.objectURL - } as any, false, true); - - const finish = () => { - itemDiv.append(docDiv); - resolve(itemDiv); - }; - - if(isPhoto) { - let img = new Image(); - img.src = params.objectURL; - img.onload = () => { - params.width = img.naturalWidth; - params.height = img.naturalHeight; - - finish(); - }; - - img.onerror = finish; - } else { - finish(); - } - - break; - } - } - - willAttach.sendFileDetails.push(params); - }); - }; - - let attachFiles = (files: File[]) => { - this.fileInput.value = ''; - - let container = this.attachMediaPopUp.container.firstElementChild as HTMLElement; - container.classList.remove('is-media', 'is-document', 'is-album'); - - this.attachMediaPopUp.captionInput.value = ''; - this.attachMediaPopUp.mediaContainer.innerHTML = ''; - this.attachMediaPopUp.mediaContainer.style.width = this.attachMediaPopUp.mediaContainer.style.height = ''; - //willAttach.sendFileDetails.length = 0; - willAttach.sendFileDetails = []; // need new array - - files = files.filter(file => { - if(willAttach.type == 'media') { - return ['image/', 'video/'].find(s => file.type.indexOf(s) === 0); - } else { - return true; - } - }); - - if(files.length) { - if(willAttach.type == 'document') { - this.attachMediaPopUp.titleEl.innerText = 'Send ' + (files.length > 1 ? files.length + ' Files' : 'File'); - container.classList.add('is-document'); - } else { - container.classList.add('is-media'); - - let foundPhotos = 0; - let foundVideos = 0; - files.forEach(file => { - if(file.type.indexOf('image/') === 0) ++foundPhotos; - else if(file.type.indexOf('video/') === 0) ++foundVideos; - }); - - if(foundPhotos && foundVideos) { - this.attachMediaPopUp.titleEl.innerText = 'Send Album'; - } else if(foundPhotos) { - this.attachMediaPopUp.titleEl.innerText = 'Send ' + (foundPhotos > 1 ? foundPhotos + ' Photos' : 'Photo'); - } else if(foundVideos) { - this.attachMediaPopUp.titleEl.innerText = 'Send ' + (foundVideos > 1 ? foundVideos + ' Videos' : 'Video'); - } - } - } - - Promise.all(files.map(attachFile)).then(results => { - if(willAttach.type == 'media') { - if(willAttach.sendFileDetails.length > 1) { - container.classList.add('is-album'); - - let layouter = new Layouter(willAttach.sendFileDetails.map(o => ({w: o.width, h: o.height})), 380, 100, 4); - let layout = layouter.layout(); - - for(let {geometry, sides} of layout) { - let div = results.shift(); - - div.style.width = geometry.width + 'px'; - div.style.height = geometry.height + 'px'; - div.style.top = geometry.y + 'px'; - div.style.left = geometry.x + 'px'; - - if(sides & RectPart.Right) { - this.attachMediaPopUp.mediaContainer.style.width = geometry.width + geometry.x + 'px'; - } - - if(sides & RectPart.Bottom) { - this.attachMediaPopUp.mediaContainer.style.height = geometry.height + geometry.y + 'px'; - } - - this.attachMediaPopUp.mediaContainer.append(div); - } - - //console.log('chatInput album layout:', layout); - } else { - let params = willAttach.sendFileDetails[0]; - let div = results[0]; - let {w, h} = calcImageInBox(params.width, params.height, 380, 320); - div.style.width = w + 'px'; - div.style.height = h + 'px'; - this.attachMediaPopUp.mediaContainer.append(div); - } - } else { - this.attachMediaPopUp.mediaContainer.append(...results); - } - - this.attachMediaPopUp.container.classList.add('active'); - }); - }; - - type SendFileParams = Partial<{ - file: File, - objectURL: string, - width: number, - height: number, - duration: number - }>; - - let willAttach: Partial<{ - type: 'media' | 'document', - isMedia: boolean, - sendFileDetails: SendFileParams[] - }> = { - sendFileDetails: [] - }; - this.fileInput.addEventListener('change', (e) => { let files = (e.target as HTMLInputElement & EventTarget).files; if(!files.length) { return; } - attachFiles(Array.from(files)); + new PopupNewMedia(Array.from(files).slice(), willAttachType); + this.fileInput.value = ''; }, false); document.addEventListener('paste', (event) => { const peerID = $rootScope.selectedPeerID; - if(!peerID || this.attachMediaPopUp.container.classList.contains('active') || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) { + if(!peerID || $rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) { return; } @@ -503,56 +301,12 @@ export class ChatInput { //console.log(items[i], file); if(!file) continue; - willAttach.type = file.type.indexOf('image/') === 0 ? 'media' : "document"; - attachFiles([file]); + willAttachType = file.type.indexOf('image/') === 0 ? 'media' : "document"; + new PopupNewMedia([file], willAttachType); } } }, true); - this.attachMediaPopUp.sendBtn.addEventListener('click', () => { - this.attachMediaPopUp.container.classList.remove('active'); - let caption = this.attachMediaPopUp.captionInput.value; - willAttach.isMedia = willAttach.type == 'media'; - - //console.log('will send files with options:', willAttach); - - let peerID = appImManager.peerID; - - if(willAttach.sendFileDetails.length > 1 && willAttach.isMedia) { - appMessagesManager.sendAlbum(peerID, willAttach.sendFileDetails.map(d => d.file), Object.assign({ - caption, - replyToMsgID: this.replyToMsgID - }, willAttach)); - } else { - if(caption) { - if(willAttach.sendFileDetails.length > 1) { - appMessagesManager.sendText(peerID, caption, {replyToMsgID: this.replyToMsgID}); - caption = ''; - this.replyToMsgID = 0; - } - } - - let promises = willAttach.sendFileDetails.map(params => { - let promise = appMessagesManager.sendFile(peerID, params.file, Object.assign({ - //isMedia: willAttach.isMedia, - isMedia: params.file.type.includes('audio/') || willAttach.isMedia, - caption, - replyToMsgID: this.replyToMsgID - }, params)); - - caption = ''; - this.replyToMsgID = 0; - return promise; - }); - } - - //Promise.all(promises); - - //appMessagesManager.sendFile(appImManager.peerID, willAttach.file, willAttach); - - this.onMessageSent(); - }); - const onBtnSendClick = (e: Event) => { cancelEvent(e); diff --git a/src/components/chat/messageRender.ts b/src/components/chat/messageRender.ts new file mode 100644 index 00000000..8f8961fe --- /dev/null +++ b/src/components/chat/messageRender.ts @@ -0,0 +1,50 @@ +import { formatNumber } from "../../lib/utils"; + +type Message = any; + +export namespace MessageRender { + /* export const setText = () => { + + }; */ + + export const setTime = (message: Message, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => { + let date = new Date(message.date * 1000); + let time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2); + + if(message.views) { + bubble.classList.add('channel-post'); + time = formatNumber(message.views, 1) + ' ' + time; + + if(!message.savedFrom) { + let forward = document.createElement('div'); + forward.classList.add('bubble-beside-button', 'forward'); + forward.innerHTML = ` + + + + + + `; + bubbleContainer.append(forward); + bubble.classList.add('with-beside-button'); + } + } + + if(message.edit_date) { + bubble.classList.add('is-edited'); + time = 'edited ' + time; + } + + let timeSpan = document.createElement('span'); + timeSpan.classList.add('time'); + + let timeInner = document.createElement('div'); + timeInner.classList.add('inner', 'tgico'); + timeInner.innerHTML = time; + + timeSpan.appendChild(timeInner); + messageDiv.append(timeSpan); + + return timeSpan; + }; +} \ No newline at end of file diff --git a/src/components/inputField.ts b/src/components/inputField.ts new file mode 100644 index 00000000..b92d15c1 --- /dev/null +++ b/src/components/inputField.ts @@ -0,0 +1,13 @@ +const InputField = (placeholder: string, label: string, name: string) => { + const div = document.createElement('div'); + div.classList.add('input-field'); + + div.innerHTML = ` + + + `; + + return div; +}; + +export default InputField; \ No newline at end of file diff --git a/src/components/popup.ts b/src/components/popup.ts index aca2bde8..4065dfab 100644 --- a/src/components/popup.ts +++ b/src/components/popup.ts @@ -1,3 +1,5 @@ +import $rootScope from "../lib/rootScope"; +import { cancelEvent } from "../lib/utils"; import AvatarElement from "./avatar"; import { ripple } from "./ripple"; @@ -12,6 +14,7 @@ export class PopupElement { protected onClose: () => void; protected onCloseAfterTimeout: () => void; + protected onEscape: () => boolean = () => true; constructor(className: string, buttons?: Array, options: Partial<{closable: boolean, withConfirm: string, body: boolean}> = {}) { this.element.classList.add('popup'); @@ -26,14 +29,14 @@ export class PopupElement { if(options.closable) { this.closeBtn = document.createElement('span'); this.closeBtn.classList.add('btn-icon', 'popup-close', 'tgico-close'); - ripple(this.closeBtn); + //ripple(this.closeBtn); this.header.prepend(this.closeBtn); - this.closeBtn.addEventListener('click', () => { - this.destroy(); - }, {once: true}); + this.closeBtn.addEventListener('click', this.destroy, {once: true}); } + window.addEventListener('keydown', this._onKeyDown, {capture: true}); + if(options.withConfirm) { this.confirmBtn = document.createElement('button'); this.confirmBtn.classList.add('btn-primary'); @@ -80,20 +83,33 @@ export class PopupElement { this.element.append(this.container); } + private _onKeyDown = (e: KeyboardEvent) => { + if(e.key == 'Escape' && this.onEscape()) { + cancelEvent(e); + this.destroy(); + } + }; + public show() { document.body.append(this.element); void this.element.offsetWidth; // reflow this.element.classList.add('active'); + $rootScope.overlayIsActive = true; } - public destroy() { + public destroy = () => { this.onClose && this.onClose(); this.element.classList.remove('active'); + + window.removeEventListener('keydown', this._onKeyDown, {capture: true}); + if(this.closeBtn) this.closeBtn.removeEventListener('click', this.destroy); + $rootScope.overlayIsActive = false; + setTimeout(() => { this.element.remove(); this.onCloseAfterTimeout && this.onCloseAfterTimeout(); }, 1000); - } + }; } export type PopupButton = { diff --git a/src/components/popupAvatar.ts b/src/components/popupAvatar.ts index 5c594551..77c00093 100644 --- a/src/components/popupAvatar.ts +++ b/src/components/popupAvatar.ts @@ -1,11 +1,14 @@ -import resizeableImage from "../lib/cropper"; import appDownloadManager from "../lib/appManagers/appDownloadManager"; +import resizeableImage from "../lib/cropper"; +import { PopupElement } from "./popup"; +import { ripple } from "./ripple"; + +export default class PopupAvatar extends PopupElement { + private cropContainer: HTMLElement; + private input: HTMLInputElement; + private btnSubmit: HTMLElement; + private h6: HTMLElement; -export class PopupAvatar { - private container = document.getElementById('popup-avatar'); - private input = this.container.querySelector('input') as HTMLInputElement; - private cropContainer = this.container.querySelector('.crop') as HTMLDivElement; - private closeBtn = this.container.querySelector('.popup-close') as HTMLButtonElement; private image = new Image(); private canvas: HTMLCanvasElement; @@ -18,18 +21,31 @@ export class PopupAvatar { private onCrop: (upload: () => ReturnType) => void; constructor() { - this.container.style.display = ''; // need for no blink + super('popup-avatar', null, {closable: true}); + + this.h6 = document.createElement('h6'); + this.h6.innerText = 'Drag to Reposition'; + + this.closeBtn.classList.remove('btn-icon'); + + this.header.append(this.h6); + + this.cropContainer = document.createElement('div'); + this.cropContainer.classList.add('crop'); this.cropContainer.append(this.image); + this.input = document.createElement('input'); + this.input.type = 'file'; + this.input.style.display = 'none'; this.input.addEventListener('change', (e: any) => { - var file = e.target.files[0]; + const file = e.target.files[0]; if(!file) { return; } - var reader = new FileReader(); + const reader = new FileReader(); reader.onload = (e) => { - var contents = e.target.result as string; + const contents = e.target.result as string; this.image = new Image(); this.cropContainer.append(this.image); @@ -39,9 +55,7 @@ export class PopupAvatar { /* let {w, h} = calcImageInBox(this.image.naturalWidth, this.image.naturalHeight, 460, 554); cropContainer.style.width = w + 'px'; cropContainer.style.height = h + 'px'; */ - this.container.classList.remove('hide'); - void this.container.offsetWidth; // reflow - this.container.classList.add('active'); + this.show(); this.cropper = resizeableImage(this.image, this.canvas); this.input.value = ''; @@ -51,8 +65,10 @@ export class PopupAvatar { reader.readAsDataURL(file); }, false); - // apply - this.container.querySelector('.btn-crop').addEventListener('click', () => { + this.btnSubmit = document.createElement('button'); + this.btnSubmit.className = 'btn-primary btn-circle btn-crop btn-icon tgico-check z-depth-1'; + ripple(this.btnSubmit); + this.btnSubmit.addEventListener('click', () => { this.cropper.crop(); this.closeBtn.click(); @@ -63,16 +79,14 @@ export class PopupAvatar { }, 'image/jpeg', 1); }); - this.closeBtn.addEventListener('click', () => { - setTimeout(() => { - this.cropper.removeHandlers(); - if(this.image) { - this.image.remove(); - } + this.container.append(this.cropContainer, this.btnSubmit, this.input); - this.container.classList.add('hide'); - }, 200); - }); + this.onCloseAfterTimeout = () => { + this.cropper.removeHandlers(); + if(this.image) { + this.image.remove(); + } + }; } private resolve() { @@ -94,5 +108,3 @@ export class PopupAvatar { ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } } - -export default new PopupAvatar(); diff --git a/src/components/popupCreatePoll.ts b/src/components/popupCreatePoll.ts index 5c5ba53a..98dda5b2 100644 --- a/src/components/popupCreatePoll.ts +++ b/src/components/popupCreatePoll.ts @@ -4,23 +4,12 @@ import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager"; import $rootScope from "../lib/rootScope"; import { findUpTag, whichChild } from "../lib/utils"; import CheckboxField from "./checkbox"; +import InputField from "./inputField"; import { PopupElement } from "./popup"; import RadioField from "./radioField"; import Scrollable from "./scrollable"; import { toast } from "./toast"; -const InputField = (placeholder: string, label: string, name: string) => { - const div = document.createElement('div'); - div.classList.add('input-field'); - - div.innerHTML = ` - - - `; - - return div; -}; - export default class PopupCreatePoll extends PopupElement { private questionInput: HTMLInputElement; private questions: HTMLElement; @@ -119,6 +108,19 @@ export default class PopupCreatePoll extends PopupElement { this.scrollable = new Scrollable(this.body); this.appendMoreField(); + + this.onEscape = () => { + return !this.getFilledAnswers().length; + }; + } + + private getFilledAnswers() { + const answers = Array.from(this.questions.children).map((el, idx) => { + const input = el.querySelector('input[type="text"]') as HTMLInputElement; + return input.value; + }).filter(v => !!v.trim()); + + return answers; } onSubmitClick = (e: MouseEvent) => { @@ -134,10 +136,7 @@ export default class PopupCreatePoll extends PopupElement { return; } - const answers = Array.from(this.questions.children).map((el, idx) => { - const input = el.querySelector('input[type="text"]') as HTMLInputElement; - return input.value; - }).filter(v => !!v.trim()); + const answers = this.getFilledAnswers(); if(answers.length < 2) { toast('Please enter at least two options.'); diff --git a/src/components/popupNewMedia.ts b/src/components/popupNewMedia.ts new file mode 100644 index 00000000..e1004f1e --- /dev/null +++ b/src/components/popupNewMedia.ts @@ -0,0 +1,285 @@ +import { isTouchSupported } from "../helpers/touchSupport"; +import appImManager from "../lib/appManagers/appImManager"; +import appMessagesManager from "../lib/appManagers/appMessagesManager"; +import { calcImageInBox } from "../lib/utils"; +import { Layouter, RectPart } from "./groupedLayout"; +import InputField from "./inputField"; +import { PopupElement } from "./popup"; +import { ripple } from "./ripple"; +import { wrapDocument } from "./wrappers"; + +type SendFileParams = Partial<{ + file: File, + objectURL: string, + width: number, + height: number, + duration: number +}>; + +export default class PopupNewMedia extends PopupElement { + private btnSend: HTMLElement; + private input: HTMLInputElement; + private mediaContainer: HTMLElement; + + private willAttach: Partial<{ + type: 'media' | 'document', + isMedia: boolean, + sendFileDetails: SendFileParams[] + }> = { + sendFileDetails: [] + }; + + constructor(files: File[], willAttachType: PopupNewMedia['willAttach']['type']) { + super('popup-send-photo popup-new-media', null, {closable: true}); + + this.willAttach.type = willAttachType; + + this.btnSend = document.createElement('button'); + this.btnSend.className = 'btn-primary'; + this.btnSend.innerText = 'SEND'; + ripple(this.btnSend); + this.btnSend.addEventListener('click', this.send, {once: true}); + + this.header.append(this.btnSend); + + this.mediaContainer = document.createElement('div'); + this.mediaContainer.classList.add('popup-photo'); + + const inputField = InputField('Add a caption...', 'Caption', 'photo-caption'); + this.input = inputField.firstElementChild as HTMLInputElement; + this.container.append(this.mediaContainer, inputField); + + this.attachFiles(files); + } + + private onKeyDown = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + if(target.tagName != 'INPUT') { + this.input.focus(); + } + + if(e.key == 'Enter' && !isTouchSupported) { + this.btnSend.click(); + } + }; + + public send = () => { + this.destroy(); + let caption = this.input.value; + const willAttach = this.willAttach; + willAttach.isMedia = willAttach.type == 'media'; + + //console.log('will send files with options:', willAttach); + + const peerID = appImManager.peerID; + const chatInputC = appImManager.chatInputC; + + if(willAttach.sendFileDetails.length > 1 && willAttach.isMedia) { + appMessagesManager.sendAlbum(peerID, willAttach.sendFileDetails.map(d => d.file), Object.assign({ + caption, + replyToMsgID: chatInputC.replyToMsgID + }, willAttach)); + } else { + if(caption) { + if(willAttach.sendFileDetails.length > 1) { + appMessagesManager.sendText(peerID, caption, {replyToMsgID: chatInputC.replyToMsgID}); + caption = ''; + chatInputC.replyToMsgID = 0; + } + } + + const promises = willAttach.sendFileDetails.map(params => { + const promise = appMessagesManager.sendFile(peerID, params.file, Object.assign({ + //isMedia: willAttach.isMedia, + isMedia: params.file.type.includes('audio/') || willAttach.isMedia, + caption, + replyToMsgID: chatInputC.replyToMsgID + }, params)); + + caption = ''; + chatInputC.replyToMsgID = 0; + return promise; + }); + } + + //Promise.all(promises); + + //appMessagesManager.sendFile(appImManager.peerID, willAttach.file, willAttach); + + chatInputC.onMessageSent(); + }; + + public attachFile = (file: File) => { + const willAttach = this.willAttach; + return new Promise((resolve) => { + const params: SendFileParams = {}; + params.file = file; + //console.log('selected file:', file, typeof(file), willAttach); + const itemDiv = document.createElement('div'); + switch(willAttach.type) { + case 'media': { + const isVideo = file.type.indexOf('video/') === 0; + + itemDiv.classList.add('popup-item-media'); + + if(isVideo) { + const video = document.createElement('video'); + const source = document.createElement('source'); + source.src = params.objectURL = URL.createObjectURL(file); + video.autoplay = false; + video.controls = false; + video.muted = true; + video.setAttribute('playsinline', ''); + + video.onloadeddata = () => { + params.width = video.videoWidth; + params.height = video.videoHeight; + params.duration = Math.floor(video.duration); + + itemDiv.append(video); + resolve(itemDiv); + }; + + video.append(source); + } else { + const img = new Image(); + img.src = params.objectURL = URL.createObjectURL(file); + img.onload = () => { + params.width = img.naturalWidth; + params.height = img.naturalHeight; + + itemDiv.append(img); + resolve(itemDiv); + }; + } + + break; + } + + case 'document': { + const isPhoto = file.type.indexOf('image/') !== -1; + if(isPhoto) { + params.objectURL = URL.createObjectURL(file); + } + + const docDiv = wrapDocument({ + file: file, + file_name: file.name || '', + size: file.size, + type: isPhoto ? 'photo' : 'doc', + url: params.objectURL + } as any, false, true); + + const finish = () => { + itemDiv.append(docDiv); + resolve(itemDiv); + }; + + if(isPhoto) { + const img = new Image(); + img.src = params.objectURL; + img.onload = () => { + params.width = img.naturalWidth; + params.height = img.naturalHeight; + + finish(); + }; + + img.onerror = finish; + } else { + finish(); + } + + break; + } + } + + willAttach.sendFileDetails.push(params); + }); + }; + + public attachFiles(files: File[]) { + const container = this.container; + const willAttach = this.willAttach; + + files = files.filter(file => { + if(willAttach.type == 'media') { + return ['image/', 'video/'].find(s => file.type.indexOf(s) === 0); + } else { + return true; + } + }); + + if(files.length) { + if(willAttach.type == 'document') { + this.title.innerText = 'Send ' + (files.length > 1 ? files.length + ' Files' : 'File'); + container.classList.add('is-document'); + } else { + container.classList.add('is-media'); + + let foundPhotos = 0; + let foundVideos = 0; + files.forEach(file => { + if(file.type.indexOf('image/') === 0) ++foundPhotos; + else if(file.type.indexOf('video/') === 0) ++foundVideos; + }); + + if(foundPhotos && foundVideos) { + this.title.innerText = 'Send Album'; + } else if(foundPhotos) { + this.title.innerText = 'Send ' + (foundPhotos > 1 ? foundPhotos + ' Photos' : 'Photo'); + } else if(foundVideos) { + this.title.innerText = 'Send ' + (foundVideos > 1 ? foundVideos + ' Videos' : 'Video'); + } + } + } + + Promise.all(files.map(this.attachFile)).then(results => { + if(willAttach.type == 'media') { + if(willAttach.sendFileDetails.length > 1) { + container.classList.add('is-album'); + + const layouter = new Layouter(willAttach.sendFileDetails.map(o => ({w: o.width, h: o.height})), 380, 100, 4); + const layout = layouter.layout(); + + for(const {geometry, sides} of layout) { + const div = results.shift(); + + div.style.width = geometry.width + 'px'; + div.style.height = geometry.height + 'px'; + div.style.top = geometry.y + 'px'; + div.style.left = geometry.x + 'px'; + + if(sides & RectPart.Right) { + this.mediaContainer.style.width = geometry.width + geometry.x + 'px'; + } + + if(sides & RectPart.Bottom) { + this.mediaContainer.style.height = geometry.height + geometry.y + 'px'; + } + + this.mediaContainer.append(div); + } + + //console.log('chatInput album layout:', layout); + } else { + const params = willAttach.sendFileDetails[0]; + const div = results[0]; + const {w, h} = calcImageInBox(params.width, params.height, 380, 320); + div.style.width = w + 'px'; + div.style.height = h + 'px'; + this.mediaContainer.append(div); + } + } else { + this.mediaContainer.append(...results); + } + + // show now + document.body.addEventListener('keydown', this.onKeyDown); + this.onClose = () => { + document.body.removeEventListener('keydown', this.onKeyDown); + }; + this.show(); + }); + } +} diff --git a/src/components/sidebarLeft/tabs/editProfile.ts b/src/components/sidebarLeft/tabs/editProfile.ts index 09a537c8..500bde25 100644 --- a/src/components/sidebarLeft/tabs/editProfile.ts +++ b/src/components/sidebarLeft/tabs/editProfile.ts @@ -1,12 +1,12 @@ -import { SliderTab } from "../../slider"; -import popupAvatar from "../../popupAvatar"; -import apiManager from "../../../lib/mtproto/mtprotoworker"; -import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appSidebarLeft from ".."; -import Scrollable from "../../scrollable"; +import { InputFile } from "../../../layer"; +import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; +import apiManager from "../../../lib/mtproto/mtprotoworker"; import $rootScope from "../../../lib/rootScope"; -import { InputFile } from "../../../layer"; +import PopupAvatar from "../../popupAvatar"; +import Scrollable from "../../scrollable"; +import { SliderTab } from "../../slider"; // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) @@ -36,7 +36,7 @@ export default class AppEditProfileTab implements SliderTab { constructor() { this.container.querySelector('.avatar-edit').addEventListener('click', () => { - popupAvatar.open(this.canvas, (_upload) => { + new PopupAvatar().open(this.canvas, (_upload) => { this.uploadAvatar = _upload; this.handleChange(); this.avatarElem.remove(); diff --git a/src/components/sidebarLeft/tabs/newChannel.ts b/src/components/sidebarLeft/tabs/newChannel.ts index 4be354e1..5927823f 100644 --- a/src/components/sidebarLeft/tabs/newChannel.ts +++ b/src/components/sidebarLeft/tabs/newChannel.ts @@ -1,7 +1,7 @@ -import { SliderTab } from "../../slider"; -import popupAvatar from "../../popupAvatar"; -import appChatsManager from "../../../lib/appManagers/appChatsManager"; import appSidebarLeft, { AppSidebarLeft } from ".."; +import appChatsManager from "../../../lib/appManagers/appChatsManager"; +import PopupAvatar from "../../popupAvatar"; +import { SliderTab } from "../../slider"; export default class AppNewChannelTab implements SliderTab { private container = document.querySelector('.new-channel-container') as HTMLDivElement; @@ -14,7 +14,7 @@ export default class AppNewChannelTab implements SliderTab { constructor() { this.container.querySelector('.avatar-edit').addEventListener('click', () => { - popupAvatar.open(this.canvas, (_upload) => { + new PopupAvatar().open(this.canvas, (_upload) => { this.uploadAvatar = _upload; }); }); diff --git a/src/components/sidebarLeft/tabs/newGroup.ts b/src/components/sidebarLeft/tabs/newGroup.ts index a2d7cc6c..6f700775 100644 --- a/src/components/sidebarLeft/tabs/newGroup.ts +++ b/src/components/sidebarLeft/tabs/newGroup.ts @@ -1,11 +1,11 @@ -import { SliderTab } from "../../slider"; -import { SearchGroup } from "../../appSearch"; -import popupAvatar from "../../popupAvatar"; -import appChatsManager from "../../../lib/appManagers/appChatsManager"; import appSidebarLeft, { AppSidebarLeft } from ".."; -import Scrollable from "../../scrollable"; +import appChatsManager from "../../../lib/appManagers/appChatsManager"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; +import { SearchGroup } from "../../appSearch"; +import PopupAvatar from "../../popupAvatar"; +import Scrollable from "../../scrollable"; +import { SliderTab } from "../../slider"; export default class AppNewGroupTab implements SliderTab { private container = document.querySelector('.new-group-container') as HTMLDivElement; @@ -19,7 +19,7 @@ export default class AppNewGroupTab implements SliderTab { constructor() { this.container.querySelector('.avatar-edit').addEventListener('click', () => { - popupAvatar.open(this.canvas, (_upload) => { + new PopupAvatar().open(this.canvas, (_upload) => { this.uploadAvatar = _upload; }); }); diff --git a/src/index.hbs b/src/index.hbs index c4a00ba9..1e34b04b 100644 --- a/src/index.hbs +++ b/src/index.hbs @@ -93,7 +93,7 @@
-

Enter a password

+

Enter Your Password

Your account is protected with
an additional password

@@ -131,17 +131,6 @@
- --}}
-