diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index 644d787f..01a4717e 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -193,7 +193,7 @@ class AppMediaPlaybackController { return; } - for(let m of value.history) { + for(const {mid: m} of value.history) { if(m > mid) { this.nextMid = m; } else if(m < mid) { diff --git a/src/components/appMediaViewer.ts b/src/components/appMediaViewer.ts index 1a7f1e48..783da884 100644 --- a/src/components/appMediaViewer.ts +++ b/src/components/appMediaViewer.ts @@ -20,7 +20,7 @@ import { ButtonMenuItemOptions } from "./buttonMenu"; import ButtonMenuToggle from "./buttonMenuToggle"; import { LazyLoadQueueBase } from "./lazyLoadQueue"; import { renderImageFromUrl } from "./misc"; -import PopupForward from "./popupForward"; +import PopupForward from "./popups/forward"; import ProgressivePreloader from "./preloader"; import Scrollable from "./scrollable"; import appSidebarRight from "./sidebarRight"; @@ -1261,8 +1261,8 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet } const method = older ? value.history.forEach : value.history.forEachReverse; - method.call(value.history, mid => { - const message = appMessagesManager.getMessageByPeer(this.peerId, mid); + method.call(value.history, message => { + const mid = message.mid const media = this.getMediaFromMessage(message); if(!media) return; diff --git a/src/components/appSearch.ts b/src/components/appSearch.ts index 04823988..34c3ee15 100644 --- a/src/components/appSearch.ts +++ b/src/components/appSearch.ts @@ -251,15 +251,13 @@ export default class AppSearch { const {count, history, next_rate} = res; - if(history[0] == this.minMsgId) { + if(history[0].mid == this.minMsgId) { history.shift(); } const searchGroup = this.searchGroups.messages; - history.forEach((msgId: number) => { - const message = appMessagesManager.getMessageByPeer(this.peerId, msgId); - + history.forEach((message: any) => { const {dialog, dom} = appDialogsManager.addDialogNew({ dialog: message.peerId, container: this.scrollable/* searchGroup.list */, @@ -271,7 +269,7 @@ export default class AppSearch { searchGroup.toggle(); - this.minMsgId = history[history.length - 1]; + this.minMsgId = history[history.length - 1].mid; this.offsetRate = next_rate; if(this.loadedCount == -1) { diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 30f6d658..49011f26 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -49,15 +49,14 @@ export default class AvatarElement extends HTMLElement { if(peerId < 0) { const maxId = Number.MAX_SAFE_INTEGER; const inputFilter = 'inputMessagesFilterChatPhotos'; - const mid = await appMessagesManager.getSearch(peerId, '', {_: inputFilter}, maxId, 2, 0, 1).then(value => { + let message: any = await appMessagesManager.getSearch(peerId, '', {_: inputFilter}, maxId, 2, 0, 1).then(value => { //console.log(lol); // ! by descend return value.history[0]; }); - if(mid) { + if(message) { // ! гений в деле, костылируем (но это гениально) - let message = appMessagesManager.getMessageByPeer(peerId, mid); const messagePhoto = message.action.photo; if(messagePhoto.id != photo.id) { message = { diff --git a/src/components/chat/bubbleGroups.ts b/src/components/chat/bubbleGroups.ts index 0aa834a1..f4535315 100644 --- a/src/components/chat/bubbleGroups.ts +++ b/src/components/chat/bubbleGroups.ts @@ -1,63 +1,77 @@ import rootScope from "../../lib/rootScope"; import { generatePathData } from "../../helpers/dom"; +import { MyMessage } from "../../lib/appManagers/appMessagesManager"; -type BubbleGroup = {timestamp: number, fromId: number, mid: number, group: HTMLDivElement[]}; +type Group = {bubble: HTMLDivElement, mid: number, timestamp: number}[]; +type BubbleGroup = {timestamp: number, fromId: number, mid: number, group: Group}; export default class BubbleGroups { - bubblesByGroups: Array = []; // map to group - groups: Array = []; + private bubbles: Array = []; // map to group + private groups: Array = []; //updateRAFs: Map = new Map(); - newGroupDiff = 120; + private newGroupDiff = 121; // * 121 in scheduled messages removeBubble(bubble: HTMLDivElement, mid: number) { - let details = this.bubblesByGroups.findAndSplice(g => g.mid == mid); + const details = this.bubbles.findAndSplice(g => g.mid === mid); if(details && details.group.length) { - details.group.findAndSplice(d => d == bubble); + details.group.findAndSplice(d => d.bubble === bubble); if(!details.group.length) { - this.groups.findAndSplice(g => g == details.group); + this.groups.findAndSplice(g => g === details.group); } else { this.updateGroup(details.group); } } } - addBubble(bubble: HTMLDivElement, message: any, reverse: boolean) { - let timestamp = message.date; + addBubble(bubble: HTMLDivElement, message: MyMessage, reverse: boolean) { + const timestamp = message.date; + const mid = message.mid; let fromId = message.fromId; - let group: HTMLDivElement[]; + let group: Group; // fix for saved messages forward to self - if(fromId == rootScope.myId && message.peerId == rootScope.myId && message.fwdFromId == fromId) { + if(fromId === rootScope.myId && message.peerId === rootScope.myId && (message as any).fwdFromId === fromId) { fromId = -fromId; } // try to find added //this.removeBubble(message.mid); - if(this.bubblesByGroups.length) { - if(reverse) { - let g = this.bubblesByGroups[0]; - if(g.fromId == fromId && (g.timestamp - timestamp) < this.newGroupDiff) { - group = g.group; - group.unshift(bubble); - } else { - this.groups.unshift(group = [bubble]); - } - } else { - let g = this.bubblesByGroups[this.bubblesByGroups.length - 1]; - if(g.fromId == fromId && (timestamp - g.timestamp) < this.newGroupDiff) { - group = g.group; - group.push(bubble); - } else { - this.groups.push(group = [bubble]); + const insertObject = {bubble, mid, timestamp}; + if(this.bubbles.length) { + const foundBubble = this.bubbles.find(bubble => { + const diff = Math.abs(bubble.timestamp - timestamp); + return bubble.fromId === fromId && diff <= this.newGroupDiff; + }); + + if(!foundBubble) this.groups.push(group = [insertObject]); + else { + group = foundBubble.group; + + let i = 0, foundMidOnSameTimestamp = 0; + for(; i < group.length; ++i) { + const _timestamp = group[i].timestamp; + const _mid = group[i].mid; + + if(timestamp < _timestamp) { + break; + } else if(timestamp === _timestamp) { + foundMidOnSameTimestamp = _mid; + } + + if(foundMidOnSameTimestamp && mid < foundMidOnSameTimestamp) { + break; + } } + + group.splice(i, 0, insertObject); } } else { - this.groups.push(group = [bubble]); + this.groups.push(group = [insertObject]); } //console.log('[BUBBLE]: addBubble', bubble, message.mid, fromId, reverse, group); - this.bubblesByGroups[reverse ? 'unshift' : 'push']({timestamp, fromId, mid: message.mid, group}); + this.bubbles.push({timestamp, fromId, mid: message.mid, group}); this.updateGroup(group); } @@ -112,7 +126,7 @@ export default class BubbleGroups { } } - updateGroup(group: HTMLDivElement[]) { + updateGroup(group: Group) { /* if(this.updateRAFs.has(group)) { window.cancelAnimationFrame(this.updateRAFs.get(group)); this.updateRAFs.delete(group); @@ -125,7 +139,7 @@ export default class BubbleGroups { return; } - let first = group[0]; + const first = group[0].bubble; //console.log('[BUBBLE]: updateGroup', group, first); @@ -139,14 +153,14 @@ export default class BubbleGroups { this.setClipIfNeeded(first, true); } - let length = group.length - 1; + const length = group.length - 1; for(let i = 1; i < length; ++i) { - let bubble = group[i]; + const bubble = group[i].bubble; bubble.classList.remove('is-group-last', 'is-group-first'); this.setClipIfNeeded(bubble, true); } - let last = group[group.length - 1]; + const last = group[group.length - 1].bubble; last.classList.remove('is-group-first'); last.classList.add('is-group-last'); this.setClipIfNeeded(last); @@ -154,14 +168,14 @@ export default class BubbleGroups { } updateGroupByMessageId(mid: number) { - let details = this.bubblesByGroups.find(g => g.mid == mid); + const details = this.bubbles.find(g => g.mid == mid); if(details) { this.updateGroup(details.group); } } cleanup() { - this.bubblesByGroups = []; + this.bubbles = []; this.groups = []; /* for(let value of this.updateRAFs.values()) { window.cancelAnimationFrame(value); diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 2dd27970..3b94087b 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -1,5 +1,5 @@ import { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager"; -import type { AppMessagesManager, Dialog, HistoryResult } from "../../lib/appManagers/appMessagesManager"; +import type { AppMessagesManager, Dialog, HistoryResult, MyMessage } from "../../lib/appManagers/appMessagesManager"; import type { AppSidebarRight } from "../sidebarRight"; import type { AppStickersManager } from "../../lib/appManagers/appStickersManager"; import type { AppUsersManager } from "../../lib/appManagers/appUsersManager"; @@ -7,16 +7,16 @@ import type { AppInlineBotsManager } from "../../lib/appManagers/appInlineBotsMa import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager"; import type { AppDocsManager } from "../../lib/appManagers/appDocsManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; -import { findUpClassName, cancelEvent, findUpTag, whichChild, getElementByPoint, attachClickEvent } from "../../helpers/dom"; +import { findUpClassName, cancelEvent, findUpTag, whichChild, getElementByPoint, attachClickEvent, positionElementByIndex } from "../../helpers/dom"; import { getObjectKeysAndSort } from "../../helpers/object"; import { isTouchSupported } from "../../helpers/touchSupport"; import { logger } from "../../lib/logger"; import rootScope from "../../lib/rootScope"; import AppMediaViewer from "../appMediaViewer"; import BubbleGroups from "./bubbleGroups"; -import PopupDatePicker from "../popupDatepicker"; -import PopupForward from "../popupForward"; -import PopupStickers from "../popupStickers"; +import PopupDatePicker from "../popups/datePicker"; +import PopupForward from "../popups/forward"; +import PopupStickers from "../popups/stickers"; import ProgressivePreloader from "../preloader"; import Scrollable from "../scrollable"; import StickyIntersector from "../stickyIntersector"; @@ -35,6 +35,7 @@ import LazyLoadQueue from "../lazyLoadQueue"; import { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import Chat from "./chat"; import ListenerSetter from "../../helpers/listenerSetter"; +import PollElement from "../poll"; const IGNORE_ACTIONS = ['messageActionHistoryClear']; @@ -101,7 +102,7 @@ export default class ChatBubbles { public replyFollowHistory: number[] = []; - constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appSidebarRight: AppSidebarRight, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) { + constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appInlineBotsManager: AppInlineBotsManager, private appPhotosManager: AppPhotosManager, private appDocsManager: AppDocsManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager) { this.chat.log.error('Bubbles construction'); this.listenerSetter = new ListenerSetter(); @@ -126,32 +127,19 @@ export default class ChatBubbles { // * events - // will call when message is sent (only 1) - this.listenerSetter.add(rootScope, 'history_append', (e) => { - let details = e.detail; - - if(!this.scrolledAllDown) { - this.chat.setPeer(this.peerId, 0); - } else { - this.renderNewMessagesByIds([details.messageId], true); - } - }); - // will call when sent for update pos this.listenerSetter.add(rootScope, 'history_update', (e) => { - let details = e.detail; + const {storage, peerId, mid} = e.detail; - if(details.mid && details.peerId == this.peerId) { - let mid = details.mid; - - let bubble = this.bubbles[mid]; + if(mid && peerId == this.peerId && this.chat.getMessagesStorage() === storage) { + const bubble = this.bubbles[mid]; if(!bubble) return; - - let message = this.chat.getMessage(mid); - //this.log('history_update', this.bubbles[mid], mid, message); - let dateMessage = this.getDateContainerByMessage(message, false); - dateMessage.container.append(bubble); + const message = this.chat.getMessage(mid); + //bubble.remove(); + this.bubbleGroups.removeBubble(bubble, message.mid); + this.setBubblePosition(bubble, message, false); + //this.log('history_update', this.bubbles[mid], mid, message); this.bubbleGroups.addBubble(bubble, message, false); @@ -159,29 +147,6 @@ export default class ChatBubbles { } }); - this.listenerSetter.add(rootScope, 'history_multiappend', (e) => { - const msgIdsByPeer = e.detail; - - for(const peerId in msgIdsByPeer) { - appSidebarRight.sharedMediaTab.renderNewMessages(+peerId, msgIdsByPeer[peerId]); - } - - if(!(this.peerId in msgIdsByPeer)) return; - const msgIds = msgIdsByPeer[this.peerId]; - this.renderNewMessagesByIds(msgIds); - }); - - this.listenerSetter.add(rootScope, 'history_delete', (e) => { - const {peerId, msgs} = e.detail; - - const mids = Object.keys(msgs).map(s => +s); - appSidebarRight.sharedMediaTab.deleteDeletedMessages(peerId, mids); - - if(peerId == this.peerId) { - this.deleteMessagesByIds(mids); - } - }); - this.listenerSetter.add(rootScope, 'dialog_flush', (e) => { let peerId: number = e.detail.peerId; if(this.peerId == peerId) { @@ -191,16 +156,18 @@ export default class ChatBubbles { // Calls when message successfully sent and we have an id this.listenerSetter.add(rootScope, 'message_sent', (e) => { - const {tempId, mid} = e.detail; + const {storage, tempId, tempMessage, mid} = e.detail; + + // ! can't use peerId to validate here, because id can be the same in 'scheduled' and 'chat' types + if(this.chat.getMessagesStorage() !== storage) { + return; + } this.log('message_sent', e.detail); - const message = this.chat.getMessage(mid); - - appSidebarRight.sharedMediaTab.renderNewMessages(message.peerId, [mid]); - const mounted = this.getMountedBubble(tempId) || this.getMountedBubble(mid); if(mounted) { + const message = this.chat.getMessage(mid); const bubble = mounted.bubble; //this.bubbles[mid] = bubble; @@ -214,8 +181,9 @@ export default class ChatBubbles { if(message.media?.poll) { const newPoll = message.media.poll; - const pollElement = bubble.querySelector('poll-element'); + const pollElement = bubble.querySelector('poll-element') as PollElement; if(pollElement) { + pollElement.message = message; pollElement.setAttribute('poll-id', newPoll.id); pollElement.setAttribute('message-id', '' + mid); } @@ -261,27 +229,43 @@ export default class ChatBubbles { this.unreadOut.delete(tempId); this.unreadOut.add(mid); } + + // * check timing of scheduled message + if(this.chat.type === 'scheduled') { + const timestamp = Date.now() / 1000 | 0; + const maxTimestamp = tempMessage.date - 10; + this.log('scheduled timing:', timestamp, maxTimestamp); + if(timestamp >= maxTimestamp) { + this.deleteMessagesByIds([mid]); + } + } }); this.listenerSetter.add(rootScope, 'message_edit', (e) => { - const {peerId, mid} = e.detail; + const {storage, peerId, mid} = e.detail; - if(peerId != this.peerId) return; + if(peerId != this.peerId || storage !== this.chat.getMessagesStorage()) return; const mounted = this.getMountedBubble(mid); if(!mounted) return; - this.renderMessage(mounted.message, true, false, mounted.bubble, false); + + const updatePosition = this.chat.type === 'scheduled'; + this.renderMessage(mounted.message, true, false, mounted.bubble, updatePosition); + + if(updatePosition) { + this.deleteEmptyDateGroups(); + } }); this.listenerSetter.add(rootScope, 'album_edit', (e) => { const {peerId, groupId, deletedMids} = e.detail; if(peerId != this.peerId) return; - const mids = appMessagesManager.getMidsByAlbum(groupId); - const maxId = Math.max(...mids.concat(deletedMids)); - if(!this.bubbles[maxId]) return; + const mids = this.appMessagesManager.getMidsByAlbum(groupId); + const renderedId = mids.concat(deletedMids).find(mid => this.bubbles[mid]); + if(!renderedId) return; - const renderMaxId = getObjectKeysAndSort(appMessagesManager.groupedMessagesStorage[groupId], 'asc').pop(); - this.renderMessage(this.chat.getMessage(renderMaxId), true, false, this.bubbles[maxId], false); + const renderMaxId = getObjectKeysAndSort(this.appMessagesManager.groupedMessagesStorage[groupId], 'asc').pop(); + this.renderMessage(this.chat.getMessage(renderMaxId), true, false, this.bubbles[renderedId], false); }); this.listenerSetter.add(rootScope, 'messages_downloaded', (e) => { @@ -321,9 +305,48 @@ export default class ChatBubbles { }); this.listenerSetter.add(this.bubblesContainer, 'click', this.onBubblesClick/* , {capture: true, passive: false} */); + + this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => { + for(const timestamp in this.dateMessages) { + const dateMessage = this.dateMessages[timestamp]; + if(dateMessage.container == target) { + dateMessage.div.classList.toggle('is-sticky', stuck); + break; + } + } + }); } public constructPeerHelpers() { + // will call when message is sent (only 1) + this.listenerSetter.add(rootScope, 'history_append', (e) => { + let details = e.detail; + + if(!this.scrolledAllDown) { + this.chat.setPeer(this.peerId, 0); + } else { + this.renderNewMessagesByIds([details.messageId], true); + } + }); + + this.listenerSetter.add(rootScope, 'history_multiappend', (e) => { + const msgIdsByPeer = e.detail; + + if(!(this.peerId in msgIdsByPeer)) return; + const msgIds = msgIdsByPeer[this.peerId]; + this.renderNewMessagesByIds(msgIds); + }); + + this.listenerSetter.add(rootScope, 'history_delete', (e) => { + const {peerId, msgs} = e.detail; + + const mids = Object.keys(msgs).map(s => +s); + + if(peerId == this.peerId) { + this.deleteMessagesByIds(mids); + } + }); + this.listenerSetter.add(rootScope, 'dialog_unread', (e) => { const info = e.detail; @@ -350,16 +373,6 @@ export default class ChatBubbles { } }); - this.stickyIntersector = new StickyIntersector(this.scrollable.container, (stuck, target) => { - for(const timestamp in this.dateMessages) { - const dateMessage = this.dateMessages[timestamp]; - if(dateMessage.container == target) { - dateMessage.div.classList.toggle('is-sticky', stuck); - break; - } - } - }); - this.unreadedObserver = new IntersectionObserver((entries) => { if(this.chat.appImManager.offline) { // ! but you can scroll the page without triggering 'focus', need something now return; @@ -418,11 +431,24 @@ export default class ChatBubbles { } public constructScheduledHelpers() { + const onUpdate = () => { + this.chat.topbar.setTitle(Object.keys(this.appMessagesManager.getScheduledMessagesStorage(this.peerId)).length); + }; + + this.listenerSetter.add(rootScope, 'scheduled_new', (e) => { + const {peerId, mid} = e.detail; + if(peerId !== this.peerId) return; + + this.renderNewMessagesByIds([mid]); + onUpdate(); + }); + this.listenerSetter.add(rootScope, 'scheduled_delete', (e) => { const {peerId, mids} = e.detail; if(peerId !== this.peerId) return; this.deleteMessagesByIds(mids); + onUpdate(); }); } @@ -653,9 +679,11 @@ export default class ChatBubbles { const group = this.appMessagesManager.groupedMessagesStorage[groupId]; for(const mid in group) { if(this.bubbles[mid]) { + const maxId = Math.max(...Object.keys(group).map(id => +id)); // * because in scheduled album can be rendered by lowest mid during sending return { bubble: this.bubbles[mid], - message: this.chat.getMessage(+mid) + mid: +mid, + message: this.chat.getMessage(maxId) }; } } @@ -681,7 +709,7 @@ export default class ChatBubbles { const bubble = this.bubbles[mid]; if(!bubble) return; - return {bubble, message}; + return {bubble, mid, message}; } private findNextMountedBubbleByMsgId(mid: number) { @@ -843,14 +871,15 @@ export default class ChatBubbles { this.deleteEmptyDateGroups(); } - public renderNewMessagesByIds(msgIds: number[], scrolledDown = this.scrolledDown) { + public renderNewMessagesByIds(mids: number[], scrolledDown = this.scrolledDown) { if(!this.scrolledAllDown) { // seems search active or sliced - this.log('seems search is active, skipping render:', msgIds); + this.log('seems search is active, skipping render:', mids); return; } - - msgIds.forEach((msgId: number) => { - let message = this.chat.getMessage(msgId); + + mids = mids.filter(mid => !this.bubbles[mid]); + mids.forEach((mid: number) => { + const message = this.chat.getMessage(mid); /////////this.log('got new message to append:', message); @@ -910,6 +939,10 @@ export default class ChatBubbles { str += ', ' + date.getFullYear(); } } + + if(this.chat.type === 'scheduled') { + str = 'Scheduled for ' + str; + } const div = document.createElement('div'); div.className = 'bubble service is-date'; @@ -918,6 +951,15 @@ export default class ChatBubbles { const container = document.createElement('div'); container.className = 'bubbles-date-group'; + + const haveTimestamps = getObjectKeysAndSort(this.dateMessages, 'asc'); + let i = 0; + for(; i < haveTimestamps.length; ++i) { + const t = haveTimestamps[i]; + if(dateTimestamp < t) { + break; + } + } this.dateMessages[dateTimestamp] = { div, @@ -927,11 +969,13 @@ export default class ChatBubbles { container.append(div); - if(reverse) { + positionElementByIndex(container, this.chatInner, i); + + /* if(reverse) { this.chatInner.prepend(container); } else { this.chatInner.append(container); - } + } */ if(this.stickyIntersector) { this.stickyIntersector.observeStickyHeaderChanges(container); @@ -1060,7 +1104,7 @@ export default class ChatBubbles { this.log('setPeer peerId:', this.peerId, dialog, lastMsgId, topMessage); // add last message, bc in getHistory will load < max_id - const additionMsgId = isJump ? 0 : topMessage; + const additionMsgId = isJump || this.chat.type !== 'chat' ? 0 : topMessage; /* this.setPeerPromise = null; this.preloader.detach(); @@ -1231,7 +1275,7 @@ export default class ChatBubbles { } else if(el.readyState >= 4) return; } else if(el.complete || !el.src) return; - let promise = new Promise((resolve, reject) => { + let promise = new Promise((resolve, reject) => { let r: () => boolean; let onLoad = () => { clearTimeout(timeout); @@ -1291,12 +1335,7 @@ export default class ChatBubbles { } queue.forEach(({message, bubble, reverse}) => { - const dateMessage = this.getDateContainerByMessage(message, reverse); - if(reverse) { - dateMessage.container.insertBefore(bubble, dateMessage.div.nextSibling); - } else { - dateMessage.container.append(bubble); - } + this.setBubblePosition(bubble, message, reverse); }); //setTimeout(() => { @@ -1309,6 +1348,44 @@ export default class ChatBubbles { } } + public setBubblePosition(bubble: HTMLElement, message: any, reverse: boolean) { + const dateMessage = this.getDateContainerByMessage(message, reverse); + let children = Array.from(dateMessage.container.children).slice(1) as HTMLElement[]; + let i = 0, foundMidOnSameTimestamp = 0; + for(; i < children.length; ++i) { + const t = children[i]; + const timestamp = +t.dataset.timestamp; + if(message.date < timestamp) { + break; + } else if(message.date === timestamp) { + foundMidOnSameTimestamp = +t.dataset.mid; + } + + if(foundMidOnSameTimestamp && message.mid < foundMidOnSameTimestamp) { + break; + } + } + + // * 1 for date + let index = 1 + i; + if(bubble.parentElement) { // * if already mounted + const currentIndex = whichChild(bubble); + if(index > currentIndex) { + index -= 1; // * minus for already mounted + } + } + + positionElementByIndex(bubble, dateMessage.container, index); + + //this.bubbleGroups.updateGroupByMessageId(message.mid); + + /* if(reverse) { + dateMessage.container.insertBefore(bubble, dateMessage.div.nextSibling); + } else { + dateMessage.container.append(bubble); + } */ + } + // * will change .cleaned in cleanup() and new instance will be created public getMiddleware() { const cleanupObj = this.cleanupObj; @@ -1321,7 +1398,7 @@ export default class ChatBubbles { public renderMessage(message: any, reverse = false, multipleRender = false, bubble: HTMLDivElement = null, updatePosition = true) { this.log.debug('message to render:', message); //return; - const albumMustBeRenderedFull = this.chat.type == 'chat'; + const albumMustBeRenderedFull = this.chat.type === 'chat' || this.chat.type === 'scheduled'; if(message.deleted) return; else if(message.grouped_id && albumMustBeRenderedFull) { // will render only last album's message const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; @@ -1376,21 +1453,30 @@ export default class ChatBubbles { // * Нужно очистить прошлую информацию, полезно если удалить последний элемент из альбома в ПОСЛЕДНЕМ БАББЛЕ ГРУППЫ (видно по аватару) const originalMid = +bubble.dataset.mid; - if(+message.mid != originalMid) { + const sameMid = +message.mid == originalMid; + /* if(updatePosition) { + bubble.remove(); // * for positionElementByIndex + } */ + + if(!sameMid || updatePosition) { this.bubbleGroups.removeBubble(bubble, originalMid); + } + + if(!sameMid) { + delete this.bubbles[originalMid]; if(!updatePosition) { this.bubbleGroups.addBubble(bubble, message, reverse); } } - delete this.bubbles[originalMid]; //bubble.innerHTML = ''; } // ! reset due to album edit or delete item this.bubbles[+message.mid] = bubble; bubble.dataset.mid = message.mid; + bubble.dataset.timestamp = message.date; if(this.chat.selection.isSelecting) { this.chat.selection.toggleBubbleCheckbox(bubble, true); @@ -1950,7 +2036,7 @@ export default class ChatBubbles { case 'messageMediaPoll': { bubble.classList.remove('is-message-empty'); - const pollElement = wrapPoll(this.peerId, message.media.poll.id, message.mid); + const pollElement = wrapPoll(message); messageDiv.prepend(pollElement); messageDiv.classList.add('poll-message'); @@ -2223,7 +2309,7 @@ export default class ChatBubbles { return; } - this.chat.setPeer(this.peerId, history.messages[0].mid); + this.chat.setPeer(this.peerId, (history.messages[0] as MyMessage).mid); //console.log('got history date:', history); }); }; @@ -2233,7 +2319,8 @@ export default class ChatBubbles { if(this.chat.type === 'chat') { return this.appMessagesManager.getHistory(this.peerId, maxId, loadCount, backLimit); } else if(this.chat.type === 'pinned') { - const promise = this.appMessagesManager.getSearch(this.peerId, '', {_: 'inputMessagesFilterPinned'}, maxId, loadCount, 0, backLimit); + const promise = this.appMessagesManager.getSearch(this.peerId, '', {_: 'inputMessagesFilterPinned'}, maxId, loadCount, 0, backLimit) + .then(value => ({history: value.history.map(m => m.mid)})); /* if(maxId) { promise.then(result => { @@ -2246,7 +2333,11 @@ export default class ChatBubbles { return promise; } else if(this.chat.type === 'scheduled') { - return this.appMessagesManager.getScheduledMessages(this.peerId).then(mids => ({history: mids})); + return this.appMessagesManager.getScheduledMessages(this.peerId).then(mids => { + this.scrolledAll = true; + this.scrolledAllDown = true; + return {history: mids.slice().reverse()}; + }); } } @@ -2296,8 +2387,8 @@ export default class ChatBubbles { let additionMsgIds: number[]; if(additionMsgId && !isBackLimit) { - const historyStorage = this.appMessagesManager.historiesStorage[peerId]; - if(historyStorage && historyStorage.history.length < loadCount) { + const historyStorage = this.appMessagesManager.getHistoryStorage(peerId); + if(historyStorage.history.length < loadCount) { additionMsgIds = historyStorage.history.slice(); // * filter last album, because we don't know is this the last item @@ -2448,7 +2539,7 @@ export default class ChatBubbles { // preload more //if(!isFirstMessageRender) { if(this.chat.type === 'chat') { - const storage = this.appMessagesManager.historiesStorage[peerId]; + const storage = this.appMessagesManager.getHistoryStorage(peerId); const isMaxIdInHistory = storage.history.indexOf(maxId) !== -1; if(isMaxIdInHistory) { // * otherwise it is a search or jump setTimeout(() => { diff --git a/src/components/chat/chat.ts b/src/components/chat/chat.ts index 101f5d1e..54df773e 100644 --- a/src/components/chat/chat.ts +++ b/src/components/chat/chat.ts @@ -43,7 +43,7 @@ export default class Chat extends EventListenerBase<{ public type: ChatType = 'chat'; - constructor(public appImManager: AppImManager, private appChatsManager: AppChatsManager, private appDocsManager: AppDocsManager, private appInlineBotsManager: AppInlineBotsManager, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appPhotosManager: AppPhotosManager, private appProfileManager: AppProfileManager, private appStickersManager: AppStickersManager, private appUsersManager: AppUsersManager, private appWebPagesManager: AppWebPagesManager, private appSidebarRight: AppSidebarRight, private appPollsManager: AppPollsManager, public apiManager: ApiManagerProxy) { + constructor(public appImManager: AppImManager, public appChatsManager: AppChatsManager, public appDocsManager: AppDocsManager, public appInlineBotsManager: AppInlineBotsManager, public appMessagesManager: AppMessagesManager, public appPeersManager: AppPeersManager, public appPhotosManager: AppPhotosManager, public appProfileManager: AppProfileManager, public appStickersManager: AppStickersManager, public appUsersManager: AppUsersManager, public appWebPagesManager: AppWebPagesManager, public appPollsManager: AppPollsManager, public apiManager: ApiManagerProxy) { super(); this.container = document.createElement('div'); @@ -65,15 +65,16 @@ export default class Chat extends EventListenerBase<{ this.type = type; if(this.type === 'scheduled') { - this.getMessage = (mid) => this.appMessagesManager.getMessageFromStorage(this.appMessagesManager.getScheduledMessagesStorage(this.peerId), mid); + this.getMessagesStorage = () => this.appMessagesManager.getScheduledMessagesStorage(this.peerId); + //this.getMessage = (mid) => this.appMessagesManager.getMessageFromStorage(this.appMessagesManager.getScheduledMessagesStorage(this.peerId), mid); } } private init() { this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager); - this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appSidebarRight, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appDocsManager, this.appPeersManager, this.appChatsManager); + this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appDocsManager, this.appPeersManager, this.appChatsManager); this.input = new ChatInput(this, this.appMessagesManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager); - this.selection = new ChatSelection(this.bubbles, this.input, this.appMessagesManager); + this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager); if(this.type === 'chat') { @@ -93,6 +94,7 @@ export default class Chat extends EventListenerBase<{ this.input.constructPinnedHelpers(); } else if(this.type === 'scheduled') { this.bubbles.constructScheduledHelpers(); + this.input.constructPeerHelpers(); } this.container.classList.add('type-' + this.type); @@ -196,14 +198,21 @@ export default class Chat extends EventListenerBase<{ appSidebarRight.sharedMediaTab.fillProfileElements(); + this.log.setPrefix('CHAT-' + peerId + '-' + this.type); + rootScope.broadcast('peer_changed', peerId); } + public getMessagesStorage() { + return this.appMessagesManager.getMessagesStorage(this.peerId); + } + public getMessage(mid: number) { - return this.appMessagesManager.getMessageByPeer(this.peerId, mid); + return this.appMessagesManager.getMessageFromStorage(this.getMessagesStorage(), mid); + //return this.appMessagesManager.getMessageByPeer(this.peerId, mid); } public getMidsByMid(mid: number) { - return this.appMessagesManager.getMidsByMid(this.peerId, mid); + return this.appMessagesManager.getMidsByMessage(this.getMessage(mid)); } } \ No newline at end of file diff --git a/src/components/chat/contextMenu.ts b/src/components/chat/contextMenu.ts index 91d16fd7..b552366d 100644 --- a/src/components/chat/contextMenu.ts +++ b/src/components/chat/contextMenu.ts @@ -7,11 +7,11 @@ import { isTouchSupported } from "../../helpers/touchSupport"; import { attachClickEvent, cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import { attachContextMenuListener, openBtnMenu, positionMenu } from "../misc"; -import PopupDeleteMessages from "../popupDeleteMessages"; -import PopupForward from "../popupForward"; -import PopupPinMessage from "../popupUnpinMessage"; +import PopupDeleteMessages from "../popups/deleteMessages"; +import PopupForward from "../popups/forward"; +import PopupPinMessage from "../popups/unpinMessage"; import { copyTextToClipboard } from "../../helpers/clipboard"; -import { isAppleMobile, isSafari } from "../../helpers/userAgent"; +import PopupSendNow from "../popups/sendNow"; export default class ChatContextMenu { private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true})[]; @@ -21,6 +21,7 @@ export default class ChatContextMenu { private isTargetAGroupedItem: boolean; public peerId: number; public mid: number; + public message: any; constructor(private attachTo: HTMLElement, private chat: Chat, private appMessagesManager: AppMessagesManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appPollsManager: AppPollsManager) { const onContextMenu = (e: MouseEvent | Touch) => { @@ -71,6 +72,8 @@ export default class ChatContextMenu { this.mid = mid; } + this.message = this.chat.getMessage(this.mid); + this.buttons.forEach(button => { let good: boolean; @@ -138,21 +141,50 @@ export default class ChatContextMenu { private init() { this.buttons = [{ + icon: 'send2', + text: 'Send Now', + onClick: this.onSendScheduledClick, + verify: () => this.chat.type === 'scheduled' + }, { + icon: 'send2', + text: 'Send Now selected', + onClick: this.onSendScheduledClick, + verify: () => this.chat.type === 'scheduled' && this.chat.selection.selectedMids.has(this.mid) && !this.chat.selection.selectionSendNowBtn.hasAttribute('disabled'), + notDirect: () => true, + withSelection: true + }, { + icon: 'schedule', + text: 'Reschedule', + onClick: () => { + this.chat.input.scheduleSending(() => { + this.appMessagesManager.editMessage(this.message, this.message.message, { + scheduleDate: this.chat.input.scheduleDate, + entities: this.message.entities + }); + + this.chat.input.onMessageSent(false, false); + }, new Date(this.message.date * 1000)); + }, + verify: () => this.chat.type === 'scheduled' + }, { icon: 'reply', text: 'Reply', onClick: this.onReplyClick, - verify: () => (this.peerId > 0 || this.appChatsManager.hasRights(-this.peerId, 'send')) && this.mid > 0 && !!this.chat.input.messageInput/* , + verify: () => (this.peerId > 0 || this.appChatsManager.hasRights(-this.peerId, 'send')) && + this.mid > 0 && + !!this.chat.input.messageInput && + this.chat.type !== 'scheduled'/* , cancelEvent: true */ }, { icon: 'edit', text: 'Edit', onClick: this.onEditClick, - verify: () => this.appMessagesManager.canEditMessage(this.peerId, this.mid, 'text') && !!this.chat.input.messageInput + verify: () => this.appMessagesManager.canEditMessage(this.message, 'text') && !!this.chat.input.messageInput }, { icon: 'copy', text: 'Copy', onClick: this.onCopyClick, - verify: () => !!this.chat.getMessage(this.mid).message + verify: () => !!this.message.message }, { icon: 'copy', text: 'Copy selected', @@ -164,25 +196,22 @@ export default class ChatContextMenu { icon: 'pin', text: 'Pin', onClick: this.onPinClick, - verify: () => { - const message = this.chat.getMessage(this.mid); - return this.mid > 0 && message._ != 'messageService' && !message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerId); - } + verify: () => this.mid > 0 && + this.message._ != 'messageService' && + !this.message.pFlags.pinned && + this.appPeersManager.canPinMessage(this.peerId) && + this.chat.type !== 'scheduled', }, { icon: 'unpin', text: 'Unpin', onClick: this.onUnpinClick, - verify: () => { - const message = this.chat.getMessage(this.mid); - return message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerId); - } + verify: () => this.message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerId), }, { icon: 'revote', text: 'Revote', onClick: this.onRetractVote, verify: () => { - const message = this.chat.getMessage(this.mid); - const poll = message.media?.poll as Poll; + const poll = this.message.media?.poll as Poll; return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz; }/* , cancelEvent: true */ @@ -191,31 +220,27 @@ export default class ChatContextMenu { text: 'Stop poll', onClick: this.onStopPoll, verify: () => { - const message = this.chat.getMessage(this.mid); - const poll = message.media?.poll; - return this.appMessagesManager.canEditMessage(this.peerId, this.mid, 'poll') && poll && !poll.pFlags.closed && this.mid > 0; + const poll = this.message.media?.poll; + return this.appMessagesManager.canEditMessage(this.message, 'poll') && poll && !poll.pFlags.closed && this.mid > 0; }/* , cancelEvent: true */ }, { icon: 'forward', text: 'Forward', onClick: this.onForwardClick, - verify: () => this.mid > 0 + verify: () => this.mid > 0 && this.chat.type !== 'scheduled' }, { icon: 'forward', text: 'Forward selected', onClick: this.onForwardClick, - verify: () => this.chat.selection.selectedMids.has(this.mid) && !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'), + verify: () => this.chat.selection.selectionForwardBtn && this.chat.selection.selectedMids.has(this.mid) && !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'), notDirect: () => true, withSelection: true }, { icon: 'select', text: 'Select', onClick: this.onSelectClick, - verify: () => { - const message = this.chat.getMessage(this.mid); - return !message.action && !this.chat.selection.selectedMids.has(this.mid); - }, + verify: () => !this.message.action && !this.chat.selection.selectedMids.has(this.mid), notDirect: () => true, withSelection: true }, { @@ -229,7 +254,7 @@ export default class ChatContextMenu { icon: 'delete danger', text: 'Delete', onClick: this.onDeleteClick, - verify: () => this.appMessagesManager.canDeleteMessage(this.peerId, this.mid) + verify: () => this.appMessagesManager.canDeleteMessage(this.message) }, { icon: 'delete danger', text: 'Delete selected', @@ -244,6 +269,14 @@ export default class ChatContextMenu { this.chat.container.append(this.element); }; + private onSendScheduledClick = () => { + if(this.chat.selection.isSelecting) { + this.chat.selection.selectionSendNowBtn.click(); + } else { + new PopupSendNow(this.peerId, this.chat.getMidsByMid(this.mid)); + } + }; + private onReplyClick = () => { const message = this.chat.getMessage(this.mid); const chatInputC = this.chat.input; @@ -277,11 +310,11 @@ export default class ChatContextMenu { }; private onRetractVote = () => { - this.appPollsManager.sendVote(this.peerId, this.mid, []); + this.appPollsManager.sendVote(this.message, []); }; private onStopPoll = () => { - this.appPollsManager.stopPoll(this.peerId, this.mid); + this.appPollsManager.stopPoll(this.message); }; private onForwardClick = () => { @@ -304,7 +337,7 @@ export default class ChatContextMenu { if(this.chat.selection.isSelecting) { this.chat.selection.selectionDeleteBtn.click(); } else { - new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid)); + new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid), this.chat.type); } }; } \ No newline at end of file diff --git a/src/components/chat/input.ts b/src/components/chat/input.ts index ad664ec5..76a757f5 100644 --- a/src/components/chat/input.ts +++ b/src/components/chat/input.ts @@ -1,5 +1,5 @@ import type { AppChatsManager } from '../../lib/appManagers/appChatsManager'; -import type { AppDocsManager } from "../../lib/appManagers/appDocsManager"; +import type { AppDocsManager, MyDocument } from "../../lib/appManagers/appDocsManager"; import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppPeersManager } from '../../lib/appManagers/appPeersManager'; import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager"; @@ -11,12 +11,12 @@ import apiManager from "../../lib/mtproto/mtprotoworker"; //import Recorder from '../opus-recorder/dist/recorder.min'; import opusDecodeController from "../../lib/opusDecodeController"; import RichTextProcessor from "../../lib/richtextprocessor"; -import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getRichValue, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; -import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu'; +import { attachClickEvent, blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, serializeNodes } from "../../helpers/dom"; +import { ButtonMenuItemOptions } from '../buttonMenu'; import emoticonsDropdown from "../emoticonsDropdown"; -import PopupCreatePoll from "../popupCreatePoll"; -import PopupForward from '../popupForward'; -import PopupNewMedia from '../popupNewMedia'; +import PopupCreatePoll from "../popups/createPoll"; +import PopupForward from '../popups/forward'; +import PopupNewMedia from '../popups/newMedia'; import Scrollable from "../scrollable"; import { toast } from "../toast"; import { wrapReply } from "../wrappers"; @@ -28,7 +28,9 @@ import DivAndCaption from '../divAndCaption'; import ButtonMenuToggle from '../buttonMenuToggle'; import ListenerSetter from '../../helpers/listenerSetter'; import Button from '../button'; -import { attachContextMenuListener, openBtnMenu } from '../misc'; +import PopupSchedule from '../popups/schedule'; +import SendMenu from './sendContextMenu'; +import rootScope from '../../lib/rootScope'; const RECORD_MIN_TIME = 500; const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.'; @@ -37,7 +39,8 @@ type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply'; export default class ChatInput { public pageEl = document.getElementById('page-chats') as HTMLDivElement; - public messageInput: HTMLDivElement; + public messageInput: HTMLElement; + public messageInputField: InputField; public fileInput: HTMLInputElement; public inputMessageContainer: HTMLDivElement; public inputScroll: Scrollable; @@ -56,8 +59,7 @@ export default class ChatInput { public attachMenu: HTMLButtonElement; private attachMenuButtons: (ButtonMenuItemOptions & {verify: (peerId: number) => boolean})[]; - public sendMenu: HTMLDivElement; - private sendMenuButtons: (ButtonMenuItemOptions & {verify: (peerId: number) => boolean})[]; + public sendMenu: SendMenu; public replyElements: { container?: HTMLElement, @@ -69,9 +71,11 @@ export default class ChatInput { public willSendWebPage: any = null; public forwardingMids: number[] = []; public forwardingFromPeerId: number = 0; - public replyToMsgId = 0; - public editMsgId = 0; + public replyToMsgId: number; + public editMsgId: number; public noWebPage: true; + public scheduleDate: number; + public sendSilent: true; private recorder: any; private recording = false; @@ -160,8 +164,36 @@ export default class ChatInput { this.inputScroll = new Scrollable(this.inputMessageContainer); - this.btnScheduled = ButtonIcon('schedule', {noRipple: true}); - this.btnScheduled.classList.add('btn-scheduled', 'hide'); + if(this.chat.type === 'chat') { + this.btnScheduled = ButtonIcon('schedule', {noRipple: true}); + this.btnScheduled.classList.add('btn-scheduled', 'hide'); + + attachClickEvent(this.btnScheduled, (e) => { + this.appImManager.openScheduled(this.chat.peerId); + }, {listenerSetter: this.listenerSetter}); + + this.listenerSetter.add(rootScope, 'scheduled_new', (e) => { + const peerId = e.detail.peerId; + + if(this.chat.peerId !== peerId) { + return; + } + + this.btnScheduled.classList.remove('hide'); + }); + + this.listenerSetter.add(rootScope, 'scheduled_delete', (e) => { + const peerId = e.detail.peerId; + + if(this.chat.peerId !== peerId) { + return; + } + + this.appMessagesManager.getScheduledMessages(this.chat.peerId).then(value => { + this.btnScheduled.classList.toggle('hide', !value.length); + }); + }); + } this.attachMenuButtons = [{ icon: 'photo', @@ -187,7 +219,7 @@ export default class ChatInput { icon: 'poll', text: 'Poll', onClick: () => { - new PopupCreatePoll(this.chat.peerId).show(); + new PopupCreatePoll(this.chat).show(); }, verify: (peerId: number) => peerId < 0 && this.appChatsManager.hasRights(peerId, 'send', 'send_polls') }]; @@ -196,24 +228,6 @@ export default class ChatInput { this.attachMenu.classList.add('attach-file', 'tgico-attach'); this.attachMenu.classList.remove('tgico-more'); - this.sendMenuButtons = [{ - icon: 'mute', - text: 'Send Without Sound', - onClick: () => { - - }, - verify: (peerId: number) => true - }, { - icon: 'schedule', - text: 'Schedule Message', - onClick: () => { - - }, - verify: (peerId: number) => true - }]; - - this.sendMenu = ButtonMenu(this.sendMenuButtons, this.listenerSetter); - this.sendMenu.classList.add('menu-send', 'top-left'); //this.inputContainer.append(this.sendMenu); this.recordTimeEl = document.createElement('div'); @@ -224,7 +238,7 @@ export default class ChatInput { this.fileInput.multiple = true; this.fileInput.style.display = 'none'; - this.newMessageWrapper.append(this.btnToggleEmoticons, this.inputMessageContainer, this.btnScheduled, this.attachMenu, this.recordTimeEl, this.fileInput); + this.newMessageWrapper.append(...[this.btnToggleEmoticons, this.inputMessageContainer, this.btnScheduled, this.attachMenu, this.recordTimeEl, this.fileInput].filter(Boolean)); this.rowsWrapper.append(this.replyElements.container, this.newMessageWrapper); @@ -239,19 +253,32 @@ export default class ChatInput { this.btnSend = ButtonIcon('none btn-circle z-depth-1 btn-send'); this.btnSend.insertAdjacentHTML('afterbegin', ` + + `); - attachContextMenuListener(this.btnSend, (e: any) => { - if(this.isInputEmpty()) { - return; - } + this.btnSendContainer.append(this.recordRippleEl, this.btnSend); + + if(this.chat.type !== 'scheduled') { + this.sendMenu = new SendMenu({ + onSilentClick: () => { + this.sendSilent = true; + this.sendMessage(); + }, + onScheduleClick: () => { + this.scheduleSending(undefined); + }, + listenerSetter: this.listenerSetter, + openSide: 'top-left', + onContextElement: this.btnSend, + onOpen: () => { + return !this.isInputEmpty(); + } + }); - cancelEvent(e); - openBtnMenu(this.sendMenu); - }, this.listenerSetter); - - this.btnSendContainer.append(this.recordRippleEl, this.btnSend, this.sendMenu); + this.btnSendContainer.append(this.sendMenu.sendMenu); + } this.inputContainer.append(this.btnCancelRecord, this.btnSendContainer); @@ -294,7 +321,7 @@ export default class ChatInput { return; } - new PopupNewMedia(Array.from(files).slice(), this.willAttachType); + new PopupNewMedia(this.chat, Array.from(files).slice(), this.willAttachType); this.fileInput.value = ''; }, false); @@ -314,11 +341,7 @@ export default class ChatInput { cancelEvent(e); console.log(eventName + ', time: ' + (Date.now() - time)); }); */ - attachClickEvent(this.btnSend, this.onBtnSendClick, {listenerSetter: this.listenerSetter}); - - attachClickEvent(this.btnScheduled, (e) => { - this.appImManager.setInnerPeer(this.chat.peerId, 0, 'scheduled'); - }, {listenerSetter: this.listenerSetter}); + attachClickEvent(this.btnSend, this.onBtnSendClick, {listenerSetter: this.listenerSetter, touchMouseDown: true}); if(this.recorder) { const onCancelRecordClick = (e: Event) => { @@ -414,6 +437,22 @@ export default class ChatInput { this.btnToggleEmoticons.classList.toggle(toggleClass, false); }; + public scheduleSending = (callback: () => void = this.sendMessage.bind(this, true), initDate = new Date()) => { + new PopupSchedule(initDate, (timestamp) => { + const minTimestamp = (Date.now() / 1000 | 0) + 10; + if(timestamp <= minTimestamp) { + timestamp = undefined; + } + + this.scheduleDate = timestamp; + callback(); + + if(this.chat.type !== 'scheduled' && timestamp) { + this.appImManager.openScheduled(this.chat.peerId); + } + }).show(); + }; + public setUnreadCount() { const dialog = this.appMessagesManager.getDialogByPeerId(this.chat.peerId)[0]; const count = dialog?.unread_count; @@ -457,9 +496,12 @@ export default class ChatInput { this.setUnreadCount(); } - if(this.chat.type == 'pinned') { + if(this.chat.type === 'pinned') { this.chatInput.classList.toggle('can-pin', this.appPeersManager.canPinMessage(peerId)); - } else if(this.chat.type == 'chat') { + }/* else if(this.chat.type === 'chat') { + } */ + + if(this.btnScheduled) { this.btnScheduled.classList.add('hide'); const middleware = this.chat.bubbles.getMiddleware(); this.appMessagesManager.getScheduledMessages(peerId).then(mids => { @@ -468,6 +510,10 @@ export default class ChatInput { }); } + if(this.sendMenu) { + this.sendMenu.setPeerId(peerId); + } + if(this.messageInput) { const canWrite = this.appMessagesManager.canWriteToPeer(peerId); this.chatInput.classList.add('no-transition'); @@ -495,20 +541,20 @@ export default class ChatInput { } private attachMessageInputField() { - const messageInputField = InputField({ + this.messageInputField = new InputField({ placeholder: 'Message', name: 'message' }); - messageInputField.input.className = 'input-message-input'; - this.messageInput = messageInputField.input; + this.messageInputField.input.className = 'input-message-input'; + this.messageInput = this.messageInputField.input; this.attachMessageInputListeners(); const container = this.inputScroll.container; if(container.firstElementChild) { - container.replaceChild(messageInputField.input, container.firstElementChild); + container.replaceChild(this.messageInputField.input, container.firstElementChild); } else { - container.append(messageInputField.input); + container.append(this.messageInputField.input); } } @@ -752,7 +798,7 @@ export default class ChatInput { //console.log('messageInput input', this.messageInput.innerText, this.serializeNodes(Array.from(this.messageInput.childNodes))); //const value = this.messageInput.innerText; - const richValue = getRichValue(this.messageInput); + const richValue = this.messageInputField.value; //const entities = RichTextProcessor.parseEntities(value); const markdownEntities: MessageEntity[] = []; @@ -846,8 +892,8 @@ export default class ChatInput { private onBtnSendClick = (e: Event) => { cancelEvent(e); - - if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) { + + if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length || this.editMsgId) { if(this.recording) { if((Date.now() - this.recordStartTime) < RECORD_MIN_TIME) { this.btnCancelRecord.click(); @@ -1011,21 +1057,28 @@ export default class ChatInput { } public updateSendBtn() { - let icon: 'send' | 'record'; + let icon: 'send' | 'record' | 'edit' | 'schedule'; - if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length || this.editMsgId) icon = 'send'; + if(this.editMsgId) icon = 'edit'; + else if(!this.recorder || this.recording || !this.isInputEmpty() || this.forwardingMids.length) icon = this.chat.type === 'scheduled' ? 'schedule' : 'send'; else icon = 'record'; - this.btnSend.classList.toggle('send', icon == 'send'); - this.btnSend.classList.toggle('record', icon == 'record'); + ['send', 'record', 'edit', 'schedule'].forEach(i => { + this.btnSend.classList.toggle(i, icon === i); + }); } public onMessageSent(clearInput = true, clearReply?: boolean) { - let dialog = this.appMessagesManager.getDialogByPeerId(this.chat.peerId)[0]; - if(dialog && dialog.top_message) { - this.appMessagesManager.readHistory(this.chat.peerId, dialog.top_message); // lol + if(this.chat.type !== 'scheduled') { + let dialog = this.appMessagesManager.getDialogByPeerId(this.chat.peerId)[0]; + if(dialog && dialog.top_message) { + this.appMessagesManager.readHistory(this.chat.peerId, dialog.top_message); // lol + } } + this.scheduleDate = undefined; + this.sendSilent = undefined; + if(clearInput) { this.lastUrl = ''; delete this.noWebPage; @@ -1040,23 +1093,30 @@ export default class ChatInput { this.updateSendBtn(); } - public sendMessage() { + public sendMessage(force = false) { + if(this.chat.type === 'scheduled' && !force && !this.editMsgId) { + this.scheduleSending(); + return; + } + //let str = this.serializeNodes(Array.from(this.messageInput.childNodes)); - let str = getRichValue(this.messageInput); + let str = this.messageInputField.value; //console.log('childnode str after:', str/* , getRichValue(this.messageInput) */); //return; if(this.editMsgId) { - this.appMessagesManager.editMessage(this.chat.peerId, this.editMsgId, str, { + this.appMessagesManager.editMessage(this.chat.getMessage(this.editMsgId), str, { noWebPage: this.noWebPage }); } else { this.appMessagesManager.sendText(this.chat.peerId, str, { - replyToMsgId: this.replyToMsgId || this.replyToMsgId, + replyToMsgId: this.replyToMsgId, noWebPage: this.noWebPage, - webPage: this.willSendWebPage + webPage: this.willSendWebPage, + scheduleDate: this.scheduleDate, + silent: this.sendSilent }); } @@ -1065,18 +1125,40 @@ export default class ChatInput { const mids = this.forwardingMids.slice(); const fromPeerId = this.forwardingFromPeerId; const peerId = this.chat.peerId; + const silent = this.sendSilent; + const scheduleDate = this.scheduleDate; setTimeout(() => { - this.appMessagesManager.forwardMessages(peerId, fromPeerId, mids); + this.appMessagesManager.forwardMessages(peerId, fromPeerId, mids, { + silent, + scheduleDate: scheduleDate + }); }, 0); } this.onMessageSent(); } - public sendMessageWithDocument(document: any) { + public sendMessageWithDocument(document: MyDocument | string, force = false) { document = this.appDocsManager.getDoc(document); - if(document && document._ != 'documentEmpty') { - this.appMessagesManager.sendFile(this.chat.peerId, document, {isMedia: true, replyToMsgId: this.replyToMsgId}); + + const flag = document.type === 'sticker' ? 'send_stickers' : (document.type === 'gif' ? 'send_gifs' : 'send_media'); + if(this.chat.peerId < 0 && !this.appChatsManager.hasRights(this.chat.peerId, 'send', flag)) { + toast(POSTING_MEDIA_NOT_ALLOWED); + return; + } + + if(this.chat.type === 'scheduled' && !force) { + this.scheduleSending(() => this.sendMessageWithDocument(document, true)); + return false; + } + + if(document) { + this.appMessagesManager.sendFile(this.chat.peerId, document, { + isMedia: true, + replyToMsgId: this.replyToMsgId, + silent: this.sendSilent, + scheduleDate: this.scheduleDate + }); this.onMessageSent(false, true); if(document.type == 'sticker') { @@ -1089,6 +1171,18 @@ export default class ChatInput { return false; } + /* public sendSomething(callback: () => void, force = false) { + if(this.chat.type === 'scheduled' && !force) { + this.scheduleSending(() => this.sendSomething(callback, true)); + return false; + } + + callback(); + this.onMessageSent(false, true); + + return true; + } */ + public initMessageEditing(mid: number) { const message = this.chat.getMessage(mid); @@ -1146,10 +1240,10 @@ export default class ChatInput { this.willSendWebPage = null; } - this.replyToMsgId = 0; + this.replyToMsgId = undefined; this.forwardingMids.length = 0; this.forwardingFromPeerId = 0; - this.editMsgId = 0; + this.editMsgId = undefined; this.helperType = this.helperFunc = undefined; this.chat.container.classList.remove('is-helper-active'); } diff --git a/src/components/chat/pinnedContainer.ts b/src/components/chat/pinnedContainer.ts index a2c1f9bd..5714df51 100644 --- a/src/components/chat/pinnedContainer.ts +++ b/src/components/chat/pinnedContainer.ts @@ -80,6 +80,7 @@ export default class PinnedContainer { } public fill(title: string, subtitle: string, message: any) { + this.divAndCaption.container.dataset.peerId = '' + message.peerId; this.divAndCaption.container.dataset.mid = '' + message.mid; this.divAndCaption.fill(title, subtitle, message); this.topbar.setUtilsWidth(); diff --git a/src/components/chat/pinnedMessage.ts b/src/components/chat/pinnedMessage.ts index fc04c0eb..6c8d3f4d 100644 --- a/src/components/chat/pinnedMessage.ts +++ b/src/components/chat/pinnedMessage.ts @@ -2,7 +2,7 @@ import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManage import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type ChatTopbar from "./topbar"; import { ScreenSize } from "../../helpers/mediaSizes"; -import PopupPinMessage from "../popupUnpinMessage"; +import PopupPinMessage from "../popups/unpinMessage"; import PinnedContainer from "./pinnedContainer"; import PinnedMessageBorder from "./pinnedMessageBorder"; import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer"; @@ -429,7 +429,7 @@ export default class ChatPinnedMessage { const result = (await Promise.all(promises))[0]; - let backLimited = result.history.findIndex(_mid => _mid <= mid); + let backLimited = result.history.findIndex(message => message.mid <= mid); if(backLimited === -1) { backLimited = result.history.length; }/* else { @@ -437,7 +437,7 @@ export default class ChatPinnedMessage { } */ this.offsetIndex = result.offset_id_offset ? result.offset_id_offset - backLimited : 0; - this.mids = result.history.slice(); + this.mids = result.history.map(message => message.mid).slice(); this.count = result.count; if(!this.count) { diff --git a/src/components/chat/search.ts b/src/components/chat/search.ts index 1456e11e..afc5ba85 100644 --- a/src/components/chat/search.ts +++ b/src/components/chat/search.ts @@ -1,7 +1,7 @@ import type ChatTopbar from "./topbar"; import { cancelEvent, whichChild, findUpTag } from "../../helpers/dom"; import AppSearch, { SearchGroup } from "../appSearch"; -import PopupDatePicker from "../popupDatepicker"; +import PopupDatePicker from "../popups/datePicker"; import { ripple } from "../ripple"; import InputSearch from "../inputSearch"; import type Chat from "./chat"; diff --git a/src/components/chat/selection.ts b/src/components/chat/selection.ts index d5fb908e..1b17386d 100644 --- a/src/components/chat/selection.ts +++ b/src/components/chat/selection.ts @@ -1,16 +1,18 @@ import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type ChatBubbles from "./bubbles"; import type ChatInput from "./input"; +import type Chat from "./chat"; import { isTouchSupported } from "../../helpers/touchSupport"; import { blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom"; import Button from "../button"; import ButtonIcon from "../buttonIcon"; import CheckboxField from "../checkbox"; -import PopupDeleteMessages from "../popupDeleteMessages"; -import PopupForward from "../popupForward"; +import PopupDeleteMessages from "../popups/deleteMessages"; +import PopupForward from "../popups/forward"; import { toast } from "../toast"; import SetTransition from "../singleTransition"; import ListenerSetter from "../../helpers/listenerSetter"; +import PopupSendNow from "../popups/sendNow"; const MAX_SELECTION_LENGTH = 100; //const MIN_CLICK_MOVE = 32; // minimum bubble height @@ -21,6 +23,7 @@ export default class ChatSelection { private selectionContainer: HTMLElement; private selectionCountEl: HTMLElement; + public selectionSendNowBtn: HTMLElement; public selectionForwardBtn: HTMLElement; public selectionDeleteBtn: HTMLElement; @@ -28,7 +31,7 @@ export default class ChatSelection { private listenerSetter: ListenerSetter; - constructor(private bubbles: ChatBubbles, private input: ChatInput, private appMessagesManager: AppMessagesManager) { + constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, private appMessagesManager: AppMessagesManager) { const bubblesContainer = bubbles.bubblesContainer; this.listenerSetter = bubbles.listenerSetter; @@ -205,7 +208,7 @@ export default class ChatSelection { if(!this.selectedMids.size && !forceSelection) return; this.selectionCountEl.innerText = this.selectedMids.size + ' Message' + (this.selectedMids.size == 1 ? '' : 's'); - let cantForward = !this.selectedMids.size, cantDelete = !this.selectedMids.size; + let cantForward = !this.selectedMids.size, cantDelete = !this.selectedMids.size, cantSend = !this.selectedMids.size; for(const mid of this.selectedMids.values()) { const message = this.appMessagesManager.getMessageByPeer(this.bubbles.peerId, mid); if(!cantForward) { @@ -216,7 +219,7 @@ export default class ChatSelection { if(!cantDelete) { - const canDelete = this.appMessagesManager.canDeleteMessage(this.bubbles.peerId, mid); + const canDelete = this.appMessagesManager.canDeleteMessage(this.chat.getMessage(mid)); if(!canDelete) { cantDelete = true; } @@ -225,7 +228,8 @@ export default class ChatSelection { if(cantForward && cantDelete) break; } - this.selectionForwardBtn.toggleAttribute('disabled', cantForward); + this.selectionSendNowBtn && this.selectionSendNowBtn.toggleAttribute('disabled', cantSend); + this.selectionForwardBtn && this.selectionForwardBtn.toggleAttribute('disabled', cantForward); this.selectionDeleteBtn.toggleAttribute('disabled', cantDelete); } @@ -270,7 +274,7 @@ export default class ChatSelection { SetTransition(bubblesContainer, 'is-selecting', forwards, 200, () => { if(!this.isSelecting) { this.selectionContainer.remove(); - this.selectionContainer = this.selectionForwardBtn = this.selectionDeleteBtn = null; + this.selectionContainer = this.selectionSendNowBtn = this.selectionForwardBtn = this.selectionDeleteBtn = null; this.selectedText = undefined; } @@ -292,23 +296,35 @@ export default class ChatSelection { this.selectionCountEl = document.createElement('div'); this.selectionCountEl.classList.add('selection-container-count'); - this.selectionForwardBtn = Button('btn-primary btn-transparent selection-container-forward', {icon: 'forward'}); - this.selectionForwardBtn.append('Forward'); - this.listenerSetter.add(this.selectionForwardBtn, 'click', () => { - new PopupForward(this.bubbles.peerId, [...this.selectedMids], () => { - this.cancelSelection(); + if(this.chat.type === 'scheduled') { + this.selectionSendNowBtn = Button('btn-primary btn-transparent selection-container-send', {icon: 'send2'}); + this.selectionSendNowBtn.append('Send Now'); + this.listenerSetter.add(this.selectionSendNowBtn, 'click', () => { + new PopupSendNow(this.bubbles.peerId, [...this.selectedMids], () => { + this.cancelSelection(); + }) }); - }); + } + + if(this.chat.type === 'chat' || this.chat.type === 'pinned') { + this.selectionForwardBtn = Button('btn-primary btn-transparent selection-container-forward', {icon: 'forward'}); + this.selectionForwardBtn.append('Forward'); + this.listenerSetter.add(this.selectionForwardBtn, 'click', () => { + new PopupForward(this.bubbles.peerId, [...this.selectedMids], () => { + this.cancelSelection(); + }); + }); + } this.selectionDeleteBtn = Button('btn-primary btn-transparent danger selection-container-delete', {icon: 'delete'}); this.selectionDeleteBtn.append('Delete'); this.listenerSetter.add(this.selectionDeleteBtn, 'click', () => { - new PopupDeleteMessages(this.bubbles.peerId, [...this.selectedMids], () => { + new PopupDeleteMessages(this.bubbles.peerId, [...this.selectedMids], this.chat.type, () => { this.cancelSelection(); }); }); - this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn); + this.selectionContainer.append(...[btnCancel, this.selectionCountEl, this.selectionSendNowBtn, this.selectionForwardBtn, this.selectionDeleteBtn].filter(Boolean)); this.input.rowsWrapper.append(this.selectionContainer); } @@ -365,7 +381,7 @@ export default class ChatSelection { } public isGroupedMidsSelected(mid: number) { - const mids = this.appMessagesManager.getMidsByMid(this.bubbles.peerId, mid); + const mids = this.chat.getMidsByMid(mid); const selectedMids = mids.filter(mid => this.selectedMids.has(mid)); return mids.length == selectedMids.length; } @@ -376,7 +392,7 @@ export default class ChatSelection { const isGrouped = bubble.classList.contains('is-grouped'); if(isGrouped) { if(!this.isGroupedBubbleSelected(bubble)) { - const mids = this.appMessagesManager.getMidsByMid(this.bubbles.peerId, mid); + const mids = this.chat.getMidsByMid(mid); mids.forEach(mid => this.selectedMids.delete(mid)); } diff --git a/src/components/chat/sendContextMenu.ts b/src/components/chat/sendContextMenu.ts new file mode 100644 index 00000000..4bea16ed --- /dev/null +++ b/src/components/chat/sendContextMenu.ts @@ -0,0 +1,57 @@ +import { cancelEvent } from "../../helpers/dom"; +import ListenerSetter from "../../helpers/listenerSetter"; +import rootScope from "../../lib/rootScope"; +import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; +import { attachContextMenuListener, openBtnMenu } from "../misc"; + +export default class SendMenu { + public sendMenu: HTMLDivElement; + private sendMenuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[]; + private type: 'schedule' | 'reminder'; + + constructor(options: { + onSilentClick: () => void, + onScheduleClick: () => void, + listenerSetter?: ListenerSetter, + openSide: string, + onContextElement: HTMLElement, + onOpen?: () => boolean + }) { + this.sendMenuButtons = [{ + icon: 'mute', + text: 'Send Without Sound', + onClick: options.onSilentClick, + verify: () => this.type === 'schedule' + }, { + icon: 'schedule', + text: 'Schedule Message', + onClick: options.onScheduleClick, + verify: () => this.type === 'schedule' + }, { + icon: 'schedule', + text: 'Set a reminder', + onClick: options.onScheduleClick, + verify: () => this.type === 'reminder' + }]; + + this.sendMenu = ButtonMenu(this.sendMenuButtons, options.listenerSetter); + this.sendMenu.classList.add('menu-send', options.openSide); + + attachContextMenuListener(options.onContextElement, (e: any) => { + if(options.onOpen && !options.onOpen()) { + return; + } + + this.sendMenuButtons.forEach(button => { + button.element.classList.toggle('hide', !button.verify()); + }); + + cancelEvent(e); + openBtnMenu(this.sendMenu); + }, options.listenerSetter); + } + + public setPeerId(peerId: number) { + this.type = peerId == rootScope.myId ? 'reminder' : 'schedule'; + } +}; \ No newline at end of file diff --git a/src/components/chat/topbar.ts b/src/components/chat/topbar.ts index dc1b6d53..5b0f7fd9 100644 --- a/src/components/chat/topbar.ts +++ b/src/components/chat/topbar.ts @@ -115,19 +115,18 @@ export default class ChatTopbar { mediaSizes.addListener('changeScreen', this.onChangeScreen); this.listenerSetter.add(this.container, 'click', (e) => { - const pinned: HTMLElement = findUpClassName(e.target, 'pinned-container'); - if(pinned) { + const container: HTMLElement = findUpClassName(e.target, 'pinned-container'); + if(container) { cancelEvent(e); - const mid = +pinned.dataset.mid; - if(pinned.classList.contains('pinned-message')) { + const mid = +container.dataset.mid; + const peerId = +container.dataset.peerId; + if(container.classList.contains('pinned-message')) { //if(!this.pinnedMessage.locked) { this.pinnedMessage.followPinnedMessage(mid); //} } else { - const message = this.appMessagesManager.getMessageByPeer(this.peerId, mid); - - this.chat.appImManager.setInnerPeer(message.peerId, mid); + this.chat.appImManager.setInnerPeer(peerId, mid); } } else { this.appSidebarRight.toggleSidebar(true); @@ -374,7 +373,8 @@ export default class ChatTopbar { public setTitle(count?: number) { let title = ''; if(this.chat.type === 'pinned') { - title = count === -1 ? 'Pinned Messages' : (count === 1 ? 'Pinned Message' : (count + ' Pinned Messages')); + //title = !count ? 'Pinned Messages' : (count === 1 ? 'Pinned Message' : (count + ' Pinned Messages')); + title = [count > 1 ? count : false, 'Pinned Messages'].filter(Boolean).join(' '); if(count === undefined) { this.appMessagesManager.getSearchCounters(this.peerId, [{_: 'inputMessagesFilterPinned'}]).then(result => { @@ -394,7 +394,13 @@ export default class ChatTopbar { }); } } else if(this.chat.type === 'scheduled') { - title = count === -1 ? 'Scheduled Messages' : (count === 1 ? 'Scheduled Message' : (count + ' Scheduled Messages')); + if(this.peerId === rootScope.myId) { + //title = !count ? 'Reminders' : (count === 1 ? 'Reminder' : (count + ' Reminders')); + title = [count > 1 ? count : false, 'Reminders'].filter(Boolean).join(' '); + } else { + //title = !count ? 'Scheduled Messages' : (count === 1 ? 'Scheduled Message' : (count + ' Scheduled Messages')); + title = [count > 1 ? count : false, 'Scheduled Messages'].filter(Boolean).join(' '); + } if(count === undefined) { this.appMessagesManager.getScheduledMessages(this.peerId).then(mids => { diff --git a/src/components/dialogsContextMenu.ts b/src/components/dialogsContextMenu.ts index f9b543cd..fb2bf5fe 100644 --- a/src/components/dialogsContextMenu.ts +++ b/src/components/dialogsContextMenu.ts @@ -5,8 +5,8 @@ import appPeersManager from "../lib/appManagers/appPeersManager"; import rootScope from "../lib/rootScope"; import { findUpTag } from "../helpers/dom"; import { positionMenu, openBtnMenu } from "./misc"; -import { PopupButton } from "./popup"; -import PopupPeer from "./popupPeer"; +import { PopupButton } from "./popups"; +import PopupPeer from "./popups/peer"; import ButtonMenu, { ButtonMenuItemOptions } from "./buttonMenu"; export default class DialogsContextMenu { diff --git a/src/components/inputField.ts b/src/components/inputField.ts index 3fde922f..c8c0000b 100644 --- a/src/components/inputField.ts +++ b/src/components/inputField.ts @@ -50,84 +50,106 @@ const checkAndSetRTL = (input: HTMLElement) => { input.style.direction = direction; }; -const InputField = (options: { - placeholder?: string, - label?: string, - name?: string, - maxLength?: number, - showLengthOn?: number, - plainText?: true -}) => { - const div = document.createElement('div'); - div.classList.add('input-field'); - - if(options.maxLength) { - options.showLengthOn = Math.round(options.maxLength / 3); - } +class InputField { + public container: HTMLElement; + public input: HTMLElement; + + constructor(private options: { + placeholder?: string, + label?: string, + name?: string, + maxLength?: number, + showLengthOn?: number, + plainText?: true + } = {}) { + this.container = document.createElement('div'); + this.container.classList.add('input-field'); + + if(options.maxLength) { + options.showLengthOn = Math.round(options.maxLength / 3); + } + + const {placeholder, label, maxLength, showLengthOn, name, plainText} = options; - const {placeholder, label, maxLength, showLengthOn, name, plainText} = options; + let input: HTMLElement; + if(!plainText) { + if(init) { + init(); + } - let input: HTMLElement; - if(!plainText) { - if(init) { - init(); + this.container.innerHTML = ` +
+ ${label ? `` : ''} + `; + + input = this.container.firstElementChild as HTMLElement; + const observer = new MutationObserver(() => { + checkAndSetRTL(input); + + if(processInput) { + processInput(); + } + }); + + // ! childList for paste first symbol + observer.observe(input, {characterData: true, childList: true, subtree: true}); + } else { + this.container.innerHTML = ` + + ${label ? `` : ''} + `; + + input = this.container.firstElementChild as HTMLElement; + input.addEventListener('input', () => checkAndSetRTL(input)); } - div.innerHTML = ` -
- ${label ? `` : ''} - `; + let processInput: () => void; + if(maxLength) { + const labelEl = this.container.lastElementChild as HTMLLabelElement; + let showingLength = false; + + processInput = () => { + const wasError = input.classList.contains('error'); + // * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol + const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input)].length; + const diff = maxLength - inputLength; + const isError = diff < 0; + input.classList.toggle('error', isError); + + if(isError || diff <= showLengthOn) { + labelEl.innerText = label + ` (${maxLength - inputLength})`; + if(!showingLength) showingLength = true; + } else if((wasError && !isError) || showingLength) { + labelEl.innerText = label; + showingLength = false; + } + }; + + input.addEventListener('input', processInput); + } - input = div.firstElementChild as HTMLElement; - const observer = new MutationObserver(() => { - checkAndSetRTL(input); + this.input = input; + } - if(processInput) { - processInput(); - } - }); - - // ! childList for paste first symbol - observer.observe(input, {characterData: true, childList: true, subtree: true}); - } else { - div.innerHTML = ` - - ${label ? `` : ''} - `; - - input = div.firstElementChild as HTMLElement; - input.addEventListener('input', () => checkAndSetRTL(input)); + get value() { + return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input); + //return getRichValue(this.input); } - let processInput: () => void; - if(maxLength) { - const labelEl = div.lastElementChild as HTMLLabelElement; - let showingLength = false; - - processInput = () => { - const wasError = input.classList.contains('error'); - // * https://stackoverflow.com/a/54369605 #2 to count emoji as 1 symbol - const inputLength = plainText ? (input as HTMLInputElement).value.length : [...getRichValue(input)].length; - const diff = maxLength - inputLength; - const isError = diff < 0; - input.classList.toggle('error', isError); - - if(isError || diff <= showLengthOn) { - labelEl.innerText = label + ` (${maxLength - inputLength})`; - if(!showingLength) showingLength = true; - } else if((wasError && !isError) || showingLength) { - labelEl.innerText = label; - showingLength = false; - } - }; + set value(value: string) { + this.setValueSilently(value); - input.addEventListener('input', processInput); + const event = new Event('input', {bubbles: true, cancelable: true}); + this.input.dispatchEvent(event); } - return { - container: div, - input: div.firstElementChild as HTMLInputElement - }; -}; + public setValueSilently(value: string) { + if(this.options.plainText) { + (this.input as HTMLInputElement).value = value; + } else { + this.input.innerHTML = value; + } + } +} export default InputField; \ No newline at end of file diff --git a/src/components/inputSearch.ts b/src/components/inputSearch.ts index 2cedc8ff..eb3b60f1 100644 --- a/src/components/inputSearch.ts +++ b/src/components/inputSearch.ts @@ -3,7 +3,8 @@ import InputField from "./inputField"; export default class InputSearch { public container: HTMLElement; - public input: HTMLInputElement; + public input: HTMLElement; + public inputField: InputField; public clearBtn: HTMLElement; public prevValue = ''; @@ -11,18 +12,18 @@ export default class InputSearch { public onChange: (value: string) => void; constructor(placeholder: string, onChange?: (value: string) => void) { - const inputField = InputField({ + this.inputField = new InputField({ placeholder, plainText: true }); - this.container = inputField.container; + this.container = this.inputField.container; this.container.classList.remove('input-field'); this.container.classList.add('input-search'); this.onChange = onChange; - this.input = inputField.input; + this.input = this.inputField.input; this.input.classList.add('input-search-input'); const searchIcon = document.createElement('span'); @@ -59,18 +60,13 @@ export default class InputSearch { }; get value() { - return this.input.value; - //return getRichValue(this.input); + return this.inputField.value; } set value(value: string) { - //this.input.innerHTML = value; - this.input.value = value; this.prevValue = value; clearTimeout(this.timeout); - - const event = new Event('input', {bubbles: true, cancelable: true}); - this.input.dispatchEvent(event); + this.inputField.value = value; } public remove() { diff --git a/src/components/poll.ts b/src/components/poll.ts index 456da9f9..cc8f97be 100644 --- a/src/components/poll.ts +++ b/src/components/poll.ts @@ -64,7 +64,7 @@ export const roundPercents = (percents: number[]) => { //console.log('roundPercents after percents:', percents); }; -const connectedPolls: {id: string, element: PollElement}[] = []; +/* const connectedPolls: {id: string, element: PollElement}[] = []; rootScope.on('poll_update', (e) => { const {poll, results} = e.detail as {poll: Poll, results: PollResults}; @@ -76,6 +76,17 @@ rootScope.on('poll_update', (e) => { pollElement.performResults(results, poll.chosenIndexes); } } +}); */ + +rootScope.on('poll_update', (e) => { + const {poll, results} = e.detail as {poll: Poll, results: PollResults}; + + const pollElement = document.querySelector(`poll-element[poll-id="${poll.id}"]`) as PollElement; + //console.log('poll_update', poll, results); + if(pollElement) { + pollElement.isClosed = !!poll.pFlags.closed; + pollElement.performResults(results, poll.chosenIndexes); + } }); rootScope.on('peer_changed', () => { @@ -152,9 +163,7 @@ export default class PollElement extends HTMLElement { private chosenIndexes: number[] = []; private percents: number[]; - private peerId: number; - private pollId: string; - private mid: number; + public message: any; private quizInterval: number; private quizTimer: SVGSVGElement; @@ -179,12 +188,14 @@ export default class PollElement extends HTMLElement { //console.log('line total length:', lineTotalLength); } - this.peerId = +this.getAttribute('peer-id'); - this.pollId = this.getAttribute('poll-id'); - this.mid = +this.getAttribute('message-id'); - const {poll, results} = appPollsManager.getPoll(this.pollId); + const pollId = this.message.media.poll.id; + const {poll, results} = appPollsManager.getPoll(pollId); - connectedPolls.push({id: this.pollId, element: this}); + /* 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); @@ -309,7 +320,7 @@ export default class PollElement extends HTMLElement { setTimeout(() => { // нужно запросить апдейт чтобы опрос обновился - appPollsManager.getResults(this.peerId, this.mid); + appPollsManager.getResults(this.message); }, 3e3); } }, 1e3); @@ -326,7 +337,7 @@ export default class PollElement extends HTMLElement { this.viewResults.addEventListener('click', (e) => { cancelEvent(e); - appSidebarRight.pollResultsTab.init(this.peerId, this.pollId, this.mid); + appSidebarRight.pollResultsTab.init(this.message); }); ripple(this.viewResults); @@ -370,32 +381,6 @@ export default class PollElement extends HTMLElement { } } - disconnectedCallback() { - // браузер вызывает этот метод при удалении элемента из документа - // (может вызываться много раз, если элемент многократно добавляется/удаляется) - - connectedPolls.findAndSplice(c => c.element == this); - } - - static get observedAttributes(): string[] { - return ['poll-id', 'message-id'/* массив имён атрибутов для отслеживания их изменений */]; - } - - attributeChangedCallback(name: string, oldValue: string, newValue: string) { - // вызывается при изменении одного из перечисленных выше атрибутов - // console.log('Poll: attributeChangedCallback', name, oldValue, newValue, this.isConnected); - if(name == 'poll-id') { - this.pollId = newValue; - } else if(name == 'message-id') { - this.mid = +newValue; - } - - if(this.mid > 0 && oldValue !== undefined && +oldValue < 0) { - this.disconnectedCallback(); - connectedPolls.push({id: this.pollId, element: this}); - } - } - adoptedCallback() { // вызывается, когда элемент перемещается в новый документ // (происходит в document.adoptNode, используется очень редко) @@ -466,7 +451,7 @@ export default class PollElement extends HTMLElement { this.classList.add('disable-hover'); this.sentVote = true; - return this.sendVotePromise = appPollsManager.sendVote(this.peerId, this.mid, indexes).then(() => { + return this.sendVotePromise = appPollsManager.sendVote(this.message, indexes).then(() => { targets.forEach(target => { target.classList.remove('is-voting'); }); diff --git a/src/components/popupAvatar.ts b/src/components/popups/avatar.ts similarity index 91% rename from src/components/popupAvatar.ts rename to src/components/popups/avatar.ts index 77c00093..fd66c827 100644 --- a/src/components/popupAvatar.ts +++ b/src/components/popups/avatar.ts @@ -1,7 +1,7 @@ -import appDownloadManager from "../lib/appManagers/appDownloadManager"; -import resizeableImage from "../lib/cropper"; -import { PopupElement } from "./popup"; -import { ripple } from "./ripple"; +import appDownloadManager from "../../lib/appManagers/appDownloadManager"; +import resizeableImage from "../../lib/cropper"; +import PopupElement from "."; +import { ripple } from "../ripple"; export default class PopupAvatar extends PopupElement { private cropContainer: HTMLElement; @@ -26,7 +26,7 @@ export default class PopupAvatar extends PopupElement { this.h6 = document.createElement('h6'); this.h6.innerText = 'Drag to Reposition'; - this.closeBtn.classList.remove('btn-icon'); + this.btnClose.classList.remove('btn-icon'); this.header.append(this.h6); @@ -70,7 +70,7 @@ export default class PopupAvatar extends PopupElement { ripple(this.btnSubmit); this.btnSubmit.addEventListener('click', () => { this.cropper.crop(); - this.closeBtn.click(); + this.btnClose.click(); this.canvas.toBlob(blob => { this.blob = blob; // save blob to send after reg diff --git a/src/components/popupCreatePoll.ts b/src/components/popups/createPoll.ts similarity index 76% rename from src/components/popupCreatePoll.ts rename to src/components/popups/createPoll.ts index 1e94a55f..8f41ab62 100644 --- a/src/components/popupCreatePoll.ts +++ b/src/components/popups/createPoll.ts @@ -1,20 +1,20 @@ -import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import appPeersManager from "../lib/appManagers/appPeersManager"; -import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager"; -import { cancelEvent, findUpTag, getRichValue, isInputEmpty, whichChild } from "../helpers/dom"; -import CheckboxField from "./checkbox"; -import InputField from "./inputField"; -import { PopupElement } from "./popup"; -import RadioField from "./radioField"; -import Scrollable from "./scrollable"; -import { toast } from "./toast"; +import type { Poll } from "../../lib/appManagers/appPollsManager"; +import type Chat from "../chat/chat"; +import PopupElement from "."; +import { cancelEvent, findUpTag, getRichValue, isInputEmpty, whichChild } from "../../helpers/dom"; +import CheckboxField from "../checkbox"; +import InputField from "../inputField"; +import RadioField from "../radioField"; +import Scrollable from "../scrollable"; +import { toast } from "../toast"; +import SendContextMenu from "../chat/sendContextMenu"; const MAX_LENGTH_QUESTION = 255; const MAX_LENGTH_OPTION = 100; const MAX_LENGTH_SOLUTION = 200; export default class PopupCreatePoll extends PopupElement { - private questionInput: HTMLInputElement; + private questionInputField: InputField; private questions: HTMLElement; private scrollable: Scrollable; private tempId = 0; @@ -24,22 +24,41 @@ export default class PopupCreatePoll extends PopupElement { private quizCheckboxField: PopupCreatePoll['anonymousCheckboxField']; private correctAnswers: Uint8Array[]; - private quizSolutionInput: HTMLInputElement; + private quizSolutionField: InputField; - constructor(private peerId: number) { + constructor(private chat: Chat) { super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'CREATE', body: true}); this.title.innerText = 'New Poll'; - const questionField = InputField({ + this.questionInputField = new InputField({ placeholder: 'Ask a Question', label: 'Ask a Question', name: 'question', maxLength: MAX_LENGTH_QUESTION }); - this.questionInput = questionField.input; - this.header.append(questionField.container); + if(this.chat.type !== 'scheduled') { + const sendMenu = new SendContextMenu({ + onSilentClick: () => { + this.chat.input.sendSilent = true; + this.send(); + }, + onScheduleClick: () => { + this.chat.input.scheduleSending(() => { + this.send(); + }); + }, + openSide: 'bottom-left', + onContextElement: this.btnConfirm, + }); + + sendMenu.setPeerId(this.chat.peerId); + + this.header.append(sendMenu.sendMenu); + } + + this.header.append(this.questionInputField.container); const hr = document.createElement('hr'); const d = document.createElement('div'); @@ -56,7 +75,7 @@ export default class PopupCreatePoll extends PopupElement { settingsCaption.classList.add('caption'); settingsCaption.innerText = 'Settings'; - if(!appPeersManager.isBroadcast(peerId)) { + if(!this.chat.appPeersManager.isBroadcast(this.chat.peerId)) { this.anonymousCheckboxField = CheckboxField('Anonymous Voting', 'anonymous'); this.anonymousCheckboxField.input.checked = true; dd.append(this.anonymousCheckboxField.label); @@ -95,19 +114,18 @@ export default class PopupCreatePoll extends PopupElement { const quizSolutionContainer = document.createElement('div'); quizSolutionContainer.classList.add('poll-create-questions'); - const quizSolutionField = InputField({ + this.quizSolutionField = new InputField({ placeholder: 'Add a Comment (Optional)', label: 'Add a Comment (Optional)', name: 'solution', maxLength: MAX_LENGTH_SOLUTION }); - this.quizSolutionInput = quizSolutionField.input; const quizSolutionSubtitle = document.createElement('div'); quizSolutionSubtitle.classList.add('subtitle'); quizSolutionSubtitle.innerText = 'Users will see this comment after choosing a wrong answer, good for educational purposes.'; - quizSolutionContainer.append(quizSolutionField.container, quizSolutionSubtitle); + quizSolutionContainer.append(this.quizSolutionField.container, quizSolutionSubtitle); quizElements.push(quizHr, quizSolutionCaption, quizSolutionContainer); quizElements.forEach(el => el.classList.add('hide')); @@ -115,7 +133,7 @@ export default class PopupCreatePoll extends PopupElement { this.body.parentElement.insertBefore(hr, this.body); this.body.append(d, this.questions, document.createElement('hr'), settingsCaption, dd, ...quizElements); - this.confirmBtn.addEventListener('click', this.onSubmitClick); + this.btnConfirm.addEventListener('click', this.onSubmitClick); this.scrollable = new Scrollable(this.body); this.appendMoreField(); @@ -128,14 +146,18 @@ export default class PopupCreatePoll extends PopupElement { private getFilledAnswers() { const answers = Array.from(this.questions.children).map((el, idx) => { const input = el.querySelector('.input-field-input') as HTMLElement; - return getRichValue(input); + return input instanceof HTMLInputElement ? input.value : getRichValue(input); }).filter(v => !!v.trim()); return answers; } - onSubmitClick = (e: MouseEvent) => { - const question = getRichValue(this.questionInput); + private onSubmitClick = () => { + this.send(); + }; + + public send(force = false) { + const question = this.questionInputField.value; if(!question) { toast('Please enter a question.'); @@ -165,14 +187,22 @@ export default class PopupCreatePoll extends PopupElement { return; } - const quizSolution = getRichValue(this.quizSolutionInput) || undefined; + const quizSolution = this.quizSolutionField.value || undefined; if(quizSolution?.length > MAX_LENGTH_SOLUTION) { toast('Explanation is too long.'); return; } - this.closeBtn.click(); - this.confirmBtn.removeEventListener('click', this.onSubmitClick); + if(this.chat.type === 'scheduled' && !force) { + this.chat.input.scheduleSending(() => { + this.send(true); + }); + + return; + } + + this.btnClose.click(); + this.btnConfirm.removeEventListener('click', this.onSubmitClick); //const randomID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)]; //const randomIDS = bigint(randomID[0]).shiftLeft(32).add(bigint(randomID[1])).toString(); @@ -206,12 +236,17 @@ export default class PopupCreatePoll extends PopupElement { }; //poll.id = randomIDS; - const inputMediaPoll = appPollsManager.getInputMediaPoll(poll, this.correctAnswers, quizSolution); + const inputMediaPoll = this.chat.appPollsManager.getInputMediaPoll(poll, this.correctAnswers, quizSolution); //console.log('Will try to create poll:', inputMediaPoll); - appMessagesManager.sendOther(this.peerId, inputMediaPoll); - }; + this.chat.appMessagesManager.sendOther(this.chat.peerId, inputMediaPoll, { + scheduleDate: this.chat.input.scheduleDate, + silent: this.chat.input.sendSilent + }); + + this.chat.input.onMessageSent(false, false); + } onInput = (e: Event) => { const target = e.target as HTMLInputElement; @@ -243,7 +278,7 @@ export default class PopupCreatePoll extends PopupElement { private appendMoreField() { const tempId = this.tempId++; const idx = this.questions.childElementCount + 1; - const questionField = InputField({ + const questionField = new InputField({ placeholder: 'Add an Option', label: 'Option ' + idx, name: 'question-' + tempId, diff --git a/src/components/popupDatePicker.ts b/src/components/popups/datePicker.ts similarity index 51% rename from src/components/popupDatePicker.ts rename to src/components/popups/datePicker.ts index 59c7f4ee..c07bb196 100644 --- a/src/components/popupDatePicker.ts +++ b/src/components/popups/datePicker.ts @@ -1,24 +1,36 @@ -import { PopupElement } from "./popup"; +import PopupElement, { PopupOptions } from "."; +import { getFullDate, months } from "../../helpers/date"; +import InputField from "../inputField"; export default class PopupDatePicker extends PopupElement { - private controlsDiv: HTMLElement; - private monthTitle: HTMLElement; - private prevBtn: HTMLElement; - private nextBtn: HTMLElement; - - private monthsContainer: HTMLElement; - private month: HTMLElement; - - private minMonth: Date; - private maxMonth: Date; - private minDate = new Date('2013-08-01T00:00:00'); - private maxDate: Date; - private selectedDate: Date; - private selectedMonth: Date; - private selectedEl: HTMLElement; - - constructor(initDate: Date, public onPick: (timestamp: number) => void) { - super('popup-date-picker', [{ + protected controlsDiv: HTMLElement; + protected monthTitle: HTMLElement; + protected prevBtn: HTMLElement; + protected nextBtn: HTMLElement; + + protected monthsContainer: HTMLElement; + protected month: HTMLElement; + + protected minMonth: Date; + protected maxMonth: Date; + protected minDate: Date; + protected maxDate: Date; + protected selectedDate: Date; + protected selectedMonth: Date; + protected selectedEl: HTMLElement; + + protected timeDiv: HTMLDivElement; + protected hoursInputField: InputField; + protected minutesInputField: InputField; + + constructor(initDate: Date, public onPick: (timestamp: number) => void, protected options: Partial<{ + noButtons: true, + noTitle: true, + minDate: Date, + maxDate: Date + withTime: true + }> & PopupOptions = {}) { + super('popup-date-picker', options.noButtons ? [] : [{ text: 'CANCEL', isCancel: true }, { @@ -28,10 +40,9 @@ export default class PopupDatePicker extends PopupElement { this.onPick(this.selectedDate.getTime() / 1000 | 0); } } - }]); + }], {body: true, ...options}); - const popupBody = document.createElement('div'); - popupBody.classList.add('popup-body'); + this.minDate = options.minDate || new Date('2013-08-01T00:00:00'); // Controls this.controlsDiv = document.createElement('div'); @@ -55,8 +66,80 @@ export default class PopupDatePicker extends PopupElement { this.monthsContainer.classList.add('date-picker-months'); this.monthsContainer.addEventListener('click', this.onDateClick); - popupBody.append(this.controlsDiv, this.monthsContainer); - this.container.append(popupBody); + this.body.append(this.controlsDiv, this.monthsContainer); + + // Time inputs + if(options.withTime) { + this.timeDiv = document.createElement('div'); + this.timeDiv.classList.add('date-picker-time'); + + const delimiter = document.createElement('div'); + delimiter.classList.add('date-picker-time-delimiter'); + delimiter.append(':'); + + const handleTimeInput = (max: number, inputField: InputField, onInput: (length: number) => void, onOverflow?: (number: number) => void) => { + const maxString = '' + max; + inputField.input.addEventListener('input', (e) => { + let value = inputField.value.replace(/\D/g, ''); + if(value.length > 2) { + value = value.slice(0, 2); + } else { + if((value.length === 1 && +value[0] > +maxString[0]) || (value.length === 2 && +value > max)) { + if(value.length === 2 && onOverflow) { + onOverflow(+value[1]); + } + + value = '0' + value[0]; + } + } + + inputField.setValueSilently(value); + onInput(value.length); + }); + }; + + this.hoursInputField = new InputField({plainText: true}); + this.minutesInputField = new InputField({plainText: true}); + + handleTimeInput(23, this.hoursInputField, (length) => { + if(length === 2) { + this.minutesInputField.input.focus(); + } + + this.setTimeTitle(); + }, (number) => { + this.minutesInputField.value = (number + this.minutesInputField.value).slice(0, 2); + }); + handleTimeInput(59, this.minutesInputField, (length) => { + if(!length) { + this.hoursInputField.input.focus(); + } + + this.setTimeTitle(); + }); + + this.selectedDate = initDate; + + initDate.setMinutes(initDate.getMinutes() + 10); + + this.hoursInputField.setValueSilently(('0' + initDate.getHours()).slice(-2)); + this.minutesInputField.setValueSilently(('0' + initDate.getMinutes()).slice(-2)); + + initDate.setHours(0, 0, 0, 0); + + this.timeDiv.append(this.hoursInputField.container, delimiter, this.minutesInputField.container); + + this.btnConfirm.addEventListener('click', () => { + if(this.onPick) { + this.selectedDate.setHours(+this.hoursInputField.value || 0, +this.minutesInputField.value || 0, 0, 0); + this.onPick(this.selectedDate.getTime() / 1000 | 0); + } + + this.destroy(); + }, {once: true}); + + this.body.append(this.timeDiv); + } const popupCenterer = document.createElement('div'); popupCenterer.classList.add('popup-centerer'); @@ -68,7 +151,7 @@ export default class PopupDatePicker extends PopupElement { initDate.setHours(0, 0, 0, 0); this.selectedDate = initDate; - this.maxDate = new Date(); + this.maxDate = options.maxDate || new Date(); this.maxDate.setHours(0, 0, 0, 0); this.selectedMonth = new Date(this.selectedDate); @@ -78,6 +161,7 @@ export default class PopupDatePicker extends PopupElement { this.maxMonth.setDate(1); this.minMonth = new Date(this.minDate); + this.minMonth.setHours(0, 0, 0, 0); this.minMonth.setDate(1); if(this.selectedMonth.getTime() == this.minMonth.getTime()) { @@ -88,6 +172,11 @@ export default class PopupDatePicker extends PopupElement { this.nextBtn.setAttribute('disabled', 'true'); } + if(options.noTitle) { + this.setTitle = () => {}; + } + + this.setTimeTitle(); this.setTitle(); this.setMonth(); } @@ -132,15 +221,37 @@ export default class PopupDatePicker extends PopupElement { this.setTitle(); this.setMonth(); + this.setTimeTitle(); }; + public setTimeTitle() { + if(this.btnConfirm && this.selectedDate) { + let dayStr = ''; + const date = new Date(); + date.setHours(0, 0, 0, 0); + + if(this.selectedDate.getTime() === date.getTime()) { + dayStr = 'Today'; + } else if(this.selectedDate.getTime() === (date.getTime() + 86400e3)) { + dayStr = 'Tomorrow'; + } else { + dayStr = 'on ' + getFullDate(this.selectedDate, { + noTime: true, + monthAsNumber: true, + leadingZero: true + }); + } + + this.btnConfirm.innerText = 'Send ' + dayStr + ' at ' + ('00' + this.hoursInputField.value).slice(-2) + ':' + ('00' + this.minutesInputField.value).slice(-2); + } + } + public setTitle() { const splitted = this.selectedDate.toString().split(' ', 3); this.title.innerText = splitted[0] + ', ' + splitted[1] + ' ' + splitted[2]; } public setMonth() { - const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; this.monthTitle.innerText = months[this.selectedMonth.getMonth()] + ' ' + this.selectedMonth.getFullYear(); if(this.month) { @@ -176,11 +287,11 @@ export default class PopupDatePicker extends PopupElement { el.innerText = '' + date; el.dataset.timestamp = '' + firstDate.getTime(); - if(firstDate > this.maxDate) { + if(firstDate > this.maxDate || firstDate < this.minDate) { el.setAttribute('disabled', 'true'); } - if(firstDate.getTime() == this.selectedDate.getTime()) { + if(firstDate.getTime() === this.selectedDate.getTime()) { this.selectedEl = el; el.classList.add('active'); } @@ -188,7 +299,7 @@ export default class PopupDatePicker extends PopupElement { this.month.append(el); firstDate.setDate(date + 1); - } while(firstDate.getDate() != 1); + } while(firstDate.getDate() !== 1); this.container.classList.toggle('is-max-lines', (this.month.childElementCount / 7) > 6); diff --git a/src/components/popupDeleteMessages.ts b/src/components/popups/deleteMessages.ts similarity index 78% rename from src/components/popupDeleteMessages.ts rename to src/components/popups/deleteMessages.ts index 97324501..5bb38d16 100644 --- a/src/components/popupDeleteMessages.ts +++ b/src/components/popups/deleteMessages.ts @@ -1,25 +1,30 @@ -import appChatsManager from "../lib/appManagers/appChatsManager"; -import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import appPeersManager from "../lib/appManagers/appPeersManager"; -import rootScope from "../lib/rootScope"; -import { PopupButton } from "./popup"; -import PopupPeer from "./popupPeer"; +import appChatsManager from "../../lib/appManagers/appChatsManager"; +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import appPeersManager from "../../lib/appManagers/appPeersManager"; +import rootScope from "../../lib/rootScope"; +import { PopupButton } from "."; +import PopupPeer from "./peer"; +import { ChatType } from "../chat/chat"; export default class PopupDeleteMessages { - constructor(peerId: number, mids: number[], onConfirm?: () => void) { + constructor(peerId: number, mids: number[], type: ChatType, onConfirm?: () => void) { const firstName = appPeersManager.getPeerTitle(peerId, false, true); mids = mids.slice(); const callback = (revoke: boolean) => { onConfirm && onConfirm(); - appMessagesManager.deleteMessages(peerId, mids, revoke); + if(type === 'scheduled') { + appMessagesManager.deleteScheduledMessages(peerId, mids); + } else { + appMessagesManager.deleteMessages(peerId, mids, revoke); + } }; let title: string, description: string, buttons: PopupButton[]; title = `Delete ${mids.length == 1 ? '' : mids.length + ' '}Message${mids.length == 1 ? '' : 's'}?`; description = `Are you sure you want to delete ${mids.length == 1 ? 'this message' : 'these messages'}?`; - if(peerId == rootScope.myId) { + if(peerId == rootScope.myId || type === 'scheduled') { buttons = [{ text: 'DELETE', isDanger: true, diff --git a/src/components/popupForward.ts b/src/components/popups/forward.ts similarity index 82% rename from src/components/popupForward.ts rename to src/components/popups/forward.ts index ba1fcd96..ba1607f3 100644 --- a/src/components/popupForward.ts +++ b/src/components/popups/forward.ts @@ -1,7 +1,7 @@ -import { isTouchSupported } from "../helpers/touchSupport"; -import appImManager from "../lib/appManagers/appImManager"; -import AppSelectPeers from "./appSelectPeers"; -import { PopupElement } from "./popup"; +import { isTouchSupported } from "../../helpers/touchSupport"; +import appImManager from "../../lib/appManagers/appImManager"; +import AppSelectPeers from "../appSelectPeers"; +import PopupElement from "."; export default class PopupForward extends PopupElement { private selector: AppSelectPeers; @@ -14,7 +14,7 @@ export default class PopupForward extends PopupElement { this.selector = new AppSelectPeers(this.body, async() => { const peerId = this.selector.getSelected()[0]; - this.closeBtn.click(); + this.btnClose.click(); this.selector = null; diff --git a/src/components/popup.ts b/src/components/popups/index.ts similarity index 77% rename from src/components/popup.ts rename to src/components/popups/index.ts index 0156cb6d..9c49bdbd 100644 --- a/src/components/popup.ts +++ b/src/components/popups/index.ts @@ -1,21 +1,22 @@ -import rootScope from "../lib/rootScope"; -import { blurActiveElement, cancelEvent, findUpClassName } from "../helpers/dom"; -import { ripple } from "./ripple"; +import rootScope from "../../lib/rootScope"; +import { blurActiveElement, cancelEvent, findUpClassName } from "../../helpers/dom"; +import { ripple } from "../ripple"; -export class PopupElement { +export type PopupOptions = Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}>; +export default class PopupElement { protected element = document.createElement('div'); protected container = document.createElement('div'); protected header = document.createElement('div'); protected title = document.createElement('div'); - protected closeBtn: HTMLElement; - protected confirmBtn: HTMLElement; + protected btnClose: HTMLElement; + protected btnConfirm: HTMLElement; protected body: HTMLElement; protected onClose: () => void; protected onCloseAfterTimeout: () => void; protected onEscape: () => boolean = () => true; - constructor(className: string, buttons?: Array, options: Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}> = {}) { + constructor(className: string, buttons?: Array, options: PopupOptions = {}) { this.element.classList.add('popup'); this.element.className = 'popup' + (className ? ' ' + className : ''); this.container.classList.add('popup-container', 'z-depth-1'); @@ -26,17 +27,17 @@ export class PopupElement { this.header.append(this.title); if(options.closable) { - this.closeBtn = document.createElement('span'); - this.closeBtn.classList.add('btn-icon', 'popup-close', 'tgico-close'); + this.btnClose = document.createElement('span'); + this.btnClose.classList.add('btn-icon', 'popup-close', 'tgico-close'); //ripple(this.closeBtn); - this.header.prepend(this.closeBtn); + this.header.prepend(this.btnClose); - this.closeBtn.addEventListener('click', this.destroy, {once: true}); + this.btnClose.addEventListener('click', this.destroy, {once: true}); if(options.overlayClosable) { const onOverlayClick = (e: MouseEvent) => { if(!findUpClassName(e.target, 'popup-container')) { - this.closeBtn.click(); + this.btnClose.click(); } }; @@ -47,11 +48,11 @@ export class PopupElement { window.addEventListener('keydown', this._onKeyDown, {capture: true}); if(options.withConfirm) { - this.confirmBtn = document.createElement('button'); - this.confirmBtn.classList.add('btn-primary'); - this.confirmBtn.innerText = options.withConfirm; - this.header.append(this.confirmBtn); - ripple(this.confirmBtn); + this.btnConfirm = document.createElement('button'); + this.btnConfirm.classList.add('btn-primary'); + this.btnConfirm.innerText = options.withConfirm; + this.header.append(this.btnConfirm); + ripple(this.btnConfirm); } this.container.append(this.header); @@ -112,7 +113,7 @@ export class PopupElement { this.element.classList.remove('active'); window.removeEventListener('keydown', this._onKeyDown, {capture: true}); - if(this.closeBtn) this.closeBtn.removeEventListener('click', this.destroy); + if(this.btnClose) this.btnClose.removeEventListener('click', this.destroy); rootScope.overlayIsActive = false; setTimeout(() => { diff --git a/src/components/popupNewMedia.ts b/src/components/popups/newMedia.ts similarity index 76% rename from src/components/popupNewMedia.ts rename to src/components/popups/newMedia.ts index a6c1d5c1..9c782a7d 100644 --- a/src/components/popupNewMedia.ts +++ b/src/components/popups/newMedia.ts @@ -1,14 +1,13 @@ -import { isTouchSupported } from "../helpers/touchSupport"; -import appImManager from "../lib/appManagers/appImManager"; -import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { calcImageInBox, getRichValue } from "../helpers/dom"; -import InputField from "./inputField"; -import { PopupElement } from "./popup"; -import { ripple } from "./ripple"; -import Scrollable from "./scrollable"; -import { toast } from "./toast"; -import { prepareAlbum, wrapDocument } from "./wrappers"; -import CheckboxField from "./checkbox"; +import type Chat from "../chat/chat"; +import { isTouchSupported } from "../../helpers/touchSupport"; +import { calcImageInBox, placeCaretAtEnd } from "../../helpers/dom"; +import InputField from "../inputField"; +import PopupElement from "."; +import Scrollable from "../scrollable"; +import { toast } from "../toast"; +import { prepareAlbum, wrapDocument } from "../wrappers"; +import CheckboxField from "../checkbox"; +import SendContextMenu from "../chat/sendContextMenu"; type SendFileParams = Partial<{ file: File, @@ -23,10 +22,10 @@ const MAX_LENGTH_CAPTION = 1024; // TODO: .gif upload as video export default class PopupNewMedia extends PopupElement { - private btnSend: HTMLElement; - private input: HTMLInputElement; + private input: HTMLElement; private mediaContainer: HTMLElement; private groupCheckboxField: { label: HTMLLabelElement; input: HTMLInputElement; span: HTMLSpanElement; }; + private wasInputValue = ''; private willAttach: Partial<{ type: 'media' | 'document', @@ -37,39 +36,57 @@ export default class PopupNewMedia extends PopupElement { sendFileDetails: [], group: false }; + inputField: InputField; - constructor(files: File[], willAttachType: PopupNewMedia['willAttach']['type']) { - super('popup-send-photo popup-new-media', null, {closable: true}); + constructor(private chat: Chat, files: File[], willAttachType: PopupNewMedia['willAttach']['type']) { + super('popup-send-photo popup-new-media', null, {closable: true, withConfirm: 'SEND'}); 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); - - this.header.append(this.btnSend); + this.btnConfirm.addEventListener('click', () => this.send()); + + if(this.chat.type !== 'scheduled') { + const sendMenu = new SendContextMenu({ + onSilentClick: () => { + this.chat.input.sendSilent = true; + this.send(); + }, + onScheduleClick: () => { + this.chat.input.scheduleSending(() => { + this.send(); + }); + }, + openSide: 'bottom-left', + onContextElement: this.btnConfirm, + }); + + sendMenu.setPeerId(this.chat.peerId); + + this.header.append(sendMenu.sendMenu); + } this.mediaContainer = document.createElement('div'); this.mediaContainer.classList.add('popup-photo'); const scrollable = new Scrollable(null); scrollable.container.append(this.mediaContainer); - const inputField = InputField({ + this.inputField = new InputField({ placeholder: 'Add a caption...', label: 'Caption', name: 'photo-caption', maxLength: MAX_LENGTH_CAPTION, showLengthOn: 80 }); - this.input = inputField.input; + this.input = this.inputField.input; + + this.inputField.value = this.wasInputValue = this.chat.input.messageInputField.value; + this.chat.input.messageInputField.value = ''; this.container.append(scrollable.container); if(files.length > 1) { this.groupCheckboxField = CheckboxField('Group items', 'group-items'); - this.container.append(this.groupCheckboxField.label, inputField.container); + this.container.append(this.groupCheckboxField.label, this.inputField.container); this.groupCheckboxField.input.checked = true; this.willAttach.group = true; @@ -86,7 +103,7 @@ export default class PopupNewMedia extends PopupElement { }); } - this.container.append(inputField.container); + this.container.append(this.inputField.container); this.attachFiles(files); } @@ -95,15 +112,24 @@ export default class PopupNewMedia extends PopupElement { const target = e.target as HTMLElement; if(target.tagName != 'INPUT') { this.input.focus(); + placeCaretAtEnd(this.input); } if(e.key == 'Enter' && !isTouchSupported) { - this.btnSend.click(); + this.btnConfirm.click(); } }; - public send = () => { - let caption = getRichValue(this.input); + public send(force = false) { + if(this.chat.type === 'scheduled' && !force) { + this.chat.input.scheduleSending(() => { + this.send(true); + }); + + return; + } + + let caption = this.inputField.value; if(caption.length > MAX_LENGTH_CAPTION) { toast('Caption is too long.'); return; @@ -115,8 +141,10 @@ export default class PopupNewMedia extends PopupElement { //console.log('will send files with options:', willAttach); - const peerId = appImManager.chat.peerId; - const chatInputC = appImManager.chat.input; + const peerId = this.chat.peerId; + const input = this.chat.input; + const silent = input.sendSilent; + const scheduleDate = input.scheduleDate; if(willAttach.sendFileDetails.length > 1 && willAttach.group) { for(let i = 0; i < willAttach.sendFileDetails.length;) { @@ -131,34 +159,38 @@ export default class PopupNewMedia extends PopupElement { const w = {...willAttach}; w.sendFileDetails = willAttach.sendFileDetails.slice(i - k, i); - appMessagesManager.sendAlbum(peerId, w.sendFileDetails.map(d => d.file), Object.assign({ + this.chat.appMessagesManager.sendAlbum(peerId, w.sendFileDetails.map(d => d.file), Object.assign({ caption, - replyToMsgId: chatInputC.replyToMsgId, - isMedia: willAttach.isMedia + replyToMsgId: input.replyToMsgId, + isMedia: willAttach.isMedia, + silent, + scheduleDate }, w)); caption = undefined; - chatInputC.replyToMsgId = 0; + input.replyToMsgId = undefined; } } else { if(caption) { if(willAttach.sendFileDetails.length > 1) { - appMessagesManager.sendText(peerId, caption, {replyToMsgId: chatInputC.replyToMsgId}); + this.chat.appMessagesManager.sendText(peerId, caption, {replyToMsgId: input.replyToMsgId, silent, scheduleDate}); caption = ''; - chatInputC.replyToMsgId = 0; + input.replyToMsgId = undefined; } } const promises = willAttach.sendFileDetails.map(params => { - const promise = appMessagesManager.sendFile(peerId, params.file, Object.assign({ + const promise = this.chat.appMessagesManager.sendFile(peerId, params.file, Object.assign({ //isMedia: willAttach.isMedia, isMedia: willAttach.isMedia, caption, - replyToMsgId: chatInputC.replyToMsgId + replyToMsgId: input.replyToMsgId, + silent, + scheduleDate }, params)); caption = ''; - chatInputC.replyToMsgId = 0; + input.replyToMsgId = undefined; return promise; }); } @@ -167,8 +199,8 @@ export default class PopupNewMedia extends PopupElement { //appMessagesManager.sendFile(appImManager.peerId, willAttach.file, willAttach); - chatInputC.onMessageSent(); - }; + input.onMessageSent(); + } public attachFile = (file: File) => { const willAttach = this.willAttach; @@ -343,6 +375,10 @@ export default class PopupNewMedia extends PopupElement { if(!this.element.classList.contains('active')) { document.body.addEventListener('keydown', this.onKeyDown); this.onClose = () => { + if(this.wasInputValue) { + this.chat.input.messageInputField.value = this.wasInputValue; + } + document.body.removeEventListener('keydown', this.onKeyDown); }; this.show(); diff --git a/src/components/popupPeer.ts b/src/components/popups/peer.ts similarity index 89% rename from src/components/popupPeer.ts rename to src/components/popups/peer.ts index 3c6d3914..3c580d23 100644 --- a/src/components/popupPeer.ts +++ b/src/components/popups/peer.ts @@ -1,5 +1,5 @@ -import AvatarElement from "./avatar"; -import { PopupElement, PopupButton } from "./popup"; +import AvatarElement from "../avatar"; +import PopupElement, { PopupButton } from "."; export default class PopupPeer extends PopupElement { constructor(private className: string, options: Partial<{ diff --git a/src/components/popups/schedule.ts b/src/components/popups/schedule.ts new file mode 100644 index 00000000..1171cbbf --- /dev/null +++ b/src/components/popups/schedule.ts @@ -0,0 +1,32 @@ +import PopupDatePicker from "./datePicker"; + +const getMinDate = () => { + const date = new Date(); + date.setDate(date.getDate() - 1); + //date.setHours(0, 0, 0, 0); + return date; +}; + +export default class PopupSchedule extends PopupDatePicker { + constructor(initDate: Date, onPick: (timestamp: number) => void) { + super(initDate, onPick, { + noButtons: true, + noTitle: true, + closable: true, + withConfirm: 'Send Today', + minDate: getMinDate(), + maxDate: (() => { + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + date.setDate(date.getDate() - 1); + return date; + })(), + withTime: true + }); + + this.element.classList.add('popup-schedule'); + this.header.append(this.controlsDiv); + this.title.replaceWith(this.monthTitle); + this.body.append(this.btnConfirm); + } +} \ No newline at end of file diff --git a/src/components/popups/sendNow.ts b/src/components/popups/sendNow.ts new file mode 100644 index 00000000..0bb2261d --- /dev/null +++ b/src/components/popups/sendNow.ts @@ -0,0 +1,36 @@ +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import { PopupButton } from "."; +import PopupPeer from "./peer"; + +export default class PopupSendNow { + constructor(peerId: number, mids: number[], onConfirm?: () => void) { + let title: string, description: string, buttons: PopupButton[] = []; + + title = `Send Message${mids.length > 1 ? 's' : ''} Now`; + description = mids.length > 1 ? 'Send ' + mids.length + ' messages now?' : 'Send message now?'; + + const callback = () => { + onConfirm && onConfirm(); + appMessagesManager.sendScheduledMessages(peerId, mids); + }; + + buttons.push({ + text: 'SEND', + callback + }); + + buttons.push({ + text: 'CANCEL', + isCancel: true + }); + + const popup = new PopupPeer('popup-delete-chat', { + peerId, + title, + description, + buttons + }); + + popup.show(); + } +} \ No newline at end of file diff --git a/src/components/popupStickers.ts b/src/components/popups/stickers.ts similarity index 85% rename from src/components/popupStickers.ts rename to src/components/popups/stickers.ts index 40da60c0..ca5c6d27 100644 --- a/src/components/popupStickers.ts +++ b/src/components/popups/stickers.ts @@ -1,15 +1,15 @@ -import { PopupElement } from "./popup"; -import appStickersManager from "../lib/appManagers/appStickersManager"; -import { RichTextProcessor } from "../lib/richtextprocessor"; -import Scrollable from "./scrollable"; -import { wrapSticker } from "./wrappers"; -import LazyLoadQueue from "./lazyLoadQueue"; -import { putPreloader } from "./misc"; -import animationIntersector from "./animationIntersector"; -import { findUpClassName } from "../helpers/dom"; -import appImManager from "../lib/appManagers/appImManager"; -import { StickerSet } from "../layer"; -import mediaSizes from "../helpers/mediaSizes"; +import PopupElement from "."; +import appStickersManager from "../../lib/appManagers/appStickersManager"; +import { RichTextProcessor } from "../../lib/richtextprocessor"; +import Scrollable from "../scrollable"; +import { wrapSticker } from "../wrappers"; +import LazyLoadQueue from "../lazyLoadQueue"; +import { putPreloader } from "../misc"; +import animationIntersector from "../animationIntersector"; +import { findUpClassName } from "../../helpers/dom"; +import appImManager from "../../lib/appManagers/appImManager"; +import { StickerSet } from "../../layer"; +import mediaSizes from "../../helpers/mediaSizes"; const ANIMATION_GROUP = 'STICKERS-POPUP'; @@ -74,7 +74,7 @@ export default class PopupStickers extends PopupElement { this.stickersFooter.setAttribute('disabled', 'true'); appStickersManager.toggleStickerSet(this.set).then(() => { - this.closeBtn.click(); + this.btnClose.click(); }).catch(() => { this.stickersFooter.removeAttribute('disabled'); }); @@ -86,7 +86,7 @@ export default class PopupStickers extends PopupElement { const fileId = target.dataset.docId; if(appImManager.chat.input.sendMessageWithDocument(fileId)) { - this.closeBtn.click(); + this.btnClose.click(); } else { console.warn('got no doc by id:', fileId); } diff --git a/src/components/popupUnpinMessage.ts b/src/components/popups/unpinMessage.ts similarity index 87% rename from src/components/popupUnpinMessage.ts rename to src/components/popups/unpinMessage.ts index c3986448..04fc9884 100644 --- a/src/components/popupUnpinMessage.ts +++ b/src/components/popups/unpinMessage.ts @@ -1,6 +1,6 @@ -import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import { PopupButton } from "./popup"; -import PopupPeer from "./popupPeer"; +import appMessagesManager from "../../lib/appManagers/appMessagesManager"; +import { PopupButton } from "."; +import PopupPeer from "./peer"; export default class PopupPinMessage { constructor(peerId: number, mid: number, unpin?: true) { diff --git a/src/components/sidebarLeft/tabs/editProfile.ts b/src/components/sidebarLeft/tabs/editProfile.ts index 31502ad0..84045ea9 100644 --- a/src/components/sidebarLeft/tabs/editProfile.ts +++ b/src/components/sidebarLeft/tabs/editProfile.ts @@ -1,5 +1,4 @@ import appSidebarLeft from ".."; -import { getRichValue } from "../../../helpers/dom"; import { InputFile } from "../../../layer"; import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager"; @@ -8,7 +7,7 @@ import RichTextProcessor from "../../../lib/richtextprocessor"; import rootScope from "../../../lib/rootScope"; import AvatarElement from "../../avatar"; import InputField from "../../inputField"; -import PopupAvatar from "../../popupAvatar"; +import PopupAvatar from "../../popups/avatar"; import Scrollable from "../../scrollable"; import { SliderTab } from "../../slider"; @@ -21,10 +20,10 @@ export default class AppEditProfileTab implements SliderTab { private canvas: HTMLCanvasElement; private uploadAvatar: () => Promise = null; - private firstNameInput: HTMLInputElement; - private lastNameInput: HTMLInputElement; - private bioInput: HTMLInputElement; - private userNameInput: HTMLInputElement; + private firstNameInput: HTMLElement; + private lastNameInput: HTMLElement; + private bioInput: HTMLElement; + private userNameInput: HTMLElement; private avatarElem: AvatarElement; @@ -37,6 +36,10 @@ export default class AppEditProfileTab implements SliderTab { userName: '', bio: '' }; + firstNameInputField: InputField; + lastNameInputField: InputField; + bioInputField: InputField; + userNameInputField: InputField; public init() { this.container = document.querySelector('.edit-profile-container'); @@ -63,27 +66,27 @@ export default class AppEditProfileTab implements SliderTab { const inputWrapper = document.createElement('div'); inputWrapper.classList.add('input-wrapper'); - const firstNameInputField = InputField({ + this.firstNameInputField = new InputField({ label: 'Name', name: 'first-name', maxLength: 70 }); - const lastNameInputField = InputField({ + this.lastNameInputField = new InputField({ label: 'Last Name', name: 'last-name', maxLength: 64 }); - const bioInputField = InputField({ + this.bioInputField = new InputField({ label: 'Bio (optional)', name: 'bio', maxLength: 70 }); - this.firstNameInput = firstNameInputField.input; - this.lastNameInput = lastNameInputField.input; - this.bioInput = bioInputField.input; + this.firstNameInput = this.firstNameInputField.input; + this.lastNameInput = this.lastNameInputField.input; + this.bioInput = this.bioInputField.input; - inputWrapper.append(firstNameInputField.container, lastNameInputField.container, bioInputField.container); + inputWrapper.append(this.firstNameInputField.container, this.lastNameInputField.container, this.bioInputField.container); avatarEdit.parentElement.insertBefore(inputWrapper, avatarEdit.nextElementSibling); } @@ -91,14 +94,14 @@ export default class AppEditProfileTab implements SliderTab { const inputWrapper = document.createElement('div'); inputWrapper.classList.add('input-wrapper'); - const userNameInputField = InputField({ + this.userNameInputField = new InputField({ label: 'Username (optional)', name: 'username', plainText: true }); - this.userNameInput = userNameInputField.input; + this.userNameInput = this.userNameInputField.input; - inputWrapper.append(userNameInputField.container); + inputWrapper.append(this.userNameInputField.container); const caption = this.profileUrlContainer.parentElement; caption.parentElement.insertBefore(inputWrapper, caption); @@ -110,7 +113,7 @@ export default class AppEditProfileTab implements SliderTab { this.lastNameInput.addEventListener('input', this.handleChange); this.bioInput.addEventListener('input', this.handleChange); this.userNameInput.addEventListener('input', () => { - let value = this.userNameInput.value; + let value = this.userNameInputField.value; //console.log('userNameInput:', value); if(value == this.originalValues.userName || !value.length) { @@ -136,7 +139,7 @@ export default class AppEditProfileTab implements SliderTab { apiManager.invokeApi('account.checkUsername', { username: value }).then(available => { - if(this.userNameInput.value != value) return; + if(this.userNameInputField.value != value) return; if(available) { this.userNameInput.classList.add('valid'); @@ -148,7 +151,7 @@ export default class AppEditProfileTab implements SliderTab { userNameLabel.innerText = 'Username is already taken'; } }, (err) => { - if(this.userNameInput.value != value) return; + if(this.userNameInputField.value != value) return; switch(err.type) { case 'USERNAME_INVALID': { @@ -169,7 +172,7 @@ export default class AppEditProfileTab implements SliderTab { let promises: Promise[] = []; - promises.push(appProfileManager.updateProfile(getRichValue(this.firstNameInput), getRichValue(this.lastNameInput), getRichValue(this.bioInput)).then(() => { + promises.push(appProfileManager.updateProfile(this.firstNameInputField.value, this.lastNameInputField.value, this.bioInputField.value).then(() => { appSidebarLeft.selectTab(0); }, (err) => { console.error('updateProfile error:', err); @@ -181,8 +184,8 @@ export default class AppEditProfileTab implements SliderTab { })); } - if(this.userNameInput.value != this.originalValues.userName && this.userNameInput.classList.contains('valid')) { - promises.push(appProfileManager.updateUsername(this.userNameInput.value)); + if(this.userNameInputField.value != this.originalValues.userName && this.userNameInput.classList.contains('valid')) { + promises.push(appProfileManager.updateUsername(this.userNameInputField.value)); } Promise.race(promises).finally(() => { @@ -211,7 +214,7 @@ export default class AppEditProfileTab implements SliderTab { this.firstNameInput.innerHTML = user.rFirstName; this.lastNameInput.innerHTML = RichTextProcessor.wrapRichText(user.last_name, {noLinks: true, noLinebreaks: true}); this.bioInput.innerHTML = ''; - this.userNameInput.value = this.originalValues.userName = user.username ?? ''; + this.userNameInputField.value = this.originalValues.userName = user.username ?? ''; this.userNameInput.classList.remove('valid', 'error'); this.userNameInput.nextElementSibling.innerHTML = 'Username (optional)'; @@ -242,18 +245,18 @@ export default class AppEditProfileTab implements SliderTab { private isChanged() { return !!this.uploadAvatar - || (!this.firstNameInput.classList.contains('error') && getRichValue(this.firstNameInput) != this.originalValues.firstName) - || (!this.lastNameInput.classList.contains('error') && getRichValue(this.lastNameInput) != this.originalValues.lastName) - || (!this.bioInput.classList.contains('error') && getRichValue(this.bioInput) != this.originalValues.bio) - || (this.userNameInput.value != this.originalValues.userName && !this.userNameInput.classList.contains('error')); + || (!this.firstNameInput.classList.contains('error') && this.firstNameInputField.value != this.originalValues.firstName) + || (!this.lastNameInput.classList.contains('error') && this.lastNameInputField.value != this.originalValues.lastName) + || (!this.bioInput.classList.contains('error') && this.bioInputField.value != this.originalValues.bio) + || (this.userNameInputField.value != this.originalValues.userName && !this.userNameInput.classList.contains('error')); } private setProfileUrl() { - if(this.userNameInput.classList.contains('error') || !this.userNameInput.value.length) { + if(this.userNameInput.classList.contains('error') || !this.userNameInputField.value.length) { this.profileUrlContainer.style.display = 'none'; } else { this.profileUrlContainer.style.display = ''; - let url = 'https://t.me/' + this.userNameInput.value; + let url = 'https://t.me/' + this.userNameInputField.value; this.profileUrlAnchor.innerText = url; this.profileUrlAnchor.href = url; } diff --git a/src/components/sidebarLeft/tabs/newChannel.ts b/src/components/sidebarLeft/tabs/newChannel.ts index b3c1c522..1c4b1f17 100644 --- a/src/components/sidebarLeft/tabs/newChannel.ts +++ b/src/components/sidebarLeft/tabs/newChannel.ts @@ -1,7 +1,7 @@ import appSidebarLeft, { AppSidebarLeft } from ".."; import { InputFile } from "../../../layer"; import appChatsManager from "../../../lib/appManagers/appChatsManager"; -import PopupAvatar from "../../popupAvatar"; +import PopupAvatar from "../../popups/avatar"; import { SliderTab } from "../../slider"; export default class AppNewChannelTab implements SliderTab { diff --git a/src/components/sidebarLeft/tabs/newGroup.ts b/src/components/sidebarLeft/tabs/newGroup.ts index 83339d7b..7ae2502f 100644 --- a/src/components/sidebarLeft/tabs/newGroup.ts +++ b/src/components/sidebarLeft/tabs/newGroup.ts @@ -4,7 +4,7 @@ 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 PopupAvatar from "../../popups/avatar"; import Scrollable from "../../scrollable"; import { SliderTab } from "../../slider"; diff --git a/src/components/sidebarRight/tabs/pollResults.ts b/src/components/sidebarRight/tabs/pollResults.ts index dba8f4cf..204d1371 100644 --- a/src/components/sidebarRight/tabs/pollResults.ts +++ b/src/components/sidebarRight/tabs/pollResults.ts @@ -14,9 +14,7 @@ export default class AppPollResultsTab implements SliderTab { private resultsDiv = this.contentDiv.firstElementChild as HTMLDivElement; private scrollable: Scrollable; - private peerId: number; - private pollId: string; - private mid: number; + private message: any; constructor() { this.scrollable = new Scrollable(this.contentDiv, 'POLL-RESULTS'); @@ -24,26 +22,23 @@ export default class AppPollResultsTab implements SliderTab { public cleanup() { this.resultsDiv.innerHTML = ''; - this.pollId = ''; - this.mid = 0; + this.message = undefined; } public onCloseAfterTimeout() { this.cleanup(); } - public init(peerId: number, pollId: string, mid: number) { - if(this.peerId == peerId && this.pollId == pollId && this.mid == mid) return; + public init(message: any) { + if(this.message === message) return; this.cleanup(); - this.peerId = peerId; - this.pollId = pollId; - this.mid = mid; + this.message = message; appSidebarRight.selectTab(AppSidebarRight.SLIDERITEMSIDS.pollResults); - const poll = appPollsManager.getPoll(pollId); + const poll = appPollsManager.getPoll(message.media.poll.id); const title = document.createElement('h3'); title.innerHTML = poll.poll.rQuestion; @@ -88,7 +83,7 @@ export default class AppPollResultsTab implements SliderTab { if(loading) return; loading = true; - appPollsManager.getVotes(peerId, mid, answer.option, offset, limit).then(votesList => { + appPollsManager.getVotes(message, answer.option, offset, limit).then(votesList => { votesList.votes.forEach(vote => { const {dom} = appDialogsManager.addDialogNew({ dialog: vote.user_id, diff --git a/src/components/sidebarRight/tabs/sharedMedia.ts b/src/components/sidebarRight/tabs/sharedMedia.ts index 42898c8c..422ec86f 100644 --- a/src/components/sidebarRight/tabs/sharedMedia.ts +++ b/src/components/sidebarRight/tabs/sharedMedia.ts @@ -498,7 +498,7 @@ export default class AppSharedMediaTab implements SliderTab { div.append(img); if(isDownloaded || willHaveThumb) { - const promise = new Promise((resolve, reject) => { + const promise = new Promise((resolve, reject) => { (thumb || img).addEventListener('load', () => { clearTimeout(timeout); resolve(); @@ -542,14 +542,14 @@ export default class AppSharedMediaTab implements SliderTab { if(message.media?.webpage && message.media.webpage._ != 'webPageEmpty') { webpage = message.media.webpage; } else { - const entity = message.totalEntities.find((e: any) => e._ == 'messageEntityUrl' || e._ == 'messageEntityTextUrl'); + const entity = message.totalEntities ? message.totalEntities.find((e: any) => e._ == 'messageEntityUrl' || e._ == 'messageEntityTextUrl') : null; let url: string, display_url: string, sliced: string; if(!entity) { - this.log.error('NO ENTITY:', message); + //this.log.error('NO ENTITY:', message); const match = RichTextProcessor.matchUrl(message.message); if(!match) { - this.log.error('NO ENTITY AND NO MATCH:', message); + //this.log.error('NO ENTITY AND NO MATCH:', message); continue; } @@ -745,7 +745,8 @@ export default class AppSharedMediaTab implements SliderTab { //let loadCount = history.length ? 50 : 15; return this.loadSidebarMediaPromises[type] = appMessagesManager.getSearch(peerId, '', {_: type}, maxId, loadCount) .then(value => { - history.push(...value.history); + const mids = value.history.map(message => message.mid); + history.push(...mids); this.log(logStr + 'search house of glass', type, value); @@ -783,7 +784,7 @@ export default class AppSharedMediaTab implements SliderTab { } //if(value.history.length) { - return this.performSearchResult(this.filterMessagesByType(value.history, type), type); + return this.performSearchResult(this.filterMessagesByType(mids, type), type); //} }).catch(err => { this.log.error('load error:', err); diff --git a/src/components/sidebarRight/tabs/stickers.ts b/src/components/sidebarRight/tabs/stickers.ts index 6189e22b..efbcdbc2 100644 --- a/src/components/sidebarRight/tabs/stickers.ts +++ b/src/components/sidebarRight/tabs/stickers.ts @@ -5,7 +5,7 @@ import LazyLoadQueue from "../../lazyLoadQueue"; import { findUpClassName } from "../../../helpers/dom"; import appImManager from "../../../lib/appManagers/appImManager"; import appStickersManager from "../../../lib/appManagers/appStickersManager"; -import PopupStickers from "../../popupStickers"; +import PopupStickers from "../../popups/stickers"; import animationIntersector from "../../animationIntersector"; import { RichTextProcessor } from "../../../lib/richtextprocessor"; import { wrapSticker } from "../../wrappers"; diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 5f4f8c0d..de94db7b 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1041,10 +1041,11 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, return nameContainer; } -export function wrapPoll(peerId: number, pollId: string, mid: number) { +export function wrapPoll(message: any) { const elem = new PollElement(); - elem.setAttribute('peer-id', '' + peerId); - elem.setAttribute('poll-id', pollId); - elem.setAttribute('message-id', '' + mid); + elem.message = message; + elem.setAttribute('peer-id', '' + message.peerId); + elem.setAttribute('poll-id', message.media.poll.id); + elem.setAttribute('message-id', '' + message.mid); return elem; } diff --git a/src/helpers/date.ts b/src/helpers/date.ts index 028578ae..ae26750b 100644 --- a/src/helpers/date.ts +++ b/src/helpers/date.ts @@ -31,9 +31,19 @@ export const formatDateAccordingToToday = (time: Date) => { return timeStr; }; -export const getFullDate = (date: Date) => { - return date.getDate() + ' ' + months[date.getMonth()] + ' ' + date.getFullYear() + - ', ' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2); +export const getFullDate = (date: Date, options: Partial<{ + noTime: true, + noSeconds: true, + monthAsNumber: true, + leadingZero: true +}> = {}) => { + const joiner = options.monthAsNumber ? '.' : ' '; + const time = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + (options.noSeconds ? '' : ':' + ('0' + date.getSeconds()).slice(-2)); + + return (options.leadingZero ? ('0' + date.getDate()).slice(-2) : date.getDate()) + + joiner + (options.monthAsNumber ? ('0' + (date.getMonth() + 1)).slice(-2) : months[date.getMonth()]) + + joiner + date.getFullYear() + + (options.noTime ? '' : ', ' + time); }; export function tsNow(seconds?: true) { diff --git a/src/helpers/dom.ts b/src/helpers/dom.ts index 9ec7ac9b..ee0e7dfd 100644 --- a/src/helpers/dom.ts +++ b/src/helpers/dom.ts @@ -467,12 +467,14 @@ export function blurActiveElement() { } export const CLICK_EVENT_NAME = isTouchSupported ? 'touchend' : 'click'; -export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter}>; +export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter, touchMouseDown: true}>; export const attachClickEvent = (elem: HTMLElement, callback: (e: TouchEvent | MouseEvent) => void, options: AttachClickOptions = {}) => { const add = options.listenerSetter ? options.listenerSetter.add.bind(options.listenerSetter, elem) : elem.addEventListener.bind(elem); const remove = options.listenerSetter ? options.listenerSetter.removeManual.bind(options.listenerSetter, elem) : elem.removeEventListener.bind(elem); - if(CLICK_EVENT_NAME == 'touchend') { + if(options.touchMouseDown && CLICK_EVENT_NAME === 'touchend') { + add('mousedown', callback, options); + } else if(CLICK_EVENT_NAME === 'touchend') { const o = {...options, once: true}; const onTouchStart = (e: TouchEvent) => { @@ -600,7 +602,7 @@ export async function getFilesFromEvent(e: ClipboardEvent | DragEvent, onlyTypes const scanFiles = async(item: any) => { if(item.isDirectory) { const directoryReader = item.createReader(); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { directoryReader.readEntries(async(entries: any) => { for(const entry of entries) { await scanFiles(entry); diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index d76a1180..cad3939a 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -21,7 +21,7 @@ import appProfileManager from './appProfileManager'; import appStickersManager from './appStickersManager'; import appWebPagesManager from './appWebPagesManager'; import { cancelEvent, getFilesFromEvent, placeCaretAtEnd } from '../../helpers/dom'; -import PopupNewMedia from '../../components/popupNewMedia'; +import PopupNewMedia from '../../components/popups/newMedia'; import { TransitionSlider } from '../../components/transition'; import { numberWithCommas } from '../../helpers/number'; import MarkupTooltip from '../../components/chat/markupTooltip'; @@ -203,15 +203,15 @@ export class AppImManager { return; } else if(e.code == 'ArrowUp') { if(!chat.input.editMsgId) { - const history = appMessagesManager.historiesStorage[chat.peerId]; - if(history?.history) { + const history = appMessagesManager.getHistoryStorage(chat.peerId); + if(history.history.length) { let goodMid: number; for(const mid of history.history) { const message = appMessagesManager.getMessageByPeer(chat.peerId, mid); const good = this.myId == chat.peerId ? message.fromId == this.myId : message.pFlags.out; if(good) { - if(appMessagesManager.canEditMessage(this.chat.peerId, mid, 'text')) { + if(appMessagesManager.canEditMessage(this.chat.getMessage(mid), 'text')) { goodMid = mid; } @@ -235,6 +235,28 @@ export class AppImManager { document.body.addEventListener('keydown', onKeyDown); + rootScope.addEventListener('history_multiappend', (e) => { + const msgIdsByPeer = e.detail; + + for(const peerId in msgIdsByPeer) { + appSidebarRight.sharedMediaTab.renderNewMessages(+peerId, msgIdsByPeer[peerId]); + } + }); + + rootScope.addEventListener('history_delete', (e) => { + const {peerId, msgs} = e.detail; + + const mids = Object.keys(msgs).map(s => +s); + appSidebarRight.sharedMediaTab.deleteDeletedMessages(peerId, mids); + }); + + // Calls when message successfully sent and we have an id + rootScope.addEventListener('message_sent', (e) => { + const {storage, tempId, mid} = e.detail; + const message = appMessagesManager.getMessageFromStorage(storage, mid); + appSidebarRight.sharedMediaTab.renderNewMessages(message.peerId, [mid]); + }); + if(!isTouchSupported) { this.attachDragAndDropListeners(); } @@ -379,7 +401,7 @@ export class AppImManager { const chatInput = this.chat.input; chatInput.willAttachType = attachType || (files[0].type.indexOf('image/') === 0 ? 'media' : "document"); - new PopupNewMedia(files, chatInput.willAttachType); + new PopupNewMedia(this.chat, files, chatInput.willAttachType); } }); }; @@ -406,7 +428,7 @@ export class AppImManager { } private createNewChat() { - const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appSidebarRight, appPollsManager, apiManager); + const chat = new Chat(this, appChatsManager, appDocsManager, appInlineBotsManager, appMessagesManager, appPeersManager, appPhotosManager, appProfileManager, appStickersManager, appUsersManager, appWebPagesManager, appPollsManager, apiManager); this.chats.push(chat); } @@ -511,6 +533,10 @@ export class AppImManager { return this.setPeer(peerId, lastMsgId); } + public openScheduled(peerId: number) { + this.setInnerPeer(peerId, undefined, 'scheduled'); + } + public async getPeerStatus(peerId: number) { let subtitle = ''; if(!peerId) return subtitle; diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 730d73dd..2d34d0a9 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -96,17 +96,23 @@ export class AppMessagesManager { } = {}; public pinnedMessages: {[peerId: string]: PinnedStorage} = {}; - public pendingByRandomId: {[randomId: string]: [number, number]} = {}; - public pendingByMessageId: any = {}; + public pendingByRandomId: { + [randomId: string]: { + peerId: number, + tempId: number, + storage: MessagesStorage + } + } = {}; + public pendingByMessageId: {[mid: string]: string} = {}; public pendingAfterMsgs: any = {}; public pendingTopMsgs: {[peerId: string]: number} = {}; public sendFilePromise: CancellablePromise = Promise.resolve(); public tempId = -1; public tempFinalizeCallbacks: { - [mid: string]: { + [tempId: string]: { [callbackName: string]: Partial<{ deferred: CancellablePromise, - callback: (mid: number) => Promise + callback: (message: any) => Promise }> } } = {}; @@ -141,7 +147,7 @@ export class AppMessagesManager { folderId: 0 }; - private log = logger('MESSAGES'/* , LogLevels.error | LogLevels.debug | LogLevels.log | LogLevels.warn */); + private log = logger('MESSAGES', LogLevels.error | LogLevels.debug | LogLevels.log | LogLevels.warn); public dialogsStorage: DialogsStorage; public filtersStorage: FiltersStorage; @@ -164,10 +170,12 @@ export class AppMessagesManager { webpage: appWebPagesManager.getWebPage(eventData.id) }; + const peerId = this.getMessagePeer(message); + const storage = this.getMessagesStorage(peerId); rootScope.broadcast('message_edit', { - peerId: this.getMessagePeer(message), - mid: mid, - justMedia: true + storage, + peerId, + mid }); }); }); @@ -216,8 +224,8 @@ export class AppMessagesManager { const folder = this.dialogsStorage.getFolder(+folderId); for(let dialog of folder) { - const historyStorage = this.historiesStorage[dialog.peerId]; - const history = [].concat(historyStorage?.pending ?? [], historyStorage?.history ?? []); + const historyStorage = this.getHistoryStorage(dialog.peerId); + const history = [].concat(historyStorage.pending, historyStorage.history); dialog = copy(dialog); let removeUnread = 0; @@ -311,8 +319,8 @@ export class AppMessagesManager { return sendEntites; } - public invokeAfterMessageIsSent(messageId: number, callbackName: string, callback: (mid: number) => Promise) { - const finalize = this.tempFinalizeCallbacks[messageId] ?? (this.tempFinalizeCallbacks[messageId] = {}); + public invokeAfterMessageIsSent(tempId: number, callbackName: string, callback: (message: any) => Promise) { + const finalize = this.tempFinalizeCallbacks[tempId] ?? (this.tempFinalizeCallbacks[tempId] = {}); const obj = finalize[callbackName] ?? (finalize[callbackName] = {deferred: deferredPromise()}); obj.callback = callback; @@ -320,27 +328,32 @@ export class AppMessagesManager { return obj.deferred; } - public editMessage(peerId: number, mid: number, text: string, options: Partial<{ + public editMessage(message: any, text: string, options: Partial<{ noWebPage: true, - newMedia: any + newMedia: any, + scheduleDate: number, + entities: MessageEntity[] }> = {}): Promise { /* if(!this.canEditMessage(messageId)) { return Promise.reject({type: 'MESSAGE_EDIT_FORBIDDEN'}); } */ + const {mid, peerId} = message; + if(mid < 0) { - return this.invokeAfterMessageIsSent(mid, 'edit', (mid) => { - this.log('invoke editMessage callback', mid); - return this.editMessage(peerId, mid, text, options); + return this.invokeAfterMessageIsSent(mid, 'edit', (message) => { + this.log('invoke editMessage callback', message); + return this.editMessage(message, text, options); }); } - let entities: any[]; - if(typeof(text) === 'string') { + let entities = options.entities; + if(typeof(text) === 'string' && !entities) { entities = []; text = RichTextProcessor.parseMarkdown(text, entities); } + const schedule_date = options.scheduleDate || (message.pFlags.is_scheduled ? message.date : undefined); return apiManager.invokeApi('messages.editMessage', { peer: appPeersManager.getInputPeerById(peerId), id: mid, @@ -348,6 +361,7 @@ export class AppMessagesManager { media: options.newMedia, entities: entities ? this.getInputEntities(entities) : undefined, no_webpage: options.noWebPage, + schedule_date }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, (error) => { @@ -373,12 +387,16 @@ export class AppMessagesManager { noWebPage: true, reply_markup: any, clearDraft: true, - webPage: any + webPage: any, + scheduleDate: number, + silent: true }> = {}) { if(typeof(text) != 'string' || !text.length) { return; } + //this.checkSendOptions(options); + const MAX_LENGTH = 4096; if(text.length > MAX_LENGTH) { const splitted = splitStringByLength(text, MAX_LENGTH); @@ -409,7 +427,6 @@ export class AppMessagesManager { var messageId = this.tempId--; var randomIdS = randomLong(); - var historyStorage = this.historiesStorage[peerId]; var pFlags: any = {}; var replyToMsgId = options.replyToMsgId; var isChannel = appPeersManager.isChannel(peerId); @@ -417,10 +434,6 @@ export class AppMessagesManager { var asChannel = isChannel && !isMegagroup ? true : false; var message: any; - if(historyStorage === undefined) { - historyStorage = this.historiesStorage[peerId] = {count: null, history: [], pending: []}; - } - var fromId = appUsersManager.getSelf().id; if(peerId != fromId) { pFlags.out = true; @@ -440,8 +453,8 @@ export class AppMessagesManager { id: messageId, from_id: appPeersManager.getOutputPeer(fromId), peer_id: appPeersManager.getOutputPeer(peerId), - pFlags: pFlags, - date: tsNow(true) + serverTimeManager.serverTimeOffset, + pFlags, + date: options.scheduleDate || (tsNow(true) + serverTimeManager.serverTimeOffset), message: text, random_id: randomIdS, reply_to: {reply_to_msg_id: replyToMsgId}, @@ -493,7 +506,9 @@ export class AppMessagesManager { random_id: randomIdS, reply_to_msg_id: replyToMsgId || undefined, entities: sendEntites, - clear_draft: options.clearDraft + clear_draft: options.clearDraft, + schedule_date: options.scheduleDate || undefined, + silent: options.silent }, sentRequestOptions); } @@ -504,6 +519,8 @@ export class AppMessagesManager { message.id = updates.id; message.media = updates.media; message.entities = updates.entities; + + // * override with new updates updates = { _: 'updates', users: [], @@ -514,9 +531,7 @@ export class AppMessagesManager { random_id: randomIdS, id: updates.id }, { - _: isChannel - ? 'updateNewChannelMessage' - : 'updateNewMessage', + _: options.scheduleDate ? 'updateNewScheduledMessage' : (isChannel ? 'updateNewChannelMessage' : 'updateNewMessage'), message: message, pts: updates.pts, pts_count: updates.pts_count @@ -549,20 +564,7 @@ export class AppMessagesManager { this.pendingAfterMsgs[peerId] = sentRequestOptions; } - this.saveMessages([message]); - historyStorage.pending.unshift(messageId); - rootScope.broadcast('history_append', {peerId, messageId, my: true}); - - setTimeout(() => message.send(), 0); - // setTimeout(function () { - // message.send() - // }, 5000) - - /* if(options.clearDraft) { // WARNING - DraftsManager.clearDraft(peerId) - } */ - - this.pendingByRandomId[randomIdS] = [peerId, messageId]; + this.beforeMessageSending(message, {isScheduled: !!options.scheduleDate || undefined}); } public sendFile(peerId: number, file: File | Blob | MyDocument, options: Partial<{ @@ -579,13 +581,15 @@ export class AppMessagesManager { objectURL: string, duration: number, background: true, + silent: true, + scheduleDate: number, waveform: Uint8Array }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; + //this.checkSendOptions(options); const messageId = this.tempId--; const randomIdS = randomLong(); - const historyStorage = this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: [], pending: []}); const pFlags: any = {}; const replyToMsgId = options.replyToMsgId; const isChannel = appPeersManager.isChannel(peerId); @@ -598,7 +602,7 @@ export class AppMessagesManager { const isDocument = !(file instanceof File) && !(file instanceof Blob); let caption = options.caption || ''; - const date = tsNow(true) + serverTimeManager.serverTimeOffset; + const date = options.scheduleDate || (tsNow(true) + serverTimeManager.serverTimeOffset); this.log('sendFile', file, fileType); @@ -860,7 +864,7 @@ export class AppMessagesManager { deferred.resolve(); sentDeferred.reject(err); - this.cancelPendingMessage(peerId, randomIdS); + this.cancelPendingMessage(randomIdS); this.setTyping(peerId, 'sendMessageCancelAction'); } }); @@ -874,24 +878,21 @@ export class AppMessagesManager { return sentDeferred; }; - historyStorage.pending.unshift(messageId); - this.pendingByRandomId[randomIdS] = [peerId, messageId]; + this.beforeMessageSending(message, {isGroupedItem: options.isGroupedItem, isScheduled: !!options.scheduleDate || undefined}); if(!options.isGroupedItem) { - this.saveMessages([message]); - rootScope.broadcast('history_append', {peerId, messageId, my: true}); - setTimeout(message.send, 0); sentDeferred.then(inputMedia => { this.setTyping(peerId, 'sendMessageCancelAction'); return apiManager.invokeApi('messages.sendMedia', { background: options.background, - clear_draft: true, peer: appPeersManager.getInputPeerById(peerId), media: inputMedia, message: caption, random_id: randomIdS, - reply_to_msg_id: replyToMsgId + reply_to_msg_id: replyToMsgId, + schedule_date: options.scheduleDate, + silent: options.silent }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, (error) => { @@ -923,8 +924,12 @@ export class AppMessagesManager { width: number, height: number, objectURL: string, - }>[] + }>[], + silent: true, + scheduleDate: number }> = {}) { + //this.checkSendOptions(options); + if(files.length === 1) { return this.sendFile(peerId, files[0], {...options, ...options.sendFileDetails[0]}); } @@ -946,6 +951,8 @@ export class AppMessagesManager { const o: any = { isGroupedItem: true, isMedia: options.isMedia, + scheduleDate: options.scheduleDate, + silent: options.silent, ...details }; @@ -962,9 +969,15 @@ export class AppMessagesManager { messages.forEach(message => { message.grouped_id = groupId; }); - this.saveMessages(messages); - rootScope.broadcast('history_append', {peerId, messageId: groupId, my: true}); + const storage = options.scheduleDate ? this.getScheduledMessagesStorage(peerId) : this.getMessagesStorage(peerId); + if(options.scheduleDate) { + this.saveMessages(messages, {storage, isScheduled: true}); + rootScope.broadcast('scheduled_new', {peerId, mid: groupId}); + } else { + this.saveMessages(messages, {storage}); + rootScope.broadcast('history_append', {peerId, messageId: groupId, my: true}); + } //return; @@ -985,7 +998,9 @@ export class AppMessagesManager { return apiManager.invokeApi('messages.sendMultiMedia', { peer: inputPeer, multi_media: multiMedia, - reply_to_msg_id: replyToMsgId + reply_to_msg_id: replyToMsgId, + schedule_date: options.scheduleDate, + silent: options.silent }).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, (error) => { @@ -1038,13 +1053,15 @@ export class AppMessagesManager { reply_markup: any, clearDraft: true, queryId: string - resultId: string + resultId: string, + scheduleDate: number, + silent: true }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; + //this.checkSendOptions(options); const messageId = this.tempId--; const randomIdS = randomLong(); - const historyStorage = this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: [], pending: []}); const replyToMsgId = options.replyToMsgId; const isChannel = appPeersManager.isChannel(peerId); const isMegagroup = isChannel && appPeersManager.isMegagroup(peerId); @@ -1150,10 +1167,10 @@ export class AppMessagesManager { id: messageId, from_id: appPeersManager.getOutputPeer(fromId), peer_id: appPeersManager.getOutputPeer(peerId), - pFlags: pFlags, - date: tsNow(true) + serverTimeManager.serverTimeOffset, + pFlags, + date: options.scheduleDate || (tsNow(true) + serverTimeManager.serverTimeOffset), message: '', - media: media, + media, random_id: randomIdS, reply_to: {reply_to_msg_id: replyToMsgId}, via_bot_id: options.viaBotId, @@ -1201,7 +1218,9 @@ export class AppMessagesManager { random_id: randomIdS, reply_to_msg_id: replyToMsgId || undefined, message: '', - clear_draft: options.clearDraft + clear_draft: options.clearDraft, + schedule_date: options.scheduleDate, + silent: options.silent }, sentRequestOptions); } @@ -1225,28 +1244,56 @@ export class AppMessagesManager { this.pendingAfterMsgs[peerId] = sentRequestOptions; } - this.saveMessages([message]); - historyStorage.pending.unshift(messageId); - rootScope.broadcast('history_append', {peerId, messageId, my: true}); + this.beforeMessageSending(message, {isScheduled: !!options.scheduleDate || undefined}); + } + + /* private checkSendOptions(options: Partial<{ + scheduleDate: number + }>) { + if(options.scheduleDate) { + const minTimestamp = (Date.now() / 1000 | 0) + 10; + if(options.scheduleDate <= minTimestamp) { + delete options.scheduleDate; + } + } + } */ - setTimeout(message.send, 0); + private beforeMessageSending(message: any, options: Partial<{isGroupedItem: true, isScheduled: true}> = {}) { + const messageId = message.id; + const peerId = this.getMessagePeer(message); + const storage = options.isScheduled ? this.getScheduledMessagesStorage(peerId) : this.getMessagesStorage(peerId); - /* if(options.clearDraft) { - DraftsManager.clearDraft(peerId) - } */ + if(options.isScheduled) { + if(!options.isGroupedItem) { + this.saveMessages([message], {storage, isScheduled: true}); + rootScope.broadcast('scheduled_new', {peerId, mid: messageId}); + } + } else { + const historyStorage = this.getHistoryStorage(peerId); + historyStorage.pending.unshift(messageId); + + if(!options.isGroupedItem) { + this.saveMessages([message], {storage}); + rootScope.broadcast('history_append', {peerId, messageId, my: true}); + } + } + + this.pendingByRandomId[message.random_id] = {peerId, tempId: messageId, storage}; - this.pendingByRandomId[randomIdS] = [peerId, messageId]; + if(!options.isGroupedItem) { + setTimeout(message.send, 0); + //setTimeout(message.send, 4000); + } } - public cancelPendingMessage(peerId: number, randomId: string) { + public cancelPendingMessage(randomId: string) { const pendingData = this.pendingByRandomId[randomId]; this.log('cancelPendingMessage', randomId, pendingData); if(pendingData) { - const peerId = pendingData[0]; - const tempId = pendingData[1]; - const historyStorage = this.historiesStorage[peerId]; + const {peerId, tempId, storage} = pendingData; + const historyStorage = this.getHistoryStorage(peerId); const pos = historyStorage.pending.indexOf(tempId); apiUpdatesManager.processUpdateMessage({ @@ -1257,11 +1304,11 @@ export class AppMessagesManager { } }); - if(pos != -1) { + if(pos !== -1) { historyStorage.pending.splice(pos, 1); } - const storage = this.getMessagesStorage(peerId); + delete this.pendingByRandomId[randomId]; delete storage[tempId]; return true; @@ -1473,7 +1520,9 @@ export class AppMessagesManager { } public forwardMessages(peerId: number, fromPeerId: number, msgIds: number[], options: Partial<{ - withMyScore: true + withMyScore: true, + silent: true, + scheduleDate: number }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; msgIds = msgIds.slice().sort((a, b) => a - b); @@ -1490,7 +1539,9 @@ export class AppMessagesManager { id: msgIds, random_id: randomIds, to_peer: appPeersManager.getInputPeerById(peerId), - with_my_score: options.withMyScore + with_my_score: options.withMyScore, + silent: options.silent, + schedule_date: options.scheduleDate }, sentRequestOptions).then((updates) => { apiUpdatesManager.processUpdateMessage(updates); }, () => {}).then(() => { @@ -1522,8 +1573,10 @@ export class AppMessagesManager { continue; } - const storage = this.messagesStorageByPeerId[peerId]; - return this.getMessageFromStorage(storage, messageId); + const message = this.messagesStorageByPeerId[peerId][messageId]; + if(message) { + return message; + } } return this.getMessageFromStorage(null, messageId); @@ -1596,12 +1649,12 @@ export class AppMessagesManager { public async flushHistory(peerId: number, justClear?: true) { if(appPeersManager.isChannel(peerId)) { - let promise = this.getHistory(peerId, 0, 1); + const promise = this.getHistory(peerId, 0, 1); - let historyResult = promise instanceof Promise ? await promise : promise; + const historyResult = promise instanceof Promise ? await promise : promise; - let channelId = -peerId; - let maxId = historyResult.history[0] || 0; + const channelId = -peerId; + const maxId = historyResult.history[0] || 0; return apiManager.invokeApi('channels.deleteHistory', { channel: appChatsManager.getChannelInput(channelId), max_id: maxId @@ -1651,7 +1704,7 @@ export class AppMessagesManager { return p.promise = this.getSearch(peerId, '', {_: 'inputMessagesFilterPinned'}, 0, 1).then(result => { p.count = result.count; - p.maxId = result.history[0]; + p.maxId = result.history[0]?.mid; return p; }).finally(() => { delete p.promise; @@ -1730,10 +1783,9 @@ export class AppMessagesManager { //return Object.keys(this.groupedMessagesStorage[grouped_id]).map(id => +id).sort((a, b) => a - b); } - public getMidsByMid(peerId: number, mid: number) { - const message = this.getMessageByPeer(peerId, mid); + public getMidsByMessage(message: any) { if(message?.grouped_id) return this.getMidsByAlbum(message.grouped_id); - else return [mid]; + else return [message.mid]; } public saveMessages(messages: any[], options: Partial<{ @@ -1784,7 +1836,10 @@ export class AppMessagesManager { message.reply_to_mid = message.reply_to.reply_to_msg_id; } - message.date -= serverTimeManager.serverTimeOffset; + const overwriting = !!message.peerId; + if(!overwriting) { + message.date -= serverTimeManager.serverTimeOffset; + } const myId = appUsersManager.getSelf().id; @@ -1813,7 +1868,9 @@ export class AppMessagesManager { message.fwdFromId = appPeersManager.getPeerId(fwdHeader.from_id); - fwdHeader.date -= serverTimeManager.serverTimeOffset; + if(!overwriting) { + fwdHeader.date -= serverTimeManager.serverTimeOffset; + } } if(message.via_bot_id > 0) { @@ -1950,7 +2007,7 @@ export class AppMessagesManager { if(message.grouped_id) { if(!groups) { - groups = new Map(); + groups = new Map(); } groups.set(peerId, message.grouped_id); @@ -2243,13 +2300,7 @@ export class AppMessagesManager { return true; } - public canEditMessage(peerId: number, messageId: number, kind: 'text' | 'poll' = 'text') { - const storage = this.getMessagesStorage(peerId); - if(!storage[messageId]) { - return false; - } - - const message = storage[messageId]; + public canEditMessage(message: any, kind: 'text' | 'poll' = 'text') { if(!message || !this.canMessageBeEdited(message, kind)) { return false; } @@ -2266,8 +2317,7 @@ export class AppMessagesManager { return true; } - public canDeleteMessage(peerId: number, messageId: number) { - const message = this.getMessagesStorage(peerId)[messageId]; + public canDeleteMessage(message: any) { return message && ( message.peerId > 0 || message.fromId == rootScope.myId @@ -2421,9 +2471,8 @@ export class AppMessagesManager { else delete message.pFlags.unread; } - let historyStorage = this.historiesStorage[peerId]; + let historyStorage = this.getHistoryStorage(peerId); if(historyStorage === undefined/* && !message.deleted */) { // warning - historyStorage = this.historiesStorage[peerId] = {count: null, history: [], pending: []}; historyStorage[mid > 0 ? 'history' : 'pending'].push(mid); /* if(mid < 0 && message.pFlags.unread) { dialog.unread_count++; @@ -2536,9 +2585,9 @@ export class AppMessagesManager { count: number, next_rate: number, offset_id_offset: number, - history: number[] + history: MyMessage[] }> { - const foundMsgs: number[] = []; + const foundMsgs: any[] = []; //this.log('search', maxId); @@ -2558,7 +2607,7 @@ export class AppMessagesManager { if(peerId && !backLimit && !maxId && !query && limit !== 1/* && inputFilter._ !== 'inputMessagesFilterPinned' */) { storage = beta ? this.getSearchStorage(peerId, inputFilter._) : - this.historiesStorage[peerId]; + this.getHistoryStorage(peerId); let filtering = true; const history = maxId ? storage.history.slice(storage.history.indexOf(maxId) + 1) : storage.history; @@ -2667,7 +2716,7 @@ export class AppMessagesManager { } */ if(found) { - foundMsgs.push(message.mid); + foundMsgs.push(message); if(foundMsgs.length >= limit) { break; } @@ -2769,7 +2818,7 @@ export class AppMessagesManager { } } - foundMsgs.push(message.mid); + foundMsgs.push(message); }); return { @@ -2885,11 +2934,11 @@ export class AppMessagesManager { public readHistory(peerId: number, maxId = 0) { // console.trace('start read') const isChannel = appPeersManager.isChannel(peerId); - const historyStorage = this.historiesStorage[peerId]; + const historyStorage = this.getHistoryStorage(peerId); const foundDialog = this.getDialogByPeerId(peerId)[0]; if(!foundDialog || !foundDialog.unread_count) { - if(!historyStorage || !historyStorage.history.length) { + if(!historyStorage.history.length) { return Promise.resolve(false); } @@ -3001,32 +3050,34 @@ export class AppMessagesManager { } } + public getHistoryStorage(peerId: number) { + return this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: [], pending: []}); + } + public handleUpdate(update: Update) { - this.log.debug('handleUpdate', update._); + this.log.debug('handleUpdate', update._, update); switch(update._) { case 'updateMessageID': { const randomId = update.random_id; const pendingData = this.pendingByRandomId[randomId]; //this.log('AMM updateMessageID:', update, pendingData); if(pendingData) { - const peerId: number = pendingData[0]; - const tempId = pendingData[1]; + const {peerId, tempId, storage} = pendingData; const mid = update.id; - const storage = this.getMessagesStorage(peerId); - const message = storage[mid]; - if(message) { - const historyStorage = this.historiesStorage[peerId]; + const message = this.getMessageFromStorage(storage, mid); + if(!message.deleted) { + const historyStorage = this.getHistoryStorage(peerId); const pos = historyStorage.pending.indexOf(tempId); - if(pos != -1) { + if(pos !== -1) { historyStorage.pending.splice(pos, 1); } - delete storage[tempId]; - this.finalizePendingMessageCallbacks(peerId, tempId, mid); + this.finalizePendingMessageCallbacks(storage, tempId, mid); } else { this.pendingByMessageId[mid] = randomId; } } + break; } @@ -3034,6 +3085,7 @@ export class AppMessagesManager { case 'updateNewChannelMessage': { const message = update.message as MyMessage; const peerId = this.getMessagePeer(message); + const storage = this.getMessagesStorage(peerId); const foundDialog = this.getDialogByPeerId(peerId); if(!foundDialog.length) { @@ -3053,17 +3105,13 @@ export class AppMessagesManager { } } - this.saveMessages([message]); + this.saveMessages([message], {storage}); // this.log.warn(dT(), 'message unread', message.mid, message.pFlags.unread) - let historyStorage = this.historiesStorage[peerId]; - if(historyStorage === undefined) { - historyStorage = this.historiesStorage[peerId] = { - count: null, - history: [], - pending: [] - }; - } + const dialog = foundDialog[0]; + const pendingMessage = this.checkPendingMessage(message); + + const historyStorage = this.getHistoryStorage(peerId); const history = message.mid > 0 ? historyStorage.history : historyStorage.pending; if(history.indexOf(message.mid) != -1) { @@ -3076,36 +3124,24 @@ export class AppMessagesManager { return b - a; }); } - - if(message.mid > 0 && - historyStorage.count !== null) { + + if(message.mid > 0 && historyStorage.count !== null) { historyStorage.count++; } - + if(this.mergeReplyKeyboard(historyStorage, message)) { rootScope.broadcast('history_reply_markup', {peerId}); } - + if(!message.pFlags.out && message.from_id) { appUsersManager.forceUserOnline(appPeersManager.getPeerId(message.from_id), message.date); } - const randomId = this.pendingByMessageId[message.mid]; - let pendingMessage: any; - - if(randomId) { - if(pendingMessage = this.finalizePendingMessage(peerId, randomId, message)) { - rootScope.broadcast('history_update', {peerId, mid: message.mid}); - } - - delete this.pendingByMessageId[message.mid]; - } - if(!pendingMessage) { if(this.newMessagesToHandle[peerId] === undefined) { this.newMessagesToHandle[peerId] = []; } - + this.newMessagesToHandle[peerId].push(message.mid); if(!this.newMessagesHandlePromise) { this.newMessagesHandlePromise = window.setTimeout(this.handleNewMessages, 0); @@ -3113,7 +3149,6 @@ export class AppMessagesManager { } const inboxUnread = !message.pFlags.out && message.pFlags.unread; - const dialog = foundDialog[0]; dialog.top_message = message.mid; if(inboxUnread) { dialog.unread_count++; @@ -3301,26 +3336,26 @@ export class AppMessagesManager { break; } - const oldMessage = storage[mid]; - if(oldMessage.media?.webpage) { - appWebPagesManager.deleteWebPageFromPending(oldMessage.media.webpage, mid); - } - // console.trace(dT(), 'edit message', message) - this.saveMessages([message]); + + const oldMessage = this.getMessageFromStorage(storage, mid); + this.saveMessages([message], {storage}); + const newMessage = this.getMessageFromStorage(storage, mid); + + this.handleEditedMessage(oldMessage, newMessage); const dialog = this.getDialogByPeerId(peerId)[0]; const isTopMessage = dialog && dialog.top_message == mid; // @ts-ignore if(message.clear_history) { // that's will never happen if(isTopMessage) { - rootScope.broadcast('dialog_flush', {peerId: peerId}); + rootScope.broadcast('dialog_flush', {peerId}); } } else { rootScope.broadcast('message_edit', { + storage, peerId, - mid, - justMedia: false + mid }); const groupId = (message as Message.message).grouped_id; @@ -3387,7 +3422,7 @@ export class AppMessagesManager { } // this.log.warn('read', messageId, message.pFlags.unread, message) - if(message && message.pFlags.unread) { + if(message.pFlags.unread) { delete message.pFlags.unread; if(!foundAffected) { foundAffected = true; @@ -3429,7 +3464,7 @@ export class AppMessagesManager { const messages: number[] = update.messages; for(const messageId of messages) { const message = this.getMessageByPeer(peerId, messageId); - if(message) { + if(!message.deleted) { delete message.pFlags.media_unread; } } @@ -3442,7 +3477,7 @@ export class AppMessagesManager { const channelId: number = update.channel_id; const messages: number[] = []; const peerId: number = -channelId; - const history = (this.historiesStorage[peerId] || {}).history || []; + const history = this.getHistoryStorage(peerId).history; if(history.length) { history.forEach((msgId: number) => { if(!update.available_min_id || msgId <= update.available_min_id) { @@ -3456,129 +3491,50 @@ export class AppMessagesManager { case 'updateDeleteMessages': case 'updateDeleteChannelMessages': { - const historiesUpdated: { - [peerId: number]: { - count: number, - unread: number, - msgs: {[mid: number]: true}, - albums?: {[groupId: string]: Set}, - } - } = {}; const channelId: number = (update as Update.updateDeleteChannelMessages).channel_id; const messages = (update as any as Update.updateDeleteChannelMessages).messages; - - for(const _messageId of messages) { - const mid = _messageId; - const message: MyMessage = this.getMessageByPeer(-channelId, mid); - if(message) { - const peerId = this.getMessagePeer(message); - const history = historiesUpdated[peerId] || (historiesUpdated[peerId] = {count: 0, unread: 0, msgs: {}}); - - if((message as Message.message).media) { - // @ts-ignore - const c = message.media.webpage || message.media; - const smth = c.photo || c.document; - - if(smth?.file_reference) { - referenceDatabase.deleteContext(smth.file_reference, {type: 'message', peerId, messageId: mid}); - } - - // @ts-ignore - if(message.media.webpage) { - // @ts-ignore - appWebPagesManager.deleteWebPageFromPending(message.media.webpage, mid); - } - } - - if(!message.pFlags.out && message.pFlags.unread) { - history.unread++; - } - history.count++; - history.msgs[mid] = true; - - message.deleted = true; - - delete this.getMessagesStorage(peerId)[mid]; - - if(message._ != 'messageService' && message.grouped_id) { - const groupedStorage = this.groupedMessagesStorage[message.grouped_id]; - if(groupedStorage) { - delete groupedStorage[mid]; - - if(!history.albums) history.albums = {}; - (history.albums[message.grouped_id] || (history.albums[message.grouped_id] = new Set())).add(mid); - - if(!Object.keys(groupedStorage).length) { - delete history.albums; - delete this.groupedMessagesStorage[message.grouped_id]; - } - } - } - - /* if(this.pinnedMessagesStorage[peerId] == mid) { - this.savePinnedMessage(peerId, 0); - } */ - - const peerMessagesToHandle = this.newMessagesToHandle[peerId]; - if(peerMessagesToHandle && peerMessagesToHandle.length) { - const peerMessagesHandlePos = peerMessagesToHandle.indexOf(mid); - if(peerMessagesHandlePos != -1) { - peerMessagesToHandle.splice(peerMessagesHandlePos); - } - } - } + const peerId = channelId ? -channelId : this.getMessageById(messages[0]).peerId; + + if(!peerId) { + break; } - - Object.keys(historiesUpdated).forEach(_peerId => { - const peerId = +_peerId; - const updatedData = historiesUpdated[peerId]; - - if(updatedData.albums) { - for(const groupId in updatedData.albums) { - rootScope.broadcast('album_edit', {peerId, groupId, deletedMids: [...updatedData.albums[groupId]]}); - /* const mids = this.getMidsByAlbum(groupId); - if(mids.length) { - const mid = Math.max(...mids); - rootScope.$broadcast('message_edit', {peerId, mid, justMedia: false}); - } */ + + const historyUpdated = this.handleDeletedMessages(peerId, this.getMessagesStorage(peerId), messages); + + const historyStorage = this.getHistoryStorage(peerId); + //if(historyStorage !== undefined) { + const newHistory = historyStorage.history.filter(mid => !historyUpdated.msgs[mid]); + const newPending = historyStorage.pending.filter(mid => !historyUpdated.msgs[mid]); + historyStorage.history = newHistory; + if(historyUpdated.count && + historyStorage.count !== null && + historyStorage.count > 0) { + historyStorage.count -= historyUpdated.count; + if(historyStorage.count < 0) { + historyStorage.count = 0; } } - const historyStorage = this.historiesStorage[peerId]; - if(historyStorage !== undefined) { - const newHistory = historyStorage.history.filter(mid => !updatedData.msgs[mid]); - const newPending = historyStorage.pending.filter(mid => !updatedData.msgs[mid]); - historyStorage.history = newHistory; - if(updatedData.count && - historyStorage.count !== null && - historyStorage.count > 0) { - historyStorage.count -= updatedData.count; - if(historyStorage.count < 0) { - historyStorage.count = 0; - } - } - - historyStorage.pending = newPending; + historyStorage.pending = newPending; - rootScope.broadcast('history_delete', {peerId, msgs: updatedData.msgs}); - } + rootScope.broadcast('history_delete', {peerId, msgs: historyUpdated.msgs}); + //} - const foundDialog = this.getDialogByPeerId(peerId)[0]; - if(foundDialog) { - if(updatedData.unread) { - foundDialog.unread_count -= updatedData.unread; + const foundDialog = this.getDialogByPeerId(peerId)[0]; + if(foundDialog) { + if(historyUpdated.unread) { + foundDialog.unread_count -= historyUpdated.unread; - rootScope.broadcast('dialog_unread', { - peerId, - count: foundDialog.unread_count - }); - } + rootScope.broadcast('dialog_unread', { + peerId, + count: foundDialog.unread_count + }); + } - if(updatedData.msgs[foundDialog.top_message]) { - this.reloadConversation(peerId); - } + if(historyUpdated.msgs[foundDialog.top_message]) { + this.reloadConversation(peerId); } - }); + } break; } @@ -3633,7 +3589,7 @@ export class AppMessagesManager { const views = update.views; const mid = update.id; const message = this.getMessageByPeer(-update.channel_id, mid); - if(message && message.views && message.views < views) { + if(!message.deleted && message.views && message.views < views) { message.views = views; rootScope.broadcast('message_views', {mid, views}); } @@ -3747,17 +3703,27 @@ export class AppMessagesManager { break; } - // * https://core.telegram.org/api/scheduled-messages case 'updateNewScheduledMessage': { - const message = update.message as Message.message; - + const message = update.message as MyMessage; const peerId = this.getMessagePeer(message); + const storage = this.scheduledMessagesStorage[peerId]; if(storage) { - this.saveMessages([message]); - storage.unshift(message.mid); - - rootScope.broadcast('scheduled_new', {peerId, mid: message.mid}); + const mid = message.id; + + const oldMessage = this.getMessageFromStorage(storage, mid); + this.saveMessages([message], {storage, isScheduled: true}); + const newMessage = this.getMessageFromStorage(storage, mid); + + if(!oldMessage.deleted) { + this.handleEditedMessage(oldMessage, newMessage); + rootScope.broadcast('message_edit', {storage, peerId, mid: message.mid}); + } else { + const pendingMessage = this.checkPendingMessage(message); + if(!pendingMessage) { + rootScope.broadcast('scheduled_new', {peerId, mid: message.mid}); + } + } } break; @@ -3768,9 +3734,7 @@ export class AppMessagesManager { const storage = this.scheduledMessagesStorage[peerId]; if(storage) { - for(const mid of update.messages) { - delete storage[mid]; - } + this.handleDeletedMessages(peerId, storage, update.messages); rootScope.broadcast('scheduled_delete', {peerId, mids: update.messages}); } @@ -3780,6 +3744,21 @@ export class AppMessagesManager { } } + private checkPendingMessage(message: any) { + const randomId = this.pendingByMessageId[message.mid]; + let pendingMessage: any; + if(randomId) { + const pendingData = this.pendingByRandomId[randomId]; + if(pendingMessage = this.finalizePendingMessage(randomId, message)) { + rootScope.broadcast('history_update', {storage: pendingData.storage, peerId: message.peerId, mid: message.mid}); + } + + delete this.pendingByMessageId[message.mid]; + } + + return pendingMessage; + } + public isPeerMuted(peerId: number) { if(peerId == rootScope.myId) return false; @@ -3840,24 +3819,22 @@ export class AppMessagesManager { return (!isChannel || hasRights) && (peerId < 0 || appUsersManager.canSendToUser(peerId)); } - public finalizePendingMessage(peerId: number, randomId: number, finalMessage: any) { + public finalizePendingMessage(randomId: string, finalMessage: any) { const pendingData = this.pendingByRandomId[randomId]; // this.log('pdata', randomID, pendingData) if(pendingData) { - const peerId = pendingData[0]; - const tempId = pendingData[1]; - const historyStorage = this.historiesStorage[peerId]; + const {peerId, tempId, storage} = pendingData; + const historyStorage = this.getHistoryStorage(peerId); // this.log('pending', randomID, historyStorage.pending) const pos = historyStorage.pending.indexOf(tempId); - if(pos != -1) { + if(pos !== -1) { historyStorage.pending.splice(pos, 1); } - const storage = this.getMessagesStorage(peerId); - const message = storage[tempId]; - if(message) { + const message = this.getMessageFromStorage(storage, tempId); + if(!message.deleted) { delete message.pending; delete message.error; delete message.random_id; @@ -3865,10 +3842,10 @@ export class AppMessagesManager { rootScope.broadcast('messages_pending'); } + + delete this.pendingByRandomId[randomId]; - delete storage[tempId]; - - this.finalizePendingMessageCallbacks(peerId, tempId, finalMessage.mid); + this.finalizePendingMessageCallbacks(storage, tempId, finalMessage.mid); return message; } @@ -3876,21 +3853,21 @@ export class AppMessagesManager { return false; } - public finalizePendingMessageCallbacks(peerId: number, tempId: number, mid: number) { + public finalizePendingMessageCallbacks(storage: MessagesStorage, tempId: number, mid: number) { + const message = this.getMessageFromStorage(storage, mid); const callbacks = this.tempFinalizeCallbacks[tempId]; this.log.warn(callbacks, tempId); if(callbacks !== undefined) { for(const name in callbacks) { const {deferred, callback} = callbacks[name]; this.log(`finalizePendingMessageCallbacks: will invoke ${name} callback`); - callback(mid).then(deferred.resolve, deferred.reject); + callback(message).then(deferred.resolve, deferred.reject); } delete this.tempFinalizeCallbacks[tempId]; } // set cached url to media - const message = this.getMessageByPeer(peerId, mid); if(message.media) { if(message.media.photo) { const photo = appPhotosManager.getPhoto('' + tempId); @@ -3914,7 +3891,10 @@ export class AppMessagesManager { } } - rootScope.broadcast('message_sent', {tempId, mid}); + const tempMessage = this.getMessageFromStorage(storage, tempId); + delete storage[tempId]; + + rootScope.broadcast('message_sent', {storage, tempId, tempMessage, mid}); } public incrementMaxSeenId(maxId: number) { @@ -3938,8 +3918,8 @@ export class AppMessagesManager { public getScheduledMessages(peerId: number): Promise { if(!this.canWriteToPeer(peerId)) return Promise.resolve([]); - const storage = this.scheduledMessagesStorage[peerId]; - if(storage?.length) { + const storage = this.getScheduledMessagesStorage(peerId); + if(Object.keys(storage).length) { return Promise.resolve(Object.keys(storage).map(id => +id)); } @@ -3960,12 +3940,30 @@ export class AppMessagesManager { }); } + public sendScheduledMessages(peerId: number, mids: number[]) { + return apiManager.invokeApi('messages.sendScheduledMessages', { + peer: appPeersManager.getInputPeerById(peerId), + id: mids + }).then(updates => { + apiUpdatesManager.processUpdateMessage(updates); + }); + } + + public deleteScheduledMessages(peerId: number, mids: number[]) { + return apiManager.invokeApi('messages.deleteScheduledMessages', { + peer: appPeersManager.getInputPeerById(peerId), + id: mids + }).then(updates => { + apiUpdatesManager.processUpdateMessage(updates); + }); + } + public getHistory(peerId: number, maxId = 0, limit: number, backLimit?: number) { if(this.migratedFromTo[peerId]) { peerId = this.migratedFromTo[peerId]; } - const historyStorage = this.historiesStorage[peerId] ?? (this.historiesStorage[peerId] = {count: null, history: [], pending: []}); + const historyStorage = this.getHistoryStorage(peerId); const unreadOffset = 0; const unreadSkip = false; @@ -4026,7 +4024,7 @@ export class AppMessagesManager { } return this.requestHistory(reqPeerId, maxId, limit, offset).then((historyResult) => { - historyStorage.count = historyResult.count || historyResult.messages.length; + historyStorage.count = (historyResult as MessagesMessages.messagesMessagesSlice).count || historyResult.messages.length; if(isMigrated) { historyStorage.count++; } @@ -4076,11 +4074,11 @@ export class AppMessagesManager { public fillHistoryStorage(peerId: number, maxId: number, fullLimit: number, historyStorage: HistoryStorage): Promise { // this.log('fill history storage', peerId, maxId, fullLimit, angular.copy(historyStorage)) const offset = (this.migratedFromTo[peerId] && !maxId) ? 1 : 0; - return this.requestHistory(peerId, maxId, fullLimit, offset).then((historyResult: any) => { - historyStorage.count = historyResult.count || historyResult.messages.length; + return this.requestHistory(peerId, maxId, fullLimit, offset).then((historyResult) => { + historyStorage.count = (historyResult as MessagesMessages.messagesMessagesSlice).count || historyResult.messages.length; if(!maxId && historyResult.messages.length) { - maxId = historyResult.messages[0].mid + 1; + maxId = (historyResult.messages[0] as MyMessage).mid + 1; } let offset = 0; @@ -4095,12 +4093,12 @@ export class AppMessagesManager { const wasTotalCount = historyStorage.history.length; historyStorage.history.splice(offset, historyStorage.history.length - offset); - historyResult.messages.forEach((message: any) => { + historyResult.messages.forEach((message) => { if(this.mergeReplyKeyboard(historyStorage, message)) { rootScope.broadcast('history_reply_markup', {peerId}); } - historyStorage.history.push(message.mid); + historyStorage.history.push((message as MyMessage).mid); }); const totalCount = historyStorage.history.length; @@ -4140,8 +4138,8 @@ export class AppMessagesManager { public wrapHistoryResult(peerId: number, result: HistoryResult) { if(result.unreadOffset) { for(let i = result.history.length - 1; i >= 0; i--) { - const message = this.getMessagesStorage(peerId)[result.history[i]]; - if(message && !message.pFlags.out && message.pFlags.unread) { + const message = this.getMessageByPeer(peerId, result.history[i]); + if(!message.deleted && !message.pFlags.out && message.pFlags.unread) { result.unreadOffset = i + 1; break; } @@ -4150,14 +4148,14 @@ export class AppMessagesManager { return result; } - public requestHistory(peerId: number, maxId: number, limit = 0, offset = 0, offsetDate = 0): Promise { + public requestHistory(peerId: number, maxId: number, limit = 0, offset = 0, offsetDate = 0): Promise> { const isChannel = appPeersManager.isChannel(peerId); //console.trace('requestHistory', peerId, maxId, limit, offset); - rootScope.broadcast('history_request'); - - return apiManager.invokeApi('messages.getHistory', { + //rootScope.broadcast('history_request'); + + const promise = apiManager.invokeApi('messages.getHistory', { peer: appPeersManager.getInputPeerById(peerId), offset_id: maxId || 0, offset_date: offsetDate, @@ -4169,12 +4167,14 @@ export class AppMessagesManager { }, { //timeout: APITIMEOUT, noErrorBox: true - }).then((historyResult) => { + }) as ReturnType; + + return promise.then((historyResult) => { this.log('requestHistory result:', peerId, historyResult, maxId, limit, offset); - if(historyResult._ == 'messages.messagesNotModified') { - return historyResult; - } + /* if(historyResult._ == 'messages.messagesNotModified') { + return historyResult as any; + } */ appUsersManager.saveApiUsers(historyResult.users); appChatsManager.saveApiChats(historyResult.chats); @@ -4192,7 +4192,7 @@ export class AppMessagesManager { } // will load more history if last message is album grouped (because it can be not last item) - const historyStorage = this.historiesStorage[peerId]; + const historyStorage = this.getHistoryStorage(peerId); // historyResult.messages: desc sorted if(length && (historyResult.messages[length - 1] as Message.message).grouped_id && (historyStorage.history.length + historyResult.messages.length) < (historyResult as MessagesMessages.messagesMessagesSlice).count) { @@ -4205,7 +4205,7 @@ export class AppMessagesManager { /* if(peerId < 0 || !appUsersManager.isBot(peerId) || (length == limit && limit < historyResult.count)) { return historyResult; } */ - return historyResult; + return historyResult as any; /* return appProfileManager.getProfile(peerId).then((userFull: any) => { var description = userFull.bot_info && userFull.bot_info.description; @@ -4327,6 +4327,88 @@ export class AppMessagesManager { action }) as Promise; } + + private handleDeletedMessages(peerId: number, storage: MessagesStorage, messages: number[]) { + const history: { + count: number, + unread: number, + msgs: {[mid: number]: true}, + albums?: {[groupId: string]: Set}, + } = {count: 0, unread: 0, msgs: {}} as any; + + for(const mid of messages) { + const message: MyMessage = this.getMessageFromStorage(storage, mid); + if(message.deleted) continue; + + if((message as Message.message).media) { + // @ts-ignore + const c = message.media.webpage || message.media; + const smth = c.photo || c.document; + + if(smth?.file_reference) { + referenceDatabase.deleteContext(smth.file_reference, {type: 'message', peerId, messageId: mid}); + } + + // @ts-ignore + if(message.media.webpage) { + // @ts-ignore + appWebPagesManager.deleteWebPageFromPending(message.media.webpage, mid); + } + } + + if(!message.pFlags.out && message.pFlags.unread) { + history.unread++; + } + history.count++; + history.msgs[mid] = true; + + message.deleted = true; + + if(message._ != 'messageService' && message.grouped_id) { + const groupedStorage = this.groupedMessagesStorage[message.grouped_id]; + if(groupedStorage) { + delete groupedStorage[mid]; + + if(!history.albums) history.albums = {}; + (history.albums[message.grouped_id] || (history.albums[message.grouped_id] = new Set())).add(mid); + + if(!Object.keys(groupedStorage).length) { + delete history.albums; + delete this.groupedMessagesStorage[message.grouped_id]; + } + } + } + + delete storage[mid]; + + const peerMessagesToHandle = this.newMessagesToHandle[peerId]; + if(peerMessagesToHandle && peerMessagesToHandle.length) { + const peerMessagesHandlePos = peerMessagesToHandle.indexOf(mid); + if(peerMessagesHandlePos != -1) { + peerMessagesToHandle.splice(peerMessagesHandlePos); + } + } + } + + if(history.albums) { + for(const groupId in history.albums) { + rootScope.broadcast('album_edit', {peerId, groupId, deletedMids: [...history.albums[groupId]]}); + /* const mids = this.getMidsByAlbum(groupId); + if(mids.length) { + const mid = Math.max(...mids); + rootScope.$broadcast('message_edit', {peerId, mid, justMedia: false}); + } */ + } + } + + return history; + } + + private handleEditedMessage(oldMessage: any, newMessage: any) { + if(oldMessage.media?.webpage) { + appWebPagesManager.deleteWebPageFromPending(oldMessage.media.webpage, oldMessage.mid); + } + } } const appMessagesManager = new AppMessagesManager(); diff --git a/src/lib/appManagers/appPollsManager.ts b/src/lib/appManagers/appPollsManager.ts index 556b5ba6..8054c555 100644 --- a/src/lib/appManagers/appPollsManager.ts +++ b/src/lib/appManagers/appPollsManager.ts @@ -159,20 +159,21 @@ export class AppPollsManager { }; } - public sendVote(peerId: number, messageId: number, optionIds: number[]): Promise { - const message = appMessagesManager.getMessageByPeer(peerId, messageId); + public sendVote(message: any, optionIds: number[]): Promise { const poll: Poll = message.media.poll; const options: Uint8Array[] = optionIds.map(index => { return poll.answers[index].option; }); + const messageId = message.mid; + const peerId = message.peerId; const inputPeer = appPeersManager.getInputPeerById(peerId); if(messageId < 0) { - return appMessagesManager.invokeAfterMessageIsSent(messageId, 'sendVote', (mid) => { + return appMessagesManager.invokeAfterMessageIsSent(messageId, 'sendVote', (message) => { this.log('invoke sendVote callback'); - return this.sendVote(peerId, mid, optionIds); + return this.sendVote(message, optionIds); }); } @@ -186,33 +187,22 @@ export class AppPollsManager { }); } - public getResults(peerId: number, messageId: number) { - const message = appMessagesManager.getMessageByPeer(peerId, messageId); + public getResults(message: any) { const inputPeer = appPeersManager.getInputPeerById(message.peerId); return apiManager.invokeApi('messages.getPollResults', { peer: inputPeer, - msg_id: messageId + msg_id: message.mid }).then(updates => { apiUpdatesManager.processUpdateMessage(updates); this.log('getResults updates:', updates); }); } - public getVotes(peerId: number, messageId: number, option?: Uint8Array, offset?: string, limit = 20) { - let flags = 0; - if(option) { - flags |= 1 << 0; - } - - if(offset) { - flags |= 1 << 1; - } - + public getVotes(message: any, option?: Uint8Array, offset?: string, limit = 20) { return apiManager.invokeApi('messages.getPollVotes', { - flags, - peer: appPeersManager.getInputPeerById(peerId), - id: messageId, + peer: appPeersManager.getInputPeerById(message.peerId), + id: message.mid, option, offset, limit @@ -225,15 +215,14 @@ export class AppPollsManager { }); } - public stopPoll(peerId: number, messageId: number) { - const message = appMessagesManager.getMessageByPeer(peerId, messageId); + public stopPoll(message: any) { const poll: Poll = message.media.poll; if(poll.pFlags.closed) return Promise.resolve(); const newPoll = copy(poll); newPoll.pFlags.closed = true; - return appMessagesManager.editMessage(peerId, messageId, undefined, { + return appMessagesManager.editMessage(message, undefined, { newMedia: this.getInputMediaPoll(newPoll) }).then(() => { //console.log('stopped poll'); diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 6b07b7aa..7f4cd77a 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -19,8 +19,6 @@ export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | //level = LogLevels.log | LogLevels.warn | LogLevels.error | LogLevels.debug - prefix = '[' + prefix + ']:'; - function Log(...args: any[]) { return level & LogLevels.log && console.log(dT(), prefix, ...args); } @@ -48,6 +46,12 @@ export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | Log.debug = function(...args: any[]) { return level & LogLevels.debug && console.debug(dT(), prefix, ...args); }; + + Log.setPrefix = function(_prefix: string) { + prefix = '[' + _prefix + ']:'; + }; + + Log.setPrefix(prefix); return Log; }; \ No newline at end of file diff --git a/src/lib/mtproto/apiManager.ts b/src/lib/mtproto/apiManager.ts index 775eaf11..a3ebde45 100644 --- a/src/lib/mtproto/apiManager.ts +++ b/src/lib/mtproto/apiManager.ts @@ -183,7 +183,7 @@ export class ApiManager { } const networkers = cache[dcId]; - if(networkers.length >= /* 1 */(connectionType == 'client' ? 1 : 3)) { + if(networkers.length >= /* 1 */(connectionType !== 'download' ? 1 : 3)) { const networker = networkers.pop(); networkers.unshift(networker); return Promise.resolve(networker); diff --git a/src/lib/richtextprocessor.ts b/src/lib/richtextprocessor.ts index 9ab79aaf..5a9b4abc 100644 --- a/src/lib/richtextprocessor.ts +++ b/src/lib/richtextprocessor.ts @@ -1148,7 +1148,7 @@ namespace RichTextProcessor { } export function matchUrl(text: string) { - return text.match(urlRegExp); + return !text ? null : text.match(urlRegExp); } /* const el = document.createElement('span'); diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 174e985c..abe61741 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -1,6 +1,6 @@ import type { StickerSet, Update } from "../layer"; import type { MyDocument } from "./appManagers/appDocsManager"; -import type { AppMessagesManager, Dialog } from "./appManagers/appMessagesManager"; +import type { AppMessagesManager, Dialog, MessagesStorage } from "./appManagers/appMessagesManager"; import type { Poll, PollResults } from "./appManagers/appPollsManager"; import type { MyDialogFilter } from "./storages/filters"; import type { ConnectionStatusChange } from "../types"; @@ -30,17 +30,17 @@ type BroadcastEvents = { 'dialogs_archived_unread': {count: number}, 'history_append': {peerId: number, messageId: number, my?: boolean}, - 'history_update': {peerId: number, mid: number}, + 'history_update': {storage: MessagesStorage, peerId: number, mid: number}, 'history_reply_markup': {peerId: number}, 'history_multiappend': AppMessagesManager['newMessagesToHandle'], 'history_delete': {peerId: number, msgs: {[mid: number]: true}}, 'history_forbidden': number, 'history_reload': number, - 'history_request': void, + //'history_request': void, - 'message_edit': {peerId: number, mid: number, justMedia: boolean}, + 'message_edit': {storage: MessagesStorage, peerId: number, mid: number}, 'message_views': {mid: number, views: number}, - 'message_sent': {tempId: number, mid: number}, + 'message_sent': {storage: MessagesStorage, tempId: number, tempMessage: any, mid: number}, 'messages_pending': void, 'messages_read': void, 'messages_downloaded': number[], diff --git a/src/pages/pageSignUp.ts b/src/pages/pageSignUp.ts index 299772fd..06ef2a15 100644 --- a/src/pages/pageSignUp.ts +++ b/src/pages/pageSignUp.ts @@ -1,5 +1,5 @@ import { putPreloader } from '../components/misc'; -import PopupAvatar from '../components/popupAvatar'; +import PopupAvatar from '../components/popups/avatar'; import appStateManager from '../lib/appManagers/appStateManager'; //import apiManager from '../lib/mtproto/apiManager'; import apiManager from '../lib/mtproto/mtprotoworker'; diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 7dceedb6..e3257977 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -178,12 +178,33 @@ $chat-helper-size: 39px; height: 24px; } - &.send { + .tgico-send { color: $color-blue !important; } + .tgico-check { + color: $color-blue !important; + height: 32px!important; + font-size: 2rem; + + &:before { + font-weight: bold; + } + } + + .tgico-schedule { + background-color: $color-blue; + color: #fff; + border-radius: 50%; + width: 34px; + height: 34px; + line-height: 38px; + } + &.send .tgico-send, - &.record .tgico-microphone2 { + &.record .tgico-microphone2, + &.edit .tgico-check, + &.schedule .tgico-schedule { animation: grow-icon .4s forwards ease-in-out; } } diff --git a/src/scss/partials/popups/_mediaAttacher.scss b/src/scss/partials/popups/_mediaAttacher.scss index 45803ccd..395d0db8 100644 --- a/src/scss/partials/popups/_mediaAttacher.scss +++ b/src/scss/partials/popups/_mediaAttacher.scss @@ -1,4 +1,5 @@ .popup-new-media { + user-select: none; $parent: ".popup"; #{$parent} { @@ -54,6 +55,7 @@ align-items: center; margin-bottom: 9px; padding: 12px 20px 15px; + position: relative; .btn-primary { width: 79px; @@ -148,6 +150,15 @@ font-size: inherit; } } + + .btn-menu-overlay { + z-index: 3; + } + + .menu-send { + z-index: 4; + top: calc(100% + .25rem); + } } .popup-new-media.popup-send-photo {