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 { @@ -191,7 +191,7 @@ export class AnimationIntersector {
this.visible.has(player) &&
animation.autoplay &&
(!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);
animation.play();

24
src/components/appMediaPlaybackController.ts

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

6
src/components/appMediaViewer.ts

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

21
src/components/appSearchSuper..ts

@ -213,9 +213,9 @@ class SearchContextMenu { @@ -213,9 +213,9 @@ class SearchContextMenu {
}
private onGotoClick = () => {
rootScope.dispatchEvent('history_focus', {
appImManager.setInnerPeer({
peerId: this.peerId,
mid: this.mid,
lastMsgId: this.mid,
threadId: this.searchSuper.searchContext.threadId
});
};
@ -597,13 +597,26 @@ export default class AppSearchSuper { @@ -597,13 +597,26 @@ export default class AppSearchSuper {
}
private processEmptyFilter({message, searchGroup}: ProcessSearchSuperResult) {
const loadPromises: Promise<any>[] = [];
const {dom} = appDialogsManager.addDialogNew({
peerId: message.peerId,
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) {

2
src/components/avatar.ts

@ -198,7 +198,7 @@ export default class AvatarElement extends HTMLElement { @@ -198,7 +198,7 @@ export default class AvatarElement extends HTMLElement {
private r(onlyThumb = false) {
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) {
this.loadPromises.push(promise);

374
src/components/chat/bubbleGroups.ts

@ -16,16 +16,19 @@ import { Message } from "../../layer"; @@ -16,16 +16,19 @@ import { Message } from "../../layer";
import { NULL_PEER_ID, REPLIES_PEER_ID } from "../../lib/mtproto/mtproto_config";
import { SERVICE_AS_REGULAR, STICKY_OFFSET } from "./bubbles";
import forEachReverse from "../../helpers/array/forEachReverse";
import partition from "../../helpers/array/partition";
type GroupItem = {
bubble: HTMLElement,
fromId: PeerId,
mid: number,
groupMid?: number,
timestamp: number,
dateTimestamp: number,
mounted: boolean,
single: boolean,
group?: BubbleGroup
group?: BubbleGroup,
message: Message.message | Message.messageService // use it only to set avatar
};
class BubbleGroup {
@ -38,6 +41,7 @@ class BubbleGroup { @@ -38,6 +41,7 @@ class BubbleGroup {
avatar: AvatarElement;
mounted: boolean;
dateTimestamp: number;
offset: number;
constructor(chat: Chat, groups: BubbleGroups, dateTimestamp: number) {
this.container = document.createElement('div');
@ -46,6 +50,7 @@ class BubbleGroup { @@ -46,6 +50,7 @@ class BubbleGroup {
this.groups = groups;
this.items = [];
this.dateTimestamp = dateTimestamp;
this.offset = 0;
}
createAvatar(message: Message.message | Message.messageService) {
@ -57,6 +62,7 @@ class BubbleGroup { @@ -57,6 +62,7 @@ class BubbleGroup {
this.avatarContainer = document.createElement('div');
this.avatarContainer.classList.add('bubbles-group-avatar-container');
++this.offset;
const fwdFrom = message.fwd_from;
const fwdFromId = message.fwdFromId;
@ -88,17 +94,29 @@ class BubbleGroup { @@ -88,17 +94,29 @@ class BubbleGroup {
return this.items[this.items.length - 1];
}
get lastMid() {
return this.lastItem.mid;
}
get lastItem() {
return this.items[0];
}
updateClassNames() {
const items = this.items;
if(!items.length) {
const length = items.length;
if(!length) {
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;
if(items.length === 1) {
@ -111,7 +129,7 @@ class BubbleGroup { @@ -111,7 +129,7 @@ class BubbleGroup {
//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;
bubble.classList.remove('is-group-last', 'is-group-first');
//this.setClipIfNeeded(bubble, true);
@ -145,33 +163,51 @@ class BubbleGroup { @@ -145,33 +163,51 @@ class BubbleGroup {
items.splice(i, 0, item);
} else {
insertInDescendSortedArray(items, item, 'mid');
// insertInDescendSortedArray(items, item, 'mid');
insertInDescendSortedArray(items, item, 'groupMid');
}
item.group = this;
if(items.length === 1) {
insertInDescendSortedArray(this.groups.groups, this, 'firstMid');
insertInDescendSortedArray(this.groups.groups, this, 'lastMid');
}
}
mount() {
if(!this.groups.groups.includes(this)) { // group can be already removed
return;
removeItem(item: GroupItem) {
indexOfAndSplice(this.items, item);
if(!this.items.length) {
indexOfAndSplice(this.groups.groups, this);
}
const offset = this.avatar ? 1 : 0;
const items = this.items;
item.group = undefined;
}
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;
forEachReverse(items, (item, idx) => {
this.mountItem(item, length - 1 - idx, offset);
});
if(updateClassNames) {
this.updateClassNames();
}
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) {
return;
}
@ -181,11 +217,13 @@ class BubbleGroup { @@ -181,11 +217,13 @@ class BubbleGroup {
}
unmountItem(item: GroupItem) {
if(item.mounted) {
item.bubble.remove();
item.mounted = false;
this.onItemUnmount();
if(!item.mounted) {
return;
}
item.bubble.remove();
item.mounted = false;
this.onItemUnmount();
}
onItemMount() {
@ -234,7 +272,7 @@ class BubbleGroup { @@ -234,7 +272,7 @@ class BubbleGroup {
// }
export default class BubbleGroups {
private itemsArr: Array<GroupItem> = []; // descend sorted
public itemsArr: Array<GroupItem> = []; // descend sorted
private itemsMap: Map<HTMLElement, GroupItem> = new Map();
public groups: Array<BubbleGroup> = []; // descend sorted
private newGroupDiff = 121; // * 121 in scheduled messages
@ -243,32 +281,69 @@ export default class BubbleGroups { @@ -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);
if(!item) {
return;
}
const items = this.itemsArr;
const index = items.indexOf(item);
const siblings = this.getSiblingsAtIndex(index, items);
const group = item.group;
const items = group.items;
if(items.length) {
indexOfAndSplice(items, item);
if(!items.length) {
indexOfAndSplice(this.groups, group);
}
this.removeItem(item);
group.unmountItem(item);
const modifiedGroups: Set<BubbleGroup> = new Set();
modifiedGroups.add(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.itemsMap.delete(bubble);
return item;
this.mountUnmountGroups(Array.from(modifiedGroups));
}
removeAndUnmountBubble(bubble: HTMLElement) {
const item = this.removeBubble(bubble);
if(item) {
item.group.unmountItem(item);
mountUnmountGroups(groups: BubbleGroup[]) {
// groups.sort((a, b) => (b.lastItem?.mid ?? 0) - (a.lastItem?.mid ?? 0));
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 { @@ -280,20 +355,22 @@ export default class BubbleGroups {
return this.groups[0];
}
// changeBubbleMid(bubble: HTMLElement, mid: number) {
// const item = this.getItemByBubble(bubble);
// if(!item) {
// return;
// }
// item.mid = mid;
// // indexOfAndSplice(item.group.items, item);
// // item.group.insertItem(item);
changeBubbleMid(bubble: HTMLElement, mid: number) {
const item = this.getItemByBubble(bubble);
if(!item) {
return;
}
// indexOfAndSplice(this.itemsArr, item);
// insertInDescendSortedArray(this.itemsArr, item, 'mid');
// }
item.mid = 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) {
this.itemsMap.delete(item.bubble);
@ -310,45 +387,51 @@ export default class BubbleGroups { @@ -310,45 +387,51 @@ export default class BubbleGroups {
this.changeItemBubble(item, to);
}
/**
*
* @param item
* @param items expect descend sorted array
* @returns
*/
findIndexForItemInItems(item: GroupItem, items: GroupItem[]) {
let foundAtIndex = -1;
for(let i = 0, length = items.length; i < length; ++i) {
const _item = items[i];
const diff = Math.abs(_item.timestamp - item.timestamp);
const good = _item.fromId === item.fromId
&& diff <= this.newGroupDiff
&& item.dateTimestamp === _item.dateTimestamp
&& !item.single
&& !_item.single;
if(good) {
foundAtIndex = i;
if(this.chat.type === 'scheduled') {
break;
}
} else {
foundAtIndex = -1;
}
canItemsBeGrouped(item1: GroupItem, item2: GroupItem) {
return item2.fromId === item1.fromId
&& Math.abs(item2.timestamp - item1.timestamp) <= this.newGroupDiff
&& item1.dateTimestamp === item2.dateTimestamp
&& !item1.single
&& !item2.single;
}
getSiblingsAtIndex(itemIndex: number, items: GroupItem[]) {
return [items[itemIndex - 1], items[itemIndex + 1]] as const;
}
// findGroupSiblingInSiblings(item: GroupItem, siblings: ReturnType<BubbleGroups['getSiblingsAtIndex']>) {
// return siblings.find((sibling) => sibling && this.canItemsBeGrouped(item, sibling));
// }
findGroupSiblingByItem(item: GroupItem, items: GroupItem[]) {
items = items.slice();
const idx = insertInDescendSortedArray(items, item, 'mid');
// return this.findGroupSiblingInSiblings(item, this.getSiblingsAtIndex(idx, items));
return this.findGroupSiblingInItems(item, items, idx);
}
if(this.chat.type !== 'scheduled') {
if(item.mid > _item.mid) {
findGroupSiblingInItems(item: GroupItem, items: GroupItem[], index = items.indexOf(item), length = items.length) {
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;
}
}
}
return foundAtIndex;
return siblingGroupedItem;
}
addItemToGroup(item: GroupItem, group: BubbleGroup) {
item.group = group;
group.insertItem(item);
this.addItemToCache(item);
}
@ -358,6 +441,11 @@ export default class BubbleGroups { @@ -358,6 +441,11 @@ export default class BubbleGroups {
this.itemsMap.set(item.bubble, item);
}
removeItemFromCache(item: GroupItem) {
indexOfAndSplice(this.itemsArr, item);
this.itemsMap.delete(item.bubble);
}
getMessageFromId(message: MyMessage) {
let fromId = message.viaBotId || message.fromId;
@ -374,55 +462,113 @@ export default class BubbleGroups { @@ -374,55 +462,113 @@ export default class BubbleGroups {
const {mid, date: timestamp} = message;
const {dateTimestamp} = this.chat.bubbles.getDateForDateContainer(timestamp);
const item: GroupItem = {
bubble,
fromId: this.getMessageFromId(message),
mid,
groupMid: mid,
fromId: this.getMessageFromId(message),
bubble,
timestamp,
dateTimestamp,
mounted: false,
single: single
single,
message
};
return item;
}
// prepareForGrouping(bubble: HTMLElement, message: MyMessage) {
// const item = this.createItem(bubble, message);
// this.addItemToCache(item);
// }
splitSiblingsOnGrouping(siblings: ReturnType<BubbleGroups['getSiblingsAtIndex']>) {
const [previousSibling, nextSibling] = siblings;
const previousGroup = previousSibling?.group;
const nextGroup = nextSibling?.group;
// groupUngrouped() {
// const items = this.itemsArr;
// const length = items.length;
// for(let i = length - 1; i >= 0; --i) {
// const item = items[i];
// if(item.gr)
// }
// }
if(!previousGroup) {
return;
}
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;
// will refresh group
// if(previousGroup === nextGroup) {
const items = previousGroup.items;
const index = items.indexOf(previousSibling) + 1;
const length = items.length;
if(index === length) {
return;
}
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);
this.addItemToCache(item);
}
const foundAtIndex = this.findIndexForItemInItems(item, this.itemsArr);
const foundItem = this.itemsArr[foundAtIndex];
groupUngrouped() {
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);
this.addItemToGroup(item, group);
return {item, group};
let hadGroup = true;
const siblings = this.getSiblingsAtIndex(i, items);
const siblingGroupedItem = this.findGroupSiblingInItems(item, items, i, length);
// 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) {
//console.log('setClipIfNeeded', bubble, remove);
const className = bubble.className;
@ -485,4 +631,18 @@ export default class BubbleGroups { @@ -485,4 +631,18 @@ export default class BubbleGroups {
this.groups = [];
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[]) { @@ -156,7 +156,7 @@ function getMainMidForGrouped(mids: number[]) {
export default class ChatBubbles {
public container: HTMLDivElement;
private chatInner: HTMLDivElement;
public chatInner: HTMLDivElement;
public scrollable: Scrollable;
private getHistoryTopPromise: Promise<boolean>;
@ -248,11 +248,14 @@ export default class ChatBubbles { @@ -248,11 +248,14 @@ export default class ChatBubbles {
private attachPlaceholderOnRender: () => void;
private bubblesToEject: Set<HTMLElement> = new Set();
private bubblesToReplace: Set<HTMLElement> = new Set();
private updatePlaceholderPosition: () => void;
private setPeerOptions: {lastMsgId: number; topMessage: number;};
private setPeerTempId: number = 0;
private renderNewPromises: Set<Promise<any>> = new Set();
// private reactions: Map<number, ReactionsElement>;
constructor(
@ -280,62 +283,104 @@ export default class ChatBubbles { @@ -280,62 +283,104 @@ export default class ChatBubbles {
// * events
// 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) {
return;
}
const log = false ? this.log.bindPrefix('history_update-' + mid) : undefined;
log && log('start');
const bubble = this.bubbles[mid];
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);
if(!item) return; // probably a group item
const unmountIfFound = !sequential || this.bubbleGroups.getLastGroup() !== item.group;
if(unmountIfFound) {
const groupIndex = this.bubbleGroups.groups.indexOf(item.group);
this.bubbleGroups.removeAndUnmountBubble(bubble);
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;
}
if(!item) { // probably a group item
log && log.error('no item by bubble', bubble);
return;
} else if(item.mid === mid) {
log && log.warn('wow what', item, mid);
return;
}
this.bubbleGroups.removeAndUnmountBubble(groupItem.bubble);
this.bubbleGroups.addItemToGroup(groupItem, nextGroup);
moved = true;
}
const group = item.group;
const newItem = this.bubbleGroups.createItem(bubble, message);
// 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) {
nextGroup.mount();
}
// return;
if(_break) {
break;
}
}
}
// await fastRafPromise();
// if(this.bubbles[mid] !== bubble) return;
const {group} = this.setBubblePosition(bubble, message, unmountIfFound);
group.mount();
// const groupIndex = this.bubbleGroups.groups.indexOf(group);
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) {
this.scrollToEnd();
}
}/* else {
this.bubbleGroups.changeBubbleMid(bubble, mid);
} */
const {groups} = this.groupBubbles([{bubble, message}]);
this.bubbleGroups.mountUnmountGroups(groups);
if(this.scrollingToBubble) {
this.scrollToEnd();
}
log && log('end');
// this.bubbleGroups.findIncorrentPositions();
});
this.listenerSetter.add(rootScope)('dialog_flush', ({peerId}) => {
@ -358,6 +403,7 @@ export default class ChatBubbles { @@ -358,6 +403,7 @@ export default class ChatBubbles {
if(_bubble) {
const bubble = bubbles[tempId];
bubbles[mid] = bubble;
bubble.dataset.mid = '' + mid;
delete bubbles[tempId];
fastRaf(() => {
@ -367,8 +413,6 @@ export default class ChatBubbles { @@ -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.dataset.mid = '' + mid;
}
if(this.unreadOut.has(tempId)) {
@ -385,6 +429,10 @@ export default class ChatBubbles { @@ -385,6 +429,10 @@ export default class ChatBubbles {
}
}
if(!_bubble) {
return;
}
let messages: (Message.message | Message.messageService)[], tempIds: number[];
const groupedId = (message as Message.message).grouped_id;
if(groupedId) {
@ -1261,7 +1309,7 @@ export default class ChatBubbles { @@ -1261,7 +1309,7 @@ export default class ChatBubbles {
if(this.readPromise) return;
const middleware = this.getMiddleware();
this.readPromise = idleController.idle.focusPromise.then(async() => {
this.readPromise = idleController.getFocusPromise().then(async() => {
if(!middleware()) return;
let maxId = Math.max(...Array.from(this.unreadedSeen));
@ -2112,8 +2160,17 @@ export default class ChatBubbles { @@ -2112,8 +2160,17 @@ export default class ChatBubbles {
} : 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
//this.log('renderNewMessagesByIds: seems search is active, skipping render:', mids);
const setPeerPromise = this.chat.setPeerPromise;
@ -2131,7 +2188,7 @@ export default class ChatBubbles { @@ -2131,7 +2188,7 @@ export default class ChatBubbles {
if(this.chat.threadId) {
mids = await filterAsync(mids, async(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;
});
}
@ -2179,6 +2236,8 @@ export default class ChatBubbles { @@ -2179,6 +2236,8 @@ export default class ChatBubbles {
}, 10); */
});
}
return promise;
}
public getLastBubble() {
@ -2194,6 +2253,10 @@ export default class ChatBubbles { @@ -2194,6 +2253,10 @@ export default class ChatBubbles {
) {
const bubble = findUpClassName(element, 'bubble');
if(!element.parentElement) {
this.log.error('element is not connected', bubble);
}
let fallbackToElementStartWhenCentering: HTMLElement;
// * if it's a start, then scroll to start of the group
if(bubble && position !== 'end') {
@ -2454,6 +2517,7 @@ export default class ChatBubbles { @@ -2454,6 +2517,7 @@ export default class ChatBubbles {
this.unreadOut.clear();
this.needUpdate.length = 0;
this.lazyLoadQueue.clear();
this.renderNewPromises.clear();
// clear messages
if(bubblesToo) {
@ -2501,6 +2565,7 @@ export default class ChatBubbles { @@ -2501,6 +2565,7 @@ export default class ChatBubbles {
this.renderingMessages.clear();
this.bubblesToEject.clear();
this.bubblesToReplace.clear();
// this.reactions.clear();
@ -3012,7 +3077,9 @@ export default class ChatBubbles { @@ -3012,7 +3077,9 @@ export default class ChatBubbles {
const possibleError = PEER_CHANGED_ERROR;
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();
this.messagesQueue.length = 0;
@ -3025,15 +3092,27 @@ export default class ChatBubbles { @@ -3025,15 +3092,27 @@ export default class ChatBubbles {
return promise;
});
const loadQueue = (await m(Promise.all(renderQueuePromises))).filter(Boolean);
log.warn('messages rendered');
let loadQueue = await m(Promise.all(renderQueuePromises));
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();
const promises = loadQueue.reduce((acc, details) => {
if(!details || this.bubbles[details.message.mid] !== details.bubble) {
return acc; // message was deleted during rendering
}
loadQueue = filterQueue(loadQueue);
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 promises = details.promises.slice();
@ -3046,32 +3125,34 @@ export default class ChatBubbles { @@ -3046,32 +3125,34 @@ export default class ChatBubbles {
log.groupEnd();
});
if(details.updatePosition) {
const res = this.setBubblePosition(details.bubble, details.message, false);
if(res) {
groups.add(res.group);
if(details.needAvatar) {
details.promises.push(res.group.createAvatar(details.message));
}
}
}
// if(details.updatePosition) {
// if(res) {
// groups.add(res.group);
// if(details.needAvatar) {
// details.promises.push(res.group.createAvatar(details.message));
// }
// }
// }
acc.push(...details.promises);
return acc;
}, [] as Promise<any>[]);
promises.push(...avatarPromises);
// promises.push(pause(200));
// * это нужно для того, чтобы если захочет подгрузить reply или какое-либо сообщение, то скролл не прервался
// * если добавить этот промис - в таком случае нужно сделать, чтобы скроллило к последнему сообщению после рендера
// 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(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) {
// this.messagesQueueOnRender();
// }
@ -3081,35 +3162,58 @@ export default class ChatBubbles { @@ -3081,35 +3162,58 @@ export default class ChatBubbles {
}
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) {
loadQueue.forEach(({message, bubble}) => {
if(this.bubbles[message.mid] !== bubble) { // maybe message was deleted during rendering
return;
}
loadQueue.forEach(({bubble}) => {
this.chat.selection.toggleElementCheckbox(bubble, true);
});
}
// const sortedGroups = Array.from(groups).sort((a, b) => a.firstTimestamp - b.firstTimestamp);
// for(const group of sortedGroups) {
for(const group of groups) {
group.mount();
}
loadQueue.forEach(({message, bubble, updatePosition}) => {
if(message.pFlags.local && updatePosition) {
this.chatInner[(message as Message.message).pFlags.sponsored ? 'append' : 'prepend'](bubble);
return;
}
});
this.bubbleGroups.mountUnmountGroups(groups);
// this.bubbleGroups.findIncorrentPositions();
if(this.updatePlaceholderPosition) {
this.updatePlaceholderPosition();
}
if(restoreScroll) {
restoreScroll();
}
// 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) {
this.messagesQueuePromise = null;
if(this.messagesQueue.length) {
this.setMessagesQueuePromise();
}
}
});
@ -3121,16 +3225,35 @@ export default class ChatBubbles { @@ -3121,16 +3225,35 @@ export default class ChatBubbles {
bubble.remove();
// this.bubbleGroups.removeAndUnmountBubble(bubble);
}
this.bubblesToEject.clear();
}
public setBubblePosition(bubble: HTMLElement, message: Message.message | Message.messageService, unmountIfFound: boolean) {
if(message.pFlags.local) {
this.chatInner[(message as Message.message).pFlags.sponsored ? 'append' : 'prepend'](bubble);
return;
}
public groupBubbles(items: Array<{
// Awaited<ReturnType<ChatBubbles['safeRenderMessage']>> &
bubble: HTMLElement,
message: Message.message | Message.messageService
}/* & {
unmountIfFound?: boolean
} */>) {
items.forEach(({bubble, message}) => {
this.bubbleGroups.prepareForGrouping(bubble, message);
});
const result = this.bubbleGroups.addBubble(bubble, message, unmountIfFound);
return result;
const groups = this.bubbleGroups.groupUngrouped();
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) {
@ -3156,6 +3279,9 @@ export default class ChatBubbles { @@ -3156,6 +3279,9 @@ export default class ChatBubbles {
// const groupedId = (message as Message.message).grouped_id;
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] ??= {
// bubble: newBubble,
@ -3167,7 +3293,10 @@ export default class ChatBubbles { @@ -3167,7 +3293,10 @@ export default class ChatBubbles {
if(bubble) {
this.skippedMids.delete(message.mid);
this.bubblesToEject.add(bubble);
this.bubblesToReplace.delete(bubble);
this.bubblesToReplace.add(newBubble);
this.bubbleGroups.changeBubbleByBubble(bubble, newBubble);
}
@ -3215,6 +3344,8 @@ export default class ChatBubbles { @@ -3215,6 +3344,8 @@ export default class ChatBubbles {
// return;
// }
// await pause(1000);
const isMessage = message._ === 'message';
const groupedId = isMessage && message.grouped_id;
let albumMids: number[], reactionsMessage: Message.message;
@ -3232,96 +3363,42 @@ export default class ChatBubbles { @@ -3232,96 +3363,42 @@ export default class ChatBubbles {
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)
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');
messageDiv.classList.add('message');
//messageDiv.innerText = message.message;
let bubbleContainer: HTMLDivElement;
let contentWrapper: HTMLElement;
// bubble
// if(!bubble) {
contentWrapper = document.createElement('div');
contentWrapper.classList.add('bubble-content-wrapper');
bubbleContainer = document.createElement('div');
bubbleContainer.classList.add('bubble-content');
// bubble = document.createElement('div');
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 = '';
// }
contentWrapper = document.createElement('div');
contentWrapper.classList.add('bubble-content-wrapper');
bubbleContainer = document.createElement('div');
bubbleContainer.classList.add('bubble-content');
bubble.classList.add('bubble');
contentWrapper.append(bubbleContainer);
bubble.append(contentWrapper);
// ! reset due to album edit or delete item
// this.bubbles[+message.mid] = bubble;
bubble.dataset.mid = '' + message.mid;
bubble.dataset.peerId = '' + message.peerId;
bubble.dataset.timestamp = '' + message.date;
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);
}
}
const loadPromises: Promise<any>[] = [];
const ret = {
bubble,
promises: loadPromises,
message,
reverse,
needAvatar: undefined as boolean,
isOut: undefined as boolean
reverse
};
if(message._ === 'messageService' && (!message.action || !SERVICE_AS_REGULAR.has(message.action._))) {
@ -3339,10 +3416,15 @@ export default class ChatBubbles { @@ -3339,10 +3416,15 @@ export default class ChatBubbles {
const s = document.createElement('div');
s.classList.add('service-msg');
if(action) {
let promise: Promise<any>;
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') {
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 {
s.append(await wrapMessageActionTextNew(message));
}
@ -3524,6 +3606,7 @@ export default class ChatBubbles { @@ -3524,6 +3606,7 @@ export default class ChatBubbles {
promise.then((peerId) => {
const threadId = this.peerId === peerId ? this.chat.threadId : undefined;
this.chat.appImManager.setInnerPeer({peerId});
this.managers.appInlineBotsManager.switchInlineQuery(peerId, threadId, botId, button.query);
});
});
@ -3605,7 +3688,7 @@ export default class ChatBubbles { @@ -3605,7 +3688,7 @@ export default class ChatBubbles {
const fwdFrom = isMessage && message.fwd_from;
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;
const canHideNameIfMedia = !message.viaBotId && (message.fromId === rootScope.myId || !message.pFlags.out);
@ -3674,9 +3757,9 @@ export default class ChatBubbles { @@ -3674,9 +3757,9 @@ export default class ChatBubbles {
case 'messageMediaWebPage': {
processingWebPage = true;
let webpage: WebPage = messageMedia.webpage;
let webPage: WebPage = messageMedia.webpage;
////////this.log('messageMediaWebPage', webpage);
if(webpage._ !== 'webPage') {
if(webPage._ !== 'webPage') {
break;
}
@ -3689,8 +3772,8 @@ export default class ChatBubbles { @@ -3689,8 +3772,8 @@ export default class ChatBubbles {
quote.classList.add('quote');
let previewResizer: HTMLDivElement, preview: HTMLDivElement;
const photo: Photo.photo = webpage.photo as any;
if(photo || webpage.document) {
const photo: Photo.photo = webPage.photo as any;
if(photo || webPage.document) {
previewResizer = document.createElement('div');
previewResizer.classList.add('preview-resizer');
preview = document.createElement('div');
@ -3701,7 +3784,7 @@ export default class ChatBubbles { @@ -3701,7 +3784,7 @@ export default class ChatBubbles {
let quoteTextDiv = document.createElement('div');
quoteTextDiv.classList.add('quote-text');
const doc = webpage.document as MyDocument;
const doc = webPage.document as MyDocument;
if(doc) {
if(doc.type === 'gif' || doc.type === 'video' || doc.type === 'round') {
//if(doc.size <= 20e6) {
@ -3754,19 +3837,19 @@ export default class ChatBubbles { @@ -3754,19 +3837,19 @@ export default class ChatBubbles {
}
let t: HTMLElement;
if(webpage.site_name) {
const html = wrapRichText(webpage.url);
if(webPage.site_name) {
const html = wrapRichText(webPage.url);
const a: HTMLAnchorElement = htmlToDocumentFragment(html).firstElementChild as any;
a.classList.add('webpage-name');
const strong = document.createElement('strong');
setInnerHTML(strong, wrapEmojiText(webpage.site_name));
setInnerHTML(strong, wrapEmojiText(webPage.site_name));
a.textContent = '';
a.append(strong);
quoteTextDiv.append(a);
t = a;
}
const title = wrapWebPageTitle(webpage);
const title = wrapWebPageTitle(webPage);
if(title.textContent) {
let titleDiv = document.createElement('div');
titleDiv.classList.add('title');
@ -3777,7 +3860,7 @@ export default class ChatBubbles { @@ -3777,7 +3860,7 @@ export default class ChatBubbles {
t = titleDiv;
}
const description = wrapWebPageDescription(webpage);
const description = wrapWebPageDescription(webPage);
if(description.textContent) {
let textDiv = document.createElement('div');
textDiv.classList.add('text');
@ -4098,7 +4181,7 @@ export default class ChatBubbles { @@ -4098,7 +4181,7 @@ export default class ChatBubbles {
let savedFrom = '';
// 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
let title: HTMLElement | DocumentFragment;
let titleVia: typeof title;
@ -4200,8 +4283,6 @@ export default class ChatBubbles { @@ -4200,8 +4283,6 @@ export default class ChatBubbles {
nameDiv.classList.add('name');
nameContainer.append(nameDiv);
}
ret.needAvatar = this.chat.isAnyGroup && !isOut;
} else {
bubble.classList.add('hide-name');
}
@ -4315,7 +4396,8 @@ export default class ChatBubbles { @@ -4315,7 +4396,8 @@ export default class ChatBubbles {
return;
}
this.log.warn('onRender');
const log = this.log.bindPrefix('prepareToSaveScroll');
log('save');
const scrollSaver = this.createScrollSaver(reverse);
scrollSaver.save(); // * let's save scroll position by point before the slicing, not after
@ -4328,16 +4410,17 @@ export default class ChatBubbles { @@ -4328,16 +4410,17 @@ export default class ChatBubbles {
// const saved = scrollSaver.getSaved();
// const hadScroll = saved.scrollHeight !== saved.clientHeight;
(this.messagesQueuePromise || Promise.resolve()).then(() => {
return () => {
log('restore');
// scrollSaver.restore(_history.length === 1 && !reverse ? false : true);
scrollSaver.restore(reverse);
this.onRenderScrollSet(scrollSaver.getSaved());
});
};
}
public async performHistoryResult(historyResult: HistoryResult | {history: (Message.message | Message.messageService | number)[]}, reverse: boolean) {
const log = this.log.bindPrefix('perform-' + (Math.random() * 1000 | 0));
log.warn('start', this.chatInner.parentElement);
const log = false ? this.log.bindPrefix('perform-' + (Math.random() * 1000 | 0)) : undefined;
log && log('start', this.chatInner.parentElement);
let history = historyResult.history;
history = history.slice(); // need
@ -4393,9 +4476,10 @@ export default class ChatBubbles { @@ -4393,9 +4476,10 @@ export default class ChatBubbles {
await Promise.all(setLoadedPromises);
// ! it is important to insert bubbles to group reversed way
const length = history.length, promises: Promise<any>[] = [];
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]));
// const length = history.length, promises: Promise<any>[] = [];
// 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]));
const promises = messages.map(cb);
// cannot combine them into one promise
await Promise.all(promises);
@ -4409,7 +4493,7 @@ export default class ChatBubbles { @@ -4409,7 +4493,7 @@ export default class ChatBubbles {
}
}
log.warn('performHistoryResult end');
log && log('performHistoryResult end');
}
private onRenderScrollSet(state?: {scrollHeight: number, clientHeight: number}) {
@ -4823,9 +4907,9 @@ export default class ChatBubbles { @@ -4823,9 +4907,9 @@ export default class ChatBubbles {
};
} else {
callback = () => {
rootScope.dispatchEvent('history_focus', {
this.chat.appImManager.setInnerPeer({
peerId,
mid,
lastMsgId: mid,
startParam
});
};
@ -5268,7 +5352,7 @@ export default class ChatBubbles { @@ -5268,7 +5352,7 @@ export default class ChatBubbles {
// * filter last album, because we don't know is it the last item
for(let i = additionMsgIds.length - 1; i >= 0; --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;
}

22
src/components/chat/chat.ts

@ -34,6 +34,7 @@ import AppSharedMediaTab from "../sidebarRight/tabs/sharedMedia"; @@ -34,6 +34,7 @@ import AppSharedMediaTab from "../sidebarRight/tabs/sharedMedia";
import noop from "../../helpers/noop";
import middlewarePromise from "../../helpers/middlewarePromise";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { Message } from "../../layer";
export type ChatType = 'chat' | 'pinned' | 'replies' | 'discussion' | 'scheduled';
@ -80,6 +81,7 @@ export default class Chat extends EventListenerBase<{ @@ -80,6 +81,7 @@ export default class Chat extends EventListenerBase<{
// public renderDarkPattern: () => Promise<void>;
public isAnyGroup: boolean;
public isMegagroup: boolean;
constructor(
public appImManager: AppImManager,
@ -401,16 +403,18 @@ export default class Chat extends EventListenerBase<{ @@ -401,16 +403,18 @@ export default class Chat extends EventListenerBase<{
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.isRestricted(peerId),
this._isAnyGroup(peerId),
this.setAutoDownloadMedia()
this.setAutoDownloadMedia(),
this.managers.appPeersManager.isMegagroup(peerId)
]));
this.noForwards = noForwards;
this.isRestricted = isRestricted;
this.isAnyGroup = isAnyGroup;
this.isMegagroup = isMegagroup;
this.container.classList.toggle('no-forwards', this.noForwards);
@ -588,4 +592,18 @@ export default class Chat extends EventListenerBase<{ @@ -588,4 +592,18 @@ export default class Chat extends EventListenerBase<{
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 { @@ -245,6 +245,7 @@ export default class InlineHelper extends AutocompleteHelper {
const btnSwitchToPM = Button('btn-primary btn-secondary btn-primary-transparent primary');
setInnerHTML(btnSwitchToPM, wrapEmojiText(botResults.switch_pm.text));
attachClickEvent(btnSwitchToPM, (e) => {
this.chat.appImManager.setInnerPeer({peerId});
this.managers.appInlineBotsManager.switchToPM(peerId, peer.id, botResults.switch_pm.start_param);
});
parent.append(btnSwitchToPM);

11
src/components/chat/input.ts

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

5
src/components/connectionStatus.ts

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

5
src/components/groupCall/participants.ts

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

8
src/components/popups/index.ts

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

5
src/components/popups/joinChatInvite.ts

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

4
src/components/putPhoto.ts

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

4
src/components/sidebarLeft/index.ts

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

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

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

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

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

10
src/components/wrappers/photo.ts

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

2
src/config/app.ts

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

24
src/config/databases/state.ts

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

2
src/environment/sharedWorkerSupport.ts

@ -1,3 +1,3 @@ @@ -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;

15
src/helpers/array/partition.ts

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

24
src/helpers/listenMessagePort.ts

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

1
src/helpers/scrollSaver.ts

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

8
src/helpers/toggleStorages.ts

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

4
src/index.ts

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

2
src/lang.ts

@ -55,6 +55,8 @@ const lang = { @@ -55,6 +55,8 @@ const lang = {
},
"Deactivated.Title": "Too many tabs...",
"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": {
// "one_value": "%d draft",
// "other_value": "%d drafts",

230
src/layer.d.ts vendored

@ -498,8 +498,6 @@ export namespace User { @@ -498,8 +498,6 @@ export namespace User {
apply_min_photo?: true,
fake?: true,
bot_attach_menu?: true,
premium?: true,
attach_menu_enabled?: true,
}>,
id: string | number,
access_hash?: string | number,
@ -1121,10 +1119,8 @@ export namespace MessageAction { @@ -1121,10 +1119,8 @@ export namespace MessageAction {
export type messageActionPaymentSent = {
_: 'messageActionPaymentSent',
flags?: number,
currency: string,
total_amount: string | number,
invoice_slug?: string
total_amount: string | number
};
export type messageActionPhoneCall = {
@ -1994,7 +1990,7 @@ export namespace MessagesFilter { @@ -1994,7 +1990,7 @@ export namespace MessagesFilter {
/**
* @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 type updateNewMessage = {
@ -2702,16 +2698,6 @@ export namespace Update { @@ -2702,16 +2698,6 @@ export namespace Update {
_: 'updateSavedRingtones'
};
export type updateTranscribeAudio = {
_: 'updateTranscribeAudio',
flags?: number,
pFlags?: Partial<{
final?: true,
}>,
transcription_id: string | number,
text: string
};
export type updateNewDiscussionMessage = {
_: 'updateNewDiscussionMessage',
message?: Message
@ -2950,7 +2936,6 @@ export namespace DcOption { @@ -2950,7 +2936,6 @@ export namespace DcOption {
tcpo_only?: true,
cdn?: true,
static?: true,
this_port_only?: true,
}>,
id: number,
ip_address: string,
@ -2976,7 +2961,6 @@ export namespace Config { @@ -2976,7 +2961,6 @@ export namespace Config {
revoke_pm_inbox?: true,
blocked_mode?: true,
pfs_enabled?: true,
force_try_ipv6?: true,
}>,
date: number,
expires: number,
@ -3157,7 +3141,7 @@ export namespace EncryptedFile { @@ -3157,7 +3141,7 @@ export namespace EncryptedFile {
_: 'encryptedFile',
id: string | number,
access_hash: string | number,
size: string | number,
size: number,
dc_id: number,
key_fingerprint: number
};
@ -3944,7 +3928,7 @@ export namespace ReceivedNotifyMessage { @@ -3944,7 +3928,7 @@ export namespace ReceivedNotifyMessage {
/**
* @link https://core.telegram.org/type/ExportedChatInvite
*/
export type ExportedChatInvite = ExportedChatInvite.chatInviteExported | ExportedChatInvite.chatInvitePublicJoinRequests;
export type ExportedChatInvite = ExportedChatInvite.chatInviteExported;
export namespace ExportedChatInvite {
export type chatInviteExported = {
@ -3965,10 +3949,6 @@ export namespace ExportedChatInvite { @@ -3965,10 +3949,6 @@ export namespace ExportedChatInvite {
requested?: number,
title?: string
};
export type chatInvitePublicJoinRequests = {
_: 'chatInvitePublicJoinRequests'
};
}
/**
@ -4110,13 +4090,10 @@ export type BotInfo = BotInfo.botInfo; @@ -4110,13 +4090,10 @@ export type BotInfo = BotInfo.botInfo;
export namespace BotInfo {
export type botInfo = {
_: 'botInfo',
flags?: number,
user_id?: string | number,
description?: string,
description_photo?: Photo,
description_document?: Document,
commands?: Array<BotCommand>,
menu_button?: BotMenuButton
user_id: string | number,
description: string,
commands: Array<BotCommand>,
menu_button: BotMenuButton
};
}
@ -4295,7 +4272,7 @@ export namespace ReplyMarkup { @@ -4295,7 +4272,7 @@ export namespace ReplyMarkup {
/**
* @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 type messageEntityUnknown = {
@ -4423,12 +4400,6 @@ export namespace MessageEntity { @@ -4423,12 +4400,6 @@ export namespace MessageEntity {
length: number
};
export type messageEntityAnimatedEmoji = {
_: 'messageEntityAnimatedEmoji',
offset: number,
length: number
};
export type messageEntityEmoji = {
_: 'messageEntityEmoji',
offset?: number,
@ -5991,9 +5962,6 @@ export namespace PaymentsPaymentForm { @@ -5991,9 +5962,6 @@ export namespace PaymentsPaymentForm {
}>,
form_id: string | number,
bot_id: string | number,
title: string,
description: string,
photo?: WebDocument,
invoice: Invoice,
provider_id: string | number,
url: string,
@ -6262,10 +6230,6 @@ export type PhoneConnection = PhoneConnection.phoneConnection | PhoneConnection. @@ -6262,10 +6230,6 @@ export type PhoneConnection = PhoneConnection.phoneConnection | PhoneConnection.
export namespace PhoneConnection {
export type phoneConnection = {
_: 'phoneConnection',
flags?: number,
pFlags?: Partial<{
tcp?: true,
}>,
id: string | number,
ip: string,
ipv6: string,
@ -6913,7 +6877,7 @@ export type FileHash = FileHash.fileHash; @@ -6913,7 +6877,7 @@ export type FileHash = FileHash.fileHash;
export namespace FileHash {
export type fileHash = {
_: 'fileHash',
offset: string | number,
offset: number,
limit: number,
hash: Uint8Array
};
@ -6986,7 +6950,7 @@ export namespace SecureFile { @@ -6986,7 +6950,7 @@ export namespace SecureFile {
_: 'secureFile',
id: string | number,
access_hash: string | number,
size: string | number,
size: number,
dc_id: number,
date: number,
file_hash: Uint8Array,
@ -8365,7 +8329,7 @@ export namespace PaymentsBankCardData { @@ -8365,7 +8329,7 @@ export namespace PaymentsBankCardData {
/**
* @link https://core.telegram.org/type/DialogFilter
*/
export type DialogFilter = DialogFilter.dialogFilter | DialogFilter.dialogFilterDefault;
export type DialogFilter = DialogFilter.dialogFilter;
export namespace DialogFilter {
export type dialogFilter = {
@ -8391,10 +8355,6 @@ export namespace DialogFilter { @@ -8391,10 +8355,6 @@ export namespace DialogFilter {
peerId?: PeerId,
folder_id?: number
};
export type dialogFilterDefault = {
_: 'dialogFilterDefault'
};
}
/**
@ -9235,9 +9195,6 @@ export namespace SponsoredMessage { @@ -9235,9 +9195,6 @@ export namespace SponsoredMessage {
export type sponsoredMessage = {
_: 'sponsoredMessage',
flags?: number,
pFlags?: Partial<{
recommended?: true,
}>,
random_id: Uint8Array,
from_id?: Peer,
chat_invite?: ChatInvite,
@ -9446,7 +9403,6 @@ export namespace AvailableReaction { @@ -9446,7 +9403,6 @@ export namespace AvailableReaction {
flags?: number,
pFlags?: Partial<{
inactive?: true,
premium?: true,
}>,
reaction: string,
title: string,
@ -9592,7 +9548,6 @@ export namespace AttachMenuBot { @@ -9592,7 +9548,6 @@ export namespace AttachMenuBot {
}>,
bot_id: string | number,
short_name: string,
peer_types: Array<AttachMenuPeerType>,
icons: Array<AttachMenuBotIcon>
};
}
@ -9746,80 +9701,6 @@ export namespace AccountSavedRingtone { @@ -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 {
'error': Error.error,
'inputPeerEmpty': InputPeer.inputPeerEmpty,
@ -10803,19 +10684,6 @@ export interface ConstructorDeclMap { @@ -10803,19 +10684,6 @@ export interface ConstructorDeclMap {
'notificationSoundRingtone': NotificationSound.notificationSoundRingtone,
'account.savedRingtone': AccountSavedRingtone.accountSavedRingtone,
'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,
'messageEntityHighlight': MessageEntity.messageEntityHighlight,
'messageEntityLinebreak': MessageEntity.messageEntityLinebreak,
@ -11201,7 +11069,7 @@ export type UploadGetFile = { @@ -11201,7 +11069,7 @@ export type UploadGetFile = {
precise?: boolean,
cdn_supported?: boolean,
location: InputFileLocation,
offset: string | number,
offset: number,
limit: number
};
@ -11626,7 +11494,7 @@ export type MessagesReorderStickerSets = { @@ -11626,7 +11494,7 @@ export type MessagesReorderStickerSets = {
export type MessagesGetDocumentByHash = {
sha256: Uint8Array,
size: string | number,
size: number,
mime_type: string
};
@ -11925,7 +11793,8 @@ export type UploadGetWebFile = { @@ -11925,7 +11793,8 @@ export type UploadGetWebFile = {
export type PaymentsGetPaymentForm = {
flags?: number,
invoice: InputInvoice,
peer: InputPeer,
msg_id: number,
theme_params?: DataJSON
};
@ -11937,14 +11806,16 @@ export type PaymentsGetPaymentReceipt = { @@ -11937,14 +11806,16 @@ export type PaymentsGetPaymentReceipt = {
export type PaymentsValidateRequestedInfo = {
flags?: number,
save?: boolean,
invoice: InputInvoice,
peer: InputPeer,
msg_id: number,
info: PaymentRequestedInfo
};
export type PaymentsSendPaymentForm = {
flags?: number,
form_id: string | number,
invoice: InputInvoice,
peer: InputPeer,
msg_id: number,
requested_info_id?: string,
shipping_option_id?: string,
credentials: InputPaymentCredentials,
@ -12066,7 +11937,7 @@ export type PhoneSaveCallDebug = { @@ -12066,7 +11937,7 @@ export type PhoneSaveCallDebug = {
export type UploadGetCdnFile = {
file_token: Uint8Array,
offset: string | number,
offset: number,
limit: number
};
@ -12119,7 +11990,7 @@ export type ChannelsGetAdminLog = { @@ -12119,7 +11990,7 @@ export type ChannelsGetAdminLog = {
export type UploadGetCdnFileHashes = {
file_token: Uint8Array,
offset: string | number
offset: number
};
export type MessagesSendScreenshotNotification = {
@ -12225,7 +12096,7 @@ export type MessagesSearchStickerSets = { @@ -12225,7 +12096,7 @@ export type MessagesSearchStickerSets = {
export type UploadGetFileHashes = {
location: InputFileLocation,
offset: string | number
offset: number
};
export type HelpGetTermsOfServiceUpdate = {
@ -12312,7 +12183,7 @@ export type AccountInitTakeoutSession = { @@ -12312,7 +12183,7 @@ export type AccountInitTakeoutSession = {
message_megagroups?: boolean,
message_channels?: boolean,
files?: boolean,
file_max_size?: string | number
file_max_size?: number
};
export type AccountFinishTakeoutSession = {
@ -13261,8 +13132,7 @@ export type MessagesRequestWebView = { @@ -13261,8 +13132,7 @@ export type MessagesRequestWebView = {
url?: string,
start_param?: string,
theme_params?: DataJSON,
reply_to_msg_id?: number,
send_as?: InputPeer
reply_to_msg_id?: number
};
export type MessagesProlongWebView = {
@ -13271,8 +13141,7 @@ export type MessagesProlongWebView = { @@ -13271,8 +13141,7 @@ export type MessagesProlongWebView = {
peer: InputPeer,
bot: InputUser,
query_id: string | number,
reply_to_msg_id?: number,
send_as?: InputPeer
reply_to_msg_id?: number
};
export type MessagesRequestSimpleWebView = {
@ -13326,45 +13195,6 @@ export type BotsSetBotGroupDefaultAdminRights = { @@ -13326,45 +13195,6 @@ export type BotsSetBotGroupDefaultAdminRights = {
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 {
'invokeAfterMsg': {req: InvokeAfterMsg, res: any},
'invokeAfterMsgs': {req: InvokeAfterMsgs, res: any},
@ -13800,13 +13630,5 @@ export interface MethodDeclMap { @@ -13800,13 +13630,5 @@ export interface MethodDeclMap {
'account.uploadRingtone': {req: AccountUploadRingtone, res: Document},
'bots.setBotBroadcastDefaultAdminRights': {req: BotsSetBotBroadcastDefaultAdminRights, 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 { @@ -377,8 +377,6 @@ export class AppChatsManager extends AppManager {
this.apiUpdatesManager.processUpdateMessage(updates);
const channelId = (updates as any).chats[0].id;
this.rootScope.dispatchEvent('history_focus', {peerId: channelId.toPeerId(true)});
return channelId;
});
}
@ -401,8 +399,6 @@ export class AppChatsManager extends AppManager { @@ -401,8 +399,6 @@ export class AppChatsManager extends AppManager {
this.apiUpdatesManager.processUpdateMessage(updates);
const chatId = (updates as any as Updates.updates).chats[0].id;
this.rootScope.dispatchEvent('history_focus', {peerId: chatId.toPeerId(true)});
return chatId;
});
}

2
src/lib/appManagers/appDialogsManager.ts

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

13
src/lib/appManagers/appDownloadManager.ts

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

51
src/lib/appManagers/appImManager.ts

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

2
src/lib/appManagers/appInlineBotsManager.ts

@ -97,7 +97,6 @@ export class AppInlineBotsManager extends AppManager { @@ -97,7 +97,6 @@ export class AppInlineBotsManager extends AppManager {
public switchToPM(fromPeerId: PeerId, botId: BotId, startParam: string) {
this.setHash[botId] = {peerId: fromPeerId, time: Date.now()};
this.rootScope.dispatchEvent('history_focus', {peerId: botId.toPeerId()});
return this.appMessagesManager.startBot(botId, undefined, startParam);
}
@ -224,7 +223,6 @@ export class AppInlineBotsManager extends AppManager { @@ -224,7 +223,6 @@ export class AppInlineBotsManager extends AppManager {
}
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);
}

3
src/lib/appManagers/appManagersManager.ts

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

29
src/lib/appManagers/appMessagesManager.ts

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

2
src/lib/appManagers/appStateManager.ts

@ -43,7 +43,7 @@ export class AppStateManager { @@ -43,7 +43,7 @@ export class AppStateManager {
}
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({
[key]: value

52
src/lib/appManagers/appTabsManager.ts

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

18
src/lib/appManagers/uiNotificationsManager.ts

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

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

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

6
src/lib/cacheStorage.ts

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

1
src/lib/crypto/aesCtrUtils.ts

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

12
src/lib/localStorage.ts

@ -113,7 +113,7 @@ class LocalStorage<Storage extends Record<string, any>> { @@ -113,7 +113,7 @@ class LocalStorage<Storage extends Record<string, any>> {
} */
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) {
keys.push(`dc${i}_server_salt`);
keys.push(`dc${i}_auth_key`);
@ -124,9 +124,13 @@ class LocalStorage<Storage extends Record<string, any>> { @@ -124,9 +124,13 @@ class LocalStorage<Storage extends Record<string, any>> {
}
}
public toggleStorage(enabled: boolean) {
public toggleStorage(enabled: boolean, clearWrite: boolean) {
this.useStorage = enabled;
if(!clearWrite) {
return;
}
if(!enabled) {
this.clear();
} else {
@ -191,7 +195,7 @@ export default class LocalStorageController<Storage extends Record<string, any>> @@ -191,7 +195,7 @@ export default class LocalStorageController<Storage extends Record<string, any>>
return this.proxy<void>('clear'/* , preserveKeys */);
}
public toggleStorage(enabled: boolean) {
return this.proxy<void>('toggleStorage', enabled);
public toggleStorage(enabled: boolean, clearWrite: boolean) {
return this.proxy<void>('toggleStorage', enabled, clearWrite);
}
}

11
src/lib/mtproto/apiFileManager.ts

@ -596,19 +596,24 @@ export class ApiFileManager extends AppManager { @@ -596,19 +596,24 @@ export class ApiFileManager extends AppManager {
}
public downloadMedia(options: DownloadMediaOptions): DownloadPromise {
const {media, thumb} = options;
const isPhoto = media?._ === 'photo';
let {media, thumb} = options;
const isPhoto = media._ === 'photo';
if(media._ === 'photoEmpty' || (isPhoto && !thumb)) {
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);
let promise = this.getDownload(fileName);
if(!promise) {
promise = this.download(downloadOptions);
if(media._ === 'document') {
if(isDocument) {
this.rootScope.dispatchEvent('document_downloading', media.id);
promise.catch(noop).finally(() => {
this.rootScope.dispatchEvent('document_downloaded', media.id);

2
src/lib/mtproto/apiManager.ts

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

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

@ -9,7 +9,6 @@ import '../polyfill'; @@ -9,7 +9,6 @@ import '../polyfill';
import '../../helpers/peerIdPolyfill';
import cryptoWorker from "../crypto/cryptoMessagePort";
import CacheStorageController from '../cacheStorage';
import { setEnvironment } from '../../environment/utils';
import appStateManager from '../appManagers/appStateManager';
import transportController from './transports/controller';
@ -19,8 +18,8 @@ import appManagersManager from '../appManagers/appManagersManager'; @@ -19,8 +18,8 @@ import appManagersManager from '../appManagers/appManagersManager';
import listenMessagePort from '../../helpers/listenMessagePort';
import { logger } from '../logger';
import { State } from '../../config/state';
import AppStorage from '../storage';
import toggleStorages from '../../helpers/toggleStorages';
import appTabsManager from '../appManagers/appTabsManager';
let _isServiceWorkerOnline = true;
export function isServiceWorkerOnline() {
@ -55,7 +54,9 @@ port.addMultipleEventsListeners({ @@ -55,7 +54,9 @@ port.addMultipleEventsListeners({
RESET_STORAGES_PROMISE.resolve(resetStorages);
},
toggleStorages: (enabled) => toggleStorages(enabled),
toggleStorages: ({enabled, clearWrite}) => {
return toggleStorages(enabled, clearWrite);
},
event: (payload, source) => {
log('will redirect event', payload, source);
@ -68,7 +69,7 @@ port.addMultipleEventsListeners({ @@ -68,7 +69,7 @@ port.addMultipleEventsListeners({
createObjectURL: (blob) => {
return URL.createObjectURL(blob);
}
},
// socketProxy: (task) => {
// const socketTask = task.payload;
@ -84,24 +85,16 @@ port.addMultipleEventsListeners({ @@ -84,24 +85,16 @@ port.addMultipleEventsListeners({
// 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');
appManagersManager.start();
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"; @@ -10,7 +10,7 @@ import type loadState from "../appManagers/utils/state/loadState";
import type { StoragesResults } from "../appManagers/utils/storages/loadStorages";
import type { LocalStorageProxyTask } from "../localStorage";
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 SuperMessagePort from "./superMessagePort";
@ -25,20 +25,22 @@ export default class MTProtoMessagePort<Master extends boolean = true> extends S @@ -25,20 +25,22 @@ export default class MTProtoMessagePort<Master extends boolean = true> extends S
crypto: (payload: {method: string, args: any[]}) => Promise<any>,
state: (payload: {userId: UserId} & Awaited<ReturnType<typeof loadState>> & {storagesResults?: StoragesResults}) => void,
manager: (payload: MTProtoManagerTaskPayload) => any,
toggleStorages: typeof toggleStorages,
toggleStorages: (payload: {enabled: boolean, clearWrite: boolean}) => ReturnType<typeof toggleStorages>,
serviceWorkerOnline: (online: boolean) => void,
cryptoPort: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
createObjectURL: (blob: Blob) => string
createObjectURL: (blob: Blob) => string,
tabState: (payload: TabState, source: MessageEventSource) => void,
} & MTProtoBroadcastEvent, {
convertWebp: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>,
convertOpus: (payload: {fileName: string, bytes: Uint8Array}) => Promise<Uint8Array>,
localStorageProxy: (payload: LocalStorageProxyTask['payload']) => Promise<any>,
mirror: (payload: MirrorTaskPayload) => void
mirror: (payload: MirrorTaskPayload) => void,
notificationBuild: (payload: NotificationBuildTaskPayload) => void
} & MTProtoBroadcastEvent, Master> {
private static INSTANCE: MTProtoMessagePort;
constructor() {
super(false);
super();
MTProtoMessagePort.INSTANCE = this;

51
src/lib/mtproto/mtprotoworker.ts

@ -6,16 +6,17 @@ @@ -6,16 +6,17 @@
import type { RequestFilePartTask, RequestFilePartTaskResponse, ServiceWorkerTask } from '../serviceWorker/index.service';
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 rootScope from '../rootScope';
import webpWorkerController from '../webp/webpWorkerController';
import { MOUNT_CLASS_TO } from '../../config/debug';
import sessionStorage from '../sessionStorage';
import webPushApiManager from './webPushApiManager';
import AppStorage from '../storage';
import appRuntimeManager from '../appManagers/appRuntimeManager';
import telegramMeWebManager from './telegramMeWebManager';
import CacheStorageController, { CacheStorageDbName } from '../cacheStorage';
import pause from '../../helpers/schedulers/pause';
import isObject from '../../helpers/object/isObject';
import ENVIRONMENT from '../../environment';
@ -25,12 +26,12 @@ import MTProtoMessagePort from './mtprotoMessagePort'; @@ -25,12 +26,12 @@ import MTProtoMessagePort from './mtprotoMessagePort';
import cryptoMessagePort from '../crypto/cryptoMessagePort';
import SuperMessagePort from './superMessagePort';
import IS_SHARED_WORKER_SUPPORTED from '../../environment/sharedWorkerSupport';
import type { State } from '../../config/state';
import toggleStorages from '../../helpers/toggleStorages';
import idleController from '../../helpers/idleController';
export interface ToggleStorageTask extends WorkerTaskVoidTemplate {
type: 'toggleStorages',
payload: boolean
payload: {enabled: boolean, clearWrite: boolean}
};
export type Mirrors = {
@ -43,6 +44,18 @@ export type MirrorTaskPayload<T extends keyof Mirrors = keyof Mirrors, K extends @@ -43,6 +44,18 @@ export type MirrorTaskPayload<T extends keyof Mirrors = keyof Mirrors, K extends
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 {
private worker: /* Window */Worker;
private isSWRegistered: boolean;
@ -53,12 +66,18 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -53,12 +66,18 @@ class ApiManagerProxy extends MTProtoMessagePort {
public newVersion: string;
public oldVersion: string;
private tabState: TabState;
constructor() {
super();
this.isSWRegistered = true;
this.taskListenersSW = {};
this.mirrors = {} as any;
this.tabState = {
chatPeerIds: [],
idleStartTime: 0
};
this.log('constructor');
@ -158,7 +177,7 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -158,7 +177,7 @@ class ApiManagerProxy extends MTProtoMessagePort {
rootScope.addEventListener('logging_out', () => {
const toClear: CacheStorageDbName[] = ['cachedFiles', 'cachedStreamChunks'];
Promise.all([
toggleStorages(false),
toggleStorages(false, true),
sessionStorage.clear(),
Promise.race([
telegramMeWebManager.setAuthorized(false),
@ -171,6 +190,11 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -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.invoke('environment', ENVIRONMENT);
// this.sendState();
@ -357,10 +381,10 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -357,10 +381,10 @@ class ApiManagerProxy extends MTProtoMessagePort {
}
/// #endif
public async toggleStorages(enabled: boolean) {
await toggleStorages(enabled);
this.invoke('toggleStorages', enabled);
const task: ToggleStorageTask = {type: 'toggleStorages', payload: enabled};
public async toggleStorages(enabled: boolean, clearWrite: boolean) {
await toggleStorages(enabled, clearWrite);
this.invoke('toggleStorages', {enabled, clearWrite});
const task: ToggleStorageTask = {type: 'toggleStorages', payload: {enabled, clearWrite}};
this.postSWMessage(task);
}
@ -373,6 +397,15 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -373,6 +397,15 @@ class ApiManagerProxy extends MTProtoMessagePort {
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) => {
const {name, key, value} = payload;
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 @@ @@ -9,14 +9,16 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import App from "../../config/app";
import { MOUNT_CLASS_TO } from "../../config/debug";
import tabId from "../../config/tabId";
import IS_SHARED_WORKER_SUPPORTED from "../../environment/sharedWorkerSupport";
import EventListenerBase from "../../helpers/eventListenerBase";
import idleController from "../../helpers/idleController";
import { nextRandomUint } from "../../helpers/random";
import { logger } from "../logger";
import rootScope from "../rootScope";
import sessionStorage from "../sessionStorage";
import apiManagerProxy from "./mtprotoworker";
export type AppInstance = {
id: number,
@ -24,29 +26,40 @@ export type AppInstance = { @@ -24,29 +26,40 @@ export type AppInstance = {
time: number
};
export type InstanceDeactivateReason = 'version' | 'tabs';
const CHECK_INSTANCE_INTERVAL = 5000;
const DEACTIVATE_TIMEOUT = 30000;
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<{
activated: () => void,
deactivated: () => void
deactivated: (reason: InstanceDeactivateReason) => void
}> {
private instanceID: number;
private instanceId: number;
private started: boolean;
private masterInstance: boolean;
private deactivateTimeout: number;
private deactivated: boolean;
private initial: boolean;
private deactivated: InstanceDeactivateReason;
private log = logger('INSTANCE');
constructor() {
super(false);
this.log = logger('INSTANCE');
this.instanceId = tabId;
}
public get deactivatedReason() {
return this.deactivated;
}
public start() {
if(!this.started && !IS_MULTIPLE_INSTANCES_SUPPORTED/* && !Config.Navigator.mobile && !Config.Modes.packed */) {
this.started = true;
this.reset();
this.reset();
//IdleManager.start();
if(!this.started/* && !Config.Navigator.mobile && !Config.Modes.packed */) {
this.started = true;
idleController.addEventListener('change', this.checkInstance);
setInterval(this.checkInstance, CHECK_INSTANCE_INTERVAL);
@ -59,94 +72,93 @@ export class SingleInstance extends EventListenerBase<{ @@ -59,94 +72,93 @@ export class SingleInstance extends EventListenerBase<{
}
private reset() {
if(IS_MULTIPLE_INSTANCES_SUPPORTED) return;
this.instanceID = nextRandomUint(32);
this.masterInstance = false;
if(this.deactivateTimeout) clearTimeout(this.deactivateTimeout);
this.deactivateTimeout = 0;
this.deactivated = false;
this.initial = false;
this.clearDeactivateTimeout();
this.deactivated = undefined;
}
private clearInstance = () => {
if(this.masterInstance && !this.deactivated && !IS_MULTIPLE_INSTANCES_SUPPORTED) {
if(this.masterInstance && !this.deactivated) {
this.log.warn('clear master instance');
sessionStorage.delete('xt_instance');
}
};
public activateInstance() {
if(this.deactivated && !IS_MULTIPLE_INSTANCES_SUPPORTED) {
if(this.deactivated) {
this.reset();
this.checkInstance(false);
this.dispatchEvent('activated');
}
}
private deactivateInstance = () => {
if(this.masterInstance || this.deactivated || IS_MULTIPLE_INSTANCES_SUPPORTED) {
private deactivateInstance(reason: InstanceDeactivateReason) {
if(this.masterInstance || this.deactivated) {
return;
}
this.log('deactivate');
this.deactivateTimeout = 0;
this.deactivated = true;
this.clearInstance();
//$modalStack.dismissAll();
this.log.warn('deactivate', reason);
this.clearDeactivateTimeout();
this.deactivated = reason;
//document.title = _('inactive_tab_title_raw')
this.dispatchEvent('deactivated', reason);
}
idleController.idle.deactivated = true;
this.dispatchEvent('deactivated');
};
private clearDeactivateTimeout() {
if(this.deactivateTimeout) {
clearTimeout(this.deactivateTimeout);
this.deactivateTimeout = 0;
}
}
private checkInstance = (idle = idleController.idle?.isIDLE) => {
if(this.deactivated || IS_MULTIPLE_INSTANCES_SUPPORTED) {
private checkInstance = async(idle = idleController.isIdle) => {
if(this.deactivated) {
return;
}
const time = Date.now();
const newInstance: AppInstance = {
id: this.instanceID,
id: this.instanceId,
idle,
time
};
sessionStorage.get('xt_instance', false).then((curInstance: AppInstance) => {
// this.log('check instance', newInstance, curInstance)
if(!idle ||
!curInstance ||
curInstance.id === this.instanceID ||
curInstance.time < (time - MULTIPLE_TABS_THRESHOLD)) {
sessionStorage.set({xt_instance: newInstance});
if(!this.masterInstance) {
rootScope.managers.networkerFactory.startAll();
if(!this.initial) {
this.initial = true;
} else {
this.log.warn('now master instance', newInstance);
}
this.masterInstance = true;
}
if(this.deactivateTimeout) {
clearTimeout(this.deactivateTimeout);
this.deactivateTimeout = 0;
}
} else {
if(this.masterInstance) {
rootScope.managers.networkerFactory.stopAll();
this.log.warn('now idle instance', newInstance);
if(!this.deactivateTimeout) {
this.deactivateTimeout = window.setTimeout(this.deactivateInstance, DEACTIVATE_TIMEOUT);
}
this.masterInstance = false;
}
const [curInstance, build = App.build] = await Promise.all([
sessionStorage.get('xt_instance', false),
sessionStorage.get('k_build', false)
]);
if(build > App.build) {
this.masterInstance = false;
rootScope.managers.networkerFactory.stopAll();
this.deactivateInstance('version');
apiManagerProxy.toggleStorages(false, false);
return;
} else if(IS_MULTIPLE_TABS_SUPPORTED) {
sessionStorage.set({xt_instance: newInstance});
return;
}
// this.log('check instance', newInstance, curInstance)
if(!idle ||
!curInstance ||
curInstance.id === this.instanceId ||
curInstance.time < (time - MULTIPLE_TABS_THRESHOLD)) {
sessionStorage.set({xt_instance: newInstance});
if(!this.masterInstance) {
this.masterInstance = true;
rootScope.managers.networkerFactory.startAll();
this.log.warn('now master instance', newInstance);
}
});
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"; @@ -9,7 +9,6 @@ import ctx from "../../environment/ctx";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { IS_SERVICE_WORKER, IS_WORKER, notifyAll } from "../../helpers/context";
import EventListenerBase from "../../helpers/eventListenerBase";
import pause from "../../helpers/schedulers/pause";
import { Awaited, WorkerTaskTemplate, WorkerTaskVoidTemplate } from "../../types";
import { logger } from "../logger";
@ -54,9 +53,13 @@ interface BatchTask extends SuperMessagePortTask { @@ -54,9 +53,13 @@ interface BatchTask extends SuperMessagePortTask {
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 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> = {
@ -77,8 +80,8 @@ type SendPort = WindowProxy | MessagePort | ServiceWorker | Worker; @@ -77,8 +80,8 @@ type SendPort = WindowProxy | MessagePort | ServiceWorker | Worker;
type ListenerCallback = (payload: any, source: MessageEventSource, event: MessageEvent<any>) => any;
type Listeners = Record<string, ListenerCallback>;
const PING_INTERVAL = DEBUG || true ? 0x7FFFFFFF : 1000;
const PING_TIMEOUT = DEBUG || true ? 0x7FFFFFFF : 5000;
// const PING_INTERVAL = DEBUG && false ? 0x7FFFFFFF : 5000;
// const PING_TIMEOUT = DEBUG && false ? 0x7FFFFFFF : 10000;
export default class SuperMessagePort<
Workers extends Listeners,
@ -105,6 +108,23 @@ export default class SuperMessagePort< @@ -105,6 +108,23 @@ export default class SuperMessagePort<
protected debug: 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() {
super._constructor(false);
@ -116,6 +136,17 @@ export default class SuperMessagePort< @@ -116,6 +136,17 @@ export default class SuperMessagePort<
this.pending = new Map();
this.log = logger('MP');
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) {
@ -129,51 +160,64 @@ export default class SuperMessagePort< @@ -129,51 +160,64 @@ export default class SuperMessagePort<
}
public attachSendPort(port: SendPort) {
this.log.warn('attaching port');
if((port as MessagePort).start) {
(port as MessagePort).start();
}
this.sendPorts.push(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();
}
});
// this.sendPing(port);
}
protected sendPingWithTimeout(port: SendPort, timeout = PING_INTERVAL) {
ctx.setTimeout(() => {
if(!this.sendPorts.includes(port)) {
return;
}
// ! Can't rely on ping because timers can be suspended
// 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(() => {
// // 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);
}, timeout);
this.onPortDisconnect && this.onPortDisconnect(port as any);
}
protected postMessage(port: SendPort | SendPort[], task: Task) {
@ -188,27 +232,20 @@ export default class SuperMessagePort< @@ -188,27 +232,20 @@ export default class SuperMessagePort<
// this.log('got message', task);
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;
task.payload.forEach((task) => {
// @ts-ignore
newEvent.data = task;
this.onMessage(newEvent);
});
} else if(task.type === 'result') {
this.processResultTask(task);
} else if(task.type === 'ack') {
this.processAckTask(task);
} 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);
}
} */
// @ts-ignore
this.processTaskMap[task.type](task, source, event);
};
protected async releasePending() {
protected /* async */ releasePending() {
//return;
if(!this.listenPorts.length || this.releasingPending) {
@ -217,28 +254,28 @@ export default class SuperMessagePort< @@ -217,28 +254,28 @@ export default class SuperMessagePort<
this.releasingPending = true;
// 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.pending.forEach((portTasks, port) => {
let batchTask: BatchTask;
const tasks: Task[] = [];
portTasks.forEach((task) => {
if(task.transfer) {
batchTask = undefined;
tasks.push(task);
} else {
if(!batchTask) {
batchTask = this.createTask('batch', []);
tasks.push(batchTask);
}
batchTask.payload.push(task);
}
});
// const tasks = portTasks;
// let batchTask: BatchTask;
// const tasks: Task[] = [];
// portTasks.forEach((task) => {
// if(task.transfer) {
// batchTask = undefined;
// tasks.push(task);
// } else {
// if(!batchTask) {
// batchTask = this.createTask('batch', []);
// tasks.push(batchTask);
// }
// batchTask.payload.push(task);
// }
// });
const tasks = portTasks;
tasks.forEach((task) => {
// if(task.type === 'batch') {
@ -263,7 +300,7 @@ export default class SuperMessagePort< @@ -263,7 +300,7 @@ export default class SuperMessagePort<
this.releasingPending = false;
}
protected processResultTask(task: ResultTask) {
protected processResultTask = (task: ResultTask) => {
const {taskId, result, error} = task.payload;
const deferred = this.awaiting[taskId];
if(!deferred) {
@ -271,11 +308,11 @@ export default class SuperMessagePort< @@ -271,11 +308,11 @@ export default class SuperMessagePort<
}
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];
}
};
protected processAckTask(task: AckTask) {
protected processAckTask = (task: AckTask) => {
const payload = task.payload;
const deferred = this.awaiting[payload.taskId];
if(!deferred) {
@ -316,21 +353,25 @@ export default class SuperMessagePort< @@ -316,21 +353,25 @@ export default class SuperMessagePort<
};
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);
}
};
protected processPongTask(task: PongTask, source: MessageEventSource, event: MessageEvent) {
protected processPongTask = (task: PongTask, source: MessageEventSource, event: MessageEvent) => {
const pingResolve = this.pingResolves.get(source);
if(pingResolve) {
this.pingResolves.delete(source);
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 innerTask = task.payload;
@ -397,7 +438,7 @@ export default class SuperMessagePort< @@ -397,7 +438,7 @@ export default class SuperMessagePort<
}
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 {
return {

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

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

32
src/lib/mtproto/webPushApiManager.ts

@ -12,14 +12,15 @@ @@ -12,14 +12,15 @@
import type { ServiceWorkerNotificationsClearTask, ServiceWorkerPingTask, ServiceWorkerPushClickTask } from "../serviceWorker/index.service";
import { MOUNT_CLASS_TO } from "../../config/debug";
import { logger } from "../logger";
import rootScope from "../rootScope";
import apiManager from "./mtprotoworker";
import apiManagerProxy from "./mtprotoworker";
import I18n, { LangPackKey } from "../langPack";
import { IS_MOBILE } from "../../environment/userAgent";
import appRuntimeManager from "../appManagers/appRuntimeManager";
import copy from "../../helpers/object/copy";
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 PushSubscriptionNotifyEvent = `push_${PushSubscriptionNotifyType}`;
@ -29,7 +30,12 @@ export type PushSubscriptionNotify = { @@ -29,7 +30,12 @@ export type PushSubscriptionNotify = {
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;
private isPushEnabled = false;
private localNotificationsAvailable = true;
@ -41,6 +47,8 @@ export class WebPushApiManager { @@ -41,6 +47,8 @@ export class WebPushApiManager {
private log = logger('PM');
constructor() {
super(false);
if(!('PushManager' in window) ||
!('Notification' in window) ||
!('serviceWorker' in navigator)) {
@ -156,7 +164,7 @@ export class WebPushApiManager { @@ -156,7 +164,7 @@ export class WebPushApiManager {
}
public isAliveNotify = () => {
if(!this.isAvailable || idleController.idle?.deactivated) {
if(!this.isAvailable || singleInstance.deactivatedReason) {
return;
}
@ -182,7 +190,7 @@ export class WebPushApiManager { @@ -182,7 +190,7 @@ export class WebPushApiManager {
}
};
apiManager.postSWMessage(task);
apiManagerProxy.postSWMessage(task);
this.isAliveTO = setTimeout(this.isAliveNotify, 10000);
}
@ -199,7 +207,7 @@ export class WebPushApiManager { @@ -199,7 +207,7 @@ export class WebPushApiManager {
}
const task: ServiceWorkerNotificationsClearTask = {type: 'notifications_clear'};
apiManager.postSWMessage(task);
apiManagerProxy.postSWMessage(task);
}
public setUpServiceWorkerChannel() {
@ -207,13 +215,13 @@ export class WebPushApiManager { @@ -207,13 +215,13 @@ export class WebPushApiManager {
return;
}
apiManager.addServiceWorkerTaskListener('push_click', (task: ServiceWorkerPushClickTask) => {
if(idleController.idle?.deactivated) {
apiManagerProxy.addServiceWorkerTaskListener('push_click', (task: ServiceWorkerPushClickTask) => {
if(singleInstance.deactivatedReason) {
appRuntimeManager.reload();
return;
}
rootScope.dispatchEvent('push_notification_click', task.payload);
this.dispatchEvent('push_notification_click', task.payload);
});
navigator.serviceWorker.ready.then(this.isAliveNotify);
@ -235,13 +243,13 @@ export class WebPushApiManager { @@ -235,13 +243,13 @@ export class WebPushApiManager {
}
this.log.warn('Push', event, subscriptionObj);
rootScope.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, {
this.dispatchEvent(('push_' + event) as PushSubscriptionNotifyEvent, {
tokenType: 10,
tokenValue: JSON.stringify(subscriptionObj)
});
} else {
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"; @@ -10,8 +10,6 @@ import type { MyDialogFilter } from "./storages/filters";
import type { Folder } from "./storages/dialogs";
import type { UserTyping } from "./appManagers/appProfileManager";
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 { GroupCallId } from "./appManagers/appGroupCallsManager";
import type { AppManagers } from "./appManagers/managers";
@ -67,7 +65,6 @@ export type BroadcastEvents = { @@ -67,7 +65,6 @@ export type BroadcastEvents = {
'history_delete': {peerId: PeerId, msgs: Set<number>},
'history_forbidden': PeerId,
'history_reload': PeerId,
'history_focus': {peerId: PeerId, threadId?: number, mid?: number, startParam?: string},
//'history_request': void,
'message_edit': {storageKey: MessagesStorageKey, peerId: PeerId, mid: number, message: MyMessage},
@ -104,8 +101,6 @@ export type BroadcastEvents = { @@ -104,8 +101,6 @@ export type BroadcastEvents = {
'settings_updated': {key: string, value: any, settings: State['settings']},
'draft_updated': {peerId: PeerId, threadId: number, draft: MyDraftMessage | undefined, force?: boolean},
'im_tab_change': number,
'background_change': void,
'privacy_update': Update.updatePrivacy,
@ -115,21 +110,12 @@ export type BroadcastEvents = { @@ -115,21 +110,12 @@ export type BroadcastEvents = {
'notification_reset': string,
'notification_cancel': string,
'notification_build': {
message: Message.message | Message.messageService,
fwdCount?: number,
peerReaction?: MessagePeerReaction,
peerTypeNotifySettings?: PeerNotifySettings
},
'language_change': string,
'theme_change': void,
'push_notification_click': PushNotificationObject,
'push_init': PushSubscriptionNotify,
'push_subscribe': PushSubscriptionNotify,
'push_unsubscribe': PushSubscriptionNotify,
'media_play': void,
'emoji_recent': string,
@ -153,9 +139,11 @@ export type BroadcastEvents = { @@ -153,9 +139,11 @@ export type BroadcastEvents = {
'logging_out': void
};
export class RootScope extends EventListenerBase<{
export type BroadcastEventsListeners = {
[name in keyof BroadcastEvents]: (e: BroadcastEvents[name]) => void
}> {
};
export class RootScope extends EventListenerBase<BroadcastEventsListeners> {
public myId: PeerId = NULL_PEER_ID;
private connectionStatus: {[name: string]: ConnectionStatusChange} = {};
public settings: State['settings'];

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

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

3
src/lib/sessionStorage.ts

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

4
src/lib/storage.ts

@ -292,11 +292,11 @@ export default class AppStorage< @@ -292,11 +292,11 @@ export default class AppStorage<
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) => {
storage.useStorage = enabled;
if(!IS_WORKER) {
if(!IS_WORKER || !clearWrite) {
return;
}

10
src/lib/storages/filters.ts

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

2
src/pages/pageSignIn.ts

@ -341,7 +341,7 @@ let onFirstMount = () => { @@ -341,7 +341,7 @@ let onFirstMount = () => {
const keepSigned = signedCheckboxField.checked;
rootScope.managers.appStateManager.pushToState('keepSigned', keepSigned);
apiManagerProxy.toggleStorages(keepSigned);
apiManagerProxy.toggleStorages(keepSigned, true);
});
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; @@ -946,6 +946,12 @@ $bubble-beside-button-width: 38px;
left: 50%;
transform: translate(-50%, -50%);
@include animation-level(2) {
.bubble-content-wrapper {
transition: var(--bubble-transition-in);
}
}
&.has-description {
.service-msg {
flex-direction: column;

Loading…
Cancel
Save