Browse Source

Pause media in other tabs

Deactivate tab when new version in installed
Fix messages grouping
Fix downloading photos from media viewer
DIspatch notification to better tab
master
Eduard Kuzmenko 2 years ago
parent
commit
353824d12e
  1. 2
      src/components/animationIntersector.ts
  2. 24
      src/components/appMediaPlaybackController.ts
  3. 6
      src/components/appMediaViewer.ts
  4. 21
      src/components/appSearchSuper..ts
  5. 2
      src/components/avatar.ts
  6. 374
      src/components/chat/bubbleGroups.ts
  7. 472
      src/components/chat/bubbles.ts
  8. 22
      src/components/chat/chat.ts
  9. 1
      src/components/chat/inlineHelper.ts
  10. 11
      src/components/chat/input.ts
  11. 6
      src/components/chat/selection.ts
  12. 5
      src/components/connectionStatus.ts
  13. 5
      src/components/groupCall/participants.ts
  14. 8
      src/components/popups/index.ts
  15. 5
      src/components/popups/joinChatInvite.ts
  16. 4
      src/components/putPhoto.ts
  17. 4
      src/components/sidebarLeft/index.ts
  18. 3
      src/components/sidebarLeft/tabs/newChannel.ts
  19. 29
      src/components/sidebarLeft/tabs/newGroup.ts
  20. 10
      src/components/wrappers/photo.ts
  21. 2
      src/config/app.ts
  22. 24
      src/config/databases/state.ts
  23. 2
      src/environment/sharedWorkerSupport.ts
  24. 15
      src/helpers/array/partition.ts
  25. 32
      src/helpers/idleController.ts
  26. 24
      src/helpers/listenMessagePort.ts
  27. 1
      src/helpers/scrollSaver.ts
  28. 8
      src/helpers/toggleStorages.ts
  29. 4
      src/index.ts
  30. 2
      src/lang.ts
  31. 230
      src/layer.d.ts
  32. 4
      src/lib/appManagers/appChatsManager.ts
  33. 2
      src/lib/appManagers/appDialogsManager.ts
  34. 13
      src/lib/appManagers/appDownloadManager.ts
  35. 51
      src/lib/appManagers/appImManager.ts
  36. 2
      src/lib/appManagers/appInlineBotsManager.ts
  37. 3
      src/lib/appManagers/appManagersManager.ts
  38. 29
      src/lib/appManagers/appMessagesManager.ts
  39. 2
      src/lib/appManagers/appStateManager.ts
  40. 52
      src/lib/appManagers/appTabsManager.ts
  41. 95
      src/lib/appManagers/getProxiedManagers.ts
  42. 18
      src/lib/appManagers/uiNotificationsManager.ts
  43. 11
      src/lib/appManagers/utils/state/loadState.ts
  44. 6
      src/lib/cacheStorage.ts
  45. 1
      src/lib/crypto/aesCtrUtils.ts
  46. 12
      src/lib/localStorage.ts
  47. 11
      src/lib/mtproto/apiFileManager.ts
  48. 2
      src/lib/mtproto/apiManager.ts
  49. 29
      src/lib/mtproto/mtproto.worker.ts
  50. 12
      src/lib/mtproto/mtprotoMessagePort.ts
  51. 51
      src/lib/mtproto/mtprotoworker.ts
  52. 2
      src/lib/mtproto/schema.ts
  53. 144
      src/lib/mtproto/singleInstance.ts
  54. 209
      src/lib/mtproto/superMessagePort.ts
  55. 29
      src/lib/mtproto/transports/obfuscation.ts
  56. 32
      src/lib/mtproto/webPushApiManager.ts
  57. 24
      src/lib/rootScope.ts
  58. 3
      src/lib/serviceWorker/index.service.ts
  59. 3
      src/lib/sessionStorage.ts
  60. 4
      src/lib/storage.ts
  61. 10
      src/lib/storages/filters.ts
  62. 2
      src/pages/pageSignIn.ts
  63. 4
      src/scripts/in/schema.json
  64. 2
      src/scripts/out/schema.json
  65. 6
      src/scss/partials/_chatBubble.scss

2
src/components/animationIntersector.ts

