Browse Source

Autonomous topbar & chat & input

Inner chat
Fix pinned change from sticker
master
Eduard Kuzmenko 4 years ago
parent
commit
3611065bb7
  1. 2
      src/components/appMediaViewer.ts
  2. 2
      src/components/appSelectPeers.ts
  3. 2
      src/components/bubbleGroups.ts
  4. 26
      src/components/buttonMenu.ts
  5. 17
      src/components/buttonMenuToggle.ts
  6. 18
      src/components/chat/audio.ts
  7. 2302
      src/components/chat/bubbles.ts
  8. 172
      src/components/chat/chat.ts
  9. 108
      src/components/chat/contextMenu.ts
  10. 691
      src/components/chat/input.ts
  11. 244
      src/components/chat/markupTooltip.ts
  12. 19
      src/components/chat/pinnedContainer.ts
  13. 39
      src/components/chat/pinnedMessage.ts
  14. 2
      src/components/chat/pinnedMessageBorder.ts
  15. 15
      src/components/chat/replyContainer.ts
  16. 35
      src/components/chat/search.ts
  17. 53
      src/components/chat/selection.ts
  18. 90
      src/components/chat/stickersHelper.ts
  19. 345
      src/components/chat/topbar.ts
  20. 3
      src/components/dialogsContextMenu.ts
  21. 2
      src/components/divAndCaption.ts
  22. 38
      src/components/emoticonsDropdown/index.ts
  23. 4
      src/components/emoticonsDropdown/tabs/emoji.ts
  24. 2
      src/components/emoticonsDropdown/tabs/stickers.ts
  25. 10
      src/components/inputField.ts
  26. 22
      src/components/misc.ts
  27. 2
      src/components/poll.ts
  28. 7
      src/components/popupCreatePoll.ts
  29. 2
      src/components/popupDeleteMessages.ts
  30. 4
      src/components/popupForward.ts
  31. 5
      src/components/popupNewMedia.ts
  32. 2
      src/components/popupStickers.ts
  33. 2
      src/components/sidebarLeft/tabs/includedChats.ts
  34. 2
      src/components/sidebarLeft/tabs/newGroup.ts
  35. 2
      src/components/sidebarRight/tabs/gifs.ts
  36. 2
      src/components/sidebarRight/tabs/search.ts
  37. 63
      src/components/sidebarRight/tabs/sharedMedia.ts
  38. 2
      src/components/sidebarRight/tabs/stickers.ts
  39. 9
      src/components/transition.ts
  40. 21
      src/helpers/dom.ts
  41. 44
      src/helpers/listenerSetter.ts
  42. 176
      src/index.hbs
  43. 57
      src/lib/appManagers/appChatsManager.ts
  44. 57
      src/lib/appManagers/appDialogsManager.ts
  45. 2
      src/lib/appManagers/appDocsManager.ts
  46. 2891
      src/lib/appManagers/appImManager.ts
  47. 120
      src/lib/appManagers/appMessagesManager.ts
  48. 2
      src/lib/appManagers/appPollsManager.ts
  49. 2
      src/lib/appManagers/appWebPagesManager.ts
  50. 2
      src/lib/logger.ts
  51. 12
      src/lib/lottieLoader.ts
  52. 7
      src/lib/mtproto/networker.ts
  53. 7
      src/lib/rootScope.ts
  54. 450
      src/scss/partials/_chat.scss
  55. 22
      src/scss/partials/_chatBubble.scss
  56. 94
      src/scss/partials/_chatMarkupTooltip.scss
  57. 41
      src/scss/partials/_chatStickersHelper.scss
  58. 223
      src/scss/partials/_chatTopbar.scss
  59. 4
      src/scss/partials/_chatlist.scss
  60. 8
      src/scss/partials/_leftSidebar.scss
  61. 4
      src/scss/partials/_rightSidebar.scss
  62. 2
      src/scss/partials/_ripple.scss
  63. 2
      src/scss/partials/_selector.scss
  64. 2
      src/scss/partials/popups/_forward.scss
  65. 3
      src/scss/style.scss

2
src/components/appMediaViewer.ts

@ -1324,7 +1324,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet @@ -1324,7 +1324,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
else fromRight = this.currentMessageID > mid ? 1 : -1;
} else {
this.reverse = reverse;
this.peerID = rootScope.selectedPeerID;
this.peerID = message.peerID;
}
this.currentMessageID = mid;

2
src/components/appSelectPeers.ts

