diff --git a/src/components/chat/dragAndDrop.ts b/src/components/chat/dragAndDrop.ts new file mode 100644 index 00000000..52936037 --- /dev/null +++ b/src/components/chat/dragAndDrop.ts @@ -0,0 +1,86 @@ +import { generatePathData } from "../../helpers/dom"; + +export default class ChatDragAndDrop { + container: HTMLDivElement; + svg: SVGSVGElement; + outlineWrapper: HTMLDivElement; + path: SVGPathElement; + + constructor(appendTo: HTMLElement, private options: { + icon: string, + header: string, + subtitle: string, + onDrop: (e: DragEvent) => void + }) { + this.container = document.createElement('div'); + this.container.classList.add('drop', 'z-depth-1'); + + this.outlineWrapper = document.createElement('div'); + this.outlineWrapper.classList.add('drop-outline-wrapper'); + + this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.svg.classList.add('drop-outline'); + + this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + this.path.classList.add('drop-outline-path'); + + const dropIcon = document.createElement('div'); + dropIcon.classList.add('drop-icon', 'tgico-' + options.icon); + + const dropHeader = document.createElement('div'); + dropHeader.classList.add('drop-header'); + dropHeader.innerHTML = options.header;//'Drop files here to send them'; + + const dropSubtitle = document.createElement('div'); + dropSubtitle.classList.add('drop-subtitle'); + dropSubtitle.innerHTML = options.subtitle;//'without compression'; + + this.svg.append(this.path); + this.outlineWrapper.append(this.svg); + + this.container.append(this.outlineWrapper, dropIcon, dropHeader, dropSubtitle); + appendTo.append(this.container); + + this.container.addEventListener('dragover', this.onDragOver); + this.container.addEventListener('dragleave', this.onDragLeave); + this.container.addEventListener('drop', this.onDrop); + } + + onDragOver = (e: DragEvent) => { + this.container.classList.add('is-dragover'); + //SetTransition(this.container, 'is-dragover', true, 500); + }; + + onDragLeave = (e: DragEvent) => { + this.container.classList.remove('is-dragover'); + //SetTransition(this.container, 'is-dragover', false, 500); + }; + + onDrop = (e: DragEvent) => { + this.options.onDrop(e); + }; + + destroy() { + delete this.options; + this.container.remove(); + this.container.removeEventListener('dragover', this.onDragOver); + this.container.removeEventListener('dragleave', this.onDragLeave); + this.container.removeEventListener('drop', this.onDrop); + } + + setPath() { + const rect = this.outlineWrapper.getBoundingClientRect(); + this.svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); + this.svg.setAttributeNS(null, 'viewBox', `0 0 ${rect.width} ${rect.height}`); + this.svg.setAttributeNS(null, 'width', `${rect.width}`); + this.svg.setAttributeNS(null, 'height', `${rect.height}`); + + const radius = 10; + //const strokeWidth = 2; + const sizeX = rect.width - radius; + const sizeY = rect.height - radius; + const pos = radius / 2; + const d = generatePathData(pos, pos, sizeX, sizeY, radius, radius, radius, radius); + this.path.setAttributeNS(null, 'd', d); + } +} \ No newline at end of file diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 20a8edea..e9762d76 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -321,6 +321,8 @@ export function generatePathData(x: number, y: number, width: number, height: nu return data.join(' '); }; +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.generatePathData = generatePathData); + //export function findUpClassName(el: any, className: string): T; export function findUpClassName(el: any, className: string): HTMLElement { return el.closest('.' + className); @@ -590,3 +592,65 @@ export const getElementByPoint = (container: HTMLElement, verticalSide: 'top' | const y = verticalSide == 'bottom' ? Math.floor(rect.top + rect.height - 1) : Math.ceil(rect.top + 1); return document.elementFromPoint(x, y) as any; }; + +export async function getFilesFromEvent(e: ClipboardEvent | DragEvent, onlyTypes = false): Promise { + const files: any[] = []; + + const scanFiles = async(item: any) => { + if(item.isDirectory) { + const directoryReader = item.createReader(); + await new Promise((resolve, reject) => { + directoryReader.readEntries(async(entries: any) => { + for(const entry of entries) { + await scanFiles(entry); + } + + resolve(); + }); + }); + } else if(item) { + if(onlyTypes) { + files.push(item.type); + } else { + const file = item instanceof File ? + item : + ( + item instanceof DataTransferItem ? + item.getAsFile() : + await new Promise((resolve, reject) => item.file(resolve, reject)) + ); + + /* if(!onlyTypes) { + console.log('getFilesFromEvent: got file', item, file); + } */ + + if(!file) return; + files.push(file); + } + } + }; + + if(e instanceof DragEvent && e.dataTransfer.files && !e.dataTransfer.items) { + for(let i = 0; i < e.dataTransfer.files.length; i++) { + const file = e.dataTransfer.files[i]; + files.push(onlyTypes ? file.type : file); + } + } else { + // @ts-ignore + const items = (e.dataTransfer || e.clipboardData || e.originalEvent.clipboardData).items; + + for(let i = 0; i < items.length; ++i) { + const item: DataTransferItem = items[i]; + if(item.kind === 'file') { + const entry = onlyTypes ? item : item.webkitGetAsEntry() || item.getAsFile(); + await scanFiles(entry); + } + } + } + + /* if(!onlyTypes) { + console.log('getFilesFromEvent: got files:', e, files); + } */ + + return files; +} diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index bc5c2385..15c3f550 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -20,13 +20,16 @@ import appPhotosManager from './appPhotosManager'; import appProfileManager from './appProfileManager'; import appStickersManager from './appStickersManager'; import appWebPagesManager from './appWebPagesManager'; -import { cancelEvent, placeCaretAtEnd } from '../../helpers/dom'; +import { cancelEvent, findUpClassName, generatePathData, getFilesFromEvent, placeCaretAtEnd } from '../../helpers/dom'; import PopupNewMedia from '../../components/popupNewMedia'; import { TransitionSlider } from '../../components/transition'; import { numberWithCommas } from '../../helpers/number'; import MarkupTooltip from '../../components/chat/markupTooltip'; import { isTouchSupported } from '../../helpers/touchSupport'; import appPollsManager from './appPollsManager'; +import SetTransition from '../../components/singleTransition'; +import { isSafari } from '../../helpers/userAgent'; +import ChatDragAndDrop from '../../components/chat/dragAndDrop'; //console.log('appImManager included33!'); @@ -199,40 +202,134 @@ export class AppImManager { document.body.addEventListener('keydown', onKeyDown); + if(!isTouchSupported) { + this.attachDragAndDropListeners(); + } + if(!isTouchSupported) { this.markupTooltip = new MarkupTooltip(this); this.markupTooltip.handleSelection(); } } - private onDocumentPaste = (e: ClipboardEvent) => { + private attachDragAndDropListeners() { + const drops: ChatDragAndDrop[] = []; + let mounted = false; + const toggle = async(e: DragEvent, mount: boolean) => { + if(mount == mounted) return; + + if(mount && !drops.length) { + const types: string[] = await getFilesFromEvent(e, true) + const isFiles = e.dataTransfer.types[0] === 'Files' && !types.length; // * can't get file items not from 'drop' on Safari + + const foundMedia = types.filter(t => ['image', 'video'].includes(t.split('/')[0])).length; + const foundDocuments = types.length - foundMedia; + + this.log('drag files', types); + + if(types.length || isFiles) { + drops.push(new ChatDragAndDrop(dropsContainer, { + icon: 'dragfiles', + header: 'Drop files here to send them', + subtitle: 'without compression', + onDrop: (e: DragEvent) => { + toggle(e, false); + appImManager.log('drop', e); + appImManager.onDocumentPaste(e, 'document'); + } + })); + } + + if((foundMedia && !foundDocuments) || isFiles) { + drops.push(new ChatDragAndDrop(dropsContainer, { + icon: 'dragmedia', + header: 'Drop files here to send them', + subtitle: 'in a quick way', + onDrop: (e: DragEvent) => { + toggle(e, false); + appImManager.log('drop', e); + appImManager.onDocumentPaste(e, 'media'); + } + })); + } + + this.chat.container.append(dropsContainer); + } + + //if(!mount) return; + + SetTransition(dropsContainer, 'is-visible', mount, 200, () => { + if(!mount) { + drops.forEach(drop => { + drop.destroy(); + }); + + drops.length = 0; + } + }); + + if(mount) { + drops.forEach(drop => { + drop.setPath(); + }); + } else { + counter = 0; + } + + document.body.classList.toggle('is-dragging', mount); + mounted = mount; + }; + + /* document.body.addEventListener('dragover', (e) => { + cancelEvent(e); + }); */ + + let counter = 0; + document.body.addEventListener('dragenter', (e) => { + counter++; + }); + + document.body.addEventListener('dragover', (e) => { + //this.log('dragover', e/* , e.dataTransfer.types[0] */); + toggle(e, true); + cancelEvent(e); + }); + + document.body.addEventListener('dragleave', (e) => { + //this.log('dragleave', e, counter); + //if((e.pageX <= 0 || e.pageX >= appPhotosManager.windowW) || (e.pageY <= 0 || e.pageY >= appPhotosManager.windowH)) { + counter--; + if(counter === 0) { + //if(!findUpClassName(e.target, 'drops-container')) { + toggle(e, false); + } + }); + + const dropsContainer = document.createElement('div'); + dropsContainer.classList.add('drops-container'); + } + + private onDocumentPaste = (e: ClipboardEvent | DragEvent, attachType?: 'media' | 'document') => { const peerId = this.chat?.peerId; if(!peerId || rootScope.overlayIsActive || (peerId < 0 && !appChatsManager.hasRights(peerId, 'send', 'send_media'))) { return; } //console.log('document paste'); - - // @ts-ignore - const items = (e.clipboardData || e.originalEvent.clipboardData).items; //console.log('item', event.clipboardData.getData()); - //let foundFile = false; - const chatInput = this.chat.input; - for(let i = 0; i < items.length; ++i) { - if(items[i].kind == 'file') { - e.preventDefault() - e.cancelBubble = true; - e.stopPropagation(); - //foundFile = true; - - let file = items[i].getAsFile(); - //console.log(items[i], file); - if(!file) continue; - - chatInput.willAttachType = file.type.indexOf('image/') === 0 ? 'media' : "document"; - new PopupNewMedia([file], chatInput.willAttachType); + + cancelEvent(e); + getFilesFromEvent(e).then((files: File[]) => { + if(files.length) { + if(attachType == 'media' && files.find(file => !['image', 'video'].includes(file.type.split('/')[0]))) { + attachType = 'document'; + } + + const chatInput = this.chat.input; + chatInput.willAttachType = attachType || (files[0].type.indexOf('image/') === 0 ? 'media' : "document"); + new PopupNewMedia(files, chatInput.willAttachType); } - } + }); }; public selectTab(id: number) { diff --git a/src/scss/partials/_chatDrop.scss b/src/scss/partials/_chatDrop.scss new file mode 100644 index 00000000..66c0fd84 --- /dev/null +++ b/src/scss/partials/_chatDrop.scss @@ -0,0 +1,123 @@ +.drops-container { + --padding: 20px; + --pinned-floating-height: 0px; + position: absolute !important; + z-index: 3; + top: calc(56px + var(--pinned-floating-height) + var(--padding)); + right: var(--padding); + bottom: var(--padding); + left: var(--padding); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + user-select: none; + width: auto !important; + + @include respond-to(medium-screens) { + //transition: transform var(--layer-transition); + + body.is-right-column-shown & { + //left: calc(var(--right-column-width) / -2); + right: calc(var(--right-column-width)); + } + } + + @include respond-to(handhelds) { + --padding: 10px; + } + + &:not(.is-visible) { + display: none; + } + + &.is-visible { + animation: fade-in-opacity .2s linear forwards; + + &.backwards { + animation: fade-in-backwards-opacity .2s linear forwards; + } + } +} + +.drop { + background-color: #fff; + position: relative; + //height: 100%; + border-radius: $border-radius-big; + width: 100%; + max-width: 696px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #a2acb4; + transition: color .2s ease-in-out; + pointer-events: all; + flex: 1 1 auto; + + &-outline { + &-wrapper { + position: absolute; + top: 19px; + right: 19px; + bottom: 19px; + left: 19px; + pointer-events: none; + } + + &-path { + fill: none; + stroke-dasharray: 13.5, 11; + stroke: #a2acb4; + stroke-width: 2; + stroke-linecap: round; + transition: stroke .2s ease-in-out; + stroke-dashoffset: 0; + + .drop.is-dragover & { + animation: drop-outline-move .5s linear infinite; + stroke: $color-blue; + } + + /* .drop.is-dragover.backwards & { + //animation: drop-outline-backwards-move .5s linear forwards; + animation-direction: reverse; + animation-fill-mode: forwards; + } */ + } + } + + &-icon { + font-size: 6rem; + } + + &-header { + font-weight: 500; + font-size: 1.25rem; + } + + &.is-dragover { + color: $color-blue; + } + + & + & { + margin-top: 10px; + } +} + +@keyframes drop-outline-move { + 0% { + stroke-dashoffset: 0; + } + + 100% { + stroke-dashoffset: -24.5; + } +} + +body.is-dragging { + .page-chats { + pointer-events: none; + } +} \ No newline at end of file diff --git a/src/scss/partials/_chatTopbar.scss b/src/scss/partials/_chatTopbar.scss index b32e9909..e9991f6e 100644 --- a/src/scss/partials/_chatTopbar.scss +++ b/src/scss/partials/_chatTopbar.scss @@ -14,6 +14,9 @@ /* & + .bubbles { margin-top: 52px; } */ + & ~ .drops-container { + --pinned-floating-height: 52px; + } } &.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) { diff --git a/src/scss/style.scss b/src/scss/style.scss index fc45f862..c780fc07 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -124,6 +124,7 @@ $chat-padding-handhelds: .5rem; @import "partials/chatPinned"; @import "partials/chatMarkupTooltip"; @import "partials/chatStickersHelper"; +@import "partials/chatDrop"; @import "partials/sidebar"; @import "partials/leftSidebar"; @import "partials/rightSidebar";