@ -191,7 +191,7 @@ export class AnimationIntersector {
this.visible.has(player) && this.visible.has(player) &&
animation.autoplay && animation.autoplay &&
(!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group) && (!this.onlyOnePlayableGroup || this.onlyOnePlayableGroup === group) &&
(!idleController.idle.isIDLE || this.overrideIdleGroups.has(player.group)) (!idleController.isIdle || this.overrideIdleGroups.has(player.group))
) { ) {
//console.warn('play animation:', animation); //console.warn('play animation:', animation);
animation.play(); animation.play();

24
src/components/appMediaPlaybackController.ts

@ -102,6 +102,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
private pip: HTMLVideoElement; private pip: HTMLVideoElement;
private managers: AppManagers; private managers: AppManagers;
private skipMediaPlayEvent: boolean;
construct(managers: AppManagers) { construct(managers: AppManagers) {
this.managers = managers; this.managers = managers;
@ -140,6 +141,17 @@ export class AppMediaPlaybackController extends EventListenerBase<{
} }
}); });
rootScope.addEventListener('media_play', () => {
if(this.skipMediaPlayEvent) {
this.skipMediaPlayEvent = false;
return;
}
if(!this.pause() && this.pip) {
this.pip.pause();
}
});
const properties: {[key: PropertyKey]: PropertyDescriptor} = {}; const properties: {[key: PropertyKey]: PropertyDescriptor} = {};
const keys = [ const keys = [
'volume' as const, 'volume' as const,
@ -602,6 +614,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
} }
this.dispatchEvent('play', this.getPlayingDetails()); this.dispatchEvent('play', this.getPlayingDetails());
this.pauseMediaInOtherTabs();
}, 0); }, 0);
}; };
@ -639,6 +652,11 @@ export class AppMediaPlaybackController extends EventListenerBase<{
} }
}; };
public pauseMediaInOtherTabs() {
this.skipMediaPlayEvent = true;
rootScope.dispatchEvent('media_play');
}
// public get pip() { // public get pip() {
// return document.pictureInPictureElement as HTMLVideoElement; // return document.pictureInPictureElement as HTMLVideoElement;
// } // }
@ -879,6 +897,8 @@ export class AppMediaPlaybackController extends EventListenerBase<{
if(pip) { if(pip) {
pip.pause(); pip.pause();
} }
this.pauseMediaInOtherTabs();
}; };
if(!media.paused) { if(!media.paused) {
@ -886,6 +906,8 @@ export class AppMediaPlaybackController extends EventListenerBase<{
} }
media.addEventListener('play', onPlay); media.addEventListener('play', onPlay);
} else { // maybe it's voice recording
this.pauseMediaInOtherTabs();
} }
this.willBePlayed(undefined); this.willBePlayed(undefined);
@ -952,6 +974,8 @@ export class AppMediaPlaybackController extends EventListenerBase<{
if(this.playingMedia !== video) { if(this.playingMedia !== video) {
this.pause(); this.pause();
} }
this.pauseMediaInOtherTabs();
// if(this.pause()) { // if(this.pause()) {
// listenerSetter.add(video)('pause', () => { // listenerSetter.add(video)('pause', () => {
// this.play(); // this.play();

6
src/components/appMediaViewer.ts

@ -223,11 +223,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const message = await this.getMessageByPeer(peerId, mid); const message = await this.getMessageByPeer(peerId, mid);
const media = getMediaFromMessage(message); const media = getMediaFromMessage(message);
if(!media) return; if(!media) return;
if(media._ === 'photo') { appDownloadManager.downloadToDisc({media, queueId: appImManager.chat.bubbles.lazyLoadQueue.queueId});
appDownloadManager.downloadToDisc({media, queueId: appImManager.chat.bubbles.lazyLoadQueue.queueId});
} else {
appDownloadManager.downloadToDisc({media, queueId: appImManager.chat.bubbles.lazyLoadQueue.queueId});
}
}; };
private setCaption(message: MyMessage) { private setCaption(message: MyMessage) {

21
src/components/appSearchSuper..ts

@ -213,9 +213,9 @@ class SearchContextMenu {
} }
private onGotoClick = () => { private onGotoClick = () => {
rootScope.dispatchEvent('history_focus', { appImManager.setInnerPeer({
peerId: this.peerId, peerId: this.peerId,
mid: this.mid, lastMsgId: this.mid,
threadId: this.searchSuper.searchContext.threadId threadId: this.searchSuper.searchContext.threadId
}); });
}; };
@ -597,13 +597,26 @@ export default class AppSearchSuper {
} }
private processEmptyFilter({message, searchGroup}: ProcessSearchSuperResult) { private processEmptyFilter({message, searchGroup}: ProcessSearchSuperResult) {
const loadPromises: Promise<any>[] = [];
const {dom} = appDialogsManager.addDialogNew({ const {dom} = appDialogsManager.addDialogNew({
peerId: message.peerId, peerId: message.peerId,
container: searchGroup.list, container: searchGroup.list,
avatarSize: 54 avatarSize: 54,
loadPromises
}); });
// appDialogsManager.setLastMessage(dialog, message, dom, this.searchContext.query); const setLastMessagePromise = appDialogsManager.setLastMessageN({
dialog: {
_: 'dialog',
peerId: message.peerId
} as any,
lastMessage: message,
dom,
highlightWord: this.searchContext.query
});
loadPromises.push(setLastMessagePromise);
return Promise.all(loadPromises);
} }
private async processPhotoVideoFilter({message, promises, middleware}: ProcessSearchSuperResult) { private async processPhotoVideoFilter({message, promises, middleware}: ProcessSearchSuperResult) {

2
src/components/avatar.ts

@ -198,7 +198,7 @@ export default class AvatarElement extends HTMLElement {
private r(onlyThumb = false) { private r(onlyThumb = false) {
const promise = putPhoto(this, this.peerId, this.isDialog, this.peerTitle, onlyThumb, this.isBig); const promise = putPhoto(this, this.peerId, this.isDialog, this.peerTitle, onlyThumb, this.isBig);
recordPromise(promise, 'avatar putPhoto-' + this.peerId); // recordPromise(promise, 'avatar putPhoto-' + this.peerId);
if(this.loadPromises) { if(this.loadPromises) {
this.loadPromises.push(promise); this.loadPromises.push(promise);

374
src/components/chat/bubbleGroups.ts

@ -16,16 +16,19 @@ import { Message } from "../../layer";
import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config"; import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
import { SERVICE_AS_REGULAR, STICKY_OFFSET } from "./bubbles"; import { SERVICE_AS_REGULAR, STICKY_OFFSET } from "./bubbles";
import forEachReverse from "../../helpers/array/forEachReverse"; import forEachReverse from "../../helpers/array/forEachReverse";
import partition from "../../helpers/array/partition";
type GroupItem = { type GroupItem = {
bubble: HTMLElement, bubble: HTMLElement,
fromId: PeerId, fromId: PeerId,
mid: number, mid: number,
groupMid?: number,
timestamp: number, timestamp: number,
dateTimestamp: number, dateTimestamp: number,
mounted: boolean, mounted: boolean,
single: boolean, single: boolean,
group?: BubbleGroup group?: BubbleGroup,
message: Message.message | Message.messageService // use it only to set avatar
}; };
class BubbleGroup { class BubbleGroup {
@ -38,6 +41,7 @@ class BubbleGroup {
avatar: AvatarElement; avatar: AvatarElement;
mounted: boolean; mounted: boolean;
dateTimestamp: number; dateTimestamp: number;
offset: number;
constructor(chat: Chat, groups: BubbleGroups, dateTimestamp: number) { constructor(chat: Chat, groups: BubbleGroups, dateTimestamp: number) {
this.container = document.createElement('div'); this.container = document.createElement('div');
@ -46,6 +50,7 @@ class BubbleGroup {
this.groups = groups; this.groups = groups;
this.items = []; this.items = [];
this.dateTimestamp = dateTimestamp; this.dateTimestamp = dateTimestamp;
this.offset = 0;
} }
createAvatar(message: Message.message | Message.messageService) { createAvatar(message: Message.message | Message.messageService) {
@ -57,6 +62,7 @@ class BubbleGroup {
this.avatarContainer = document.createElement('div'); this.avatarContainer = document.createElement('div');
this.avatarContainer.classList.add('bubbles-group-avatar-container'); this.avatarContainer.classList.add('bubbles-group-avatar-container');
++this.offset;
const fwdFrom = message.fwd_from; const fwdFrom = message.fwd_from;
const fwdFromId = message.fwdFromId; const fwdFromId = message.fwdFromId;
@ -88,17 +94,29 @@ class BubbleGroup {
return this.items[this.items.length - 1]; return this.items[this.items.length - 1];
} }
get lastMid() {
return this.lastItem.mid;
}
get lastItem() { get lastItem() {
return this.items[0]; return this.items[0];
} }
updateClassNames() { updateClassNames() {
const items = this.items; const items = this.items;
if(!items.length) { const length = items.length;
if(!length) {
return; return;
} }
const length = items.length; // const elements = Array.from(this.container.children);
// if(this.offset) elements.splice(0, this.offset);
// const length = elements.length;
// if(!length) {
// return;
// }
const first = items[length - 1].bubble; const first = items[length - 1].bubble;
if(items.length === 1) { if(items.length === 1) {
@ -111,7 +129,7 @@ class BubbleGroup {
//this.setClipIfNeeded(first, true); //this.setClipIfNeeded(first, true);
} }
for(let i = length - 2; i > 0; --i) { for(let i = 1, _length = length - 1; i < _length; ++i) {
const bubble = items[i].bubble; const bubble = items[i].bubble;
bubble.classList.remove('is-group-last', 'is-group-first'); bubble.classList.remove('is-group-last', 'is-group-first');
//this.setClipIfNeeded(bubble, true); //this.setClipIfNeeded(bubble, true);
@ -145,33 +163,51 @@ class BubbleGroup {
items.splice(i, 0, item); items.splice(i, 0, item);
} else { } else {
insertInDescendSortedArray(items, item, 'mid'); // insertInDescendSortedArray(items, item, 'mid');
insertInDescendSortedArray(items, item, 'groupMid');
} }
item.group = this;
if(items.length === 1) { if(items.length === 1) {
insertInDescendSortedArray(this.groups.groups, this, 'firstMid'); insertInDescendSortedArray(this.groups.groups, this, 'lastMid');
} }
} }
mount() { removeItem(item: GroupItem) {
if(!this.groups.groups.includes(this)) { // group can be already removed indexOfAndSplice(this.items, item);
return;
if(!this.items.length) {
indexOfAndSplice(this.groups.groups, this);
} }
const offset = this.avatar ? 1 : 0; item.group = undefined;
const items = this.items; }
mount(updateClassNames?: boolean) {
if(!this.groups.groups.includes(this) || !this.items.length) { // group can be already removed
debugger;
if(this.mounted) {
this.onItemUnmount();
}
this.updateClassNames(); return;
}
const {offset, items} = this;
const {length} = items; const {length} = items;
forEachReverse(items, (item, idx) => { forEachReverse(items, (item, idx) => {
this.mountItem(item, length - 1 - idx, offset); this.mountItem(item, length - 1 - idx, offset);
}); });
if(updateClassNames) {
this.updateClassNames();
}
this.onItemMount(); this.onItemMount();
} }
mountItem(item: GroupItem, idx = this.items.indexOf(item), offset = this.avatar ? 1 : 0) { mountItem(item: GroupItem, idx = this.items.indexOf(item), offset = this.offset) {
if(item.mounted) { if(item.mounted) {
return; return;
} }
@ -181,11 +217,13 @@ class BubbleGroup {
} }
unmountItem(item: GroupItem) { unmountItem(item: GroupItem) {
if(item.mounted) { if(!item.mounted) {
item.bubble.remove(); return;
item.mounted = false;
this.onItemUnmount();
} }
item.bubble.remove();
item.mounted = false;
this.onItemUnmount();
} }
onItemMount() { onItemMount() {
@ -234,7 +272,7 @@ class BubbleGroup {
// } // }
export default class BubbleGroups { export default class BubbleGroups {
private itemsArr: Array<GroupItem> = []; // descend sorted public itemsArr: Array<GroupItem> = []; // descend sorted
private itemsMap: Map<HTMLElement, GroupItem> = new Map(); private itemsMap: Map<HTMLElement, GroupItem> = new Map();
public groups: Array<BubbleGroup> = []; // descend sorted public groups: Array<BubbleGroup> = []; // descend sorted
private newGroupDiff = 121; // * 121 in scheduled messages private newGroupDiff = 121; // * 121 in scheduled messages
@ -243,32 +281,69 @@ export default class BubbleGroups {
} }
removeBubble(bubble: HTMLElement) { removeItem(item: GroupItem) {
item.group.removeItem(item);
this.removeItemFromCache(item);
}
removeAndUnmountBubble(bubble: HTMLElement) {
const item = this.getItemByBubble(bubble); const item = this.getItemByBubble(bubble);
if(!item) { if(!item) {
return; return;
} }
const items = this.itemsArr;
const index = items.indexOf(item);
const siblings = this.getSiblingsAtIndex(index, items);
const group = item.group; const group = item.group;
const items = group.items; this.removeItem(item);
if(items.length) { group.unmountItem(item);
indexOfAndSplice(items, item);
const modifiedGroups: Set<BubbleGroup> = new Set();
if(!items.length) { modifiedGroups.add(group);
indexOfAndSplice(this.groups, group);
} const [previousSibling, nextSibling] = siblings;
if(
previousSibling
&& nextSibling
&& this.canItemsBeGrouped(previousSibling, nextSibling)
&& previousSibling.group !== nextSibling.group
) {
const group = nextSibling.group;
this.f(nextSibling.group.items);
group.onItemUnmount();
modifiedGroups.add(previousSibling.group);
this.groupUngrouped();
} }
indexOfAndSplice(this.itemsArr, item); this.mountUnmountGroups(Array.from(modifiedGroups));
this.itemsMap.delete(bubble);
return item;
} }
removeAndUnmountBubble(bubble: HTMLElement) { mountUnmountGroups(groups: BubbleGroup[]) {
const item = this.removeBubble(bubble); // groups.sort((a, b) => (b.lastItem?.mid ?? 0) - (a.lastItem?.mid ?? 0));
if(item) {
item.group.unmountItem(item); const [toMount, toUnmount] = partition(groups, (group) => !!group.items.length);
toUnmount.forEach((group) => {
group.onItemUnmount();
})
toMount.forEach((group) => {
group.mount(true);
});
// toMount.forEach((group) => {
// group.updateClassNames();
// });
}
f(items: GroupItem[], index: number = 0, length = items.length) {
for(; index < length; ++index) {
const item = items[index];
item.mounted = false;
item.group.removeItem(item);
--length;
--index;
} }
} }
@ -280,20 +355,22 @@ export default class BubbleGroups {
return this.groups[0]; return this.groups[0];
} }
// changeBubbleMid(bubble: HTMLElement, mid: number) { changeBubbleMid(bubble: HTMLElement, mid: number) {
// const item = this.getItemByBubble(bubble); const item = this.getItemByBubble(bubble);
// if(!item) { if(!item) {
// return; return;
// } }
// item.mid = mid;
// // indexOfAndSplice(item.group.items, item);
// // item.group.insertItem(item);
// indexOfAndSplice(this.itemsArr, item); item.mid = mid;
// insertInDescendSortedArray(this.itemsArr, item, 'mid');
// } // indexOfAndSplice(item.group.items, item);
// // const canChangeGroupMid = !item.group.items.length || item.group.items.every((item) => item.groupMid === item.mid);
// // if(canChangeGroupMid) item.groupMid = mid;
// item.group.insertItem(item);
indexOfAndSplice(this.itemsArr, item);
insertInDescendSortedArray(this.itemsArr, item, 'mid');
}
changeItemBubble(item: GroupItem, bubble: HTMLElement) { changeItemBubble(item: GroupItem, bubble: HTMLElement) {
this.itemsMap.delete(item.bubble); this.itemsMap.delete(item.bubble);
@ -310,45 +387,51 @@ export default class BubbleGroups {
this.changeItemBubble(item, to); this.changeItemBubble(item, to);
} }
/** canItemsBeGrouped(item1: GroupItem, item2: GroupItem) {
* return item2.fromId === item1.fromId
* @param item && Math.abs(item2.timestamp - item1.timestamp) <= this.newGroupDiff
* @param items expect descend sorted array && item1.dateTimestamp === item2.dateTimestamp
* @returns && !item1.single
*/ && !item2.single;
findIndexForItemInItems(item: GroupItem, items: GroupItem[]) { }
let foundAtIndex = -1;
for(let i = 0, length = items.length; i < length; ++i) { getSiblingsAtIndex(itemIndex: number, items: GroupItem[]) {
const _item = items[i]; return [items[itemIndex - 1], items[itemIndex + 1]] as const;
const diff = Math.abs(_item.timestamp - item.timestamp); }
const good = _item.fromId === item.fromId
&& diff <= this.newGroupDiff // findGroupSiblingInSiblings(item: GroupItem, siblings: ReturnType<BubbleGroups['getSiblingsAtIndex']>) {
&& item.dateTimestamp === _item.dateTimestamp // return siblings.find((sibling) => sibling && this.canItemsBeGrouped(item, sibling));
&& !item.single // }
&& !_item.single;
findGroupSiblingByItem(item: GroupItem, items: GroupItem[]) {
if(good) { items = items.slice();
foundAtIndex = i; const idx = insertInDescendSortedArray(items, item, 'mid');
// return this.findGroupSiblingInSiblings(item, this.getSiblingsAtIndex(idx, items));
if(this.chat.type === 'scheduled') { return this.findGroupSiblingInItems(item, items, idx);
break; }
}
} else {
foundAtIndex = -1;
}
if(this.chat.type !== 'scheduled') { findGroupSiblingInItems(item: GroupItem, items: GroupItem[], index = items.indexOf(item), length = items.length) {
if(item.mid > _item.mid) { const previousItem = items[index - 1];
let siblingGroupedItem: GroupItem;
if(previousItem?.group && this.canItemsBeGrouped(item, previousItem)) {
siblingGroupedItem = previousItem;
} else {
for(let k = index + 1; k < length; ++k) {
const nextItem = items[k];
if(this.canItemsBeGrouped(item, nextItem)) {
if(nextItem.group) {
siblingGroupedItem = nextItem;
}
} else {
break; break;
} }
} }
} }
return foundAtIndex; return siblingGroupedItem;
} }
addItemToGroup(item: GroupItem, group: BubbleGroup) { addItemToGroup(item: GroupItem, group: BubbleGroup) {
item.group = group;
group.insertItem(item); group.insertItem(item);
this.addItemToCache(item); this.addItemToCache(item);
} }
@ -358,6 +441,11 @@ export default class BubbleGroups {
this.itemsMap.set(item.bubble, item); this.itemsMap.set(item.bubble, item);
} }
removeItemFromCache(item: GroupItem) {
indexOfAndSplice(this.itemsArr, item);
this.itemsMap.delete(item.bubble);
}
getMessageFromId(message: MyMessage) { getMessageFromId(message: MyMessage) {
let fromId = message.viaBotId || message.fromId; let fromId = message.viaBotId || message.fromId;
@ -374,55 +462,113 @@ export default class BubbleGroups {
const {mid, date: timestamp} = message; const {mid, date: timestamp} = message;
const {dateTimestamp} = this.chat.bubbles.getDateForDateContainer(timestamp); const {dateTimestamp} = this.chat.bubbles.getDateForDateContainer(timestamp);
const item: GroupItem = { const item: GroupItem = {
bubble,
fromId: this.getMessageFromId(message),
mid, mid,
groupMid: mid,
fromId: this.getMessageFromId(message),
bubble,
timestamp, timestamp,
dateTimestamp, dateTimestamp,
mounted: false, mounted: false,
single: single single,
message
}; };
return item; return item;
} }
// prepareForGrouping(bubble: HTMLElement, message: MyMessage) { splitSiblingsOnGrouping(siblings: ReturnType<BubbleGroups['getSiblingsAtIndex']>) {
// const item = this.createItem(bubble, message); const [previousSibling, nextSibling] = siblings;
// this.addItemToCache(item); const previousGroup = previousSibling?.group;
// } const nextGroup = nextSibling?.group;
// groupUngrouped() { if(!previousGroup) {
// const items = this.itemsArr; return;
// const length = items.length; }
// for(let i = length - 1; i >= 0; --i) {
// const item = items[i];
// if(item.gr)
// }
// }
addBubble(bubble: HTMLElement, message: MyMessage, unmountIfFound?: boolean) { // will refresh group
const oldItem = this.getItemByBubble(bubble); // if(previousGroup === nextGroup) {
if(unmountIfFound) { // updating position const items = previousGroup.items;
this.removeAndUnmountBubble(bubble); const index = items.indexOf(previousSibling) + 1;
} else if(oldItem) { // editing const length = items.length;
const group = oldItem.group; if(index === length) {
this.changeItemBubble(oldItem, bubble); return;
oldItem.mounted = false; }
const modifiedGroups: BubbleGroup[] = [previousGroup];
// if(previousGroup !== nextGroup && nextGroup) {
// modifiedGroups.push(nextGroup);
// }
return {item: oldItem, group}; this.f(items, index, length);
return modifiedGroups;
// }
}
prepareForGrouping(bubble: HTMLElement, message: MyMessage) {
if(this.getItemByBubble(bubble)) {
debugger;
return;
} }
const item = this.createItem(bubble, message); const item = this.createItem(bubble, message);
this.addItemToCache(item);
}
const foundAtIndex = this.findIndexForItemInItems(item, this.itemsArr); groupUngrouped() {
const foundItem = this.itemsArr[foundAtIndex]; const items = this.itemsArr;
const length = items.length;
const modifiedGroups: Set<BubbleGroup> = new Set();
// for(let i = length - 1; i >= 0; --i) {
for(let i = 0; i < length; ++i) {
const item = items[i];
if(item.group) {
continue;
}
const group = foundItem?.group ?? new BubbleGroup(this.chat, this, item.dateTimestamp); let hadGroup = true;
this.addItemToGroup(item, group); const siblings = this.getSiblingsAtIndex(i, items);
const siblingGroupedItem = this.findGroupSiblingInItems(item, items, i, length);
return {item, group};
// const foundItem = this.findGroupSiblingInSiblings(item, siblings);
const foundItem = siblingGroupedItem;
const group = foundItem?.group ?? (hadGroup = false, new BubbleGroup(this.chat, this, item.dateTimestamp));
modifiedGroups.add(group);
group.insertItem(item);
if(!hadGroup) {
const splittedGroups = this.splitSiblingsOnGrouping(siblings);
if(splittedGroups) {
splittedGroups.forEach((group) => modifiedGroups.add(group));
}
}
}
return modifiedGroups;
} }
// addBubble(bubble: HTMLElement, message: MyMessage, unmountIfFound?: boolean) {
// const oldItem = this.getItemByBubble(bubble);
// if(unmountIfFound) { // updating position
// this.removeAndUnmountBubble(bubble);
// } else if(oldItem) { // editing
// const group = oldItem.group;
// this.changeItemBubble(oldItem, bubble);
// oldItem.mounted = false;
// return {item: oldItem, group};
// }
// const item = this.createItem(bubble, message);
// const foundItem = this.findSameGroupItem(item, this.itemsArr);
// const group = foundItem?.group ?? new BubbleGroup(this.chat, this, item.dateTimestamp);
// this.addItemToGroup(item, group);
// return {item, group};
// }
/* setClipIfNeeded(bubble: HTMLDivElement, remove = false) { /* setClipIfNeeded(bubble: HTMLDivElement, remove = false) {
//console.log('setClipIfNeeded', bubble, remove); //console.log('setClipIfNeeded', bubble, remove);
const className = bubble.className; const className = bubble.className;
@ -485,4 +631,18 @@ export default class BubbleGroups {
this.groups = []; this.groups = [];
this.itemsMap.clear(); this.itemsMap.clear();
} }
// findIncorrentPositions() {
// var bubbles = Array.from(this.chat.bubbles.chatInner.querySelectorAll('.bubbles-group .bubble')).reverse();
// var items = this.itemsArr;
// for(var i = 0, length = items.length; i < length; ++i) {
// const item = items[i];
// const foundBubble = bubbles[i];
// if(item.bubble !== foundBubble) {
// console.log('incorrect position', i, item, foundBubble);
// // debugger;
// // break;
// }
// }
// }
} }

472
src/components/chat/bubbles.ts

@ -156,7 +156,7 @@ function getMainMidForGrouped(mids: number[]) {
export default class ChatBubbles { export default class ChatBubbles {
public container: HTMLDivElement; public container: HTMLDivElement;
private chatInner: HTMLDivElement; public chatInner: HTMLDivElement;
public scrollable: Scrollable; public scrollable: Scrollable;
private getHistoryTopPromise: Promise<boolean>; private getHistoryTopPromise: Promise<boolean>;
@ -248,11 +248,14 @@ export default class ChatBubbles {
private attachPlaceholderOnRender: () => void; private attachPlaceholderOnRender: () => void;
private bubblesToEject: Set<HTMLElement> = new Set(); private bubblesToEject: Set<HTMLElement> = new Set();
private bubblesToReplace: Set<HTMLElement> = new Set();
private updatePlaceholderPosition: () => void; private updatePlaceholderPosition: () => void;
private setPeerOptions: {lastMsgId: number; topMessage: number;}; private setPeerOptions: {lastMsgId: number; topMessage: number;};
private setPeerTempId: number = 0; private setPeerTempId: number = 0;
private renderNewPromises: Set<Promise<any>> = new Set();
// private reactions: Map<number, ReactionsElement>; // private reactions: Map<number, ReactionsElement>;
constructor( constructor(
@ -280,62 +283,104 @@ export default class ChatBubbles {
// * events // * events
// will call when sent for update pos // will call when sent for update pos
this.listenerSetter.add(rootScope)('history_update', ({storageKey, peerId, mid, message, sequential}) => { this.listenerSetter.add(rootScope)('history_update', async({storageKey, mid, message}) => {
if(this.chat.messagesStorageKey !== storageKey) { if(this.chat.messagesStorageKey !== storageKey) {
return; return;
} }
const log = false ? this.log.bindPrefix('history_update-' + mid) : undefined;
log && log('start');
const bubble = this.bubbles[mid]; const bubble = this.bubbles[mid];
if(!bubble) return; if(!bubble) return;
if(this.renderNewPromises.size) {
log && log.error('will await new messages render');
await Promise.all(Array.from(this.renderNewPromises));
}
if(this.messagesQueuePromise) {
log && log.error('messages render in process');
await this.messagesQueuePromise;
}
if(this.bubbles[mid] !== bubble) return;
// await getHeavyAnimationPromise();
const item = this.bubbleGroups.getItemByBubble(bubble); const item = this.bubbleGroups.getItemByBubble(bubble);
if(!item) return; // probably a group item if(!item) { // probably a group item
log && log.error('no item by bubble', bubble);
const unmountIfFound = !sequential || this.bubbleGroups.getLastGroup() !== item.group; return;
if(unmountIfFound) { } else if(item.mid === mid) {
const groupIndex = this.bubbleGroups.groups.indexOf(item.group); log && log.warn('wow what', item, mid);
this.bubbleGroups.removeAndUnmountBubble(bubble); return;
if(!item.group.items.length) { // group has collapsed, next message can have higher mid so have to reposition them too }
const nearbyGroups = this.bubbleGroups.groups.slice(0, groupIndex + 1);
for(let length = nearbyGroups.length, i = length - 2; i >= 0; --i) {
const group = nearbyGroups[i];
const groupItems = group.items;
const nextGroup = nearbyGroups[i + 1];
const nextGroupItems = nextGroup.items;
let _break = false, moved = false;
for(let j = groupItems.length - 1; j >= 0; --j) {
const groupItem = groupItems[j];
const possibleIndex = this.bubbleGroups.findIndexForItemInItems(groupItem, nextGroupItems);
if(possibleIndex === -1) {
_break = true;
break;
}
this.bubbleGroups.removeAndUnmountBubble(groupItem.bubble); const group = item.group;
this.bubbleGroups.addItemToGroup(groupItem, nextGroup); const newItem = this.bubbleGroups.createItem(bubble, message);
moved = true; // newItem.mid = item.mid;
} const _items = this.bubbleGroups.itemsArr.slice();
indexOfAndSplice(_items, item);
const foundItem = this.bubbleGroups.findGroupSiblingByItem(newItem, _items);
if(group === foundItem?.group/* && false */) {
log && log('item has correct position', item);
this.bubbleGroups.changeBubbleMid(bubble, mid);
return;
}
if(moved) { // return;
nextGroup.mount();
}
if(_break) { // await fastRafPromise();
break; // if(this.bubbles[mid] !== bubble) return;
}
}
}
const {group} = this.setBubblePosition(bubble, message, unmountIfFound); // const groupIndex = this.bubbleGroups.groups.indexOf(group);
group.mount(); this.bubbleGroups.removeAndUnmountBubble(bubble);
// if(!group.items.length) { // group has collapsed, next message can have higher mid so have to reposition them too
// log && log('group has collapsed', item);
// const siblingGroups = this.bubbleGroups.groups.slice(0, groupIndex + 1);
// for(let length = siblingGroups.length, i = length - 2; i >= 0; --i) {
// const siblingGroup = siblingGroups[i];
// const siblingItems = siblingGroup.items;
// const nextGroup = siblingGroups[i + 1];
// const nextItems = nextGroup.items;
// let _break = false, moved = false;
// for(let j = siblingItems.length - 1; j >= 0; --j) {
// const siblingItem = siblingItems[j];
// const foundItem = this.bubbleGroups.findGroupSiblingByItem(siblingItem, nextItems);
// if(!foundItem) {
// _break = true;
// break;
// }
// log('will move item', siblingItem, nextGroup);
// this.bubbleGroups.removeAndUnmountBubble(siblingItem.bubble);
// this.bubbleGroups.addItemToGroup(siblingItem, nextGroup);
// moved = true;
// }
// if(moved) {
// nextGroup.mount();
// }
// if(_break) {
// break;
// }
// }
// }
if(this.scrollingToBubble) { const {groups} = this.groupBubbles([{bubble, message}]);
this.scrollToEnd(); this.bubbleGroups.mountUnmountGroups(groups);
}
}/* else { if(this.scrollingToBubble) {
this.bubbleGroups.changeBubbleMid(bubble, mid); this.scrollToEnd();
} */ }
log && log('end');
// this.bubbleGroups.findIncorrentPositions();
}); });
this.listenerSetter.add(rootScope)('dialog_flush', ({peerId}) => { this.listenerSetter.add(rootScope)('dialog_flush', ({peerId}) => {
@ -358,6 +403,7 @@ export default class ChatBubbles {
if(_bubble) { if(_bubble) {
const bubble = bubbles[tempId]; const bubble = bubbles[tempId];
bubbles[mid] = bubble; bubbles[mid] = bubble;
bubble.dataset.mid = '' + mid;
delete bubbles[tempId]; delete bubbles[tempId];
fastRaf(() => { fastRaf(() => {
@ -367,8 +413,6 @@ export default class ChatBubbles {
bubble.classList.add((this.peerId === rootScope.myId && this.chat.type !== 'scheduled') || !this.unreadOut.has(mid) ? 'is-read' : 'is-sent'); bubble.classList.add((this.peerId === rootScope.myId && this.chat.type !== 'scheduled') || !this.unreadOut.has(mid) ? 'is-read' : 'is-sent');
} }
}); });
bubble.dataset.mid = '' + mid;
} }
if(this.unreadOut.has(tempId)) { if(this.unreadOut.has(tempId)) {
@ -385,6 +429,10 @@ export default class ChatBubbles {
} }
} }
if(!_bubble) {
return;
}
let messages: (Message.message | Message.messageService)[], tempIds: number[]; let messages: (Message.message | Message.messageService)[], tempIds: number[];
const groupedId = (message as Message.message).grouped_id; const groupedId = (message as Message.message).grouped_id;
if(groupedId) { if(groupedId) {
@ -1261,7 +1309,7 @@ export default class ChatBubbles {
if(this.readPromise) return; if(this.readPromise) return;
const middleware = this.getMiddleware(); const middleware = this.getMiddleware();
this.readPromise = idleController.idle.focusPromise.then(async() => { this.readPromise = idleController.getFocusPromise().then(async() => {
if(!middleware()) return; if(!middleware()) return;
let maxId = Math.max(...Array.from(this.unreadedSeen)); let maxId = Math.max(...Array.from(this.unreadedSeen));
@ -2112,8 +2160,17 @@ export default class ChatBubbles {
} : undefined } : undefined
}; };
} }
private renderNewMessagesByIds(mids: number[], scrolledDown?: boolean) {
const promise = this._renderNewMessagesByIds(mids, scrolledDown);
this.renderNewPromises.add(promise);
promise.catch(noop).finally(() => {
this.renderNewPromises.delete(promise);
});
return promise;
}
public async renderNewMessagesByIds(mids: number[], scrolledDown?: boolean) { private async _renderNewMessagesByIds(mids: number[], scrolledDown?: boolean) {
if(!this.scrollable.loadedAll.bottom) { // seems search active or sliced if(!this.scrollable.loadedAll.bottom) { // seems search active or sliced
//this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids); //this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids);
const setPeerPromise = this.chat.setPeerPromise; const setPeerPromise = this.chat.setPeerPromise;
@ -2131,7 +2188,7 @@ export default class ChatBubbles {
if(this.chat.threadId) { if(this.chat.threadId) {
mids = await filterAsync(mids, async(mid) => { mids = await filterAsync(mids, async(mid) => {
const message = await this.chat.getMessage(mid); const message = await this.chat.getMessage(mid);
const replyTo = message.reply_to as MessageReplyHeader; const replyTo = message?.reply_to as MessageReplyHeader;
return replyTo && (replyTo.reply_to_top_id || replyTo.reply_to_msg_id) === this.chat.threadId; return replyTo && (replyTo.reply_to_top_id || replyTo.reply_to_msg_id) === this.chat.threadId;
}); });
} }
@ -2179,6 +2236,8 @@ export default class ChatBubbles {
}, 10); */ }, 10); */
}); });
} }
return promise;
} }
public getLastBubble() { public getLastBubble() {
@ -2194,6 +2253,10 @@ export default class ChatBubbles {
) { ) {
const bubble = findUpClassName(element, 'bubble'); const bubble = findUpClassName(element, 'bubble');
if(!element.parentElement) {
this.log.error('element is not connected', bubble);
}
let fallbackToElementStartWhenCentering: HTMLElement; let fallbackToElementStartWhenCentering: HTMLElement;
// * if it's a start, then scroll to start of the group // * if it's a start, then scroll to start of the group
if(bubble && position !== 'end') { if(bubble && position !== 'end') {
@ -2454,6 +2517,7 @@ export default class ChatBubbles {
this.unreadOut.clear(); this.unreadOut.clear();
this.needUpdate.length = 0; this.needUpdate.length = 0;
this.lazyLoadQueue.clear(); this.lazyLoadQueue.clear();
this.renderNewPromises.clear();
// clear messages // clear messages
if(bubblesToo) { if(bubblesToo) {
@ -2501,6 +2565,7 @@ export default class ChatBubbles {
this.renderingMessages.clear(); this.renderingMessages.clear();
this.bubblesToEject.clear(); this.bubblesToEject.clear();
this.bubblesToReplace.clear();
// this.reactions.clear(); // this.reactions.clear();
@ -3012,7 +3077,9 @@ export default class ChatBubbles {
const possibleError = PEER_CHANGED_ERROR; const possibleError = PEER_CHANGED_ERROR;
const m = middlewarePromise(middleware, possibleError); const m = middlewarePromise(middleware, possibleError);
const promise = this.messagesQueuePromise = m(pause(0)).then(async() => { const processQueue = async(): Promise<void> => {
log('start');
const renderQueue = this.messagesQueue.slice(); const renderQueue = this.messagesQueue.slice();
this.messagesQueue.length = 0; this.messagesQueue.length = 0;
@ -3025,15 +3092,27 @@ export default class ChatBubbles {
return promise; return promise;
}); });
const loadQueue = (await m(Promise.all(renderQueuePromises))).filter(Boolean); let loadQueue = await m(Promise.all(renderQueuePromises));
log.warn('messages rendered'); const filterQueue = (queue: typeof loadQueue) => {
return queue.filter((details) => {
// message can be deleted during rendering
return details && this.bubbles[details.bubble.dataset.mid] === details.bubble;
});
};
const groups: Set<BubbleGroups['groups'][0]> = new Set(); loadQueue = filterQueue(loadQueue);
const promises = loadQueue.reduce((acc, details) => {
if(!details || this.bubbles[details.message.mid] !== details.bubble) {
return acc; // message was deleted during rendering
}
log('messages rendered');
const reverse = loadQueue[0]?.reverse;
const {groups, avatarPromises} = this.groupBubbles(loadQueue.filter((details) => details.updatePosition));
// if(groups.length > 2 && loadQueue.length === 1) {
// debugger;
// }
const promises = loadQueue.reduce((acc, details) => {
const perf = performance.now(); const perf = performance.now();
const promises = details.promises.slice(); const promises = details.promises.slice();
@ -3046,32 +3125,34 @@ export default class ChatBubbles {
log.groupEnd(); log.groupEnd();
}); });
if(details.updatePosition) { // if(details.updatePosition) {
const res = this.setBubblePosition(details.bubble, details.message, false); // if(res) {
if(res) { // groups.add(res.group);
groups.add(res.group); // if(details.needAvatar) {
if(details.needAvatar) { // details.promises.push(res.group.createAvatar(details.message));
details.promises.push(res.group.createAvatar(details.message)); // }
} // }
} // }
}
acc.push(...details.promises); acc.push(...details.promises);
return acc; return acc;
}, [] as Promise<any>[]); }, [] as Promise<any>[]);
promises.push(...avatarPromises);
// promises.push(pause(200)); // promises.push(pause(200));
// * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался // * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался
// * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера // * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера
// promises.push(getHeavyAnimationPromise()); // promises.push(getHeavyAnimationPromise());
log.warn('media promises to call', promises, loadQueue, this.isHeavyAnimationInProgress); log('media promises to call', promises, loadQueue, this.isHeavyAnimationInProgress);
await m(Promise.all([...promises, this.setUnreadDelimiter()])); // не нашёл места лучше await m(Promise.all([...promises, this.setUnreadDelimiter()])); // не нашёл места лучше
await m(fastRafPromise()); // have to be the last await m(fastRafPromise()); // have to be the last
log.warn('media promises end'); log('media promises end');
loadQueue = filterQueue(loadQueue);
this.prepareToSaveScroll(loadQueue[0] && loadQueue[0].reverse); const restoreScroll = this.prepareToSaveScroll(reverse);
// if(this.messagesQueueOnRender) { // if(this.messagesQueueOnRender) {
// this.messagesQueueOnRender(); // this.messagesQueueOnRender();
// } // }
@ -3081,35 +3162,58 @@ export default class ChatBubbles {
} }
this.ejectBubbles(); this.ejectBubbles();
for(const bubble of this.bubblesToReplace) {
if(!loadQueue.find((details) => details.bubble === bubble)) {
continue;
}
const item = this.bubbleGroups.getItemByBubble(bubble);
item.mounted = false;
if(!groups.includes(item.group)) {
groups.push(item.group);
}
this.bubblesToReplace.delete(bubble);
}
if(this.chat.selection.isSelecting) { if(this.chat.selection.isSelecting) {
loadQueue.forEach(({message, bubble}) => { loadQueue.forEach(({bubble}) => {
if(this.bubbles[message.mid] !== bubble) { // maybe message was deleted during rendering
return;
}
this.chat.selection.toggleElementCheckbox(bubble, true); this.chat.selection.toggleElementCheckbox(bubble, true);
}); });
} }
// const sortedGroups = Array.from(groups).sort((a, b) => a.firstTimestamp - b.firstTimestamp); loadQueue.forEach(({message, bubble, updatePosition}) => {
// for(const group of sortedGroups) { if(message.pFlags.local && updatePosition) {
for(const group of groups) { this.chatInner[(message as Message.message).pFlags.sponsored ? 'append' : 'prepend'](bubble);
group.mount(); return;
} }
});
this.bubbleGroups.mountUnmountGroups(groups);
// this.bubbleGroups.findIncorrentPositions();
if(this.updatePlaceholderPosition) { if(this.updatePlaceholderPosition) {
this.updatePlaceholderPosition(); this.updatePlaceholderPosition();
} }
if(restoreScroll) {
restoreScroll();
}
// this.setStickyDateManually(); // this.setStickyDateManually();
}).finally(() => {
if(this.messagesQueue.length) {
log('have new messages to render');
return processQueue();
} else {
log('end');
}
};
log('setting pause');
const promise = this.messagesQueuePromise = m(pause(0)).then(processQueue).finally(() => {
if(this.messagesQueuePromise === promise) { if(this.messagesQueuePromise === promise) {
this.messagesQueuePromise = null; this.messagesQueuePromise = null;
if(this.messagesQueue.length) {
this.setMessagesQueuePromise();
}
} }
}); });
@ -3121,16 +3225,35 @@ export default class ChatBubbles {
bubble.remove(); bubble.remove();
// this.bubbleGroups.removeAndUnmountBubble(bubble); // this.bubbleGroups.removeAndUnmountBubble(bubble);
} }
this.bubblesToEject.clear();
} }
public setBubblePosition(bubble: HTMLElement, message: Message.message | Message.messageService, unmountIfFound: boolean) { public groupBubbles(items: Array<{
if(message.pFlags.local) { // Awaited<ReturnType<ChatBubbles['safeRenderMessage']>> &
this.chatInner[(message as Message.message).pFlags.sponsored ? 'append' : 'prepend'](bubble); bubble: HTMLElement,
return; message: Message.message | Message.messageService
} }/* & {
unmountIfFound?: boolean
} */>) {
items.forEach(({bubble, message}) => {
this.bubbleGroups.prepareForGrouping(bubble, message);
});
const result = this.bubbleGroups.addBubble(bubble, message, unmountIfFound); const groups = this.bubbleGroups.groupUngrouped();
return result;
const avatarPromises = Array.from(groups).map((group) => {
if(group.avatar) return;
const firstItem = group.firstItem;
if(this.chat.isAvatarNeeded(firstItem.message)) {
return group.createAvatar(firstItem.message);
}
}).filter(Boolean);
return {
groups: [...groups],
avatarPromises
};
} }
public getMiddleware(additionalCallback?: () => boolean) { public getMiddleware(additionalCallback?: () => boolean) {
@ -3156,6 +3279,9 @@ export default class ChatBubbles {
// const groupedId = (message as Message.message).grouped_id; // const groupedId = (message as Message.message).grouped_id;
const newBubble = document.createElement('div'); const newBubble = document.createElement('div');
newBubble.dataset.mid = '' + message.mid;
newBubble.dataset.peerId = '' + message.peerId;
newBubble.dataset.timestamp = '' + message.date;
// const bubbleNew: Bubble = this.bubblesNew[message.mid] ??= { // const bubbleNew: Bubble = this.bubblesNew[message.mid] ??= {
// bubble: newBubble, // bubble: newBubble,
@ -3167,7 +3293,10 @@ export default class ChatBubbles {
if(bubble) { if(bubble) {
this.skippedMids.delete(message.mid); this.skippedMids.delete(message.mid);
this.bubblesToEject.add(bubble); this.bubblesToEject.add(bubble);
this.bubblesToReplace.delete(bubble);
this.bubblesToReplace.add(newBubble);
this.bubbleGroups.changeBubbleByBubble(bubble, newBubble); this.bubbleGroups.changeBubbleByBubble(bubble, newBubble);
} }
@ -3215,6 +3344,8 @@ export default class ChatBubbles {
// return; // return;
// } // }
// await pause(1000);
const isMessage = message._ === 'message'; const isMessage = message._ === 'message';
const groupedId = isMessage && message.grouped_id; const groupedId = isMessage && message.grouped_id;
let albumMids: number[], reactionsMessage: Message.message; let albumMids: number[], reactionsMessage: Message.message;
@ -3232,96 +3363,42 @@ export default class ChatBubbles {
reactionsMessage = groupedId ? await this.managers.appMessagesManager.getGroupsFirstMessage(message) : message; reactionsMessage = groupedId ? await this.managers.appMessagesManager.getGroupsFirstMessage(message) : message;
} }
const peerId = this.peerId;
// * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right) // * can't use 'message.pFlags.out' here because this check will be used to define side of message (left-right)
const our = message.fromId === rootScope.myId || (message.pFlags.out && await this.managers.appPeersManager.isMegagroup(peerId)); const our = this.chat.isOurMessage(message);
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
messageDiv.classList.add('message'); messageDiv.classList.add('message');
//messageDiv.innerText = message.message;
let bubbleContainer: HTMLDivElement; let bubbleContainer: HTMLDivElement;
let contentWrapper: HTMLElement; let contentWrapper: HTMLElement;
// bubble contentWrapper = document.createElement('div');
// if(!bubble) { contentWrapper.classList.add('bubble-content-wrapper');
contentWrapper = document.createElement('div');
contentWrapper.classList.add('bubble-content-wrapper'); bubbleContainer = document.createElement('div');
bubbleContainer.classList.add('bubble-content');
bubbleContainer = document.createElement('div');
bubbleContainer.classList.add('bubble-content'); bubble.classList.add('bubble');
contentWrapper.append(bubbleContainer);
// bubble = document.createElement('div'); bubble.append(contentWrapper);
bubble.classList.add('bubble');
contentWrapper.appendChild(bubbleContainer);
bubble.appendChild(contentWrapper);
if(!our && !message.pFlags.out && this.observer) {
//this.log('not our message', message, message.pFlags.unread);
const isUnread = message.pFlags.unread ||
isMentionUnread(message)/* ||
(this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid) */;
if(isUnread) {
this.observer.observe(bubble, this.unreadedObserverCallback);
this.unreaded.set(bubble, message.mid);
}
}
// } else {
// const save = ['is-highlighted', 'is-group-first', 'is-group-last'];
// const wasClassNames = bubble.className.split(' ');
// const classNames = ['bubble'].concat(save.filter((c) => wasClassNames.includes(c)));
// bubble.className = classNames.join(' ');
// contentWrapper = bubble.lastElementChild as HTMLElement;
// if(!contentWrapper.classList.contains('bubble-content-wrapper')) {
// contentWrapper = bubble.querySelector('.bubble-content-wrapper');
// }
// bubbleContainer = contentWrapper.firstElementChild as HTMLDivElement;
// bubbleContainer.innerHTML = '';
// bubbleContainer.style.cssText = '';
// contentWrapper.innerHTML = '';
// contentWrapper.appendChild(bubbleContainer);
// //bubbleContainer.style.marginBottom = '';
// const transitionDelay = contentWrapper.style.transitionDelay;
// contentWrapper.style.cssText = '';
// contentWrapper.style.transitionDelay = transitionDelay;
// if(bubble === this.firstUnreadBubble) {
// bubble.classList.add('is-first-unread');
// }
// // * Нужно очистить прошлую информацию, полезно если удалить последний элемент из альбома в ПОСЛЕДНЕМ БАББЛЕ ГРУППЫ (видно по аватару)
// const originalMid = +bubble.dataset.mid;
// const sameMid = +message.mid === originalMid;
// /* if(updatePosition) {
// bubble.remove(); // * for positionElementByIndex
// } */
// ! WARNING
// if(!sameMid) {
// delete this.bubbles[originalMid];
// this.skippedMids.delete(originalMid);
// }
// //bubble.innerHTML = '';
// }
// ! reset due to album edit or delete item if(!our && !message.pFlags.out && this.observer) {
// this.bubbles[+message.mid] = bubble; //this.log('not our message', message, message.pFlags.unread);
bubble.dataset.mid = '' + message.mid; const isUnread = message.pFlags.unread ||
bubble.dataset.peerId = '' + message.peerId; isMentionUnread(message)/* ||
bubble.dataset.timestamp = '' + message.date; (this.historyStorage.readMaxId !== undefined && this.historyStorage.readMaxId < message.mid) */;
if(isUnread) {
this.observer.observe(bubble, this.unreadedObserverCallback);
this.unreaded.set(bubble, message.mid);
}
}
const loadPromises: Promise<any>[] = []; const loadPromises: Promise<any>[] = [];
const ret = { const ret = {
bubble, bubble,
promises: loadPromises, promises: loadPromises,
message, message,
reverse, reverse
needAvatar: undefined as boolean,
isOut: undefined as boolean
}; };
if(message._ === 'messageService' && (!message.action || !SERVICE_AS_REGULAR.has(message.action._))) { if(message._ === 'messageService' && (!message.action || !SERVICE_AS_REGULAR.has(message.action._))) {
@ -3339,10 +3416,15 @@ export default class ChatBubbles {
const s = document.createElement('div'); const s = document.createElement('div');
s.classList.add('service-msg'); s.classList.add('service-msg');
if(action) { if(action) {
let promise: Promise<any>;
if(action._ === 'messageActionChannelMigrateFrom') { if(action._ === 'messageActionChannelMigrateFrom') {
s.append(i18n('ChatMigration.From', [new PeerTitle({peerId: action.chat_id.toPeerId(true)}).element])); const peerTitle = new PeerTitle();
promise = peerTitle.update({peerId: action.chat_id.toPeerId(true)});
s.append(i18n('ChatMigration.From', [peerTitle.element]));
} else if(action._ === 'messageActionChatMigrateTo') { } else if(action._ === 'messageActionChatMigrateTo') {
s.append(i18n('ChatMigration.To', [new PeerTitle({peerId: action.channel_id.toPeerId(true)}).element])); const peerTitle = new PeerTitle();
promise = peerTitle.update({peerId: action.channel_id.toPeerId(true)});
s.append(i18n('ChatMigration.To', [peerTitle.element]));
} else { } else {
s.append(await wrapMessageActionTextNew(message)); s.append(await wrapMessageActionTextNew(message));
} }
@ -3524,6 +3606,7 @@ export default class ChatBubbles {
promise.then((peerId) => { promise.then((peerId) => {
const threadId = this.peerId === peerId ? this.chat.threadId : undefined; const threadId = this.peerId === peerId ? this.chat.threadId : undefined;
this.chat.appImManager.setInnerPeer({peerId});
this.managers.appInlineBotsManager.switchInlineQuery(peerId, threadId, botId, button.query); this.managers.appInlineBotsManager.switchInlineQuery(peerId, threadId, botId, button.query);
}); });
}); });
@ -3605,7 +3688,7 @@ export default class ChatBubbles {
const fwdFrom = isMessage && message.fwd_from; const fwdFrom = isMessage && message.fwd_from;
const fwdFromId = isMessage && message.fwdFromId; const fwdFromId = isMessage && message.fwdFromId;
const isOut = ret.isOut = our && (!fwdFrom || this.peerId !== rootScope.myId); const isOut = this.chat.isOutMessage(message);
let nameContainer: HTMLElement = bubbleContainer; let nameContainer: HTMLElement = bubbleContainer;
const canHideNameIfMedia = !message.viaBotId && (message.fromId === rootScope.myId || !message.pFlags.out); const canHideNameIfMedia = !message.viaBotId && (message.fromId === rootScope.myId || !message.pFlags.out);
@ -3674,9 +3757,9 @@ export default class ChatBubbles {
case 'messageMediaWebPage': { case 'messageMediaWebPage': {
processingWebPage = true; processingWebPage = true;
let webpage: WebPage = messageMedia.webpage; let webPage: WebPage = messageMedia.webpage;
////////this.log('messageMediaWebPage', webpage); ////////this.log('messageMediaWebPage', webpage);
if(webpage._ !== 'webPage') { if(webPage._ !== 'webPage') {
break; break;
} }
@ -3689,8 +3772,8 @@ export default class ChatBubbles {
quote.classList.add('quote'); quote.classList.add('quote');
let previewResizer: HTMLDivElement, preview: HTMLDivElement; let previewResizer: HTMLDivElement, preview: HTMLDivElement;
const photo: Photo.photo = webpage.photo as any; const photo: Photo.photo = webPage.photo as any;
if(photo || webpage.document) { if(photo || webPage.document) {
previewResizer = document.createElement('div'); previewResizer = document.createElement('div');
previewResizer.classList.add('preview-resizer'); previewResizer.classList.add('preview-resizer');
preview = document.createElement('div'); preview = document.createElement('div');
@ -3701,7 +3784,7 @@ export default class ChatBubbles {
let quoteTextDiv = document.createElement('div'); let quoteTextDiv = document.createElement('div');
quoteTextDiv.classList.add('quote-text'); quoteTextDiv.classList.add('quote-text');
const doc = webpage.document as MyDocument; const doc = webPage.document as MyDocument;
if(doc) { if(doc) {
if(doc.type === 'gif' || doc.type === 'video' || doc.type === 'round') { if(doc.type === 'gif' || doc.type === 'video' || doc.type === 'round') {
//if(doc.size <= 20e6) { //if(doc.size <= 20e6) {
@ -3754,19 +3837,19 @@ export default class ChatBubbles {
} }
let t: HTMLElement; let t: HTMLElement;
if(webpage.site_name) { if(webPage.site_name) {
const html = wrapRichText(webpage.url); const html = wrapRichText(webPage.url);
const a: HTMLAnchorElement = htmlToDocumentFragment(html).firstElementChild as any; const a: HTMLAnchorElement = htmlToDocumentFragment(html).firstElementChild as any;
a.classList.add('webpage-name'); a.classList.add('webpage-name');
const strong = document.createElement('strong'); const strong = document.createElement('strong');
setInnerHTML(strong, wrapEmojiText(webpage.site_name)); setInnerHTML(strong, wrapEmojiText(webPage.site_name));
a.textContent = ''; a.textContent = '';
a.append(strong); a.append(strong);
quoteTextDiv.append(a); quoteTextDiv.append(a);
t = a; t = a;
} }
const title = wrapWebPageTitle(webpage); const title = wrapWebPageTitle(webPage);
if(title.textContent) { if(title.textContent) {
let titleDiv = document.createElement('div'); let titleDiv = document.createElement('div');
titleDiv.classList.add('title'); titleDiv.classList.add('title');
@ -3777,7 +3860,7 @@ export default class ChatBubbles {
t = titleDiv; t = titleDiv;
} }
const description = wrapWebPageDescription(webpage); const description = wrapWebPageDescription(webPage);
if(description.textContent) { if(description.textContent) {
let textDiv = document.createElement('div'); let textDiv = document.createElement('div');
textDiv.classList.add('text'); textDiv.classList.add('text');
@ -4098,7 +4181,7 @@ export default class ChatBubbles {
let savedFrom = ''; let savedFrom = '';
// const needName = ((peerId.isAnyChat() && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId; // const needName = ((peerId.isAnyChat() && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId;
const needName = (message.fromId !== rootScope.myId && await this.managers.appPeersManager.isAnyGroup(peerId)) || message.viaBotId || (message as Message.message).pFlags.sponsored; const needName = (message.fromId !== rootScope.myId && this.chat.isAnyGroup) || message.viaBotId || (message as Message.message).pFlags.sponsored;
if(needName || fwdFrom || message.reply_to_mid) { // chat if(needName || fwdFrom || message.reply_to_mid) { // chat
let title: HTMLElement | DocumentFragment; let title: HTMLElement | DocumentFragment;
let titleVia: typeof title; let titleVia: typeof title;
@ -4200,8 +4283,6 @@ export default class ChatBubbles {
nameDiv.classList.add('name'); nameDiv.classList.add('name');
nameContainer.append(nameDiv); nameContainer.append(nameDiv);
} }
ret.needAvatar = this.chat.isAnyGroup && !isOut;
} else { } else {
bubble.classList.add('hide-name'); bubble.classList.add('hide-name');
} }
@ -4315,7 +4396,8 @@ export default class ChatBubbles {
return; return;
} }
this.log.warn('onRender'); const log = this.log.bindPrefix('prepareToSaveScroll');
log('save');
const scrollSaver = this.createScrollSaver(reverse); const scrollSaver = this.createScrollSaver(reverse);
scrollSaver.save(); // * let's save scroll position by point before the slicing, not after scrollSaver.save(); // * let's save scroll position by point before the slicing, not after
@ -4328,16 +4410,17 @@ export default class ChatBubbles {
// const saved = scrollSaver.getSaved(); // const saved = scrollSaver.getSaved();
// const hadScroll = saved.scrollHeight !== saved.clientHeight; // const hadScroll = saved.scrollHeight !== saved.clientHeight;
(this.messagesQueuePromise || Promise.resolve()).then(() => { return () => {
log('restore');
// scrollSaver.restore(_history.length === 1 && !reverse ? false : true); // scrollSaver.restore(_history.length === 1 && !reverse ? false : true);
scrollSaver.restore(reverse); scrollSaver.restore(reverse);
this.onRenderScrollSet(scrollSaver.getSaved()); this.onRenderScrollSet(scrollSaver.getSaved());
}); };
} }
public async performHistoryResult(historyResult: HistoryResult | {history: (Message.message | Message.messageService | number)[]}, reverse: boolean) { public async performHistoryResult(historyResult: HistoryResult | {history: (Message.message | Message.messageService | number)[]}, reverse: boolean) {
const log = this.log.bindPrefix('perform-' + (Math.random() * 1000 | 0)); const log = false ? this.log.bindPrefix('perform-' + (Math.random() * 1000 | 0)) : undefined;
log.warn('start', this.chatInner.parentElement); log && log('start', this.chatInner.parentElement);
let history = historyResult.history; let history = historyResult.history;
history = history.slice(); // need history = history.slice(); // need
@ -4393,9 +4476,10 @@ export default class ChatBubbles {
await Promise.all(setLoadedPromises); await Promise.all(setLoadedPromises);
// ! it is important to insert bubbles to group reversed way // ! it is important to insert bubbles to group reversed way
const length = history.length, promises: Promise<any>[] = []; // const length = history.length, promises: Promise<any>[] = [];
if(reverse) for(let i = 0; i < length; ++i) promises.push(cb(messages[i])); // if(reverse) for(let i = 0; i < length; ++i) promises.push(cb(messages[i]));
else for(let i = length - 1; i >= 0; --i) promises.push(cb(messages[i])); // else for(let i = length - 1; i >= 0; --i) promises.push(cb(messages[i]));
const promises = messages.map(cb);
// cannot combine them into one promise // cannot combine them into one promise
await Promise.all(promises); await Promise.all(promises);
@ -4409,7 +4493,7 @@ export default class ChatBubbles {
} }
} }
log.warn('performHistoryResult end'); log && log('performHistoryResult end');
} }
private onRenderScrollSet(state?: {scrollHeight: number, clientHeight: number}) { private onRenderScrollSet(state?: {scrollHeight: number, clientHeight: number}) {
@ -4823,9 +4907,9 @@ export default class ChatBubbles {
}; };
} else { } else {
callback = () => { callback = () => {
rootScope.dispatchEvent('history_focus', { this.chat.appImManager.setInnerPeer({
peerId, peerId,
mid, lastMsgId: mid,
startParam startParam
}); });
}; };
@ -5268,7 +5352,7 @@ export default class ChatBubbles {
// * filter last album, because we don't know is it the last item // * filter last album, because we don't know is it the last item
for(let i = additionMsgIds.length - 1; i >= 0; --i) { for(let i = additionMsgIds.length - 1; i >= 0; --i) {
const message = await this.chat.getMessage(additionMsgIds[i]); const message = await this.chat.getMessage(additionMsgIds[i]);
if((message as Message.message).grouped_id) additionMsgIds.splice(i, 1); if((message as Message.message)?.grouped_id) additionMsgIds.splice(i, 1);
else break; else break;
} }

22
src/components/chat/chat.ts

@ -34,6 +34,7 @@ import AppSharedMediaTab from "../sidebarRight/tabs/sharedMedia";
import noop from "../../helpers/noop"; import noop from "../../helpers/noop";
import middlewarePromise from "../../helpers/middlewarePromise"; import middlewarePromise from "../../helpers/middlewarePromise";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { Message } from "../../layer";
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled'; export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
@ -80,6 +81,7 @@ export default class Chat extends EventListenerBase<{
// public renderDarkPattern: () => Promise<void>; // public renderDarkPattern: () => Promise<void>;
public isAnyGroup: boolean; public isAnyGroup: boolean;
public isMegagroup: boolean;
constructor( constructor(
public appImManager: AppImManager, public appImManager: AppImManager,
@ -401,16 +403,18 @@ export default class Chat extends EventListenerBase<{
searchTab.close(); searchTab.close();
} }
const [noForwards, isRestricted, isAnyGroup] = await m(Promise.all([ const [noForwards, isRestricted, isAnyGroup, _, isMegagroup] = await m(Promise.all([
this.managers.appPeersManager.noForwards(peerId), this.managers.appPeersManager.noForwards(peerId),
this.managers.appPeersManager.isRestricted(peerId), this.managers.appPeersManager.isRestricted(peerId),
this._isAnyGroup(peerId), this._isAnyGroup(peerId),
this.setAutoDownloadMedia() this.setAutoDownloadMedia(),
this.managers.appPeersManager.isMegagroup(peerId)
])); ]));
this.noForwards = noForwards; this.noForwards = noForwards;
this.isRestricted = isRestricted; this.isRestricted = isRestricted;
this.isAnyGroup = isAnyGroup; this.isAnyGroup = isAnyGroup;
this.isMegagroup = isMegagroup;
this.container.classList.toggle('no-forwards', this.noForwards); this.container.classList.toggle('no-forwards', this.noForwards);
@ -588,4 +592,18 @@ export default class Chat extends EventListenerBase<{
sendAsPeerId: this.input.sendAsPeerId sendAsPeerId: this.input.sendAsPeerId
}; };
} }
public isOurMessage(message: Message.message | Message.messageService) {
return message.fromId === rootScope.myId || (message.pFlags.out && this.isMegagroup);
}
public isOutMessage(message: Message.message | Message.messageService) {
const fwdFrom = (message as Message.message).fwd_from;
const isOut = this.isOurMessage(message) && (!fwdFrom || this.peerId !== rootScope.myId);
return isOut;
}
public isAvatarNeeded(message: Message.message | Message.messageService) {
return this.isAnyGroup && !this.isOutMessage(message);
}
} }

1
src/components/chat/inlineHelper.ts

@ -245,6 +245,7 @@ export default class InlineHelper extends AutocompleteHelper {
const btnSwitchToPM = Button('btn-primary btn-secondary btn-primary-transparent primary'); const btnSwitchToPM = Button('btn-primary btn-secondary btn-primary-transparent primary');
setInnerHTML(btnSwitchToPM, wrapEmojiText(botResults.switch_pm.text)); setInnerHTML(btnSwitchToPM, wrapEmojiText(botResults.switch_pm.text));
attachClickEvent(btnSwitchToPM, (e) => { attachClickEvent(btnSwitchToPM, (e) => {
this.chat.appImManager.setInnerPeer({peerId});
this.managers.appInlineBotsManager.switchToPM(peerId, peer.id, botResults.switch_pm.start_param); this.managers.appInlineBotsManager.switchToPM(peerId, peer.id, botResults.switch_pm.start_param);
}); });
parent.append(btnSwitchToPM); parent.append(btnSwitchToPM);

11
src/components/chat/input.ts

@ -482,7 +482,16 @@ export default class ChatInput {
attachClickEvent(this.goMentionBtn, (e) => { attachClickEvent(this.goMentionBtn, (e) => {
cancelEvent(e); cancelEvent(e);
this.managers.appMessagesManager.goToNextMention(this.chat.peerId); const middleware = this.chat.bubbles.getMiddleware();
this.managers.appMessagesManager.goToNextMention(this.chat.peerId).then((mid) => {
if(!middleware()) {
return;
}
if(mid) {
this.chat.setMessageId(mid);
}
});
}, {listenerSetter: this.listenerSetter}); }, {listenerSetter: this.listenerSetter});
this.btnScheduled = ButtonIcon('scheduled btn-scheduled float hide', {noRipple: true}); this.btnScheduled = ButtonIcon('scheduled btn-scheduled float hide', {noRipple: true});

6
src/components/chat/selection.ts

@ -38,6 +38,7 @@ import safeAssign from "../../helpers/object/safeAssign";
import { AppManagers } from "../../lib/appManagers/managers"; import { AppManagers } from "../../lib/appManagers/managers";
import { attachContextMenuListener } from "../../helpers/dom/attachContextMenuListener"; import { attachContextMenuListener } from "../../helpers/dom/attachContextMenuListener";
import filterUnique from "../../helpers/array/filterUnique"; import filterUnique from "../../helpers/array/filterUnique";
import appImManager from "../../lib/appManagers/appImManager";
const accumulateMapSet = (map: Map<any, Set<number>>) => { const accumulateMapSet = (map: Map<any, Set<number>>) => {
return [...map.values()].reduce((acc, v) => acc + v.size, 0); return [...map.values()].reduce((acc, v) => acc + v.size, 0);
@ -627,10 +628,7 @@ export class SearchSelection extends AppSelection {
const mid = [...this.selectedMids.get(peerId)][0]; const mid = [...this.selectedMids.get(peerId)][0];
this.cancelSelection(); this.cancelSelection();
rootScope.dispatchEvent('history_focus', { appImManager.setInnerPeer({peerId, lastMsgId: mid});
peerId,
mid
});
}, attachClickOptions); }, attachClickOptions);
this.selectionForwardBtn = ButtonIcon(`forward ${BASE_CLASS}-forward`); this.selectionForwardBtn = ButtonIcon(`forward ${BASE_CLASS}-forward`);

5
src/components/connectionStatus.ts

@ -18,6 +18,7 @@ import { ConnectionStatus } from "../lib/mtproto/connectionStatus";
import cancelEvent from "../helpers/dom/cancelEvent"; import cancelEvent from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
import { AppManagers } from "../lib/appManagers/managers"; import { AppManagers } from "../lib/appManagers/managers";
import singleInstance from "../lib/mtproto/singleInstance";
export default class ConnectionStatusComponent { export default class ConnectionStatusComponent {
public static CHANGE_STATE_DELAY = 1000; public static CHANGE_STATE_DELAY = 1000;
@ -145,6 +146,10 @@ export default class ConnectionStatusComponent {
} }
private setState = () => { private setState = () => {
if(singleInstance.deactivatedReason) {
return;
}
const timeout = ConnectionStatusComponent.CHANGE_STATE_DELAY; const timeout = ConnectionStatusComponent.CHANGE_STATE_DELAY;
if(this.connecting) { if(this.connecting) {
if(this.timedOut) { if(this.timedOut) {

5
src/components/groupCall/participants.ts

@ -19,6 +19,7 @@ import ScrollableLoader from "../../helpers/scrollableLoader";
import { GroupCallParticipant } from "../../layer"; import { GroupCallParticipant } from "../../layer";
import type { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import type { AppChatsManager } from "../../lib/appManagers/appChatsManager";
import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager"; import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
import appImManager from "../../lib/appManagers/appImManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import { AppManagers } from "../../lib/appManagers/managers"; import { AppManagers } from "../../lib/appManagers/managers";
import getPeerId from "../../lib/appManagers/utils/peers/getPeerId"; import getPeerId from "../../lib/appManagers/utils/peers/getPeerId";
@ -157,9 +158,7 @@ export class GroupCallParticipantContextMenu {
popup.hide(); popup.hide();
} }
rootScope.dispatchEvent('history_focus', { appImManager.setInnerPeer({peerId: this.targetPeerId});
peerId: this.targetPeerId
});
}; };
private toggleParticipantMuted = (muted: boolean) => { private toggleParticipantMuted = (muted: boolean) => {

8
src/components/popups/index.ts

@ -32,10 +32,10 @@ export type PopupButton = {
}; };
export type PopupOptions = Partial<{ export type PopupOptions = Partial<{
closable: true, closable: boolean,
overlayClosable: true, overlayClosable: boolean,
withConfirm: LangPackKey | true, withConfirm: LangPackKey | boolean,
body: true, body: boolean,
confirmShortcutIsSendShortcut: boolean, confirmShortcutIsSendShortcut: boolean,
withoutOverlay: boolean withoutOverlay: boolean
}>; }>;

5
src/components/popups/joinChatInvite.ts

@ -8,11 +8,10 @@ import PopupElement, { addCancelButton } from ".";
import setInnerHTML from "../../helpers/dom/setInnerHTML"; import setInnerHTML from "../../helpers/dom/setInnerHTML";
import numberThousandSplitter from "../../helpers/number/numberThousandSplitter"; import numberThousandSplitter from "../../helpers/number/numberThousandSplitter";
import { ChatInvite } from "../../layer"; import { ChatInvite } from "../../layer";
import { AppManagers } from "../../lib/appManagers/managers"; import appImManager from "../../lib/appManagers/appImManager";
import { i18n, _i18n } from "../../lib/langPack"; import { i18n, _i18n } from "../../lib/langPack";
import { NULL_PEER_ID } from "../../lib/mtproto/mtproto_config"; import { NULL_PEER_ID } from "../../lib/mtproto/mtproto_config";
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText"; import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
import rootScope from "../../lib/rootScope";
import AvatarElement from "../avatar"; import AvatarElement from "../avatar";
import putPhoto from "../putPhoto"; import putPhoto from "../putPhoto";
import { toastNew } from "../toast"; import { toastNew } from "../toast";
@ -31,7 +30,7 @@ export default class PopupJoinChatInvite extends PopupElement {
this.managers.appChatsManager.importChatInvite(hash) this.managers.appChatsManager.importChatInvite(hash)
.then((chatId) => { .then((chatId) => {
const peerId = chatId.toPeerId(true); const peerId = chatId.toPeerId(true);
rootScope.dispatchEvent('history_focus', {peerId}); appImManager.setInnerPeer({peerId});
}, (error) => { }, (error) => {
if(error.type === 'INVITE_REQUEST_SENT') { if(error.type === 'INVITE_REQUEST_SENT') {
toastNew({langPackKey: 'RequestToJoinSent'}); toastNew({langPackKey: 'RequestToJoinSent'});

4
src/components/putPhoto.ts

@ -94,7 +94,7 @@ export async function putAvatar(
} }
const renderPromise = loadPromise const renderPromise = loadPromise
.then((url) => renderImageFromUrlPromise(img, url, !cached)) .then((url) => renderImageFromUrlPromise(img, url/* , !cached */))
.then(callback); .then(callback);
await (renderThumbPromise || renderPromise); await (renderThumbPromise || renderPromise);
@ -166,7 +166,7 @@ export default async function putPhoto(
if(avatarAvailable/* && false */) { if(avatarAvailable/* && false */) {
const promise = putAvatar(div, peerId, photo, size, undefined, onlyThumb); const promise = putAvatar(div, peerId, photo, size, undefined, onlyThumb);
recordPromise(promise, 'putAvatar-' + peerId); // recordPromise(promise, 'putAvatar-' + peerId);
return promise; return promise;
} }
} }

4
src/components/sidebarLeft/index.ts

@ -282,6 +282,10 @@ export class AppSidebarLeft extends SidebarSlider {
// this.updateBtn.prepend(weaveContainer); // this.updateBtn.prepend(weaveContainer);
attachClickEvent(this.updateBtn, () => { attachClickEvent(this.updateBtn, () => {
if(this.updateBtn.classList.contains('is-hidden')) {
return;
}
location.reload(); location.reload();
}); });

3
src/components/sidebarLeft/tabs/newChannel.ts

@ -12,6 +12,7 @@ import AvatarEdit from "../../avatarEdit";
import AppAddMembersTab from "./addMembers"; import AppAddMembersTab from "./addMembers";
import { _i18n } from "../../../lib/langPack"; import { _i18n } from "../../../lib/langPack";
import ButtonCorner from "../../buttonCorner"; import ButtonCorner from "../../buttonCorner";
import appImManager from "../../../lib/appManagers/appImManager";
export default class AppNewChannelTab extends SliderSuperTab { export default class AppNewChannelTab extends SliderSuperTab {
private uploadAvatar: () => Promise<InputFile> = null; private uploadAvatar: () => Promise<InputFile> = null;
@ -74,6 +75,8 @@ export default class AppNewChannelTab extends SliderSuperTab {
this.managers.appChatsManager.editPhoto(channelId, inputFile); this.managers.appChatsManager.editPhoto(channelId, inputFile);
}); });
} }
appImManager.setInnerPeer({peerId: channelId.toPeerId(true)});
appSidebarLeft.removeTabFromHistory(this); appSidebarLeft.removeTabFromHistory(this);
this.slider.createTab(AppAddMembersTab).open({ this.slider.createTab(AppAddMembersTab).open({

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

@ -13,6 +13,7 @@ import AvatarEdit from "../../avatarEdit";
import I18n from "../../../lib/langPack"; import I18n from "../../../lib/langPack";
import ButtonCorner from "../../buttonCorner"; import ButtonCorner from "../../buttonCorner";
import getUserStatusString from "../../wrappers/getUserStatusString"; import getUserStatusString from "../../wrappers/getUserStatusString";
import appImManager from "../../../lib/appManagers/appImManager";
interface OpenStreetMapInterface { interface OpenStreetMapInterface {
place_id?: number; place_id?: number;
@ -79,9 +80,10 @@ export default class AppNewGroupTab extends SliderSuperTab {
this.nextBtn.addEventListener('click', () => { this.nextBtn.addEventListener('click', () => {
const title = this.groupNameInputField.value; const title = this.groupNameInputField.value;
if(this.isGeoChat){ let promise: Promise<ChatId>;
if(this.isGeoChat) {
if(!this.userLocationAddress || !this.userLocationCoords) return; if(!this.userLocationAddress || !this.userLocationCoords) return;
this.managers.appChatsManager.createChannel({ promise = this.managers.appChatsManager.createChannel({
title, title,
about: '', about: '',
geo_point: { geo_point: {
@ -97,26 +99,35 @@ export default class AppNewGroupTab extends SliderSuperTab {
}); });
} }
if(this.peerIds.length){ if(this.peerIds.length) {
this.managers.appChatsManager.inviteToChannel(chatId, this.peerIds); this.managers.appChatsManager.inviteToChannel(chatId, this.peerIds);
} }
appSidebarLeft.removeTabFromHistory(this); return chatId;
appSidebarLeft.selectTab(0);
}); });
} else { } else {
this.nextBtn.disabled = true; this.nextBtn.disabled = true;
this.managers.appChatsManager.createChat(title, this.peerIds.map((peerId) => peerId.toUserId())).then((chatId) => { promise = this.managers.appChatsManager.createChat(title, this.peerIds.map((peerId) => peerId.toUserId())).then((chatId) => {
if(this.uploadAvatar) { if(this.uploadAvatar) {
this.uploadAvatar().then((inputFile) => { this.uploadAvatar().then((inputFile) => {
this.managers.appChatsManager.editPhoto(chatId, inputFile); this.managers.appChatsManager.editPhoto(chatId, inputFile);
}); });
} }
appSidebarLeft.removeTabFromHistory(this); return chatId;
appSidebarLeft.selectTab(0);
}); });
} }
if(!promise) {
return;
}
promise.then((chatId) => {
appSidebarLeft.removeTabFromHistory(this);
appSidebarLeft.selectTab(0);
appImManager.setInnerPeer({peerId: chatId.toPeerId(true)});
});
}); });
const chatsSection = new SettingSection({ const chatsSection = new SettingSection({

10
src/components/wrappers/photo.ts

@ -257,13 +257,13 @@ export default async function wrapPhoto({photo, message, container, boxWidth, bo
loadPromises.push(loadThumbPromise); loadPromises.push(loadThumbPromise);
} }
const perf = performance.now(); // const perf = performance.now();
await loadThumbPromise; await loadThumbPromise;
const elapsedTime = performance.now() - perf; // const elapsedTime = performance.now() - perf;
if(elapsedTime > 4) { // if(elapsedTime > 4) {
console.log('wrapping photo thumb time', elapsedTime, photo, size); // console.log('wrapping photo thumb time', elapsedTime, photo, size);
} // }
return { return {
loadPromises: { loadPromises: {

2
src/config/app.ts

@ -19,7 +19,7 @@ const App = {
version: process.env.VERSION, version: process.env.VERSION,
versionFull: process.env.VERSION_FULL, versionFull: process.env.VERSION_FULL,
build: +process.env.BUILD, build: +process.env.BUILD,
langPackVersion: '0.4.0', langPackVersion: '0.4.1',
langPack: 'macos', langPack: 'macos',
langPackCode: 'en', langPackCode: 'en',
domains: [MAIN_DOMAIN] as string[], domains: [MAIN_DOMAIN] as string[],

24
src/config/databases/state.ts

@ -9,7 +9,7 @@ import type { IDBIndex } from '../../lib/idb';
const DATABASE_STATE: Database<'session' | 'stickerSets' | 'users' | 'chats' | 'messages' | 'dialogs'> = { const DATABASE_STATE: Database<'session' | 'stickerSets' | 'users' | 'chats' | 'messages' | 'dialogs'> = {
name: 'tweb', name: 'tweb',
version: 11, version: 7,
stores: [{ stores: [{
name: 'session' name: 'session'
}, { }, {
@ -20,18 +20,18 @@ const DATABASE_STATE: Database<'session' | 'stickerSets' | 'users' | 'chats' | '
name: 'chats' name: 'chats'
}, { }, {
name: 'dialogs', name: 'dialogs',
indexes: [ // indexes: [
...(new Array(20 + 2).fill(0)).map((_, idx) => { // ...(new Array(20 + 2).fill(0)).map((_, idx) => {
const name = `index_${idx}`; // const name = `index_${idx}`;
const index: IDBIndex = { // const index: IDBIndex = {
indexName: name, // indexName: name,
keyPath: name, // keyPath: name,
objectParameters: {} // objectParameters: {}
}; // };
return index // return index
}) // })
] // ]
}, { }, {
name: 'messages' name: 'messages'
}] }]

2
src/environment/sharedWorkerSupport.ts

@ -1,3 +1,3 @@
const IS_SHARED_WORKER_SUPPORTED = typeof(SharedWorker) !== 'undefined' && false; const IS_SHARED_WORKER_SUPPORTED = typeof(SharedWorker) !== 'undefined'/* && false */;
export default IS_SHARED_WORKER_SUPPORTED; export default IS_SHARED_WORKER_SUPPORTED;

15
src/helpers/array/partition.ts

@ -0,0 +1,15 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
export default function partition<T>(arr: T[], callback: (item: T, idx: number, arr: T[]) => boolean) {
const good: T[] = [], bad: T[] = [];
for(let i = 0, length = arr.length; i < length; ++i) {
const item = arr[i];
(callback(item, i, arr) ? good : bad).push(item);
}
return [good, bad];
}

32
src/helpers/idleController.ts

@ -12,16 +12,18 @@ const FOCUS_EVENT_NAME = IS_TOUCH_SUPPORTED ? 'touchstart' : 'mousemove';
export class IdleController extends EventListenerBase<{ export class IdleController extends EventListenerBase<{
change: (idle: boolean) => void change: (idle: boolean) => void
}> { }> {
public idle = { private _isIdle: boolean;
isIDLE: true,
deactivated: false, private focusPromise: Promise<void>;
focusPromise: Promise.resolve(), private focusResolve: () => void;
focusResolve: () => {}
};
constructor() { constructor() {
super(); super();
this._isIdle = true;
this.focusPromise = Promise.resolve();
this.focusResolve = () => {};
window.addEventListener('blur', () => { window.addEventListener('blur', () => {
this.isIdle = true; this.isIdle = true;
@ -37,21 +39,29 @@ export class IdleController extends EventListenerBase<{
this.addEventListener('change', (idle) => { this.addEventListener('change', (idle) => {
if(idle) { if(idle) {
this.idle.focusPromise = new Promise((resolve) => { this.focusPromise = new Promise((resolve) => {
this.idle.focusResolve = resolve; this.focusResolve = resolve;
}); });
} else { } else {
this.idle.focusResolve(); this.focusResolve();
} }
}); });
} }
public getFocusPromise() {
return this.focusPromise;
}
public get isIdle() {
return this._isIdle;
}
public set isIdle(value: boolean) { public set isIdle(value: boolean) {
if(this.idle.isIDLE === value) { if(this._isIdle === value) {
return; return;
} }
this.idle.isIDLE = value; this._isIdle = value;
this.dispatchEvent('change', value); this.dispatchEvent('change', value);
} }
} }

24
src/helpers/listenMessagePort.ts

@ -7,15 +7,21 @@
import ctx from "../environment/ctx"; import ctx from "../environment/ctx";
import SuperMessagePort from "../lib/mtproto/superMessagePort"; import SuperMessagePort from "../lib/mtproto/superMessagePort";
export default function listenMessagePort(messagePort: SuperMessagePort<any, any, any>, onConnect?: (source: MessageEventSource) => void) { export default function listenMessagePort(
if(typeof SharedWorkerGlobalScope !== 'undefined') { messagePort: SuperMessagePort<any, any, any>,
(self as any as SharedWorkerGlobalScope).addEventListener('connect', (e) => { onConnect?: (source: MessageEventSource) => void,
const source = e.source; onDisconnect?: (source: MessageEventSource) => void
messagePort.attachPort(source); ) {
onConnect && onConnect(source); const attachPort = (s: any) => {
}); messagePort.attachPort(s);
onConnect && onConnect(s);
};
onDisconnect && messagePort.setOnPortDisconnect(onDisconnect);
if(typeof(SharedWorkerGlobalScope) !== 'undefined') {
(ctx as any as SharedWorkerGlobalScope).addEventListener('connect', (e) => attachPort(e.source));
} else { } else {
messagePort.attachPort(ctx); attachPort(ctx);
onConnect && onConnect(ctx);
} }
} }

1
src/helpers/scrollSaver.ts

@ -134,6 +134,7 @@ export default class ScrollSaver {
const newRect = this.anchor.getBoundingClientRect(); const newRect = this.anchor.getBoundingClientRect();
const diff = newRect.bottom - rect.bottom; const diff = newRect.bottom - rect.bottom;
this.setScrollTop(scrollTop + diff, useReflow); this.setScrollTop(scrollTop + diff, useReflow);
// if(diff) debugger;
// console.warn('scroll restore', rect, diff, newRect); // console.warn('scroll restore', rect, diff, newRect);
} }

8
src/helpers/toggleStorages.ts

@ -9,10 +9,10 @@ import AppStorage from "../lib/storage";
import sessionStorage from "../lib/sessionStorage"; import sessionStorage from "../lib/sessionStorage";
import noop from "./noop"; import noop from "./noop";
export default function toggleStorages(enabled: boolean) { export default function toggleStorages(enabled: boolean, clearWrite: boolean) {
return Promise.all([ return Promise.all([
AppStorage.toggleStorage(enabled), AppStorage.toggleStorage(enabled, clearWrite),
CacheStorageController.toggleStorage(enabled), CacheStorageController.toggleStorage(enabled, clearWrite),
sessionStorage.toggleStorage(enabled) sessionStorage.toggleStorage(enabled, clearWrite)
]).then(noop, noop); ]).then(noop, noop);
} }

4
src/index.ts

@ -100,14 +100,14 @@ document.addEventListener('DOMContentLoaded', async() => {
}; };
let tabId: number; let tabId: number;
rootScope.addEventListener('im_tab_change', (id) => { (window as any).onImTabChange = (id: number) => {
const wasTabId = tabId !== undefined; const wasTabId = tabId !== undefined;
tabId = id; tabId = id;
if(wasTabId || tabId === 1) { if(wasTabId || tabId === 1) {
toggleResizeMode(); toggleResizeMode();
} }
}); };
overlayCounter.addEventListener('change', () => { overlayCounter.addEventListener('change', () => {
toggleResizeMode(); toggleResizeMode();

2
src/lang.ts

@ -55,6 +55,8 @@ const lang = {
}, },
"Deactivated.Title": "Too many tabs...", "Deactivated.Title": "Too many tabs...",
"Deactivated.Subtitle": "Telegram supports only one active tab with the app.\nClick anywhere to continue using this tab.", "Deactivated.Subtitle": "Telegram supports only one active tab with the app.\nClick anywhere to continue using this tab.",
"Deactivated.Version.Title": "WebK has updated...",
"Deactivated.Version.Subtitle": "Another tab is running a newer version of Telegram.\nClick anywhere to reload this tab.",
// "Drafts": { // "Drafts": {
// "one_value": "%d draft", // "one_value": "%d draft",
// "other_value": "%d drafts", // "other_value": "%d drafts",

230
src/layer.d.ts vendored

@ -498,8 +498,6 @@ export namespace User {
apply_min_photo?: true, apply_min_photo?: true,
fake?: true, fake?: true,
bot_attach_menu?: true, bot_attach_menu?: true,
premium?: true,
attach_menu_enabled?: true,
}>, }>,
id: string | number, id: string | number,
access_hash?: string | number, access_hash?: string | number,
@ -1121,10 +1119,8 @@ export namespace MessageAction {
export type messageActionPaymentSent = { export type messageActionPaymentSent = {
_: 'messageActionPaymentSent', _: 'messageActionPaymentSent',
flags?: number,
currency: string, currency: string,
total_amount: string | number, total_amount: string | number
invoice_slug?: string
}; };
export type messageActionPhoneCall = { export type messageActionPhoneCall = {
@ -1994,7 +1990,7 @@ export namespace MessagesFilter {
/** /**
* @link https://core.telegram.org/type/Update * @link https://core.telegram.org/type/Update
*/ */
export type Update = Update.updateNewMessage | Update.updateMessageID | Update.updateDeleteMessages | Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChatParticipants | Update.updateUserStatus | Update.updateUserName | Update.updateUserPhoto | Update.updateNewEncryptedMessage | Update.updateEncryptedChatTyping | Update.updateEncryption | Update.updateEncryptedMessagesRead | Update.updateChatParticipantAdd | Update.updateChatParticipantDelete | Update.updateDcOptions | Update.updateNotifySettings | Update.updateServiceNotification | Update.updatePrivacy | Update.updateUserPhone | Update.updateReadHistoryInbox | Update.updateReadHistoryOutbox | Update.updateWebPage | Update.updateReadMessagesContents | Update.updateChannelTooLong | Update.updateChannel | Update.updateNewChannelMessage | Update.updateReadChannelInbox | Update.updateDeleteChannelMessages | Update.updateChannelMessageViews | Update.updateChatParticipantAdmin | Update.updateNewStickerSet | Update.updateStickerSetsOrder | Update.updateStickerSets | Update.updateSavedGifs | Update.updateBotInlineQuery | Update.updateBotInlineSend | Update.updateEditChannelMessage | Update.updateBotCallbackQuery | Update.updateEditMessage | Update.updateInlineBotCallbackQuery | Update.updateReadChannelOutbox | Update.updateDraftMessage | Update.updateReadFeaturedStickers | Update.updateRecentStickers | Update.updateConfig | Update.updatePtsChanged | Update.updateChannelWebPage | Update.updateDialogPinned | Update.updatePinnedDialogs | Update.updateBotWebhookJSON | Update.updateBotWebhookJSONQuery | Update.updateBotShippingQuery | Update.updateBotPrecheckoutQuery | Update.updatePhoneCall | Update.updateLangPackTooLong | Update.updateLangPack | Update.updateFavedStickers | Update.updateChannelReadMessagesContents | Update.updateContactsReset | Update.updateChannelAvailableMessages | Update.updateDialogUnreadMark | Update.updateMessagePoll | Update.updateChatDefaultBannedRights | Update.updateFolderPeers | Update.updatePeerSettings | Update.updatePeerLocated | Update.updateNewScheduledMessage | Update.updateDeleteScheduledMessages | Update.updateTheme | Update.updateGeoLiveViewed | Update.updateLoginToken | Update.updateMessagePollVote | Update.updateDialogFilter | Update.updateDialogFilterOrder | Update.updateDialogFilters | Update.updatePhoneCallSignalingData | Update.updateChannelMessageForwards | Update.updateReadChannelDiscussionInbox | Update.updateReadChannelDiscussionOutbox | Update.updatePeerBlocked | Update.updateChannelUserTyping | Update.updatePinnedMessages | Update.updatePinnedChannelMessages | Update.updateChat | Update.updateGroupCallParticipants | Update.updateGroupCall | Update.updatePeerHistoryTTL | Update.updateChatParticipant | Update.updateChannelParticipant | Update.updateBotStopped | Update.updateGroupCallConnection | Update.updateBotCommands | Update.updatePendingJoinRequests | Update.updateBotChatInviteRequester | Update.updateMessageReactions | Update.updateAttachMenuBots | Update.updateWebViewResultSent | Update.updateBotMenuButton | Update.updateSavedRingtones | Update.updateTranscribeAudio | Update.updateNewDiscussionMessage | Update.updateDeleteDiscussionMessages | Update.updateChannelReload; export type Update = Update.updateNewMessage | Update.updateMessageID | Update.updateDeleteMessages | Update.updateUserTyping | Update.updateChatUserTyping | Update.updateChatParticipants | Update.updateUserStatus | Update.updateUserName | Update.updateUserPhoto | Update.updateNewEncryptedMessage | Update.updateEncryptedChatTyping | Update.updateEncryption | Update.updateEncryptedMessagesRead | Update.updateChatParticipantAdd | Update.updateChatParticipantDelete | Update.updateDcOptions | Update.updateNotifySettings | Update.updateServiceNotification | Update.updatePrivacy | Update.updateUserPhone | Update.updateReadHistoryInbox | Update.updateReadHistoryOutbox | Update.updateWebPage | Update.updateReadMessagesContents | Update.updateChannelTooLong | Update.updateChannel | Update.updateNewChannelMessage | Update.updateReadChannelInbox | Update.updateDeleteChannelMessages | Update.updateChannelMessageViews | Update.updateChatParticipantAdmin | Update.updateNewStickerSet | Update.updateStickerSetsOrder | Update.updateStickerSets | Update.updateSavedGifs | Update.updateBotInlineQuery | Update.updateBotInlineSend | Update.updateEditChannelMessage | Update.updateBotCallbackQuery | Update.updateEditMessage | Update.updateInlineBotCallbackQuery | Update.updateReadChannelOutbox | Update.updateDraftMessage | Update.updateReadFeaturedStickers | Update.updateRecentStickers | Update.updateConfig | Update.updatePtsChanged | Update.updateChannelWebPage | Update.updateDialogPinned | Update.updatePinnedDialogs | Update.updateBotWebhookJSON | Update.updateBotWebhookJSONQuery | Update.updateBotShippingQuery | Update.updateBotPrecheckoutQuery | Update.updatePhoneCall | Update.updateLangPackTooLong | Update.updateLangPack | Update.updateFavedStickers | Update.updateChannelReadMessagesContents | Update.updateContactsReset | Update.updateChannelAvailableMessages | Update.updateDialogUnreadMark | Update.updateMessagePoll | Update.updateChatDefaultBannedRights | Update.updateFolderPeers | Update.updatePeerSettings | Update.updatePeerLocated | Update.updateNewScheduledMessage | Update.updateDeleteScheduledMessages | Update.updateTheme | Update.updateGeoLiveViewed | Update.updateLoginToken | Update.updateMessagePollVote | Update.updateDialogFilter | Update.updateDialogFilterOrder | Update.updateDialogFilters | Update.updatePhoneCallSignalingData | Update.updateChannelMessageForwards | Update.updateReadChannelDiscussionInbox | Update.updateReadChannelDiscussionOutbox | Update.updatePeerBlocked | Update.updateChannelUserTyping | Update.updatePinnedMessages | Update.updatePinnedChannelMessages | Update.updateChat | Update.updateGroupCallParticipants | Update.updateGroupCall | Update.updatePeerHistoryTTL | Update.updateChatParticipant | Update.updateChannelParticipant | Update.updateBotStopped | Update.updateGroupCallConnection | Update.updateBotCommands | Update.updatePendingJoinRequests | Update.updateBotChatInviteRequester | Update.updateMessageReactions | Update.updateAttachMenuBots | Update.updateWebViewResultSent | Update.updateBotMenuButton | Update.updateSavedRingtones | Update.updateNewDiscussionMessage | Update.updateDeleteDiscussionMessages | Update.updateChannelReload;
export namespace Update { export namespace Update {
export type updateNewMessage = { export type updateNewMessage = {
@ -2702,16 +2698,6 @@ export namespace Update {
_: 'updateSavedRingtones' _: 'updateSavedRingtones'
}; };
export type updateTranscribeAudio = {
_: 'updateTranscribeAudio',
flags?: number,
pFlags?: Partial<{
final?: true,
}>,
transcription_id: string | number,
text: string
};
export type updateNewDiscussionMessage = { export type updateNewDiscussionMessage = {
_: 'updateNewDiscussionMessage', _: 'updateNewDiscussionMessage',
message?: Message message?: Message
@ -2950,7 +2936,6 @@ export namespace DcOption {
tcpo_only?: true, tcpo_only?: true,
cdn?: true, cdn?: true,
static?: true, static?: true,
this_port_only?: true,
}>, }>,
id: number, id: number,
ip_address: string, ip_address: string,
@ -2976,7 +2961,6 @@ export namespace Config {
revoke_pm_inbox?: true, revoke_pm_inbox?: true,
blocked_mode?: true, blocked_mode?: true,
pfs_enabled?: true, pfs_enabled?: true,
force_try_ipv6?: true,
}>, }>,
date: number, date: number,
expires: number, expires: number,
@ -3157,7 +3141,7 @@ export namespace EncryptedFile {
_: 'encryptedFile', _: 'encryptedFile',
id: string | number, id: string | number,
access_hash: string | number, access_hash: string | number,
size: string | number, size: number,
dc_id: number, dc_id: number,
key_fingerprint: number key_fingerprint: number
}; };
@ -3944,7 +3928,7 @@ export namespace ReceivedNotifyMessage {
/** /**
* @link https://core.telegram.org/type/ExportedChatInvite * @link https://core.telegram.org/type/ExportedChatInvite
*/ */
export type ExportedChatInvite = ExportedChatInvite.chatInviteExported | ExportedChatInvite.chatInvitePublicJoinRequests; export type ExportedChatInvite = ExportedChatInvite.chatInviteExported;
export namespace ExportedChatInvite { export namespace ExportedChatInvite {
export type chatInviteExported = { export type chatInviteExported = {
@ -3965,10 +3949,6 @@ export namespace ExportedChatInvite {
requested?: number, requested?: number,
title?: string title?: string
}; };
export type chatInvitePublicJoinRequests = {
_: 'chatInvitePublicJoinRequests'
};
} }
/** /**
@ -4110,13 +4090,10 @@ export type BotInfo = BotInfo.botInfo;
export namespace BotInfo { export namespace BotInfo {
export type botInfo = { export type botInfo = {
_: 'botInfo', _: 'botInfo',
flags?: number, user_id: string | number,
user_id?: string | number, description: string,
description?: string, commands: Array<BotCommand>,
description_photo?: Photo, menu_button: BotMenuButton
description_document?: Document,
commands?: Array<BotCommand>,
menu_button?: BotMenuButton
}; };
} }
@ -4295,7 +4272,7 @@ export namespace ReplyMarkup {
/** /**
* @link https://core.telegram.org/type/MessageEntity * @link https://core.telegram.org/type/MessageEntity
*/ */
export type MessageEntity = MessageEntity.messageEntityUnknown | MessageEntity.messageEntityMention | MessageEntity.messageEntityHashtag | MessageEntity.messageEntityBotCommand | MessageEntity.messageEntityUrl | MessageEntity.messageEntityEmail | MessageEntity.messageEntityBold | MessageEntity.messageEntityItalic | MessageEntity.messageEntityCode | MessageEntity.messageEntityPre | MessageEntity.messageEntityTextUrl | MessageEntity.messageEntityMentionName | MessageEntity.inputMessageEntityMentionName | MessageEntity.messageEntityPhone | MessageEntity.messageEntityCashtag | MessageEntity.messageEntityUnderline | MessageEntity.messageEntityStrike | MessageEntity.messageEntityBlockquote | MessageEntity.messageEntityBankCard | MessageEntity.messageEntitySpoiler | MessageEntity.messageEntityAnimatedEmoji | MessageEntity.messageEntityEmoji | MessageEntity.messageEntityHighlight | MessageEntity.messageEntityLinebreak | MessageEntity.messageEntityCaret; export type MessageEntity = MessageEntity.messageEntityUnknown | MessageEntity.messageEntityMention | MessageEntity.messageEntityHashtag | MessageEntity.messageEntityBotCommand | MessageEntity.messageEntityUrl | MessageEntity.messageEntityEmail | MessageEntity.messageEntityBold | MessageEntity.messageEntityItalic | MessageEntity.messageEntityCode | MessageEntity.messageEntityPre | MessageEntity.messageEntityTextUrl | MessageEntity.messageEntityMentionName | MessageEntity.inputMessageEntityMentionName | MessageEntity.messageEntityPhone | MessageEntity.messageEntityCashtag | MessageEntity.messageEntityUnderline | MessageEntity.messageEntityStrike | MessageEntity.messageEntityBlockquote | MessageEntity.messageEntityBankCard | MessageEntity.messageEntitySpoiler | MessageEntity.messageEntityEmoji | MessageEntity.messageEntityHighlight | MessageEntity.messageEntityLinebreak | MessageEntity.messageEntityCaret;
export namespace MessageEntity { export namespace MessageEntity {
export type messageEntityUnknown = { export type messageEntityUnknown = {
@ -4423,12 +4400,6 @@ export namespace MessageEntity {
length: number length: number
}; };
export type messageEntityAnimatedEmoji = {
_: 'messageEntityAnimatedEmoji',
offset: number,
length: number
};
export type messageEntityEmoji = { export type messageEntityEmoji = {
_: 'messageEntityEmoji', _: 'messageEntityEmoji',
offset?: number, offset?: number,
@ -5991,9 +5962,6 @@ export namespace PaymentsPaymentForm {
}>, }>,
form_id: string | number, form_id: string | number,
bot_id: string | number, bot_id: string | number,
title: string,
description: string,
photo?: WebDocument,
invoice: Invoice, invoice: Invoice,
provider_id: string | number, provider_id: string | number,
url: string, url: string,
@ -6262,10 +6230,6 @@ export type PhoneConnection = PhoneConnection.phoneConnection | PhoneConnection.
export namespace PhoneConnection { export namespace PhoneConnection {
export type phoneConnection = { export type phoneConnection = {
_: 'phoneConnection', _: 'phoneConnection',
flags?: number,
pFlags?: Partial<{
tcp?: true,
}>,
id: string | number, id: string | number,
ip: string, ip: string,
ipv6: string, ipv6: string,
@ -6913,7 +6877,7 @@ export type FileHash = FileHash.fileHash;
export namespace FileHash { export namespace FileHash {
export type fileHash = { export type fileHash = {
_: 'fileHash', _: 'fileHash',
offset: string | number, offset: number,
limit: number, limit: number,
hash: Uint8Array hash: Uint8Array
}; };
@ -6986,7 +6950,7 @@ export namespace SecureFile {
_: 'secureFile', _: 'secureFile',
id: string | number, id: string | number,
access_hash: string | number, access_hash: string | number,
size: string | number, size: number,
dc_id: number, dc_id: number,
date: number, date: number,
file_hash: Uint8Array, file_hash: Uint8Array,
@ -8365,7 +8329,7 @@ export namespace PaymentsBankCardData {
/** /**
* @link https://core.telegram.org/type/DialogFilter * @link https://core.telegram.org/type/DialogFilter
*/ */
export type DialogFilter = DialogFilter.dialogFilter | DialogFilter.dialogFilterDefault; export type DialogFilter = DialogFilter.dialogFilter;
export namespace DialogFilter { export namespace DialogFilter {
export type dialogFilter = { export type dialogFilter = {
@ -8391,10 +8355,6 @@ export namespace DialogFilter {
peerId?: PeerId, peerId?: PeerId,
folder_id?: number folder_id?: number
}; };
export type dialogFilterDefault = {
_: 'dialogFilterDefault'
};
} }
/** /**
@ -9235,9 +9195,6 @@ export namespace SponsoredMessage {
export type sponsoredMessage = { export type sponsoredMessage = {
_: 'sponsoredMessage', _: 'sponsoredMessage',
flags?: number, flags?: number,
pFlags?: Partial<{
recommended?: true,
}>,
random_id: Uint8Array, random_id: Uint8Array,
from_id?: Peer, from_id?: Peer,
chat_invite?: ChatInvite, chat_invite?: ChatInvite,
@ -9446,7 +9403,6 @@ export namespace AvailableReaction {
flags?: number, flags?: number,
pFlags?: Partial<{ pFlags?: Partial<{
inactive?: true, inactive?: true,
premium?: true,
}>, }>,
reaction: string, reaction: string,
title: string, title: string,
@ -9592,7 +9548,6 @@ export namespace AttachMenuBot {
}>, }>,
bot_id: string | number, bot_id: string | number,
short_name: string, short_name: string,
peer_types: Array<AttachMenuPeerType>,
icons: Array<AttachMenuBotIcon> icons: Array<AttachMenuBotIcon>
}; };
} }
@ -9746,80 +9701,6 @@ export namespace AccountSavedRingtone {
}; };
} }
/**
* @link https://core.telegram.org/type/AttachMenuPeerType
*/
export type AttachMenuPeerType = AttachMenuPeerType.attachMenuPeerTypeSameBotPM | AttachMenuPeerType.attachMenuPeerTypeBotPM | AttachMenuPeerType.attachMenuPeerTypePM | AttachMenuPeerType.attachMenuPeerTypeChat | AttachMenuPeerType.attachMenuPeerTypeBroadcast;
export namespace AttachMenuPeerType {
export type attachMenuPeerTypeSameBotPM = {
_: 'attachMenuPeerTypeSameBotPM'
};
export type attachMenuPeerTypeBotPM = {
_: 'attachMenuPeerTypeBotPM'
};
export type attachMenuPeerTypePM = {
_: 'attachMenuPeerTypePM'
};
export type attachMenuPeerTypeChat = {
_: 'attachMenuPeerTypeChat'
};
export type attachMenuPeerTypeBroadcast = {
_: 'attachMenuPeerTypeBroadcast'
};
}
/**
* @link https://core.telegram.org/type/InputInvoice
*/
export type InputInvoice = InputInvoice.inputInvoiceMessage | InputInvoice.inputInvoiceSlug;
export namespace InputInvoice {
export type inputInvoiceMessage = {
_: 'inputInvoiceMessage',
peer: InputPeer,
msg_id: number
};
export type inputInvoiceSlug = {
_: 'inputInvoiceSlug',
slug: string
};
}
/**
* @link https://core.telegram.org/type/payments.ExportedInvoice
*/
export type PaymentsExportedInvoice = PaymentsExportedInvoice.paymentsExportedInvoice;
export namespace PaymentsExportedInvoice {
export type paymentsExportedInvoice = {
_: 'payments.exportedInvoice',
url: string
};
}
/**
* @link https://core.telegram.org/type/messages.TranscribedAudio
*/
export type MessagesTranscribedAudio = MessagesTranscribedAudio.messagesTranscribedAudio;
export namespace MessagesTranscribedAudio {
export type messagesTranscribedAudio = {
_: 'messages.transcribedAudio',
flags?: number,
pFlags?: Partial<{
pending?: true,
}>,
transcription_id: string | number,
text: string
};
}
export interface ConstructorDeclMap { export interface ConstructorDeclMap {
'error': Error.error, 'error': Error.error,
'inputPeerEmpty': InputPeer.inputPeerEmpty, 'inputPeerEmpty': InputPeer.inputPeerEmpty,
@ -10803,19 +10684,6 @@ export interface ConstructorDeclMap {
'notificationSoundRingtone': NotificationSound.notificationSoundRingtone, 'notificationSoundRingtone': NotificationSound.notificationSoundRingtone,
'account.savedRingtone': AccountSavedRingtone.accountSavedRingtone, 'account.savedRingtone': AccountSavedRingtone.accountSavedRingtone,
'account.savedRingtoneConverted': AccountSavedRingtone.accountSavedRingtoneConverted, 'account.savedRingtoneConverted': AccountSavedRingtone.accountSavedRingtoneConverted,
'attachMenuPeerTypeSameBotPM': AttachMenuPeerType.attachMenuPeerTypeSameBotPM,
'attachMenuPeerTypeBotPM': AttachMenuPeerType.attachMenuPeerTypeBotPM,
'attachMenuPeerTypePM': AttachMenuPeerType.attachMenuPeerTypePM,
'attachMenuPeerTypeChat': AttachMenuPeerType.attachMenuPeerTypeChat,
'attachMenuPeerTypeBroadcast': AttachMenuPeerType.attachMenuPeerTypeBroadcast,
'chatInvitePublicJoinRequests': ExportedChatInvite.chatInvitePublicJoinRequests,
'inputInvoiceMessage': InputInvoice.inputInvoiceMessage,
'inputInvoiceSlug': InputInvoice.inputInvoiceSlug,
'payments.exportedInvoice': PaymentsExportedInvoice.paymentsExportedInvoice,
'updateTranscribeAudio': Update.updateTranscribeAudio,
'messages.transcribedAudio': MessagesTranscribedAudio.messagesTranscribedAudio,
'dialogFilterDefault': DialogFilter.dialogFilterDefault,
'messageEntityAnimatedEmoji': MessageEntity.messageEntityAnimatedEmoji,
'messageEntityEmoji': MessageEntity.messageEntityEmoji, 'messageEntityEmoji': MessageEntity.messageEntityEmoji,
'messageEntityHighlight': MessageEntity.messageEntityHighlight, 'messageEntityHighlight': MessageEntity.messageEntityHighlight,
'messageEntityLinebreak': MessageEntity.messageEntityLinebreak, 'messageEntityLinebreak': MessageEntity.messageEntityLinebreak,
@ -11201,7 +11069,7 @@ export type UploadGetFile = {
precise?: boolean, precise?: boolean,
cdn_supported?: boolean, cdn_supported?: boolean,
location: InputFileLocation, location: InputFileLocation,
offset: string | number, offset: number,
limit: number limit: number
}; };
@ -11626,7 +11494,7 @@ export type MessagesReorderStickerSets = {
export type MessagesGetDocumentByHash = { export type MessagesGetDocumentByHash = {
sha256: Uint8Array, sha256: Uint8Array,
size: string | number, size: number,
mime_type: string mime_type: string
}; };
@ -11925,7 +11793,8 @@ export type UploadGetWebFile = {
export type PaymentsGetPaymentForm = { export type PaymentsGetPaymentForm = {
flags?: number, flags?: number,
invoice: InputInvoice, peer: InputPeer,
msg_id: number,
theme_params?: DataJSON theme_params?: DataJSON
}; };
@ -11937,14 +11806,16 @@ export type PaymentsGetPaymentReceipt = {
export type PaymentsValidateRequestedInfo = { export type PaymentsValidateRequestedInfo = {
flags?: number, flags?: number,
save?: boolean, save?: boolean,
invoice: InputInvoice, peer: InputPeer,
msg_id: number,
info: PaymentRequestedInfo info: PaymentRequestedInfo
}; };
export type PaymentsSendPaymentForm = { export type PaymentsSendPaymentForm = {
flags?: number, flags?: number,
form_id: string | number, form_id: string | number,
invoice: InputInvoice, peer: InputPeer,
msg_id: number,
requested_info_id?: string, requested_info_id?: string,
shipping_option_id?: string, shipping_option_id?: string,
credentials: InputPaymentCredentials, credentials: InputPaymentCredentials,
@ -12066,7 +11937,7 @@ export type PhoneSaveCallDebug = {
export type UploadGetCdnFile = { export type UploadGetCdnFile = {
file_token: Uint8Array, file_token: Uint8Array,
offset: string | number, offset: number,
limit: number limit: number
}; };
@ -12119,7 +11990,7 @@ export type ChannelsGetAdminLog = {
export type UploadGetCdnFileHashes = { export type UploadGetCdnFileHashes = {
file_token: Uint8Array, file_token: Uint8Array,
offset: string | number offset: number
}; };
export type MessagesSendScreenshotNotification = { export type MessagesSendScreenshotNotification = {
@ -12225,7 +12096,7 @@ export type MessagesSearchStickerSets = {
export type UploadGetFileHashes = { export type UploadGetFileHashes = {
location: InputFileLocation, location: InputFileLocation,
offset: string | number offset: number
}; };
export type HelpGetTermsOfServiceUpdate = { export type HelpGetTermsOfServiceUpdate = {
@ -12312,7 +12183,7 @@ export type AccountInitTakeoutSession = {
message_megagroups?: boolean, message_megagroups?: boolean,
message_channels?: boolean, message_channels?: boolean,
files?: boolean, files?: boolean,
file_max_size?: string | number file_max_size?: number
}; };
export type AccountFinishTakeoutSession = { export type AccountFinishTakeoutSession = {
@ -13261,8 +13132,7 @@ export type MessagesRequestWebView = {
url?: string, url?: string,
start_param?: string, start_param?: string,
theme_params?: DataJSON, theme_params?: DataJSON,
reply_to_msg_id?: number, reply_to_msg_id?: number
send_as?: InputPeer
}; };
export type MessagesProlongWebView = { export type MessagesProlongWebView = {
@ -13271,8 +13141,7 @@ export type MessagesProlongWebView = {
peer: InputPeer, peer: InputPeer,
bot: InputUser, bot: InputUser,
query_id: string | number, query_id: string | number,
reply_to_msg_id?: number, reply_to_msg_id?: number
send_as?: InputPeer
}; };
export type MessagesRequestSimpleWebView = { export type MessagesRequestSimpleWebView = {
@ -13326,45 +13195,6 @@ export type BotsSetBotGroupDefaultAdminRights = {
admin_rights: ChatAdminRights admin_rights: ChatAdminRights
}; };
export type PhoneSaveCallLog = {
peer: InputPhoneCall,
file: InputFile
};
export type ChannelsToggleJoinToSend = {
channel: InputChannel,
enabled: boolean
};
export type ChannelsToggleJoinRequest = {
channel: InputChannel,
enabled: boolean
};
export type PaymentsExportInvoice = {
invoice_media: InputMedia
};
export type MessagesTranscribeAudio = {
peer: InputPeer,
msg_id: number
};
export type MessagesRateTranscribedAudio = {
peer: InputPeer,
msg_id: number,
transcription_id: string | number,
good: boolean
};
export type PaymentsAssignAppStoreTransaction = {
transaction_id: string
};
export type PaymentsAssignPlayMarketTransaction = {
purchase_token: string
};
export interface MethodDeclMap { export interface MethodDeclMap {
'invokeAfterMsg': {req: InvokeAfterMsg, res: any}, 'invokeAfterMsg': {req: InvokeAfterMsg, res: any},
'invokeAfterMsgs': {req: InvokeAfterMsgs, res: any}, 'invokeAfterMsgs': {req: InvokeAfterMsgs, res: any},
@ -13800,13 +13630,5 @@ export interface MethodDeclMap {
'account.uploadRingtone': {req: AccountUploadRingtone, res: Document}, 'account.uploadRingtone': {req: AccountUploadRingtone, res: Document},
'bots.setBotBroadcastDefaultAdminRights': {req: BotsSetBotBroadcastDefaultAdminRights, res: boolean}, 'bots.setBotBroadcastDefaultAdminRights': {req: BotsSetBotBroadcastDefaultAdminRights, res: boolean},
'bots.setBotGroupDefaultAdminRights': {req: BotsSetBotGroupDefaultAdminRights, res: boolean}, 'bots.setBotGroupDefaultAdminRights': {req: BotsSetBotGroupDefaultAdminRights, res: boolean},
'phone.saveCallLog': {req: PhoneSaveCallLog, res: boolean},
'channels.toggleJoinToSend': {req: ChannelsToggleJoinToSend, res: Updates},
'channels.toggleJoinRequest': {req: ChannelsToggleJoinRequest, res: Updates},
'payments.exportInvoice': {req: PaymentsExportInvoice, res: PaymentsExportedInvoice},
'messages.transcribeAudio': {req: MessagesTranscribeAudio, res: MessagesTranscribedAudio},
'messages.rateTranscribedAudio': {req: MessagesRateTranscribedAudio, res: boolean},
'payments.assignAppStoreTransaction': {req: PaymentsAssignAppStoreTransaction, res: Updates},
'payments.assignPlayMarketTransaction': {req: PaymentsAssignPlayMarketTransaction, res: Updates},
} }

4
src/lib/appManagers/appChatsManager.ts

@ -377,8 +377,6 @@ export class AppChatsManager extends AppManager {
this.apiUpdatesManager.processUpdateMessage(updates); this.apiUpdatesManager.processUpdateMessage(updates);
const channelId = (updates as any).chats[0].id; const channelId = (updates as any).chats[0].id;
this.rootScope.dispatchEvent('history_focus', {peerId: channelId.toPeerId(true)});
return channelId; return channelId;
}); });
} }
@ -401,8 +399,6 @@ export class AppChatsManager extends AppManager {
this.apiUpdatesManager.processUpdateMessage(updates); this.apiUpdatesManager.processUpdateMessage(updates);
const chatId = (updates as any as Updates.updates).chats[0].id; const chatId = (updates as any as Updates.updates).chats[0].id;
this.rootScope.dispatchEvent('history_focus', {peerId: chatId.toPeerId(true)});
return chatId; return chatId;
}); });
} }

2
src/lib/appManagers/appDialogsManager.ts

@ -1618,7 +1618,7 @@ export class AppDialogsManager {
} }
lastMessage = dialog.topMessage; lastMessage = dialog.topMessage;
if(!lastMessage) { if(!lastMessage || lastMessage.mid !== dialog.top_message) {
const promise = this.managers.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message); const promise = this.managers.appMessagesManager.getMessageByPeer(dialog.peerId, dialog.top_message);
lastMessage = await middleware(promise); lastMessage = await middleware(promise);
} }

13
src/lib/appManagers/appDownloadManager.ts

@ -6,7 +6,7 @@
import type { ApiFileManager, DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager"; import type { ApiFileManager, DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise"; import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
import { Document, InputFile } from "../../layer"; import { Document, InputFile, Photo, PhotoSize } from "../../layer";
import { getFileNameByLocation } from "../../helpers/fileName"; import { getFileNameByLocation } from "../../helpers/fileName";
import getFileNameForUpload from "../../helpers/getFileNameForUpload"; import getFileNameForUpload from "../../helpers/getFileNameForUpload";
import { AppManagers } from "./managers"; import { AppManagers } from "./managers";
@ -201,11 +201,18 @@ export class AppDownloadManager {
// } // }
public downloadToDisc(options: DownloadMediaOptions) { public downloadToDisc(options: DownloadMediaOptions) {
const media = options.media;
const isDocument = media._ === 'document';
if(!isDocument && !options.thumb) {
options.thumb = (media as Photo.photo).sizes.slice().pop() as PhotoSize.photoSize;
}
const promise = this.downloadMedia(options); const promise = this.downloadMedia(options);
promise.then((blob) => { promise.then((blob) => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const media = options.media; const downloadOptions = isDocument ?
const downloadOptions = media._ === 'document' ? getDocumentDownloadOptions(media) : getPhotoDownloadOptions(media as any, {} as any); getDocumentDownloadOptions(media) :
getPhotoDownloadOptions(media as any, options.thumb);
const fileName = (options.media as Document.document).file_name || getFileNameByLocation(downloadOptions.location); const fileName = (options.media as Document.document).file_name || getFileNameByLocation(downloadOptions.location);
createDownloadAnchor(url, fileName, () => { createDownloadAnchor(url, fileName, () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);

51
src/lib/appManagers/appImManager.ts

@ -35,7 +35,7 @@ import placeCaretAtEnd from '../../helpers/dom/placeCaretAtEnd';
import replaceContent from '../../helpers/dom/replaceContent'; import replaceContent from '../../helpers/dom/replaceContent';
import whichChild from '../../helpers/dom/whichChild'; import whichChild from '../../helpers/dom/whichChild';
import PopupElement from '../../components/popups'; import PopupElement from '../../components/popups';
import singleInstance from '../mtproto/singleInstance'; import singleInstance, { InstanceDeactivateReason, SingleInstance } from '../mtproto/singleInstance';
import PopupStickers from '../../components/popups/stickers'; import PopupStickers from '../../components/popups/stickers';
import PopupJoinChatInvite from '../../components/popups/joinChatInvite'; import PopupJoinChatInvite from '../../components/popups/joinChatInvite';
import { toast, toastNew } from '../../components/toast'; import { toast, toastNew } from '../../components/toast';
@ -88,6 +88,7 @@ import callsController from '../calls/callsController';
import getFilesFromEvent from '../../helpers/files/getFilesFromEvent'; import getFilesFromEvent from '../../helpers/files/getFilesFromEvent';
import apiManagerProxy from '../mtproto/mtprotoworker'; import apiManagerProxy from '../mtproto/mtprotoworker';
import wrapPeerTitle from '../../components/wrappers/peerTitle'; import wrapPeerTitle from '../../components/wrappers/peerTitle';
import appRuntimeManager from './appRuntimeManager';
export const CHAT_ANIMATION_GROUP = 'chat'; export const CHAT_ANIMATION_GROUP = 'chat';
@ -244,20 +245,6 @@ export class AppImManager extends EventListenerBase<{
}); });
}); });
rootScope.addEventListener('history_focus', (e) => {
let {peerId, threadId, mid, startParam} = e;
if(threadId) threadId = generateMessageId(threadId);
if(mid) mid = generateMessageId(mid); // because mid can come from notification, i.e. server message id
this.setInnerPeer({
peerId,
lastMsgId: mid,
type: threadId ? 'discussion' : undefined,
threadId,
startParam
});
});
this.addEventListener('peer_changing', (chat) => { this.addEventListener('peer_changing', (chat) => {
this.saveChatPosition(chat); this.saveChatPosition(chat);
}); });
@ -305,7 +292,8 @@ export class AppImManager extends EventListenerBase<{
} }
}); });
singleInstance.addEventListener('deactivated', () => { const onInstanceDeactivated = (reason: InstanceDeactivateReason) => {
const isUpdated = reason === 'version';
const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true}); const popup = new PopupElement('popup-instance-deactivated', undefined, {overlayClosable: true});
const c = document.createElement('div'); const c = document.createElement('div');
c.classList.add('instance-deactivated-container'); c.classList.add('instance-deactivated-container');
@ -313,17 +301,19 @@ export class AppImManager extends EventListenerBase<{
const header = document.createElement('div'); const header = document.createElement('div');
header.classList.add('header'); header.classList.add('header');
header.append(i18n('Deactivated.Title')); header.append(i18n(isUpdated ? 'Deactivated.Version.Title' : 'Deactivated.Title'));
const subtitle = document.createElement('div'); const subtitle = document.createElement('div');
subtitle.classList.add('subtitle'); subtitle.classList.add('subtitle');
subtitle.append(i18n('Deactivated.Subtitle')); subtitle.append(i18n(isUpdated ? 'Deactivated.Version.Subtitle' : 'Deactivated.Subtitle'));
c.append(header, subtitle); c.append(header, subtitle);
document.body.classList.add('deactivated'); document.body.classList.add('deactivated');
popup.addEventListener('close', () => { const onClose = isUpdated ? () => {
appRuntimeManager.reload();
} : () => {
document.body.classList.add('deactivated-backwards'); document.body.classList.add('deactivated-backwards');
singleInstance.activateInstance(); singleInstance.activateInstance();
@ -331,10 +321,16 @@ export class AppImManager extends EventListenerBase<{
setTimeout(() => { setTimeout(() => {
document.body.classList.remove('deactivated', 'deactivated-backwards'); document.body.classList.remove('deactivated', 'deactivated-backwards');
}, 333); }, 333);
}); };
popup.addEventListener('close', onClose);
popup.show(); popup.show();
}); };
singleInstance.addEventListener('deactivated', onInstanceDeactivated);
if(singleInstance.deactivatedReason) {
onInstanceDeactivated(singleInstance.deactivatedReason);
}
// remove scroll listener when setting chat to tray // remove scroll listener when setting chat to tray
this.addEventListener('chat_changing', ({to}) => { this.addEventListener('chat_changing', ({to}) => {
@ -348,8 +344,8 @@ export class AppImManager extends EventListenerBase<{
}); });
}); });
rootScope.addEventListener('notification_build', (options) => { apiManagerProxy.addEventListener('notificationBuild', (options) => {
if(this.chat.peerId === options.message.peerId && !idleController.idle.isIDLE) { if(this.chat.peerId === options.message.peerId && !idleController.isIdle) {
return; return;
} }
@ -366,6 +362,8 @@ export class AppImManager extends EventListenerBase<{
} }
appNavigationController.overrideHash(str); appNavigationController.overrideHash(str);
apiManagerProxy.updateTabState('chatPeerIds', this.chats.map((chat) => chat.peerId).filter(Boolean));
}); });
// stateStorage.get('chatPositions').then((c) => { // stateStorage.get('chatPositions').then((c) => {
@ -1502,7 +1500,8 @@ export class AppImManager extends EventListenerBase<{
} }
} }
rootScope.dispatchEvent('im_tab_change', id); const onImTabChange = (window as any).onImTabChange;
onImTabChange && onImTabChange(id);
//this._selectTab(id, mediaSizes.isMobile); //this._selectTab(id, mediaSizes.isMobile);
//document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, id === 2); //document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, id === 2);
@ -1664,6 +1663,10 @@ export class AppImManager extends EventListenerBase<{
return; return;
} }
if(options.threadId) {
options.type = 'discussion';
}
const type = options.type ??= 'chat'; const type = options.type ??= 'chat';
// * prevent opening already opened peer // * prevent opening already opened peer

2
src/lib/appManagers/appInlineBotsManager.ts

@ -97,7 +97,6 @@ export class AppInlineBotsManager extends AppManager {
public switchToPM(fromPeerId: PeerId, botId: BotId, startParam: string) { public switchToPM(fromPeerId: PeerId, botId: BotId, startParam: string) {
this.setHash[botId] = {peerId: fromPeerId, time: Date.now()}; this.setHash[botId] = {peerId: fromPeerId, time: Date.now()};
this.rootScope.dispatchEvent('history_focus', {peerId: botId.toPeerId()});
return this.appMessagesManager.startBot(botId, undefined, startParam); return this.appMessagesManager.startBot(botId, undefined, startParam);
} }
@ -224,7 +223,6 @@ export class AppInlineBotsManager extends AppManager {
} }
public switchInlineQuery(peerId: PeerId, threadId: number, botId: BotId, query: string) { public switchInlineQuery(peerId: PeerId, threadId: number, botId: BotId, query: string) {
this.rootScope.dispatchEvent('history_focus', {peerId, threadId});
this.appDraftsManager.setDraft(peerId, threadId, '@' + this.appUsersManager.getUser(botId).username + ' ' + query); this.appDraftsManager.setDraft(peerId, threadId, '@' + this.appUsersManager.getUser(botId).username + ' ' + query);
} }

3
src/lib/appManagers/appManagersManager.ts

@ -44,8 +44,7 @@ export class AppManagersManager {
this.cryptoPortAttached = true; this.cryptoPortAttached = true;
const port = event.ports[0]; const port = event.ports[0];
cryptoMessagePort.attachListenPort(port); cryptoMessagePort.attachPort(port);
cryptoMessagePort.attachSendPort(port);
this.cryptoPortPromise.resolve(); this.cryptoPortPromise.resolve();
}); });
} }

29
src/lib/appManagers/appMessagesManager.ts

@ -56,6 +56,8 @@ import getDocumentInputFileName from "./utils/docs/getDocumentInputFileName";
import getFileNameForUpload from "../../helpers/getFileNameForUpload"; import getFileNameForUpload from "../../helpers/getFileNameForUpload";
import type { Progress } from "./appDownloadManager"; import type { Progress } from "./appDownloadManager";
import noop from "../../helpers/noop"; import noop from "../../helpers/noop";
import appTabsManager from "./appTabsManager";
import MTProtoMessagePort from "../mtproto/mtprotoMessagePort";
//console.trace('include'); //console.trace('include');
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках // TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
@ -209,7 +211,7 @@ export class AppMessagesManager extends AppManager {
private middleware: ReturnType<typeof getMiddleware>; private middleware: ReturnType<typeof getMiddleware>;
private unreadMentions: {[peerId: PeerId]: SlicedArray<number>} = {}; private unreadMentions: {[peerId: PeerId]: SlicedArray<number>} = {};
private goToNextMentionPromises: {[peerId: PeerId]: Promise<any>} = {}; private goToNextMentionPromises: {[peerId: PeerId]: Promise<number>} = {};
private batchUpdates: { private batchUpdates: {
[k in keyof BatchUpdates]?: { [k in keyof BatchUpdates]?: {
@ -3543,9 +3545,10 @@ export class AppMessagesManager extends AppManager {
private handleNewMessage(peerId: PeerId, mid: number) { private handleNewMessage(peerId: PeerId, mid: number) {
(this.newMessagesToHandle[peerId] ??= new Set()).add(mid); (this.newMessagesToHandle[peerId] ??= new Set()).add(mid);
if(!this.newMessagesHandleTimeout) { // if(!this.newMessagesHandleTimeout) {
this.newMessagesHandleTimeout = ctx.setTimeout(this.handleNewMessages, 0); // this.newMessagesHandleTimeout = ctx.setTimeout(this.handleNewMessages, 0);
} // }
this.handleNewMessages();
} }
private handleNewMessages = () => { private handleNewMessages = () => {
@ -3822,7 +3825,7 @@ export class AppMessagesManager extends AppManager {
const mid = last && last[last.length - 1]; const mid = last && last[last.length - 1];
if(mid) { if(mid) {
slicedArray.delete(mid); slicedArray.delete(mid);
this.rootScope.dispatchEvent('history_focus', {peerId, mid}); return mid;
} else { } else {
this.fixUnreadMentionsCountIfNeeded(peerId, slicedArray); this.fixUnreadMentionsCountIfNeeded(peerId, slicedArray);
} }
@ -5104,10 +5107,22 @@ export class AppMessagesManager extends AppManager {
return; return;
} }
this.rootScope.dispatchEvent('notification_build', { const tabs = appTabsManager.getTabs();
let tab = tabs.find((tab) => {
const {chatPeerIds} = tab.state;
return chatPeerIds[chatPeerIds.length - 1] === peerId;
});
if(!tab) {
tabs.sort((a, b) => a.state.idleStartTime - b.state.idleStartTime);
tab = !tabs[0].state.idleStartTime ? tabs[0] : tabs[tabs.length - 1];
}
const port = MTProtoMessagePort.getInstance<false>();
port.invokeVoid('notificationBuild', {
message, message,
...options ...options
}); }, tab.source);
} }
public getScheduledMessagesStorage(peerId: PeerId) { public getScheduledMessagesStorage(peerId: PeerId) {

2
src/lib/appManagers/appStateManager.ts

@ -43,7 +43,7 @@ export class AppStateManager {
} }
public setKeyValueToStorage<T extends keyof State>(key: T, value: State[T] = this.state[key], onlyLocal?: boolean) { public setKeyValueToStorage<T extends keyof State>(key: T, value: State[T] = this.state[key], onlyLocal?: boolean) {
MTProtoMessagePort.getInstance<false>().invoke('mirror', {name: 'state', key, value}); MTProtoMessagePort.getInstance<false>().invokeVoid('mirror', {name: 'state', key, value});
this.storage.set({ this.storage.set({
[key]: value [key]: value

52
src/lib/appManagers/appTabsManager.ts

@ -0,0 +1,52 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { TabState } from "../mtproto/mtprotoworker";
import { MOUNT_CLASS_TO } from "../../config/debug";
import MTProtoMessagePort from "../mtproto/mtprotoMessagePort";
type Tab = {
source: MessageEventSource,
state: TabState
};
export class AppTabsManager {
private tabs: Map<Tab['source'], Tab>;
constructor() {
this.tabs = new Map();
}
public start() {
const port = MTProtoMessagePort.getInstance<false>();
port.addEventListener('tabState', (state, source) => {
const tab = this.tabs.get(source);
tab.state = state;
});
}
public getTabs() {
return [...this.tabs.values()].filter((tab) => !!tab.state);
}
public addTab(source: MessageEventSource) {
const tab: Tab = {
source,
state: undefined
};
this.tabs.set(source, tab);
}
public deleteTab(source: MessageEventSource) {
this.tabs.delete(source);
}
}
const appTabsManager = new AppTabsManager();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appTabsManager = appTabsManager);
export default appTabsManager;

95
src/lib/appManagers/getProxiedManagers.ts

@ -11,52 +11,52 @@ import { AckedResult } from "../mtproto/superMessagePort";
import noop from "../../helpers/noop"; import noop from "../../helpers/noop";
import dT from "../../helpers/dT"; import dT from "../../helpers/dT";
let stats: { // let stats: {
[manager: string]: { // [manager: string]: {
[method: string]: { // [method: string]: {
times: number[], // times: number[],
byArgs: { // byArgs: {
[args: string]: number[] // [args: string]: number[]
} // }
} // }
} // }
} = {}; // } = {};
let sentCount = 0; // let sentCount = 0;
let sentMethods: {[key: string]: number} = {}; // let sentMethods: {[key: string]: number} = {};
let sentMethods2: {[key: string]: number} = {}; // let sentMethods2: {[key: string]: number} = {};
function collectStats(manager: string, method: string, args: any[], promise: Promise<any>) { // function collectStats(manager: string, method: string, args: any[], promise: Promise<any>) {
++sentCount; // ++sentCount;
const key = [manager, method].join('-'); // const key = [manager, method].join('-');
if(!sentMethods[key]) sentMethods[key] = 0; // if(!sentMethods[key]) sentMethods[key] = 0;
++sentMethods[key]; // ++sentMethods[key];
const key2 = [('00000' + sentCount).slice(-5), key].join('-'); // const key2 = [('00000' + sentCount).slice(-5), key].join('-');
let byManager = stats[manager] ??= {}; // let byManager = stats[manager] ??= {};
let byMethod = byManager[method] ??= {times: [], byArgs: {}}; // let byMethod = byManager[method] ??= {times: [], byArgs: {}};
const perf = performance.now(); // const perf = performance.now();
promise.catch(noop).finally(() => { // promise.catch(noop).finally(() => {
const time = performance.now() - perf; // const time = performance.now() - perf;
byMethod.times.push(time); // byMethod.times.push(time);
sentMethods2[key2] = time; // sentMethods2[key2] = time;
try { // try {
const argsString = JSON.stringify(args); // const argsString = JSON.stringify(args);
byMethod.byArgs[argsString].push(time); // byMethod.byArgs[argsString].push(time);
} catch(err) {} // } catch(err) {}
}); // });
} // }
setInterval(() => { // setInterval(() => {
console.log(dT(), '[PROXY] stats', stats, sentCount, sentMethods, sentMethods2); // // console.log(dT(), '[PROXY] stats', stats, sentCount, sentMethods, sentMethods2);
sentCount = 0; // sentCount = 0;
sentMethods = {}; // sentMethods = {};
sentMethods2 = {}; // sentMethods2 = {};
}, 2000); // }, 2000);
function createProxy(/* source: T, */name: string, ack?: boolean) { function createProxy(/* source: T, */name: string, ack?: boolean) {
const proxy = new Proxy({}, { const proxy = new Proxy({}, {
@ -75,7 +75,7 @@ function createProxy(/* source: T, */name: string, ack?: boolean) {
args args
}, ack as any); }, ack as any);
collectStats(name, p as string, args, promise); // collectStats(name, p as string, args, promise);
return promise; return promise;
@ -114,7 +114,6 @@ export default function getProxiedManagers() {
proxied = createProxyProxy({}, false); proxied = createProxyProxy({}, false);
proxied.acknowledged = createProxyProxy({}, true); proxied.acknowledged = createProxyProxy({}, true);
proxied.acknowledged.apiFileManager.cancelDownload('asd');
return proxied; return proxied;
} }

18
src/lib/appManagers/uiNotificationsManager.ts

@ -22,8 +22,10 @@ import webPushApiManager, { PushSubscriptionNotify } from "../mtproto/webPushApi
import fixEmoji from "../richTextProcessor/fixEmoji"; import fixEmoji from "../richTextProcessor/fixEmoji";
import wrapPlainText from "../richTextProcessor/wrapPlainText"; import wrapPlainText from "../richTextProcessor/wrapPlainText";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import appImManager from "./appImManager";
import appRuntimeManager from "./appRuntimeManager"; import appRuntimeManager from "./appRuntimeManager";
import { AppManagers } from "./managers"; import { AppManagers } from "./managers";
import generateMessageId from "./utils/messageId/generateMessageId";
import getPeerId from "./utils/peers/getPeerId"; import getPeerId from "./utils/peers/getPeerId";
type MyNotification = Notification & { type MyNotification = Notification & {
@ -124,7 +126,7 @@ export class UiNotificationsManager {
this.cancel(str); this.cancel(str);
}); });
rootScope.addEventListener('push_init', (tokenData) => { webPushApiManager.addEventListener('push_init', (tokenData) => {
this.pushInited = true; this.pushInited = true;
if(!this.settings.nodesktop && !this.settings.nopush) { if(!this.settings.nodesktop && !this.settings.nopush) {
if(tokenData) { if(tokenData) {
@ -136,10 +138,10 @@ export class UiNotificationsManager {
this.unregisterDevice(tokenData); this.unregisterDevice(tokenData);
} }
}); });
rootScope.addEventListener('push_subscribe', (tokenData) => { webPushApiManager.addEventListener('push_subscribe', (tokenData) => {
this.registerDevice(tokenData); this.registerDevice(tokenData);
}); });
rootScope.addEventListener('push_unsubscribe', (tokenData) => { webPushApiManager.addEventListener('push_unsubscribe', (tokenData) => {
this.unregisterDevice(tokenData); this.unregisterDevice(tokenData);
}); });
@ -148,7 +150,7 @@ export class UiNotificationsManager {
this.topMessagesDeferred.resolve(); this.topMessagesDeferred.resolve();
}, {once: true}); }, {once: true});
rootScope.addEventListener('push_notification_click', (notificationData) => { webPushApiManager.addEventListener('push_notification_click', (notificationData) => {
if(notificationData.action === 'push_settings') { if(notificationData.action === 'push_settings') {
/* this.topMessagesDeferred.then(() => { /* this.topMessagesDeferred.then(() => {
$modal.open({ $modal.open({
@ -192,9 +194,9 @@ export class UiNotificationsManager {
return; return;
} }
rootScope.dispatchEvent('history_focus', { appImManager.setInnerPeer({
peerId, peerId,
mid: +notificationData.custom.msg_id lastMsgId: generateMessageId(+notificationData.custom.msg_id)
}); });
}); });
} }
@ -253,7 +255,7 @@ export class UiNotificationsManager {
notification.title = wrapPlainText(notification.title); notification.title = wrapPlainText(notification.title);
notification.onclick = () => { notification.onclick = () => {
rootScope.dispatchEvent('history_focus', {peerId, mid: message.mid}); appImManager.setInnerPeer({peerId, lastMsgId: message.mid});
}; };
notification.message = notificationMessage; notification.message = notificationMessage;
@ -275,7 +277,7 @@ export class UiNotificationsManager {
} }
} }
private toggleToggler(enable = idleController.idle.isIDLE) { private toggleToggler(enable = idleController.isIdle) {
if(IS_MOBILE) return; if(IS_MOBILE) return;
const resetTitle = () => { const resetTitle = () => {

11
src/lib/appManagers/utils/state/loadState.ts

@ -44,7 +44,11 @@ async function loadStateInner() {
const recordPromise = recordPromiseBound(log); const recordPromise = recordPromiseBound(log);
const promises = ALL_KEYS.map((key) => recordPromise(stateStorage.get(key), 'state ' + key)) const promises = ALL_KEYS.map((key) => recordPromise(stateStorage.get(key), 'state ' + key))
.concat(recordPromise(sessionStorage.get('user_auth'), 'auth'), recordPromise(sessionStorage.get('state_id'), 'auth')) .concat(
recordPromise(sessionStorage.get('user_auth'), 'auth'),
recordPromise(sessionStorage.get('state_id'), 'auth'),
recordPromise(sessionStorage.get('k_build'), 'auth')
)
.concat(recordPromise(stateStorage.get('user_auth'), 'old auth')); // support old webk format .concat(recordPromise(stateStorage.get('user_auth'), 'old auth')); // support old webk format
const arr = await Promise.all(promises); const arr = await Promise.all(promises);
@ -119,6 +123,7 @@ async function loadStateInner() {
// * Read auth // * Read auth
let auth = arr.shift() as UserAuth | number; let auth = arr.shift() as UserAuth | number;
const stateId = arr.shift() as number; const stateId = arr.shift() as number;
const sessionBuild = arr.shift() as number;
const shiftedWebKAuth = arr.shift() as UserAuth | number; const shiftedWebKAuth = arr.shift() as UserAuth | number;
if(!auth && shiftedWebKAuth) { // support old webk auth if(!auth && shiftedWebKAuth) { // support old webk auth
auth = shiftedWebKAuth; auth = shiftedWebKAuth;
@ -335,6 +340,10 @@ async function loadStateInner() {
pushToState('build', BUILD); pushToState('build', BUILD);
} }
if(sessionBuild !== BUILD && (!sessionBuild || sessionBuild < BUILD)) {
sessionStorage.set({k_build: BUILD});
}
// ! probably there is better place for it // ! probably there is better place for it
rootScope.settings = state.settings; rootScope.settings = state.settings;

6
src/lib/cacheStorage.ts

@ -131,9 +131,13 @@ export default class CacheStorageController {
return Promise.resolve(fakeWriter); return Promise.resolve(fakeWriter);
} }
public static toggleStorage(enabled: boolean) { public static toggleStorage(enabled: boolean, clearWrite: boolean) {
return Promise.all(this.STORAGES.map((storage) => { return Promise.all(this.STORAGES.map((storage) => {
storage.useStorage = enabled; storage.useStorage = enabled;
if(!clearWrite) {
return;
}
if(!enabled) { if(!enabled) {
return storage.deleteAll(); return storage.deleteAll();

1
src/lib/crypto/aesCtrUtils.ts

@ -5,7 +5,6 @@
*/ */
import CTR from "./utils/aesCTR"; import CTR from "./utils/aesCTR";
import bytesToHex from "../../helpers/bytes/bytesToHex";
import subtle from "./subtle"; import subtle from "./subtle";
const aesCTRs: Map<number, K> = new Map(); const aesCTRs: Map<number, K> = new Map();

12
src/lib/localStorage.ts

@ -113,7 +113,7 @@ class LocalStorage<Storage extends Record<string, any>> {
} */ } */
public clear() { public clear() {
const keys: string[] = ['dc', 'server_time_offset', 'xt_instance', 'user_auth', 'state_id']; const keys: string[] = ['dc', 'server_time_offset', 'xt_instance', 'user_auth', 'state_id', 'k_build'];
for(let i = 1; i <= 5; ++i) { for(let i = 1; i <= 5; ++i) {
keys.push(`dc${i}_server_salt`); keys.push(`dc${i}_server_salt`);
keys.push(`dc${i}_auth_key`); keys.push(`dc${i}_auth_key`);
@ -124,9 +124,13 @@ class LocalStorage<Storage extends Record<string, any>> {
} }
} }
public toggleStorage(enabled: boolean) { public toggleStorage(enabled: boolean, clearWrite: boolean) {
this.useStorage = enabled; this.useStorage = enabled;
if(!clearWrite) {
return;
}
if(!enabled) { if(!enabled) {
this.clear(); this.clear();
} else { } else {
@ -191,7 +195,7 @@ export default class LocalStorageController<Storage extends Record<string, any>>
return this.proxy<void>('clear'/* , preserveKeys */); return this.proxy<void>('clear'/* , preserveKeys */);
} }
public toggleStorage(enabled: boolean) { public toggleStorage(enabled: boolean, clearWrite: boolean) {
return this.proxy<void>('toggleStorage', enabled); return this.proxy<void>('toggleStorage', enabled, clearWrite);
} }
} }

11
src/lib/mtproto/apiFileManager.ts

@ -596,19 +596,24 @@ export class ApiFileManager extends AppManager {
} }
public downloadMedia(options: DownloadMediaOptions): DownloadPromise { public downloadMedia(options: DownloadMediaOptions): DownloadPromise {
const {media, thumb} = options; let {media, thumb} = options;
const isPhoto = media?._ === 'photo'; const isPhoto = media._ === 'photo';
if(media._ === 'photoEmpty' || (isPhoto && !thumb)) { if(media._ === 'photoEmpty' || (isPhoto && !thumb)) {
return Promise.reject('preloadPhoto photoEmpty!'); return Promise.reject('preloadPhoto photoEmpty!');
} }
// get original instance with correct file_reference instead of using copies
const isDocument = media._ === 'document';
if(isDocument) media = this.appDocsManager.getDoc(media.id);
else if(isPhoto) media = this.appPhotosManager.getPhoto(media.id);
const {fileName, downloadOptions} = getDownloadMediaDetails(options); const {fileName, downloadOptions} = getDownloadMediaDetails(options);
let promise = this.getDownload(fileName); let promise = this.getDownload(fileName);
if(!promise) { if(!promise) {
promise = this.download(downloadOptions); promise = this.download(downloadOptions);
if(media._ === 'document') { if(isDocument) {
this.rootScope.dispatchEvent('document_downloading', media.id); this.rootScope.dispatchEvent('document_downloading', media.id);
promise.catch(noop).finally(() => { promise.catch(noop).finally(() => {
this.rootScope.dispatchEvent('document_downloaded', media.id); this.rootScope.dispatchEvent('document_downloaded', media.id);

2
src/lib/mtproto/apiManager.ts

@ -297,7 +297,7 @@ export class ApiManager extends ApiManagerMethods {
const clear = async() => { const clear = async() => {
this.baseDcId = undefined; this.baseDcId = undefined;
//this.telegramMeNotify(false); //this.telegramMeNotify(false);
await toggleStorages(false); await toggleStorages(false, true);
IDB.closeDatabases(); IDB.closeDatabases();
this.rootScope.dispatchEvent('logging_out'); this.rootScope.dispatchEvent('logging_out');
}; };

29
src/lib/mtproto/mtproto.worker.ts

@ -9,7 +9,6 @@ import '../polyfill';
import '../../helpers/peerIdPolyfill'; import '../../helpers/peerIdPolyfill';
import cryptoWorker from "../crypto/cryptoMessagePort"; import cryptoWorker from "../crypto/cryptoMessagePort";
import CacheStorageController from '../cacheStorage';
import { setEnvironment } from '../../environment/utils'; import { setEnvironment } from '../../environment/utils';
import appStateManager from '../appManagers/appStateManager'; import appStateManager from '../appManagers/appStateManager';
import transportController from './transports/controller'; import transportController from './transports/controller';
@ -19,8 +18,8 @@ import appManagersManager from '../appManagers/appManagersManager';
import listenMessagePort from '../../helpers/listenMessagePort'; import listenMessagePort from '../../helpers/listenMessagePort';
import { logger } from '../logger'; import { logger } from '../logger';
import { State } from '../../config/state'; import { State } from '../../config/state';
import AppStorage from '../storage';
import toggleStorages from '../../helpers/toggleStorages'; import toggleStorages from '../../helpers/toggleStorages';
import appTabsManager from '../appManagers/appTabsManager';
let _isServiceWorkerOnline = true; let _isServiceWorkerOnline = true;
export function isServiceWorkerOnline() { export function isServiceWorkerOnline() {
@ -55,7 +54,9 @@ port.addMultipleEventsListeners({
RESET_STORAGES_PROMISE.resolve(resetStorages); RESET_STORAGES_PROMISE.resolve(resetStorages);
}, },
toggleStorages: (enabled) => toggleStorages(enabled), toggleStorages: ({enabled, clearWrite}) => {
return toggleStorages(enabled, clearWrite);
},
event: (payload, source) => { event: (payload, source) => {
log('will redirect event', payload, source); log('will redirect event', payload, source);
@ -68,7 +69,7 @@ port.addMultipleEventsListeners({
createObjectURL: (blob) => { createObjectURL: (blob) => {
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} },
// socketProxy: (task) => { // socketProxy: (task) => {
// const socketTask = task.payload; // const socketTask = task.payload;
@ -84,24 +85,16 @@ port.addMultipleEventsListeners({
// socketsProxied.delete(id); // socketsProxied.delete(id);
// } // }
// }, // },
// refreshReference: (task: RefreshReferenceTaskResponse) => {
// const hex = bytesToHex(task.originalPayload);
// const r = apiFileManager.refreshReferencePromises[hex];
// const deferred = r?.deferred;
// if(deferred) {
// if(task.error) {
// deferred.reject(task.error);
// } else {
// deferred.resolve(task.payload);
// }
// }
// },
}); });
log('MTProto start'); log('MTProto start');
appManagersManager.start(); appManagersManager.start();
appManagersManager.getManagers(); appManagersManager.getManagers();
appTabsManager.start();
listenMessagePort(port); listenMessagePort(port, (source) => {
appTabsManager.addTab(source);
}, (source) => {
appTabsManager.deleteTab(source);
});

12
src/lib/mtproto/mtprotoMessagePort.ts

@ -10,7 +10,7 @@ import type loadState from "../appManagers/utils/state/loadState";
import type { StoragesResults } from "../appManagers/utils/storages/loadStorages"; import type { StoragesResults } from "../appManagers/utils/storages/loadStorages";
import type { LocalStorageProxyTask } from "../localStorage"; import type { LocalStorageProxyTask } from "../localStorage";
import type { Awaited } from "../../types"; import type { Awaited } from "../../types";
import type { Mirrors, MirrorTaskPayload } from "./mtprotoworker"; import type { Mirrors, MirrorTaskPayload, NotificationBuildTaskPayload, TabState } from "./mtprotoworker";
import type toggleStorages from "../../helpers/toggleStorages"; import type toggleStorages from "../../helpers/toggleStorages";
import SuperMessagePort from "./superMessagePort"; import SuperMessagePort from "./superMessagePort";
@ -25,20 +25,22 @@ export default class MTProtoMessagePort<Master extends boolean = true> extends S
crypto: (payload: {method: string, args: any[]}) => Promise<any>, crypto: (payload: {method: string, args: any[]}) => Promise<any>,
state: (payload: {userId: UserId} & Awaited<ReturnType<typeof loadState>> & {storagesResults?: StoragesResults}) => void, state: (payload: {userId: UserId} & Awaited<ReturnType<typeof loadState>> & {storagesResults?: StoragesResults}) => void,
manager: (payload: MTProtoManagerTaskPayload) => any, manager: (payload: MTProtoManagerTaskPayload) => any,
toggleStorages: typeof toggleStorages, toggleStorages: (payload: {enabled: boolean, clearWrite: boolean}) => ReturnType<typeof toggleStorages>,
serviceWorkerOnline: (online: boolean) => void, serviceWorkerOnline: (online: boolean) => void,
cryptoPort: (payload: void, source: MessageEventSource, event: MessageEvent) => void, cryptoPort: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
createObjectURL: (blob: Blob) => string createObjectURL: (blob: Blob) => string,
tabState: (payload: TabState, source: MessageEventSource) => void,
} & MTProtoBroadcastEvent, { } & MTProtoBroadcastEvent, {
convertWebp: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>, convertWebp: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>,
convertOpus: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>, convertOpus: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>,
localStorageProxy: (payload: LocalStorageProxyTask['payload']) => Promise<any>, localStorageProxy: (payload: LocalStorageProxyTask['payload']) => Promise<any>,
mirror: (payload: MirrorTaskPayload) => void mirror: (payload: MirrorTaskPayload) => void,
notificationBuild: (payload: NotificationBuildTaskPayload) => void
} & MTProtoBroadcastEvent, Master> { } & MTProtoBroadcastEvent, Master> {
private static INSTANCE: MTProtoMessagePort; private static INSTANCE: MTProtoMessagePort;
constructor() { constructor() {
super(false); super();
MTProtoMessagePort.INSTANCE = this; MTProtoMessagePort.INSTANCE = this;

51
src/lib/mtproto/mtprotoworker.ts

@ -6,16 +6,17 @@
import type { RequestFilePartTask, RequestFilePartTaskResponse, ServiceWorkerTask } from '../serviceWorker/index.service'; import type { RequestFilePartTask, RequestFilePartTaskResponse, ServiceWorkerTask } from '../serviceWorker/index.service';
import type { Awaited, WorkerTaskVoidTemplate } from '../../types'; import type { Awaited, WorkerTaskVoidTemplate } from '../../types';
import type { CacheStorageDbName } from '../cacheStorage';
import type { State } from '../../config/state';
import type { Message, MessagePeerReaction, PeerNotifySettings } from '../../layer';
import { CryptoMethods } from '../crypto/crypto_methods'; import { CryptoMethods } from '../crypto/crypto_methods';
import rootScope from '../rootScope'; import rootScope from '../rootScope';
import webpWorkerController from '../webp/webpWorkerController'; import webpWorkerController from '../webp/webpWorkerController';
import { MOUNT_CLASS_TO } from '../../config/debug'; import { MOUNT_CLASS_TO } from '../../config/debug';
import sessionStorage from '../sessionStorage'; import sessionStorage from '../sessionStorage';
import webPushApiManager from './webPushApiManager'; import webPushApiManager from './webPushApiManager';
import AppStorage from '../storage';
import appRuntimeManager from '../appManagers/appRuntimeManager'; import appRuntimeManager from '../appManagers/appRuntimeManager';
import telegramMeWebManager from './telegramMeWebManager'; import telegramMeWebManager from './telegramMeWebManager';
import CacheStorageController, { CacheStorageDbName } from '../cacheStorage';
import pause from '../../helpers/schedulers/pause'; import pause from '../../helpers/schedulers/pause';
import isObject from '../../helpers/object/isObject'; import isObject from '../../helpers/object/isObject';
import ENVIRONMENT from '../../environment'; import ENVIRONMENT from '../../environment';
@ -25,12 +26,12 @@ import MTProtoMessagePort from './mtprotoMessagePort';
import cryptoMessagePort from '../crypto/cryptoMessagePort'; import cryptoMessagePort from '../crypto/cryptoMessagePort';
import SuperMessagePort from './superMessagePort'; import SuperMessagePort from './superMessagePort';
import IS_SHARED_WORKER_SUPPORTED from '../../environment/sharedWorkerSupport'; import IS_SHARED_WORKER_SUPPORTED from '../../environment/sharedWorkerSupport';
import type { State } from '../../config/state';
import toggleStorages from '../../helpers/toggleStorages'; import toggleStorages from '../../helpers/toggleStorages';
import idleController from '../../helpers/idleController';
export interface ToggleStorageTask extends WorkerTaskVoidTemplate { export interface ToggleStorageTask extends WorkerTaskVoidTemplate {
type: 'toggleStorages', type: 'toggleStorages',
payload: boolean payload: {enabled: boolean, clearWrite: boolean}
}; };
export type Mirrors = { export type Mirrors = {
@ -43,6 +44,18 @@ export type MirrorTaskPayload<T extends keyof Mirrors = keyof Mirrors, K extends
value: any value: any
}; };
export type NotificationBuildTaskPayload = {
message: Message.message | Message.messageService,
fwdCount?: number,
peerReaction?: MessagePeerReaction,
peerTypeNotifySettings?: PeerNotifySettings
};
export type TabState = {
chatPeerIds: PeerId[],
idleStartTime: number,
};
class ApiManagerProxy extends MTProtoMessagePort { class ApiManagerProxy extends MTProtoMessagePort {
private worker: /* Window */Worker; private worker: /* Window */Worker;
private isSWRegistered: boolean; private isSWRegistered: boolean;
@ -53,12 +66,18 @@ class ApiManagerProxy extends MTProtoMessagePort {
public newVersion: string; public newVersion: string;
public oldVersion: string; public oldVersion: string;
private tabState: TabState;
constructor() { constructor() {
super(); super();
this.isSWRegistered = true; this.isSWRegistered = true;
this.taskListenersSW = {}; this.taskListenersSW = {};
this.mirrors = {} as any; this.mirrors = {} as any;
this.tabState = {
chatPeerIds: [],
idleStartTime: 0
};
this.log('constructor'); this.log('constructor');
@ -158,7 +177,7 @@ class ApiManagerProxy extends MTProtoMessagePort {
rootScope.addEventListener('logging_out', () => { rootScope.addEventListener('logging_out', () => {
const toClear: CacheStorageDbName[] = ['cachedFiles', 'cachedStreamChunks']; const toClear: CacheStorageDbName[] = ['cachedFiles', 'cachedStreamChunks'];
Promise.all([ Promise.all([
toggleStorages(false), toggleStorages(false, true),
sessionStorage.clear(), sessionStorage.clear(),
Promise.race([ Promise.race([
telegramMeWebManager.setAuthorized(false), telegramMeWebManager.setAuthorized(false),
@ -171,6 +190,11 @@ class ApiManagerProxy extends MTProtoMessagePort {
}); });
}); });
idleController.addEventListener('change', (idle) => {
this.updateTabStateIdle(idle);
});
this.updateTabStateIdle(idleController.isIdle);
this.log('Passing environment:', ENVIRONMENT); this.log('Passing environment:', ENVIRONMENT);
this.invoke('environment', ENVIRONMENT); this.invoke('environment', ENVIRONMENT);
// this.sendState(); // this.sendState();
@ -357,10 +381,10 @@ class ApiManagerProxy extends MTProtoMessagePort {
} }
/// #endif /// #endif
public async toggleStorages(enabled: boolean) { public async toggleStorages(enabled: boolean, clearWrite: boolean) {
await toggleStorages(enabled); await toggleStorages(enabled, clearWrite);
this.invoke('toggleStorages', enabled); this.invoke('toggleStorages', {enabled, clearWrite});
const task: ToggleStorageTask = {type: 'toggleStorages', payload: enabled}; const task: ToggleStorageTask = {type: 'toggleStorages', payload: {enabled, clearWrite}};
this.postSWMessage(task); this.postSWMessage(task);
} }
@ -373,6 +397,15 @@ class ApiManagerProxy extends MTProtoMessagePort {
return this.getMirror('state'); return this.getMirror('state');
} }
public updateTabState<T extends keyof TabState>(key: T, value: TabState[T]) {
this.tabState[key] = value;
this.invokeVoid('tabState', this.tabState);
}
public updateTabStateIdle(idle: boolean) {
this.updateTabState('idleStartTime', idle ? Date.now() : 0);
}
private onMirrorTask = (payload: MirrorTaskPayload) => { private onMirrorTask = (payload: MirrorTaskPayload) => {
const {name, key, value} = payload; const {name, key, value} = payload;
if(!payload.hasOwnProperty('key')) { if(!payload.hasOwnProperty('key')) {

2
src/lib/mtproto/schema.ts

File diff suppressed because one or more lines are too long

144
src/lib/mtproto/singleInstance.ts

@ -9,14 +9,16 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE * https://github.com/zhukov/webogram/blob/master/LICENSE
*/ */
import App from "../../config/app";
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import tabId from "../../config/tabId";
import IS_SHARED_WORKER_SUPPORTED from "../../environment/sharedWorkerSupport"; import IS_SHARED_WORKER_SUPPORTED from "../../environment/sharedWorkerSupport";
import EventListenerBase from "../../helpers/eventListenerBase"; import EventListenerBase from "../../helpers/eventListenerBase";
import idleController from "../../helpers/idleController"; import idleController from "../../helpers/idleController";
import { nextRandomUint } from "../../helpers/random";
import { logger } from "../logger"; import { logger } from "../logger";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import sessionStorage from "../sessionStorage"; import sessionStorage from "../sessionStorage";
import apiManagerProxy from "./mtprotoworker";
export type AppInstance = { export type AppInstance = {
id: number, id: number,
@ -24,29 +26,40 @@ export type AppInstance = {
time: number time: number
}; };
export type InstanceDeactivateReason = 'version' | 'tabs';
const CHECK_INSTANCE_INTERVAL = 5000; const CHECK_INSTANCE_INTERVAL = 5000;
const DEACTIVATE_TIMEOUT = 30000; const DEACTIVATE_TIMEOUT = 30000;
const MULTIPLE_TABS_THRESHOLD = 20000; const MULTIPLE_TABS_THRESHOLD = 20000;
const IS_MULTIPLE_INSTANCES_SUPPORTED = IS_SHARED_WORKER_SUPPORTED; const IS_MULTIPLE_TABS_SUPPORTED = IS_SHARED_WORKER_SUPPORTED;
export class SingleInstance extends EventListenerBase<{ export class SingleInstance extends EventListenerBase<{
activated: () => void, activated: () => void,
deactivated: () => void deactivated: (reason: InstanceDeactivateReason) => void
}> { }> {
private instanceID: number; private instanceId: number;
private started: boolean; private started: boolean;
private masterInstance: boolean; private masterInstance: boolean;
private deactivateTimeout: number; private deactivateTimeout: number;
private deactivated: boolean; private deactivated: InstanceDeactivateReason;
private initial: boolean;
private log = logger('INSTANCE'); private log = logger('INSTANCE');
constructor() {
super(false);
this.log = logger('INSTANCE');
this.instanceId = tabId;
}
public get deactivatedReason() {
return this.deactivated;
}
public start() { public start() {
if(!this.started && !IS_MULTIPLE_INSTANCES_SUPPORTED/* && !Config.Navigator.mobile && !Config.Modes.packed */) { this.reset();
this.started = true;
this.reset(); if(!this.started/* && !Config.Navigator.mobile && !Config.Modes.packed */) {
//IdleManager.start(); this.started = true;
idleController.addEventListener('change', this.checkInstance); idleController.addEventListener('change', this.checkInstance);
setInterval(this.checkInstance, CHECK_INSTANCE_INTERVAL); setInterval(this.checkInstance, CHECK_INSTANCE_INTERVAL);
@ -59,94 +72,93 @@ export class SingleInstance extends EventListenerBase<{
} }
private reset() { private reset() {
if(IS_MULTIPLE_INSTANCES_SUPPORTED) return;
this.instanceID = nextRandomUint(32);
this.masterInstance = false; this.masterInstance = false;
if(this.deactivateTimeout) clearTimeout(this.deactivateTimeout); this.clearDeactivateTimeout();
this.deactivateTimeout = 0; this.deactivated = undefined;
this.deactivated = false;
this.initial = false;
} }
private clearInstance = () => { private clearInstance = () => {
if(this.masterInstance && !this.deactivated && !IS_MULTIPLE_INSTANCES_SUPPORTED) { if(this.masterInstance && !this.deactivated) {
this.log.warn('clear master instance'); this.log.warn('clear master instance');
sessionStorage.delete('xt_instance'); sessionStorage.delete('xt_instance');
} }
}; };
public activateInstance() { public activateInstance() {
if(this.deactivated && !IS_MULTIPLE_INSTANCES_SUPPORTED) { if(this.deactivated) {
this.reset(); this.reset();
this.checkInstance(false); this.checkInstance(false);
this.dispatchEvent('activated'); this.dispatchEvent('activated');
} }
} }
private deactivateInstance = () => { private deactivateInstance(reason: InstanceDeactivateReason) {
if(this.masterInstance || this.deactivated || IS_MULTIPLE_INSTANCES_SUPPORTED) { if(this.masterInstance || this.deactivated) {
return; return;
} }
this.log('deactivate'); this.log.warn('deactivate', reason);
this.deactivateTimeout = 0; this.clearDeactivateTimeout();
this.deactivated = true; this.deactivated = reason;
this.clearInstance();
//$modalStack.dismissAll();
//document.title = _('inactive_tab_title_raw') this.dispatchEvent('deactivated', reason);
}
idleController.idle.deactivated = true; private clearDeactivateTimeout() {
this.dispatchEvent('deactivated'); if(this.deactivateTimeout) {
}; clearTimeout(this.deactivateTimeout);
this.deactivateTimeout = 0;
}
}
private checkInstance = (idle = idleController.idle?.isIDLE) => { private checkInstance = async(idle = idleController.isIdle) => {
if(this.deactivated || IS_MULTIPLE_INSTANCES_SUPPORTED) { if(this.deactivated) {
return; return;
} }
const time = Date.now(); const time = Date.now();
const newInstance: AppInstance = { const newInstance: AppInstance = {
id: this.instanceID, id: this.instanceId,
idle, idle,
time time
}; };
sessionStorage.get('xt_instance', false).then((curInstance: AppInstance) => { const [curInstance, build = App.build] = await Promise.all([
// this.log('check instance', newInstance, curInstance) sessionStorage.get('xt_instance', false),
if(!idle || sessionStorage.get('k_build', false)
!curInstance || ]);
curInstance.id === this.instanceID ||
curInstance.time < (time - MULTIPLE_TABS_THRESHOLD)) { if(build > App.build) {
sessionStorage.set({xt_instance: newInstance}); this.masterInstance = false;
rootScope.managers.networkerFactory.stopAll();
if(!this.masterInstance) { this.deactivateInstance('version');
rootScope.managers.networkerFactory.startAll(); apiManagerProxy.toggleStorages(false, false);
if(!this.initial) { return;
this.initial = true; } else if(IS_MULTIPLE_TABS_SUPPORTED) {
} else { sessionStorage.set({xt_instance: newInstance});
this.log.warn('now master instance', newInstance); return;
} }
this.masterInstance = true; // this.log('check instance', newInstance, curInstance)
} if(!idle ||
!curInstance ||
if(this.deactivateTimeout) { curInstance.id === this.instanceId ||
clearTimeout(this.deactivateTimeout); curInstance.time < (time - MULTIPLE_TABS_THRESHOLD)) {
this.deactivateTimeout = 0; sessionStorage.set({xt_instance: newInstance});
}
} else { if(!this.masterInstance) {
if(this.masterInstance) { this.masterInstance = true;
rootScope.managers.networkerFactory.stopAll(); rootScope.managers.networkerFactory.startAll();
this.log.warn('now idle instance', newInstance); this.log.warn('now master instance', newInstance);
if(!this.deactivateTimeout) {
this.deactivateTimeout = window.setTimeout(this.deactivateInstance, DEACTIVATE_TIMEOUT);
}
this.masterInstance = false;
}
} }
});
this.clearDeactivateTimeout();
} else if(this.masterInstance) {
this.masterInstance = false;
rootScope.managers.networkerFactory.stopAll();
this.log.warn('now idle instance', newInstance);
this.deactivateTimeout ||= window.setTimeout(() => this.deactivateInstance('tabs'), DEACTIVATE_TIMEOUT);
}
}; };
} }

209
src/lib/mtproto/superMessagePort.ts

@ -9,7 +9,6 @@ import ctx from "../../environment/ctx";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice"; import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { IS_SERVICE_WORKER, IS_WORKER, notifyAll } from "../../helpers/context"; import { IS_SERVICE_WORKER, IS_WORKER, notifyAll } from "../../helpers/context";
import EventListenerBase from "../../helpers/eventListenerBase"; import EventListenerBase from "../../helpers/eventListenerBase";
import pause from "../../helpers/schedulers/pause";
import { Awaited, WorkerTaskTemplate, WorkerTaskVoidTemplate } from "../../types"; import { Awaited, WorkerTaskTemplate, WorkerTaskVoidTemplate } from "../../types";
import { logger } from "../logger"; import { logger } from "../logger";
@ -54,9 +53,13 @@ interface BatchTask extends SuperMessagePortTask {
payload: Task[] payload: Task[]
} }
type Task = InvokeTask | ResultTask | AckTask | PingTask | PongTask | BatchTask; interface CloseTask extends SuperMessagePortTask {
type: 'close'
}
type Task = InvokeTask | ResultTask | AckTask | PingTask | PongTask | BatchTask | CloseTask;
type TaskMap = { type TaskMap = {
[type in Task as type['type']]?: (task: Extract<Task, type>) => void | Promise<any> [type in Task as type['type']]?: (task: Extract<Task, type>, source: MessageEventSource, event: MessageEvent<any>) => void | Promise<any>
}; };
export type AckedResult<T> = { export type AckedResult<T> = {
@ -77,8 +80,8 @@ type SendPort = WindowProxy | MessagePort | ServiceWorker | Worker;
type ListenerCallback = (payload: any, source: MessageEventSource, event: MessageEvent<any>) => any; type ListenerCallback = (payload: any, source: MessageEventSource, event: MessageEvent<any>) => any;
type Listeners = Record<string, ListenerCallback>; type Listeners = Record<string, ListenerCallback>;
const PING_INTERVAL = DEBUG || true ? 0x7FFFFFFF : 1000; // const PING_INTERVAL = DEBUG && false ? 0x7FFFFFFF : 5000;
const PING_TIMEOUT = DEBUG || true ? 0x7FFFFFFF : 5000; // const PING_TIMEOUT = DEBUG && false ? 0x7FFFFFFF : 10000;
export default class SuperMessagePort< export default class SuperMessagePort<
Workers extends Listeners, Workers extends Listeners,
@ -105,6 +108,23 @@ export default class SuperMessagePort<
protected debug: boolean; protected debug: boolean;
protected releasingPending: boolean; protected releasingPending: boolean;
protected processTaskMap: TaskMap;
protected onPortDisconnect: (source: MessageEventSource) => void;
constructor() {
super(false);
this.processTaskMap = {
result: this.processResultTask,
ack: this.processAckTask,
invoke: this.processInvokeTask,
ping: this.processPingTask,
pong: this.processPongTask,
close: this.processCloseTask
};
}
public _constructor() { public _constructor() {
super._constructor(false); super._constructor(false);
@ -116,6 +136,17 @@ export default class SuperMessagePort<
this.pending = new Map(); this.pending = new Map();
this.log = logger('MP'); this.log = logger('MP');
this.debug = DEBUG; this.debug = DEBUG;
if(typeof(window) !== 'undefined') {
window.addEventListener('beforeunload', () => {
const task = this.createTask('close', undefined);
this.postMessage(undefined, task);
});
}
}
public setOnPortDisconnect(callback: (source: MessageEventSource) => void) {
this.onPortDisconnect = callback;
} }
public attachPort(port: MessageEventSource) { public attachPort(port: MessageEventSource) {
@ -129,51 +160,64 @@ export default class SuperMessagePort<
} }
public attachSendPort(port: SendPort) { public attachSendPort(port: SendPort) {
this.log.warn('attaching port');
if((port as MessagePort).start) { if((port as MessagePort).start) {
(port as MessagePort).start(); (port as MessagePort).start();
} }
this.sendPorts.push(port); this.sendPorts.push(port);
this.sendPing(port); // this.sendPing(port);
}
protected sendPing(port: SendPort, loop = IS_WORKER) {
let timeout: number;
const promise = new Promise<void>((resolve, reject) => {
this.pingResolves.set(port, resolve);
this.pushTask(this.createTask('ping', undefined), port);
timeout = ctx.setTimeout(() => {
reject();
}, PING_TIMEOUT);
});
promise.then(() => {
clearTimeout(timeout);
this.pingResolves.delete(port);
if(loop) {
this.sendPingWithTimeout(port);
}
}, () => {
this.pingResolves.delete(port);
indexOfAndSplice(this.listenPorts, port);
indexOfAndSplice(this.sendPorts, port);
if((port as MessagePort).close) {
(port as MessagePort).close();
}
});
} }
protected sendPingWithTimeout(port: SendPort, timeout = PING_INTERVAL) { // ! Can't rely on ping because timers can be suspended
ctx.setTimeout(() => { // protected sendPing(port: SendPort, loop = IS_WORKER) {
if(!this.sendPorts.includes(port)) { // let timeout: number;
return; // const promise = new Promise<void>((resolve, reject) => {
} // this.pingResolves.set(port, resolve);
// this.pushTask(this.createTask('ping', undefined), port);
// timeout = ctx.setTimeout(() => {
// reject();
// }, PING_TIMEOUT);
// });
// promise.then(() => {
// // this.log('got pong');
// clearTimeout(timeout);
// this.pingResolves.delete(port);
// if(loop) {
// this.sendPingWithTimeout(port);
// }
// }, () => {
// this.pingResolves.delete(port);
// this.detachPort(port);
// });
// }
// protected sendPingWithTimeout(port: SendPort, timeout = PING_INTERVAL) {
// ctx.setTimeout(() => {
// if(!this.sendPorts.includes(port)) {
// return;
// }
// this.sendPing(port);
// }, timeout);
// }
protected detachPort(port: SendPort) {
this.log.warn('disconnecting port');
port.removeEventListener('message', this.onMessage as any);
indexOfAndSplice(this.listenPorts, port);
indexOfAndSplice(this.sendPorts, port);
if((port as MessagePort).close) {
(port as MessagePort).close();
}
this.sendPing(port); this.onPortDisconnect && this.onPortDisconnect(port as any);
}, timeout);
} }
protected postMessage(port: SendPort | SendPort[], task: Task) { protected postMessage(port: SendPort | SendPort[], task: Task) {
@ -188,27 +232,20 @@ export default class SuperMessagePort<
// this.log('got message', task); // this.log('got message', task);
const source: MessageEventSource = event.source || event.currentTarget as any; // can have no source const source: MessageEventSource = event.source || event.currentTarget as any; // can have no source
if(task.type === 'batch') { /* if(task.type === 'batch') {
const newEvent: MessageEvent = {data: event.data, source: event.source, currentTarget: event.currentTarget} as any; const newEvent: MessageEvent = {data: event.data, source: event.source, currentTarget: event.currentTarget} as any;
task.payload.forEach((task) => { task.payload.forEach((task) => {
// @ts-ignore // @ts-ignore
newEvent.data = task; newEvent.data = task;
this.onMessage(newEvent); this.onMessage(newEvent);
}); });
} else if(task.type === 'result') { } */
this.processResultTask(task);
} else if(task.type === 'ack') { // @ts-ignore
this.processAckTask(task); this.processTaskMap[task.type](task, source, event);
} else if(task.type === 'invoke') {
this.processInvokeTask(task, source, event);
} else if(task.type === 'ping') {
this.processPingTask(task, source, event);
} else if(task.type === 'pong') {
this.processPongTask(task, source, event);
}
}; };
protected async releasePending() { protected /* async */ releasePending() {
//return; //return;
if(!this.listenPorts.length || this.releasingPending) { if(!this.listenPorts.length || this.releasingPending) {
@ -217,28 +254,28 @@ export default class SuperMessagePort<
this.releasingPending = true; this.releasingPending = true;
// const perf = performance.now(); // const perf = performance.now();
await pause(0); // await pause(0);
this.debug && this.log.debug('releasing tasks, length:', this.pending.size/* , performance.now() - perf */); this.debug && this.log.debug('releasing tasks, length:', this.pending.size/* , performance.now() - perf */);
this.pending.forEach((portTasks, port) => { this.pending.forEach((portTasks, port) => {
let batchTask: BatchTask; // let batchTask: BatchTask;
const tasks: Task[] = []; // const tasks: Task[] = [];
portTasks.forEach((task) => { // portTasks.forEach((task) => {
if(task.transfer) { // if(task.transfer) {
batchTask = undefined; // batchTask = undefined;
tasks.push(task); // tasks.push(task);
} else { // } else {
if(!batchTask) { // if(!batchTask) {
batchTask = this.createTask('batch', []); // batchTask = this.createTask('batch', []);
tasks.push(batchTask); // tasks.push(batchTask);
} // }
batchTask.payload.push(task); // batchTask.payload.push(task);
} // }
}); // });
// const tasks = portTasks; const tasks = portTasks;
tasks.forEach((task) => { tasks.forEach((task) => {
// if(task.type === 'batch') { // if(task.type === 'batch') {
@ -263,7 +300,7 @@ export default class SuperMessagePort<
this.releasingPending = false; this.releasingPending = false;
} }
protected processResultTask(task: ResultTask) { protected processResultTask = (task: ResultTask) => {
const {taskId, result, error} = task.payload; const {taskId, result, error} = task.payload;
const deferred = this.awaiting[taskId]; const deferred = this.awaiting[taskId];
if(!deferred) { if(!deferred) {
@ -271,11 +308,11 @@ export default class SuperMessagePort<
} }
this.debug && this.log.debug('done', deferred.taskType, result, error); this.debug && this.log.debug('done', deferred.taskType, result, error);
error ? deferred.reject(error) : deferred.resolve(result); 'error' in task.payload ? deferred.reject(error) : deferred.resolve(result);
delete this.awaiting[taskId]; delete this.awaiting[taskId];
} };
protected processAckTask(task: AckTask) { protected processAckTask = (task: AckTask) => {
const payload = task.payload; const payload = task.payload;
const deferred = this.awaiting[payload.taskId]; const deferred = this.awaiting[payload.taskId];
if(!deferred) { if(!deferred) {
@ -316,21 +353,25 @@ export default class SuperMessagePort<
}; };
previousResolve(ret); previousResolve(ret);
} };
protected processPingTask(task: PingTask, source: MessageEventSource, event: MessageEvent) { protected processPingTask = (task: PingTask, source: MessageEventSource, event: MessageEvent) => {
this.pushTask(this.createTask('pong', undefined), event.source); this.pushTask(this.createTask('pong', undefined), event.source);
} };
protected processPongTask(task: PongTask, source: MessageEventSource, event: MessageEvent) { protected processPongTask = (task: PongTask, source: MessageEventSource, event: MessageEvent) => {
const pingResolve = this.pingResolves.get(source); const pingResolve = this.pingResolves.get(source);
if(pingResolve) { if(pingResolve) {
this.pingResolves.delete(source); this.pingResolves.delete(source);
pingResolve(); pingResolve();
} }
} };
protected processCloseTask = (task: CloseTask, source: MessageEventSource, event: MessageEvent) => {
this.detachPort(source);
};
protected async processInvokeTask(task: InvokeTask, source: MessageEventSource, event: MessageEvent) { protected processInvokeTask = async(task: InvokeTask, source: MessageEventSource, event: MessageEvent) => {
const id = task.id; const id = task.id;
const innerTask = task.payload; const innerTask = task.payload;
@ -397,7 +438,7 @@ export default class SuperMessagePort<
} }
this.pushTask(resultTask, source); this.pushTask(resultTask, source);
} };
protected createTask<T extends Task['type'], K extends Task = Parameters<TaskMap[T]>[0]>(type: T, payload: K['payload'], transfer?: Transferable[]): K { protected createTask<T extends Task['type'], K extends Task = Parameters<TaskMap[T]>[0]>(type: T, payload: K['payload'], transfer?: Transferable[]): K {
return { return {

29
src/lib/mtproto/transports/obfuscation.ts

@ -17,6 +17,8 @@ export default class Obfuscation {
private dec: aesjs.ModeOfOperation.ModeOfOperationCTR; */ private dec: aesjs.ModeOfOperation.ModeOfOperationCTR; */
private id: number; private id: number;
private idPromise: Promise<Obfuscation['id']>;
private process: (data: Uint8Array, operation: 'encrypt' | 'decrypt') => ReturnType<Obfuscation['_process']>;
// private cryptoEncKey: CryptoKey; // private cryptoEncKey: CryptoKey;
// private cryptoDecKey: CryptoKey; // private cryptoDecKey: CryptoKey;
@ -28,7 +30,7 @@ export default class Obfuscation {
// private decIvCounter: Counter; // private decIvCounter: Counter;
public async init(codec: Codec) { public async init(codec: Codec) {
if(this.id !== undefined) { if(this.idPromise !== undefined) {
this.release(); this.release();
} }
@ -69,12 +71,21 @@ export default class Obfuscation {
// console.log('encKey', encKey.hex, encIv.hex); // console.log('encKey', encKey.hex, encIv.hex);
// console.log('decKey', decKey.hex, decIv.hex); // console.log('decKey', decKey.hex, decIv.hex);
this.id = await cryptoMessagePort.invokeCrypto('aes-ctr-prepare', { const idPromise = this.idPromise = cryptoMessagePort.invokeCrypto('aes-ctr-prepare', {
encKey, encKey,
encIv, encIv,
decKey, decKey,
decIv decIv
}); });
this.process = async(data, operation) => {
await idPromise;
return this._process(data, operation);
};
this.id = await idPromise;
this.process = this._process;
// this.decIvCounter = new Counter(this.decIv); // this.decIvCounter = new Counter(this.decIv);
/* const key = this.cryptoEncKey = await subtle.importKey( /* const key = this.cryptoEncKey = await subtle.importKey(
@ -145,12 +156,12 @@ export default class Obfuscation {
return res; return res;
} */ } */
private process(data: Uint8Array, operation: 'encrypt' | 'decrypt') { private _process = (data: Uint8Array, operation: 'encrypt' | 'decrypt') => {
return cryptoMessagePort.invoke('invoke', { return cryptoMessagePort.invoke('invoke', {
method: 'aes-ctr-process', method: 'aes-ctr-process',
args: [{id: this.id, data, operation}], args: [{id: this.id, data, operation}],
}, undefined, undefined, [data.buffer]) as Promise<Uint8Array>; }, undefined, undefined, [data.buffer]) as Promise<Uint8Array>;
} };
public encode(payload: Uint8Array) { public encode(payload: Uint8Array) {
/* return subtle.encrypt({ /* return subtle.encrypt({
@ -168,13 +179,17 @@ export default class Obfuscation {
return this.process(payload, 'decrypt'); return this.process(payload, 'decrypt');
} }
public release() { public async release() {
if(this.id === undefined) { const idPromise = this.idPromise;
if(idPromise === undefined) {
return; return;
} }
cryptoMessagePort.invokeCrypto('aes-ctr-destroy', this.id);
this.id = undefined; this.id = undefined;
this.idPromise = undefined;
const id = await idPromise;
cryptoMessagePort.invokeCrypto('aes-ctr-destroy', id);
} }
public destroy() { public destroy() {

32
src/lib/mtproto/webPushApiManager.ts

@ -12,14 +12,15 @@
import type { ServiceWorkerNotificationsClearTask, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "../serviceWorker/index.service"; import type { ServiceWorkerNotificationsClearTask, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "../serviceWorker/index.service";
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import { logger } from "../logger"; import { logger } from "../logger";
import rootScope from "../rootScope"; import apiManagerProxy from "./mtprotoworker";
import apiManager from "./mtprotoworker";
import I18n, { LangPackKey } from "../langPack"; import I18n, { LangPackKey } from "../langPack";
import { IS_MOBILE } from "../../environment/userAgent"; import { IS_MOBILE } from "../../environment/userAgent";
import appRuntimeManager from "../appManagers/appRuntimeManager"; import appRuntimeManager from "../appManagers/appRuntimeManager";
import copy from "../../helpers/object/copy"; import copy from "../../helpers/object/copy";
import type { NotificationSettings } from "../appManagers/uiNotificationsManager"; import type { NotificationSettings } from "../appManagers/uiNotificationsManager";
import idleController from "../../helpers/idleController"; import singleInstance from "./singleInstance";
import EventListenerBase from "../../helpers/eventListenerBase";
import type { PushNotificationObject } from "../serviceWorker/push";
export type PushSubscriptionNotifyType = 'init' | 'subscribe' | 'unsubscribe'; export type PushSubscriptionNotifyType = 'init' | 'subscribe' | 'unsubscribe';
export type PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`; export type PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`;
@ -29,7 +30,12 @@ export type PushSubscriptionNotify = {
tokenValue: string tokenValue: string
}; };
export class WebPushApiManager { export class WebPushApiManager extends EventListenerBase<{
push_notification_click: (n: PushNotificationObject) => void,
push_init: (n: PushSubscriptionNotify) => void,
push_subscribe: (n: PushSubscriptionNotify) => void,
push_unsubscribe: (n: PushSubscriptionNotify) => void
}> {
public isAvailable = true; public isAvailable = true;
private isPushEnabled = false; private isPushEnabled = false;
private localNotificationsAvailable = true; private localNotificationsAvailable = true;
@ -41,6 +47,8 @@ export class WebPushApiManager {
private log = logger('PM'); private log = logger('PM');
constructor() { constructor() {
super(false);
if(!('PushManager' in window) || if(!('PushManager' in window) ||
!('Notification' in window) || !('Notification' in window) ||
!('serviceWorker' in navigator)) { !('serviceWorker' in navigator)) {
@ -156,7 +164,7 @@ export class WebPushApiManager {
} }
public isAliveNotify = () => { public isAliveNotify = () => {
if(!this.isAvailable || idleController.idle?.deactivated) { if(!this.isAvailable || singleInstance.deactivatedReason) {
return; return;
} }
@ -182,7 +190,7 @@ export class WebPushApiManager {
} }
}; };
apiManager.postSWMessage(task); apiManagerProxy.postSWMessage(task);
this.isAliveTO = setTimeout(this.isAliveNotify, 10000); this.isAliveTO = setTimeout(this.isAliveNotify, 10000);
} }
@ -199,7 +207,7 @@ export class WebPushApiManager {
} }
const task: ServiceWorkerNotificationsClearTask = {type: 'notifications_clear'}; const task: ServiceWorkerNotificationsClearTask = {type: 'notifications_clear'};
apiManager.postSWMessage(task); apiManagerProxy.postSWMessage(task);
} }
public setUpServiceWorkerChannel() { public setUpServiceWorkerChannel() {
@ -207,13 +215,13 @@ export class WebPushApiManager {
return; return;
} }
apiManager.addServiceWorkerTaskListener('push_click', (task: ServiceWorkerPushClickTask) => { apiManagerProxy.addServiceWorkerTaskListener('push_click', (task: ServiceWorkerPushClickTask) => {
if(idleController.idle?.deactivated) { if(singleInstance.deactivatedReason) {
appRuntimeManager.reload(); appRuntimeManager.reload();
return; return;
} }
rootScope.dispatchEvent('push_notification_click', task.payload); this.dispatchEvent('push_notification_click', task.payload);
}); });
navigator.serviceWorker.ready.then(this.isAliveNotify); navigator.serviceWorker.ready.then(this.isAliveNotify);
@ -235,13 +243,13 @@ export class WebPushApiManager {
} }
this.log.warn('Push', event, subscriptionObj); this.log.warn('Push', event, subscriptionObj);
rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, { this.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, {
tokenType: 10, tokenType: 10,
tokenValue: JSON.stringify(subscriptionObj) tokenValue: JSON.stringify(subscriptionObj)
}); });
} else { } else {
this.log.warn('Push', event, false); this.log.warn('Push', event, false);
rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, false as any); this.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, false as any);
} }
} }
} }

24
src/lib/rootScope.ts

@ -10,8 +10,6 @@ import type { MyDialogFilter } from "./storages/filters";
import type { Folder } from "./storages/dialogs"; import type { Folder } from "./storages/dialogs";
import type { UserTyping } from "./appManagers/appProfileManager"; import type { UserTyping } from "./appManagers/appProfileManager";
import type { MyDraftMessage } from "./appManagers/appDraftsManager"; import type { MyDraftMessage } from "./appManagers/appDraftsManager";
import type { PushSubscriptionNotify } from "./mtproto/webPushApiManager";
import type { PushNotificationObject } from "./serviceWorker/push";
import type { ConnectionStatusChange } from "./mtproto/connectionStatus"; import type { ConnectionStatusChange } from "./mtproto/connectionStatus";
import type { GroupCallId } from "./appManagers/appGroupCallsManager"; import type { GroupCallId } from "./appManagers/appGroupCallsManager";
import type { AppManagers } from "./appManagers/managers"; import type { AppManagers } from "./appManagers/managers";
@ -67,7 +65,6 @@ export type BroadcastEvents = {
'history_delete': {peerId: PeerId, msgs: Set<number>}, 'history_delete': {peerId: PeerId, msgs: Set<number>},
'history_forbidden': PeerId, 'history_forbidden': PeerId,
'history_reload': PeerId, 'history_reload': PeerId,
'history_focus': {peerId: PeerId, threadId?: number, mid?: number, startParam?: string},
//'history_request': void, //'history_request': void,
'message_edit': {storageKey: MessagesStorageKey, peerId: PeerId, mid: number, message: MyMessage}, 'message_edit': {storageKey: MessagesStorageKey, peerId: PeerId, mid: number, message: MyMessage},
@ -104,8 +101,6 @@ export type BroadcastEvents = {
'settings_updated': {key: string, value: any, settings: State['settings']}, 'settings_updated': {key: string, value: any, settings: State['settings']},
'draft_updated': {peerId: PeerId, threadId: number, draft: MyDraftMessage | undefined, force?: boolean}, 'draft_updated': {peerId: PeerId, threadId: number, draft: MyDraftMessage | undefined, force?: boolean},
'im_tab_change': number,
'background_change': void, 'background_change': void,
'privacy_update': Update.updatePrivacy, 'privacy_update': Update.updatePrivacy,
@ -115,21 +110,12 @@ export type BroadcastEvents = {
'notification_reset': string, 'notification_reset': string,
'notification_cancel': string, 'notification_cancel': string,
'notification_build': {
message: Message.message | Message.messageService,
fwdCount?: number,
peerReaction?: MessagePeerReaction,
peerTypeNotifySettings?: PeerNotifySettings
},
'language_change': string, 'language_change': string,
'theme_change': void, 'theme_change': void,
'push_notification_click': PushNotificationObject, 'media_play': void,
'push_init': PushSubscriptionNotify,
'push_subscribe': PushSubscriptionNotify,
'push_unsubscribe': PushSubscriptionNotify,
'emoji_recent': string, 'emoji_recent': string,
@ -153,9 +139,11 @@ export type BroadcastEvents = {
'logging_out': void 'logging_out': void
}; };
export class RootScope extends EventListenerBase<{ export type BroadcastEventsListeners = {
[name in keyof BroadcastEvents]: (e: BroadcastEvents[name]) => void [name in keyof BroadcastEvents]: (e: BroadcastEvents[name]) => void
}> { };
export class RootScope extends EventListenerBase<BroadcastEventsListeners> {
public myId: PeerId = NULL_PEER_ID; public myId: PeerId = NULL_PEER_ID;
private connectionStatus: {[name: string]: ConnectionStatusChange} = {}; private connectionStatus: {[name: string]: ConnectionStatusChange} = {};
public settings: State['settings']; public settings: State['settings'];

3
src/lib/serviceWorker/index.service.ts

@ -94,7 +94,8 @@ const taskListeners: {
} }
}, },
toggleStorages: (task: ToggleStorageTask) => { toggleStorages: (task: ToggleStorageTask) => {
CacheStorageController.toggleStorage(task.payload); const {enabled, clearWrite} = task.payload;
CacheStorageController.toggleStorage(enabled, clearWrite);
} }
}; };
ctx.addEventListener('message', (e) => { ctx.addEventListener('message', (e) => {

3
src/lib/sessionStorage.ts

@ -30,7 +30,8 @@ const sessionStorage = new LocalStorageController<{
tgme_sync: { tgme_sync: {
canRedirect: boolean, canRedirect: boolean,
ts: number ts: number
} },
k_build: number
}>(/* ['kz_version'] */); }>(/* ['kz_version'] */);
MOUNT_CLASS_TO.appStorage = sessionStorage; MOUNT_CLASS_TO.appStorage = sessionStorage;
export default sessionStorage; export default sessionStorage;

4
src/lib/storage.ts

@ -292,11 +292,11 @@ export default class AppStorage<
return this.storage.clear().catch(noop); return this.storage.clear().catch(noop);
} }
public static toggleStorage(enabled: boolean) { public static toggleStorage(enabled: boolean, clearWrite: boolean) {
return Promise.all(this.STORAGES.map((storage) => { return Promise.all(this.STORAGES.map((storage) => {
storage.useStorage = enabled; storage.useStorage = enabled;
if(!IS_WORKER) { if(!IS_WORKER || !clearWrite) {
return; return;
} }

10
src/lib/storages/filters.ts

@ -442,11 +442,11 @@ export default class FiltersStorage extends AppManager {
public saveDialogFilter(filter: DialogFilter, update = true) { public saveDialogFilter(filter: DialogFilter, update = true) {
// defineNotNumerableProperties(filter, ['includePeerIds', 'excludePeerIds', 'pinnedPeerIds']); // defineNotNumerableProperties(filter, ['includePeerIds', 'excludePeerIds', 'pinnedPeerIds']);
if(filter._ === 'dialogFilterDefault') { // if(filter._ === 'dialogFilterDefault') {
return; // return;
// filter = this.getLocalFilter(0); // // filter = this.getLocalFilter(0);
// delete filter.orderIndex; // // delete filter.orderIndex;
} // }
assumeType<MyDialogFilter>(filter); assumeType<MyDialogFilter>(filter);
convertment.forEach(([from, to]) => { convertment.forEach(([from, to]) => {

2
src/pages/pageSignIn.ts

@ -341,7 +341,7 @@ let onFirstMount = () => {
const keepSigned = signedCheckboxField.checked; const keepSigned = signedCheckboxField.checked;
rootScope.managers.appStateManager.pushToState('keepSigned', keepSigned); rootScope.managers.appStateManager.pushToState('keepSigned', keepSigned);
apiManagerProxy.toggleStorages(keepSigned); apiManagerProxy.toggleStorages(keepSigned, true);
}); });
apiManagerProxy.getState().then((state) => { apiManagerProxy.getState().then((state) => {

4
src/scripts/in/schema.json

File diff suppressed because one or more lines are too long

2
src/scripts/out/schema.json

File diff suppressed because one or more lines are too long

6
src/scss/partials/_chatBubble.scss

@ -946,6 +946,12 @@ $bubble-beside-button-width: 38px;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
@include animation-level(2) {
.bubble-content-wrapper {
transition: var(--bubble-transition-in);
}
}
&.has-description { &.has-description {
.service-msg { .service-msg {
flex-direction: column; flex-direction: column;

Loading…
Cancel
Save