Refactor messages

Scheduled messages
Respect stickers sending rights
Edit icon
This commit is contained in:
Eduard Kuzmenko 2020-12-18 05:07:32 +02:00
parent 122c34fd4a
commit d76ed7320e
50 changed files with 1713 additions and 991 deletions

View File

@ -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) {

View File

@ -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;

View File

@ -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) {

View File

@ -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 = {

View File

@ -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<BubbleGroup> = []; // map to group
groups: Array<HTMLDivElement[]> = [];
private bubbles: Array<BubbleGroup> = []; // map to group
private groups: Array<Group> = [];
//updateRAFs: Map<HTMLDivElement[], number> = 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);

View File

@ -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<void>((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(() => {

View File

@ -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));
}
}

View File

@ -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);
}
};
}

View File

@ -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', `
<span class="tgico tgico-send"></span>
<span class="tgico tgico-schedule"></span>
<span class="tgico tgico-check"></span>
<span class="tgico tgico-microphone2"></span>
`);
attachContextMenuListener(this.btnSend, (e: any) => {
if(this.isInputEmpty()) {
return;
}
cancelEvent(e);
openBtnMenu(this.sendMenu);
}, this.listenerSetter);
this.btnSendContainer.append(this.recordRippleEl, this.btnSend);
this.btnSendContainer.append(this.recordRippleEl, this.btnSend, this.sendMenu);
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();
}
});
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');
}

View File

@ -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();

View File

@ -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) {

View File

@ -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";

View File

@ -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));
}

View File

@ -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';
}
};

View File

@ -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 => {

View File

@ -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 {

View File

@ -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');
class InputField {
public container: HTMLElement;
public input: HTMLElement;
if(options.maxLength) {
options.showLengthOn = Math.round(options.maxLength / 3);
}
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');
const {placeholder, label, maxLength, showLengthOn, name, plainText} = options;
let input: HTMLElement;
if(!plainText) {
if(init) {
init();
if(options.maxLength) {
options.showLengthOn = Math.round(options.maxLength / 3);
}
div.innerHTML = `
<div ${placeholder ? `data-placeholder="${placeholder}"` : ''} contenteditable="true" class="input-field-input"></div>
${label ? `<label>${label}</label>` : ''}
`;
const {placeholder, label, maxLength, showLengthOn, name, plainText} = options;
input = div.firstElementChild as HTMLElement;
const observer = new MutationObserver(() => {
checkAndSetRTL(input);
if(processInput) {
processInput();
let input: HTMLElement;
if(!plainText) {
if(init) {
init();
}
});
// ! childList for paste first symbol
observer.observe(input, {characterData: true, childList: true, subtree: true});
} else {
div.innerHTML = `
<input type="text" ${name ? `name="${name}"` : ''} ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" ${label ? 'required=""' : ''} class="input-field-input">
${label ? `<label>${label}</label>` : ''}
`;
input = div.firstElementChild as HTMLElement;
input.addEventListener('input', () => checkAndSetRTL(input));
this.container.innerHTML = `
<div ${placeholder ? `data-placeholder="${placeholder}"` : ''} contenteditable="true" class="input-field-input"></div>
${label ? `<label>${label}</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 = `
<input type="text" ${name ? `name="${name}"` : ''} ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" ${label ? 'required=""' : ''} class="input-field-input">
${label ? `<label>${label}</label>` : ''}
`;
input = this.container.firstElementChild as HTMLElement;
input.addEventListener('input', () => checkAndSetRTL(input));
}
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);
}
this.input = 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;
}
};
input.addEventListener('input', processInput);
get value() {
return this.options.plainText ? (this.input as HTMLInputElement).value : getRichValue(this.input);
//return getRichValue(this.input);
}
return {
container: div,
input: div.firstElementChild as HTMLInputElement
};
};
set value(value: string) {
this.setValueSilently(value);
const event = new Event('input', {bubbles: true, cancelable: true});
this.input.dispatchEvent(event);
}
public setValueSilently(value: string) {
if(this.options.plainText) {
(this.input as HTMLInputElement).value = value;
} else {
this.input.innerHTML = value;
}
}
}
export default InputField;

View File

@ -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() {

View File

@ -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');
});

View File

@ -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

View File

@ -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,

View File

@ -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;
protected controlsDiv: HTMLElement;
protected monthTitle: HTMLElement;
protected prevBtn: HTMLElement;
protected nextBtn: HTMLElement;
private monthsContainer: HTMLElement;
private month: HTMLElement;
protected monthsContainer: HTMLElement;
protected 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;
protected minMonth: Date;
protected maxMonth: Date;
protected minDate: Date;
protected maxDate: Date;
protected selectedDate: Date;
protected selectedMonth: Date;
protected selectedEl: HTMLElement;
constructor(initDate: Date, public onPick: (timestamp: number) => void) {
super('popup-date-picker', [{
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);

View File

@ -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,

View File

@ -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;

View File

@ -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<PopupButton>, options: Partial<{closable: true, overlayClosable: true, withConfirm: string, body: true}> = {}) {
constructor(className: string, buttons?: Array<PopupButton>, 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(() => {

View File

@ -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();

View File

@ -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<{

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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<InputFile> = 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<any>[] = [];
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;
}

View File

@ -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 {

View File

@ -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";

View File

@ -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,

View File

@ -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<void>((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);

View File

@ -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";

View File

@ -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;
}

View File

@ -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) {

View File

@ -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<void>((resolve, reject) => {
directoryReader.readEntries(async(entries: any) => {
for(const entry of entries) {
await scanFiles(entry);

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -159,20 +159,21 @@ export class AppPollsManager {
};
}
public sendVote(peerId: number, messageId: number, optionIds: number[]): Promise<void> {
const message = appMessagesManager.getMessageByPeer(peerId, messageId);
public sendVote(message: any, optionIds: number[]): Promise<void> {
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');

View File

@ -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;
};

View File

@ -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);

View File

@ -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');

View File

@ -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[],

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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 {