@ -90,7 +90,7 @@ export default class AppSelectPeers { @@ -90,7 +90,7 @@ export default class AppSelectPeers {
this.container.append(topContainer, delimiter);
}
this.chatsContainer.classList.add('chats-container');
this.chatsContainer.classList.add('chatlist-container');
this.chatsContainer.append(this.list);
this.scrollable = new Scrollable(this.chatsContainer);
this.scrollable.setVirtualContainer(this.list);

2
src/components/bubbleGroups.ts

@ -25,7 +25,7 @@ export default class BubbleGroups { @@ -25,7 +25,7 @@ export default class BubbleGroups {
let group: HTMLDivElement[];
// fix for saved messages forward to self
if(fromID == rootScope.myID && rootScope.selectedPeerID == rootScope.myID && message.fwdFromID == fromID) {
if(fromID == rootScope.myID && message.peerID == rootScope.myID && message.fwdFromID == fromID) {
fromID = -fromID;
}

26
src/components/buttonMenu.ts

@ -1,8 +1,16 @@ @@ -1,8 +1,16 @@
import { attachClickEvent, cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
import { attachClickEvent, AttachClickOptions, cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
import ListenerSetter from "../helpers/listenerSetter";
import { closeBtnMenu } from "./misc";
import { ripple } from "./ripple";
export type ButtonMenuItemOptions = {icon: string, text: string, onClick: (e: MouseEvent | TouchEvent) => void, element?: HTMLElement/* , cancelEvent?: true */};
export type ButtonMenuItemOptions = {
icon: string,
text: string,
onClick: (e: MouseEvent | TouchEvent) => void,
element?: HTMLElement,
options?: AttachClickOptions
/* , cancelEvent?: true */
};
const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
if(options.element) return options.element;
@ -19,15 +27,25 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => { @@ -19,15 +27,25 @@ const ButtonMenuItem = (options: ButtonMenuItemOptions) => {
cancelEvent(e);
onClick(e);
closeBtnMenu();
} : onClick);
} : onClick, options.options);
return options.element = el;
};
const ButtonMenu = (buttons: ButtonMenuItemOptions[]) => {
const ButtonMenu = (buttons: ButtonMenuItemOptions[], listenerSetter?: ListenerSetter) => {
const el = document.createElement('div');
el.classList.add('btn-menu');
if(listenerSetter) {
buttons.forEach(b => {
if(b.options) {
b.options.listenerSetter = listenerSetter;
} else {
b.options = {listenerSetter};
}
});
}
const items = buttons.map(ButtonMenuItem);
el.append(...items);

17
src/components/buttonMenuToggle.ts

@ -1,19 +1,23 @@ @@ -1,19 +1,23 @@
import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
import { AttachClickOptions, cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
import ListenerSetter from "../helpers/listenerSetter";
import ButtonIcon from "./buttonIcon";
import ButtonMenu, { ButtonMenuItemOptions } from "./buttonMenu";
import { closeBtnMenu, openBtnMenu } from "./misc";
const ButtonMenuToggle = (options: Partial<{noRipple: true, onlyMobile: true}> = {}, direction: 'bottom-left', buttons: ButtonMenuItemOptions[]) => {
const ButtonMenuToggle = (options: Partial<{noRipple: true, onlyMobile: true, listenerSetter: ListenerSetter}> = {}, direction: 'bottom-left' | 'top-left', buttons: ButtonMenuItemOptions[], onOpen?: () => void) => {
const button = ButtonIcon('more btn-menu-toggle', options);
const btnMenu = ButtonMenu(buttons);
const btnMenu = ButtonMenu(buttons, options.listenerSetter);
btnMenu.classList.add(direction);
ButtonMenuToggleHandler(button);
ButtonMenuToggleHandler(button, onOpen, options);
button.append(btnMenu);
return button;
};
const ButtonMenuToggleHandler = (el: HTMLElement) => {
(el as HTMLElement).addEventListener(CLICK_EVENT_NAME, (e) => {
const ButtonMenuToggleHandler = (el: HTMLElement, onOpen?: () => void, options?: AttachClickOptions) => {
const add = options?.listenerSetter ? options.listenerSetter.add.bind(options.listenerSetter, el) : el.addEventListener.bind(el);
add(CLICK_EVENT_NAME, (e: Event) => {
//console.log('click pageIm');
if(!el.classList.contains('btn-menu-toggle')) return false;
@ -24,6 +28,7 @@ const ButtonMenuToggleHandler = (el: HTMLElement) => { @@ -24,6 +28,7 @@ const ButtonMenuToggleHandler = (el: HTMLElement) => {
if(el.classList.contains('menu-open')) {
closeBtnMenu();
} else {
onOpen && onOpen();
openBtnMenu(openedMenu);
}
});

18
src/components/chat/audio.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import appPeersManager from "../../lib/appManagers/appPeersManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type ChatTopbar from "./topbar";
import { RichTextProcessor } from "../../lib/richtextprocessor";
import rootScope from "../../lib/rootScope";
import { cancelEvent } from "../../helpers/dom";
@ -7,12 +8,13 @@ import appMediaPlaybackController from "../appMediaPlaybackController"; @@ -7,12 +8,13 @@ import appMediaPlaybackController from "../appMediaPlaybackController";
import DivAndCaption from "../divAndCaption";
import { formatDate } from "../wrappers";
import PinnedContainer from "./pinnedContainer";
import Chat from "./chat";
export class ChatAudio extends PinnedContainer {
export default class ChatAudio extends PinnedContainer {
private toggleEl: HTMLElement;
constructor() {
super('audio', new DivAndCaption('pinned-audio', (title: string, subtitle: string) => {
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager, protected appPeersManager: AppPeersManager) {
super(topbar, chat, 'audio', new DivAndCaption('pinned-audio', (title: string, subtitle: string) => {
this.divAndCaption.title.innerHTML = title;
this.divAndCaption.subtitle.innerHTML = subtitle;
}), () => {
@ -25,14 +27,14 @@ export class ChatAudio extends PinnedContainer { @@ -25,14 +27,14 @@ export class ChatAudio extends PinnedContainer {
this.toggleEl = document.createElement('button');
this.toggleEl.classList.add('pinned-audio-ico', 'tgico', 'btn-icon');
this.toggleEl.addEventListener('click', (e) => {
this.topbar.listenerSetter.add(this.toggleEl, 'click', (e) => {
cancelEvent(e);
appMediaPlaybackController.toggle();
});
this.wrapper.prepend(this.toggleEl);
rootScope.on('audio_play', (e) => {
this.topbar.listenerSetter.add(rootScope, 'audio_play', (e) => {
const {doc, mid} = e.detail;
let title: string, subtitle: string;
@ -51,7 +53,7 @@ export class ChatAudio extends PinnedContainer { @@ -51,7 +53,7 @@ export class ChatAudio extends PinnedContainer {
this.toggle(false);
});
rootScope.on('audio_pause', () => {
this.topbar.listenerSetter.add(rootScope, 'audio_pause', () => {
this.toggleEl.classList.remove('flip-icon');
});
}

2302
src/components/chat/bubbles.ts

File diff suppressed because it is too large Load Diff

172
src/components/chat/chat.ts

@ -0,0 +1,172 @@ @@ -0,0 +1,172 @@
import type { AppChatsManager } from "../../lib/appManagers/appChatsManager";
import type { AppDocsManager } from "../../lib/appManagers/appDocsManager";
import type { AppImManager } from "../../lib/appManagers/appImManager";
import type { AppInlineBotsManager } from "../../lib/appManagers/AppInlineBotsManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type { AppPhotosManager } from "../../lib/appManagers/appPhotosManager";
import type { AppPollsManager } from "../../lib/appManagers/appPollsManager";
import type { AppProfileManager } from "../../lib/appManagers/appProfileManager";
import type { AppStickersManager } from "../../lib/appManagers/appStickersManager";
import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
import type { AppWebPagesManager } from "../../lib/appManagers/appWebPagesManager";
import { logger, LogLevels } from "../../lib/logger";
import rootScope from "../../lib/rootScope";
import appSidebarRight, { AppSidebarRight } from "../sidebarRight";
import ChatBubbles from "./bubbles";
import ChatContextMenu from "./contextMenu";
import ChatInput from "./input";
import ChatSelection from "./selection";
import ChatTopbar from "./topbar";
export default class Chat {
public container: HTMLElement;
public backgroundEl: HTMLElement;
public topbar: ChatTopbar;
public bubbles: ChatBubbles;
public input: ChatInput;
public selection: ChatSelection;
public contextMenu: ChatContextMenu;
public peerID = 0;
public setPeerPromise: Promise<void>;
public peerChanged: boolean;
public log: ReturnType<typeof logger>;
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) {
this.container = document.createElement('div');
this.container.classList.add('chat');
this.backgroundEl = document.createElement('div');
this.backgroundEl.classList.add('chat-background');
// * constructor end
this.log = logger('CHAT', LogLevels.log | LogLevels.warn | LogLevels.debug | LogLevels.error);
this.log.error('Chat construction');
this.container.append(this.backgroundEl);
this.appImManager.chatsContainer.append(this.container);
}
private init() {
this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appUsersManager, this.appProfileManager);
this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appSidebarRight, 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.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appChatsManager, this.appPeersManager, this.appPollsManager);
this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput);
}
public destroy() {
const perf = performance.now();
this.topbar.destroy();
this.bubbles.destroy();
this.input.destroy();
delete this.topbar;
delete this.bubbles;
delete this.input;
delete this.selection;
delete this.contextMenu;
this.container.remove();
this.log.error('Chat destroy time:', performance.now() - perf);
}
public cleanup() {
this.input.cleanup();
this.selection.cleanup();
this.peerChanged = false;
}
public setPeer(peerID: number, lastMsgID?: number) {
if(this.init) {
this.init();
this.init = null;
}
//console.time('appImManager setPeer');
//console.time('appImManager setPeer pre promise');
////console.time('appImManager: pre render start');
if(peerID == 0) {
appSidebarRight.toggleSidebar(false);
this.peerID = peerID;
this.cleanup();
this.topbar.setPeer(peerID);
this.bubbles.setPeer(peerID);
rootScope.broadcast('peer_changed', peerID);
return;
}
const samePeer = this.peerID == peerID;
// set new
if(!samePeer) {
if(appSidebarRight.historyTabIDs[appSidebarRight.historyTabIDs.length - 1] == AppSidebarRight.SLIDERITEMSIDS.search) {
appSidebarRight.searchTab.closeBtn?.click();
}
this.peerID = peerID;
appSidebarRight.sharedMediaTab.setPeer(peerID);
this.cleanup();
} else {
this.peerChanged = true;
}
const result = this.bubbles.setPeer(peerID, lastMsgID);
if(!result) {
return;
}
const {cached, promise} = result;
// clear
if(!cached) {
if(!samePeer) {
this.finishPeerChange();
}
}
//console.timeEnd('appImManager setPeer pre promise');
this.setPeerPromise = promise.then(() => {
if(cached) {
if(!samePeer) {
this.finishPeerChange();
}
}
}).finally(() => {
if(this.peerID == peerID) {
this.setPeerPromise = null;
}
});
appSidebarRight.sharedMediaTab.setLoadMutex(this.setPeerPromise);
appSidebarRight.sharedMediaTab.loadSidebarMedia(true);
return this.setPeerPromise;
}
public finishPeerChange() {
if(this.peerChanged) return;
let peerID = this.peerID;
this.peerChanged = true;
this.topbar.setPeer(peerID);
this.bubbles.finishPeerChange();
this.input.finishPeerChange();
appSidebarRight.sharedMediaTab.fillProfileElements();
rootScope.broadcast('peer_changed', this.peerID);
}
}

108
src/components/chat/contextMenu.ts

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppChatsManager } from "../../lib/appManagers/appChatsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type { AppPollsManager, Poll } from "../../lib/appManagers/appPollsManager";
import type Chat from "./chat";
import { isTouchSupported } from "../../helpers/touchSupport";
import appChatsManager from "../../lib/appManagers/appChatsManager";
import appImManager from "../../lib/appManagers/appImManager";
import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import appPeersManager from "../../lib/appManagers/appPeersManager";
import appPollsManager, { Poll } from "../../lib/appManagers/appPollsManager";
import rootScope from "../../lib/rootScope";
import { attachClickEvent, cancelEvent, cancelSelection, findUpClassName } from "../../helpers/dom";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
@ -22,7 +22,7 @@ export default class ChatContextMenu { @@ -22,7 +22,7 @@ export default class ChatContextMenu {
public peerID: number;
public msgID: number;
constructor(private attachTo: HTMLElement) {
constructor(private attachTo: HTMLElement, private chat: Chat, private appMessagesManager: AppMessagesManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appPollsManager: AppPollsManager) {
const onContextMenu = (e: MouseEvent | Touch) => {
if(this.init) {
this.init();
@ -49,17 +49,17 @@ export default class ChatContextMenu { @@ -49,17 +49,17 @@ export default class ChatContextMenu {
if(!mid) return;
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
if(appImManager.chatSelection.isSelecting && !bubbleContainer) {
if(chat.selection.isSelecting && !bubbleContainer) {
const mids = appMessagesManager.getMidsByMid(mid);
if(mids.length > 1) {
const selectedMid = appImManager.chatSelection.selectedMids.has(mid) ? mid : mids.find(mid => appImManager.chatSelection.selectedMids.has(mid));
const selectedMid = chat.selection.selectedMids.has(mid) ? mid : mids.find(mid => chat.selection.selectedMids.has(mid));
if(selectedMid) {
mid = selectedMid;
}
}
}
this.peerID = rootScope.selectedPeerID;
this.peerID = this.chat.peerID;
//this.msgID = msgID;
this.target = e.target as HTMLElement;
@ -75,7 +75,7 @@ export default class ChatContextMenu { @@ -75,7 +75,7 @@ export default class ChatContextMenu {
let good: boolean;
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
if(appImManager.chatSelection.isSelecting && !button.withSelection) {
if(chat.selection.isSelecting && !button.withSelection) {
good = false;
} else {
good = bubbleContainer || isTouchSupported ?
@ -98,24 +98,24 @@ export default class ChatContextMenu { @@ -98,24 +98,24 @@ export default class ChatContextMenu {
if(isTouchSupported) {
attachClickEvent(attachTo, (e) => {
if(appImManager.chatSelection.isSelecting) {
if(chat.selection.isSelecting) {
return;
}
const className = (e.target as HTMLElement).className;
if(!className || !className.includes) return;
appImManager.log('touchend', e);
chat.log('touchend', e);
const good = ['bubble', 'bubble__container', 'message', 'time', 'inner'].find(c => className.match(new RegExp(c + '($|\\s)')));
if(good) {
cancelEvent(e);
onContextMenu((e as TouchEvent).changedTouches[0]);
}
});
}, {listenerSetter: this.chat.bubbles.listenerSetter});
attachContextMenuListener(attachTo, (e) => {
if(appImManager.chatSelection.isSelecting) return;
if(chat.selection.isSelecting) return;
// * these two lines will fix instant text selection on iOS Safari
attachTo.classList.add('no-select');
@ -127,34 +127,34 @@ export default class ChatContextMenu { @@ -127,34 +127,34 @@ export default class ChatContextMenu {
//cancelEvent(e as any);
const bubble = findUpClassName(e.target, 'album-item') || findUpClassName(e.target, 'bubble');
if(bubble) {
appImManager.chatSelection.toggleByBubble(bubble);
chat.selection.toggleByBubble(bubble);
}
});
} else attachContextMenuListener(attachTo, onContextMenu);
}, this.chat.bubbles.listenerSetter);
} else attachContextMenuListener(attachTo, onContextMenu, this.chat.bubbles.listenerSetter);
}
private init = () => {
private init() {
this.buttons = [{
icon: 'reply',
text: 'Reply',
onClick: this.onReplyClick,
verify: () => (this.peerID > 0 || appChatsManager.hasRights(-this.peerID, 'send')) && this.msgID > 0/* ,
verify: () => (this.peerID > 0 || this.appChatsManager.hasRights(-this.peerID, 'send')) && this.msgID > 0/* ,
cancelEvent: true */
}, {
icon: 'edit',
text: 'Edit',
onClick: this.onEditClick,
verify: () => appMessagesManager.canEditMessage(this.msgID, 'text')
verify: () => this.appMessagesManager.canEditMessage(this.msgID, 'text')
}, {
icon: 'copy',
text: 'Copy',
onClick: this.onCopyClick,
verify: () => !!appMessagesManager.getMessage(this.msgID).message
verify: () => !!this.appMessagesManager.getMessage(this.msgID).message
}, {
icon: 'copy',
text: 'Copy selected',
onClick: this.onCopyClick,
verify: () => appImManager.chatSelection.selectedMids.has(this.msgID) && !![...appImManager.chatSelection.selectedMids].find(mid => !!appMessagesManager.getMessage(mid).message),
verify: () => this.chat.selection.selectedMids.has(this.msgID) && !![...this.chat.selection.selectedMids].find(mid => !!this.appMessagesManager.getMessage(mid).message),
notDirect: () => true,
withSelection: true
}, {
@ -162,22 +162,22 @@ export default class ChatContextMenu { @@ -162,22 +162,22 @@ export default class ChatContextMenu {
text: 'Pin',
onClick: this.onPinClick,
verify: () => {
const message = appMessagesManager.getMessage(this.msgID);
const message = this.appMessagesManager.getMessage(this.msgID);
// for new layer
// return this.msgID > 0 && message._ != 'messageService' && appImManager.pinnedMsgID != this.msgID && (this.peerID > 0 || appChatsManager.hasRights(-this.peerID, 'pin'));
return this.msgID > 0 && message._ != 'messageService' && /* appImManager.pinnedMsgID != this.msgID && */ (this.peerID == rootScope.myID || (this.peerID < 0 && appChatsManager.hasRights(-this.peerID, 'pin')));
return this.msgID > 0 && message._ != 'messageService' && /* appImManager.pinnedMsgID != this.msgID && */ (this.peerID == rootScope.myID || (this.peerID < 0 && this.appChatsManager.hasRights(-this.peerID, 'pin')));
}
}, {
icon: 'unpin',
text: 'Unpin',
onClick: this.onUnpinClick,
verify: () => /* appImManager.pinnedMsgID == this.msgID && */ appPeersManager.canPinMessage(this.peerID)
verify: () => /* appImManager.pinnedMsgID == this.msgID && */ this.appPeersManager.canPinMessage(this.peerID)
}, {
icon: 'revote',
text: 'Revote',
onClick: this.onRetractVote,
verify: () => {
const message = appMessagesManager.getMessage(this.msgID);
const message = this.appMessagesManager.getMessage(this.msgID);
const poll = message.media?.poll as Poll;
return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz;
}/* ,
@ -187,9 +187,9 @@ export default class ChatContextMenu { @@ -187,9 +187,9 @@ export default class ChatContextMenu {
text: 'Stop poll',
onClick: this.onStopPoll,
verify: () => {
const message = appMessagesManager.getMessage(this.msgID);
const message = this.appMessagesManager.getMessage(this.msgID);
const poll = message.media?.poll;
return appMessagesManager.canEditMessage(this.msgID, 'poll') && poll && !poll.pFlags.closed && this.msgID > 0;
return this.appMessagesManager.canEditMessage(this.msgID, 'poll') && poll && !poll.pFlags.closed && this.msgID > 0;
}/* ,
cancelEvent: true */
}, {
@ -201,7 +201,7 @@ export default class ChatContextMenu { @@ -201,7 +201,7 @@ export default class ChatContextMenu {
icon: 'forward',
text: 'Forward selected',
onClick: this.onForwardClick,
verify: () => appImManager.chatSelection.selectedMids.has(this.msgID) && !appImManager.chatSelection.selectionForwardBtn.hasAttribute('disabled'),
verify: () => this.chat.selection.selectedMids.has(this.msgID) && !this.chat.selection.selectionForwardBtn.hasAttribute('disabled'),
notDirect: () => true,
withSelection: true
}, {
@ -209,8 +209,8 @@ export default class ChatContextMenu { @@ -209,8 +209,8 @@ export default class ChatContextMenu {
text: 'Select',
onClick: this.onSelectClick,
verify: () => {
const message = appMessagesManager.getMessage(this.msgID);
return !message.action && !appImManager.chatSelection.selectedMids.has(this.msgID);
const message = this.appMessagesManager.getMessage(this.msgID);
return !message.action && !this.chat.selection.selectedMids.has(this.msgID);
},
notDirect: () => true,
withSelection: true
@ -218,46 +218,46 @@ export default class ChatContextMenu { @@ -218,46 +218,46 @@ export default class ChatContextMenu {
icon: 'select',
text: 'Clear selection',
onClick: this.onClearSelectionClick,
verify: () => appImManager.chatSelection.selectedMids.has(this.msgID),
verify: () => this.chat.selection.selectedMids.has(this.msgID),
notDirect: () => true,
withSelection: true
}, {
icon: 'delete danger',
text: 'Delete',
onClick: this.onDeleteClick,
verify: () => appMessagesManager.canDeleteMessage(this.msgID)
verify: () => this.appMessagesManager.canDeleteMessage(this.msgID)
}, {
icon: 'delete danger',
text: 'Delete selected',
onClick: this.onDeleteClick,
verify: () => appImManager.chatSelection.selectedMids.has(this.msgID) && !appImManager.chatSelection.selectionDeleteBtn.hasAttribute('disabled'),
verify: () => this.chat.selection.selectedMids.has(this.msgID) && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'),
notDirect: () => true,
withSelection: true
}];
this.element = ButtonMenu(this.buttons);
this.element = ButtonMenu(this.buttons, this.chat.bubbles.listenerSetter);
this.element.id = 'bubble-contextmenu';
appImManager.chatInput.parentElement.insertBefore(this.element, appImManager.chatInput);
this.chat.container.append(this.element);
};
private onReplyClick = () => {
const message = appMessagesManager.getMessage(this.msgID);
const chatInputC = appImManager.chatInputC;
const message = this.appMessagesManager.getMessage(this.msgID);
const chatInputC = this.chat.input;
const f = () => {
chatInputC.setTopInfo('reply', f, appPeersManager.getPeerTitle(message.fromID, true), message.message, undefined, message);
chatInputC.setTopInfo('reply', f, this.appPeersManager.getPeerTitle(message.fromID, true), message.message, undefined, message);
chatInputC.replyToMsgID = this.msgID;
};
f();
};
private onEditClick = () => {
appImManager.chatInputC.initMessageEditing(this.msgID);
this.chat.input.initMessageEditing(this.msgID);
};
private onCopyClick = () => {
const mids = appImManager.chatSelection.isSelecting ? [...appImManager.chatSelection.selectedMids] : [this.msgID];
const mids = this.chat.selection.isSelecting ? [...this.chat.selection.selectedMids] : [this.msgID];
const str = mids.reduce((acc, mid) => {
const message = appMessagesManager.getMessage(mid);
const message = this.appMessagesManager.getMessage(mid);
return acc + (message?.message ? message.message + '\n' : '');
}, '').trim();
@ -265,42 +265,42 @@ export default class ChatContextMenu { @@ -265,42 +265,42 @@ export default class ChatContextMenu {
};
private onPinClick = () => {
new PopupPinMessage(rootScope.selectedPeerID, this.msgID);
new PopupPinMessage(this.peerID, this.msgID);
};
private onUnpinClick = () => {
new PopupPinMessage(rootScope.selectedPeerID, this.msgID, true);
new PopupPinMessage(this.peerID, this.msgID, true);
};
private onRetractVote = () => {
appPollsManager.sendVote(this.msgID, []);
this.appPollsManager.sendVote(this.msgID, []);
};
private onStopPoll = () => {
appPollsManager.stopPoll(this.msgID);
this.appPollsManager.stopPoll(this.msgID);
};
private onForwardClick = () => {
if(appImManager.chatSelection.isSelecting) {
appImManager.chatSelection.selectionForwardBtn.click();
if(this.chat.selection.isSelecting) {
this.chat.selection.selectionForwardBtn.click();
} else {
new PopupForward(this.isTargetAnAlbumItem ? [this.msgID] : appMessagesManager.getMidsByMid(this.msgID));
new PopupForward(this.isTargetAnAlbumItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID));
}
};
private onSelectClick = () => {
appImManager.chatSelection.toggleByBubble(findUpClassName(this.target, 'album-item') || findUpClassName(this.target, 'bubble'));
this.chat.selection.toggleByBubble(findUpClassName(this.target, 'album-item') || findUpClassName(this.target, 'bubble'));
};
private onClearSelectionClick = () => {
appImManager.chatSelection.cancelSelection();
this.chat.selection.cancelSelection();
};
private onDeleteClick = () => {
if(appImManager.chatSelection.isSelecting) {
appImManager.chatSelection.selectionDeleteBtn.click();
if(this.chat.selection.isSelecting) {
this.chat.selection.selectionDeleteBtn.click();
} else {
new PopupDeleteMessages(this.isTargetAnAlbumItem ? [this.msgID] : appMessagesManager.getMidsByMid(this.msgID));
new PopupDeleteMessages(this.isTargetAnAlbumItem ? [this.msgID] : this.appMessagesManager.getMidsByMid(this.msgID));
}
};
}

691
src/components/chat/input.ts

@ -1,381 +1,65 @@ @@ -1,381 +1,65 @@
import type { AppChatsManager } from '../../lib/appManagers/appChatsManager';
import type { AppDocsManager } 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";
import type { AppImManager } from '../../lib/appManagers/appImManager';
import type Chat from './chat';
import Recorder from '../../../public/recorder.min';
import { isTouchSupported } from "../../helpers/touchSupport";
import appChatsManager from '../../lib/appManagers/appChatsManager';
import appDocsManager, { MyDocument } from "../../lib/appManagers/appDocsManager";
import appImManager, { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import appMessagesManager from "../../lib/appManagers/appMessagesManager";
import appPeersManager from '../../lib/appManagers/appPeersManager';
import appWebPagesManager from "../../lib/appManagers/appWebPagesManager";
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 rootScope from '../../lib/rootScope';
import { blurActiveElement, cancelEvent, CLICK_EVENT_NAME, findUpClassName, getRichValue, getSelectedNodes, isInputEmpty, markdownTags, MarkdownType, placeCaretAtEnd, serializeNodes } from "../../helpers/dom";
import ButtonMenu, { ButtonMenuItemOptions } from '../buttonMenu';
import emoticonsDropdown, { EmoticonsDropdown } from "../emoticonsDropdown";
import RichTextProcessor from "../../lib/richtextprocessor";
import { blurActiveElement, cancelEvent, cancelSelection, CLICK_EVENT_NAME, findUpClassName, getRichValue, 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 { ripple } from '../ripple';
import Scrollable from "../scrollable";
import { toast } from "../toast";
import { wrapReply } from "../wrappers";
import InputField from '../inputField';
import { MessageEntity } from '../../layer';
import MarkupTooltip from './markupTooltip';
import StickersHelper from './stickersHelper';
import ButtonIcon from '../buttonIcon';
import appStickersManager from '../../lib/appManagers/appStickersManager';
import SetTransition from '../singleTransition';
import { SuperStickerRenderer } from '../emoticonsDropdown/tabs/stickers';
import LazyLoadQueue from '../lazyLoadQueue';
import DivAndCaption from '../divAndCaption';
import ButtonMenuToggle from '../buttonMenuToggle';
import ListenerSetter from '../../helpers/listenerSetter';
const RECORD_MIN_TIME = 500;
const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this group.';
type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
export class StickersHelper {
private container: HTMLElement;
private stickersContainer: HTMLElement;
private scrollable: Scrollable;
private superStickerRenderer: SuperStickerRenderer;
private lazyLoadQueue: LazyLoadQueue;
private lastEmoticon = '';
constructor(private appendTo: HTMLElement) {
}
public checkEmoticon(emoticon: string) {
if(this.lastEmoticon == emoticon) return;
if(this.lastEmoticon && !emoticon) {
if(this.container) {
SetTransition(this.container, 'is-visible', false, 200/* , () => {
this.stickersContainer.innerHTML = '';
} */);
}
}
this.lastEmoticon = emoticon;
if(this.lazyLoadQueue) {
this.lazyLoadQueue.clear();
}
if(!emoticon) {
return;
}
appStickersManager.getStickersByEmoticon(emoticon)
.then(stickers => {
if(this.lastEmoticon != emoticon) {
return;
}
if(this.init) {
this.init();
this.init = null;
}
this.stickersContainer.innerHTML = '';
this.lazyLoadQueue.clear();
if(stickers.length) {
stickers.forEach(sticker => {
this.stickersContainer.append(this.superStickerRenderer.renderSticker(sticker as MyDocument));
});
}
SetTransition(this.container, 'is-visible', true, 200);
this.scrollable.scrollTop = 0;
});
}
private init() {
this.container = document.createElement('div');
this.container.classList.add('stickers-helper', 'z-depth-1');
this.stickersContainer = document.createElement('div');
this.stickersContainer.classList.add('stickers-helper-stickers', 'super-stickers');
this.stickersContainer.addEventListener('click', (e) => {
if(!findUpClassName(e.target, 'super-sticker')) {
return;
}
appImManager.chatInputC.clearInput();
EmoticonsDropdown.onMediaClick(e);
});
this.container.append(this.stickersContainer);
this.scrollable = new Scrollable(this.container);
this.lazyLoadQueue = new LazyLoadQueue();
this.superStickerRenderer = new SuperStickerRenderer(this.lazyLoadQueue, CHAT_ANIMATION_GROUP);
this.appendTo.append(this.container);
}
}
export class MarkupTooltip {
public container: HTMLElement;
private wrapper: HTMLElement;
private buttons: {[type in MarkdownType]: HTMLElement} = {} as any;
private linkBackButton: HTMLElement;
private hideTimeout: number;
private inputs: HTMLElement[] = [];
private addedListener = false;
private waitingForMouseUp = false;
private linkInput: HTMLInputElement;
private savedRange: Range;
private init() {
this.container = document.createElement('div');
this.container.classList.add('markup-tooltip', 'z-depth-1', 'hide');
this.wrapper = document.createElement('div');
this.wrapper.classList.add('markup-tooltip-wrapper');
const tools1 = document.createElement('div');
const tools2 = document.createElement('div');
tools1.classList.add('markup-tooltip-tools');
tools2.classList.add('markup-tooltip-tools');
const arr = ['bold', 'italic', 'underline', 'strikethrough', 'monospace', 'link'] as (keyof MarkupTooltip['buttons'])[];
arr.forEach(c => {
const button = ButtonIcon(c, {noRipple: true});
tools1.append(this.buttons[c] = button);
if(c !== 'link') {
button.addEventListener('click', () => {
appImManager.chatInputC.applyMarkdown(c);
});
} else {
button.addEventListener('click', () => {
this.container.classList.add('is-link');
if(button.classList.contains('active')) {
const startContainer = this.savedRange.startContainer;
const anchor = startContainer.parentElement as HTMLAnchorElement;
this.linkInput.value = anchor.href;
} else {
this.linkInput.value = '';
}
});
}
});
this.linkBackButton = ButtonIcon('back', {noRipple: true});
this.linkInput = document.createElement('input');
this.linkInput.placeholder = 'Enter URL...';
this.linkInput.classList.add('input-clear');
this.linkInput.addEventListener('keydown', (e) => {
if(e.code == 'Enter') {
const valid = !this.linkInput.value.length || RichTextProcessor.matchUrl(this.linkInput.value);///^(http)|(https):\/\//i.test(this.linkInput.value);
if(!valid) {
if(this.linkInput.classList.contains('error')) {
this.linkInput.classList.remove('error');
void this.linkInput.offsetLeft; // reflow
}
this.linkInput.classList.add('error');
} else {
cancelEvent(e);
this.resetSelection();
appImManager.chatInputC.applyMarkdown('link', this.linkInput.value);
this.hide();
}
} else {
this.linkInput.classList.remove('error');
}
});
this.linkBackButton.addEventListener('click', () => {
this.container.classList.remove('is-link');
//input.value = '';
this.resetSelection();
});
const delimiter1 = document.createElement('span');
const delimiter2 = document.createElement('span');
delimiter1.classList.add('markup-tooltip-delimiter');
delimiter2.classList.add('markup-tooltip-delimiter');
tools1.insertBefore(delimiter1, this.buttons.link);
tools2.append(this.linkBackButton, delimiter2, this.linkInput);
//tools1.insertBefore(delimiter2, this.buttons.link.nextSibling);
this.wrapper.append(tools1, tools2);
this.container.append(this.wrapper);
document.body.append(this.container);
}
private resetSelection() {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(this.savedRange);
this.inputs[0].focus();
}
public hide() {
if(this.init) return;
this.container.classList.remove('is-visible');
document.removeEventListener('mouseup', this.onMouseUp);
if(this.hideTimeout) clearTimeout(this.hideTimeout);
this.hideTimeout = window.setTimeout(() => {
this.hideTimeout = undefined;
this.container.classList.add('hide');
this.container.classList.remove('is-link');
}, 200);
}
public getActiveMarkupButton() {
const nodes = getSelectedNodes();
const parents = [...new Set(nodes.map(node => node.parentNode))];
if(parents.length > 1) return undefined;
const node = parents[0] as HTMLElement;
let currentMarkup: HTMLElement;
for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType];
if(node.matches(tag.match)) {
currentMarkup = this.buttons[type as MarkdownType];
break;
}
}
return currentMarkup;
}
public setActiveMarkupButton() {
const activeButton = this.getActiveMarkupButton();
for(const i in this.buttons) {
// @ts-ignore
const button = this.buttons[i];
if(button != activeButton) {
button.classList.remove('active');
}
}
if(activeButton) {
activeButton.classList.add('active');
}
return activeButton;
}
public show() {
if(this.init) {
this.init();
this.init = null;
}
const selection = document.getSelection();
if(!selection.toString().trim().length) {
this.hide();
return;
}
if(this.hideTimeout !== undefined) {
clearTimeout(this.hideTimeout);
}
const range = this.savedRange = selection.getRangeAt(0);
const activeButton = this.setActiveMarkupButton();
this.container.classList.remove('is-link');
const isFirstShow = this.container.classList.contains('hide');
if(isFirstShow) {
this.container.classList.remove('hide');
this.container.classList.add('no-transition');
}
const selectionRect = range.getBoundingClientRect();
//const containerRect = this.container.getBoundingClientRect();
const sizesRect = this.container.firstElementChild.firstElementChild.getBoundingClientRect();
const top = selectionRect.top - sizesRect.height - 8;
const left = selectionRect.left + (selectionRect.width - sizesRect.width) / 2;
//const top = selectionRect.top - 44 - 8;
this.container.style.transform = `translate3d(${left}px, ${top}px, 0)`;
if(isFirstShow) {
void this.container.offsetLeft; // reflow
this.container.classList.remove('no-transition');
}
this.container.classList.add('is-visible');
console.log('selection', selectionRect, activeButton);
}
private onMouseUp = (e: Event) => {
if(findUpClassName(e.target, 'markup-tooltip')) return;
this.hide();
document.removeEventListener('mouseup', this.onMouseUp);
};
public setMouseUpEvent() {
if(this.waitingForMouseUp) return;
this.waitingForMouseUp = true;
document.addEventListener('mouseup', (e) => {
this.waitingForMouseUp = false;
this.show();
document.addEventListener('mouseup', this.onMouseUp);
}, {once: true});
}
public handleSelection(input: HTMLElement) {
this.inputs.push(input);
if(this.addedListener) return;
this.addedListener = true;
document.addEventListener('selectionchange', (e) => {
if(document.activeElement == this.linkInput) {
return;
}
if(!this.inputs.includes(document.activeElement as HTMLElement)) {
this.hide();
return;
}
const selection = document.getSelection();
if(!selection.toString().trim().length) {
this.hide();
return;
}
this.setMouseUpEvent();
});
}
}
export class ChatInput {
export default class ChatInput {
public pageEl = document.getElementById('page-chats') as HTMLDivElement;
public messageInput: HTMLDivElement/* HTMLInputElement */;
public fileInput = document.getElementById('input-file') as HTMLInputElement;
public inputMessageContainer = document.getElementsByClassName('input-message-container')[0] as HTMLDivElement;
public inputScroll = new Scrollable(this.inputMessageContainer);
public messageInput: HTMLDivElement;
public fileInput: HTMLInputElement;
public inputMessageContainer: HTMLDivElement;
public inputScroll: Scrollable;
public btnSend = document.getElementById('btn-send') as HTMLButtonElement;
public btnCancelRecord = this.btnSend.parentElement.previousElementSibling as HTMLButtonElement;
public btnCancelRecord: HTMLButtonElement;
public lastUrl = '';
public lastTimeType = 0;
private inputContainer = this.btnSend.parentElement.parentElement as HTMLDivElement;
private chatInput = this.inputContainer.parentElement as HTMLDivElement;
public chatInput: HTMLElement;
public inputContainer: HTMLElement;
public rowsWrapper: HTMLDivElement;
private newMessageWrapper: HTMLDivElement;
private btnToggleEmoticons: HTMLButtonElement;
private btnSendContainer: HTMLDivElement;
public attachMenu: HTMLButtonElement;
private attachMenuButtons: (ButtonMenuItemOptions & {verify: (peerID: number) => boolean})[];
public replyElements: {
container?: HTMLDivElement,
container?: HTMLElement,
cancelBtn?: HTMLButtonElement,
titleEl?: HTMLDivElement,
subtitleEl?: HTMLDivElement
titleEl?: HTMLElement,
subtitleEl?: HTMLElement
} = {};
public willSendWebPage: any = null;
@ -387,8 +71,8 @@ export class ChatInput { @@ -387,8 +71,8 @@ export class ChatInput {
private recorder: any;
private recording = false;
private recordCanceled = false;
private recordTimeEl = this.inputContainer.querySelector('.record-time') as HTMLDivElement;
private recordRippleEl = this.inputContainer.querySelector('.record-ripple') as HTMLDivElement;
private recordTimeEl: HTMLElement;
private recordRippleEl: HTMLElement;
private recordStartTime = 0;
private scrollTop = 0;
@ -399,7 +83,7 @@ export class ChatInput { @@ -399,7 +83,7 @@ export class ChatInput {
private helperFunc: () => void;
private helperWaitingForward: boolean;
private willAttachType: 'document' | 'media';
public willAttachType: 'document' | 'media';
private lockRedo = false;
private canRedoFromHTML = '';
@ -407,17 +91,42 @@ export class ChatInput { @@ -407,17 +91,42 @@ export class ChatInput {
readonly executedHistory: string[] = [];
private canUndoFromHTML = '';
public markupTooltip: MarkupTooltip;
public stickersHelper: StickersHelper;
public listenerSetter: ListenerSetter;
constructor() {
if(!isTouchSupported) {
this.markupTooltip = new MarkupTooltip();
}
constructor(private chat: Chat, private appMessagesManager: AppMessagesManager, private appDocsManager: AppDocsManager, private appChatsManager: AppChatsManager, private appPeersManager: AppPeersManager, private appWebPagesManager: AppWebPagesManager, private appImManager: AppImManager) {
this.listenerSetter = new ListenerSetter();
this.attachMessageInputField();
this.chatInput = document.createElement('div');
this.chatInput.classList.add('chat-input');
this.inputContainer = document.createElement('div');
this.inputContainer.classList.add('chat-input-container');
this.rowsWrapper = document.createElement('div');
this.rowsWrapper.classList.add('rows-wrapper');
this.replyElements.container = document.createElement('div');
this.replyElements.container.classList.add('reply-wrapper');
this.replyElements.cancelBtn = ButtonIcon('close reply-cancel');
const dac = new DivAndCaption('reply');
this.replyElements.titleEl = dac.title;
this.replyElements.subtitleEl = dac.subtitle;
this.replyElements.container.append(this.replyElements.cancelBtn, dac.container);
this.attachMenu = document.getElementById('attach-file') as HTMLButtonElement;
this.newMessageWrapper = document.createElement('div');
this.newMessageWrapper.classList.add('new-message-wrapper');
this.btnToggleEmoticons = ButtonIcon('none toggle-emoticons', {noRipple: true});
this.inputMessageContainer = document.createElement('div');
this.inputMessageContainer.classList.add('input-message-container');
this.inputScroll = new Scrollable(this.inputMessageContainer);
this.attachMenuButtons = [{
icon: 'photo',
@ -443,11 +152,55 @@ export class ChatInput { @@ -443,11 +152,55 @@ export class ChatInput {
icon: 'poll',
text: 'Poll',
onClick: () => {
new PopupCreatePoll().show();
new PopupCreatePoll(this.chat.peerID).show();
},
verify: (peerID: number) => peerID < 0 && appChatsManager.hasRights(peerID, 'send', 'send_polls')
}];
this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons);
this.attachMenu.classList.add('attach-file', 'tgico-attach');
this.attachMenu.classList.remove('tgico-more');
this.recordTimeEl = document.createElement('div');
this.recordTimeEl.classList.add('record-time');
this.fileInput = document.createElement('input');
this.fileInput.type = 'file';
this.fileInput.multiple = true;
this.fileInput.style.display = 'none';
this.newMessageWrapper.append(this.btnToggleEmoticons, this.inputMessageContainer, this.attachMenu, this.recordTimeEl, this.fileInput);
this.rowsWrapper.append(this.replyElements.container, this.newMessageWrapper);
this.btnCancelRecord = ButtonIcon('delete danger btn-circle z-depth-1 btn-record-cancel');
this.btnSendContainer = document.createElement('div');
this.btnSendContainer.classList.add('btn-send-container');
this.recordRippleEl = document.createElement('div');
this.recordRippleEl.classList.add('record-ripple');
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-microphone2"></span>
`);
this.btnSendContainer.append(this.recordRippleEl, this.btnSend);
this.inputContainer.append(this.rowsWrapper, this.btnCancelRecord, this.btnSendContainer);
this.chatInput.append(this.inputContainer);
// * constructor end
const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons);
emoticonsDropdown.events.onOpen.push(this.onEmoticonsOpen);
emoticonsDropdown.events.onClose.push(this.onEmoticonsClose);
this.attachMessageInputField();
/* this.attachMenu.addEventListener('mousedown', (e) => {
const hidden = this.attachMenu.querySelectorAll('.hide');
if(hidden.length == this.attachMenuButtons.length) {
@ -457,18 +210,7 @@ export class ChatInput { @@ -457,18 +210,7 @@ export class ChatInput {
}
}, {passive: false, capture: true}); */
const attachBtnMenu = ButtonMenu(this.attachMenuButtons);
attachBtnMenu.classList.add('top-left');
this.attachMenu.append(attachBtnMenu);
ripple(this.attachMenu);
this.replyElements.container = this.pageEl.querySelector('.reply-wrapper') as HTMLDivElement;
this.replyElements.cancelBtn = this.replyElements.container.querySelector('.reply-cancel') as HTMLButtonElement;
this.replyElements.titleEl = this.replyElements.container.querySelector('.reply-title') as HTMLDivElement;
this.replyElements.subtitleEl = this.replyElements.container.querySelector('.reply-subtitle') as HTMLDivElement;
this.stickersHelper = new StickersHelper(this.replyElements.container.parentElement);
this.stickersHelper = new StickersHelper(this.rowsWrapper);
try {
this.recorder = new Recorder({
@ -486,20 +228,7 @@ export class ChatInput { @@ -486,20 +228,7 @@ export class ChatInput {
this.updateSendBtn();
rootScope.on('peer_changed', (e) => {
const peerID = e.detail;
const visible = this.attachMenuButtons.filter(button => {
const good = button.verify(peerID);
button.element.classList.toggle('hide', !good);
return good;
});
this.attachMenu.toggleAttribute('disabled', !visible.length);
this.updateSendBtn();
});
this.fileInput.addEventListener('change', (e) => {
this.listenerSetter.add(this.fileInput, 'change', (e) => {
let files = (e.target as HTMLInputElement & EventTarget).files;
if(!files.length) {
return;
@ -509,9 +238,7 @@ export class ChatInput { @@ -509,9 +238,7 @@ export class ChatInput {
this.fileInput.value = '';
}, false);
document.addEventListener('paste', this.onDocumentPaste, true);
this.btnSend.addEventListener(CLICK_EVENT_NAME, this.onBtnSendClick);
this.listenerSetter.add(this.btnSend, CLICK_EVENT_NAME, this.onBtnSendClick);
if(this.recorder) {
const onCancelRecordClick = (e: Event) => {
@ -520,7 +247,7 @@ export class ChatInput { @@ -520,7 +247,7 @@ export class ChatInput {
this.recorder.stop();
opusDecodeController.setKeepAlive(false);
};
this.btnCancelRecord.addEventListener(CLICK_EVENT_NAME, onCancelRecordClick);
this.listenerSetter.add(this.btnCancelRecord, CLICK_EVENT_NAME, onCancelRecordClick);
this.recorder.onstop = () => {
this.recording = false;
@ -536,33 +263,14 @@ export class ChatInput { @@ -536,33 +263,14 @@ export class ChatInput {
const dataBlob = new Blob([typedArray], {type: 'audio/ogg'});
/* const fileName = new Date().toISOString() + ".opus";
console.log('Recorder data received', typedArray, dataBlob); */
/* var url = URL.createObjectURL( dataBlob );
var audio = document.createElement('audio');
audio.controls = true;
audio.src = url;
var link = document.createElement('a');
link.href = url;
link.download = fileName;
link.innerHTML = link.download;
var li = document.createElement('li');
li.appendChild(link);
li.appendChild(audio);
document.body.append(li);
return; */
//let perf = performance.now();
opusDecodeController.decode(typedArray, true).then(result => {
//console.log('WAVEFORM!:', /* waveform, */performance.now() - perf);
opusDecodeController.setKeepAlive(false);
let peerID = appImManager.peerID;
let peerID = this.chat.peerID;
// тут objectURL ставится уже с audio/wav
appMessagesManager.sendFile(peerID, dataBlob, {
isVoiceMessage: true,
@ -575,28 +283,62 @@ export class ChatInput { @@ -575,28 +283,62 @@ export class ChatInput {
this.onMessageSent(false, true);
});
/* const url = URL.createObjectURL(dataBlob);
var audio = document.createElement('audio');
audio.controls = true;
audio.src = url;
var link = document.createElement('a');
link.href = url;
link.download = fileName;
link.innerHTML = link.download;
var li = document.createElement('li');
li.appendChild(link);
li.appendChild(audio);
recordingslist.appendChild(li); */
};
}
this.replyElements.cancelBtn.addEventListener(CLICK_EVENT_NAME, this.onHelperCancel);
this.replyElements.container.addEventListener(CLICK_EVENT_NAME, this.onHelperClick);
this.listenerSetter.add(this.replyElements.cancelBtn, CLICK_EVENT_NAME, this.onHelperCancel);
this.listenerSetter.add(this.replyElements.container, CLICK_EVENT_NAME, this.onHelperClick);
}
private onEmoticonsOpen = () => {
const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
this.btnToggleEmoticons.classList.toggle(toggleClass, true);
};
private onEmoticonsClose = () => {
const toggleClass = isTouchSupported ? 'flip-icon' : 'active';
this.btnToggleEmoticons.classList.toggle(toggleClass, false);
};
public destroy() {
this.chat.log.error('Input destroying');
emoticonsDropdown.events.onOpen.findAndSplice(f => f == this.onEmoticonsOpen);
emoticonsDropdown.events.onClose.findAndSplice(f => f == this.onEmoticonsClose);
this.listenerSetter.removeAll();
}
public cleanup() {
if(!this.chat.peerID) {
this.chatInput.style.display = 'none';
}
cancelSelection();
this.clearInput();
this.clearHelper();
}
public finishPeerChange() {
const peerID = this.chat.peerID;
const visible = this.attachMenuButtons.filter(button => {
const good = button.verify(peerID);
button.element.classList.toggle('hide', !good);
return good;
});
const canWrite = this.appMessagesManager.canWriteToPeer(peerID);
this.chatInput.style.display = '';
this.chatInput.classList.toggle('is-hidden', !canWrite);
if(!canWrite) {
this.messageInput.removeAttribute('contenteditable');
} else {
this.messageInput.setAttribute('contenteditable', 'true');
}
this.attachMenu.toggleAttribute('disabled', !visible.length);
this.updateSendBtn();
}
private attachMessageInputField() {
@ -605,7 +347,7 @@ export class ChatInput { @@ -605,7 +347,7 @@ export class ChatInput {
name: 'message'
});
messageInputField.input.className = '';
messageInputField.input.className = 'input-message-input';
this.messageInput = messageInputField.input;
this.attachMessageInputListeners();
@ -618,7 +360,7 @@ export class ChatInput { @@ -618,7 +360,7 @@ export class ChatInput {
}
private attachMessageInputListeners() {
this.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {
this.listenerSetter.add(this.messageInput, 'keydown', (e: KeyboardEvent) => {
if(e.key == 'Enter' && !isTouchSupported) {
/* if(e.ctrlKey || e.metaKey) {
this.messageInput.innerHTML += '<br>';
@ -637,18 +379,18 @@ export class ChatInput { @@ -637,18 +379,18 @@ export class ChatInput {
});
if(isTouchSupported) {
this.messageInput.addEventListener('touchend', (e) => {
appImManager.selectTab(1); // * set chat tab for album orientation
this.listenerSetter.add(this.messageInput, 'touchend', (e) => {
this.appImManager.selectTab(1); // * set chat tab for album orientation
this.saveScroll();
emoticonsDropdown.toggle(false);
});
window.addEventListener('resize', () => {
this.listenerSetter.add(window, 'resize', () => {
this.restoreScroll();
});
}
this.messageInput.addEventListener('beforeinput', (e: Event) => {
this.listenerSetter.add(this.messageInput, 'beforeinput', (e: Event) => {
// * validate due to manual formatting through browser's context menu
const inputType = (e as InputEvent).inputType;
//console.log('message beforeinput event', e);
@ -661,42 +403,9 @@ export class ChatInput { @@ -661,42 +403,9 @@ export class ChatInput {
}
}
});
this.messageInput.addEventListener('input', this.onMessageInput);
if(this.markupTooltip) {
this.markupTooltip.handleSelection(this.messageInput);
}
this.listenerSetter.add(this.messageInput, 'input', this.onMessageInput);
}
private onDocumentPaste = (e: ClipboardEvent) => {
const peerID = rootScope.selectedPeerID;
if(!peerID || rootScope.overlayIsActive || (peerID < 0 && !appChatsManager.hasRights(peerID, 'send', 'send_media'))) {
return;
}
//console.log('document paste');
// @ts-ignore
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
//console.log('item', event.clipboardData.getData());
//let foundFile = false;
for(let i = 0; i < items.length; ++i) {
if(items[i].kind == 'file') {
e.preventDefault()
e.cancelBubble = true;
e.stopPropagation();
//foundFile = true;
let file = items[i].getAsFile();
//console.log(items[i], file);
if(!file) continue;
this.willAttachType = file.type.indexOf('image/') === 0 ? 'media' : "document";
new PopupNewMedia([file], this.willAttachType);
}
}
};
private prepareDocumentExecute = () => {
this.executedHistory.push(this.messageInput.innerHTML);
return () => this.canUndoFromHTML = this.messageInput.innerHTML;
@ -812,8 +521,8 @@ export class ChatInput { @@ -812,8 +521,8 @@ export class ChatInput {
checkForSingle();
saveExecuted();
if(this.markupTooltip) {
this.markupTooltip.setActiveMarkupButton();
if(this.appImManager.markupTooltip) {
this.appImManager.markupTooltip.setActiveMarkupButton();
}
return true;
@ -922,7 +631,7 @@ export class ChatInput { @@ -922,7 +631,7 @@ export class ChatInput {
url: url,
hash: 0
}).then((webpage) => {
webpage = appWebPagesManager.saveWebPage(webpage);
webpage = this.appWebPagesManager.saveWebPage(webpage);
if(webpage._ == 'webPage') {
if(this.lastUrl != url) return;
//console.log('got webpage: ', webpage);
@ -952,12 +661,12 @@ export class ChatInput { @@ -952,12 +661,12 @@ export class ChatInput {
if(!value.trim() && !serializeNodes(Array.from(this.messageInput.childNodes)).trim()) {
this.messageInput.innerHTML = '';
appMessagesManager.setTyping(rootScope.selectedPeerID, 'sendMessageCancelAction');
this.appMessagesManager.setTyping(this.chat.peerID, 'sendMessageCancelAction');
} else {
const time = Date.now();
if(time - this.lastTimeType >= 6000) {
this.lastTimeType = time;
appMessagesManager.setTyping(rootScope.selectedPeerID, 'sendMessageTypingAction');
this.appMessagesManager.setTyping(this.chat.peerID, 'sendMessageTypingAction');
}
}
@ -978,7 +687,7 @@ export class ChatInput { @@ -978,7 +687,7 @@ export class ChatInput {
this.sendMessage();
}
} else {
if(rootScope.selectedPeerID < 0 && !appChatsManager.hasRights(rootScope.selectedPeerID, 'send', 'send_media')) {
if(this.chat.peerID < 0 && !this.appChatsManager.hasRights(this.chat.peerID, 'send', 'send_media')) {
toast(POSTING_MEDIA_NOT_ALLOWED);
return;
}
@ -1097,9 +806,9 @@ export class ChatInput { @@ -1097,9 +806,9 @@ export class ChatInput {
}
});
} else if(this.helperType == 'reply') {
appImManager.setPeer(rootScope.selectedPeerID, this.replyToMsgID);
this.chat.setPeer(this.chat.peerID, this.replyToMsgID);
} else if(this.helperType == 'edit') {
appImManager.setPeer(rootScope.selectedPeerID, this.editMsgID);
this.chat.setPeer(this.chat.peerID, this.editMsgID);
}
};
@ -1134,9 +843,9 @@ export class ChatInput { @@ -1134,9 +843,9 @@ export class ChatInput {
}
public onMessageSent(clearInput = true, clearReply?: boolean) {
let dialog = appMessagesManager.getDialogByPeerID(appImManager.peerID)[0];
let dialog = this.appMessagesManager.getDialogByPeerID(this.chat.peerID)[0];
if(dialog && dialog.top_message) {
appMessagesManager.readHistory(appImManager.peerID, dialog.top_message); // lol
this.appMessagesManager.readHistory(this.chat.peerID, dialog.top_message); // lol
}
if(clearInput) {
@ -1162,11 +871,11 @@ export class ChatInput { @@ -1162,11 +871,11 @@ export class ChatInput {
//return;
if(this.editMsgID) {
appMessagesManager.editMessage(this.editMsgID, str, {
this.appMessagesManager.editMessage(this.editMsgID, str, {
noWebPage: this.noWebPage
});
} else {
appMessagesManager.sendText(appImManager.peerID, str, {
this.appMessagesManager.sendText(this.chat.peerID, str, {
replyToMsgID: this.replyToMsgID == 0 ? undefined : this.replyToMsgID,
noWebPage: this.noWebPage,
webPage: this.willSendWebPage
@ -1177,7 +886,7 @@ export class ChatInput { @@ -1177,7 +886,7 @@ export class ChatInput {
if(this.forwardingMids.length) {
const mids = this.forwardingMids.slice();
setTimeout(() => {
appMessagesManager.forwardMessages(appImManager.peerID, mids);
this.appMessagesManager.forwardMessages(this.chat.peerID, mids);
}, 0);
}
@ -1185,9 +894,9 @@ export class ChatInput { @@ -1185,9 +894,9 @@ export class ChatInput {
}
public sendMessageWithDocument(document: any) {
document = appDocsManager.getDoc(document);
document = this.appDocsManager.getDoc(document);
if(document && document._ != 'documentEmpty') {
appMessagesManager.sendFile(appImManager.peerID, document, {isMedia: true, replyToMsgID: this.replyToMsgID});
this.appMessagesManager.sendFile(this.chat.peerID, document, {isMedia: true, replyToMsgID: this.replyToMsgID});
this.onMessageSent(false, true);
if(document.type == 'sticker') {
@ -1201,7 +910,7 @@ export class ChatInput { @@ -1201,7 +910,7 @@ export class ChatInput {
}
public initMessageEditing(mid: number) {
const message = appMessagesManager.getMessage(mid);
const message = this.appMessagesManager.getMessage(mid);
let input = RichTextProcessor.wrapDraftText(message.message, {entities: message.totalEntities});
const f = () => {
@ -1216,7 +925,7 @@ export class ChatInput { @@ -1216,7 +925,7 @@ export class ChatInput {
const f = () => {
//const peerTitles: string[]
const smth: Set<string | number> = new Set(mids.map(mid => {
const message = appMessagesManager.getMessage(mid);
const message = this.appMessagesManager.getMessage(mid);
if(message.fwd_from && message.fwd_from.from_name && !message.fromID && !message.fwdFromID) {
return message.fwd_from.from_name;
} else {
@ -1227,13 +936,13 @@ export class ChatInput { @@ -1227,13 +936,13 @@ export class ChatInput {
const onlyFirstName = smth.size > 1;
const peerTitles = [...smth].map(smth => {
return typeof(smth) === 'number' ?
appPeersManager.getPeerTitle(smth, true, onlyFirstName) :
this.appPeersManager.getPeerTitle(smth, true, onlyFirstName) :
(onlyFirstName ? smth.split(' ')[0] : smth);
});
const title = peerTitles.length < 3 ? peerTitles.join(' and ') : peerTitles[0] + ' and ' + (peerTitles.length - 1) + ' others';
if(mids.length == 1) {
const message = appMessagesManager.getMessage(mids[0]);
const message = this.appMessagesManager.getMessage(mids[0]);
this.setTopInfo('forward', f, title, message.message, undefined, message);
} else {
this.setTopInfo('forward', f, title, mids.length + ' forwarded messages');
@ -1260,7 +969,7 @@ export class ChatInput { @@ -1260,7 +969,7 @@ export class ChatInput {
this.forwardingMids.length = 0;
this.editMsgID = 0;
this.helperType = this.helperFunc = undefined;
this.chatInput.parentElement.classList.remove('is-helper-active');
this.chat.container.classList.remove('is-helper-active');
}
public setTopInfo(type: ChatInputHelperType, callerFunc: () => void, title = '', subtitle = '', input?: string, message?: any) {
@ -1275,7 +984,7 @@ export class ChatInput { @@ -1275,7 +984,7 @@ export class ChatInput {
this.replyElements.container.append(wrapReply(title, subtitle, message));
}
this.chatInput.parentElement.classList.add('is-helper-active');
this.chat.container.classList.add('is-helper-active');
/* const scroll = appImManager.scrollable;
if(scroll.isScrolledDown && !scroll.scrollLocked && !appImManager.messagesQueuePromise && !appImManager.setPeerPromise) {
scroll.scrollTo(scroll.scrollHeight, 'top', true, true, 200);
@ -1297,7 +1006,7 @@ export class ChatInput { @@ -1297,7 +1006,7 @@ export class ChatInput {
}
public saveScroll() {
this.scrollTop = appImManager.scrollable.container.scrollTop;
this.scrollTop = this.chat.bubbles.scrollable.container.scrollTop;
this.scrollOffsetTop = this.chatInput.offsetTop;
}
@ -1305,7 +1014,7 @@ export class ChatInput { @@ -1305,7 +1014,7 @@ export class ChatInput {
if(this.chatInput.style.display) return;
//console.log('input resize', offsetTop, this.chatInput.offsetTop);
let newOffsetTop = this.chatInput.offsetTop;
let container = appImManager.scrollable.container;
let container = this.chat.bubbles.scrollable.container;
let scrollTop = container.scrollTop;
let clientHeight = container.clientHeight;
let maxScrollTop = container.scrollHeight;

244
src/components/chat/markupTooltip.ts

@ -0,0 +1,244 @@ @@ -0,0 +1,244 @@
import type { AppImManager } from "../../lib/appManagers/appImManager";
import { MarkdownType, cancelEvent, getSelectedNodes, markdownTags, findUpClassName } from "../../helpers/dom";
import RichTextProcessor from "../../lib/richtextprocessor";
import ButtonIcon from "../buttonIcon";
export default class MarkupTooltip {
public container: HTMLElement;
private wrapper: HTMLElement;
private buttons: {[type in MarkdownType]: HTMLElement} = {} as any;
private linkBackButton: HTMLElement;
private hideTimeout: number;
private addedListener = false;
private waitingForMouseUp = false;
private linkInput: HTMLInputElement;
private savedRange: Range;
constructor(private appImManager: AppImManager) {
}
private init() {
this.container = document.createElement('div');
this.container.classList.add('markup-tooltip', 'z-depth-1', 'hide');
this.wrapper = document.createElement('div');
this.wrapper.classList.add('markup-tooltip-wrapper');
const tools1 = document.createElement('div');
const tools2 = document.createElement('div');
tools1.classList.add('markup-tooltip-tools');
tools2.classList.add('markup-tooltip-tools');
const arr = ['bold', 'italic', 'underline', 'strikethrough', 'monospace', 'link'] as (keyof MarkupTooltip['buttons'])[];
arr.forEach(c => {
const button = ButtonIcon(c, {noRipple: true});
tools1.append(this.buttons[c] = button);
if(c !== 'link') {
button.addEventListener('click', () => {
this.appImManager.chat.input.applyMarkdown(c);
});
} else {
button.addEventListener('click', () => {
this.container.classList.add('is-link');
if(button.classList.contains('active')) {
const startContainer = this.savedRange.startContainer;
const anchor = startContainer.parentElement as HTMLAnchorElement;
this.linkInput.value = anchor.href;
} else {
this.linkInput.value = '';
}
});
}
});
this.linkBackButton = ButtonIcon('back', {noRipple: true});
this.linkInput = document.createElement('input');
this.linkInput.placeholder = 'Enter URL...';
this.linkInput.classList.add('input-clear');
this.linkInput.addEventListener('keydown', (e) => {
if(e.code == 'Enter') {
const valid = !this.linkInput.value.length || RichTextProcessor.matchUrl(this.linkInput.value);///^(http)|(https):\/\//i.test(this.linkInput.value);
if(!valid) {
if(this.linkInput.classList.contains('error')) {
this.linkInput.classList.remove('error');
void this.linkInput.offsetLeft; // reflow
}
this.linkInput.classList.add('error');
} else {
cancelEvent(e);
this.resetSelection();
this.appImManager.chat.input.applyMarkdown('link', this.linkInput.value);
this.hide();
}
} else {
this.linkInput.classList.remove('error');
}
});
this.linkBackButton.addEventListener('click', () => {
this.container.classList.remove('is-link');
//input.value = '';
this.resetSelection();
});
const delimiter1 = document.createElement('span');
const delimiter2 = document.createElement('span');
delimiter1.classList.add('markup-tooltip-delimiter');
delimiter2.classList.add('markup-tooltip-delimiter');
tools1.insertBefore(delimiter1, this.buttons.link);
tools2.append(this.linkBackButton, delimiter2, this.linkInput);
//tools1.insertBefore(delimiter2, this.buttons.link.nextSibling);
this.wrapper.append(tools1, tools2);
this.container.append(this.wrapper);
document.body.append(this.container);
}
private resetSelection() {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(this.savedRange);
this.appImManager.chat.input.messageInput.focus();
}
public hide() {
if(this.init) return;
this.container.classList.remove('is-visible');
document.removeEventListener('mouseup', this.onMouseUp);
if(this.hideTimeout) clearTimeout(this.hideTimeout);
this.hideTimeout = window.setTimeout(() => {
this.hideTimeout = undefined;
this.container.classList.add('hide');
this.container.classList.remove('is-link');
}, 200);
}
public getActiveMarkupButton() {
const nodes = getSelectedNodes();
const parents = [...new Set(nodes.map(node => node.parentNode))];
if(parents.length > 1) return undefined;
const node = parents[0] as HTMLElement;
let currentMarkup: HTMLElement;
for(const type in markdownTags) {
const tag = markdownTags[type as MarkdownType];
if(node.matches(tag.match)) {
currentMarkup = this.buttons[type as MarkdownType];
break;
}
}
return currentMarkup;
}
public setActiveMarkupButton() {
const activeButton = this.getActiveMarkupButton();
for(const i in this.buttons) {
// @ts-ignore
const button = this.buttons[i];
if(button != activeButton) {
button.classList.remove('active');
}
}
if(activeButton) {
activeButton.classList.add('active');
}
return activeButton;
}
public show() {
if(this.init) {
this.init();
this.init = null;
}
const selection = document.getSelection();
if(!selection.toString().trim().length) {
this.hide();
return;
}
if(this.hideTimeout !== undefined) {
clearTimeout(this.hideTimeout);
}
const range = this.savedRange = selection.getRangeAt(0);
const activeButton = this.setActiveMarkupButton();
this.container.classList.remove('is-link');
const isFirstShow = this.container.classList.contains('hide');
if(isFirstShow) {
this.container.classList.remove('hide');
this.container.classList.add('no-transition');
}
const selectionRect = range.getBoundingClientRect();
//const containerRect = this.container.getBoundingClientRect();
const sizesRect = this.container.firstElementChild.firstElementChild.getBoundingClientRect();
const top = selectionRect.top - sizesRect.height - 8;
const left = selectionRect.left + (selectionRect.width - sizesRect.width) / 2;
//const top = selectionRect.top - 44 - 8;
this.container.style.transform = `translate3d(${left}px, ${top}px, 0)`;
if(isFirstShow) {
void this.container.offsetLeft; // reflow
this.container.classList.remove('no-transition');
}
this.container.classList.add('is-visible');
//console.log('selection', selectionRect, activeButton);
}
private onMouseUp = (e: Event) => {
if(findUpClassName(e.target, 'markup-tooltip')) return;
this.hide();
document.removeEventListener('mouseup', this.onMouseUp);
};
public setMouseUpEvent() {
if(this.waitingForMouseUp) return;
this.waitingForMouseUp = true;
document.addEventListener('mouseup', (e) => {
this.waitingForMouseUp = false;
this.show();
document.addEventListener('mouseup', this.onMouseUp);
}, {once: true});
}
public handleSelection() {
if(this.addedListener) return;
this.addedListener = true;
document.addEventListener('selectionchange', (e) => {
if(document.activeElement == this.linkInput) {
return;
}
if(document.activeElement != this.appImManager.chat.input.messageInput) {
this.hide();
return;
}
const selection = document.getSelection();
if(!selection.toString().trim().length) {
this.hide();
return;
}
this.setMouseUpEvent();
});
}
}

19
src/components/chat/pinnedContainer.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import type Chat from "./chat";
import type ChatTopbar from "./topbar";
import mediaSizes from "../../helpers/mediaSizes";
import appImManager from "../../lib/appManagers/appImManager";
import { cancelEvent } from "../../helpers/dom";
import DivAndCaption from "../divAndCaption";
import { ripple } from "../ripple";
@ -12,7 +13,7 @@ export default class PinnedContainer { @@ -12,7 +13,7 @@ export default class PinnedContainer {
private close: HTMLElement;
protected wrapper: HTMLElement;
constructor(protected className: string, public divAndCaption: DivAndCaption<(title: string, subtitle: string, message?: any) => void>, onClose?: () => void | Promise<boolean>) {
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected className: string, public divAndCaption: DivAndCaption<(title: string, subtitle: string, message?: any) => void>, onClose?: () => void | Promise<boolean>) {
/* const prev = this.divAndCaption.fill;
this.divAndCaption.fill = (mid, title, subtitle) => {
this.divAndCaption.container.dataset.mid = '' + mid;
@ -38,7 +39,7 @@ export default class PinnedContainer { @@ -38,7 +39,7 @@ export default class PinnedContainer {
divAndCaption.container.append(this.close, this.wrapper);
this.close.addEventListener('click', (e) => {
this.topbar.listenerSetter.add(this.close, 'click', (e) => {
cancelEvent(e);
((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => {
@ -59,24 +60,24 @@ export default class PinnedContainer { @@ -59,24 +60,24 @@ export default class PinnedContainer {
this.divAndCaption.container.classList.toggle('is-floating', mediaSizes.isMobile);
const scrollTop = mediaSizes.isMobile /* && !appImManager.scrollable.isScrolledDown */ ? appImManager.scrollable.scrollTop : undefined;
const scrollTop = mediaSizes.isMobile /* && !appImManager.scrollable.isScrolledDown */ ? this.chat.bubbles.scrollable.scrollTop : undefined;
this.divAndCaption.container.classList.toggle('hide', hide);
const className = `is-pinned-${this.className}-shown`;
appImManager.topbar.classList.toggle(className, !hide);
this.topbar.container.classList.toggle(className, !hide);
const active = classNames.filter(className => appImManager.topbar.classList.contains(className));
const active = classNames.filter(className => this.topbar.container.classList.contains(className));
const maxActive = hide ? 0 : 1;
if(scrollTop !== undefined && active.length <= maxActive) {
appImManager.scrollable.scrollTop = scrollTop + ((hide ? -1 : 1) * HEIGHT);
this.chat.bubbles.scrollable.scrollTop = scrollTop + ((hide ? -1 : 1) * HEIGHT);
}
appImManager.setUtilsWidth();
this.topbar.setUtilsWidth();
}
public fill(title: string, subtitle: string, message: any) {
this.divAndCaption.container.dataset.mid = '' + message.mid;
this.divAndCaption.fill(title, subtitle, message);
appImManager.setUtilsWidth();
this.topbar.setUtilsWidth();
}
}

39
src/components/chat/pinnedMessage.ts

@ -1,13 +1,15 @@ @@ -1,13 +1,15 @@
import type { AppImManager } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type ChatTopbar from "./topbar";
import { ScreenSize } from "../../helpers/mediaSizes";
import appPeersManager from "../../lib/appManagers/appPeersManager";
import PopupPinMessage from "../popupUnpinMessage";
import PinnedContainer from "./pinnedContainer";
import PinnedMessageBorder from "./pinnedMessageBorder";
import ReplyContainer, { wrapReplyDivAndCaption } from "./replyContainer";
import rootScope from "../../lib/rootScope";
import { findUpClassName } from "../../helpers/dom";
import Chat from "./chat";
class AnimatedSuper {
static DURATION = 200;
@ -188,7 +190,7 @@ class AnimatedCounter { @@ -188,7 +190,7 @@ class AnimatedCounter {
}
}
export default class PinnedMessage {
export default class ChatPinnedMessage {
public pinnedMessageContainer: PinnedContainer;
public pinnedMessageBorder: PinnedMessageBorder;
public pinnedIndex = 0;
@ -200,17 +202,17 @@ export default class PinnedMessage { @@ -200,17 +202,17 @@ export default class PinnedMessage {
public animatedMedia: AnimatedSuper;
public animatedCounter: AnimatedCounter;
constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) {
this.pinnedMessageContainer = new PinnedContainer('message', new ReplyContainer('pinned-message'), () => {
if(appPeersManager.canPinMessage(this.appImManager.peerID)) {
new PopupPinMessage(this.appImManager.peerID, 0);
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
this.pinnedMessageContainer = new PinnedContainer(topbar, chat, 'message', new ReplyContainer('pinned-message'), () => {
if(appPeersManager.canPinMessage(this.topbar.peerID)) {
new PopupPinMessage(this.topbar.peerID, 0);
return Promise.resolve(false);
}
});
this.pinnedMessageBorder = new PinnedMessageBorder();
this.pinnedMessageContainer.divAndCaption.border.replaceWith(this.pinnedMessageBorder.render(1, 0));
this.appImManager.btnJoin.parentElement.insertBefore(this.pinnedMessageContainer.divAndCaption.container, this.appImManager.btnJoin);
this.topbar.btnJoin.parentElement.insertBefore(this.pinnedMessageContainer.divAndCaption.container, this.topbar.btnJoin);
this.animatedSubtitle = new AnimatedSuper();
this.pinnedMessageContainer.divAndCaption.subtitle.append(this.animatedSubtitle.container);
@ -223,17 +225,17 @@ export default class PinnedMessage { @@ -223,17 +225,17 @@ export default class PinnedMessage {
this.pinnedMessageContainer.divAndCaption.title.innerHTML = 'Pinned Message ';
this.pinnedMessageContainer.divAndCaption.title.append(this.animatedCounter.container);
rootScope.on('peer_pinned_messages', (e) => {
this.topbar.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => {
const peerID = e.detail;
if(peerID == this.appImManager.peerID) {
if(peerID == this.topbar.peerID) {
this.setPinnedMessage();
}
});
}
public setCorrectIndex(lastScrollDirection?: number) {
if(this.locked || this.appImManager.setPeerPromise) {
if(this.locked || this.chat.setPeerPromise) {
return;
}/* else if(this.waitForScrollBottom) {
if(lastScrollDirection === 1) {
@ -244,7 +246,7 @@ export default class PinnedMessage { @@ -244,7 +246,7 @@ export default class PinnedMessage {
} */
///const perf = performance.now();
const rect = this.appImManager.scrollable.container.getBoundingClientRect();
const rect = this.chat.bubbles.scrollable.container.getBoundingClientRect();
const x = Math.ceil(rect.left + ((rect.right - rect.left) / 2) + 1);
const y = Math.floor(rect.top + rect.height - 1);
let el: HTMLElement = document.elementFromPoint(x, y) as any;
@ -255,7 +257,7 @@ export default class PinnedMessage { @@ -255,7 +257,7 @@ export default class PinnedMessage {
if(el && el.dataset.mid !== undefined) {
const mid = +el.dataset.mid;
this.appMessagesManager.getPinnedMessages(this.appImManager.peerID).then(mids => {
this.appMessagesManager.getPinnedMessages(this.topbar.peerID).then(mids => {
let currentIndex = mids.findIndex(_mid => _mid <= mid);
if(currentIndex === -1) {
currentIndex = mids.length ? mids.length - 1 : 0;
@ -292,14 +294,14 @@ export default class PinnedMessage { @@ -292,14 +294,14 @@ export default class PinnedMessage {
this.pinnedIndex = index >= (mids.length - 1) ? 0 : index + 1;
this.setPinnedMessage();
const setPeerPromise = this.appImManager.setPeer(message.peerID, mid);
const setPeerPromise = this.chat.setPeer(message.peerID, mid);
if(setPeerPromise instanceof Promise) {
await setPeerPromise;
}
await this.appImManager.scrollable.scrollLockedPromise;
await this.chat.bubbles.scrollable.scrollLockedPromise;
} catch(err) {
this.appImManager.log.error('[PM]: followPinnedMessage error:', err);
this.chat.log.error('[PM]: followPinnedMessage error:', err);
}
// подождём, пока скролл остановится
@ -318,9 +320,9 @@ export default class PinnedMessage { @@ -318,9 +320,9 @@ export default class PinnedMessage {
public setPinnedMessage() {
/////this.log('setting pinned message', message);
//return;
const promise: Promise<any> = this.appImManager.setPeerPromise || this.appImManager.messagesQueuePromise || Promise.resolve();
const promise: Promise<any> = this.chat.setPeerPromise || this.chat.bubbles.messagesQueuePromise || Promise.resolve();
Promise.all([
this.appMessagesManager.getPinnedMessages(this.appImManager.peerID),
this.appMessagesManager.getPinnedMessages(this.topbar.peerID),
promise
]).then(([mids]) => {
//const mids = results[0];
@ -344,11 +346,12 @@ export default class PinnedMessage { @@ -344,11 +346,12 @@ export default class PinnedMessage {
const fromTop = pinnedIndex > this.wasPinnedIndex;
this.appImManager.log('[PM]: setPinnedMessage: fromTop', fromTop, pinnedIndex, this.wasPinnedIndex);
this.chat.log('[PM]: setPinnedMessage: fromTop', fromTop, pinnedIndex, this.wasPinnedIndex);
const writeTo = this.animatedSubtitle.getRow(pinnedIndex);
const writeMediaTo = this.animatedMedia.getRow(pinnedIndex);
writeMediaTo.classList.add('pinned-message-media');
//writeMediaTo.innerHTML = writeMediaTo.style.cssText = writeMediaTo.dataset.docID = '';
const isMediaSet = wrapReplyDivAndCaption({
title: undefined,
titleEl: null,

2
src/components/chat/pinnedMessageBorder.ts

@ -128,7 +128,7 @@ export default class PinnedMessageBorder { @@ -128,7 +128,7 @@ export default class PinnedMessageBorder {
if(this.count !== count) {
this.wrapper.className = 'pinned-message-border-wrapper-1';
this.border.classList.remove('pinned-message-border-mask');
this.wrapper.innerHTML = '';
this.wrapper.innerHTML = this.wrapper.style.cssText = '';
}
return this.border;

15
src/components/chat/replyContainer.ts

@ -35,18 +35,31 @@ export function wrapReplyDivAndCaption(options: { @@ -35,18 +35,31 @@ export function wrapReplyDivAndCaption(options: {
//console.log('wrap reply', media);
if(media.photo || (media.document && ['video', 'sticker', 'gif'].indexOf(media.document.type) !== -1)) {
/* const middlewareOriginal = appImManager.chat.bubbles.getMiddleware();
const middleware = () => {
}; */
if(media.document?.type == 'sticker') {
if(mediaEl.style.backgroundImage) {
mediaEl.style.backgroundImage = '';
}
setMedia = true;
wrapSticker({
doc: media.document,
div: mediaEl,
lazyLoadQueue: appImManager.lazyLoadQueue,
lazyLoadQueue: appImManager.chat.bubbles.lazyLoadQueue,
group: CHAT_ANIMATION_GROUP,
//onlyThumb: media.document.sticker == 2,
width: 32,
height: 32
});
} else {
if(mediaEl.firstElementChild) {
mediaEl.innerHTML = '';
}
const photo = media.photo || media.document;
const cacheContext = appPhotosManager.getCacheContext(photo);

35
src/components/chat/search.ts

@ -1,12 +1,13 @@ @@ -1,12 +1,13 @@
import appImManager from "../../lib/appManagers/appImManager";
import type ChatTopbar from "./topbar";
import rootScope from "../../lib/rootScope";
import { cancelEvent, whichChild, findUpTag } from "../../helpers/dom";
import AppSearch, { SearchGroup } from "../appSearch";
import PopupDatePicker from "../popupDatepicker";
import { ripple } from "../ripple";
import SearchInput from "../searchInput";
import type Chat from "./chat";
export class ChatSearch {
export default class ChatSearch {
private element: HTMLElement;
private backBtn: HTMLElement;
private searchInput: SearchInput;
@ -27,16 +28,16 @@ export class ChatSearch { @@ -27,16 +28,16 @@ export class ChatSearch {
private selectedIndex = 0;
private setPeerPromise: Promise<any>;
constructor() {
constructor(private topbar: ChatTopbar, private chat: Chat) {
this.element = document.createElement('div');
this.element.classList.add('sidebar-header', 'chat-search', 'chats-container');
this.element.classList.add('sidebar-header', 'chat-search', 'chatlist-container');
this.backBtn = document.createElement('button');
this.backBtn.classList.add('btn-icon', 'tgico-back', 'sidebar-close-button');
ripple(this.backBtn);
this.backBtn.addEventListener('click', () => {
appImManager.topbar.classList.remove('hide-pinned');
this.topbar.container.classList.remove('hide-pinned');
this.element.remove();
this.searchInput.remove();
this.results.remove();
@ -46,14 +47,14 @@ export class ChatSearch { @@ -46,14 +47,14 @@ export class ChatSearch {
this.upBtn.removeEventListener('click', this.onUpClick);
this.downBtn.removeEventListener('click', this.onDownClick);
this.searchGroup.list.removeEventListener('click', this.onResultsClick);
appImManager.bubblesContainer.classList.remove('search-results-active');
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
}, {once: true});
this.searchInput = new SearchInput('Search');
// Results
this.results = document.createElement('div');
this.results.classList.add('chat-search-results', 'chats-container');
this.results.classList.add('chat-search-results', 'chatlist-container');
this.searchGroup = new SearchGroup('', 'messages', undefined, '', false);
this.searchGroup.list.addEventListener('click', this.onResultsClick);
@ -66,17 +67,17 @@ export class ChatSearch { @@ -66,17 +67,17 @@ export class ChatSearch {
if(!this.foundCount) {
this.foundCountEl.innerText = this.searchInput.value ? 'No results' : '';
this.results.classList.remove('active');
appImManager.bubblesContainer.classList.remove('search-results-active');
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
this.upBtn.setAttribute('disabled', 'true');
this.downBtn.setAttribute('disabled', 'true');
} else {
this.selectResult(this.searchGroup.list.children[0] as HTMLElement);
}
});
this.appSearch.beginSearch(rootScope.selectedPeerID);
this.appSearch.beginSearch(this.chat.peerID);
//appImManager.topbar.parentElement.insertBefore(this.results, appImManager.bubblesContainer);
appImManager.bubblesContainer.append(this.results);
this.chat.bubbles.bubblesContainer.append(this.results);
// Footer
this.footer = document.createElement('div');
@ -109,20 +110,20 @@ export class ChatSearch { @@ -109,20 +110,20 @@ export class ChatSearch {
this.footer.append(this.foundCountEl, this.dateBtn, this.controls);
appImManager.topbar.parentElement.insertBefore(this.footer, appImManager.chatInput);
this.topbar.container.parentElement.insertBefore(this.footer, chat.input.chatInput);
// Append container
this.element.append(this.backBtn, this.searchInput.container);
appImManager.topbar.classList.add('hide-pinned');
appImManager.topbar.parentElement.append(this.element);
this.topbar.container.classList.add('hide-pinned');
this.topbar.container.parentElement.append(this.element);
this.searchInput.input.focus();
}
onDateClick = (e: MouseEvent) => {
cancelEvent(e);
new PopupDatePicker(new Date(), appImManager.onDatePick).show();
new PopupDatePicker(new Date(), this.chat.bubbles.onDatePick).show();
};
selectResult = (elem: HTMLElement) => {
@ -146,9 +147,9 @@ export class ChatSearch { @@ -146,9 +147,9 @@ export class ChatSearch {
}
this.results.classList.remove('active');
appImManager.bubblesContainer.classList.remove('search-results-active');
this.chat.bubbles.bubblesContainer.classList.remove('search-results-active');
const res = appImManager.setPeer(peerID, lastMsgID);
const res = this.chat.setPeer(peerID, lastMsgID);
this.setPeerPromise = (res instanceof Promise ? res : Promise.resolve(res)).then(() => {
this.selectedIndex = index;
this.foundCountEl.innerText = `${index + 1} of ${this.foundCount}`;
@ -171,7 +172,7 @@ export class ChatSearch { @@ -171,7 +172,7 @@ export class ChatSearch {
onFooterClick = (e: MouseEvent) => {
if(this.foundCount) {
appImManager.bubblesContainer.classList.toggle('search-results-active');
this.chat.bubbles.bubblesContainer.classList.toggle('search-results-active');
this.results.classList.toggle('active');
}
};

53
src/components/chat/selection.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { isTouchSupported } from "../../helpers/touchSupport";
import type { AppImManager } from "../../lib/appManagers/appImManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type ChatBubbles from "./bubbles";
import type ChatInput from "./input";
import { isTouchSupported } from "../../helpers/touchSupport";
import { blurActiveElement, cancelEvent, cancelSelection, findUpClassName, getSelectedText } from "../../helpers/dom";
import Button from "../button";
import ButtonIcon from "../buttonIcon";
@ -9,6 +10,7 @@ import PopupDeleteMessages from "../popupDeleteMessages"; @@ -9,6 +10,7 @@ import PopupDeleteMessages from "../popupDeleteMessages";
import PopupForward from "../popupForward";
import { toast } from "../toast";
import SetTransition from "../singleTransition";
import ListenerSetter from "../../helpers/listenerSetter";
const MAX_SELECTION_LENGTH = 100;
//const MIN_CLICK_MOVE = 32; // minimum bubble height
@ -24,18 +26,21 @@ export default class ChatSelection { @@ -24,18 +26,21 @@ export default class ChatSelection {
public selectedText: string;
constructor(private appImManager: AppImManager, private appMessagesManager: AppMessagesManager) {
const bubblesContainer = appImManager.bubblesContainer;
private listenerSetter: ListenerSetter;
constructor(private chatBubbles: ChatBubbles, private chatInput: ChatInput, private appMessagesManager: AppMessagesManager) {
const bubblesContainer = chatBubbles.bubblesContainer;
this.listenerSetter = chatBubbles.listenerSetter;
if(isTouchSupported) {
bubblesContainer.addEventListener('touchend', (e) => {
this.listenerSetter.add(bubblesContainer, 'touchend', (e) => {
if(!this.isSelecting) return;
this.selectedText = getSelectedText();
});
return;
}
bubblesContainer.addEventListener('mousedown', (e) => {
this.listenerSetter.add(bubblesContainer, 'mousedown', (e) => {
//console.log('selection mousedown', e);
const bubble = findUpClassName(e.target, 'bubble');
// LEFT BUTTON
@ -92,8 +97,8 @@ export default class ChatSelection { @@ -92,8 +97,8 @@ export default class ChatSelection {
// * cancel selecting if selecting message text
if(e.target != bubble && selecting === undefined && !this.selectedMids.size) {
bubblesContainer.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove);
this.listenerSetter.removeManual(document, 'mouseup', onMouseUp, documentListenerOptions);
return;
}
@ -110,7 +115,7 @@ export default class ChatSelection { @@ -110,7 +115,7 @@ export default class ChatSelection {
if(!this.selectedMids.size) {
if(seen.size == 2) {
[...seen].forEach(mid => {
const mounted = this.appImManager.getMountedBubble(mid);
const mounted = this.chatBubbles.getMountedBubble(mid);
if(mounted) {
this.toggleByBubble(mounted.bubble);
}
@ -131,15 +136,16 @@ export default class ChatSelection { @@ -131,15 +136,16 @@ export default class ChatSelection {
}, {capture: true, once: true, passive: false});
}
bubblesContainer.removeEventListener('mousemove', onMouseMove);
this.listenerSetter.removeManual(bubblesContainer, 'mousemove', onMouseMove);
//bubblesContainer.classList.remove('no-select');
// ! CANCEL USER SELECTION !
cancelSelection();
};
bubblesContainer.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp, {once: true});
const documentListenerOptions = {once: true};
this.listenerSetter.add(bubblesContainer, 'mousemove', onMouseMove);
this.listenerSetter.add(document, 'mouseup', onMouseUp, documentListenerOptions);
});
}
@ -165,7 +171,7 @@ export default class ChatSelection { @@ -165,7 +171,7 @@ export default class ChatSelection {
}
if(isAlbum) {
this.appImManager.getBubbleAlbumItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show));
this.chatBubbles.getBubbleAlbumItems(bubble).forEach(item => this.toggleBubbleCheckbox(item, show));
}
}
@ -207,7 +213,7 @@ export default class ChatSelection { @@ -207,7 +213,7 @@ export default class ChatSelection {
if(wasSelecting == this.isSelecting) return;
const bubblesContainer = this.appImManager.bubblesContainer;
const bubblesContainer = this.chatBubbles.bubblesContainer;
//bubblesContainer.classList.toggle('is-selecting', !!this.selectedMids.size);
/* if(bubblesContainer.classList.contains('is-chat-input-hidden')) {
@ -236,7 +242,7 @@ export default class ChatSelection { @@ -236,7 +242,7 @@ export default class ChatSelection {
}
window.requestAnimationFrame(() => {
this.appImManager.onScroll();
this.chatBubbles.onScroll();
});
});
@ -244,19 +250,18 @@ export default class ChatSelection { @@ -244,19 +250,18 @@ export default class ChatSelection {
if(this.isSelecting) {
if(!this.selectionContainer) {
const inputMessageDiv = document.querySelector('.input-message');
this.selectionContainer = document.createElement('div');
this.selectionContainer.classList.add('selection-container');
const btnCancel = ButtonIcon('close', {noRipple: true});
btnCancel.addEventListener('click', this.cancelSelection, {once: true});
this.listenerSetter.add(btnCancel, 'click', this.cancelSelection, {once: true});
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.selectionForwardBtn.addEventListener('click', () => {
this.listenerSetter.add(this.selectionForwardBtn, 'click', () => {
new PopupForward([...this.selectedMids], () => {
this.cancelSelection();
});
@ -264,7 +269,7 @@ export default class ChatSelection { @@ -264,7 +269,7 @@ export default class ChatSelection {
this.selectionDeleteBtn = Button('btn-primary btn-transparent danger selection-container-delete', {icon: 'delete'});
this.selectionDeleteBtn.append('Delete');
this.selectionDeleteBtn.addEventListener('click', () => {
this.listenerSetter.add(this.selectionDeleteBtn, 'click', () => {
new PopupDeleteMessages([...this.selectedMids], () => {
this.cancelSelection();
});
@ -272,13 +277,13 @@ export default class ChatSelection { @@ -272,13 +277,13 @@ export default class ChatSelection {
this.selectionContainer.append(btnCancel, this.selectionCountEl, this.selectionForwardBtn, this.selectionDeleteBtn);
inputMessageDiv.append(this.selectionContainer);
this.chatInput.rowsWrapper.append(this.selectionContainer);
}
}
if(toggleCheckboxes) {
for(const mid in this.appImManager.bubbles) {
const bubble = this.appImManager.bubbles[mid];
for(const mid in this.chatBubbles.bubbles) {
const bubble = this.chatBubbles.bubbles[mid];
this.toggleBubbleCheckbox(bubble, this.isSelecting);
}
}
@ -286,7 +291,7 @@ export default class ChatSelection { @@ -286,7 +291,7 @@ export default class ChatSelection {
public cancelSelection = () => {
for(const mid of this.selectedMids) {
const mounted = this.appImManager.getMountedBubble(mid);
const mounted = this.chatBubbles.getMountedBubble(mid);
if(mounted) {
this.toggleByBubble(mounted.message.grouped_id ? mounted.bubble.querySelector(`.album-item[data-mid="${mid}"]`) : mounted.bubble);
}
@ -337,7 +342,7 @@ export default class ChatSelection { @@ -337,7 +342,7 @@ export default class ChatSelection {
mids.forEach(mid => this.selectedMids.delete(mid));
}
this.appImManager.getBubbleAlbumItems(bubble).forEach(this.toggleByBubble);
this.chatBubbles.getBubbleAlbumItems(bubble).forEach(this.toggleByBubble);
return;
}

90
src/components/chat/stickersHelper.ts

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
import { findUpClassName } from "../../helpers/dom";
import { MyDocument } from "../../lib/appManagers/appDocsManager";
import appImManager, { CHAT_ANIMATION_GROUP } from "../../lib/appManagers/appImManager";
import appStickersManager from "../../lib/appManagers/appStickersManager";
import { EmoticonsDropdown } from "../emoticonsDropdown";
import { SuperStickerRenderer } from "../emoticonsDropdown/tabs/stickers";
import LazyLoadQueue from "../lazyLoadQueue";
import Scrollable from "../scrollable";
import SetTransition from "../singleTransition";
export default class StickersHelper {
private container: HTMLElement;
private stickersContainer: HTMLElement;
private scrollable: Scrollable;
private superStickerRenderer: SuperStickerRenderer;
private lazyLoadQueue: LazyLoadQueue;
private lastEmoticon = '';
constructor(private appendTo: HTMLElement) {
}
public checkEmoticon(emoticon: string) {
if(this.lastEmoticon == emoticon) return;
if(this.lastEmoticon && !emoticon) {
if(this.container) {
SetTransition(this.container, 'is-visible', false, 200/* , () => {
this.stickersContainer.innerHTML = '';
} */);
}
}
this.lastEmoticon = emoticon;
if(this.lazyLoadQueue) {
this.lazyLoadQueue.clear();
}
if(!emoticon) {
return;
}
appStickersManager.getStickersByEmoticon(emoticon)
.then(stickers => {
if(this.lastEmoticon != emoticon) {
return;
}
if(this.init) {
this.init();
this.init = null;
}
this.stickersContainer.innerHTML = '';
this.lazyLoadQueue.clear();
if(stickers.length) {
stickers.forEach(sticker => {
this.stickersContainer.append(this.superStickerRenderer.renderSticker(sticker as MyDocument));
});
}
SetTransition(this.container, 'is-visible', true, 200);
this.scrollable.scrollTop = 0;
});
}
private init() {
this.container = document.createElement('div');
this.container.classList.add('stickers-helper', 'z-depth-1');
this.stickersContainer = document.createElement('div');
this.stickersContainer.classList.add('stickers-helper-stickers', 'super-stickers');
this.stickersContainer.addEventListener('click', (e) => {
if(!findUpClassName(e.target, 'super-sticker')) {
return;
}
appImManager.chat.input.clearInput();
EmoticonsDropdown.onMediaClick(e);
});
this.container.append(this.stickersContainer);
this.scrollable = new Scrollable(this.container);
this.lazyLoadQueue = new LazyLoadQueue();
this.superStickerRenderer = new SuperStickerRenderer(this.lazyLoadQueue, CHAT_ANIMATION_GROUP);
this.appendTo.append(this.container);
}
}

345
src/components/chat/topbar.ts

@ -0,0 +1,345 @@ @@ -0,0 +1,345 @@
import type { AppChatsManager, Channel } from "../../lib/appManagers/appChatsManager";
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import type { AppProfileManager } from "../../lib/appManagers/appProfileManager";
import type { AppUsersManager } from "../../lib/appManagers/appUsersManager";
import type { AppSidebarRight } from "../sidebarRight";
import type Chat from "./chat";
import { findUpClassName, cancelEvent, attachClickEvent } from "../../helpers/dom";
import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes";
import { isSafari } from "../../helpers/userAgent";
import rootScope from "../../lib/rootScope";
import AvatarElement from "../avatar";
import Button from "../button";
import ButtonIcon from "../buttonIcon";
import ButtonMenuToggle from "../buttonMenuToggle";
import ChatAudio from "./audio";
import ChatPinnedMessage from "./pinnedMessage";
import ChatSearch from "./search";
import { ButtonMenuItemOptions } from "../buttonMenu";
import ListenerSetter from "../../helpers/listenerSetter";
export default class ChatTopbar {
container: HTMLDivElement;
btnBack: HTMLButtonElement;
chatInfo: HTMLDivElement;
avatarElement: AvatarElement;
title: HTMLDivElement;
subtitle: HTMLDivElement;
chatUtils: HTMLDivElement;
btnJoin: HTMLButtonElement;
btnMute: HTMLButtonElement;
btnSearch: HTMLButtonElement;
btnMore: HTMLButtonElement;
public chatAudio: ChatAudio;
public pinnedMessage: ChatPinnedMessage;
private setUtilsRAF: number;
public peerID: number;
private setPeerStatusInterval: number;
public listenerSetter: ListenerSetter;
constructor(private chat: Chat, private appSidebarRight: AppSidebarRight, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager, private appChatsManager: AppChatsManager, private appUsersManager: AppUsersManager, private appProfileManager: AppProfileManager) {
this.chat.log.error('Topbar construction');
this.listenerSetter = new ListenerSetter();
this.container = document.createElement('div');
this.container.classList.add('sidebar-header', 'topbar');
this.btnBack = ButtonIcon('back sidebar-close-button', {noRipple: true});
// * chat info section
this.chatInfo = document.createElement('div');
this.chatInfo.classList.add('chat-info');
const person = document.createElement('div');
person.classList.add('person');
this.avatarElement = new AvatarElement();
this.avatarElement.setAttribute('dialog', '1');
this.avatarElement.setAttribute('clickable', '');
const content = document.createElement('div');
content.classList.add('content');
const top = document.createElement('div');
top.classList.add('top');
this.title = document.createElement('div');
this.title.classList.add('user-title');
top.append(this.title);
const bottom = document.createElement('div');
bottom.classList.add('bottom');
this.subtitle = document.createElement('div');
this.subtitle.classList.add('info');
bottom.append(this.subtitle);
content.append(top, bottom);
person.append(this.avatarElement, content);
this.chatInfo.append(person);
// * chat utils section
this.chatUtils = document.createElement('div');
this.chatUtils.classList.add('chat-utils');
this.chatAudio = new ChatAudio(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.btnJoin = Button('btn-primary chat-join hide');
this.btnJoin.append('SUBSCRIBE');
this.btnMute = ButtonIcon('mute');
this.btnSearch = ButtonIcon('search');
const menuButtons: (ButtonMenuItemOptions & {verify: () => boolean})[] = [{
icon: 'search',
text: 'Search',
onClick: () => {
new ChatSearch(this, this.chat);
},
verify: () => mediaSizes.isMobile
}, {
icon: 'mute',
text: 'Mute',
onClick: () => {
this.appMessagesManager.mutePeer(this.peerID);
},
verify: () => rootScope.myID != this.peerID && !this.appMessagesManager.isPeerMuted(this.peerID)
}, {
icon: 'unmute',
text: 'Unmute',
onClick: () => {
this.appMessagesManager.mutePeer(this.peerID);
},
verify: () => rootScope.myID != this.peerID && this.appMessagesManager.isPeerMuted(this.peerID)
}, {
icon: 'delete danger',
text: 'Delete and Leave',
onClick: () => {},
verify: () => true
}];
//menuButtons.forEach(b => b.options = {listenerSetter: this.listenerSetter});
this.btnMore = ButtonMenuToggle({listenerSetter: this.listenerSetter}, 'bottom-left', menuButtons, () => {
menuButtons.forEach(button => {
button.element.classList.toggle('hide', !button.verify());
});
});
this.chatUtils.append(this.chatAudio.divAndCaption.container, this.btnJoin, this.btnMute, this.btnSearch, this.btnMore);
this.container.append(this.btnBack, this.chatInfo, this.chatUtils);
// * construction end
// * fix topbar overflow section
this.listenerSetter.add(window, 'resize', this.onResize);
mediaSizes.addListener('changeScreen', this.onChangeScreen);
this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.listenerSetter.add(this.container, 'click', (e) => {
const pinned: HTMLElement = findUpClassName(e.target, 'pinned-container');
if(pinned) {
cancelEvent(e);
const mid = +pinned.dataset.mid;
if(pinned.classList.contains('pinned-message')) {
this.pinnedMessage.followPinnedMessage(mid);
} else {
const message = this.appMessagesManager.getMessage(mid);
this.chat.setPeer(message.peerID, mid);
}
} else {
this.appSidebarRight.toggleSidebar(true);
}
});
this.listenerSetter.add(this.btnBack, 'click', (e) => {
cancelEvent(e);
this.chat.appImManager.setPeer(0);
});
this.listenerSetter.add(this.btnSearch, 'click', (e) => {
cancelEvent(e);
if(this.peerID) {
appSidebarRight.searchTab.open(this.peerID);
}
});
this.listenerSetter.add(this.btnMute, 'click', (e) => {
cancelEvent(e);
this.appMessagesManager.mutePeer(this.peerID);
});
//this.listenerSetter.add(this.btnJoin, 'mousedown', (e) => {
attachClickEvent(this.btnJoin, (e) => {
cancelEvent(e);
this.btnJoin.setAttribute('disabled', 'true');
appChatsManager.joinChannel(-this.peerID).finally(() => {
this.btnJoin.removeAttribute('disabled');
});
//});
}, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope, 'chat_update', (e) => {
const peerID: number = e.detail;
if(this.peerID == -peerID) {
const chat = appChatsManager.getChat(peerID) as Channel/* | Chat */;
this.btnJoin.classList.toggle('hide', !(chat as Channel)?.pFlags?.left);
this.setUtilsWidth();
}
});
this.listenerSetter.add(rootScope, 'dialog_notify_settings', (e) => {
const peerID = e.detail;
if(peerID == this.peerID) {
this.setMutedState();
}
});
this.listenerSetter.add(rootScope, 'peer_typings', (e) => {
const {peerID} = e.detail;
if(this.peerID == peerID) {
this.setPeerStatus();
}
});
this.listenerSetter.add(rootScope, 'user_update', (e) => {
const userID = e.detail;
if(this.peerID == userID) {
this.setPeerStatus();
}
});
this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3);
}
private onResize = () => {
this.setUtilsWidth(true);
};
private onChangeScreen = (from: ScreenSize, to: ScreenSize) => {
this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to == ScreenSize.mobile);
this.pinnedMessage.onChangeScreen(from, to);
this.setUtilsWidth(true);
};
public destroy() {
this.chat.log.error('Topbar destroying');
this.listenerSetter.removeAll();
mediaSizes.removeListener('changeScreen', this.onChangeScreen);
window.clearInterval(this.setPeerStatusInterval);
delete this.chatAudio;
delete this.pinnedMessage;
}
public setPeer(peerID: number) {
this.peerID = peerID;
this.avatarElement.setAttribute('peer', '' + peerID);
this.avatarElement.update();
this.container.classList.remove('is-pinned-shown');
this.container.style.display = peerID ? '' : 'none';
const isBroadcast = this.appPeersManager.isBroadcast(peerID);
this.btnMute.classList.toggle('hide', !isBroadcast);
this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(-peerID)?.pFlags?.left);
this.setUtilsWidth();
window.requestAnimationFrame(() => {
this.pinnedMessage.pinnedIndex/* = this.pinnedMessage.wasPinnedIndex */ = 0;
//this.pinnedMessage.setCorrectIndex();
this.pinnedMessage.setPinnedMessage();
/* noTransition.forEach(el => {
el.classList.remove('no-transition-all');
}); */
/* if(needToChangeInputDisplay) {
this.chatInput.style.display = '';
} */
let title = '';
if(peerID == rootScope.myID) title = 'Saved Messages';
else title = this.appPeersManager.getPeerTitle(peerID);
this.title.innerHTML = title;
this.setPeerStatus(true);
this.setMutedState();
});
}
public setMutedState() {
const peerID = this.peerID;
let muted = this.appMessagesManager.isPeerMuted(peerID);
if(this.appPeersManager.isBroadcast(peerID)) { // not human
this.btnMute.classList.remove('tgico-mute', 'tgico-unmute');
this.btnMute.classList.add(muted ? 'tgico-unmute' : 'tgico-mute');
this.btnMute.style.display = '';
} else {
this.btnMute.style.display = 'none';
}
}
// ! У МЕНЯ ПРОСТО СГОРЕЛО, САФАРИ КОНЧЕННЫЙ БРАУЗЕР - ЕСЛИ НЕ СКРЫВАТЬ БЛОК, ТО ПРИ ПЕРЕВОРОТЕ ЭКРАНА НА АЙФОНЕ БЛОК БУДЕТ НЕПРАВИЛЬНО ШИРИНЫ, ДАЖЕ БЕЗ ЭТОЙ ФУНКЦИИ!
public setUtilsWidth = (resize = false) => {
//return;
if(this.setUtilsRAF) window.cancelAnimationFrame(this.setUtilsRAF);
if(isSafari && resize) {
this.chatUtils.classList.add('hide');
}
//mutationObserver.disconnect();
this.setUtilsRAF = window.requestAnimationFrame(() => {
//mutationRAF = window.requestAnimationFrame(() => {
//setTimeout(() => {
if(isSafari && resize) {
this.chatUtils.classList.remove('hide');
}
/* this.chatInfo.style.removeProperty('--utils-width');
void this.chatInfo.offsetLeft; // reflow */
const width = /* chatUtils.scrollWidth */this.chatUtils.getBoundingClientRect().width;
this.chat.log('utils width:', width);
this.chatInfo.style.setProperty('--utils-width', width + 'px');
//this.chatInfo.classList.toggle('have-utils-width', !!width);
//}, 0);
this.setUtilsRAF = 0;
//mutationObserver.observe(chatUtils, observeOptions);
//});
});
};
public setPeerStatus = (needClear = false) => {
const peerID = this.peerID;
if(needClear) {
this.subtitle.innerHTML = '';
}
this.chat.appImManager.getPeerStatus(this.peerID).then((subtitle) => {
if(peerID != this.peerID) {
return;
}
this.subtitle.innerHTML = subtitle;
});
};
}

3
src/components/dialogsContextMenu.ts

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import appChatsManager from "../lib/appManagers/appChatsManager";
import appDialogsManager from "../lib/appManagers/appDialogsManager";
import appImManager from "../lib/appManagers/appImManager";
import appMessagesManager, {Dialog} from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import rootScope from "../lib/rootScope";
@ -102,7 +101,7 @@ export default class DialogsContextMenu { @@ -102,7 +101,7 @@ export default class DialogsContextMenu {
};
private onMuteClick = () => {
appImManager.mutePeer(this.selectedID);
appMessagesManager.mutePeer(this.selectedID);
};
private onUnreadClick = () => {

2
src/components/divAndCaption.ts

@ -5,7 +5,7 @@ export default class DivAndCaption<T> { @@ -5,7 +5,7 @@ export default class DivAndCaption<T> {
public title: HTMLElement;
public subtitle: HTMLElement;
constructor(protected className: string, public fill: T) {
constructor(protected className: string, public fill?: T) {
this.container = document.createElement('div');
this.container.className = className;

38
src/components/emoticonsDropdown/index.ts

@ -40,7 +40,6 @@ export class EmoticonsDropdown { @@ -40,7 +40,6 @@ export class EmoticonsDropdown {
public searchButton: HTMLElement;
public deleteBtn: HTMLElement;
public toggleEl: HTMLElement;
private displayTimeout: number;
public events: {
@ -57,26 +56,28 @@ export class EmoticonsDropdown { @@ -57,26 +56,28 @@ export class EmoticonsDropdown {
private selectTab: ReturnType<typeof horizontalMenu>;
private firstTime = true;
constructor() {
this.element = document.getElementById('emoji-dropdown') as HTMLDivElement;
}
let firstTime = true;
this.toggleEl = document.getElementById('toggle-emoticons');
public attachButtonListener(button: HTMLElement) {
if(isTouchSupported) {
this.toggleEl.addEventListener('click', () => {
if(firstTime) {
firstTime = false;
button.addEventListener('click', () => {
if(this.firstTime) {
this.firstTime = false;
this.toggle(true);
} else {
this.toggle();
}
});
} else {
this.toggleEl.onmouseover = (e) => {
button.onmouseover = (e) => {
clearTimeout(this.displayTimeout);
//this.displayTimeout = setTimeout(() => {
if(firstTime) {
this.toggleEl.onmouseout = this.element.onmouseout = (e) => {
if(this.firstTime) {
button.onmouseout = this.element.onmouseout = (e) => {
if(test) return;
if(!this.element.classList.contains('active')) return;
@ -95,7 +96,7 @@ export class EmoticonsDropdown { @@ -95,7 +96,7 @@ export class EmoticonsDropdown {
clearTimeout(this.displayTimeout);
};
firstTime = false;
this.firstTime = false;
}
this.toggle(true);
@ -138,7 +139,7 @@ export class EmoticonsDropdown { @@ -138,7 +139,7 @@ export class EmoticonsDropdown {
this.deleteBtn = this.element.querySelector('.emoji-tabs-delete');
this.deleteBtn.addEventListener('click', () => {
const input = appImManager.chatInputC.messageInput;
const input = appImManager.chat.input.messageInput;
if((input.lastChild as any)?.tagName) {
input.lastElementChild.remove();
} else if(input.lastChild) {
@ -150,7 +151,7 @@ export class EmoticonsDropdown { @@ -150,7 +151,7 @@ export class EmoticonsDropdown {
}
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chatInputC.messageInput.dispatchEvent(event);
appImManager.chat.input.messageInput.dispatchEvent(event);
//appSidebarRight.stickersTab.init();
});
@ -174,7 +175,7 @@ export class EmoticonsDropdown { @@ -174,7 +175,7 @@ export class EmoticonsDropdown {
};
public checkRights = () => {
const peerID = rootScope.selectedPeerID;
const peerID = appImManager.chat.peerID;
const children = this.tabsEl.children;
const tabsElements = Array.from(children) as HTMLElement[];
@ -206,17 +207,18 @@ export class EmoticonsDropdown { @@ -206,17 +207,18 @@ export class EmoticonsDropdown {
}
if(isTouchSupported) {
this.toggleEl.classList.toggle('flip-icon', willBeActive);
if(willBeActive) {
appImManager.chatInputC.saveScroll();
appImManager.chat.input.saveScroll();
// @ts-ignore
document.activeElement.blur();
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
}
} else {
this.toggleEl.classList.toggle('active', enable);
}
if(this.element.parentElement !== appImManager.chat.input.chatInput) {
appImManager.chat.input.chatInput.append(this.element);
}
if((this.element.style.display && enable === undefined) || enable) {
@ -349,7 +351,7 @@ export class EmoticonsDropdown { @@ -349,7 +351,7 @@ export class EmoticonsDropdown {
const fileID = target.dataset.docID;
if(!fileID) return;
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
if(appImManager.chat.input.sendMessageWithDocument(fileID)) {
/* dropdown.classList.remove('active');
toggleEl.classList.remove('active'); */
emoticonsDropdown.toggle(false);

4
src/components/emoticonsDropdown/tabs/emoji.ts

@ -187,7 +187,7 @@ export default class EmojiTab implements EmoticonsTab { @@ -187,7 +187,7 @@ export default class EmojiTab implements EmoticonsTab {
//console.log('contentEmoji div', target);
appImManager.chatInputC.messageInput.innerHTML += target.outerHTML;
appImManager.chat.input.messageInput.innerHTML += target.outerHTML;
// Recent
const emoji = this.getEmojiFromElement(target);
@ -210,7 +210,7 @@ export default class EmojiTab implements EmoticonsTab { @@ -210,7 +210,7 @@ export default class EmojiTab implements EmoticonsTab {
// Append to input
const event = new Event('input', {bubbles: true, cancelable: true});
appImManager.chatInputC.messageInput.dispatchEvent(event);
appImManager.chat.input.messageInput.dispatchEvent(event);
};
onClose() {

2
src/components/emoticonsDropdown/tabs/stickers.ts

@ -338,7 +338,7 @@ export default class StickersTab implements EmoticonsTab { @@ -338,7 +338,7 @@ export default class StickersTab implements EmoticonsTab {
}
pushRecentSticker(doc: MyDocument) {
if(!this.recentDiv.parentElement) {
if(!this.recentDiv?.parentElement) {
return;
}

10
src/components/inputField.ts

@ -37,7 +37,7 @@ let init = () => { @@ -37,7 +37,7 @@ let init = () => {
const InputField = (options: {
placeholder?: string,
label?: string,
name: string,
name?: string,
maxLength?: number,
showLengthOn?: number,
plainText?: true
@ -57,8 +57,8 @@ const InputField = (options: { @@ -57,8 +57,8 @@ const InputField = (options: {
}
div.innerHTML = `
<div id="input-${name}" ${placeholder ? `data-placeholder="${placeholder}"` : ''} contenteditable="true" class="input-field-input"></div>
${label ? `<label for="input-${name}">${label}</label>` : ''}
<div ${placeholder ? `data-placeholder="${placeholder}"` : ''} contenteditable="true" class="input-field-input"></div>
${label ? `<label>${label}</label>` : ''}
`;
const input = div.firstElementChild as HTMLElement;
@ -86,8 +86,8 @@ const InputField = (options: { @@ -86,8 +86,8 @@ const InputField = (options: {
observer.observe(input, {characterData: true, childList: true, subtree: true});
} else {
div.innerHTML = `
<input type="text" name="${name}" id="input-${name}" ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" required="" class="input-field-input">
${label ? `<label for="input-${name}">${label}</label>` : ''}
<input type="text" name="${name}" ${placeholder ? `placeholder="${placeholder}"` : ''} autocomplete="off" required="" class="input-field-input">
${label ? `<label>${label}</label>` : ''}
`;
}

22
src/components/misc.ts

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import Countries, { Country, PhoneCodesMain } from "../countries";
import { cancelEvent, CLICK_EVENT_NAME } from "../helpers/dom";
import ListenerSetter from "../helpers/listenerSetter";
import mediaSizes from "../helpers/mediaSizes";
import { clamp } from "../helpers/number";
import { isTouchSupported } from "../helpers/touchSupport";
@ -295,7 +296,10 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme @@ -295,7 +296,10 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme
(side == 'center' ? side : (side == 'left' ? 'right' : 'left')));
}
export function attachContextMenuListener(element: HTMLElement, callback: (e: Touch | MouseEvent) => void) {
export function attachContextMenuListener(element: HTMLElement, callback: (e: Touch | MouseEvent) => void, listenerSetter?: ListenerSetter) {
const add = listenerSetter ? listenerSetter.add.bind(listenerSetter, element) : element.addEventListener.bind(element);
const remove = listenerSetter ? listenerSetter.removeManual.bind(listenerSetter, element) : element.removeEventListener.bind(element);
if(isApple && isTouchSupported) {
let timeout: number;
@ -303,20 +307,20 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To @@ -303,20 +307,20 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To
const onCancel = () => {
clearTimeout(timeout);
element.removeEventListener('touchmove', onCancel, options);
element.removeEventListener('touchend', onCancel, options);
element.removeEventListener('touchcancel', onCancel, options);
remove('touchmove', onCancel, options);
remove('touchend', onCancel, options);
remove('touchcancel', onCancel, options);
};
element.addEventListener('touchstart', (e) => {
add('touchstart', (e: TouchEvent) => {
if(e.touches.length > 1) {
onCancel();
return;
}
element.addEventListener('touchmove', onCancel, options);
element.addEventListener('touchend', onCancel, options);
element.addEventListener('touchcancel', onCancel, options);
add('touchmove', onCancel, options);
add('touchend', onCancel, options);
add('touchcancel', onCancel, options);
timeout = window.setTimeout(() => {
element.addEventListener('touchend', cancelEvent, {once: true}); // * fix instant closing
@ -325,6 +329,6 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To @@ -325,6 +329,6 @@ export function attachContextMenuListener(element: HTMLElement, callback: (e: To
}, .4e3);
});
} else {
element.addEventListener('contextmenu', callback);
add('contextmenu', callback);
}
};

2
src/components/poll.ts

@ -118,7 +118,7 @@ const setQuizHint = (solution: string, solution_entities: any[], onHide: () => v @@ -118,7 +118,7 @@ const setQuizHint = (solution: string, solution_entities: any[], onHide: () => v
element.append(container);
textEl.innerHTML = RichTextProcessor.wrapRichText(solution, {entities: solution_entities});
appImManager.bubblesContainer.append(element);
appImManager.chat.bubbles.bubblesContainer.append(element);
void element.offsetLeft; // reflow
element.classList.add('active');

7
src/components/popupCreatePoll.ts

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import appPollsManager, { Poll } from "../lib/appManagers/appPollsManager";
import rootScope from "../lib/rootScope";
import { cancelEvent, findUpTag, getRichValue, isInputEmpty, whichChild } from "../helpers/dom";
import CheckboxField from "./checkbox";
import InputField from "./inputField";
@ -27,7 +26,7 @@ export default class PopupCreatePoll extends PopupElement { @@ -27,7 +26,7 @@ export default class PopupCreatePoll extends PopupElement {
private correctAnswers: Uint8Array[];
private quizSolutionInput: HTMLInputElement;
constructor() {
constructor(private peerID: number) {
super('popup-create-poll popup-new-media', null, {closable: true, withConfirm: 'CREATE', body: true});
this.title.innerText = 'New Poll';
@ -57,8 +56,6 @@ export default class PopupCreatePoll extends PopupElement { @@ -57,8 +56,6 @@ export default class PopupCreatePoll extends PopupElement {
settingsCaption.classList.add('caption');
settingsCaption.innerText = 'Settings';
const peerID = rootScope.selectedPeerID;
if(!appPeersManager.isBroadcast(peerID)) {
this.anonymousCheckboxField = CheckboxField('Anonymous Voting', 'anonymous');
this.anonymousCheckboxField.input.checked = true;
@ -213,7 +210,7 @@ export default class PopupCreatePoll extends PopupElement { @@ -213,7 +210,7 @@ export default class PopupCreatePoll extends PopupElement {
//console.log('Will try to create poll:', inputMediaPoll);
appMessagesManager.sendOther(rootScope.selectedPeerID, inputMediaPoll);
appMessagesManager.sendOther(this.peerID, inputMediaPoll);
};
onInput = (e: Event) => {

2
src/components/popupDeleteMessages.ts

@ -7,7 +7,7 @@ import PopupPeer from "./popupPeer"; @@ -7,7 +7,7 @@ import PopupPeer from "./popupPeer";
export default class PopupDeleteMessages {
constructor(mids: number[], onConfirm?: () => void) {
const peerID = rootScope.selectedPeerID;
const peerID = appMessagesManager.getMessage(mids[0]).peerID;
const firstName = appPeersManager.getPeerTitle(peerID, false, true);
mids = mids.slice();

4
src/components/popupForward.ts

@ -20,8 +20,8 @@ export default class PopupForward extends PopupElement { @@ -20,8 +20,8 @@ export default class PopupForward extends PopupElement {
await (onSelect ? onSelect() || Promise.resolve() : Promise.resolve());
appImManager.setPeer(peerID);
appImManager.chatInputC.initMessagesForward(mids.slice());
appImManager.setInnerPeer(peerID);
appImManager.chat.input.initMessagesForward(mids.slice());
}, ['dialogs', 'contacts'], () => {
this.show();
this.selector.checkForTriggers(); // ! due to zero height before mounting

5
src/components/popupNewMedia.ts

@ -2,7 +2,6 @@ import { isTouchSupported } from "../helpers/touchSupport"; @@ -2,7 +2,6 @@ import { isTouchSupported } from "../helpers/touchSupport";
import appImManager from "../lib/appManagers/appImManager";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import { calcImageInBox, getRichValue } from "../helpers/dom";
import { Layouter, RectPart } from "./groupedLayout";
import InputField from "./inputField";
import { PopupElement } from "./popup";
import { ripple } from "./ripple";
@ -90,8 +89,8 @@ export default class PopupNewMedia extends PopupElement { @@ -90,8 +89,8 @@ export default class PopupNewMedia extends PopupElement {
//console.log('will send files with options:', willAttach);
const peerID = appImManager.peerID;
const chatInputC = appImManager.chatInputC;
const peerID = appImManager.chat.peerID;
const chatInputC = appImManager.chat.input;
if(willAttach.sendFileDetails.length > 1 && willAttach.isMedia) {
appMessagesManager.sendAlbum(peerID, willAttach.sendFileDetails.map(d => d.file), Object.assign({

2
src/components/popupStickers.ts

@ -85,7 +85,7 @@ export default class PopupStickers extends PopupElement { @@ -85,7 +85,7 @@ export default class PopupStickers extends PopupElement {
if(!target) return;
const fileID = target.dataset.docID;
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
if(appImManager.chat.input.sendMessageWithDocument(fileID)) {
this.closeBtn.click();
} else {
console.warn('got no doc by id:', fileID);

2
src/components/sidebarLeft/tabs/includedChats.ts

@ -20,7 +20,7 @@ export default class AppIncludedChatsTab implements SliderTab { @@ -20,7 +20,7 @@ export default class AppIncludedChatsTab implements SliderTab {
private originalFilter: DialogFilter;
init() {
this.container = document.querySelector('.included-chats-container');
this.container = document.querySelector('.included-chatlist-container');
this.closeBtn = this.container.querySelector('.sidebar-close-button');
this.confirmBtn = this.container.querySelector('.btn-confirm');
this.title = this.container.querySelector('.sidebar-header__title');

2
src/components/sidebarLeft/tabs/newGroup.ts

@ -46,7 +46,7 @@ export default class AppNewGroupTab implements SliderTab { @@ -46,7 +46,7 @@ export default class AppNewGroupTab implements SliderTab {
});
const chatsContainer = document.createElement('div');
chatsContainer.classList.add('chats-container');
chatsContainer.classList.add('chatlist-container');
chatsContainer.append(this.searchGroup.container);
const scrollable = new Scrollable(chatsContainer);

2
src/components/sidebarRight/tabs/gifs.ts

@ -50,7 +50,7 @@ export default class AppGifsTab implements SliderTab { @@ -50,7 +50,7 @@ export default class AppGifsTab implements SliderTab {
if(!target) return;
const fileID = target.dataset.docID;
if(appImManager.chatInputC.sendMessageWithDocument(fileID)) {
if(appImManager.chat.input.sendMessageWithDocument(fileID)) {
if(mediaSizes.isMobile) {
this.backBtn.click();
}

2
src/components/sidebarRight/tabs/search.ts

@ -26,7 +26,7 @@ export default class AppPrivateSearchTab implements SliderTab { @@ -26,7 +26,7 @@ export default class AppPrivateSearchTab implements SliderTab {
this.closeBtn = this.container.querySelector('.sidebar-close-button');
this.searchInput = new SearchInput('Search');
this.closeBtn.parentElement.append(this.searchInput.container);
this.appSearch = new AppSearch(this.container.querySelector('.chats-container'), this.searchInput, {
this.appSearch = new AppSearch(this.container.querySelector('.chatlist-container'), this.searchInput, {
messages: new SearchGroup('Private Search', 'messages')
});
}

63
src/components/sidebarRight/tabs/sharedMedia.ts

@ -100,6 +100,7 @@ export default class AppSharedMediaTab implements SliderTab { @@ -100,6 +100,7 @@ export default class AppSharedMediaTab implements SliderTab {
private loadMutex: Promise<any> = Promise.resolve();
private log = logger('SM'/* , LogLevels.error */);
setPeerStatusInterval: number;
public init() {
this.container = document.getElementById('shared-media-container');
@ -193,14 +194,57 @@ export default class AppSharedMediaTab implements SliderTab { @@ -193,14 +194,57 @@ export default class AppSharedMediaTab implements SliderTab {
this.profileElements.notificationsCheckbox.addEventListener('change', () => {
//let checked = this.profileElements.notificationsCheckbox.checked;
appImManager.mutePeer(this.peerID);
appMessagesManager.mutePeer(this.peerID);
});
rootScope.on('dialog_notify_settings', (e) => {
if(this.peerID == e.detail) {
const muted = appMessagesManager.isPeerMuted(this.peerID);
this.profileElements.notificationsCheckbox.checked = !muted;
this.profileElements.notificationsStatus.innerText = muted ? 'Disabled' : 'Enabled';
}
});
rootScope.on('peer_typings', (e) => {
const {peerID} = e.detail;
if(this.peerID == peerID) {
this.setPeerStatus();
}
});
rootScope.on('user_update', (e) => {
const userID = e.detail;
if(this.peerID == userID) {
this.setPeerStatus();
}
});
this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3);
/* this.closeBtn.addEventListener('click', () => {
this.toggleSidebar(false);
}); */
}
public setPeerStatus = (needClear = false) => {
if(!this.peerID) return;
const peerID = this.peerID;
if(needClear) {
this.profileElements.subtitle.innerHTML = '';
}
appImManager.getPeerStatus(this.peerID).then((subtitle) => {
if(peerID != this.peerID) {
return;
}
this.profileElements.subtitle.innerHTML = subtitle;
});
};
public renderNewMessages(peerID: number, mids: number[]) {
if(this.init) return; // * not inited yet
@ -408,7 +452,7 @@ export default class AppSharedMediaTab implements SliderTab { @@ -408,7 +452,7 @@ export default class AppSharedMediaTab implements SliderTab {
const load = () => appPhotosManager.preloadPhoto(isPhoto ? media.id : media, appPhotosManager.choosePhotoSize(media, 200, 200))
.then(() => {
if(rootScope.selectedPeerID != peerID) {
if(appImManager.chat.peerID != peerID) {
this.log.warn('peer changed');
return;
}
@ -552,7 +596,7 @@ export default class AppSharedMediaTab implements SliderTab { @@ -552,7 +596,7 @@ export default class AppSharedMediaTab implements SliderTab {
if(webpage.photo) {
let load = () => appPhotosManager.preloadPhoto(webpage.photo.id, appPhotosManager.choosePhotoSize(webpage.photo, 60, 60))
.then(() => {
if(rootScope.selectedPeerID != peerID) {
if(appImManager.chat.peerID != peerID) {
this.log.warn('peer changed');
return;
}
@ -702,7 +746,7 @@ export default class AppSharedMediaTab implements SliderTab { @@ -702,7 +746,7 @@ export default class AppSharedMediaTab implements SliderTab {
this.log(logStr + 'search house of glass', type, value);
if(rootScope.selectedPeerID != peerID) {
if(appImManager.chat.peerID != peerID) {
this.log.warn('peer changed');
return;
}
@ -827,7 +871,7 @@ export default class AppSharedMediaTab implements SliderTab { @@ -827,7 +871,7 @@ export default class AppSharedMediaTab implements SliderTab {
}
public fillProfileElements() {
let peerID = this.peerID = rootScope.selectedPeerID;
let peerID = this.peerID = appImManager.chat.peerID;
this.cleanupHTML();
@ -846,8 +890,6 @@ export default class AppSharedMediaTab implements SliderTab { @@ -846,8 +890,6 @@ export default class AppSharedMediaTab implements SliderTab {
if(dialog.notify_settings && dialog.notify_settings.mute_until) {
muted = new Date(dialog.notify_settings.mute_until * 1000) > new Date();
}
appImManager.setMutedState(muted);
}
} else {
window.requestAnimationFrame(() => {
@ -893,6 +935,13 @@ export default class AppSharedMediaTab implements SliderTab { @@ -893,6 +935,13 @@ export default class AppSharedMediaTab implements SliderTab {
}
});
}
let title: string;
if(peerID == rootScope.myID) title = 'Saved Messages';
else title = appPeersManager.getPeerTitle(peerID);
this.profileElements.name.innerHTML = title;
this.setPeerStatus(true);
}
/* onOpen() {

2
src/components/sidebarRight/tabs/stickers.ts

@ -37,7 +37,7 @@ export default class AppStickersTab implements SliderTab { @@ -37,7 +37,7 @@ export default class AppStickersTab implements SliderTab {
const sticker = findUpClassName(e.target, 'sticker-set-sticker');
if(sticker) {
const docID = sticker.dataset.docID;
appImManager.chatInputC.sendMessageWithDocument(docID);
appImManager.chat.input.sendMessageWithDocument(docID);
return;
}

9
src/components/transition.ts

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
import { whichChild } from "../helpers/dom";
function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
const width = prevTabContent.getBoundingClientRect().width;
const elements = [tabContent, prevTabContent];
@ -79,8 +81,13 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction, @@ -79,8 +81,13 @@ const Transition = (content: HTMLElement, animationFunction: TransitionFunction,
let transitionEndTimeout: number;
let prevTabContent: HTMLElement = null;
function selectTab(id: number, animate = true) {
function selectTab(id: number | HTMLElement, animate = true) {
const self = selectTab;
if(id instanceof HTMLElement) {
id = whichChild(id);
}
if(id == self.prevId) return false;
//console.log('selectTab id:', id);

21
src/helpers/dom.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { MOUNT_CLASS_TO } from "../lib/mtproto/mtproto_config";
import ListenerSetter from "./listenerSetter";
import { isTouchSupported } from "./touchSupport";
import { isSafari } from "./userAgent";
@ -464,30 +465,34 @@ export function blurActiveElement() { @@ -464,30 +465,34 @@ export function blurActiveElement() {
}
export const CLICK_EVENT_NAME = isTouchSupported ? 'touchend' : 'click';
export const attachClickEvent = (elem: HTMLElement, callback: (e: TouchEvent | MouseEvent) => void, options: AddEventListenerOptions = {}) => {
export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter}>;
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') {
const o = {...options, once: true};
const onTouchStart = (e: TouchEvent) => {
const onTouchMove = (e: TouchEvent) => {
elem.removeEventListener('touchend', onTouchEnd, o);
remove('touchend', onTouchEnd, o);
};
const onTouchEnd = (e: TouchEvent) => {
elem.removeEventListener('touchmove', onTouchMove, o);
remove('touchmove', onTouchMove, o);
callback(e);
if(options.once) {
elem.removeEventListener('touchstart', onTouchStart);
remove('touchstart', onTouchStart);
}
};
elem.addEventListener('touchend', onTouchEnd, o);
elem.addEventListener('touchmove', onTouchMove, o);
add('touchend', onTouchEnd, o);
add('touchmove', onTouchMove, o);
};
elem.addEventListener('touchstart', onTouchStart);
add('touchstart', onTouchStart);
} else {
elem.addEventListener(CLICK_EVENT_NAME, callback, options);
add(CLICK_EVENT_NAME, callback, options);
}
};

44
src/helpers/listenerSetter.ts

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
export type Listener = {element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions};
export type ListenerElement = any;
export type ListenerEvent = string;
export type ListenerOptions = any;
export type ListenerCallback = (...args: any[]) => any;
export default class ListenerSetter {
private listeners: Set<Listener> = new Set();
public add = (element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions) => {
const listener = {element, event, callback, options};
this.addManual(listener);
return listener;
};
public addManual = (listener: Listener) => {
listener.element.addEventListener(listener.event, listener.callback, listener.options);
this.listeners.add(listener);
};
public remove = (listener: Listener) => {
listener.element.removeEventListener(listener.event, listener.callback, listener.options);
this.listeners.delete(listener);
};
public removeManual = (element: ListenerElement, event: ListenerEvent, callback: ListenerCallback, options?: ListenerOptions) => {
let listener: Listener;
for(const _listener of this.listeners) {
if(_listener.element === element && _listener.event === event && _listener.callback === callback && _listener.options === options) {
listener = _listener;
break;
}
}
if(listener) {
this.remove(listener);
}
};
public removeAll = () => {
this.listeners.forEach(listener => {
this.remove(listener);
});
};
}

176
src/index.hbs

@ -134,7 +134,7 @@ @@ -134,7 +134,7 @@
</defs>
</svg>
<div id="main-columns" class="tabs-container">
<div class="chats-container sidebar sidebar-left main-column" id="column-left">
<div class="chatlist-container sidebar sidebar-left main-column" id="column-left">
<div class="sidebar-slider tabs-container">
<div class="sidebar-slider-item item-main">
<div class="sidebar-header">
@ -153,7 +153,7 @@ @@ -153,7 +153,7 @@
</div>
</div>
<div class="sidebar-content transition zoom-fade">
<div class="transition-item active" id="chats-container">
<div class="transition-item active" id="chatlist-container">
<div class="folders-tabs-scrollable hide">
<nav class="menu-horizontal" id="folders-tabs">
<ul>
@ -365,7 +365,7 @@ @@ -365,7 +365,7 @@
</div>
</div>
</div>
<div class="sidebar-slider-item included-chats-container">
<div class="sidebar-slider-item included-chatlist-container">
<div class="sidebar-header">
<button class="btn-icon tgico-back sidebar-close-button"></button>
<div class="sidebar-header__title"></div>
@ -374,132 +374,14 @@ @@ -374,132 +374,14 @@
</div>
</div>
</div>
<div class="chat-container main-column" id="column-center">
{{!-- <canvas id="chat-background-canvas"></canvas> --}}
<div class="chat-background"></div>
<div id="topbar" style="display: none;" class="sidebar-header">
<button class="btn-icon tgico-back sidebar-close-button"></button>
<div class="chat-info">
<div class="person">
<avatar-element id="im-avatar" dialog="1" clickable></avatar-element>
<div class="content">
<div class="top">
<div class="user-title" id="im-title"></div>
</div>
<div class="bottom">
<div class="info" id="im-subtitle"></div>
</div>
</div>
</div>
</div>
<div class="chat-utils">
<button class="btn-primary rp chat-join hide">SUBSCRIBE</button>
<div class="btn-icon rp chat-mute-button hide"></div>
<div class="btn-icon rp tgico-search chat-search-button"></div>
<div class="btn-icon btn-menu-toggle rp tgico-more chat-more-button">
<div class="btn-menu bottom-left">
<div class="btn-menu-item menu-search tgico-search rp">Search</div>
<div class="btn-menu-item menu-mute rp">Mute</div>
<div class="btn-menu-item menu-delete tgico-delete danger rp btn-disabled">Delete and Leave</div>
</div>
</div>
</div>
</div>
<div id="bubbles" class="scrolled-down">
{{!-- <div id="bubbles-transform-helper"> --}}
<div id="bubbles-inner"></div>
<div id="bubbles-go-down" class="tgico-down btn-corner z-depth-1 rp hide"></div>
{{!-- </div> --}}
</div>
<div id="chat-input" style="display: none;">
<div class="chat-input-container">
<div class="input-message">
<div class="reply-wrapper">
<button class="btn-icon rp tgico-close reply-cancel"></button>
<div class="reply">
<div class="reply-border"></div>
<div class="reply-content">
<div class="reply-title"></div>
<div class="reply-subtitle"></div>
</div>
</div>
</div>
<div class="new-message-wrapper">
<button class="btn-icon rp tgico toggle-emoticons" id="toggle-emoticons"></button>
<!-- <textarea type="text" id="input-message" placeholder="Message" contenteditable="true"></textarea> -->
<div class="input-message-container"></div>
<button class="btn-icon tgico-attach btn-menu-toggle" id="attach-file"></button>
<div class="record-time"></div>
<input type="file" id="input-file" style="display: none;" multiple />
</div>
</div>
<button class="btn-circle z-depth-1 btn-icon tgico-delete danger" id="btn-record-cancel"></button>
<div class="btn-send-container">
<div class="record-ripple"></div>
<button class="btn-circle rp z-depth-1 btn-icon" id="btn-send">
<span class="tgico tgico-send"></span>
<span class="tgico tgico-microphone2"></span>
</button>
</div>
</div>
<div class="emoji-dropdown" id="emoji-dropdown" style="display: none;">
<div class="emoji-container">
<div class="tabs-container">
<div class="emoji-padding">
<nav class="menu-horizontal-div no-stripe">
<button class="menu-horizontal-div-item active btn-icon tgico-recent rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-smile rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-animals rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-eats rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-car rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-sport rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-lamp rp"></button>
<!-- <button class="menu-horizontal-div-item btn-icon tgico-info rp"></button> -->
<button class="menu-horizontal-div-item btn-icon tgico-flag rp"></button>
</nav>
<div class="emoticons-content" id="content-emoji"></div>
</div>
<div class="stickers-padding">
<div class="menu-wrapper">
<nav class="menu-horizontal-div no-stripe justify-start">
<button class="menu-horizontal-div-item btn-icon tgico-recent active"></button>
</nav>
</div>
<div class="emoticons-content" id="content-stickers"></div>
</div>
<div class="gifs-padding">
<div class="emoticons-content" id="content-gifs">
<div class="gifs-masonry"></div>
</div>
</div>
</div>
</div>
<div class="emoji-tabs menu-horizontal-div no-stripe">
<button class="menu-horizontal-div-item emoji-tabs-search justify-self-start btn-icon tgico-search rp" data-tab="-1"></button>
<button class="menu-horizontal-div-item emoji-tabs-emoji btn-icon tgico-smile rp" data-tab="0"></button>
<button class="menu-horizontal-div-item emoji-tabs-stickers btn-icon tgico-stickers rp" data-tab="1"></button>
<button class="menu-horizontal-div-item emoji-tabs-gifs btn-icon tgico-gifs rp" data-tab="2"></button>
<button class="menu-horizontal-div-item emoji-tabs-delete justify-self-end btn-icon tgico-deleteleft rp" data-tab="-1"></button>
</div>
</div>
</div>
</div>
<div class="main-column" id="column-center"></div>
<div class="sidebar sidebar-right main-column" id="column-right">
<div class="sidebar-content sidebar-slider tabs-container">
<div class="sidebar-slider-item profile-container" id="shared-media-container">
<div class="sidebar-header">
<button class="btn-icon tgico sidebar-close-button"></button>
<div class="sidebar-header__title">Info</div>
<!-- <button class="btn-icon rp tgico-edit sidebar-edit-button"></button> -->
<div class="btn-icon tgico-more"></div>
<!-- <div class="btn-icon tgico-more rp btn-menu-toggle">
<div class="btn-menu bottom-left"> -->
<!-- <div class="btn-menu-item menu-mute rp">Mute</div>
<div class="btn-menu-item menu-delete tgico-delete danger rp">Delete and Leave</div> -->
<!-- </div>
</div> -->
</div>
<div class="profile-content">
<div class="profile-content-wrapper">
@ -547,26 +429,26 @@ @@ -547,26 +429,26 @@
</div>
</div>
</div>
<div class="sidebar-slider-item chats-container" id="search-private-container">
<div class="sidebar-slider-item chatlist-container" id="search-private-container">
<div class="sidebar-header">
<button class="btn-icon tgico-close sidebar-close-button"></button>
</div>
<div class="chats-container"></div>
<div class="chatlist-container"></div>
</div>
<div class="sidebar-slider-item chats-container" id="stickers-container">
<div class="sidebar-slider-item chatlist-container" id="stickers-container">
<div class="sidebar-header">
<button class="btn-icon tgico-close sidebar-close-button"></button>
</div>
<div class="sidebar-content"><div class="sticker-sets"></div></div>
</div>
<div class="sidebar-slider-item chats-container" id="poll-results-container">
<div class="sidebar-slider-item chatlist-container" id="poll-results-container">
<div class="sidebar-header">
<button class="btn-icon tgico-close sidebar-close-button"></button>
<div class="sidebar-header__title">Results</div>
</div>
<div class="sidebar-content"><div class="poll-results"></div></div>
</div>
<div class="sidebar-slider-item chats-container" id="search-gifs-container">
<div class="sidebar-slider-item chatlist-container" id="search-gifs-container">
<div class="sidebar-header">
<button class="btn-icon tgico-close sidebar-close-button"></button>
</div>
@ -582,6 +464,46 @@ @@ -582,6 +464,46 @@
<div class="btn-menu-item menu-archive tgico rp"><div></div></div>
<div class="btn-menu-item menu-delete tgico-delete danger rp"><div></div></div>
</div>
<div class="emoji-dropdown" id="emoji-dropdown" style="display: none;">
<div class="emoji-container">
<div class="tabs-container">
<div class="emoji-padding">
<nav class="menu-horizontal-div no-stripe">
<button class="menu-horizontal-div-item active btn-icon tgico-recent rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-smile rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-animals rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-eats rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-car rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-sport rp"></button>
<button class="menu-horizontal-div-item btn-icon tgico-lamp rp"></button>
<!-- <button class="menu-horizontal-div-item btn-icon tgico-info rp"></button> -->
<button class="menu-horizontal-div-item btn-icon tgico-flag rp"></button>
</nav>
<div class="emoticons-content" id="content-emoji"></div>
</div>
<div class="stickers-padding">
<div class="menu-wrapper">
<nav class="menu-horizontal-div no-stripe justify-start">
<button class="menu-horizontal-div-item btn-icon tgico-recent active"></button>
</nav>
</div>
<div class="emoticons-content" id="content-stickers"></div>
</div>
<div class="gifs-padding">
<div class="emoticons-content" id="content-gifs">
<div class="gifs-masonry"></div>
</div>
</div>
</div>
</div>
<div class="emoji-tabs menu-horizontal-div no-stripe">
<button class="menu-horizontal-div-item emoji-tabs-search justify-self-start btn-icon tgico-search rp" data-tab="-1"></button>
<button class="menu-horizontal-div-item emoji-tabs-emoji btn-icon tgico-smile rp" data-tab="0"></button>
<button class="menu-horizontal-div-item emoji-tabs-stickers btn-icon tgico-stickers rp" data-tab="1"></button>
<button class="menu-horizontal-div-item emoji-tabs-gifs btn-icon tgico-gifs rp" data-tab="2"></button>
<button class="menu-horizontal-div-item emoji-tabs-delete justify-self-end btn-icon tgico-deleteleft rp" data-tab="-1"></button>
</div>
</div>
</div>
{{# each htmlWebpackPlugin.files.js }}
<script src="{{ this }}"></script>

57
src/lib/appManagers/appChatsManager.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { numberWithCommas } from "../../helpers/number";
import { isObject, safeReplaceObject, copy } from "../../helpers/object";
import { ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, Updates } from "../../layer";
import { ChatAdminRights, ChatBannedRights, ChatFull, ChatParticipants, InputChannel, InputChatPhoto, InputFile, InputPeer, SendMessageAction, Updates } from "../../layer";
import { addFunction } from "../../rlottie.github.io/a.out";
import apiManager from '../mtproto/mtprotoworker';
import { MOUNT_CLASS_TO } from "../mtproto/mtproto_config";
import { RichTextProcessor } from "../richtextprocessor";
@ -64,6 +65,8 @@ export type Chat = { @@ -64,6 +65,8 @@ export type Chat = {
export type ChatRights = 'send' | 'edit_title' | 'edit_photo' | 'invite' | 'pin' | 'deleteRevoke' | 'delete';
export type UserTyping = Partial<{userID: number, action: SendMessageAction, timeout: number}>;
export class AppChatsManager {
public chats: {[id: number]: Channel | Chat | any} = {};
//public usernames: any = {};
@ -73,6 +76,8 @@ export class AppChatsManager { @@ -73,6 +76,8 @@ export class AppChatsManager {
public megagroupOnlines: {[id: number]: {timestamp: number, onlines: number}} = {};
public typingsInPeer: {[peerID: number]: UserTyping[]} = {};
constructor() {
rootScope.on('apiUpdate', (e) => {
// console.log('on apiUpdate', update)
@ -83,6 +88,56 @@ export class AppChatsManager { @@ -83,6 +88,56 @@ export class AppChatsManager {
//console.log('updateChannel:', update);
rootScope.broadcast('channel_settings', {channelID: channelID});
break;
case 'updateUserTyping':
case 'updateChatUserTyping': {
if(rootScope.myID == update.user_id) {
return;
}
const peerID = update._ == 'updateUserTyping' ? update.user_id : -update.chat_id;
const typings = this.typingsInPeer[peerID] ?? (this.typingsInPeer[peerID] = []);
let typing = typings.find(t => t.userID == update.user_id);
if(!typing) {
typing = {
userID: update.user_id
};
typings.push(typing);
}
//console.log('updateChatUserTyping', update, typings);
typing.action = update.action;
if(!appUsersManager.hasUser(update.user_id)) {
if(update._ == 'updateChatUserTyping') {
if(update.chat_id && appChatsManager.hasChat(update.chat_id) && !appChatsManager.isChannel(update.chat_id)) {
appProfileManager.getChatFull(update.chat_id);
}
}
//return;
}
appUsersManager.forceUserOnline(update.user_id);
if(typing.timeout !== undefined) clearTimeout(typing.timeout);
typing.timeout = window.setTimeout(() => {
delete typing.timeout;
typings.findAndSplice(t => t.userID == update.user_id);
rootScope.broadcast('peer_typings', {peerID, typings});
if(!typings.length) {
delete this.typingsInPeer[peerID];
}
}, 6000);
rootScope.broadcast('peer_typings', {peerID, typings});
break;
}
}
});

57
src/lib/appManagers/appDialogsManager.ts

@ -160,11 +160,7 @@ export class AppDialogsManager { @@ -160,11 +160,7 @@ export class AppDialogsManager {
public doms: {[peerID: number]: DialogDom} = {};
public lastActiveListElement: HTMLElement = null;
/* private rippleCallback: (value?: boolean | PromiseLike<boolean>) => void = null;
private lastClickID = 0;
private lastGoodClickID = 0; */
public chatsContainer = document.getElementById('chats-container') as HTMLDivElement;
public chatsContainer = document.getElementById('chatlist-container') as HTMLDivElement;
private chatsPreloader: HTMLDivElement;
public loadDialogsPromise: Promise<any>;
@ -276,10 +272,6 @@ export class AppDialogsManager { @@ -276,10 +272,6 @@ export class AppDialogsManager {
dom.avatarEl.classList.toggle('is-online', online);
}
}
if(rootScope.selectedPeerID == user.id) {
appImManager.setPeerStatus();
}
});
/* rootScope.$on('dialog_top', (e) => {
@ -331,11 +323,6 @@ export class AppDialogsManager { @@ -331,11 +323,6 @@ export class AppDialogsManager {
const dialog = appMessagesManager.getDialogByPeerID(info.peerID)[0];
if(dialog) {
this.setUnreadMessages(dialog);
if(dialog.peerID == rootScope.selectedPeerID) {
appImManager.updateUnreadByDialog(dialog);
}
this.validateForFilter();
this.setFiltersUnreadCount();
}
@ -424,6 +411,19 @@ export class AppDialogsManager { @@ -424,6 +411,19 @@ export class AppDialogsManager {
}
});
rootScope.on('peer_typings', (e) => {
const {peerID, typings} = e.detail;
const dialog = appMessagesManager.getDialogByPeerID(peerID)[0];
if(!dialog) return;
if(typings.length) {
this.setTyping(dialog, appUsersManager.getUser(typings[0]));
} else {
this.unsetTyping(dialog);
}
});
const foldersScrollable = new ScrollableX(this.folders.menuScrollContainer);
bottomPart.prepend(this.folders.menuScrollContainer);
const selectTab = horizontalMenu(this.folders.menu, this.folders.container, (id, tabContent) => {
@ -861,8 +861,6 @@ export class AppDialogsManager { @@ -861,8 +861,6 @@ export class AppDialogsManager {
this.lastActiveListElement.classList.remove('active');
}
let result: ReturnType<AppImManager['setPeer']>;
//console.log('appDialogsManager: lock lazyLoadQueue');
if(elem) {
if(onFound) onFound();
@ -874,14 +872,9 @@ export class AppDialogsManager { @@ -874,14 +872,9 @@ export class AppDialogsManager {
this.lastActiveListElement = elem;
}
result = appImManager.setPeer(peerID, lastMsgID);
/* if(result instanceof Promise) {
this.lastGoodClickID = this.lastClickID;
appImManager.lazyLoadQueue.lock();
} */
appImManager.setPeer(peerID, lastMsgID);
} else {
result = appImManager.setPeer(0);
appImManager.setPeer(0);
}
}, {capture: true});
@ -1217,23 +1210,7 @@ export class AppDialogsManager { @@ -1217,23 +1210,7 @@ export class AppDialogsManager {
if(rippleEnabled) {
ripple(paddingDiv);
/* ripple(paddingDiv, (id) => {
this.log('dialogs click element');
this.lastClickID = id;
return new Promise((resolve, reject) => {
this.rippleCallback = resolve;
//setTimeout(() => resolve(), 100);
//window.requestAnimationFrame(() => window.requestAnimationFrame(() => resolve()));
});
}, (id) => {
//console.log('appDialogsManager: ripple onEnd called!');
if(id == this.lastGoodClickID) {
appImManager.lazyLoadQueue.unlock();
}
}); */
}
const li = document.createElement('li');
li.append(paddingDiv);
@ -1287,7 +1264,7 @@ export class AppDialogsManager { @@ -1287,7 +1264,7 @@ export class AppDialogsManager {
this.doms[dialog.peerID] = dom;
if(rootScope.selectedPeerID == peerID) {
if(appImManager.chat?.peerID == peerID) {
li.classList.add('active');
this.lastActiveListElement = li;
}

2
src/lib/appManagers/appDocsManager.ts

@ -13,7 +13,7 @@ export type MyDocument = Document.document; @@ -13,7 +13,7 @@ export type MyDocument = Document.document;
// TODO: если залить картинку файлом, а потом перезайти в диалог - превьюшка заново скачается
class AppDocsManager {
export class AppDocsManager {
private docs: {[docID: string]: MyDocument} = {};
private savingLottiePreview: {[docID: string]: true} = {};

2891
src/lib/appManagers/appImManager.ts

File diff suppressed because it is too large Load Diff

120
src/lib/appManagers/appMessagesManager.ts

@ -5,7 +5,7 @@ import { tsNow } from "../../helpers/date"; @@ -5,7 +5,7 @@ import { tsNow } from "../../helpers/date";
import { copy, defineNotNumerableProperties, deepEqual, safeReplaceObject, getObjectKeysAndSort } from "../../helpers/object";
import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols } from "../../helpers/string";
import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMessage, Message, MessageAction, MessageEntity, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, PhotoSize, SendMessageAction, Update } from "../../layer";
import { Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMessage, InputNotifyPeer, InputPeerNotifySettings, Message, MessageAction, MessageEntity, MessagesDialogs, MessagesFilter, MessagesMessages, MessagesPeerDialogs, MethodDeclMap, NotifyPeer, PhotoSize, SendMessageAction, Update } from "../../layer";
import { InvokeApiOptions } from "../../types";
import { langPack } from "../langPack";
import { logger, LogLevels } from "../logger";
@ -3857,7 +3857,98 @@ export class AppMessagesManager { @@ -3857,7 +3857,98 @@ export class AppMessagesManager {
break;
}
case 'updateNotifySettings': {
const {peer, notify_settings} = update;
const peerID = appPeersManager.getPeerID((peer as NotifyPeer.notifyPeer).peer);
const dialog = this.getDialogByPeerID(peerID)[0];
if(dialog) {
dialog.notify_settings = notify_settings;
rootScope.broadcast('dialog_notify_settings', peerID);
}
/////this.log('updateNotifySettings', peerID, notify_settings);
break;
}
}
}
public isPeerMuted(peerID: number) {
if(peerID == rootScope.myID) return false;
const dialog = this.getDialogByPeerID(peerID)[0];
let muted = false;
if(dialog && dialog.notify_settings && dialog.notify_settings.mute_until) {
muted = new Date(dialog.notify_settings.mute_until * 1000) > new Date();
}
return muted;
}
public mutePeer(peerID: number) {
let inputPeer = appPeersManager.getInputPeerByID(peerID);
let inputNotifyPeer: InputNotifyPeer.inputNotifyPeer = {
_: 'inputNotifyPeer',
peer: inputPeer
};
let settings: InputPeerNotifySettings = {
_: 'inputPeerNotifySettings'
};
let dialog = appMessagesManager.getDialogByPeerID(peerID)[0];
let muted = true;
if(dialog && dialog.notify_settings) {
muted = dialog.notify_settings.mute_until > (Date.now() / 1000 | 0);
}
if(!muted) {
settings.mute_until = 2147483647;
}
apiManager.invokeApi('account.updateNotifySettings', {
peer: inputNotifyPeer,
settings: settings
}).then(bool => {
if(bool) {
this.handleUpdate({
_: 'updateNotifySettings',
peer: {
_: 'notifyPeer',
peer: appPeersManager.getOutputPeer(peerID)
},
notify_settings: { // ! WOW, IT WORKS !
...settings,
_: 'peerNotifySettings',
}
});
}
});
/* return apiManager.invokeApi('account.getNotifySettings', {
peer: inputNotifyPeer
}).then((settings: any) => {
settings.mute_until = 2000000000; // 2147483646
return apiManager.invokeApi('account.updateNotifySettings', {
peer: inputNotifyPeer,
settings: Object.assign(settings, {
_: 'inputPeerNotifySettings'
})
}).then(res => {
this.log('mute result:', res);
});
}); */
}
public canWriteToPeer(peerID: number) {
const isChannel = appPeersManager.isChannel(peerID);
const hasRights = isChannel && appChatsManager.hasRights(-peerID, 'send');
return (!isChannel || hasRights) && (peerID < 0 || appUsersManager.canSendToUser(peerID));
}
public finalizePendingMessage(randomID: number, finalMessage: any) {
@ -3908,6 +3999,31 @@ export class AppMessagesManager { @@ -3908,6 +3999,31 @@ export class AppMessagesManager {
delete this.tempFinalizeCallbacks[tempID];
}
// set cached url to media
const message = appMessagesManager.getMessage(mid);
if(message.media) {
if(message.media.photo) {
const photo = appPhotosManager.getPhoto('' + tempID);
if(/* photo._ != 'photoEmpty' */photo) {
const newPhoto = message.media.photo;
// костыль
defineNotNumerableProperties(newPhoto, ['downloaded', 'url']);
newPhoto.downloaded = photo.downloaded;
newPhoto.url = photo.url;
}
} else if(message.media.document) {
const doc = appDocsManager.getDoc('' + tempID);
if(/* doc._ != 'documentEmpty' && */doc?.type && doc.type != 'sticker') {
const newDoc = message.media.document;
newDoc.downloaded = doc.downloaded;
newDoc.url = doc.url;
}
} else if(message.media.poll) {
delete appPollsManager.polls[tempID];
delete appPollsManager.results[tempID];
}
}
rootScope.broadcast('message_sent', {tempID, mid});
}
@ -4286,7 +4402,7 @@ export class AppMessagesManager { @@ -4286,7 +4402,7 @@ export class AppMessagesManager {
}
public setTyping(peerID: number, _action: any): Promise<boolean> {
if(!rootScope.myID) return Promise.resolve(false);
if(!rootScope.myID || !peerID || !this.canWriteToPeer(peerID)) return Promise.resolve(false);
const action: SendMessageAction = typeof(_action) == 'string' ? {_: _action} : _action;
return apiManager.invokeApi('messages.setTyping', {

2
src/lib/appManagers/appPollsManager.ts

@ -70,7 +70,7 @@ export type Poll = { @@ -70,7 +70,7 @@ export type Poll = {
chosenIndexes?: number[]
};
class AppPollsManager {
export class AppPollsManager {
public polls: {[id: string]: Poll} = {};
public results: {[id: string]: PollResults} = {};

2
src/lib/appManagers/appWebPagesManager.ts

@ -6,7 +6,7 @@ import rootScope from "../rootScope"; @@ -6,7 +6,7 @@ import rootScope from "../rootScope";
import { safeReplaceObject } from "../../helpers/object";
import { limitSymbols } from "../../helpers/string";
class AppWebPagesManager {
export class AppWebPagesManager {
webpages: any = {};
pendingWebPages: any = {};

2
src/lib/logger.ts

@ -13,7 +13,7 @@ function dT() { @@ -13,7 +13,7 @@ function dT() {
}
export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | LogLevels.error) {
if(process.env.NODE_ENV != 'development'/* || true */) {
if(process.env.NODE_ENV != 'development' || true) {
level = LogLevels.error;
}

12
src/lib/lottieLoader.ts

@ -2,6 +2,7 @@ import RLottieWorker from 'worker-loader!./rlottie/rlottie.worker'; @@ -2,6 +2,7 @@ import RLottieWorker from 'worker-loader!./rlottie/rlottie.worker';
import animationIntersector from "../components/animationIntersector";
import EventListenerBase from "../helpers/eventListenerBase";
import mediaSizes from "../helpers/mediaSizes";
import { clamp } from '../helpers/number';
import { isAndroid, isApple, isAppleMobile, isSafari } from "../helpers/userAgent";
import { logger, LogLevels } from "./logger";
import apiManager from "./mtproto/mtprotoworker";
@ -91,7 +92,7 @@ export class RLottiePlayer extends EventListenerBase<{ @@ -91,7 +92,7 @@ export class RLottiePlayer extends EventListenerBase<{
}
}
// Skip ratio
// * Skip ratio (30fps)
let skipRatio: number;
if(options.skipRatio !== undefined) skipRatio = options.skipRatio;
else if((isAndroid || isAppleMobile || (isApple && !isSafari)) && this.width < 100 && this.height < 100) {
@ -100,8 +101,11 @@ export class RLottiePlayer extends EventListenerBase<{ @@ -100,8 +101,11 @@ export class RLottiePlayer extends EventListenerBase<{
this.skipDelta = skipRatio !== undefined ? 1 / skipRatio | 0 : 1;
// Pixel ratio
const pixelRatio = window.devicePixelRatio;
//options.needUpscale = true;
// * Pixel ratio
//const pixelRatio = window.devicePixelRatio;
const pixelRatio = clamp(window.devicePixelRatio, 1, 2);
if(pixelRatio > 1) {
//this.cachingEnabled = true;//this.width < 100 && this.height < 100;
if(options.needUpscale) {
@ -125,7 +129,7 @@ export class RLottiePlayer extends EventListenerBase<{ @@ -125,7 +129,7 @@ export class RLottiePlayer extends EventListenerBase<{
}
}
// Cache frames params
// * Cache frames params
if(!options.noCache) {
// проверка на размер уже после скейлинга, сделано для попапа и сайдбара, где стикеры 80х80 и 68х68, туда нужно 75%
if(isApple && this.width > 100 && this.height > 100) {

7
src/lib/mtproto/networker.ts

@ -1160,9 +1160,14 @@ export default class MTPNetworker { @@ -1160,9 +1160,14 @@ export default class MTPNetworker {
// * https://core.telegram.org/mtproto/service_messages_about_messages#notice-of-ignored-error-message
public processMessage(message: any, messageID: string, sessionID: Uint8Array | number[]) {
if(message._ == 'messageEmpty') {
this.log.warn('processMessage: messageEmpty', message, messageID);
return;
}
const msgidInt = parseInt(messageID.substr(0, -10), 10);
if(msgidInt % 2) {
this.log.warn('[MT] Server even message id: ', messageID, message);
this.log.warn('Server even message id: ', messageID, message);
return;
}

7
src/lib/rootScope.ts

@ -4,6 +4,7 @@ import type { AppMessagesManager, Dialog } from "./appManagers/appMessagesManage @@ -4,6 +4,7 @@ import type { AppMessagesManager, Dialog } from "./appManagers/appMessagesManage
import type { Poll, PollResults } from "./appManagers/appPollsManager";
import type { MyDialogFilter } from "./storages/filters";
import type { ConnectionStatusChange } from "../types";
import type { UserTyping } from "./appManagers/appChatsManager";
import { MOUNT_CLASS_TO, UserAuth } from "./mtproto/mtproto_config";
type BroadcastEvents = {
@ -11,6 +12,7 @@ type BroadcastEvents = { @@ -11,6 +12,7 @@ type BroadcastEvents = {
'user_auth': UserAuth,
'peer_changed': number,
'peer_pinned_messages': number,
'peer_typings': {peerID: number, typings: UserTyping[]},
'filter_delete': MyDialogFilter,
'filter_update': MyDialogFilter,
@ -70,7 +72,6 @@ type BroadcastEvents = { @@ -70,7 +72,6 @@ type BroadcastEvents = {
class RootScope {
public overlayIsActive: boolean = false;
public selectedPeerID = 0;
public myID = 0;
public idle = {
isIDLE: false
@ -102,10 +103,14 @@ class RootScope { @@ -102,10 +103,14 @@ class RootScope {
document.addEventListener(name, callback);
};
public addEventListener = this.on;
public off = <T extends keyof BroadcastEvents>(name: T, callback: (e: Omit<CustomEvent, 'detail'> & {detail: BroadcastEvents[T]}) => any) => {
// @ts-ignore
document.removeEventListener(name, callback);
};
public removeEventListener = this.off;
}
const rootScope = new RootScope();

450
src/scss/partials/_chat.scss

@ -13,194 +13,7 @@ $chat-helper-size: 39px; @@ -13,194 +13,7 @@ $chat-helper-size: 39px;
}
} */
#topbar {
width: 100%;
background-color: #fff;
user-select: none;
box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .21);
z-index: 1;
min-height: 3.5rem;
max-height: 3.5rem;
// border-bottom: 1px solid #DADCE0;
@include respond-to(handhelds) {
&.is-pinned-audio-shown, &.is-pinned-message-shown:not(.hide-pinned) {
& + #bubbles {
margin-top: 52px;
}
}
&.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) {
.pinned-message {
display: flex;
}
}
}
@include respond-to(not-handhelds) {
border-left: 1px solid #DADCE0;
border-right: 1px solid #DADCE0;
.menu-search {
display: none;
}
&.is-pinned-message-shown:not(.hide-pinned) {
.pinned-message {
display: flex;
}
}
}
@include respond-to(no-floating-left-sidebar) {
.sidebar-close-button {
display: none;
}
}
/* @include respond-to(wide-screens) {
transition: .2s ease-in-out;
align-self: start;
body.is-right-column-shown & {
width: calc(100% - (#{$large-screen} / 4));
}
body.animation-level-0 & {
transition: none;
}
} */
@include respond-to(handhelds) {
.chat-mute-button, .chat-search-button {
display: none;
}
}
/* @include respond-to(handhelds) {
position: sticky;
top: 0;
z-index: 3;
} */
.chat-more-button {
.btn-menu {
top: calc(100% + 7px);
@include respond-to(handhelds) {
top: 29px;
}
}
@include respond-to(handhelds) {
margin-left: 0;
}
}
.chat-info {
flex: 1 1 auto;
overflow: hidden;
//--utils-width: NaN;
//&.have-utils-width {
max-width: calc(100% - var(--utils-width));
@include respond-to(medium-screens) {
body.is-right-column-shown & {
max-width: calc(100% - var(--right-column-width) - var(--utils-width));
}
}
//}
}
.chat-utils {
display: flex;
align-items: center;
flex: 0 0 auto;
/* position: absolute;
right: 0px;
padding-right: inherit; */
@include respond-to(medium-screens) {
transition: transform var(--layer-transition);
body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
}
body.animation-level-0 & {
transition: none;
}
}
}
.chat-join {
width: auto;
width: 117px;
height: 36px;
font-weight: 400;
font-size: 0.875rem;
margin-right: .5rem;
&:not(.hide) + .chat-mute-button {
display: none;
}
}
.content {
flex: 1 1 auto;
padding-left: 10px;
max-width: 100%;
overflow: hidden;
}
.person {
display: flex;
align-items: center;
cursor: pointer;
margin-left: 7px;
@include respond-to(handhelds) {
margin-left: 10px;
}
.bottom {
font-size: 14px;
//line-height: 18px;
color: #707579;
.online {
color: $color-blue;
}
}
}
#im-avatar {
width: 40px;
height: 40px;
line-height: 40px;
//font-size: 16px;
font-size: 16px;
flex: 0 0 auto;
&:before {
font-size: 20px;
}
&.tgico-avatar_deletedaccount:before {
font-size: 40px;
}
}
&.hide-pinned + #bubbles {
#bubbles-inner {
margin-bottom: .25rem;
}
}
}
#chat-input {
.chat-input {
--translateY: 0;
display: flex;
width: 100%;
@ -220,11 +33,11 @@ $chat-helper-size: 39px; @@ -220,11 +33,11 @@ $chat-helper-size: 39px;
@include respond-to(esg-top) {
/* flex: 0 0 auto;
height: auto; */
max-width: var(--messages-container-width);
max-width: var(--messages-container-width) !important;
}
@include respond-to(medium-screens) {
width: calc(100% - var(--right-column-width));
width: calc(100% - var(--right-column-width)) !important;
//transition: transform var(--layer-transition);
body.is-right-column-shown & {
@ -242,7 +55,7 @@ $chat-helper-size: 39px; @@ -242,7 +55,7 @@ $chat-helper-size: 39px;
position: absolute;
bottom: 0;
#bubbles.is-selecting:not(.backwards) ~ & {
.bubbles.is-selecting:not(.backwards) ~ & {
--translateY: 0;
}
}
@ -286,7 +99,7 @@ $chat-helper-size: 39px; @@ -286,7 +99,7 @@ $chat-helper-size: 39px;
overflow: hidden;
} */
#input-message {
.input-message-input {
background: none;
border: none;
width: 100%;
@ -321,7 +134,7 @@ $chat-helper-size: 39px; @@ -321,7 +134,7 @@ $chat-helper-size: 39px;
}
}
#btn-record-cancel {
.btn-record-cancel {
visibility: hidden;
opacity: 0;
transition: visibility 0s .1s, opacity .1s 0s;
@ -347,7 +160,7 @@ $chat-helper-size: 39px; @@ -347,7 +160,7 @@ $chat-helper-size: 39px;
padding-bottom: inherit;
}
#btn-send {
.btn-send {
color: #9e9e9e;
> .tgico {
@ -357,7 +170,7 @@ $chat-helper-size: 39px; @@ -357,7 +170,7 @@ $chat-helper-size: 39px;
}
&.send {
color: $color-blue;
color: $color-blue !important;
}
&.send .tgico-send,
@ -370,10 +183,10 @@ $chat-helper-size: 39px; @@ -370,10 +183,10 @@ $chat-helper-size: 39px;
transition: .2s color, background-color .2s, .2s opacity;
}
#btn-record-cancel, #btn-send {
.btn-record-cancel, .btn-send {
font-size: 1.5rem;
line-height: 1.5rem;
background-color: #fff;
background-color: #fff !important;
}
.record-time {
@ -420,24 +233,24 @@ $chat-helper-size: 39px; @@ -420,24 +233,24 @@ $chat-helper-size: 39px;
color: #c6cbce;
}
&:not(.is-recording) #btn-send {
&:not(.is-recording) .btn-send {
color: #c6cbce;
}
}
&.is-recording {
#btn-record-cancel {
.btn-record-cancel {
opacity: 1;
visibility: visible;
transition: visibility 0s .1s, opacity .1s .1s;
}
// unlock
#btn-send, #btn-record-cancel {
.btn-send, .btn-record-cancel {
pointer-events: all;
}
.input-message {
.rows-wrapper {
width: calc(100% - #{$chat-input-size * 2 + $btn-send-margin * 2});
@include respond-to(handhelds) {
@ -445,7 +258,7 @@ $chat-helper-size: 39px; @@ -445,7 +258,7 @@ $chat-helper-size: 39px;
}
}
#attach-file {
.attach-file {
display: none;
}
@ -460,13 +273,13 @@ $chat-helper-size: 39px; @@ -460,13 +273,13 @@ $chat-helper-size: 39px;
}
&:not(.is-recording) {
#btn-record-cancel {
.btn-record-cancel {
margin-right: 0;
width: 0px;
}
}
#bubbles.is-selecting ~ & {
.bubbles.is-selecting ~ & {
.new-message-wrapper {
html:not(.is-safari) & {
transition: .1s opacity;
@ -477,13 +290,13 @@ $chat-helper-size: 39px; @@ -477,13 +290,13 @@ $chat-helper-size: 39px;
opacity: 0;
}
#btn-send {
.btn-send {
html:not(.is-safari) & {
transition: .2s transform;
}
}
.input-message {
.rows-wrapper {
html:not(.is-safari) & {
transition: width .2s, border-bottom-right-radius .1s, transform .2s;
@ -500,7 +313,7 @@ $chat-helper-size: 39px; @@ -500,7 +313,7 @@ $chat-helper-size: 39px;
}
}
#bubbles.is-selecting:not(.backwards) ~ & {
.bubbles.is-selecting:not(.backwards) ~ & {
.new-message-wrapper {
opacity: 0;
}
@ -515,7 +328,7 @@ $chat-helper-size: 39px; @@ -515,7 +328,7 @@ $chat-helper-size: 39px;
}
}
.input-message {
.rows-wrapper {
max-height: $chat-input-size;
border-bottom-right-radius: 12px;
transform-origin: left;
@ -556,14 +369,14 @@ $chat-helper-size: 39px; @@ -556,14 +369,14 @@ $chat-helper-size: 39px;
border-radius: inherit;
}
#btn-send {
.btn-send {
transform: scale(0);
}
}
#bubbles.is-selecting.backwards ~ & {
.bubbles.is-selecting.backwards ~ & {
.new-message-wrapper {
html:not(.is-safari) & {
transition-delay: .1s;
@ -593,41 +406,8 @@ $chat-helper-size: 39px; @@ -593,41 +406,8 @@ $chat-helper-size: 39px;
}
}
#im-title {
cursor: pointer;
font-size: 18px;
line-height: 24px;
max-width: calc(100% - 1.5rem);
/* @include respond-to(handhelds) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} */
span.emoji {
vertical-align: inherit;
}
}
#im-title, #im-subtitle {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
#im-subtitle {
margin-top: -2px;
}
// !WARNING, уже нельзя переименовать в ID, так как это заденет другие селекторы и вёрстка в чате поедет, например - стикер вместе с реплаем
.chat-container {
display: flex;
// padding: 200px;
#column-center {
width: 100%;
align-items: center;
//overflow: hidden;
flex-direction: column;
position: relative;
flex: 3;
@ -648,8 +428,21 @@ $chat-helper-size: 39px; @@ -648,8 +428,21 @@ $chat-helper-size: 39px;
}
}
}
}
.chats-container {
height: 100%;
}
.chat-background {
.chat {
display: flex;
// padding: 200px;
width: 100%;
align-items: center;
//overflow: hidden;
flex-direction: column;
&-background {
overflow: hidden;
&.no-transition:before {
@ -688,7 +481,7 @@ $chat-helper-size: 39px; @@ -688,7 +481,7 @@ $chat-helper-size: 39px;
}
}
.input-message {
.rows-wrapper {
--padding-vertical: .3125rem;
--padding-horizontal: .5rem;
--padding: var(--padding-vertical) var(--padding-horizontal);
@ -760,7 +553,7 @@ $chat-helper-size: 39px; @@ -760,7 +553,7 @@ $chat-helper-size: 39px;
transform: scaleX(-1);
}
#attach-file {
.attach-file {
&.menu-open {
color: $color-blue;
background-color: transparent;
@ -806,7 +599,7 @@ $chat-helper-size: 39px; @@ -806,7 +599,7 @@ $chat-helper-size: 39px;
padding-top: .25rem;
}
.chat-container.is-helper-active & {
.chat.is-helper-active & {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
transform: translateY(#{-$chat-helper-size});
@ -934,7 +727,7 @@ $chat-helper-size: 39px; @@ -934,7 +727,7 @@ $chat-helper-size: 39px;
}
}
#bubbles {
.bubbles {
--translateY: 0;
/* overflow-y: scroll;
scrollbar-width: none;
@ -953,11 +746,11 @@ $chat-helper-size: 39px; @@ -953,11 +746,11 @@ $chat-helper-size: 39px;
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); // fix safari overflow
} */
.chat-container.is-helper-active & {
.chat.is-helper-active & {
&:not(.is-selecting), &.is-selecting.backwards {
--translateY: -#{$chat-helper-size};
#bubbles-inner {
.bubbles-inner {
transform: translateY(calc(var(--translateY) * -1));
//margin-top: $chat-helper-size;
//transition: none;
@ -972,14 +765,14 @@ $chat-helper-size: 39px; @@ -972,14 +765,14 @@ $chat-helper-size: 39px;
--translateY: -58px;
}
#bubbles-inner {
.bubbles-inner {
transform: translateY(calc(var(--translateY) * -1));
//margin-top: $chat-helper-size;
//transition: none;
}
}
/* #bubbles-transform-helper {
/* .bubbles-transform-helper {
width: 100%;
height: 100%;
max-height: 100%;
@ -1041,14 +834,14 @@ $chat-helper-size: 39px; @@ -1041,14 +834,14 @@ $chat-helper-size: 39px;
} */
&:not(.scrolled-down):not(.search-results-active) {
//> #bubbles-transform-helper {
//> .bubbles-transform-helper {
// ! these lines will blur messages if chat input helper is active
> .scrollable {
-webkit-mask-image: -webkit-linear-gradient(bottom, transparent, #000 20px);
mask-image: linear-gradient(0deg, transparent 0, #000 20px);
}
> #bubbles-go-down {
> .bubbles-go-down {
cursor: pointer;
--translateY: 0;
opacity: 1;
@ -1079,7 +872,7 @@ $chat-helper-size: 39px; @@ -1079,7 +872,7 @@ $chat-helper-size: 39px;
}
}
#bubbles-inner {
.bubbles-inner {
width: 100%;
display: flex;
flex-direction: column;
@ -1132,7 +925,7 @@ $chat-helper-size: 39px; @@ -1132,7 +925,7 @@ $chat-helper-size: 39px;
}
}
/* #bubbles.is-chat-input-hidden & {
/* .bubbles.is-chat-input-hidden & {
padding-bottom: 55px;
} */
@ -1143,7 +936,7 @@ $chat-helper-size: 39px; @@ -1143,7 +936,7 @@ $chat-helper-size: 39px;
}
&.is-scrolling .is-sticky {
opacity: 0.99999; // 0.99999 сделано для сафари, т.к. без этого будет прыжок при скролле в самом низу или верху
opacity: 0.99999 !important; // 0.99999 сделано для сафари, т.к. без этого будет прыжок при скролле в самом низу или верху
html.is-safari & {
transform: translateY(calc(var(--translateY) * -1));
@ -1152,7 +945,7 @@ $chat-helper-size: 39px; @@ -1152,7 +945,7 @@ $chat-helper-size: 39px;
}
}
#bubbles-go-down {
.bubbles-go-down {
position: absolute;
background-color: #fff;
border-radius: 50%;
@ -1224,7 +1017,7 @@ $chat-helper-size: 39px; @@ -1224,7 +1017,7 @@ $chat-helper-size: 39px;
color: #949596;
}
& + #chat-input {
& + .chat-input {
display: none;
}
}
@ -1325,140 +1118,3 @@ $chat-helper-size: 39px; @@ -1325,140 +1118,3 @@ $chat-helper-size: 39px;
}
}
}
.markup-tooltip {
$widthRegular: 218px;
$widthLink: 420px;
$padding: 7px;
background: #fff;
border-radius: $border-radius-medium;
transform: translateZ(0);
opacity: 0;
transition: opacity var(--layer-transition), transform var(--layer-transition), width var(--layer-transition);
position: fixed;
left: 0;
top: 0;
height: 44px;
width: $widthRegular;
overflow: hidden;
&-wrapper {
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: start;
//width: 420px;
width: #{$widthRegular + $widthLink};
height: 100%;
transform: translateX(0);
transition: transform var(--layer-transition);
}
&-tools {
display: flex;
align-items: center;
justify-content: space-between;
padding: $padding;
&:first-child {
width: $widthRegular;
}
&:last-child {
width: $widthLink;
.markup-tooltip-delimiter {
margin-left: .25rem;
margin-right: .75rem;
}
}
}
&-delimiter {
width: 1px;
height: 25px;
background-color: #DADCE0;
}
.btn-icon {
border-radius: $border-radius !important;
width: 30px;
height: 30px;
padding: 0;
&.active {
color: #fff!important;
background-color: $color-blue!important;
}
}
&:not(.is-visible) {
pointer-events: none;
}
&.is-visible {
opacity: 1;
}
&.is-link {
width: $widthLink;
}
&.is-link &-wrapper {
transform: translateX(#{-$widthRegular});
}
.input-clear {
flex: 1 1 auto;
text-overflow: ellipsis;
}
&.no-transition {
transition: none;
}
}
.stickers-helper {
position: absolute !important;
bottom: calc(100% + 10px);
opacity: 0;
transition: opacity .2s ease-in-out;
overflow: hidden;
padding: 0 !important;
> .scrollable {
position: relative;
max-height: 220px;
min-height: var(--esg-sticker-size);
}
&-stickers {
display: flex;
flex-wrap: wrap;
}
&-sticker {
position: relative;
width: var(--esg-sticker-size);
height: var(--esg-sticker-size);
margin: 5px;
img {
width: 100%;
height: 100%;
}
}
&:not(.is-visible) {
display: none;
}
&.is-visible {
&:not(.backwards) {
opacity: 1;
}
}
}

22
src/scss/partials/_chatBubble.scss

@ -64,7 +64,7 @@ $bubble-margin: .25rem; @@ -64,7 +64,7 @@ $bubble-margin: .25rem;
margin: 0 auto;
user-select: none;
&.is-highlighted, &.is-selected, /* #bubbles.is-selecting */ & {
&.is-highlighted, &.is-selected, /* .bubbles.is-selecting */ & {
&:after {
position: absolute;
left: -50%;
@ -158,7 +158,7 @@ $bubble-margin: .25rem; @@ -158,7 +158,7 @@ $bubble-margin: .25rem;
&.is-sticky {
opacity: .00001; // for safari
#bubbles-inner:not(.is-scrolling) & {
.bubbles-inner:not(.is-scrolling) & {
//transition-delay: 1.35s;
.bubble__container {
@ -166,7 +166,7 @@ $bubble-margin: .25rem; @@ -166,7 +166,7 @@ $bubble-margin: .25rem;
}
}
/* #bubbles-inner.is-scrolling & {
/* .bubbles-inner.is-scrolling & {
transition-delay: 0;
} */
}
@ -230,7 +230,7 @@ $bubble-margin: .25rem; @@ -230,7 +230,7 @@ $bubble-margin: .25rem;
}
}
#bubbles.is-selecting &:not(.is-album) {
.bubbles.is-selecting &:not(.is-album) {
.audio, .document, .attachment, poll-element {
pointer-events: none !important;
}
@ -252,8 +252,8 @@ $bubble-margin: .25rem; @@ -252,8 +252,8 @@ $bubble-margin: .25rem;
transition: .2s transform;
user-select: none;
html.no-touch #bubbles:not(.is-selecting) &,
html.is-touch #bubbles.is-selecting:not(.no-select) & {
html.no-touch .bubbles:not(.is-selecting) &,
html.is-touch .bubbles.is-selecting:not(.no-select) & {
user-select: text;
}
@ -276,13 +276,13 @@ $bubble-margin: .25rem; @@ -276,13 +276,13 @@ $bubble-margin: .25rem;
font-size: 1rem;
cursor: pointer;
#bubbles.is-selecting & {
.bubbles.is-selecting & {
transform: scale3d(1, 1, 1);
transform-origin: bottom;
transition: transform var(--layer-transition);
}
#bubbles.is-selecting:not(.backwards) & {
.bubbles.is-selecting:not(.backwards) & {
transform: scale3d(.76, .76, 1);
}
@ -350,7 +350,7 @@ $bubble-margin: .25rem; @@ -350,7 +350,7 @@ $bubble-margin: .25rem;
bottom: 8px;
}
#bubbles-inner.is-chat &.is-in {
.bubbles-inner.is-chat &.is-in {
.bubble-select-checkbox {
bottom: 7px;
}
@ -641,7 +641,7 @@ $bubble-margin: .25rem; @@ -641,7 +641,7 @@ $bubble-margin: .25rem;
position: absolute;
overflow: hidden;
/* #bubbles.is-selecting & {
/* .bubbles.is-selecting & {
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); // fix safari overflow
} */
@ -1014,6 +1014,8 @@ $bubble-margin: .25rem; @@ -1014,6 +1014,8 @@ $bubble-margin: .25rem;
&-details {
padding-left: 12px;
margin-top: 8px;
display: flex;
flex-direction: column;
}
&-name {

94
src/scss/partials/_chatMarkupTooltip.scss

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
.markup-tooltip {
$widthRegular: 218px;
$widthLink: 420px;
$padding: 7px;
background: #fff;
border-radius: $border-radius-medium;
transform: translateZ(0);
opacity: 0;
transition: opacity var(--layer-transition), transform var(--layer-transition), width var(--layer-transition);
position: fixed;
left: 0;
top: 0;
height: 44px;
width: $widthRegular;
overflow: hidden;
&-wrapper {
position: absolute;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: start;
//width: 420px;
width: #{$widthRegular + $widthLink};
height: 100%;
transform: translateX(0);
transition: transform var(--layer-transition);
}
&-tools {
display: flex;
align-items: center;
justify-content: space-between;
padding: $padding;
&:first-child {
width: $widthRegular;
}
&:last-child {
width: $widthLink;
.markup-tooltip-delimiter {
margin-left: .25rem;
margin-right: .75rem;
}
}
}
&-delimiter {
width: 1px;
height: 25px;
background-color: #DADCE0;
}
.btn-icon {
border-radius: $border-radius !important;
width: 30px;
height: 30px;
padding: 0;
&.active {
color: #fff!important;
background-color: $color-blue!important;
}
}
&:not(.is-visible) {
pointer-events: none;
}
&.is-visible {
opacity: 1;
}
&.is-link {
width: $widthLink;
}
&.is-link &-wrapper {
transform: translateX(#{-$widthRegular});
}
.input-clear {
flex: 1 1 auto;
text-overflow: ellipsis;
}
&.no-transition {
transition: none;
}
}

41
src/scss/partials/_chatStickersHelper.scss

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
.stickers-helper {
position: absolute !important;
bottom: calc(100% + 10px);
opacity: 0;
transition: opacity .2s ease-in-out;
overflow: hidden;
padding: 0 !important;
> .scrollable {
position: relative;
max-height: 220px;
min-height: var(--esg-sticker-size);
}
&-stickers {
display: flex;
flex-wrap: wrap;
}
&-sticker {
position: relative;
width: var(--esg-sticker-size);
height: var(--esg-sticker-size);
margin: 5px;
img {
width: 100%;
height: 100%;
}
}
&:not(.is-visible) {
display: none;
}
&.is-visible {
&:not(.backwards) {
opacity: 1;
}
}
}

223
src/scss/partials/_chatTopbar.scss

@ -0,0 +1,223 @@ @@ -0,0 +1,223 @@
.topbar {
width: 100%;
background-color: #fff;
user-select: none;
box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .21);
z-index: 1;
min-height: 3.5rem;
max-height: 3.5rem;
// border-bottom: 1px solid #DADCE0;
@include respond-to(handhelds) {
&.is-pinned-audio-shown, &.is-pinned-message-shown:not(.hide-pinned) {
& + .bubbles {
margin-top: 52px;
}
}
&.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) {
.pinned-message {
display: flex;
}
}
}
@include respond-to(not-handhelds) {
border-left: 1px solid #DADCE0;
border-right: 1px solid #DADCE0;
.menu-search {
display: none;
}
&.is-pinned-message-shown:not(.hide-pinned) {
.pinned-message {
display: flex;
}
}
}
@include respond-to(no-floating-left-sidebar) {
.chat:first-child & {
.sidebar-close-button {
display: none;
}
.chat-info {
padding-left: 7px;
}
}
}
.sidebar-close-button {
position: absolute;
}
/* @include respond-to(wide-screens) {
transition: .2s ease-in-out;
align-self: start;
body.is-right-column-shown & {
width: calc(100% - (#{$large-screen} / 4));
}
body.animation-level-0 & {
transition: none;
}
} */
@include respond-to(handhelds) {
.chat-mute-button, .chat-search-button {
display: none;
}
}
/* @include respond-to(handhelds) {
position: sticky;
top: 0;
z-index: 3;
} */
.user-title {
cursor: pointer;
font-size: 18px;
line-height: 24px;
max-width: calc(100% - 1.5rem);
/* @include respond-to(handhelds) {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} */
span.emoji {
vertical-align: inherit;
}
}
.user-title, .info {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.info {
margin-top: -2px;
}
.btn-menu-toggle {
.btn-menu {
top: calc(100% + 7px);
@include respond-to(handhelds) {
top: 29px;
}
}
@include respond-to(handhelds) {
margin-left: 0;
}
}
.chat-info {
flex: 1 1 auto;
overflow: hidden;
padding-left: 49px;
//--utils-width: NaN;
//&.have-utils-width {
max-width: calc(100% - var(--utils-width));
@include respond-to(medium-screens) {
body.is-right-column-shown & {
max-width: calc(100% - var(--right-column-width) - var(--utils-width));
}
}
//}
}
.chat-utils {
display: flex;
align-items: center;
flex: 0 0 auto;
/* position: absolute;
right: 0px;
padding-right: inherit; */
@include respond-to(medium-screens) {
transition: transform var(--layer-transition);
body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
}
body.animation-level-0 & {
transition: none;
}
}
}
.chat-join {
width: auto;
width: 117px;
height: 36px;
font-weight: 400;
font-size: 0.875rem;
margin-right: .5rem;
&:not(.hide) + .chat-mute-button {
display: none;
}
}
.content {
flex: 1 1 auto;
padding-left: 10px;
max-width: 100%;
overflow: hidden;
}
.person {
display: flex;
align-items: center;
cursor: pointer;
@include respond-to(handhelds) {
margin-left: 10px;
}
.bottom {
font-size: 14px;
//line-height: 18px;
color: #707579;
.online {
color: $color-blue;
}
}
}
avatar-element {
width: 40px;
height: 40px;
line-height: 40px;
//font-size: 16px;
font-size: 16px;
flex: 0 0 auto;
&:before {
font-size: 20px;
}
&.tgico-avatar_deletedaccount:before {
font-size: 40px;
}
}
&.hide-pinned + .bubbles {
.bubbles-inner {
margin-bottom: .25rem;
}
}
}

4
src/scss/partials/_chatlist.scss

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
.chats-container {
.chatlist-container {
position: relative;
/* .scrollable {
@ -391,7 +391,7 @@ @@ -391,7 +391,7 @@
}
}
// use together like class="chats-container contacts-container"
// use together like class="chatlist-container contacts-container"
.contacts-container, .search-group-contacts {
.dialog-avatar {
width: 48px;

8
src/scss/partials/_leftSidebar.scss

@ -109,7 +109,7 @@ @@ -109,7 +109,7 @@
}
}
#chats-container {
#chatlist-container {
max-height: 100%;
overflow: hidden;
position: relative;
@ -393,7 +393,7 @@ @@ -393,7 +393,7 @@
}
}
.chats-container {
.chatlist-container {
flex: 1 1 auto;
}
@ -690,7 +690,7 @@ @@ -690,7 +690,7 @@
color: #50a2e9;
}
.popup-forward, .included-chats-container {
.popup-forward, .included-chatlist-container {
.selector {
ul {
li > .rp {
@ -721,7 +721,7 @@ @@ -721,7 +721,7 @@
}
}
.included-chats-container {
.included-chatlist-container {
.sidebar-left-h2 {
color: #707579;
font-size: 15px;

4
src/scss/partials/_rightSidebar.scss

@ -51,7 +51,7 @@ @@ -51,7 +51,7 @@
}
#search-private-container {
.chats-container {
.chatlist-container {
position: relative;
flex: 1 1 auto;
}
@ -150,7 +150,7 @@ @@ -150,7 +150,7 @@
margin-top: 3px;
}
&.online {
.online {
color: $color-blue;
}
}

2
src/scss/partials/_ripple.scss

@ -75,7 +75,7 @@ @@ -75,7 +75,7 @@
}
@include respond-to(until-floating-left-sidebar) {
.chats-container ul li > .rp {
.chatlist-container ul li > .rp {
.c-ripple {
--ripple-duration: .2s;

2
src/scss/partials/_selector.scss

@ -111,7 +111,7 @@ @@ -111,7 +111,7 @@
}
}
.chats-container {
.chatlist-container {
height: 100%;
flex: 1 1 auto;
}

2
src/scss/partials/popups/_forward.scss

@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
}
}
.selector, .chats-container {
.selector, .chatlist-container {
height: auto;
overflow: hidden;
display: flex;

3
src/scss/style.scss

@ -108,8 +108,11 @@ $messages-container-width: 728px; @@ -108,8 +108,11 @@ $messages-container-width: 728px;
@import "partials/checkbox";
@import "partials/chatlist";
@import "partials/chat";
@import "partials/chatTopbar";
@import "partials/chatBubble";
@import "partials/chatPinned";
@import "partials/chatMarkupTooltip";
@import "partials/chatStickersHelper";
@import "partials/sidebar";
@import "partials/leftSidebar";
@import "partials/rightSidebar";

Loading…
Cancel
Save