Browse Source

Support noforwards

Fix chat date blinking
Fix displaying sent messages to new dialog
Scroll to date bubble if message is bigger than viewport
Fix releasing keyboard by inline helper
Fix clearing self user
Fix displaying sent public poll
Update contacts counter in dialogs placeholder
Improve multiselect animation
Disable lottie icon animations if they're disabled
Fix changing mtproto transport during authorization
master
morethanwords 2 years ago
parent
commit
2319a60f4d
  1. 25
      src/components/appMediaViewer.ts
  2. 8
      src/components/appMediaViewerBase.ts
  3. 13
      src/components/appSearchSuper..ts
  4. 40
      src/components/appSelectPeers.ts
  5. 58
      src/components/call/button.ts
  6. 85
      src/components/call/description.ts
  7. 30
      src/components/call/videoCanvasBlur.ts
  8. 12
      src/components/chat/autocompleteHelper.ts
  9. 6
      src/components/chat/autocompleteHelperController.ts
  10. 566
      src/components/chat/bubbles.ts
  11. 15
      src/components/chat/chat.ts
  12. 28
      src/components/chat/contextMenu.ts
  13. 2
      src/components/chat/inlineHelper.ts
  14. 42
      src/components/chat/input.ts
  15. 85
      src/components/chat/messageRender.ts
  16. 53
      src/components/chat/selection.ts
  17. 31
      src/components/chat/topbar.ts
  18. 5
      src/components/confirmationPopup.ts
  19. 17
      src/components/groupCall/description.ts
  20. 162
      src/components/groupCall/index.ts
  21. 54
      src/components/groupCall/microphoneIconMini.ts
  22. 28
      src/components/groupCall/participantVideo.ts
  23. 15
      src/components/groupCall/participantVideos.ts
  24. 3
      src/components/groupCall/participants.ts
  25. 2
      src/components/groupCall/participantsList.ts
  26. 15
      src/components/groupCall/title.ts
  27. 8
      src/components/horizontalMenu.ts
  28. 20
      src/components/movableElement.ts
  29. 14
      src/components/peerProfile.ts
  30. 11
      src/components/peerProfileAvatars.ts
  31. 4
      src/components/poll.ts
  32. 10
      src/components/popups/createContact.ts
  33. 5
      src/components/popups/createPoll.ts
  34. 3
      src/components/popups/index.ts
  35. 22
      src/components/ripple.ts
  36. 18
      src/components/scrollable.ts
  37. 52
      src/components/sidebarLeft/index.ts
  38. 7
      src/components/sidebarLeft/tabs/activeSessions.ts
  39. 1
      src/components/sidebarLeft/tabs/addMembers.ts
  40. 4
      src/components/sidebarLeft/tabs/background.ts
  41. 6
      src/components/sidebarLeft/tabs/backgroundColor.ts
  42. 1
      src/components/sidebarLeft/tabs/blockedUsers.ts
  43. 11
      src/components/sidebarLeft/tabs/editFolder.ts
  44. 34
      src/components/sidebarLeft/tabs/editProfile.ts
  45. 1
      src/components/sidebarLeft/tabs/generalSettings.ts
  46. 15
      src/components/sidebarLeft/tabs/includedChats.ts
  47. 1
      src/components/sidebarLeft/tabs/language.ts
  48. 13
      src/components/sidebarLeft/tabs/newChannel.ts
  49. 33
      src/components/sidebarLeft/tabs/newGroup.ts
  50. 3
      src/components/sidebarLeft/tabs/notifications.ts
  51. 1
      src/components/sidebarLeft/tabs/privacy/addToGroups.ts
  52. 1
      src/components/sidebarLeft/tabs/privacy/calls.ts
  53. 1
      src/components/sidebarLeft/tabs/privacy/forwardMessages.ts
  54. 1
      src/components/sidebarLeft/tabs/privacy/lastSeen.ts
  55. 1
      src/components/sidebarLeft/tabs/privacy/profilePhoto.ts
  56. 37
      src/components/sidebarRight/tabs/chatType.ts
  57. 5
      src/components/sidebarRight/tabs/editChat.ts
  58. 5
      src/components/sidebarRight/tabs/sharedMedia.ts
  59. 51
      src/components/superIcon.ts
  60. 215
      src/components/topbarCall.ts
  61. 2
      src/config/app.ts
  62. 5
      src/environment/callSupport.ts
  63. 4
      src/environment/parallaxSupport.ts
  64. 60
      src/helpers/audioAssetPlayer.ts
  65. 28
      src/helpers/dom/attachListNavigation.ts
  66. 14
      src/helpers/dom/getVisibleRect.ts
  67. 23
      src/helpers/eachMinute.ts
  68. 31
      src/helpers/eachTimeout.ts
  69. 6
      src/helpers/eventListenerBase.ts
  70. 116
      src/helpers/fastSmoothScroll.ts
  71. 2
      src/helpers/formatPhoneNumber.ts
  72. 4
      src/helpers/middleware.ts
  73. 84
      src/helpers/movablePanel.ts
  74. 53
      src/lang.ts
  75. 655
      src/layer.d.ts
  76. 13
      src/lib/appManagers/appChatsManager.ts
  77. 75
      src/lib/appManagers/appDialogsManager.ts
  78. 1331
      src/lib/appManagers/appGroupCallsManager.ts
  79. 134
      src/lib/appManagers/appImManager.ts
  80. 141
      src/lib/appManagers/appMessagesManager.ts
  81. 14
      src/lib/appManagers/appPeersManager.ts
  82. 18
      src/lib/appManagers/appProfileManager.ts
  83. 6
      src/lib/appManagers/appStateManager.ts
  84. 18
      src/lib/appManagers/appStickersManager.ts
  85. 6
      src/lib/appManagers/appUsersManager.ts
  86. 98
      src/lib/calls/callConnectionInstanceBase.ts
  87. 222
      src/lib/calls/callInstanceBase.ts
  88. 17
      src/lib/calls/callState.ts
  89. 371
      src/lib/calls/groupCallConnectionInstance.ts
  90. 492
      src/lib/calls/groupCallInstance.ts
  91. 31
      src/lib/calls/helpers/createDataChannel.ts
  92. 30
      src/lib/calls/helpers/createMainStreamManager.ts
  93. 43
      src/lib/calls/helpers/createPeerConnection.ts
  94. 42
      src/lib/calls/helpers/filterServerCodecs.ts
  95. 101
      src/lib/calls/helpers/fixLocalOffer.ts
  96. 22
      src/lib/calls/helpers/getAudioConstraints.ts
  97. 12
      src/lib/calls/helpers/getScreenConstraints.ts
  98. 4
      src/lib/calls/helpers/getScreenStream.ts
  99. 20
      src/lib/calls/helpers/getStream.ts
  100. 65
      src/lib/calls/helpers/getStreamCached.ts
  101. Some files were not shown because too many files have changed in this diff Show More

25
src/components/appMediaViewer.ts

@ -31,8 +31,10 @@ type AppMediaViewerTargetType = {
peerId: PeerId peerId: PeerId
}; };
export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> { export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> {
protected btnMenuDelete: HTMLElement;
protected listLoader: SearchListLoader<AppMediaViewerTargetType>; protected listLoader: SearchListLoader<AppMediaViewerTargetType>;
protected btnMenuForward: ButtonMenuItemOptions;
protected btnMenuDownload: ButtonMenuItemOptions;
protected btnMenuDelete: ButtonMenuItemOptions;
get searchContext() { get searchContext() {
return this.listLoader.searchContext; return this.listLoader.searchContext;
@ -98,22 +100,21 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
attachClickEvent(this.buttons.delete, this.onDeleteClick); attachClickEvent(this.buttons.delete, this.onDeleteClick);
const buttons: ButtonMenuItemOptions[] = [{ const buttons: ButtonMenuItemOptions[] = [this.btnMenuForward = {
icon: 'forward', icon: 'forward',
text: 'Forward', text: 'Forward',
onClick: this.onForwardClick onClick: this.onForwardClick
}, { }, this.btnMenuDownload = {
icon: 'download', icon: 'download',
text: 'MediaViewer.Context.Download', text: 'MediaViewer.Context.Download',
onClick: this.onDownloadClick onClick: this.onDownloadClick
}, { }, this.btnMenuDelete = {
icon: 'delete danger', icon: 'delete danger',
text: 'Delete', text: 'Delete',
onClick: this.onDeleteClick onClick: this.onDeleteClick
}]; }];
this.setBtnMenuToggle(buttons); this.setBtnMenuToggle(buttons);
this.btnMenuDelete = buttons[buttons.length - 1].element;
// * constructing html end // * constructing html end
@ -256,10 +257,20 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const fromId = (message as Message.message).fwd_from && !message.fromId ? (message as Message.message).fwd_from.from_name : message.fromId; const fromId = (message as Message.message).fwd_from && !message.fromId ? (message as Message.message).fwd_from.from_name : message.fromId;
const media = appMessagesManager.getMediaFromMessage(message); const media = appMessagesManager.getMediaFromMessage(message);
this.buttons.forward.classList.toggle('hide', message._ === 'messageService'); const cantForwardMessage = message._ === 'messageService' || !appMessagesManager.canForward(message);
[this.buttons.forward, this.btnMenuForward.element].forEach(button => {
button.classList.toggle('hide', cantForwardMessage);
});
this.wholeDiv.classList.toggle('no-forwards', cantForwardMessage);
const cantDownloadMessage = cantForwardMessage;
[this.buttons.download, this.btnMenuDownload.element].forEach(button => {
button.classList.toggle('hide', cantDownloadMessage);
});
const canDeleteMessage = appMessagesManager.canDeleteMessage(message); const canDeleteMessage = appMessagesManager.canDeleteMessage(message);
[this.buttons.delete, this.btnMenuDelete].forEach(button => { [this.buttons.delete, this.btnMenuDelete.element].forEach(button => {
button.classList.toggle('hide', !canDeleteMessage); button.classList.toggle('hide', !canDeleteMessage);
}); });

8
src/components/appMediaViewerBase.ts

@ -610,7 +610,7 @@ export default class AppMediaViewerBase<
let needOpacity = false; let needOpacity = false;
if(target !== this.content.media && !target.classList.contains('profile-avatars-avatar')) { if(target !== this.content.media && !target.classList.contains('profile-avatars-avatar')) {
const overflowElement = findUpClassName(realParent, 'scrollable'); const overflowElement = findUpClassName(realParent, 'scrollable');
const visibleRect = getVisibleRect(realParent, overflowElement); const visibleRect = getVisibleRect(realParent, overflowElement, true);
if(closing && (!visibleRect || visibleRect.overflow.vertical === 2 || visibleRect.overflow.horizontal === 2)) { if(closing && (!visibleRect || visibleRect.overflow.vertical === 2 || visibleRect.overflow.horizontal === 2)) {
target = this.content.media; target = this.content.media;
@ -1411,6 +1411,12 @@ export default class AppMediaViewerBase<
} }
}); });
if(this.wholeDiv.classList.contains('no-forwards')) {
video.addEventListener('contextmenu', (e) => {
cancelEvent(e);
});
}
attachCanPlay(); attachCanPlay();
} }

13
src/components/appSearchSuper..ts

@ -145,7 +145,8 @@ class SearchContextMenu {
this.buttons = [{ this.buttons = [{
icon: 'forward', icon: 'forward',
text: 'Forward', text: 'Forward',
onClick: this.onForwardClick onClick: this.onForwardClick,
verify: () => appMessagesManager.canForward(appMessagesManager.getMessageByPeer(this.peerId, this.mid))
}, { }, {
icon: 'forward', icon: 'forward',
text: 'Message.Context.Selection.Forward', text: 'Message.Context.Selection.Forward',
@ -391,7 +392,10 @@ export default class AppSearchSuper {
this.selectTab = horizontalMenu(this.tabsMenu, this.tabsContainer, (id, tabContent, animate) => { this.selectTab = horizontalMenu(this.tabsMenu, this.tabsContainer, (id, tabContent, animate) => {
if(this.prevTabId === id && !this.skipScroll) { if(this.prevTabId === id && !this.skipScroll) {
this.scrollable.scrollIntoViewNew(this.container, 'start'); this.scrollable.scrollIntoViewNew({
element: this.container,
position: 'start'
});
return; return;
} }
@ -413,7 +417,10 @@ export default class AppSearchSuper {
const offsetTop = this.container.offsetTop; const offsetTop = this.container.offsetTop;
let scrollTop = this.scrollable.scrollTop; let scrollTop = this.scrollable.scrollTop;
if(scrollTop < offsetTop) { if(scrollTop < offsetTop) {
this.scrollable.scrollIntoViewNew(this.container, 'start'); this.scrollable.scrollIntoViewNew({
element: this.container,
position: 'start'
});
scrollTop = offsetTop; scrollTop = offsetTop;
} }

40
src/components/appSelectPeers.ts

@ -24,6 +24,7 @@ import { filterUnique, indexOfAndSplice } from "../helpers/array";
import debounce from "../helpers/schedulers/debounce"; import debounce from "../helpers/schedulers/debounce";
import windowSize from "../helpers/windowSize"; import windowSize from "../helpers/windowSize";
import appPeersManager, { IsPeerType } from "../lib/appManagers/appPeersManager"; import appPeersManager, { IsPeerType } from "../lib/appManagers/appPeersManager";
import { generateDelimiter, SettingSection } from "./sidebarLeft";
type SelectSearchPeerType = 'contacts' | 'dialogs' | 'channelParticipants'; type SelectSearchPeerType = 'contacts' | 'dialogs' | 'channelParticipants';
@ -35,11 +36,11 @@ export default class AppSelectPeers {
handheldsSize: 66, handheldsSize: 66,
avatarSize: 48 avatarSize: 48
} */); } */);
public chatsContainer = document.createElement('div'); private chatsContainer = document.createElement('div');
public scrollable: Scrollable; public scrollable: Scrollable;
public selectedScrollable: Scrollable; private selectedScrollable: Scrollable;
public selectedContainer: HTMLElement; private selectedContainer: HTMLElement;
public input: HTMLInputElement; public input: HTMLInputElement;
//public selected: {[peerId: PeerId]: HTMLElement} = {}; //public selected: {[peerId: PeerId]: HTMLElement} = {};
@ -78,6 +79,8 @@ export default class AppSelectPeers {
private needSwitchList = false; private needSwitchList = false;
private sectionNameLangPackKey: LangPackKey;
constructor(options: { constructor(options: {
appendTo: AppSelectPeers['appendTo'], appendTo: AppSelectPeers['appendTo'],
onChange?: AppSelectPeers['onChange'], onChange?: AppSelectPeers['onChange'],
@ -92,7 +95,8 @@ export default class AppSelectPeers {
placeholder?: AppSelectPeers['placeholder'], placeholder?: AppSelectPeers['placeholder'],
selfPresence?: AppSelectPeers['selfPresence'], selfPresence?: AppSelectPeers['selfPresence'],
exceptSelf?: AppSelectPeers['exceptSelf'], exceptSelf?: AppSelectPeers['exceptSelf'],
filterPeerTypeBy?: AppSelectPeers['filterPeerTypeBy'] filterPeerTypeBy?: AppSelectPeers['filterPeerTypeBy'],
sectionNameLangPackKey?: AppSelectPeers['sectionNameLangPackKey']
}) { }) {
safeAssign(this, options); safeAssign(this, options);
@ -139,6 +143,8 @@ export default class AppSelectPeers {
this.input.type = 'text'; this.input.type = 'text';
if(this.multiSelect) { if(this.multiSelect) {
const section = new SettingSection({});
section.innerContainer.classList.add('selector-search-section');
let topContainer = document.createElement('div'); let topContainer = document.createElement('div');
topContainer.classList.add('selector-search-container'); topContainer.classList.add('selector-search-container');
@ -149,7 +155,7 @@ export default class AppSelectPeers {
topContainer.append(this.selectedContainer); topContainer.append(this.selectedContainer);
this.selectedScrollable = new Scrollable(topContainer); this.selectedScrollable = new Scrollable(topContainer);
let delimiter = document.createElement('hr'); // let delimiter = document.createElement('hr');
this.selectedContainer.addEventListener('click', (e) => { this.selectedContainer.addEventListener('click', (e) => {
if(this.freezed) return; if(this.freezed) return;
@ -167,11 +173,18 @@ export default class AppSelectPeers {
} }
}); });
this.container.append(topContainer, delimiter); section.content.append(topContainer);
this.container.append(section.container/* , delimiter */);
} }
this.chatsContainer.classList.add('chatlist-container'); this.chatsContainer.classList.add('chatlist-container');
this.chatsContainer.append(this.list); // this.chatsContainer.append(this.list);
const section = new SettingSection({
name: this.sectionNameLangPackKey,
noShadow: true
});
section.content.append(this.list);
this.chatsContainer.append(section.container);
this.scrollable = new Scrollable(this.chatsContainer); this.scrollable = new Scrollable(this.chatsContainer);
this.scrollable.setVirtualContainer(this.list); this.scrollable.setVirtualContainer(this.list);
@ -208,6 +221,8 @@ export default class AppSelectPeers {
this.getMoreResults(); this.getMoreResults();
}; };
this.scrollable.container.prepend(generateDelimiter());
this.container.append(this.chatsContainer); this.container.append(this.chatsContainer);
this.appendTo.append(this.container); this.appendTo.append(this.container);
@ -565,7 +580,10 @@ export default class AppSelectPeers {
this.onChange && this.onChange(this.selected.size); this.onChange && this.onChange(this.selected.size);
if(scroll) { if(scroll) {
this.selectedScrollable.scrollIntoViewNew(this.input, 'center'); this.selectedScrollable.scrollIntoViewNew({
element: this.input,
position: 'center'
});
} }
return div; return div;
@ -602,7 +620,11 @@ export default class AppSelectPeers {
}); });
window.requestAnimationFrame(() => { // ! not the best place for this raf though it works window.requestAnimationFrame(() => { // ! not the best place for this raf though it works
this.selectedScrollable.scrollIntoViewNew(this.input, 'center', undefined, undefined, FocusDirection.Static); this.selectedScrollable.scrollIntoViewNew({
element: this.input,
position: 'center',
forceDirection: FocusDirection.Static
});
}); });
} }
} }

58
src/components/call/button.ts

@ -0,0 +1,58 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import ListenerSetter from "../../helpers/listenerSetter";
import { i18n, LangPackKey } from "../../lib/langPack";
import { ripple } from "../ripple";
export default function makeButton(className: string, listenerSetter: ListenerSetter, options: {
text?: LangPackKey | HTMLElement,
isDanger?: boolean,
noRipple?: boolean,
callback?: () => void,
icon?: string,
isConfirm?: boolean,
}) {
const _className = className + '-button';
const buttonDiv = document.createElement('div');
buttonDiv.classList.add(_className, 'call-button', 'rp-overflow');
if(options.icon) {
buttonDiv.classList.add('tgico-' + options.icon);
}
if(!options.noRipple) {
ripple(buttonDiv);
}
if(options.isDanger) {
buttonDiv.classList.add(_className + '-red');
}
if(options.isConfirm) {
buttonDiv.classList.add(_className + '-green');
}
if(options.callback) {
attachClickEvent(buttonDiv, options.callback, {listenerSetter});
}
let ret = buttonDiv;
if(options.text) {
const div = document.createElement('div');
div.classList.add(_className + '-container', 'call-button-container');
const textEl = typeof(options.text) === 'string' ? i18n(options.text) : options.text;
textEl.classList.add(_className + '-text', 'call-button-text');
div.append(buttonDiv, textEl);
ret = div;
}
return ret;
}

85
src/components/call/description.ts

@ -0,0 +1,85 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import replaceContent from "../../helpers/dom/replaceContent";
import type CallInstance from "../../lib/calls/callInstance";
import CALL_STATE from "../../lib/calls/callState";
import { i18n, LangPackKey } from "../../lib/langPack";
export default class CallDescriptionElement {
private container: HTMLElement;
private state: CALL_STATE;
private interval: number;
constructor(private appendTo: HTMLElement) {
this.container = document.createElement('div');
this.container.classList.add('call-description');
}
public detach() {
if(this.interval !== undefined) {
clearInterval(this.interval);
this.interval = undefined;
}
this.container.remove();
this.state = undefined;
}
public update(instance: CallInstance) {
const {connectionState} = instance;
if(this.state === connectionState) {
return;
}
this.state = connectionState;
let element: HTMLElement;
if(connectionState === CALL_STATE.CONNECTED) {
element = document.createElement('span');
element.classList.add('call-description-duration');
const setTime = () => {
element.innerText = ('' + instance.duration).toHHMMSS(true);
};
this.interval = window.setInterval(setTime, 1000);
setTime();
} else {
let langPackKey: LangPackKey;
switch(connectionState) {
case CALL_STATE.PENDING:
langPackKey = instance.isOutgoing ? 'Call.StatusRinging' : 'Call.StatusCalling';
break;
case CALL_STATE.REQUESTING:
langPackKey = 'Call.StatusRequesting';
break;
case CALL_STATE.EXCHANGING_KEYS:
langPackKey = 'VoipExchangingKeys';
break;
case CALL_STATE.CLOSED:
langPackKey = instance.connectedAt !== undefined ? 'Call.StatusEnded' : 'Call.StatusFailed';
break;
default:
langPackKey = 'Call.StatusConnecting';
break;
}
element = i18n(langPackKey);
if(this.interval !== undefined) {
clearInterval(this.interval);
this.interval = undefined;
}
}
replaceContent(this.container, element);
if(!this.container.parentElement) {
this.appendTo.append(this.container);
}
}
}

30
src/components/call/videoCanvasBlur.ts

@ -0,0 +1,30 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { animate } from "../../helpers/animation";
export default function callVideoCanvasBlur(video: HTMLVideoElement) {
const canvas = document.createElement('canvas');
canvas.classList.add('call-video-blur');
const size = 16;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.filter = 'blur(2px)';
const renderFrame = () => {
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);
};
animate(() => {
renderFrame();
return canvas.isConnected;
});
renderFrame();
return canvas;
}

12
src/components/chat/autocompleteHelper.ts

@ -21,6 +21,7 @@ export default class AutocompleteHelper extends EventListenerBase<{
protected container: HTMLElement; protected container: HTMLElement;
protected list: HTMLElement; protected list: HTMLElement;
protected resetTarget: () => void; protected resetTarget: () => void;
protected attach: () => void;
protected detach: () => void; protected detach: () => void;
protected init?(): void; protected init?(): void;
@ -52,13 +53,21 @@ export default class AutocompleteHelper extends EventListenerBase<{
this.controller.addHelper(this); this.controller.addHelper(this);
} }
public toggleListNavigation(enabled: boolean) {
if(enabled) {
this.attach && this.attach();
} else {
this.detach && this.detach();
}
}
protected onVisible = () => { protected onVisible = () => {
if(this.detach) { // it can be so because 'visible' calls before animation's end if(this.detach) { // it can be so because 'visible' calls before animation's end
this.detach(); this.detach();
} }
const list = this.list; const list = this.list;
const {detach, resetTarget} = attachListNavigation({ const {attach, detach, resetTarget} = attachListNavigation({
list, list,
type: this.listType, type: this.listType,
onSelect: this.onSelect, onSelect: this.onSelect,
@ -66,6 +75,7 @@ export default class AutocompleteHelper extends EventListenerBase<{
waitForKey: this.waitForKey waitForKey: this.waitForKey
}); });
this.attach = attach;
this.detach = detach; this.detach = detach;
this.resetTarget = resetTarget; this.resetTarget = resetTarget;
if(!IS_MOBILE && !this.navigationItem) { if(!IS_MOBILE && !this.navigationItem) {

6
src/components/chat/autocompleteHelperController.ts

@ -20,6 +20,12 @@ export default class AutocompleteHelperController {
return this.tempId; return this.tempId;
} */ } */
public toggleListNavigation(enabled: boolean) {
for(const helper of this.helpers) {
helper.toggleListNavigation(enabled);
}
}
public getMiddleware() { public getMiddleware() {
this.middleware.clean(); this.middleware.clean();
return this.middleware.get(); return this.middleware.get();

566
src/components/chat/bubbles.ts

File diff suppressed because it is too large Load Diff

15
src/components/chat/chat.ts

@ -57,7 +57,7 @@ export default class Chat extends EventListenerBase<{
public contextMenu: ChatContextMenu; public contextMenu: ChatContextMenu;
public search: ChatSearch; public search: ChatSearch;
public wasAlreadyUsed = false; public wasAlreadyUsed: boolean;
// public initPeerId = 0; // public initPeerId = 0;
public peerId: PeerId; public peerId: PeerId;
public threadId: number; public threadId: number;
@ -66,11 +66,12 @@ export default class Chat extends EventListenerBase<{
public log: ReturnType<typeof logger>; public log: ReturnType<typeof logger>;
public type: ChatType = 'chat'; public type: ChatType;
public noAutoDownloadMedia: boolean; public noAutoDownloadMedia: boolean;
public noForwards: boolean;
public inited = false; public inited: boolean;
constructor(public appImManager: AppImManager, constructor(public appImManager: AppImManager,
public appChatsManager: AppChatsManager, public appChatsManager: AppChatsManager,
@ -95,6 +96,8 @@ export default class Chat extends EventListenerBase<{
) { ) {
super(); super();
this.type = 'chat';
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('chat', 'tabs-tab'); this.container.classList.add('chat', 'tabs-tab');
@ -177,7 +180,7 @@ export default class Chat extends EventListenerBase<{
// this.initPeerId = peerId; // this.initPeerId = peerId;
this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appNotificationsManager, this.appProfileManager, this.appUsersManager, this.appGroupCallsManager); this.topbar = new ChatTopbar(this, appSidebarRight, this.appMessagesManager, this.appPeersManager, this.appChatsManager, this.appNotificationsManager, this.appProfileManager, this.appUsersManager, this.appGroupCallsManager);
this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appPeersManager, this.appProfileManager, this.appDraftsManager, this.appMessagesIdsManager); this.bubbles = new ChatBubbles(this, this.appMessagesManager, this.appStickersManager, this.appUsersManager, this.appInlineBotsManager, this.appPhotosManager, this.appPeersManager, this.appProfileManager, this.appDraftsManager, this.appMessagesIdsManager, this.appChatsManager);
this.input = new ChatInput(this, this.appMessagesManager, this.appMessagesIdsManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager, this.appNotificationsManager, this.appEmojiManager, this.appUsersManager, this.appInlineBotsManager); this.input = new ChatInput(this, this.appMessagesManager, this.appMessagesIdsManager, this.appDocsManager, this.appChatsManager, this.appPeersManager, this.appWebPagesManager, this.appImManager, this.appDraftsManager, this.serverTimeManager, this.appNotificationsManager, this.appEmojiManager, this.appUsersManager, this.appInlineBotsManager);
this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager); this.selection = new ChatSelection(this, this.bubbles, this.input, this.appMessagesManager);
this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager, this.appMessagesIdsManager); this.contextMenu = new ChatContextMenu(this.bubbles.bubblesContainer, this, this.appMessagesManager, this.appPeersManager, this.appPollsManager, this.appDocsManager, this.appMessagesIdsManager);
@ -254,7 +257,7 @@ export default class Chat extends EventListenerBase<{
public setPeer(peerId: PeerId, lastMsgId?: number) { public setPeer(peerId: PeerId, lastMsgId?: number) {
if(!peerId) { if(!peerId) {
this.inited = false; this.inited = undefined;
} else if(!this.inited) { } else if(!this.inited) {
if(this.init) { if(this.init) {
this.init(/* peerId */); this.init(/* peerId */);
@ -268,6 +271,8 @@ export default class Chat extends EventListenerBase<{
if(!samePeer) { if(!samePeer) {
rootScope.dispatchEvent('peer_changing', this); rootScope.dispatchEvent('peer_changing', this);
this.peerId = peerId; this.peerId = peerId;
this.noForwards = this.appPeersManager.noForwards(peerId);
this.container.classList.toggle('no-forwards', this.noForwards);
} else if(this.setPeerPromise) { } else if(this.setPeerPromise) {
return; return;
} }

28
src/components/chat/contextMenu.ts

@ -24,8 +24,9 @@ import findUpClassName from "../../helpers/dom/findUpClassName";
import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty"; import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty";
import { Message, Poll } from "../../layer"; import { Message, Poll, Chat as MTChat, MessageMedia } from "../../layer";
import PopupReportMessages from "../popups/reportMessages"; import PopupReportMessages from "../popups/reportMessages";
import assumeType from "../../helpers/assumeType";
export default class ChatContextMenu { export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true})[]; private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true})[];
@ -40,7 +41,8 @@ export default class ChatContextMenu {
private isUsernameTarget: boolean; private isUsernameTarget: boolean;
private peerId: PeerId; private peerId: PeerId;
private mid: number; private mid: number;
private message: any; private message: Message.message | Message.messageService;
private noForwards: boolean;
constructor(private attachTo: HTMLElement, constructor(private attachTo: HTMLElement,
private chat: Chat, private chat: Chat,
@ -109,6 +111,7 @@ export default class ChatContextMenu {
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid); this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
this.message = this.chat.getMessage(this.mid); this.message = this.chat.getMessage(this.mid);
this.noForwards = !this.appMessagesManager.canForward(this.message);
this.buttons.forEach(button => { this.buttons.forEach(button => {
let good: boolean; let good: boolean;
@ -176,6 +179,7 @@ export default class ChatContextMenu {
text: 'MessageScheduleEditTime', text: 'MessageScheduleEditTime',
onClick: () => { onClick: () => {
this.chat.input.scheduleSending(() => { this.chat.input.scheduleSending(() => {
assumeType<Message.message>(this.message);
this.appMessagesManager.editMessage(this.message, this.message.message, { this.appMessagesManager.editMessage(this.message, this.message.message, {
scheduleDate: this.chat.input.scheduleDate, scheduleDate: this.chat.input.scheduleDate,
entities: this.message.entities entities: this.message.entities
@ -203,18 +207,18 @@ export default class ChatContextMenu {
icon: 'copy', icon: 'copy',
text: 'Copy', text: 'Copy',
onClick: this.onCopyClick, onClick: this.onCopyClick,
verify: () => !!this.message.message && !this.isTextSelected && (!this.isAnchorTarget || this.message.message !== this.target.innerText) verify: () => !this.noForwards && !!(this.message as Message.message).message && !this.isTextSelected && (!this.isAnchorTarget || (this.message as Message.message).message !== this.target.innerText)
}, { }, {
icon: 'copy', icon: 'copy',
text: 'Chat.CopySelectedText', text: 'Chat.CopySelectedText',
onClick: this.onCopyClick, onClick: this.onCopyClick,
verify: () => !!this.message.message && this.isTextSelected verify: () => !this.noForwards && !!(this.message as Message.message).message && this.isTextSelected
}, { }, {
icon: 'copy', icon: 'copy',
text: 'Message.Context.Selection.Copy', text: 'Message.Context.Selection.Copy',
onClick: this.onCopyClick, onClick: this.onCopyClick,
verify: () => { verify: () => {
if(!this.isSelected) { if(!this.isSelected || this.noForwards) {
return false; return false;
} }
@ -270,19 +274,19 @@ export default class ChatContextMenu {
icon: 'unpin', icon: 'unpin',
text: 'Message.Context.Unpin', text: 'Message.Context.Unpin',
onClick: this.onUnpinClick, onClick: this.onUnpinClick,
verify: () => this.message.pFlags.pinned && this.appPeersManager.canPinMessage(this.peerId), verify: () => (this.message as Message.message).pFlags.pinned && this.appPeersManager.canPinMessage(this.peerId),
}, { }, {
icon: 'download', icon: 'download',
text: 'MediaViewer.Context.Download', text: 'MediaViewer.Context.Download',
onClick: () => { onClick: () => {
this.appDocsManager.saveDocFile(this.message.media.document); this.appDocsManager.saveDocFile((this.message as any).media.document);
}, },
verify: () => { verify: () => {
if(this.message.pFlags.is_outgoing) { if(this.message.pFlags.is_outgoing) {
return false; return false;
} }
const doc: MyDocument = this.message.media?.document; const doc: MyDocument = ((this.message as Message.message).media as MessageMedia.messageMediaDocument)?.document as any;
if(!doc) return false; if(!doc) return false;
let hasTarget = !!IS_TOUCH_SUPPORTED; let hasTarget = !!IS_TOUCH_SUPPORTED;
@ -295,7 +299,7 @@ export default class ChatContextMenu {
text: 'Chat.Poll.Unvote', text: 'Chat.Poll.Unvote',
onClick: this.onRetractVote, onClick: this.onRetractVote,
verify: () => { verify: () => {
const poll = this.message.media?.poll as Poll; const poll = (this.message as any).media?.poll as Poll;
return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz; return poll && poll.chosenIndexes.length && !poll.pFlags.closed && !poll.pFlags.quiz;
}/* , }/* ,
cancelEvent: true */ cancelEvent: true */
@ -304,7 +308,7 @@ export default class ChatContextMenu {
text: 'Chat.Poll.Stop', text: 'Chat.Poll.Stop',
onClick: this.onStopPoll, onClick: this.onStopPoll,
verify: () => { verify: () => {
const poll = this.message.media?.poll; const poll = (this.message as any).media?.poll;
return this.appMessagesManager.canEditMessage(this.message, 'poll') && poll && !poll.pFlags.closed && !this.message.pFlags.is_outgoing; return this.appMessagesManager.canEditMessage(this.message, 'poll') && poll && !poll.pFlags.closed && !this.message.pFlags.is_outgoing;
}/* , }/* ,
cancelEvent: true */ cancelEvent: true */
@ -312,7 +316,7 @@ export default class ChatContextMenu {
icon: 'forward', icon: 'forward',
text: 'Forward', text: 'Forward',
onClick: this.onForwardClick, // let forward the message if it's outgoing but not ours (like a changelog) onClick: this.onForwardClick, // let forward the message if it's outgoing but not ours (like a changelog)
verify: () => this.chat.type !== 'scheduled' && (!this.message.pFlags.is_outgoing || !this.message.pFlags.out) && this.message._ !== 'messageService' verify: () => !this.noForwards && this.chat.type !== 'scheduled' && (!this.message.pFlags.is_outgoing || !this.message.pFlags.out) && this.message._ !== 'messageService'
}, { }, {
icon: 'forward', icon: 'forward',
text: 'Message.Context.Selection.Forward', text: 'Message.Context.Selection.Forward',
@ -335,7 +339,7 @@ export default class ChatContextMenu {
icon: 'select', icon: 'select',
text: 'Message.Context.Select', text: 'Message.Context.Select',
onClick: this.onSelectClick, onClick: this.onSelectClick,
verify: () => !this.message.action && !this.isSelected && this.isSelectable, verify: () => !(this.message as Message.messageService).action && !this.isSelected && this.isSelectable,
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}, { }, {

2
src/components/chat/inlineHelper.ts

@ -46,7 +46,9 @@ export default class InlineHelper extends AutocompleteHelper {
appendTo, appendTo,
controller, controller,
listType: 'xy', listType: 'xy',
waitForKey: 'ArrowUp',
onSelect: (target) => { onSelect: (target) => {
if(!target) return false; // can happen when there is only button
const {peerId, botId, queryId} = this.list.dataset; const {peerId, botId, queryId} = this.list.dataset;
return this.chat.input.getReadyToSend(() => { return this.chat.input.getReadyToSend(() => {
const queryAndResultIds = this.appInlineBotsManager.generateQId(queryId, (target as HTMLElement).dataset.resultId); const queryAndResultIds = this.appInlineBotsManager.generateQId(queryId, (target as HTMLElement).dataset.resultId);

42
src/components/chat/input.ts

@ -198,6 +198,8 @@ export default class ChatInput {
private previousQuery: string; private previousQuery: string;
private releaseMediaPlayback: () => void; private releaseMediaPlayback: () => void;
botStartBtn: HTMLButtonElement;
fakeBotStartBtn: HTMLElement;
constructor(private chat: Chat, constructor(private chat: Chat,
private appMessagesManager: AppMessagesManager, private appMessagesManager: AppMessagesManager,
@ -636,6 +638,14 @@ export default class ChatInput {
} }
}); });
this.listenerSetter.add(rootScope)('chat_changing', ({from, to}) => {
if(this.chat === from) {
this.autocompleteHelperController.toggleListNavigation(false);
} else if(this.chat === to) {
this.autocompleteHelperController.toggleListNavigation(true);
}
});
if(this.chat.type === 'scheduled') { if(this.chat.type === 'scheduled') {
this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId, mids}) => { this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId, mids}) => {
if(this.chat.peerId === peerId && mids.includes(this.editMsgId)) { if(this.chat.peerId === peerId && mids.includes(this.editMsgId)) {
@ -765,6 +775,31 @@ export default class ChatInput {
attachClickEvent(this.replyElements.container, this.onHelperClick, {listenerSetter: this.listenerSetter}); attachClickEvent(this.replyElements.container, this.onHelperClick, {listenerSetter: this.listenerSetter});
this.saveDraftDebounced = debounce(() => this.saveDraft(), 2500, false, true); this.saveDraftDebounced = debounce(() => this.saveDraft(), 2500, false, true);
/* this.constructCenteredContainer((container, fakeContainer) => {
this.botStartBtn = Button('btn-primary btn-transparent text-bold');
container.append(this.botStartBtn);
this.fakeBotStartBtn = this.botStartBtn.cloneNode(true) as HTMLElement;
fakeContainer.append(this.fakeBotStartBtn);
this.botStartBtn.append(i18n('BotStart'));
this.fakeBotStartBtn.append(i18n('BotStart'));
}); */
}
private constructCenteredContainer(fill: (container: HTMLElement, fakeContainer: HTMLElement) => void) {
const container = document.createElement('div');
container.classList.add('input-centered-container', 'rows-wrapper', 'is-centered', 'chat-input-wrapper');
const fakeContainer = container.cloneNode(true) as HTMLElement;
fakeContainer.classList.add('fake-wrapper', 'fake-input-centered-container');
fill(container, fakeContainer);
this.inputContainer.append(container, fakeContainer);
return container;
} }
public constructPinnedHelpers() { public constructPinnedHelpers() {
@ -1030,10 +1065,7 @@ export default class ChatInput {
key = 'Message'; key = 'Message';
} }
if(i.key !== key) { i.compareAndUpdate({key});
i.key = key;
i.update();
}
} }
const visible = this.attachMenuButtons.filter(button => { const visible = this.attachMenuButtons.filter(button => {
@ -1539,7 +1571,7 @@ export default class ChatInput {
entities = RichTextProcessor.mergeEntities(entities, RichTextProcessor.parseEntities(_value)); entities = RichTextProcessor.mergeEntities(entities, RichTextProcessor.parseEntities(_value));
} }
value = value.substr(0, caretPos); value = value.slice(0, caretPos);
if(this.previousQuery === value) { if(this.previousQuery === value) {
return; return;

85
src/components/chat/messageRender.ts

@ -24,72 +24,91 @@ const makeEdited = () => {
return edited; return edited;
}; };
const makeSponsored = () => i18n('SponsoredMessage');
export namespace MessageRender { export namespace MessageRender {
/* export const setText = () => { /* export const setText = () => {
}; */ }; */
export const setTime = (chat: Chat, message: Message.message, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => { export const setTime = (chat: Chat, message: Message.message | Message.messageService, bubble: HTMLElement, bubbleContainer: HTMLElement, messageDiv: HTMLElement) => {
const date = new Date(message.date * 1000); const date = new Date(message.date * 1000);
const args: (HTMLElement | string)[] = []; const args: (HTMLElement | string)[] = [];
let time = formatTime(date);
if(message.views) { let editedSpan: HTMLElement, sponsoredSpan: HTMLElement;
const postAuthor = message.post_author || message.fwd_from?.post_author;
const isSponsored = !!(message as Message.message).pFlags.sponsored;
const isMessage = !('action' in message) && !isSponsored;
let time: HTMLElement = isSponsored ? undefined : formatTime(date);
if(isMessage) {
if(message.views) {
const postAuthor = message.post_author || message.fwd_from?.post_author;
bubble.classList.add('channel-post'); bubble.classList.add('channel-post');
const postViewsSpan = document.createElement('span'); const postViewsSpan = document.createElement('span');
postViewsSpan.classList.add('post-views'); postViewsSpan.classList.add('post-views');
postViewsSpan.innerHTML = formatNumber(message.views, 1); postViewsSpan.innerHTML = formatNumber(message.views, 1);
const channelViews = document.createElement('i'); const channelViews = document.createElement('i');
channelViews.classList.add('tgico-channelviews', 'time-icon'); channelViews.classList.add('tgico-channelviews', 'time-icon');
args.push(postViewsSpan, channelViews); args.push(postViewsSpan, channelViews);
if(postAuthor) { if(postAuthor) {
const span = document.createElement('span'); const span = document.createElement('span');
span.innerHTML = RichTextProcessor.wrapEmojiText(postAuthor) + ',' + NBSP; span.innerHTML = RichTextProcessor.wrapEmojiText(postAuthor) + ',' + NBSP;
args.push(span); args.push(span);
}
} }
}
let editedSpan: HTMLElement; if(message.edit_date && chat.type !== 'scheduled' && !message.pFlags.edit_hide) {
if(message.edit_date && chat.type !== 'scheduled' && !message.pFlags.edit_hide) { bubble.classList.add('is-edited');
bubble.classList.add('is-edited');
args.unshift(editedSpan = makeEdited()); args.unshift(editedSpan = makeEdited());
} }
if(chat.type !== 'pinned' && message.pFlags.pinned) { if(chat.type !== 'pinned' && message.pFlags.pinned) {
bubble.classList.add('is-pinned'); bubble.classList.add('is-pinned');
const i = document.createElement('i'); const i = document.createElement('i');
i.classList.add('tgico-pinnedchat', 'time-icon'); i.classList.add('tgico-pinnedchat', 'time-icon');
args.unshift(i); args.unshift(i);
}
} else if(isSponsored) {
args.push(sponsoredSpan = makeSponsored());
} }
args.push(time); if(time) {
args.push(time);
}
const title = getFullDate(date) let title = isSponsored ? undefined : getFullDate(date);
+ (message.edit_date ? `\nEdited: ${getFullDate(new Date(message.edit_date * 1000))}` : '') if(isMessage) {
+ (message.fwd_from ? `\nOriginal: ${getFullDate(new Date(message.fwd_from.date * 1000))}` : ''); title += (message.edit_date ? `\nEdited: ${getFullDate(new Date(message.edit_date * 1000))}` : '')
+ (message.fwd_from ? `\nOriginal: ${getFullDate(new Date(message.fwd_from.date * 1000))}` : '');
}
const timeSpan = document.createElement('span'); const timeSpan = document.createElement('span');
timeSpan.classList.add('time', 'tgico'); timeSpan.classList.add('time', 'tgico');
timeSpan.title = title; if(title) timeSpan.title = title;
timeSpan.append(...args); timeSpan.append(...args);
const inner = document.createElement('div'); const inner = document.createElement('div');
inner.classList.add('inner', 'tgico'); inner.classList.add('inner', 'tgico');
inner.title = title; if(title) inner.title = title;
let clonedArgs = args; let clonedArgs = args;
if(editedSpan) { if(editedSpan) {
clonedArgs[clonedArgs.indexOf(editedSpan)] = makeEdited(); clonedArgs[clonedArgs.indexOf(editedSpan)] = makeEdited();
} }
if(sponsoredSpan) {
clonedArgs[clonedArgs.indexOf(sponsoredSpan)] = makeSponsored();
}
clonedArgs = clonedArgs.map(a => a instanceof HTMLElement && !a.classList.contains('i18n') ? a.cloneNode(true) as HTMLElement : a); clonedArgs = clonedArgs.map(a => a instanceof HTMLElement && !a.classList.contains('i18n') ? a.cloneNode(true) as HTMLElement : a);
clonedArgs[clonedArgs.length - 1] = formatTime(date); // clone time if(time) {
clonedArgs[clonedArgs.length - 1] = formatTime(date); // clone time
}
inner.append(...clonedArgs); inner.append(...clonedArgs);
timeSpan.append(inner); timeSpan.append(inner);

53
src/components/chat/selection.ts

@ -332,16 +332,11 @@ class AppSelection {
for(const mid of mids) { for(const mid of mids) {
const message = this.appMessagesManager.getMessageFromStorage(storage, mid); const message = this.appMessagesManager.getMessageFromStorage(storage, mid);
if(!cantForward) { if(!cantForward) {
if(message.action) { cantForward = !this.appMessagesManager.canForward(message);
cantForward = true;
}
} }
if(!cantDelete) { if(!cantDelete) {
const canDelete = this.appMessagesManager.canDeleteMessage(message); cantDelete = !this.appMessagesManager.canDeleteMessage(message);
if(!canDelete) {
cantDelete = true;
}
} }
if(cantForward && cantDelete) break; if(cantForward && cantDelete) break;
@ -666,6 +661,8 @@ export default class ChatSelection extends AppSelection {
public selectionSendNowBtn: HTMLElement; public selectionSendNowBtn: HTMLElement;
public selectionForwardBtn: HTMLElement; public selectionForwardBtn: HTMLElement;
public selectionDeleteBtn: HTMLElement; public selectionDeleteBtn: HTMLElement;
selectionLeft: HTMLDivElement;
selectionRight: HTMLDivElement;
constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, appMessagesManager: AppMessagesManager) { constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, appMessagesManager: AppMessagesManager) {
super({ super({
@ -822,8 +819,8 @@ export default class ChatSelection extends AppSelection {
} }
protected onToggleSelection = (forwards: boolean) => { protected onToggleSelection = (forwards: boolean) => {
let transform = '', borderRadius = ''; let transform = '', borderRadius = '', needTranslateX: number;
if(forwards) { // if(forwards) {
const p = this.input.rowsWrapper.parentElement; const p = this.input.rowsWrapper.parentElement;
const fakeSelectionWrapper = p.querySelector('.fake-selection-wrapper'); const fakeSelectionWrapper = p.querySelector('.fake-selection-wrapper');
const fakeRowsWrapper = p.querySelector('.fake-rows-wrapper'); const fakeRowsWrapper = p.querySelector('.fake-rows-wrapper');
@ -835,16 +832,19 @@ export default class ChatSelection extends AppSelection {
if(widthFrom !== widthTo) { if(widthFrom !== widthTo) {
const scale = (widthTo/* - 8 */) / widthFrom; const scale = (widthTo/* - 8 */) / widthFrom;
const initTranslateX = (widthFrom - widthTo) / 2; const initTranslateX = (widthFrom - widthTo) / 2;
const needTranslateX = fakeSelectionRect.left - fakeRowsRect.left - initTranslateX; needTranslateX = fakeSelectionRect.left - fakeRowsRect.left - initTranslateX;
transform = `translateX(${needTranslateX}px) scaleX(${scale})`;
if(forwards) {
transform = `translateX(${needTranslateX}px) scaleX(${scale})`;
if(scale < 1) { if(scale < 1) {
const br = 12; const br = 12;
borderRadius = '' + (br + br * (1 - scale)) + 'px'; borderRadius = '' + (br + br * (1 - scale)) + 'px';
}
} }
//scale = widthTo / widthFrom; //scale = widthTo / widthFrom;
} }
} // }
SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200); SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200);
this.input.rowsWrapper.style.transform = transform; this.input.rowsWrapper.style.transform = transform;
@ -857,6 +857,8 @@ export default class ChatSelection extends AppSelection {
this.selectionSendNowBtn = this.selectionSendNowBtn =
this.selectionForwardBtn = this.selectionForwardBtn =
this.selectionDeleteBtn = this.selectionDeleteBtn =
this.selectionLeft =
this.selectionRight =
null; null;
this.selectedText = undefined; this.selectedText = undefined;
} }
@ -914,13 +916,21 @@ export default class ChatSelection extends AppSelection {
}); });
}, attachClickOptions); }, attachClickOptions);
this.selectionContainer.append(...[ const left = this.selectionLeft = document.createElement('div');
btnCancel, left.classList.add('selection-container-left');
this.selectionCountEl, left.append(btnCancel, this.selectionCountEl);
const right = this.selectionRight = document.createElement('div');
right.classList.add('selection-container-right');
right.append(...[
this.selectionSendNowBtn, this.selectionSendNowBtn,
this.selectionForwardBtn, this.selectionForwardBtn,
this.selectionDeleteBtn this.selectionDeleteBtn
].filter(Boolean)); ].filter(Boolean))
left.style.transform = `translateX(-${needTranslateX * 2}px)`;
right.style.transform = `translateX(${needTranslateX * 2}px)`;
this.selectionContainer.append(left, right);
this.selectionInputWrapper.style.opacity = '0'; this.selectionInputWrapper.style.opacity = '0';
this.selectionInputWrapper.append(this.selectionContainer); this.selectionInputWrapper.append(this.selectionContainer);
@ -928,7 +938,12 @@ export default class ChatSelection extends AppSelection {
void this.selectionInputWrapper.offsetLeft; // reflow void this.selectionInputWrapper.offsetLeft; // reflow
this.selectionInputWrapper.style.opacity = ''; this.selectionInputWrapper.style.opacity = '';
left.style.transform = '';
right.style.transform = '';
} }
} else {
this.selectionLeft.style.transform = `translateX(-${needTranslateX * 2}px)`;
this.selectionRight.style.transform = `translateX(${needTranslateX * 2}px)`;
} }
}; };

31
src/components/chat/topbar.ts

@ -47,6 +47,8 @@ import AppEditContactTab from "../sidebarRight/tabs/editContact";
import appMediaPlaybackController from "../appMediaPlaybackController"; import appMediaPlaybackController from "../appMediaPlaybackController";
import { NULL_PEER_ID } from "../../lib/mtproto/mtproto_config"; import { NULL_PEER_ID } from "../../lib/mtproto/mtproto_config";
import IS_GROUP_CALL_SUPPORTED from "../../environment/groupCallSupport"; import IS_GROUP_CALL_SUPPORTED from "../../environment/groupCallSupport";
import IS_CALL_SUPPORTED from "../../environment/callSupport";
import { CallType } from "../../lib/calls/types";
type ButtonToVerify = {element?: HTMLElement, verify: () => boolean}; type ButtonToVerify = {element?: HTMLElement, verify: () => boolean};
@ -60,6 +62,7 @@ export default class ChatTopbar {
private chatUtils: HTMLDivElement; private chatUtils: HTMLDivElement;
private btnJoin: HTMLButtonElement; private btnJoin: HTMLButtonElement;
private btnPinned: HTMLButtonElement; private btnPinned: HTMLButtonElement;
private btnCall: HTMLButtonElement;
private btnGroupCall: HTMLButtonElement; private btnGroupCall: HTMLButtonElement;
private btnMute: HTMLButtonElement; private btnMute: HTMLButtonElement;
private btnSearch: HTMLButtonElement; private btnSearch: HTMLButtonElement;
@ -156,12 +159,14 @@ export default class ChatTopbar {
this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null, this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null,
this.btnJoin, this.btnJoin,
this.btnPinned, this.btnPinned,
this.btnCall,
this.btnGroupCall, this.btnGroupCall,
this.btnMute, this.btnMute,
this.btnSearch, this.btnSearch,
this.btnMore this.btnMore
].filter(Boolean)); ].filter(Boolean));
this.pushButtonToVerify(this.btnCall, this.verifyCallButton.bind(this, 'voice'));
this.pushButtonToVerify(this.btnGroupCall, this.verifyVideoChatButton); this.pushButtonToVerify(this.btnGroupCall, this.verifyVideoChatButton);
this.chatInfoContainer.append(this.btnBack, this.chatInfo, this.chatUtils); this.chatInfoContainer.append(this.btnBack, this.chatInfo, this.chatUtils);
@ -290,6 +295,14 @@ export default class ChatTopbar {
return (chat as MTChat.chat).pFlags?.call_active || this.appChatsManager.hasRights(chatId, 'manage_call'); return (chat as MTChat.chat).pFlags?.call_active || this.appChatsManager.hasRights(chatId, 'manage_call');
}; };
private verifyCallButton = (type?: CallType) => {
if(!IS_CALL_SUPPORTED || !this.peerId.isUser()) return false;
const userId = this.peerId.toUserId();
const userFull = this.appProfileManager.getCachedFullUser(userId);
return !!userFull && !!(type === 'voice' ? userFull.pFlags.phone_calls_available : userFull.pFlags.video_calls_available);
};
public constructUtils() { public constructUtils() {
this.menuButtons = [{ this.menuButtons = [{
icon: 'search', icon: 'search',
@ -332,6 +345,16 @@ export default class ChatTopbar {
const chatFull = this.appProfileManager.getCachedFullChat(this.peerId.toChatId()); const chatFull = this.appProfileManager.getCachedFullChat(this.peerId.toChatId());
return this.chat.type === 'chat' && !!(chatFull as ChatFull.channelFull)?.linked_chat_id; return this.chat.type === 'chat' && !!(chatFull as ChatFull.channelFull)?.linked_chat_id;
} }
}, {
icon: 'phone',
text: 'Call',
onClick: this.onCallClick.bind(this, 'voice'),
verify: this.verifyCallButton.bind(this, 'voice')
}, {
icon: 'videocamera',
text: 'VideoCall',
onClick: this.onCallClick.bind(this, 'video'),
verify: this.verifyCallButton.bind(this, 'video')
}, { }, {
icon: 'videochat', icon: 'videochat',
text: 'PeerInfo.Action.LiveStream', text: 'PeerInfo.Action.LiveStream',
@ -487,6 +510,10 @@ export default class ChatTopbar {
}, {listenerSetter: this.listenerSetter}); }, {listenerSetter: this.listenerSetter});
} }
private onCallClick(type: CallType) {
this.chat.appImManager.callUser(this.peerId.toUserId(), type);
}
private onJoinGroupCallClick = () => { private onJoinGroupCallClick = () => {
this.chat.appImManager.joinGroupCall(this.peerId); this.chat.appImManager.joinGroupCall(this.peerId);
}; };
@ -503,10 +530,12 @@ export default class ChatTopbar {
this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager); this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.btnJoin = Button('btn-primary btn-color-primary chat-join hide'); this.btnJoin = Button('btn-primary btn-color-primary chat-join hide');
this.btnCall = ButtonIcon('phone');
this.btnGroupCall = ButtonIcon('videochat'); this.btnGroupCall = ButtonIcon('videochat');
this.btnPinned = ButtonIcon('pinlist'); this.btnPinned = ButtonIcon('pinlist');
this.btnMute = ButtonIcon('mute'); this.btnMute = ButtonIcon('mute');
this.attachClickEvent(this.btnCall, this.onCallClick.bind(this, 'voice'));
this.attachClickEvent(this.btnGroupCall, this.onJoinGroupCallClick); this.attachClickEvent(this.btnGroupCall, this.onJoinGroupCallClick);
this.attachClickEvent(this.btnPinned, () => { this.attachClickEvent(this.btnPinned, () => {
@ -660,7 +689,7 @@ export default class ChatTopbar {
if(this.btnJoin) { if(this.btnJoin) {
if(this.appPeersManager.isAnyChat(peerId)) { if(this.appPeersManager.isAnyChat(peerId)) {
const chatId = peerId.toChatId(); const chatId = peerId.toChatId();
replaceContent(this.btnJoin, i18n(this.appChatsManager.isChannel(chatId) ? 'Chat.Subscribe' : 'ChannelJoin')); replaceContent(this.btnJoin, i18n(this.appChatsManager.isBroadcast(chatId) ? 'Chat.Subscribe' : 'ChannelJoin'));
this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(chatId)?.pFlags?.left); this.btnJoin.classList.toggle('hide', !this.appChatsManager.getChat(chatId)?.pFlags?.left);
} else { } else {
this.btnJoin.classList.add('hide'); this.btnJoin.classList.add('hide');

5
src/components/confirmationPopup.ts

@ -20,13 +20,12 @@ export default function confirmationPopup(options: PopupConfirmationOptions) {
resolve(set ? !!set.size : undefined); resolve(set ? !!set.size : undefined);
}; };
const buttons = addCancelButton([]); const buttons = addCancelButton([button]);
const cancelButton = buttons[0]; const cancelButton = buttons.find(button => button.isCancel);
cancelButton.callback = () => { cancelButton.callback = () => {
reject(); reject();
}; };
buttons.unshift(button);
options.buttons = buttons; options.buttons = buttons;
options.checkboxes = checkbox && [checkbox]; options.checkboxes = checkbox && [checkbox];

17
src/components/groupCall/description.ts

@ -4,9 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import { deepEqual } from "../../helpers/object";
import { GroupCall } from "../../layer"; import { GroupCall } from "../../layer";
import { GroupCallInstance } from "../../lib/appManagers/appGroupCallsManager"; import GroupCallInstance from "../../lib/calls/groupCallInstance";
import GROUP_CALL_STATE from "../../lib/calls/groupCallState"; import GROUP_CALL_STATE from "../../lib/calls/groupCallState";
import I18n, { LangPackKey, FormatterArguments } from "../../lib/langPack"; import I18n, { LangPackKey, FormatterArguments } from "../../lib/langPack";
@ -19,8 +18,10 @@ export default class GroupCallDescriptionElement {
}); });
this.descriptionIntl.element.classList.add('group-call-description'); this.descriptionIntl.element.classList.add('group-call-description');
}
appendTo.append(this.descriptionIntl.element); public detach() {
this.descriptionIntl.element.remove();
} }
public update(instance: GroupCallInstance) { public update(instance: GroupCallInstance) {
@ -35,11 +36,13 @@ export default class GroupCallDescriptionElement {
} }
const {descriptionIntl} = this; const {descriptionIntl} = this;
descriptionIntl.compareAndUpdate({
key,
args
});
if(descriptionIntl.key !== key || !deepEqual(descriptionIntl.args, args)) { if(!this.descriptionIntl.element.parentElement) {
descriptionIntl.key = key; this.appendTo.append(this.descriptionIntl.element);
descriptionIntl.args = args;
descriptionIntl.update();
} }
} }
} }

162
src/components/groupCall/index.ts

@ -11,10 +11,9 @@ import customProperties from "../../helpers/dom/customProperties";
import { safeAssign } from "../../helpers/object"; import { safeAssign } from "../../helpers/object";
import { GroupCall, GroupCallParticipant } from "../../layer"; import { GroupCall, GroupCallParticipant } from "../../layer";
import type { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import type { AppChatsManager } from "../../lib/appManagers/appChatsManager";
import type { AppGroupCallsManager, GroupCallInstance } from "../../lib/appManagers/appGroupCallsManager"; import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import GROUP_CALL_STATE from "../../lib/calls/groupCallState"; import GROUP_CALL_STATE from "../../lib/calls/groupCallState";
import { LangPackKey } from "../../lib/langPack";
import { RLottieColor } from "../../lib/rlottie/rlottiePlayer"; import { RLottieColor } from "../../lib/rlottie/rlottiePlayer";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import ButtonIcon from "../buttonIcon"; import ButtonIcon from "../buttonIcon";
@ -26,16 +25,16 @@ import GroupCallDescriptionElement from "./description";
import GroupCallTitleElement from "./title"; import GroupCallTitleElement from "./title";
import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../../helpers/dom/fullScreen"; import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../../helpers/dom/fullScreen";
import Scrollable from "../scrollable"; import Scrollable from "../scrollable";
import MovableElement, { MovableState } from "../movableElement"; import { MovableState } from "../movableElement";
import animationIntersector from "../animationIntersector"; import animationIntersector from "../animationIntersector";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import { IS_APPLE_MOBILE } from "../../environment/userAgent"; import { IS_APPLE_MOBILE } from "../../environment/userAgent";
import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes";
import toggleDisability from "../../helpers/dom/toggleDisability"; import toggleDisability from "../../helpers/dom/toggleDisability";
import { ripple } from "../ripple";
import throttle from "../../helpers/schedulers/throttle"; import throttle from "../../helpers/schedulers/throttle";
import IS_SCREEN_SHARING_SUPPORTED from "../../environment/screenSharingSupport"; import IS_SCREEN_SHARING_SUPPORTED from "../../environment/screenSharingSupport";
import ListenerSetter from "../../helpers/listenerSetter"; import GroupCallInstance from "../../lib/calls/groupCallInstance";
import makeButton from "../call/button";
import MovablePanel from "../../helpers/movablePanel";
import findUpClassName from "../../helpers/dom/findUpClassName";
export enum GROUP_CALL_PARTICIPANT_MUTED_STATE { export enum GROUP_CALL_PARTICIPANT_MUTED_STATE {
UNMUTED, UNMUTED,
@ -118,32 +117,6 @@ let previousState: MovableState = {
const className = 'group-call'; const className = 'group-call';
function makeButton(listenerSetter: ListenerSetter, options: {
text?: LangPackKey,
isDanger?: boolean,
noRipple?: boolean,
callback?: () => void,
listenerSetter?: ListenerSetter
}) {
const _className = className + '-button';
const div = document.createElement('div');
div.classList.add(_className, 'rp-overflow');
if(!options.noRipple) {
ripple(div);
}
if(options.isDanger) {
div.classList.add(_className + '-red');
}
if(options.callback) {
attachClickEvent(div, options.callback, {listenerSetter: options.listenerSetter});
}
return div;
}
export default class PopupGroupCall extends PopupElement { export default class PopupGroupCall extends PopupElement {
private appGroupCallsManager: AppGroupCallsManager; private appGroupCallsManager: AppGroupCallsManager;
private appPeersManager: AppPeersManager; private appPeersManager: AppPeersManager;
@ -160,7 +133,7 @@ export default class PopupGroupCall extends PopupElement {
private btnExitFullScreen: HTMLButtonElement; private btnExitFullScreen: HTMLButtonElement;
private btnInvite: HTMLButtonElement; private btnInvite: HTMLButtonElement;
private btnShowColumn: HTMLButtonElement; private btnShowColumn: HTMLButtonElement;
private movable: MovableElement; private movablePanel: MovablePanel;
private buttonsContainer: HTMLDivElement; private buttonsContainer: HTMLDivElement;
private btnFullScreen2: HTMLButtonElement; private btnFullScreen2: HTMLButtonElement;
private btnVideo: HTMLDivElement; private btnVideo: HTMLDivElement;
@ -202,7 +175,6 @@ export default class PopupGroupCall extends PopupElement {
const btnInvite = this.btnInvite = ButtonIcon('adduser'); const btnInvite = this.btnInvite = ButtonIcon('adduser');
const btnShowColumn = this.btnShowColumn = ButtonIcon('rightpanel ' + className + '-only-big'); const btnShowColumn = this.btnShowColumn = ButtonIcon('rightpanel ' + className + '-only-big');
this.toggleMovable(!IS_TOUCH_SUPPORTED);
attachClickEvent(btnShowColumn, this.toggleRightColumn, {listenerSetter}); attachClickEvent(btnShowColumn, this.toggleRightColumn, {listenerSetter});
@ -259,45 +231,54 @@ export default class PopupGroupCall extends PopupElement {
...options ...options
}); });
listenerSetter.add(rootScope)('group_call_state', (instance) => { this.movablePanel = new MovablePanel({
if(this.instance === instance) { listenerSetter,
this.updateInstance(); movableOptions: {
} minWidth: 400,
minHeight: 480,
element: this.element,
verifyTouchTarget: (e) => {
const target = e.target;
if(findUpClassName(target, 'chatlist') ||
findUpClassName(target, 'group-call-button') ||
findUpClassName(target, 'btn-icon') ||
findUpClassName(target, 'group-call-participants-video-container') ||
isFullScreen()) {
return false;
}
return true;
}
},
onResize: () => this.toggleBigLayout(),
previousState
});
listenerSetter.add(instance)('state', () => {
this.updateInstance();
}); });
listenerSetter.add(rootScope)('group_call_update', (groupCall) => { listenerSetter.add(rootScope)('group_call_update', (groupCall) => {
if(this.instance.id === groupCall.id) { if(this.instance?.id === groupCall.id) {
this.updateInstance(); this.updateInstance();
} }
}); });
listenerSetter.add(rootScope)('group_call_pinned', ({instance}) => { listenerSetter.add(instance)('pinned', () => {
if(this.instance === instance) { this.setHasPinned();
this.setHasPinned();
}
}); });
listenerSetter.add(this.groupCallParticipantsVideo)('toggleControls', this.onToggleControls); listenerSetter.add(this.groupCallParticipantsVideo)('toggleControls', this.onToggleControls);
listenerSetter.add(mediaSizes)('changeScreen', (from, to) => {
if(to === ScreenSize.mobile || from === ScreenSize.mobile) {
this.toggleMovable(!IS_TOUCH_SUPPORTED);
}
});
this.addEventListener('close', () => { this.addEventListener('close', () => {
const {movable} = this; const {movablePanel} = this;
if(movable) { previousState = movablePanel.state;
previousState = movable.state;
}
this.groupCallParticipantsVideo.destroy(); this.groupCallParticipantsVideo.destroy();
this.groupCallParticipants.destroy(); this.groupCallParticipants.destroy();
this.groupCallMicrophoneIcon.destroy(); this.groupCallMicrophoneIcon.destroy();
if(movable) { movablePanel.destroy();
movable.destroy();
}
}); });
this.toggleRightColumn(); this.toggleRightColumn();
@ -310,21 +291,20 @@ export default class PopupGroupCall extends PopupElement {
const buttons = this.buttonsContainer = document.createElement('div'); const buttons = this.buttonsContainer = document.createElement('div');
buttons.classList.add(className + '-buttons'); buttons.classList.add(className + '-buttons');
const _makeButton = makeButton.bind(null, this.listenerSetter); const _makeButton = makeButton.bind(null, className, this.listenerSetter);
const btnVideo = this.btnVideo = _makeButton({ const btnVideo = this.btnVideo = _makeButton({
text: 'VoiceChat.Video.Stream.Video', // text: 'VoiceChat.Video.Stream.Video',
callback: this.onVideoClick callback: this.onVideoClick,
icon: 'videocamera_filled'
}); });
btnVideo.classList.add('tgico-videocamera_filled');
const btnScreen = this.btnScreen = _makeButton({ const btnScreen = this.btnScreen = _makeButton({
text: 'VoiceChat.Video.Stream.Screencast', // text: 'VoiceChat.Video.Stream.Screencast',
callback: this.onScreenClick callback: this.onScreenClick,
icon: 'sharescreen_filled'
}); });
btnScreen.classList.add('tgico-sharescreen_filled');
btnScreen.classList.toggle('hide', !IS_SCREEN_SHARING_SUPPORTED); btnScreen.classList.toggle('hide', !IS_SCREEN_SHARING_SUPPORTED);
const btnMute = _makeButton({ const btnMute = _makeButton({
@ -337,20 +317,20 @@ export default class PopupGroupCall extends PopupElement {
btnMute.append(microphoneIcon.container); btnMute.append(microphoneIcon.container);
const btnMore = _makeButton({ const btnMore = _makeButton({
text: 'VoiceChat.Video.Stream.More' // text: 'VoiceChat.Video.Stream.More'
icon: 'settings_filled'
}); });
btnMore.classList.add('tgico-settings_filled', 'btn-disabled'); btnMore.classList.add('btn-disabled');
btnMore.classList.toggle('hide', !IS_SCREEN_SHARING_SUPPORTED); btnMore.classList.toggle('hide', !IS_SCREEN_SHARING_SUPPORTED);
const btnLeave = _makeButton({ const btnLeave = _makeButton({
text: 'VoiceChat.Leave', // text: 'VoiceChat.Leave',
isDanger: true, isDanger: true,
callback: this.onLeaveClick callback: this.onLeaveClick,
icon: 'close'
}); });
btnLeave.classList.add('tgico-close');
buttons.append(btnVideo, btnScreen, btnMute, btnMore, btnLeave); buttons.append(btnVideo, btnScreen, btnMute, btnMore, btnLeave);
this.container.append(buttons); this.container.append(buttons);
@ -419,36 +399,6 @@ export default class PopupGroupCall extends PopupElement {
return this.container; return this.container;
} }
private toggleMovable(enabled: boolean) {
if(enabled) {
if(this.movable) {
return;
}
const movable = this.movable = new MovableElement({
// minWidth: 366,
minWidth: 400,
minHeight: 480,
element: this.element
});
movable.state = previousState;
if(previousState.top === undefined) {
movable.setPositionToCenter();
}
this.listenerSetter.add(movable)('resize', this.toggleBigLayout);
} else {
if(!this.movable) {
return;
}
this.movable.destroyElements();
this.movable.destroy();
this.movable = undefined;
}
}
private onFullScreenChange = () => { private onFullScreenChange = () => {
this.toggleBigLayout(); this.toggleBigLayout();
const isFull = isFullScreen(); const isFull = isFullScreen();
@ -470,7 +420,8 @@ export default class PopupGroupCall extends PopupElement {
private toggleBigLayout = () => { private toggleBigLayout = () => {
const isFull = isFullScreen(); const isFull = isFullScreen();
const isBig = (isFull || !!(this.movable && this.movable.width >= 680)) && !!this.videosCount; const movable = this.movablePanel?.movable;
const isBig = (isFull || !!(movable && movable.width >= 680)) && !!this.videosCount;
/* if(!isBig && isFull) { /* if(!isBig && isFull) {
cancelFullScreen(); cancelFullScreen();
@ -519,11 +470,16 @@ export default class PopupGroupCall extends PopupElement {
return; return;
} }
const {participant, groupCall} = this.instance;
if(!participant) {
return;
}
this.setTitle(); this.setTitle();
this.setDescription(); this.setDescription();
this.setHasPinned(); this.setHasPinned();
const microphoneButtonState = getGroupCallMicrophoneButtonState(this.instance.groupCall as any, this.instance.participant); const microphoneButtonState = getGroupCallMicrophoneButtonState(groupCall as any, participant);
this.container.dataset.micState = microphoneButtonState === GROUP_CALL_MICROPHONE_BUTTON_STATE.HAND ? 'hand' : (microphoneButtonState === GROUP_CALL_MICROPHONE_BUTTON_STATE.MUTED ? 'muted' : 'unmuted'); this.container.dataset.micState = microphoneButtonState === GROUP_CALL_MICROPHONE_BUTTON_STATE.HAND ? 'hand' : (microphoneButtonState === GROUP_CALL_MICROPHONE_BUTTON_STATE.MUTED ? 'muted' : 'unmuted');
this.groupCallMicrophoneIcon.setState(microphoneButtonState); this.groupCallMicrophoneIcon.setState(microphoneButtonState);
} }

54
src/components/groupCall/microphoneIconMini.ts

@ -0,0 +1,54 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { SuperRLottieIcon } from "../superIcon";
export default class GroupCallMicrophoneIconMini extends SuperRLottieIcon<{
PartState: boolean,
ColorState: boolean,
Items: {
name: 'voice_mini'
}[]
}> {
constructor(colored?: boolean, skipAnimation?: boolean) {
super({
width: 36,
height: 36,
getPart: (state) => {
return this.getItem().getPart(state ? 'unmute' : 'mute');
},
getColor: colored ? (state) => {
return state ? [255, 255, 255] : [158, 158, 158];
} : undefined,
skipAnimation
});
this.add({
name: 'voice_mini',
parts: [{
startFrame: 0,
endFrame: 35,
name: 'hand-to-muted'
}, {
startFrame: 36,
endFrame: 68,
name: 'unmute'
}, {
startFrame: 69,
endFrame: 98,
name: 'mute'
}, {
startFrame: 99,
endFrame: 135,
name: 'muted-to-hand'
}, {
startFrame: 136,
endFrame: 171,
name: 'unmuted-to-hand'
}]
});
}
}

28
src/components/groupCall/participantVideo.ts

@ -4,15 +4,16 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import { animate } from "../../helpers/animation";
import { GroupCallParticipant } from "../../layer"; import { GroupCallParticipant } from "../../layer";
import type { GroupCallInstance, GroupCallOutputSource } from "../../lib/appManagers/appGroupCallsManager"; import type { GroupCallOutputSource } from "../../lib/appManagers/appGroupCallsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import { i18n } from "../../lib/langPack"; import { i18n } from "../../lib/langPack";
import PeerTitle from "../peerTitle"; import PeerTitle from "../peerTitle";
import { getGroupCallParticipantMutedState } from "."; import { getGroupCallParticipantMutedState } from ".";
import GroupCallParticipantMutedIcon from "./participantMutedIcon"; import GroupCallParticipantMutedIcon from "./participantMutedIcon";
import GroupCallParticipantStatusElement from "./participantStatus"; import GroupCallParticipantStatusElement from "./participantStatus";
import GroupCallInstance from "../../lib/calls/groupCallInstance";
import callVideoCanvasBlur from "../call/videoCanvasBlur";
const className = 'group-call-participant-video'; const className = 'group-call-participant-video';
@ -92,33 +93,14 @@ export default class GroupCallParticipantVideoElement {
this.right.append(this.groupCallParticipantMutedIcon.container); this.right.append(this.groupCallParticipantMutedIcon.container);
const className = 'group-call-participant-video'; video.classList.add(className, 'call-video');
video.classList.add(className);
if(video.paused) { if(video.paused) {
video.play(); video.play();
} }
const canvas = document.createElement('canvas'); const canvas = callVideoCanvasBlur(video);
canvas.classList.add(className + '-blur'); canvas.classList.add(className + '-blur');
const size = 16;
canvas.width = size;
canvas.height = size;
if(video) {
const ctx = canvas.getContext('2d');
ctx.filter = 'blur(2px)';
const renderFrame = () => {
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);
};
animate(() => {
renderFrame();
return canvas.isConnected;
});
renderFrame();
}
this.container.prepend(canvas, video); this.container.prepend(canvas, video);

15
src/components/groupCall/participantVideos.ts

@ -10,8 +10,9 @@ import findUpClassName from "../../helpers/dom/findUpClassName";
import ListenerSetter from "../../helpers/listenerSetter"; import ListenerSetter from "../../helpers/listenerSetter";
import { safeAssign } from "../../helpers/object"; import { safeAssign } from "../../helpers/object";
import { GroupCallParticipant } from "../../layer"; import { GroupCallParticipant } from "../../layer";
import { GroupCallInstance, AppGroupCallsManager, GroupCallOutputSource } from "../../lib/appManagers/appGroupCallsManager"; import { AppGroupCallsManager, GroupCallOutputSource } from "../../lib/appManagers/appGroupCallsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import GroupCallInstance from "../../lib/calls/groupCallInstance";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import GroupCallParticipantVideoElement, { GroupCallParticipantVideoType } from "./participantVideo"; import GroupCallParticipantVideoElement, { GroupCallParticipantVideoType } from "./participantVideo";
@ -54,14 +55,12 @@ export default class GroupCallParticipantsVideoElement extends ControlsHover {
} }
}); });
listenerSetter.add(rootScope)('group_call_pinned', ({instance, source}) => { listenerSetter.add(this.instance)('pinned', (source) => {
if(this.instance === instance) { this.participantsElements.forEach((map) => {
this.participantsElements.forEach((map) => { map.forEach((element) => {
map.forEach((element) => { this.setElementDisplay(element, source);
this.setElementDisplay(element, source);
});
}); });
} });
}); });
attachClickEvent(this.container, (e) => { attachClickEvent(this.container, (e) => {

3
src/components/groupCall/participants.ts

@ -14,8 +14,9 @@ import { safeAssign } from "../../helpers/object";
import ScrollableLoader from "../../helpers/scrollableLoader"; import ScrollableLoader from "../../helpers/scrollableLoader";
import { GroupCallParticipant } from "../../layer"; import { GroupCallParticipant } from "../../layer";
import type { AppChatsManager } from "../../lib/appManagers/appChatsManager"; import type { AppChatsManager } from "../../lib/appManagers/appChatsManager";
import type { GroupCallInstance, AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager"; import type { AppGroupCallsManager } from "../../lib/appManagers/appGroupCallsManager";
import type { AppPeersManager } from "../../lib/appManagers/appPeersManager"; import type { AppPeersManager } from "../../lib/appManagers/appPeersManager";
import GroupCallInstance from "../../lib/calls/groupCallInstance";
import rootScope from "../../lib/rootScope"; import rootScope from "../../lib/rootScope";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu"; import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
import confirmationPopup from "../confirmationPopup"; import confirmationPopup from "../confirmationPopup";

2
src/components/groupCall/participantsList.ts

@ -10,11 +10,11 @@ import { fastRaf } from "../../helpers/schedulers";
import SortedList, { SortedElementBase } from "../../helpers/sortedList"; import SortedList, { SortedElementBase } from "../../helpers/sortedList";
import { GroupCallParticipant } from "../../layer"; import { GroupCallParticipant } from "../../layer";
import appDialogsManager, { DialogDom, AppDialogsManager } from "../../lib/appManagers/appDialogsManager"; import appDialogsManager, { DialogDom, AppDialogsManager } from "../../lib/appManagers/appDialogsManager";
import { GroupCallInstance } from "../../lib/appManagers/appGroupCallsManager";
import { LazyLoadQueueIntersector } from "../lazyLoadQueue"; import { LazyLoadQueueIntersector } from "../lazyLoadQueue";
import { getGroupCallParticipantMutedState } from "."; import { getGroupCallParticipantMutedState } from ".";
import GroupCallParticipantMutedIcon from "./participantMutedIcon"; import GroupCallParticipantMutedIcon from "./participantMutedIcon";
import GroupCallParticipantStatusElement from "./participantStatus"; import GroupCallParticipantStatusElement from "./participantStatus";
import type GroupCallInstance from "../../lib/calls/groupCallInstance";
interface SortedParticipant extends SortedElementBase { interface SortedParticipant extends SortedElementBase {
dom: DialogDom, dom: DialogDom,

15
src/components/groupCall/title.ts

@ -6,7 +6,7 @@
import setInnerHTML from "../../helpers/dom/setInnerHTML"; import setInnerHTML from "../../helpers/dom/setInnerHTML";
import { GroupCall } from "../../layer"; import { GroupCall } from "../../layer";
import { GroupCallInstance } from "../../lib/appManagers/appGroupCallsManager"; import GroupCallInstance from "../../lib/calls/groupCallInstance";
import RichTextProcessor from "../../lib/richtextprocessor"; import RichTextProcessor from "../../lib/richtextprocessor";
import PeerTitle from "../peerTitle"; import PeerTitle from "../peerTitle";
@ -23,10 +23,15 @@ export default class GroupCallTitleElement {
const peerId = instance.chatId.toPeerId(true); const peerId = instance.chatId.toPeerId(true);
if(groupCall.title) { if(groupCall.title) {
setInnerHTML(appendTo, RichTextProcessor.wrapEmojiText(groupCall.title)); setInnerHTML(appendTo, RichTextProcessor.wrapEmojiText(groupCall.title));
} else if(peerTitle.peerId !== peerId) { } else {
peerTitle.peerId = peerId; if(peerTitle.peerId !== peerId) {
peerTitle.update(); peerTitle.peerId = peerId;
appendTo.append(peerTitle.element); peerTitle.update();
}
if(peerTitle.element.parentElement !== appendTo) {
appendTo.append(peerTitle.element);
}
} }
} }
} }

8
src/components/horizontalMenu.ts

@ -37,7 +37,13 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
} }
if(scrollableX) { if(scrollableX) {
scrollableX.scrollIntoViewNew(target.parentElement.children[id] as HTMLElement, 'center', undefined, undefined, animate ? undefined : FocusDirection.Static, transitionTime, 'x'); scrollableX.scrollIntoViewNew({
element: target.parentElement.children[id] as HTMLElement,
position: 'center',
forceDirection: animate ? undefined : FocusDirection.Static,
forceDuration: transitionTime,
axis: 'x'
});
} }
if(!rootScope.settings.animationsEnabled) { if(!rootScope.settings.animationsEnabled) {

20
src/components/movableElement.ts

@ -24,12 +24,20 @@ export type MovableState = {
const className = 'movable-element'; const className = 'movable-element';
const resizeHandlerClassName = className + '-resize-handler'; const resizeHandlerClassName = className + '-resize-handler';
export type MovableElementOptions = {
minWidth: MovableElement['minWidth'],
minHeight: MovableElement['minHeight'],
element: MovableElement['element'],
verifyTouchTarget?: MovableElement['verifyTouchTarget']
};
export default class MovableElement extends EventListenerBase<{ export default class MovableElement extends EventListenerBase<{
resize: () => void resize: () => void
}> { }> {
private minWidth: number; private minWidth: number;
private minHeight: number; private minHeight: number;
private element: HTMLElement; private element: HTMLElement;
private verifyTouchTarget: (e: TouchEvent | MouseEvent) => boolean;
private top: number; private top: number;
private left: number; private left: number;
@ -39,11 +47,7 @@ export default class MovableElement extends EventListenerBase<{
private swipeHandler: SwipeHandler; private swipeHandler: SwipeHandler;
private handlers: HTMLElement[]; private handlers: HTMLElement[];
constructor(options: { constructor(options: MovableElementOptions) {
minWidth: MovableElement['minWidth'],
minHeight: MovableElement['minHeight'],
element: MovableElement['element']
}) {
super(true); super(true);
safeAssign(this, options); safeAssign(this, options);
@ -133,11 +137,7 @@ export default class MovableElement extends EventListenerBase<{
}, },
verifyTouchTarget: (e) => { verifyTouchTarget: (e) => {
const target = e.target; const target = e.target;
if(findUpClassName(target, 'chatlist') || if(this.verifyTouchTarget && !this.verifyTouchTarget(e)) {
findUpClassName(target, 'group-call-button') ||
findUpClassName(target, 'btn-icon') ||
findUpClassName(target, 'group-call-participants-video-container') ||
isFullScreen()) {
return false; return false;
} }

14
src/components/peerProfile.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import PARALLAX_SUPPORTED from "../environment/parallaxSupport"; import IS_PARALLAX_SUPPORTED from "../environment/parallaxSupport";
import { copyTextToClipboard } from "../helpers/clipboard"; import { copyTextToClipboard } from "../helpers/clipboard";
import replaceContent from "../helpers/dom/replaceContent"; import replaceContent from "../helpers/dom/replaceContent";
import { fastRaf } from "../helpers/schedulers"; import { fastRaf } from "../helpers/schedulers";
@ -56,7 +56,7 @@ export default class PeerProfile {
private threadId: number; private threadId: number;
constructor(public scrollable: Scrollable) { constructor(public scrollable: Scrollable) {
if(!PARALLAX_SUPPORTED) { if(!IS_PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('no-parallax'); this.scrollable.container.classList.add('no-parallax');
} }
} }
@ -130,7 +130,11 @@ export default class PeerProfile {
this.section.content.append(this.phone.container, this.username.container, this.bio.container, this.notifications.container); this.section.content.append(this.phone.container, this.username.container, this.bio.container, this.notifications.container);
this.element.append(this.section.container, generateDelimiter()); this.element.append(this.section.container);
if(IS_PARALLAX_SUPPORTED) {
this.element.append(generateDelimiter());
}
this.notifications.checkboxField.input.addEventListener('change', (e) => { this.notifications.checkboxField.input.addEventListener('change', (e) => {
if(!e.isTrusted) { if(!e.isTrusted) {
@ -216,7 +220,7 @@ export default class PeerProfile {
if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container); if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container);
else this.element.prepend(this.avatars.container); else this.element.prepend(this.avatars.container);
if(PARALLAX_SUPPORTED) { if(IS_PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('parallax'); this.scrollable.container.classList.add('parallax');
} }
@ -224,7 +228,7 @@ export default class PeerProfile {
} }
} }
if(PARALLAX_SUPPORTED) { if(IS_PARALLAX_SUPPORTED) {
this.scrollable.container.classList.remove('parallax'); this.scrollable.container.classList.remove('parallax');
} }

11
src/components/peerProfileAvatars.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import PARALLAX_SUPPORTED from "../environment/parallaxSupport"; import IS_PARALLAX_SUPPORTED from "../environment/parallaxSupport";
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
@ -26,8 +26,8 @@ const LOAD_NEAREST = 3;
export default class PeerProfileAvatars { export default class PeerProfileAvatars {
private static BASE_CLASS = 'profile-avatars'; private static BASE_CLASS = 'profile-avatars';
private static SCALE = PARALLAX_SUPPORTED ? 2 : 1; private static SCALE = IS_PARALLAX_SUPPORTED ? 2 : 1;
private static TRANSLATE_TEMPLATE = PARALLAX_SUPPORTED ? `translate3d({x}, 0, -1px) scale(${PeerProfileAvatars.SCALE})` : 'translate({x}, 0)'; private static TRANSLATE_TEMPLATE = IS_PARALLAX_SUPPORTED ? `translate3d({x}, 0, -1px) scale(${PeerProfileAvatars.SCALE})` : 'translate({x}, 0)';
public container: HTMLElement; public container: HTMLElement;
public avatars: HTMLElement; public avatars: HTMLElement;
public gradient: HTMLElement; public gradient: HTMLElement;
@ -74,7 +74,10 @@ export default class PeerProfileAvatars {
const checkScrollTop = () => { const checkScrollTop = () => {
if(this.scrollable.scrollTop !== 0) { if(this.scrollable.scrollTop !== 0) {
this.scrollable.scrollIntoViewNew(this.scrollable.container.firstElementChild as HTMLElement, 'start'); this.scrollable.scrollIntoViewNew({
element: this.scrollable.container.firstElementChild as HTMLElement,
position: 'start'
});
return false; return false;
} }

4
src/components/poll.ts

@ -595,9 +595,9 @@ export default class PollElement extends HTMLElement {
* WINDOWS DESKTOP - реверс * WINDOWS DESKTOP - реверс
* все приложения накладывают аватарку первую на вторую, а в макете зато вторая на первую, ЛОЛ! * все приложения накладывают аватарку первую на вторую, а в макете зато вторая на первую, ЛОЛ!
*/ */
results.recent_voters/* .slice().reverse() */.forEach((userId, idx) => { (results.recent_voters || [])/* .slice().reverse() */.forEach((userId, idx) => {
const style = idx === 0 ? '' : `style="transform: translateX(-${idx * 3}px);"`; const style = idx === 0 ? '' : `style="transform: translateX(-${idx * 3}px);"`;
html += `<avatar-element class="avatar-16 poll-avatar" dialog="0" peer="${userId}" ${style}></avatar-element>`; html += `<avatar-element class="avatar-16 poll-avatar" dialog="0" peer="${userId.toPeerId()}" ${style}></avatar-element>`;
}); });
this.avatarsDiv.innerHTML = html; this.avatarsDiv.innerHTML = html;
} }

10
src/components/popups/createContact.ts

@ -62,13 +62,13 @@ export default class PopupCreateContact extends PopupElement {
this.listenerSetter.add(nameInputField.input)('input', onInput); this.listenerSetter.add(nameInputField.input)('input', onInput);
this.listenerSetter.add(lastNameInputField.input)('input', onInput); this.listenerSetter.add(lastNameInputField.input)('input', onInput);
telInputField.validate = () => {
return !!telInputField.value.match(/\d/);
};
const user = appUsersManager.getSelf(); const user = appUsersManager.getSelf();
const formatted = formatPhoneNumber(user.phone); const formatted = formatPhoneNumber(user.phone);
if(formatted) { if(formatted.code) {
telInputField.validate = () => {
return !!telInputField.value.match(/\d/);
};
telInputField.value = '+' + formatted.code.country_code; telInputField.value = '+' + formatted.code.country_code;
} }

5
src/components/popups/createPoll.ts

@ -380,7 +380,10 @@ export default class PopupCreatePoll extends PopupElement {
this.questions.append(radioField.label); this.questions.append(radioField.label);
this.scrollable.scrollIntoViewNew(this.questions.lastElementChild as HTMLElement, 'center'); this.scrollable.scrollIntoViewNew({
element: this.questions.lastElementChild as HTMLElement,
position: 'center'
});
//this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true); //this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true);
this.optionInputFields.push(questionField); this.optionInputFields.push(questionField);

3
src/components/popups/index.ts

@ -102,6 +102,9 @@ export default class PopupElement extends EventListenerBase<{
} }
this.withoutOverlay = options.withoutOverlay; this.withoutOverlay = options.withoutOverlay;
if(this.withoutOverlay) {
this.element.classList.add('no-overlay');
}
if(options.overlayClosable) { if(options.overlayClosable) {
attachClickEvent(this.element, (e: MouseEvent) => { attachClickEvent(this.element, (e: MouseEvent) => {

22
src/components/ripple.ts

@ -8,9 +8,16 @@ import findUpClassName from "../helpers/dom/findUpClassName";
import sequentialDom from "../helpers/sequentialDom"; import sequentialDom from "../helpers/sequentialDom";
import {IS_TOUCH_SUPPORTED} from "../environment/touchSupport"; import {IS_TOUCH_SUPPORTED} from "../environment/touchSupport";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import findUpAsChild from "../helpers/dom/findUpAsChild";
let rippleClickId = 0; let rippleClickId = 0;
export function ripple(elem: HTMLElement, callback: (id: number) => Promise<boolean | void> = () => Promise.resolve(), onEnd: (id: number) => void = null, prepend = false) { export function ripple(
elem: HTMLElement,
callback: (id: number) => Promise<boolean | void> = () => Promise.resolve(),
onEnd: (id: number) => void = null,
prepend = false,
attachListenerTo = elem
) {
//return; //return;
if(elem.querySelector('.c-ripple')) return; if(elem.querySelector('.c-ripple')) return;
elem.classList.add('rp'); elem.classList.add('rp');
@ -132,6 +139,9 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
const isRippleUnneeded = (e: Event) => e.target !== elem && ( const isRippleUnneeded = (e: Event) => e.target !== elem && (
['BUTTON', 'A'].includes((e.target as HTMLElement).tagName) ['BUTTON', 'A'].includes((e.target as HTMLElement).tagName)
|| findUpClassName(e.target as HTMLElement, 'c-ripple') !== r || findUpClassName(e.target as HTMLElement, 'c-ripple') !== r
) && (
attachListenerTo === elem
|| !findUpAsChild(e.target, attachListenerTo)
); );
// TODO: rename this variable // TODO: rename this variable
@ -141,7 +151,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
handler && handler(); handler && handler();
}; };
elem.addEventListener('touchstart', (e) => { attachListenerTo.addEventListener('touchstart', (e) => {
if(!rootScope.settings.animationsEnabled) { if(!rootScope.settings.animationsEnabled) {
return; return;
} }
@ -156,17 +166,17 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
let {clientX, clientY} = e.touches[0]; let {clientX, clientY} = e.touches[0];
drawRipple(clientX, clientY); drawRipple(clientX, clientY);
elem.addEventListener('touchend', touchEnd, {once: true}); attachListenerTo.addEventListener('touchend', touchEnd, {once: true});
window.addEventListener('touchmove', (e) => { window.addEventListener('touchmove', (e) => {
e.cancelBubble = true; e.cancelBubble = true;
e.stopPropagation(); e.stopPropagation();
touchEnd(); touchEnd();
elem.removeEventListener('touchend', touchEnd); attachListenerTo.removeEventListener('touchend', touchEnd);
}, {once: true}); }, {once: true});
}, {passive: true}); }, {passive: true});
} else { } else {
elem.addEventListener('mousedown', (e) => { attachListenerTo.addEventListener('mousedown', (e) => {
if(![0, 2].includes(e.button)) { // only left and right buttons if(![0, 2].includes(e.button)) { // only left and right buttons
return; return;
} }
@ -176,7 +186,7 @@ export function ripple(elem: HTMLElement, callback: (id: number) => Promise<bool
} }
//console.log('ripple mousedown', e, e.target, findUpClassName(e.target as HTMLElement, 'c-ripple') === r); //console.log('ripple mousedown', e, e.target, findUpClassName(e.target as HTMLElement, 'c-ripple') === r);
if(elem.dataset.ripple === '0' || isRippleUnneeded(e)) { if(attachListenerTo.dataset.ripple === '0' || isRippleUnneeded(e)) {
return; return;
} else if(touchStartFired) { } else if(touchStartFired) {
touchStartFired = false; touchStartFired = false;

18
src/components/scrollable.ts

@ -6,7 +6,7 @@
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import { logger, LogTypes } from "../lib/logger"; import { logger, LogTypes } from "../lib/logger";
import fastSmoothScroll, { FocusDirection, ScrollGetNormalSizeCallback } from "../helpers/fastSmoothScroll"; import fastSmoothScroll, { ScrollOptions } from "../helpers/fastSmoothScroll";
import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck"; import useHeavyAnimationCheck from "../hooks/useHeavyAnimationCheck";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
/* /*
@ -99,18 +99,12 @@ export class ScrollableBase {
this.container.append(element); this.container.append(element);
} }
public scrollIntoViewNew( public scrollIntoViewNew(options: Omit<ScrollOptions, 'container'>) {
element: HTMLElement,
position: ScrollLogicalPosition,
margin?: number,
maxDistance?: number,
forceDirection?: FocusDirection,
forceDuration?: number,
axis?: 'x' | 'y',
getNormalSize?: ScrollGetNormalSizeCallback
) {
//return Promise.resolve(); //return Promise.resolve();
return fastSmoothScroll(this.container, element, position, margin, maxDistance, forceDirection, forceDuration, axis, getNormalSize); return fastSmoothScroll({
...options,
container: this.container
});
} }
} }

52
src/components/sidebarLeft/index.ts

@ -25,7 +25,7 @@ import AppNewChannelTab from "./tabs/newChannel";
import AppContactsTab from "./tabs/contacts"; import AppContactsTab from "./tabs/contacts";
import AppArchivedTab from "./tabs/archivedTab"; import AppArchivedTab from "./tabs/archivedTab";
import AppAddMembersTab from "./tabs/addMembers"; import AppAddMembersTab from "./tabs/addMembers";
import { i18n_, LangPackKey } from "../../lib/langPack"; import { FormatterArguments, i18n_, LangPackKey } from "../../lib/langPack";
import { ButtonMenuItemOptions } from "../buttonMenu"; import { ButtonMenuItemOptions } from "../buttonMenu";
import CheckboxField from "../checkboxField"; import CheckboxField from "../checkboxField";
import { IS_MOBILE_SAFARI } from "../../environment/userAgent"; import { IS_MOBILE_SAFARI } from "../../environment/userAgent";
@ -602,54 +602,68 @@ export class AppSidebarLeft extends SidebarSlider {
} }
} }
const className = 'sidebar-left-section';
export class SettingSection { export class SettingSection {
public container: HTMLElement; public container: HTMLElement;
public innerContainer: HTMLElement;
public content: HTMLElement; public content: HTMLElement;
public title: HTMLElement; public title: HTMLElement;
public caption: HTMLElement; public caption: HTMLElement;
constructor(options: { constructor(options: {
name?: LangPackKey, name?: LangPackKey,
nameArgs?: FormatterArguments,
caption?: LangPackKey | true, caption?: LangPackKey | true,
noDelimiter?: boolean, noDelimiter?: boolean,
fakeGradientDelimiter?: boolean fakeGradientDelimiter?: boolean,
}) { noShadow?: boolean
this.container = document.createElement('div'); } = {}) {
this.container.classList.add('sidebar-left-section'); const container = this.container = document.createElement('div');
container.classList.add(className + '-container');
const innerContainer = this.innerContainer = document.createElement('div');
innerContainer.classList.add(className);
if(options.noShadow) {
innerContainer.classList.add('no-shadow');
}
if(options.fakeGradientDelimiter) { if(options.fakeGradientDelimiter) {
this.container.append(generateDelimiter()); innerContainer.append(generateDelimiter());
this.container.classList.add('with-fake-delimiter'); innerContainer.classList.add('with-fake-delimiter');
} else if(!options.noDelimiter) { } else if(!options.noDelimiter) {
const hr = document.createElement('hr'); const hr = document.createElement('hr');
this.container.append(hr); innerContainer.append(hr);
} else { } else {
this.container.classList.add('no-delimiter'); innerContainer.classList.add('no-delimiter');
} }
this.content = this.generateContentElement(); const content = this.content = this.generateContentElement();
if(options.name) { if(options.name) {
this.title = document.createElement('div'); const title = this.title = document.createElement('div');
this.title.classList.add('sidebar-left-h2', 'sidebar-left-section-name'); title.classList.add('sidebar-left-h2', className + '-name');
i18n_({element: this.title, key: options.name}); i18n_({element: title, key: options.name, args: options.nameArgs});
this.content.append(this.title); content.append(title);
} }
container.append(innerContainer);
if(options.caption) { if(options.caption) {
this.caption = this.generateContentElement(); const caption = this.caption = this.generateContentElement();
this.caption.classList.add('sidebar-left-section-caption'); caption.classList.add(className + '-caption');
container.append(caption);
if(options.caption !== true) { if(options.caption !== true) {
i18n_({element: this.caption, key: options.caption}); i18n_({element: caption, key: options.caption});
} }
} }
} }
public generateContentElement() { public generateContentElement() {
const content = document.createElement('div'); const content = document.createElement('div');
content.classList.add('sidebar-left-section-content'); content.classList.add(className + '-content');
this.container.append(content); this.innerContainer.append(content);
return content; return content;
} }
} }

7
src/components/sidebarLeft/tabs/activeSessions.ts

@ -27,6 +27,7 @@ export default class AppActiveSessionsTab extends SliderSuperTab {
private menuElement: HTMLElement; private menuElement: HTMLElement;
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('active-sessions-container'); this.container.classList.add('active-sessions-container');
this.setTitle('SessionsTitle'); this.setTitle('SessionsTitle');
@ -53,7 +54,8 @@ export default class AppActiveSessionsTab extends SliderSuperTab {
{ {
const section = new SettingSection({ const section = new SettingSection({
name: 'CurrentSession' name: 'CurrentSession',
caption: 'ClearOtherSessionsHelp'
}); });
const auth = authorizations.findAndSplice(auth => auth.pFlags.current); const auth = authorizations.findAndSplice(auth => auth.pFlags.current);
@ -96,7 +98,8 @@ export default class AppActiveSessionsTab extends SliderSuperTab {
} }
const otherSection = new SettingSection({ const otherSection = new SettingSection({
name: 'OtherSessions' name: 'OtherSessions',
caption: 'SessionsListInfo'
}); });
authorizations.forEach(auth => { authorizations.forEach(auth => {

1
src/components/sidebarLeft/tabs/addMembers.ts

@ -18,6 +18,7 @@ export default class AppAddMembersTab extends SliderSuperTab {
private skippable: boolean; private skippable: boolean;
protected init() { protected init() {
this.container.classList.add('add-members-container');
this.nextBtn = ButtonCorner({icon: 'arrow_next'}); this.nextBtn = ButtonCorner({icon: 'arrow_next'});
this.content.append(this.nextBtn); this.content.append(this.nextBtn);
this.scrollable.container.remove(); this.scrollable.container.remove();

4
src/components/sidebarLeft/tabs/background.ts

@ -39,6 +39,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
private blurCheckboxField: CheckboxField; private blurCheckboxField: CheckboxField;
init() { init() {
this.header.classList.add('with-border');
this.container.classList.add('background-container', 'background-image-container'); this.container.classList.add('background-container', 'background-image-container');
this.setTitle('ChatBackground'); this.setTitle('ChatBackground');
@ -93,10 +94,11 @@ export default class AppBackgroundTab extends SliderSuperTab {
//console.log(accountWallpapers); //console.log(accountWallpapers);
}); });
const gridContainer = generateSection(this.scrollable);
const grid = this.grid = document.createElement('div'); const grid = this.grid = document.createElement('div');
grid.classList.add('grid'); grid.classList.add('grid');
attachClickEvent(grid, this.onGridClick, {listenerSetter: this.listenerSetter}); attachClickEvent(grid, this.onGridClick, {listenerSetter: this.listenerSetter});
this.scrollable.append(grid); gridContainer.append(grid);
} }
private onUploadClick = () => { private onUploadClick = () => {

6
src/components/sidebarLeft/tabs/backgroundColor.ts

@ -23,6 +23,7 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
private theme: Theme; private theme: Theme;
init() { init() {
this.header.classList.add('with-border');
this.container.classList.add('background-container', 'background-color-container'); this.container.classList.add('background-container', 'background-color-container');
this.setTitle('SetColor'); this.setTitle('SetColor');
@ -35,6 +36,8 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
this.scrollable.append(section.container); this.scrollable.append(section.container);
const gridSection = new SettingSection({});
const grid = this.grid = document.createElement('div'); const grid = this.grid = document.createElement('div');
grid.classList.add('grid'); grid.classList.add('grid');
@ -81,7 +84,8 @@ export default class AppBackgroundColorTab extends SliderSuperTab {
this.applyColor(color); this.applyColor(color);
}, {listenerSetter: this.listenerSetter}); }, {listenerSetter: this.listenerSetter});
this.scrollable.append(grid); gridSection.content.append(grid);
this.scrollable.append(gridSection.container);
this.applyColor = throttle(this._applyColor, 16, true); this.applyColor = throttle(this._applyColor, 16, true);
} }

1
src/components/sidebarLeft/tabs/blockedUsers.ts

@ -21,6 +21,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab {
private menuElement: HTMLElement; private menuElement: HTMLElement;
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('blocked-users-container'); this.container.classList.add('blocked-users-container');
this.setTitle('BlockedUsers'); this.setTitle('BlockedUsers');

11
src/components/sidebarLeft/tabs/editFolder.ts

@ -82,15 +82,18 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.header.append(this.confirmBtn, this.menuBtn); this.header.append(this.confirmBtn, this.menuBtn);
const inputSection = new SettingSection({});
const inputWrapper = document.createElement('div'); const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper'); inputWrapper.classList.add('input-wrapper');
this.nameInputField = new InputField({ this.nameInputField = new InputField({
label: 'FilterNameInputLabel', label: 'FilterNameHint',
maxLength: MAX_FOLDER_NAME_LENGTH maxLength: MAX_FOLDER_NAME_LENGTH
}); });
inputWrapper.append(this.nameInputField.container); inputWrapper.append(this.nameInputField.container);
inputSection.content.append(inputWrapper);
const generateList = (className: string, h2Text: LangPackKey, buttons: {icon: string, name?: string, withRipple?: true, text: LangPackKey}[], to: any) => { const generateList = (className: string, h2Text: LangPackKey, buttons: {icon: string, name?: string, withRipple?: true, text: LangPackKey}[], to: any) => {
const section = new SettingSection({ const section = new SettingSection({
@ -164,7 +167,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
name: 'exclude_read' name: 'exclude_read'
}], this.flags); }], this.flags);
this.scrollable.append(this.stickerContainer, this.caption, inputWrapper, this.includePeerIds.container, this.excludePeerIds.container); this.scrollable.append(this.stickerContainer, this.caption, inputSection.container, this.includePeerIds.container, this.excludePeerIds.container);
const includedFlagsContainer = this.includePeerIds.container.querySelector('.folder-categories'); const includedFlagsContainer = this.includePeerIds.container.querySelector('.folder-categories');
const excludedFlagsContainer = this.excludePeerIds.container.querySelector('.folder-categories'); const excludedFlagsContainer = this.excludePeerIds.container.querySelector('.folder-categories');
@ -255,7 +258,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
} }
private onCreateOpen() { private onCreateOpen() {
this.caption.style.display = ''; // this.caption.style.display = '';
this.setTitle('FilterNew'); this.setTitle('FilterNew');
this.menuBtn.classList.add('hide'); this.menuBtn.classList.add('hide');
this.confirmBtn.classList.remove('hide'); this.confirmBtn.classList.remove('hide');
@ -268,7 +271,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
} }
private onEditOpen() { private onEditOpen() {
this.caption.style.display = 'none'; // this.caption.style.display = 'none';
this.setTitle(this.type === 'create' ? 'FilterNew' : 'FilterHeaderEdit'); this.setTitle(this.type === 'create' ? 'FilterNew' : 'FilterHeaderEdit');
if(this.type === 'edit') { if(this.type === 'edit') {

34
src/components/sidebarLeft/tabs/editProfile.ts

@ -13,6 +13,7 @@ import { UsernameInputField } from "../../usernameInputField";
import { i18n, i18n_ } from "../../../lib/langPack"; import { i18n, i18n_ } from "../../../lib/langPack";
import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import rootScope from "../../../lib/rootScope"; import rootScope from "../../../lib/rootScope";
import { generateSection, SettingSection } from "..";
// TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист) // TODO: аватарка не поменяется в этой вкладке после изменения почему-то (если поставить в другом клиенте, и потом тут проверить, для этого ещё вышел в чатлист)
@ -34,6 +35,7 @@ export default class AppEditProfileTab extends SliderSuperTab {
const inputFields: InputField[] = []; const inputFields: InputField[] = [];
{ {
const section = generateSection(this.scrollable, undefined, 'Bio.Description');
const inputWrapper = document.createElement('div'); const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper'); inputWrapper.classList.add('input-wrapper');
@ -60,23 +62,23 @@ export default class AppEditProfileTab extends SliderSuperTab {
i18n_({element: caption, key: 'Bio.Description'}); i18n_({element: caption, key: 'Bio.Description'});
inputFields.push(this.firstNameInputField, this.lastNameInputField, this.bioInputField); inputFields.push(this.firstNameInputField, this.lastNameInputField, this.bioInputField);
this.scrollable.append(inputWrapper, caption);
}
this.scrollable.append(document.createElement('hr')); this.editPeer = new EditPeer({
peerId: rootScope.myId,
inputFields,
listenerSetter: this.listenerSetter
});
this.content.append(this.editPeer.nextBtn);
this.editPeer = new EditPeer({ section.append(this.editPeer.avatarEdit.container, inputWrapper);
peerId: rootScope.myId, }
inputFields,
listenerSetter: this.listenerSetter
});
this.content.append(this.editPeer.nextBtn);
this.scrollable.prepend(this.editPeer.avatarEdit.container);
{ {
const h2 = document.createElement('div'); const section = new SettingSection({
h2.classList.add('sidebar-left-h2'); name: 'EditAccount.Username',
i18n_({element: h2, key: 'EditAccount.Username'}); caption: true
});
const inputWrapper = document.createElement('div'); const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper'); inputWrapper.classList.add('input-wrapper');
@ -97,8 +99,7 @@ export default class AppEditProfileTab extends SliderSuperTab {
inputWrapper.append(this.usernameInputField.container); inputWrapper.append(this.usernameInputField.container);
const caption = document.createElement('div'); const caption = section.caption;
caption.classList.add('caption');
caption.append(i18n('UsernameSettings.ChangeDescription')); caption.append(i18n('UsernameSettings.ChangeDescription'));
caption.append(document.createElement('br'), document.createElement('br')); caption.append(document.createElement('br'), document.createElement('br'));
@ -115,7 +116,8 @@ export default class AppEditProfileTab extends SliderSuperTab {
caption.append(profileUrlContainer); caption.append(profileUrlContainer);
inputFields.push(this.usernameInputField); inputFields.push(this.usernameInputField);
this.scrollable.append(h2, inputWrapper, caption); section.content.append(inputWrapper);
this.scrollable.append(section.container);
} }
attachClickEvent(this.editPeer.nextBtn, () => { attachClickEvent(this.editPeer.nextBtn, () => {

1
src/components/sidebarLeft/tabs/generalSettings.ts

@ -73,6 +73,7 @@ export class RangeSettingSelector {
export default class AppGeneralSettingsTab extends SliderSuperTabEventable { export default class AppGeneralSettingsTab extends SliderSuperTabEventable {
init() { init() {
this.header.classList.add('with-border');
this.container.classList.add('general-settings-container'); this.container.classList.add('general-settings-container');
this.setTitle('General'); this.setTitle('General');

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

@ -172,8 +172,6 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
const filter = this.filter; const filter = this.filter;
const fragment = document.createDocumentFragment();
const categoriesSection = new SettingSection({ const categoriesSection = new SettingSection({
noDelimiter: true, noDelimiter: true,
name: 'FilterChatTypes' name: 'FilterChatTypes'
@ -207,12 +205,6 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
} }
categoriesSection.content.append(f); categoriesSection.content.append(f);
const chatsSection = new SettingSection({
name: 'FilterChats'
});
fragment.append(categoriesSection.container, chatsSection.container);
///////////////// /////////////////
const selectedPeers = (this.type === 'included' ? filter.includePeerIds : filter.excludePeerIds).slice(); const selectedPeers = (this.type === 'included' ? filter.includePeerIds : filter.excludePeerIds).slice();
@ -222,7 +214,8 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
onChange: this.onSelectChange, onChange: this.onSelectChange,
peerType: ['dialogs'], peerType: ['dialogs'],
renderResultsFunc: this.renderResults, renderResultsFunc: this.renderResults,
placeholder: 'Search' placeholder: 'Search',
sectionNameLangPackKey: 'FilterChats'
}); });
this.selector.selected = new Set(selectedPeers); this.selector.selected = new Set(selectedPeers);
@ -249,9 +242,7 @@ export default class AppIncludedChatsTab extends SliderSuperTab {
return div; return div;
}; };
const parent = this.selector.list.parentElement; this.selector.scrollable.container.append(categoriesSection.container, this.selector.scrollable.container.lastElementChild);
chatsSection.content.append(this.selector.list);
parent.append(fragment);
this.selector.addInitial(selectedPeers); this.selector.addInitial(selectedPeers);
addedInitial = true; addedInitial = true;

1
src/components/sidebarLeft/tabs/language.ts

@ -14,6 +14,7 @@ import { SliderSuperTab } from "../../slider"
export default class AppLanguageTab extends SliderSuperTab { export default class AppLanguageTab extends SliderSuperTab {
protected async init() { protected async init() {
this.header.classList.add('with-border');
this.container.classList.add('language-container'); this.container.classList.add('language-container');
this.setTitle('Telegram.LanguageViewController'); this.setTitle('Telegram.LanguageViewController');

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

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import appSidebarLeft from ".."; import appSidebarLeft, { SettingSection } from "..";
import { InputFile } from "../../../layer"; import { InputFile } from "../../../layer";
import appChatsManager from "../../../lib/appManagers/appChatsManager"; import appChatsManager from "../../../lib/appManagers/appChatsManager";
import Button from "../../button"; import Button from "../../button";
@ -31,6 +31,10 @@ export default class AppNewChannelTab extends SliderSuperTab {
this.uploadAvatar = _upload; this.uploadAvatar = _upload;
}); });
const section = new SettingSection({
caption: 'Channel.DescriptionHolderDescrpiton'
});
const inputWrapper = document.createElement('div'); const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper'); inputWrapper.classList.add('input-wrapper');
@ -55,10 +59,6 @@ export default class AppNewChannelTab extends SliderSuperTab {
this.channelNameInputField.input.addEventListener('input', onLengthChange); this.channelNameInputField.input.addEventListener('input', onLengthChange);
this.channelDescriptionInputField.input.addEventListener('input', onLengthChange); this.channelDescriptionInputField.input.addEventListener('input', onLengthChange);
const caption = document.createElement('div');
caption.classList.add('caption');
_i18n(caption, 'Channel.DescriptionHolderDescrpiton');
this.nextBtn = ButtonCorner({icon: 'arrow_next'}); this.nextBtn = ButtonCorner({icon: 'arrow_next'});
this.nextBtn.addEventListener('click', () => { this.nextBtn.addEventListener('click', () => {
@ -87,7 +87,8 @@ export default class AppNewChannelTab extends SliderSuperTab {
}); });
this.content.append(this.nextBtn); this.content.append(this.nextBtn);
this.scrollable.append(this.avatarEdit.container, inputWrapper, caption); section.content.append(this.avatarEdit.container, inputWrapper);
this.scrollable.append(section.container);
} }
public onCloseAfterTimeout() { public onCloseAfterTimeout() {

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

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import appSidebarLeft from ".."; import appSidebarLeft, { SettingSection } from "..";
import { InputFile } from "../../../layer"; import { InputFile } from "../../../layer";
import appChatsManager from "../../../lib/appManagers/appChatsManager"; import appChatsManager from "../../../lib/appManagers/appChatsManager";
import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager";
@ -17,12 +17,12 @@ import { i18n } from "../../../lib/langPack";
import ButtonCorner from "../../buttonCorner"; import ButtonCorner from "../../buttonCorner";
export default class AppNewGroupTab extends SliderSuperTab { export default class AppNewGroupTab extends SliderSuperTab {
private searchGroup = new SearchGroup(true, 'contacts', true, 'new-group-members disable-hover', false);
private avatarEdit: AvatarEdit; private avatarEdit: AvatarEdit;
private uploadAvatar: () => Promise<InputFile> = null; private uploadAvatar: () => Promise<InputFile> = null;
private peerIds: PeerId[]; private peerIds: PeerId[];
private nextBtn: HTMLButtonElement; private nextBtn: HTMLButtonElement;
private groupNameInputField: InputField; private groupNameInputField: InputField;
list: HTMLUListElement;
protected init() { protected init() {
this.container.classList.add('new-group-container'); this.container.classList.add('new-group-container');
@ -32,6 +32,8 @@ export default class AppNewGroupTab extends SliderSuperTab {
this.uploadAvatar = _upload; this.uploadAvatar = _upload;
}); });
const section = new SettingSection({});
const inputWrapper = document.createElement('div'); const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper'); inputWrapper.classList.add('input-wrapper');
@ -65,16 +67,24 @@ export default class AppNewGroupTab extends SliderSuperTab {
}); });
}); });
const chatsContainer = document.createElement('div'); const chatsSection = new SettingSection({
chatsContainer.classList.add('chatlist-container'); name: 'Members',
chatsContainer.append(this.searchGroup.container); nameArgs: [this.peerIds.length]
});
const list = this.list = appDialogsManager.createChatList({
new: true
});
chatsSection.content.append(list);
section.content.append(this.avatarEdit.container, inputWrapper);
this.content.append(this.nextBtn); this.content.append(this.nextBtn);
this.scrollable.append(this.avatarEdit.container, inputWrapper, chatsContainer); this.scrollable.append(section.container, chatsSection.container);
} }
public onCloseAfterTimeout() { public onCloseAfterTimeout() {
this.searchGroup.clear();
this.avatarEdit.clear(); this.avatarEdit.clear();
this.uploadAvatar = null; this.uploadAvatar = null;
this.groupNameInputField.value = ''; this.groupNameInputField.value = '';
@ -82,14 +92,13 @@ export default class AppNewGroupTab extends SliderSuperTab {
} }
public open(peerIds: PeerId[]) { public open(peerIds: PeerId[]) {
this.peerIds = peerIds;
const result = super.open(); const result = super.open();
result.then(() => { result.then(() => {
this.peerIds = peerIds;
this.peerIds.forEach(userId => { this.peerIds.forEach(userId => {
let {dom} = appDialogsManager.addDialogNew({ let {dom} = appDialogsManager.addDialogNew({
dialog: userId, dialog: userId,
container: this.searchGroup.list, container: this.list,
drawStatus: false, drawStatus: false,
rippleEnabled: false, rippleEnabled: false,
avatarSize: 48 avatarSize: 48
@ -97,10 +106,6 @@ export default class AppNewGroupTab extends SliderSuperTab {
dom.lastMessageSpan.append(appUsersManager.getUserStatusString(userId)); dom.lastMessageSpan.append(appUsersManager.getUserStatusString(userId));
}); });
this.searchGroup.nameEl.textContent = '';
this.searchGroup.nameEl.append(i18n('Members', [this.peerIds.length]));
this.searchGroup.setActive();
}); });
return result; return result;

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

@ -20,7 +20,8 @@ type InputNotifyKey = Exclude<InputNotifyPeer['_'], 'inputNotifyPeer'>;
export default class AppNotificationsTab extends SliderSuperTabEventable { export default class AppNotificationsTab extends SliderSuperTabEventable {
protected init() { protected init() {
this.container.classList.add('notifications-container'); this.header.classList.add('with-border');
this.container.classList.add('notifications-container', 'with-border');
this.setTitle('Telegram.NotificationSettingsViewController'); this.setTitle('Telegram.NotificationSettingsViewController');
const NotifySection = (options: { const NotifySection = (options: {

1
src/components/sidebarLeft/tabs/privacy/addToGroups.ts

@ -11,6 +11,7 @@ import { PrivacyType } from "../../../../lib/appManagers/appPrivacyManager";
export default class AppPrivacyAddToGroupsTab extends SliderSuperTabEventable { export default class AppPrivacyAddToGroupsTab extends SliderSuperTabEventable {
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('privacy-tab', 'privacy-add-to-groups'); this.container.classList.add('privacy-tab', 'privacy-add-to-groups');
this.setTitle('PrivacySettings.Groups'); this.setTitle('PrivacySettings.Groups');

1
src/components/sidebarLeft/tabs/privacy/calls.ts

@ -10,6 +10,7 @@ import { LangPackKey } from "../../../../lib/langPack";
export default class AppPrivacyCallsTab extends SliderSuperTabEventable { export default class AppPrivacyCallsTab extends SliderSuperTabEventable {
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('privacy-tab', 'privacy-calls'); this.container.classList.add('privacy-tab', 'privacy-calls');
this.setTitle('PrivacySettings.VoiceCalls'); this.setTitle('PrivacySettings.VoiceCalls');

1
src/components/sidebarLeft/tabs/privacy/forwardMessages.ts

@ -10,6 +10,7 @@ import { LangPackKey } from "../../../../lib/langPack";
export default class AppPrivacyForwardMessagesTab extends SliderSuperTabEventable { export default class AppPrivacyForwardMessagesTab extends SliderSuperTabEventable {
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('privacy-tab', 'privacy-forward-messages'); this.container.classList.add('privacy-tab', 'privacy-forward-messages');
this.setTitle('PrivacySettings.Forwards'); this.setTitle('PrivacySettings.Forwards');

1
src/components/sidebarLeft/tabs/privacy/lastSeen.ts

@ -10,6 +10,7 @@ import { LangPackKey } from "../../../../lib/langPack";
export default class AppPrivacyLastSeenTab extends SliderSuperTabEventable { export default class AppPrivacyLastSeenTab extends SliderSuperTabEventable {
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('privacy-tab', 'privacy-last-seen'); this.container.classList.add('privacy-tab', 'privacy-last-seen');
this.setTitle('PrivacyLastSeen'); this.setTitle('PrivacyLastSeen');

1
src/components/sidebarLeft/tabs/privacy/profilePhoto.ts

@ -11,6 +11,7 @@ import { LangPackKey } from "../../../../lib/langPack";
export default class AppPrivacyProfilePhotoTab extends SliderSuperTabEventable { export default class AppPrivacyProfilePhotoTab extends SliderSuperTabEventable {
protected init() { protected init() {
this.header.classList.add('with-border');
this.container.classList.add('privacy-tab', 'privacy-profile-photo'); this.container.classList.add('privacy-tab', 'privacy-profile-photo');
this.setTitle('PrivacyProfilePhoto'); this.setTitle('PrivacyProfilePhoto');

37
src/components/sidebarRight/tabs/chatType.ts

@ -22,6 +22,8 @@ import PopupPeer from "../../popups/peer";
import ButtonCorner from "../../buttonCorner"; import ButtonCorner from "../../buttonCorner";
import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import toggleDisability from "../../../helpers/dom/toggleDisability"; import toggleDisability from "../../../helpers/dom/toggleDisability";
import CheckboxField from "../../checkboxField";
import rootScope from "../../../lib/rootScope";
export default class AppChatTypeTab extends SliderSuperTabEventable { export default class AppChatTypeTab extends SliderSuperTabEventable {
public chatId: ChatId; public chatId: ChatId;
@ -157,5 +159,40 @@ export default class AppChatTypeTab extends SliderSuperTabEventable {
linkInputField.setOriginalValue(originalValue); linkInputField.setOriginalValue(originalValue);
this.scrollable.append(section.container, privateSection.container, publicSection.container); this.scrollable.append(section.container, privateSection.container, publicSection.container);
{
const section = new SettingSection({
name: 'SavingContentTitle',
caption: isBroadcast ? 'RestrictSavingContentInfoChannel' : 'RestrictSavingContentInfoGroup'
});
const checkboxField = new CheckboxField({
text: 'RestrictSavingContent',
withRipple: true
});
this.listenerSetter.add(checkboxField.input)('change', () => {
const toggle = checkboxField.toggleDisability(true);
appChatsManager.toggleNoForwards(this.chatId, checkboxField.checked).then(() => {
toggle();
});
});
const onChatUpdate = () => {
checkboxField.setValueSilently(!!(chat as Chat.channel).pFlags.noforwards);
};
this.listenerSetter.add(rootScope)('chat_update', (chatId) => {
if(this.chatId === chatId) {
onChatUpdate();
}
});
onChatUpdate();
section.content.append(checkboxField.label);
this.scrollable.append(section.container);
}
} }
} }

5
src/components/sidebarRight/tabs/editChat.ts

@ -265,6 +265,7 @@ export default class AppEditChatTab extends SliderSuperTab {
}); });
}); });
// ! it won't be updated because chatFull will be old
const onChatUpdate = () => { const onChatUpdate = () => {
showChatHistoryCheckboxField.setValueSilently(isChannel && !(chatFull as ChatFull.channelFull).pFlags.hidden_prehistory); showChatHistoryCheckboxField.setValueSilently(isChannel && !(chatFull as ChatFull.channelFull).pFlags.hidden_prehistory);
}; };
@ -275,7 +276,9 @@ export default class AppEditChatTab extends SliderSuperTab {
section.content.append(showChatHistoryCheckboxField.label); section.content.append(showChatHistoryCheckboxField.label);
} }
this.scrollable.append(section.container); if(section.content.childElementCount) {
this.scrollable.append(section.container);
}
} }
if(appChatsManager.hasRights(this.chatId, 'delete_chat')) { if(appChatsManager.hasRights(this.chatId, 'delete_chat')) {

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

@ -114,7 +114,10 @@ export default class AppSharedMediaTab extends SliderSuperTab {
attachClickEvent(this.closeBtn, (e) => { attachClickEvent(this.closeBtn, (e) => {
if(this.closeBtn.firstElementChild.classList.contains('state-back')) { if(this.closeBtn.firstElementChild.classList.contains('state-back')) {
this.scrollable.scrollIntoViewNew(this.scrollable.container.firstElementChild as HTMLElement, 'start'); this.scrollable.scrollIntoViewNew({
element: this.scrollable.container.firstElementChild as HTMLElement,
position: 'start'
});
transition(0); transition(0);
animatedCloseIcon.classList.remove('state-back'); animatedCloseIcon.classList.remove('state-back');
} else if(!this.scrollable.isHeavyAnimationInProgress) { } else if(!this.scrollable.isHeavyAnimationInProgress) {

51
src/components/superIcon.ts

@ -1,3 +1,9 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import noop from "../helpers/noop"; import noop from "../helpers/noop";
import { safeAssign } from "../helpers/object"; import { safeAssign } from "../helpers/object";
import { LottieAssetName } from "../lib/rlottie/lottieLoader"; import { LottieAssetName } from "../lib/rlottie/lottieLoader";
@ -9,6 +15,9 @@ export type SuperRLottieIconGetInfoResult = RLottieIconItemPart;
export class SuperRLottieIcon<Options extends { export class SuperRLottieIcon<Options extends {
PartState: any, PartState: any,
ColorState?: any, ColorState?: any,
Items?: {
name: string
}[]
}> extends RLottieIcon { }> extends RLottieIcon {
protected getPart: (state: Options['PartState'], prevState?: Options['PartState']) => SuperRLottieIconGetInfoResult; protected getPart: (state: Options['PartState'], prevState?: Options['PartState']) => SuperRLottieIconGetInfoResult;
protected getColor?: (state: Options['ColorState'], prevState?: Options['ColorState']) => RLottieColor; protected getColor?: (state: Options['ColorState'], prevState?: Options['ColorState']) => RLottieColor;
@ -20,6 +29,7 @@ export class SuperRLottieIcon<Options extends {
constructor(options: { constructor(options: {
width: number, width: number,
height: number, height: number,
skipAnimation?: boolean,
getPart: (state: Options['PartState'], prevState?: Options['PartState']) => SuperRLottieIconGetInfoResult, getPart: (state: Options['PartState'], prevState?: Options['PartState']) => SuperRLottieIconGetInfoResult,
getColor?: (state: Options['ColorState'], prevState?: Options['ColorState']) => RLottieColor, getColor?: (state: Options['ColorState'], prevState?: Options['ColorState']) => RLottieColor,
}) { }) {
@ -59,37 +69,58 @@ export class SuperRLottieIcon<Options extends {
return Promise.all(promises).then(noop); return Promise.all(promises).then(noop);
} }
public setState(partState: Options['PartState'], colorState?: Options['ColorState']) { /**
* Will redirect setting color state to part callback to synchronize the rendering
*/
public setState(partState: Options['PartState'], colorState?: Options['ColorState'], partCallback?: () => void) {
if(!this.loaded) this.load(partState, colorState); if(!this.loaded) this.load(partState, colorState);
if(partState !== undefined) this.setPartState(partState);
if(colorState !== undefined && this.getColor) this.setColorState(colorState); let changedPartState = false, changedColorState = false;
if(partState !== undefined) changedPartState = this.setPartState(partState, colorState, partCallback);
else if(colorState !== undefined && this.getColor) changedColorState = this.setColorState(colorState);
return changedPartState || changedColorState;
} }
public setPartState(state: Options['PartState']) { public setPartState(state: Options['PartState'], colorState?: Options['ColorState'], callback?: () => void) {
const {partState: prevState} = this; const {partState: prevState} = this;
if(prevState === state) { if(prevState === state) {
return; return colorState !== undefined ? this.setColorState(colorState) : false;
}
if(colorState !== undefined) {
this.setColorState(colorState, false);
} }
this.partState = state; this.partState = state;
const part = this.getPart(state, prevState); const part = this.getPart(state, prevState);
part.play(); part.play(callback);
return true;
} }
public setColorState(state: Options['ColorState']) { public setColorState(state: Options['ColorState'], renderIfPaused = true) {
const {colorState: prevState} = this; const {colorState: prevState} = this;
if(prevState === state) { if(prevState === state) {
return; return false;
} }
this.colorState = state; this.colorState = state;
const item = this.getItem(); const item = this.getItem();
const color = this.getColor(state, prevState);
const invoke = () => {
item.player.setColor(color, renderIfPaused);
};
if(item.player) { if(item.player) {
const color = this.getColor(state, prevState); invoke();
item.player.setColor(color); } else {
item.onLoadForColor = invoke;
} }
return true;
} }
public destroy() { public destroy() {

215
src/components/topbarCall.ts

@ -7,7 +7,7 @@
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
import ListenerSetter from "../helpers/listenerSetter"; import ListenerSetter from "../helpers/listenerSetter";
import type { AppGroupCallsManager, GroupCallInstance } from "../lib/appManagers/appGroupCallsManager"; import type { AppGroupCallsManager } from "../lib/appManagers/appGroupCallsManager";
import GROUP_CALL_STATE from "../lib/calls/groupCallState"; import GROUP_CALL_STATE from "../lib/calls/groupCallState";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import ButtonIcon from "./buttonIcon"; import ButtonIcon from "./buttonIcon";
@ -18,49 +18,31 @@ import type { AppPeersManager } from "../lib/appManagers/appPeersManager";
import type { AppChatsManager } from "../lib/appManagers/appChatsManager"; import type { AppChatsManager } from "../lib/appManagers/appChatsManager";
import GroupCallDescriptionElement from "./groupCall/description"; import GroupCallDescriptionElement from "./groupCall/description";
import GroupCallTitleElement from "./groupCall/title"; import GroupCallTitleElement from "./groupCall/title";
import { SuperRLottieIcon } from "./superIcon";
import PopupElement from "./popups"; import PopupElement from "./popups";
import throttle from "../helpers/schedulers/throttle"; import throttle from "../helpers/schedulers/throttle";
import GroupCallInstance from "../lib/calls/groupCallInstance";
export class GroupCallMicrophoneIconMini extends SuperRLottieIcon<{ import CALL_STATE from "../lib/calls/callState";
PartState: boolean import replaceContent from "../helpers/dom/replaceContent";
}> { import PeerTitle from "./peerTitle";
constructor() { import CallDescriptionElement from "./call/description";
super({ // import PopupCall from "./call";
width: 36, import type { AppAvatarsManager } from "../lib/appManagers/appAvatarsManager";
height: 36, import GroupCallMicrophoneIconMini from "./groupCall/microphoneIconMini";
getPart: (state) => {
return this.getItem().getPart(state ? 'unmute' : 'mute'); function convertCallStateToGroupState(state: CALL_STATE, isMuted: boolean) {
} switch(state) {
}); case CALL_STATE.CLOSING:
case CALL_STATE.CLOSED:
this.add({ return GROUP_CALL_STATE.CLOSED;
name: 'voice_mini', case CALL_STATE.CONNECTED:
parts: [{ return isMuted ? GROUP_CALL_STATE.MUTED : GROUP_CALL_STATE.UNMUTED;
startFrame: 0, default:
endFrame: 35, return GROUP_CALL_STATE.CONNECTING;
name: 'hand-to-muted'
}, {
startFrame: 36,
endFrame: 68,
name: 'unmute'
}, {
startFrame: 69,
endFrame: 98,
name: 'mute'
}, {
startFrame: 99,
endFrame: 135,
name: 'muted-to-hand'
}, {
startFrame: 136,
endFrame: 171,
name: 'unmuted-to-hand'
}]
});
} }
} }
const CLASS_NAME = 'topbar-call';
export default class TopbarCall { export default class TopbarCall {
public container: HTMLElement; public container: HTMLElement;
private listenerSetter: ListenerSetter; private listenerSetter: ListenerSetter;
@ -69,15 +51,28 @@ export default class TopbarCall {
private groupCallTitle: GroupCallTitleElement; private groupCallTitle: GroupCallTitleElement;
private groupCallDescription: GroupCallDescriptionElement; private groupCallDescription: GroupCallDescriptionElement;
private groupCallMicrophoneIconMini: GroupCallMicrophoneIconMini; private groupCallMicrophoneIconMini: GroupCallMicrophoneIconMini;
private callDescription: CallDescriptionElement;
private currentDescription: GroupCallDescriptionElement | CallDescriptionElement;
private instance: GroupCallInstance | any/* CallInstance */;
private instanceListenerSetter: ListenerSetter;
constructor( constructor(
private appGroupCallsManager: AppGroupCallsManager, private appGroupCallsManager: AppGroupCallsManager,
private appPeersManager: AppPeersManager, private appPeersManager: AppPeersManager,
private appChatsManager: AppChatsManager private appChatsManager: AppChatsManager,
private appAvatarsManager: AppAvatarsManager,
) { ) {
const listenerSetter = this.listenerSetter = new ListenerSetter(); const listenerSetter = this.listenerSetter = new ListenerSetter();
listenerSetter.add(rootScope)('group_call_state', (instance) => { listenerSetter.add(rootScope)('call_instance', ({instance, hasCurrent}) => {
if(!hasCurrent) {
this.updateInstance(instance);
}
});
listenerSetter.add(rootScope)('group_call_instance', (instance) => {
this.updateInstance(instance); this.updateInstance(instance);
}); });
@ -102,15 +97,49 @@ export default class TopbarCall {
}); });
} }
private updateInstance(instance: GroupCallInstance) { private onState = () => {
this.updateInstance(this.instance);
};
private clearCurrentInstance() {
if(!this.instance) return;
this.center.textContent = '';
if(this.currentDescription) {
this.currentDescription.detach();
this.currentDescription = undefined;
}
this.instance = undefined;
this.instanceListenerSetter.removeAll();
}
private updateInstance(instance: TopbarCall['instance']) {
if(this.construct) { if(this.construct) {
this.construct(); this.construct();
this.construct = undefined; this.construct = undefined;
} }
const {state, id} = instance; if(this.instance !== instance) {
this.clearCurrentInstance();
this.instance = instance;
this.instanceListenerSetter = new ListenerSetter();
this.instanceListenerSetter.add(instance as GroupCallInstance)('state', this.onState);
if(instance instanceof GroupCallInstance) {
this.currentDescription = this.groupCallDescription;
} else {
this.currentDescription = this.callDescription;
this.instanceListenerSetter.add(instance)('muted', this.onState);
}
}
const isMuted = this.instance.isMuted;
let state = instance instanceof GroupCallInstance ? instance.state : convertCallStateToGroupState(instance.connectionState, isMuted);
const {weave, container} = this; const {weave} = this;
weave.componentDidMount(); weave.componentDidMount();
@ -122,6 +151,8 @@ export default class TopbarCall {
SetTransition(document.body, 'is-calling', !isClosed, 250, isClosed ? () => { SetTransition(document.body, 'is-calling', !isClosed, 250, isClosed ? () => {
weave.componentWillUnmount(); weave.componentWillUnmount();
this.clearCurrentInstance();
}: undefined); }: undefined);
} }
@ -129,47 +160,45 @@ export default class TopbarCall {
return; return;
} }
if(state === GROUP_CALL_STATE.CONNECTING) { weave.setCurrentState(state, true);
weave.setCurrentState(GROUP_CALL_STATE.CONNECTING, true); // if(state === GROUP_CALL_STATE.CONNECTING) {
} else { // weave.setCurrentState(state, true);
/* var a = 0; // } else {
animate(() => { // /* var a = 0;
a += 0.1; // animate(() => {
if(a > 1) a = 0; // a += 0.1;
weave.setAmplitude(a); // if(a > 1) a = 0;
return true; // weave.setAmplitude(a);
}); // return true;
weave.setAmplitude(1); */ // });
weave.setCurrentState(state, true); // weave.setAmplitude(1); */
} // weave.setCurrentState(state, true);
// }
container.dataset.callId = '' + id;
this.setTitle(instance); this.setTitle(instance);
this.setDescription(instance); this.setDescription(instance);
this.groupCallMicrophoneIconMini.setState(state === GROUP_CALL_STATE.UNMUTED); this.groupCallMicrophoneIconMini.setState(!isMuted);
const className = 'state-' + state;
if(container.classList.contains(className)) {
return;
}
} }
private setDescription(instance: GroupCallInstance) { private setDescription(instance: TopbarCall['instance']) {
return this.groupCallDescription.update(instance); return this.currentDescription.update(instance as any);
} }
private setTitle(instance: GroupCallInstance) { private setTitle(instance: TopbarCall['instance']) {
return this.groupCallTitle.update(instance); if(instance instanceof GroupCallInstance) {
return this.groupCallTitle.update(instance);
} else {
replaceContent(this.center, new PeerTitle({peerId: instance.interlocutorUserId.toPeerId()}).element);
}
} }
private construct() { private construct() {
const {listenerSetter} = this; const {listenerSetter} = this;
const container = this.container = document.createElement('div'); const container = this.container = document.createElement('div');
container.classList.add('sidebar-header', 'topbar-call-container'); container.classList.add('sidebar-header', CLASS_NAME + '-container');
const left = document.createElement('div'); const left = document.createElement('div');
left.classList.add('topbar-call-left'); left.classList.add(CLASS_NAME + '-left');
const groupCallMicrophoneIconMini = this.groupCallMicrophoneIconMini = new GroupCallMicrophoneIconMini(); const groupCallMicrophoneIconMini = this.groupCallMicrophoneIconMini = new GroupCallMicrophoneIconMini();
@ -178,7 +207,7 @@ export default class TopbarCall {
left.append(mute); left.append(mute);
const throttledMuteClick = throttle(() => { const throttledMuteClick = throttle(() => {
this.appGroupCallsManager.toggleMuted(); this.instance.toggleMuted();
}, 600, true); }, 600, true);
attachClickEvent(mute, (e) => { attachClickEvent(mute, (e) => {
@ -187,32 +216,52 @@ export default class TopbarCall {
}, {listenerSetter}); }, {listenerSetter});
const center = this.center = document.createElement('div'); const center = this.center = document.createElement('div');
center.classList.add('topbar-call-center'); center.classList.add(CLASS_NAME + '-center');
this.groupCallTitle = new GroupCallTitleElement(center); this.groupCallTitle = new GroupCallTitleElement(center);
this.groupCallDescription = new GroupCallDescriptionElement(left); this.groupCallDescription = new GroupCallDescriptionElement(left);
this.callDescription = new CallDescriptionElement(left);
const right = document.createElement('div'); const right = document.createElement('div');
right.classList.add('topbar-call-right'); right.classList.add(CLASS_NAME + '-right');
const end = ButtonIcon('endcall_filled'); const end = ButtonIcon('endcall_filled');
right.append(end); right.append(end);
attachClickEvent(end, (e) => { attachClickEvent(end, (e) => {
cancelEvent(e); cancelEvent(e);
this.appGroupCallsManager.hangUp(container.dataset.callId, false, false);
}, {listenerSetter});
attachClickEvent(container, () => { const {instance} = this;
if(PopupElement.getPopup(PopupGroupCall)) { if(!instance) {
return; return;
} }
new PopupGroupCall({ if(instance instanceof GroupCallInstance) {
appGroupCallsManager: this.appGroupCallsManager, instance.hangUp();
appPeersManager: this.appPeersManager, } else {
appChatsManager: this.appChatsManager instance.hangUp('phoneCallDiscardReasonHangup');
}).show(); }
}, {listenerSetter});
attachClickEvent(container, () => {
if(this.instance instanceof GroupCallInstance) {
if(PopupElement.getPopup(PopupGroupCall)) {
return;
}
new PopupGroupCall({
appGroupCallsManager: this.appGroupCallsManager,
appPeersManager: this.appPeersManager,
appChatsManager: this.appChatsManager
}).show();
}/* else if(this.instance instanceof CallInstance) {
new PopupCall({
appAvatarsManager: this.appAvatarsManager,
appPeersManager: this.appPeersManager,
instance: this.instance
}).show();
} */
}, {listenerSetter}); }, {listenerSetter});
container.append(left, center, right); container.append(left, center, right);

2
src/config/app.ts

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

5
src/environment/callSupport.ts

@ -0,0 +1,5 @@
import IS_WEBRTC_SUPPORTED from "./webrtcSupport";
const IS_CALL_SUPPORTED = IS_WEBRTC_SUPPORTED && false;
export default IS_CALL_SUPPORTED;

4
src/environment/parallaxSupport.ts

@ -1,5 +1,5 @@
import { IS_FIREFOX } from "./userAgent"; import { IS_FIREFOX } from "./userAgent";
const PARALLAX_SUPPORTED = !IS_FIREFOX && false; const IS_PARALLAX_SUPPORTED = !IS_FIREFOX && false;
export default PARALLAX_SUPPORTED; export default IS_PARALLAX_SUPPORTED;

60
src/helpers/audioAssetPlayer.ts

@ -0,0 +1,60 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
const ASSETS_PATH = 'assets/audio/';
export default class AudioAssetPlayer<AssetName extends string> {
private audio: HTMLAudioElement;
private tempId: number;
constructor(private assets: AssetName[]) {
this.tempId = 0;
}
public playSound(name: AssetName, loop = false) {
++this.tempId;
try {
const audio = this.createAudio();
audio.src = ASSETS_PATH + name;
audio.loop = loop;
audio.play();
} catch(e) {
console.error('playSound', name, e);
}
}
public createAudio() {
let {audio} = this;
if(audio) {
return audio;
}
audio = this.audio = new Audio();
audio.play();
return audio;
}
public stopSound() {
this.audio.pause();
}
public cancelDelayedPlay() {
++this.tempId;
}
public playSoundWithTimeout(name: AssetName, loop: boolean, timeout: number) {
// timeout = 0;
const tempId = ++this.tempId;
setTimeout(() => {
if(this.tempId !== tempId) {
return;
}
this.playSound(name, loop);
}, timeout);
}
}

28
src/helpers/dom/attachListNavigation.ts

@ -47,7 +47,13 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
target.classList.add(ACTIVE_CLASS_NAME); target.classList.add(ACTIVE_CLASS_NAME);
if(hadTarget && scrollable && scrollTo) { if(hadTarget && scrollable && scrollTo) {
fastSmoothScroll(scrollable, target as HTMLElement, 'center', undefined, undefined, undefined, 100, type === 'x' ? 'x' : 'y'); fastSmoothScroll({
container: scrollable,
element: target as HTMLElement,
position: 'center',
forceDuration: 100,
axis: type === 'x' ? 'x' : 'y'
});
} }
}; };
@ -138,7 +144,20 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
} }
}; };
let attached = false;
const attach = () => {
if(attached) return;
attached = true;
// const input = document.activeElement as HTMLElement;
// input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
list.addEventListener('mousemove', onMouseMove, {passive: true});
attachClickEvent(list, onClick);
};
const detach = () => { const detach = () => {
if(!attached) return;
attached = false;
// input.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true}); // input.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true}); document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
list.removeEventListener('mousemove', onMouseMove); list.removeEventListener('mousemove', onMouseMove);
@ -168,13 +187,10 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
resetTarget(); resetTarget();
} }
// const input = document.activeElement as HTMLElement; attach();
// input.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
document.addEventListener(HANDLE_EVENT, onKeyDown, {capture: true, passive: false});
list.addEventListener('mousemove', onMouseMove, {passive: true});
attachClickEvent(list, onClick);
return { return {
attach,
detach, detach,
resetTarget resetTarget
}; };

14
src/helpers/dom/getVisibleRect.ts

@ -4,17 +4,19 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
export default function getVisibleRect(element: HTMLElement, overflowElement: HTMLElement) { export default function getVisibleRect(element: HTMLElement, overflowElement: HTMLElement, lookForSticky?: boolean) {
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const overflowRect = overflowElement.getBoundingClientRect(); const overflowRect = overflowElement.getBoundingClientRect();
let {top: overflowTop, bottom: overflowBottom} = overflowRect; let {top: overflowTop, bottom: overflowBottom} = overflowRect;
// * respect sticky headers // * respect sticky headers
const sticky = overflowElement.querySelector('.sticky'); if(lookForSticky) {
if(sticky) { const sticky = overflowElement.querySelector('.sticky');
const stickyRect = sticky.getBoundingClientRect(); if(sticky) {
overflowTop = stickyRect.bottom; const stickyRect = sticky.getBoundingClientRect();
overflowTop = stickyRect.bottom;
}
} }
if(rect.top >= overflowBottom if(rect.top >= overflowBottom
@ -48,3 +50,5 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
overflow overflow
}; };
} }
(window as any).getVisibleRect = getVisibleRect;

23
src/helpers/eachMinute.ts

@ -4,28 +4,9 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import ctx from "../environment/ctx"; import eachTimeout from "./eachTimeout";
import noop from "./noop";
// It's better to use timeout instead of interval, because interval can be corrupted // It's better to use timeout instead of interval, because interval can be corrupted
export default function eachMinute(callback: () => any, runFirst = true) { export default function eachMinute(callback: () => any, runFirst = true) {
const cancel = () => { return eachTimeout(callback, () => (60 - new Date().getSeconds()) * 1000, runFirst);
clearTimeout(timeout);
};
// replace callback to run noop and restore after
const _callback = callback;
if(!runFirst) {
callback = noop;
}
let timeout: number;
(function run() {
callback();
timeout = ctx.setTimeout(run, (60 - new Date().getSeconds()) * 1000);
})();
callback = _callback;
return cancel;
} }

31
src/helpers/eachTimeout.ts

@ -0,0 +1,31 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import ctx from "../environment/ctx";
import noop from "./noop";
// It's better to use timeout instead of interval, because interval can be corrupted
export default function eachTimeout(callback: () => any, getNextTimeout: () => number, runFirst = true) {
const cancel = () => {
clearTimeout(timeout);
};
// replace callback to run noop and restore after
const _callback = callback;
if(!runFirst) {
callback = noop;
}
let timeout: number;
(function run() {
callback();
timeout = ctx.setTimeout(run, getNextTimeout());
})();
callback = _callback;
return cancel;
}

6
src/helpers/eventListenerBase.ts

@ -49,6 +49,8 @@ import type { ArgumentTypes, SuperReturnType } from "../types";
// const e = new EventSystem(); // const e = new EventSystem();
// MOUNT_CLASS_TO.e = e; // MOUNT_CLASS_TO.e = e;
export type EventListenerListeners = Record<string, Function>;
/** /**
* Better not to remove listeners during setting * Better not to remove listeners during setting
* Should add listener callback only once * Should add listener callback only once
@ -56,7 +58,7 @@ import type { ArgumentTypes, SuperReturnType } from "../types";
// type EventLitenerCallback<T> = (data: T) => // type EventLitenerCallback<T> = (data: T) =>
// export default class EventListenerBase<Listeners extends {[name: string]: Function}> { // export default class EventListenerBase<Listeners extends {[name: string]: Function}> {
export default class EventListenerBase<Listeners extends Record<string, Function>> { export default class EventListenerBase<Listeners extends EventListenerListeners> {
protected listeners: Partial<{ protected listeners: Partial<{
[k in keyof Listeners]: Array<{callback: Listeners[k], options: boolean | AddEventListenerOptions}> [k in keyof Listeners]: Array<{callback: Listeners[k], options: boolean | AddEventListenerOptions}>
}>; }>;
@ -128,7 +130,7 @@ export default class EventListenerBase<Listeners extends Record<string, Function
try { try {
result = listener.callback(...args); result = listener.callback(...args);
} catch(err) { } catch(err) {
console.error(err);
} }
if(arr) { if(arr) {

116
src/helpers/fastSmoothScroll.ts

@ -25,26 +25,40 @@ export enum FocusDirection {
export type ScrollGetNormalSizeCallback = (options: {rect: DOMRect}) => number; export type ScrollGetNormalSizeCallback = (options: {rect: DOMRect}) => number;
export default function fastSmoothScroll( export type ScrollOptions = {
container: HTMLElement, container: HTMLElement,
element: HTMLElement, element: HTMLElement,
position: ScrollLogicalPosition, position: ScrollLogicalPosition,
margin = 0, margin?: number,
maxDistance = LONG_TRANSITION_MAX_DISTANCE, maxDistance?: number,
forceDirection?: FocusDirection, forceDirection?: FocusDirection,
forceDuration?: number, forceDuration?: number,
axis: 'x' | 'y' = 'y', axis?: 'x' | 'y',
getNormalSize?: ScrollGetNormalSizeCallback getNormalSize?: ScrollGetNormalSizeCallback,
) { fallbackToElementStartWhenCentering?: HTMLElement
};
export default function fastSmoothScroll(options: ScrollOptions) {
if(options.margin === undefined) {
options.margin = 0;
}
if(options.maxDistance === undefined) {
options.maxDistance = LONG_TRANSITION_MAX_DISTANCE;
}
if(options.axis === undefined) {
options.axis = 'y';
}
//return; //return;
if(!rootScope.settings.animationsEnabled) { if(!rootScope.settings.animationsEnabled) {
forceDirection = FocusDirection.Static; options.forceDirection = FocusDirection.Static;
} }
if(forceDirection === FocusDirection.Static) { if(options.forceDirection === FocusDirection.Static) {
forceDuration = 0; options.forceDuration = 0;
return scrollWithJs(container, element, position, margin, forceDuration, axis, getNormalSize); return scrollWithJs(options);
/* return Promise.resolve(); /* return Promise.resolve();
element.scrollIntoView({ block: position }); element.scrollIntoView({ block: position });
@ -53,58 +67,17 @@ export default function fastSmoothScroll(
return Promise.resolve(); */ return Promise.resolve(); */
} }
if(axis === 'y' && element !== container && isInDOM(element) && container.getBoundingClientRect) {
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const offsetTop = elementRect.top - containerRect.top;
if(forceDirection === undefined) {
if(offsetTop < -maxDistance) {
container.scrollTop += (offsetTop + maxDistance);
} else if(offsetTop > maxDistance) {
container.scrollTop += (offsetTop - maxDistance);
}
} else if(forceDirection === FocusDirection.Up) { // * not tested yet
container.scrollTop = offsetTop + container.scrollTop + maxDistance;
} else if(forceDirection === FocusDirection.Down) { // * not tested yet
container.scrollTop = Math.max(0, offsetTop + container.scrollTop - maxDistance);
}
/* const { offsetTop } = element;
if(forceDirection === undefined) {
const offset = offsetTop - container.scrollTop;
if(offset < -maxDistance) {
container.scrollTop += (offset + maxDistance);
} else if(offset > maxDistance) {
container.scrollTop += (offset - maxDistance);
}
} else if(forceDirection === FocusDirection.Up) {
container.scrollTop = offsetTop + maxDistance;
} else if(forceDirection === FocusDirection.Down) {
container.scrollTop = Math.max(0, offsetTop - maxDistance);
} */
}
const promise = new Promise<void>((resolve) => { const promise = new Promise<void>((resolve) => {
fastRaf(() => { fastRaf(() => {
scrollWithJs(container, element, position, margin, forceDuration, axis, getNormalSize) scrollWithJs(options).then(resolve);
.then(resolve);
}); });
}); });
return axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise; return options.axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise;
} }
function scrollWithJs( function scrollWithJs(options: ScrollOptions): Promise<void> {
container: HTMLElement, const {element, container, getNormalSize, axis, margin, position, forceDirection, maxDistance, forceDuration} = options;
element: HTMLElement,
position: ScrollLogicalPosition,
margin = 0,
forceDuration?: number,
axis: 'x' | 'y' = 'y',
getNormalSize?: ScrollGetNormalSizeCallback
) {
if(!isInDOM(element)) { if(!isInDOM(element)) {
cancelAnimationByKey(container); cancelAnimationByKey(container);
return Promise.resolve(); return Promise.resolve();
@ -143,14 +116,23 @@ function scrollWithJs(
path = elementPosition - margin; path = elementPosition - margin;
break; break;
case 'end': case 'end':
path = elementRect[rectEndKey] + (elementSize - elementRect[sizeKey]) - containerRect[rectEndKey]; path = elementRect[rectEndKey] /* + (elementSize - elementRect[sizeKey]) */ - containerRect[rectEndKey] + margin;
break; break;
// 'nearest' is not supported yet // 'nearest' is not supported yet
case 'nearest': case 'nearest':
case 'center': case 'center':
path = elementSize < containerSize if(elementSize < containerSize) {
? (elementPosition + elementSize / 2) - (containerSize / 2) path = (elementPosition + elementSize / 2) - (containerSize / 2);
: elementPosition - margin; } else {
if(options.fallbackToElementStartWhenCentering && options.fallbackToElementStartWhenCentering !== element) {
options.element = options.fallbackToElementStartWhenCentering;
options.position = 'start';
return scrollWithJs(options);
}
path = elementPosition - margin;
}
break; break;
} }
/* switch (position) { /* switch (position) {
@ -169,6 +151,22 @@ function scrollWithJs(
break; break;
} */ } */
if(axis === 'y') {
if(forceDirection === undefined) {
if(path > maxDistance) {
container.scrollTop += path - maxDistance;
path = maxDistance;
} else if(path < -maxDistance) {
container.scrollTop += path + maxDistance;
path = -maxDistance;
}
}/* else if(forceDirection === FocusDirection.Up) { // * not tested yet
container.scrollTop = offsetTop + container.scrollTop + maxDistance;
} else if(forceDirection === FocusDirection.Down) { // * not tested yet
container.scrollTop = Math.max(0, offsetTop + container.scrollTop - maxDistance);
} */
}
// console.log('scrollWithJs: will scroll path:', path, element); // console.log('scrollWithJs: will scroll path:', path, element);
/* let existsTransform = 0; /* let existsTransform = 0;

2
src/helpers/formatPhoneNumber.ts

@ -26,6 +26,8 @@ export function formatPhoneNumber(originalStr: string): {
code: HelpCountryCode, code: HelpCountryCode,
leftPattern: string leftPattern: string
} { } {
originalStr = originalStr || '';
if(!prefixes.size) { if(!prefixes.size) {
I18n.countriesList.forEach(country => { I18n.countriesList.forEach(country => {
country.country_codes.forEach(code => { country.country_codes.forEach(code => {

4
src/helpers/middleware.ts

@ -12,10 +12,10 @@ export const getMiddleware = () => {
cleanupObj.cleaned = true; cleanupObj.cleaned = true;
cleanupObj = {cleaned: false}; cleanupObj = {cleaned: false};
}, },
get: () => { get: (additionalCallback?: () => boolean) => {
const _cleanupObj = cleanupObj; const _cleanupObj = cleanupObj;
return () => { return () => {
return !_cleanupObj.cleaned; return !_cleanupObj.cleaned && (!additionalCallback || additionalCallback());
}; };
} }
}; };

84
src/helpers/movablePanel.ts

@ -0,0 +1,84 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import MovableElement, { MovableElementOptions, MovableState } from "../components/movableElement";
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import ListenerSetter from "./listenerSetter";
import mediaSizes, { ScreenSize } from "./mediaSizes";
import { safeAssign } from "./object";
export default class MovablePanel {
#movable: MovableElement;
private listenerSetter: ListenerSetter;
private previousState: MovableState;
private onResize: () => void;
private movableOptions: MovableElementOptions;
constructor(options: {
listenerSetter: ListenerSetter,
previousState: MovableState,
onResize?: () => void,
movableOptions: MovableElementOptions
}) {
safeAssign(this, options);
this.toggleMovable(!IS_TOUCH_SUPPORTED);
this.listenerSetter.add(mediaSizes)('changeScreen', (from, to) => {
if(to === ScreenSize.mobile || from === ScreenSize.mobile) {
this.toggleMovable(!IS_TOUCH_SUPPORTED);
}
});
}
public destroy() {
const movable = this.movable;
if(movable) {
movable.destroy();
}
}
public get movable() {
return this.#movable;
}
public get state() {
return this.movable ? this.movable.state : this.previousState;
}
public set state(state: MovableState) {
this.previousState = state;
}
private toggleMovable(enabled: boolean) {
let {movable} = this;
if(enabled) {
if(movable) {
return;
}
movable = this.#movable = new MovableElement(this.movableOptions);
movable.state = this.previousState;
if(this.previousState.top === undefined) {
movable.setPositionToCenter();
}
if(this.onResize) {
this.listenerSetter.add(movable)('resize', this.onResize);
}
} else {
if(!movable) {
return;
}
this.previousState = movable.state;
movable.destroyElements();
movable.destroy();
this.#movable = undefined;
}
}
}

53
src/lang.ts

@ -6,7 +6,6 @@ const lang = {
"BlockModal.Search.Placeholder": "Block user...", "BlockModal.Search.Placeholder": "Block user...",
"DarkMode": "Dark Mode", "DarkMode": "Dark Mode",
"FilterIncludeExcludeInfo": "Choose chats and types of chats that will\nappear and never appear in this folder.", "FilterIncludeExcludeInfo": "Choose chats and types of chats that will\nappear and never appear in this folder.",
"FilterNameInputLabel": "Folder Name",
"FilterMenuDelete": "Delete Folder", "FilterMenuDelete": "Delete Folder",
"FilterHeaderEdit": "Edit Folder", "FilterHeaderEdit": "Edit Folder",
"FilterAllGroups": "All Groups", "FilterAllGroups": "All Groups",
@ -82,6 +81,8 @@ const lang = {
"Message.Context.Selection.Delete": "Delete selected", "Message.Context.Selection.Delete": "Delete selected",
"Message.Context.Selection.Forward": "Forward selected", "Message.Context.Selection.Forward": "Forward selected",
"Message.Context.Selection.SendNow": "Send Now selected", "Message.Context.Selection.SendNow": "Send Now selected",
"Message.Unsupported.Desktop": "__This message is currently not supported on Telegram Web. Try [getdesktop.telegram.org](https://getdesktop.telegram.org/)__",
"Message.Unsupported.Mobile": "__This message is currently not supported on Telegram Web. Try [telegram.org/dl](https://telegram.org/dl/)__",
"Checkbox.Enabled": "Enabled", "Checkbox.Enabled": "Enabled",
"Checkbox.Disabled": "Disabled", "Checkbox.Disabled": "Disabled",
"Error.PreviewSender.CaptionTooLong": "Caption is too long.", "Error.PreviewSender.CaptionTooLong": "Caption is too long.",
@ -592,6 +593,27 @@ const lang = {
"ClearButton": "Clear", "ClearButton": "Clear",
"FilterAllChats": "All Chats", "FilterAllChats": "All Chats",
"FilterAllChatsShort": "All", "FilterAllChatsShort": "All",
"Call": "Call",
"VideoCall": "Video Call",
"CallMessageOutgoing": "Outgoing Call",
"CallMessageIncoming": "Incoming Call",
"CallMessageVideoOutgoing": "Outgoing Video Call",
"CallMessageVideoIncoming": "Incoming Video Call",
"VoipExchangingKeys": "Exchanging encryption keys",
"VoipUnmute": "Unmute",
"SavingContentTitle": "Saving content",
"RestrictSavingContent": "Restrict saving content",
"RestrictSavingContentInfoGroup": "Members won\'t be able to copy, save and forward content from this group.",
"RestrictSavingContentInfoChannel": "Subscribers won\'t be able to copy, save and forward content from this channel.",
"ClearOtherSessionsHelp": "Logs out all devices except for this one.",
"SessionsListInfo": "The official Telegram app is available for Android, iPhone, iPad, Windows, macOS and Linux.",
"SponsoredMessage": "sponsored",
"OpenChannelPost": "VIEW POST",
"FilterNameHint": "Folder name",
"BotStart": "START",
"BotUnblock": "RESTART",
"BotStop": "Stop bot",
"BotRestart": "Restart bot",
// * macos // * macos
"AccountSettings.Filters": "Chat Folders", "AccountSettings.Filters": "Chat Folders",
@ -602,8 +624,28 @@ const lang = {
"Alert.Confirm.Discard": "Discard", "Alert.Confirm.Discard": "Discard",
"Appearance.Reset": "Reset to Defaults", "Appearance.Reset": "Reset to Defaults",
"Bio.Description": "Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco", "Bio.Description": "Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco",
"Call.Accept": "Accept",
"Call.Decline": "Decline",
"Call.End": "End",
"Call.Camera": "Camera",
"Call.Mute": "Mute",
"Call.Recall": "Recall",
"Call.Close": "Close",
"Call.Screen": "Screen",
"Call.Confirm.Discard.Voice.Header": "Video Chat in Progress", "Call.Confirm.Discard.Voice.Header": "Video Chat in Progress",
"Call.Confirm.Discard.Voice.ToVoice.Text": "Leave video chat in \"%1$@\" and start a new one in \"%2$@\"?", "Call.Confirm.Discard.Voice.ToVoice.Text": "Leave video chat in \"%1$@\" and start a new one in \"%2$@\"?",
"Call.Confirm.Discard.Voice.ToCall.Text": "Leave video chat in \"%1$@\" and start a call with \"%2$@\"?",
"Call.Confirm.Discard.Call.Header": "Call in Progress",
"Call.Confirm.Discard.Call.ToVoice.Text": "End call with \"%1$@\" and start a video chat in \"%2$@\"?",
"Call.Confirm.Discard.Call.ToCall.Text": "End call with \"%1$@\" and start a video chat in \"%2$@\"?",
"Call.PrivacyErrorMessage": "Sorry, you cannot call %@ because of their privacy settings.",
"Call.StatusRequesting": "Contacting...",
"Call.StatusRinging": "Ringing...",
"Call.StatusConnecting": "Connecting...",
"Call.StatusEnded": "Call Ended",
"Call.StatusFailed": "Call Failed",
"Call.StatusBusy": "Busy",
"Call.StatusCalling": "is calling you...",
"Contacts.PhoneNumber.NotRegistred": "The person with this phone number is not registered on Telegram yet.", "Contacts.PhoneNumber.NotRegistred": "The person with this phone number is not registered on Telegram yet.",
"Channel.UsernameAboutChannel": "People can share this link with others and can find your channel using Telegram search.", "Channel.UsernameAboutChannel": "People can share this link with others and can find your channel using Telegram search.",
"Channel.UsernameAboutGroup": "People can share this link with others and find your group using Telegram search.", "Channel.UsernameAboutGroup": "People can share this link with others and find your group using Telegram search.",
@ -642,6 +684,8 @@ const lang = {
"Chat.DropQuickDesc": "in a quick way", "Chat.DropQuickDesc": "in a quick way",
"Chat.DropAsFilesDesc": "without compression", "Chat.DropAsFilesDesc": "without compression",
"Chat.Edit.Cancel.Text": "Are you sure you want to discard all changes?", "Chat.Edit.Cancel.Text": "Are you sure you want to discard all changes?",
"Chat.Service.Call.Cancelled": "Cancelled",
"Chat.Service.Call.Missed": "Missed",
"Chat.Service.PeerJoinedTelegram": "%@ joined Telegram", "Chat.Service.PeerJoinedTelegram": "%@ joined Telegram",
"Chat.Service.Channel.UpdatedTitle": "Channel renamed to \"%@\"", "Chat.Service.Channel.UpdatedTitle": "Channel renamed to \"%@\"",
"Chat.Service.Channel.UpdatedPhoto": "Channel photo updated", "Chat.Service.Channel.UpdatedPhoto": "Channel photo updated",
@ -700,6 +744,9 @@ const lang = {
"one_value": "Do you want to unpin %d message in this chat?", "one_value": "Do you want to unpin %d message in this chat?",
"other_value": "Do you want to unpin all %d messages in this chat?" "other_value": "Do you want to unpin all %d messages in this chat?"
}, },
"Chat.Message.ViewChannel": "VIEW CHANNEL",
"Chat.Message.ViewBot": "VIEW BOT",
"Chat.Message.ViewGroup": "VIEW GROUP",
"ChatList.Context.Mute": "Mute", "ChatList.Context.Mute": "Mute",
"ChatList.Context.Unmute": "Unmute", "ChatList.Context.Unmute": "Unmute",
"ChatList.Context.Pin": "Pin", "ChatList.Context.Pin": "Pin",
@ -710,8 +757,12 @@ const lang = {
"ChatList.Context.LeaveGroup": "Leave Group", "ChatList.Context.LeaveGroup": "Leave Group",
"ChatList.Service.Call.incoming": "Incoming Call (%@)", "ChatList.Service.Call.incoming": "Incoming Call (%@)",
"ChatList.Service.Call.outgoing": "Outgoing Call (%@)", "ChatList.Service.Call.outgoing": "Outgoing Call (%@)",
"ChatList.Service.VideoCall.incoming": "Incoming Video Call (%@)",
"ChatList.Service.VideoCall.outgoing": "Outgoing Video Call (%@)",
"ChatList.Service.Call.Cancelled": "Cancelled Call", "ChatList.Service.Call.Cancelled": "Cancelled Call",
"ChatList.Service.Call.Missed": "Missed Call", "ChatList.Service.Call.Missed": "Missed Call",
"ChatList.Service.VideoCall.Cancelled": "Cancelled Video Call",
"ChatList.Service.VideoCall.Missed": "Missed Video Call",
"ChatList.Service.VoiceChatScheduled.Channel": "Voice chat scheduled for %@", "ChatList.Service.VoiceChatScheduled.Channel": "Voice chat scheduled for %@",
"ChatList.Filter.Header": "Create folders for different groups of chats and quickly switch between them.", "ChatList.Filter.Header": "Create folders for different groups of chats and quickly switch between them.",
"ChatList.Filter.NewTitle": "Create Folder", "ChatList.Filter.NewTitle": "Create Folder",

655
src/layer.d.ts vendored

File diff suppressed because it is too large Load Diff

13
src/lib/appManagers/appChatsManager.ts

@ -395,6 +395,10 @@ export class AppChatsManager {
} }
} }
public getInputPeer(id: ChatId) {
return this.isChannel(id) ? this.getChannelInputPeer(id) : this.getChatInputPeer(id);
}
public getChatInputPeer(id: ChatId): InputPeer.inputPeerChat { public getChatInputPeer(id: ChatId): InputPeer.inputPeerChat {
return { return {
_: 'inputPeerChat', _: 'inputPeerChat',
@ -761,6 +765,15 @@ export class AppChatsManager {
apiUpdatesManager.processUpdateMessage(updates); apiUpdatesManager.processUpdateMessage(updates);
}); });
} }
public toggleNoForwards(id: ChatId, enabled: boolean) {
return apiManager.invokeApi('messages.toggleNoForwards', {
peer: this.getInputPeer(id),
enabled
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
});
}
} }
const appChatsManager = new AppChatsManager(); const appChatsManager = new AppChatsManager();

75
src/lib/appManagers/appDialogsManager.ts

@ -178,6 +178,9 @@ export class AppDialogsManager {
private loadedDialogsAtLeastOnce = false; private loadedDialogsAtLeastOnce = false;
private allChatsIntlElement: I18n.IntlElement; private allChatsIntlElement: I18n.IntlElement;
private emptyDialogsPlaceholderSubtitle: I18n.IntlElement;
private updateContactsLengthPromise: Promise<number>;
constructor() { constructor() {
this.chatsPreloader = putPreloader(null, true); this.chatsPreloader = putPreloader(null, true);
@ -787,10 +790,7 @@ export class AppDialogsManager {
private changeFiltersAllChatsKey() { private changeFiltersAllChatsKey() {
const scrollable = this.folders.menuScrollContainer.firstElementChild; const scrollable = this.folders.menuScrollContainer.firstElementChild;
const key: LangPackKey = scrollable.scrollWidth > scrollable.clientWidth ? 'FilterAllChatsShort' : 'FilterAllChats'; const key: LangPackKey = scrollable.scrollWidth > scrollable.clientWidth ? 'FilterAllChatsShort' : 'FilterAllChats';
if(this.allChatsIntlElement.key !== key) { this.allChatsIntlElement.compareAndUpdate({key});
this.allChatsIntlElement.key = key;
this.allChatsIntlElement.update();
}
} }
private onFiltersLengthChange() { private onFiltersLengthChange() {
@ -986,11 +986,11 @@ export class AppDialogsManager {
return; return;
} }
let placeholder: ReturnType<AppDialogsManager['generateEmptyPlaceholder']>; let placeholder: ReturnType<AppDialogsManager['generateEmptyPlaceholder']>, type: 'dialogs' | 'folder';
if(!this.filterId) { if(!this.filterId) {
placeholder = this.generateEmptyPlaceholder({ placeholder = this.generateEmptyPlaceholder({
title: 'ChatList.Main.EmptyPlaceholder.Title', title: 'ChatList.Main.EmptyPlaceholder.Title',
classNameType: 'dialogs' classNameType: type = 'dialogs'
}); });
placeholderContainer = placeholder.container; placeholderContainer = placeholder.container;
@ -998,28 +998,17 @@ export class AppDialogsManager {
const img = document.createElement('img'); const img = document.createElement('img');
img.classList.add('empty-placeholder-dialogs-icon'); img.classList.add('empty-placeholder-dialogs-icon');
Promise.all([ this.emptyDialogsPlaceholderSubtitle = new I18n.IntlElement({
appUsersManager.getContacts().then(users => { element: placeholder.subtitle
let key: LangPackKey, args: FormatterArguments; });
if(users.length/* && false */) {
key = 'ChatList.Main.EmptyPlaceholder.Subtitle';
args = [i18n('Contacts.Count', [users.length])];
} else {
key = 'ChatList.Main.EmptyPlaceholder.SubtitleNoContacts';
args = [];
}
const subtitleEl = new I18n.IntlElement({ Promise.all([
key, this.updateContactsLength(false),
args,
element: placeholder.subtitle
});
}),
renderImageFromUrlPromise(img, 'assets/img/EmptyChats.svg'), renderImageFromUrlPromise(img, 'assets/img/EmptyChats.svg'),
fastRafPromise() fastRafPromise()
]).then(() => { ]).then(([usersLength]) => {
placeholderContainer.classList.add('visible'); placeholderContainer.classList.add('visible');
part.classList.toggle('has-contacts', !!usersLength);
}); });
placeholderContainer.prepend(img); placeholderContainer.prepend(img);
@ -1027,7 +1016,7 @@ export class AppDialogsManager {
placeholder = this.generateEmptyPlaceholder({ placeholder = this.generateEmptyPlaceholder({
title: 'FilterNoChatsToDisplay', title: 'FilterNoChatsToDisplay',
subtitle: 'FilterNoChatsToDisplayInfo', subtitle: 'FilterNoChatsToDisplayInfo',
classNameType: 'folder' classNameType: type = 'folder'
}); });
placeholderContainer = placeholder.container; placeholderContainer = placeholder.container;
@ -1052,6 +1041,40 @@ export class AppDialogsManager {
part.append(placeholderContainer); part.append(placeholderContainer);
part.classList.add('with-placeholder'); part.classList.add('with-placeholder');
part.dataset.placeholderType = type;
}
private updateContactsLength(updatePartClassName: boolean) {
if(this.updateContactsLengthPromise) return this.updateContactsLengthPromise;
return this.updateContactsLengthPromise = appUsersManager.getContacts().then(users => {
const subtitle = this.emptyDialogsPlaceholderSubtitle;
if(subtitle) {
let key: LangPackKey, args: FormatterArguments;
if(users.length/* && false */) {
key = 'ChatList.Main.EmptyPlaceholder.Subtitle';
args = [i18n('Contacts.Count', [users.length])];
} else {
key = 'ChatList.Main.EmptyPlaceholder.SubtitleNoContacts';
args = [];
}
subtitle.compareAndUpdate({
key,
args
});
}
if(updatePartClassName) {
const chatList = this.chatList;
const part = chatList.parentElement as HTMLElement;
part.classList.toggle('has-contacts', !!users.length);
}
this.updateContactsLengthPromise = undefined;
return users.length;
});
} }
private removeContactsPlaceholder() { private removeContactsPlaceholder() {
@ -1103,6 +1126,8 @@ export class AppDialogsManager {
if(ready) { if(ready) {
section.container.classList.toggle('hide', !sortedUserList.list.childElementCount); section.container.classList.toggle('hide', !sortedUserList.list.childElementCount);
} }
this.updateContactsLength(true);
}; };
const sortedUserList = new SortedUserList({ const sortedUserList = new SortedUserList({

1331
src/lib/appManagers/appGroupCallsManager.ts

File diff suppressed because it is too large Load Diff

134
src/lib/appManagers/appImManager.ts

@ -76,6 +76,9 @@ import appGroupCallsManager, { GroupCallId, MyGroupCall } from './appGroupCallsM
import TopbarCall from '../../components/topbarCall'; import TopbarCall from '../../components/topbarCall';
import confirmationPopup from '../../components/confirmationPopup'; import confirmationPopup from '../../components/confirmationPopup';
import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport'; import IS_GROUP_CALL_SUPPORTED from '../../environment/groupCallSupport';
import appAvatarsManager from './appAvatarsManager';
import IS_CALL_SUPPORTED from '../../environment/callSupport';
import { CallType } from '../calls/types';
//console.log('appImManager included33!'); //console.log('appImManager included33!');
@ -244,7 +247,24 @@ export class AppImManager {
stateStorage.setToCache('chatPositions', c || {}); stateStorage.setToCache('chatPositions', c || {});
}); });
this.topbarCall = new TopbarCall(appGroupCallsManager, appPeersManager, appChatsManager); if(IS_CALL_SUPPORTED || IS_GROUP_CALL_SUPPORTED) {
this.topbarCall = new TopbarCall(appGroupCallsManager, appPeersManager, appChatsManager, appAvatarsManager);
}
/* if(IS_CALL_SUPPORTED) {
rootScope.addEventListener('call_instance', ({instance, hasCurrent}) => {
if(hasCurrent) {
return;
}
new PopupCall({
appAvatarsManager,
appCallsManager,
appPeersManager,
instance
}).show();
});
} */
// ! do not remove this line // ! do not remove this line
// ! instance can be deactivated before the UI starts, because it waits in background for RAF that is delayed // ! instance can be deactivated before the UI starts, because it waits in background for RAF that is delayed
@ -748,6 +768,79 @@ export class AppImManager {
}); });
} }
public async callUser(userId: UserId, type: CallType) {
/* const call = appCallsManager.getCallByUserId(userId);
if(call) {
return;
}
const userFull = await appProfileManager.getProfile(userId);
if(userFull.pFlags.phone_calls_private) {
confirmationPopup({
descriptionLangKey: 'Call.PrivacyErrorMessage',
descriptionLangArgs: [new PeerTitle({peerId: userId.toPeerId()}).element],
button: {
langKey: 'OK',
isCancel: true
}
});
return;
}
await this.discardCurrentCall(userId.toPeerId());
appCallsManager.startCallInternal(userId, type === 'video'); */
}
private discardCurrentCall(toPeerId: PeerId) {
/* if(appCallsManager.currentCall) return this.discardCallConfirmation(toPeerId);
else if(appGroupCallsManager.groupCall) return this.discardGroupCallConfirmation(toPeerId);
else return Promise.resolve(); */
}
private async discardCallConfirmation(toPeerId: PeerId) {
/* const currentCall = appCallsManager.currentCall;
if(currentCall) {
await confirmationPopup({
titleLangKey: 'Call.Confirm.Discard.Call.Header',
descriptionLangKey: toPeerId.isUser() ? 'Call.Confirm.Discard.Call.ToCall.Text' : 'Call.Confirm.Discard.Call.ToVoice.Text',
descriptionLangArgs: [
new PeerTitle({peerId: currentCall.interlocutorUserId.toPeerId(false)}).element,
new PeerTitle({peerId: toPeerId}).element
],
button: {
langKey: 'OK'
}
});
if(appCallsManager.currentCall === currentCall) {
await currentCall.hangUp();
}
} */
}
private async discardGroupCallConfirmation(toPeerId: PeerId) {
const currentGroupCall = appGroupCallsManager.groupCall;
if(currentGroupCall) {
await confirmationPopup({
titleLangKey: 'Call.Confirm.Discard.Voice.Header',
descriptionLangKey: toPeerId.isUser() ? 'Call.Confirm.Discard.Voice.ToCall.Text' : 'Call.Confirm.Discard.Voice.ToVoice.Text',
descriptionLangArgs: [
new PeerTitle({peerId: currentGroupCall.chatId.toPeerId(true)}).element,
new PeerTitle({peerId: toPeerId}).element
],
button: {
langKey: 'OK'
}
});
if(appGroupCallsManager.groupCall === currentGroupCall) {
await currentGroupCall.hangUp();
}
}
}
public async joinGroupCall(peerId: PeerId, groupCallId?: GroupCallId) { public async joinGroupCall(peerId: PeerId, groupCallId?: GroupCallId) {
const chatId = peerId.toChatId(); const chatId = peerId.toChatId();
const hasRights = appChatsManager.hasRights(chatId, 'manage_call'); const hasRights = appChatsManager.hasRights(chatId, 'manage_call');
@ -787,24 +880,7 @@ export class AppImManager {
} }
} }
const currentGroupCall = appGroupCallsManager.groupCall; await this.discardCurrentCall(peerId);
if(currentGroupCall) {
await confirmationPopup({
titleLangKey: 'Call.Confirm.Discard.Voice.Header',
descriptionLangKey: 'Call.Confirm.Discard.Voice.ToVoice.Text',
descriptionLangArgs: [
new PeerTitle({peerId: currentGroupCall.chatId.toPeerId(true)}).element,
new PeerTitle({peerId: peerId}).element
],
button: {
langKey: 'OK'
}
});
if(appGroupCallsManager.groupCall === currentGroupCall) {
await currentGroupCall.hangUp();
}
}
next(); next();
}; };
@ -1242,11 +1318,14 @@ export class AppImManager {
} }
this.chats.push(chat); this.chats.push(chat);
return chat;
} }
private spliceChats(fromIndex: number, justReturn = true, animate?: boolean, spliced?: Chat[]) { private spliceChats(fromIndex: number, justReturn = true, animate?: boolean, spliced?: Chat[]) {
if(fromIndex >= this.chats.length) return; if(fromIndex >= this.chats.length) return;
const chatFrom = this.chat;
if(this.chats.length > 1 && justReturn) { if(this.chats.length > 1 && justReturn) {
rootScope.dispatchEvent('peer_changing', this.chat); rootScope.dispatchEvent('peer_changing', this.chat);
} }
@ -1255,6 +1334,8 @@ export class AppImManager {
spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex); spliced = this.chats.splice(fromIndex, this.chats.length - fromIndex);
} }
rootScope.dispatchEvent('chat_changing', {from: chatFrom, to: this.chat});
// * -1 because one item is being sliced when closing the chat by calling .removeByType // * -1 because one item is being sliced when closing the chat by calling .removeByType
for(let i = 0; i < spliced.length - 1; ++i) { for(let i = 0; i < spliced.length - 1; ++i) {
appNavigationController.removeByType('chat', true); appNavigationController.removeByType('chat', true);
@ -1382,20 +1463,23 @@ export class AppImManager {
return this.setPeer(peerId, lastMsgId); return this.setPeer(peerId, lastMsgId);
} }
const chat = this.chat; const oldChat = this.chat;
if(chat.inited) { // * use first not inited chat let chat = oldChat;
this.createNewChat(); if(oldChat.inited) { // * use first not inited chat
chat = this.createNewChat();
} }
if(type) { if(type) {
this.chat.setType(type); chat.setType(type);
if(threadId) { if(threadId) {
this.chat.threadId = threadId; chat.threadId = threadId;
} }
} }
//this.chatsSelectTab(this.chat.container); rootScope.dispatchEvent('chat_changing', {from: oldChat, to: chat});
//this.chatsSelectTab(chat.container);
return this.setPeer(peerId, lastMsgId); return this.setPeer(peerId, lastMsgId);
} }

141
src/lib/appManagers/appMessagesManager.ts

@ -19,7 +19,7 @@ import { randomLong } from "../../helpers/random";
import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string"; import { splitStringByLength, limitSymbols, escapeRegExp } from "../../helpers/string";
import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer } from "../../layer"; import { Chat, ChatFull, Dialog as MTDialog, DialogPeer, DocumentAttribute, InputMedia, InputMessage, InputPeerNotifySettings, InputSingleMedia, Message, MessageAction, MessageEntity, MessageFwdHeader, MessageMedia, MessageReplies, MessageReplyHeader, MessagesDialogs, MessagesFilter, MessagesMessages, MethodDeclMap, NotifyPeer, PeerNotifySettings, PhotoSize, SendMessageAction, Update, Photo, Updates, ReplyMarkup, InputPeer, InputPhoto, InputDocument, InputGeoPoint, WebPage, GeoPoint, ReportReason, MessagesGetDialogs, InputChannel, InputDialogPeer } from "../../layer";
import { InvokeApiOptions } from "../../types"; import { InvokeApiOptions } from "../../types";
import I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, _i18n } from "../langPack"; import I18n, { FormatterArguments, i18n, join, langPack, LangPackKey, UNSUPPORTED_LANG_PACK_KEY, _i18n } from "../langPack";
import { logger, LogTypes } from "../logger"; import { logger, LogTypes } from "../logger";
import type { ApiFileManager } from '../mtproto/apiFileManager'; import type { ApiFileManager } from '../mtproto/apiFileManager';
//import apiManager from '../mtproto/apiManager'; //import apiManager from '../mtproto/apiManager';
@ -1214,6 +1214,7 @@ export class AppMessagesManager {
flags: 4, flags: 4,
total_voters: 0, total_voters: 0,
pFlags: {}, pFlags: {},
recent_voters: []
}); });
const {poll, results} = appPollsManager.getPoll(pollId); const {poll, results} = appPollsManager.getPoll(pollId);
@ -2384,7 +2385,7 @@ export class AppMessagesManager {
return appMessagesIdsManager.generateMessageId(dialog?.top_message || 0, true); return appMessagesIdsManager.generateMessageId(dialog?.top_message || 0, true);
} }
public saveMessage(message: any, options: Partial<{ public saveMessage(message: Message, options: Partial<{
storage: MessagesStorage, storage: MessagesStorage,
isScheduled: true, isScheduled: true,
isOutgoing: true, isOutgoing: true,
@ -2406,10 +2407,7 @@ export class AppMessagesManager {
const storage = options.storage || this.getMessagesStorage(peerId); const storage = options.storage || this.getMessagesStorage(peerId);
const isChannel = message.peer_id._ === 'peerChannel'; const isChannel = message.peer_id._ === 'peerChannel';
const isBroadcast = isChannel && appChatsManager.isBroadcast(peerId.toChatId()); const isBroadcast = isChannel && appChatsManager.isBroadcast(peerId.toChatId());
const isMessage = message._ === 'message';
if(options.isScheduled) {
message.pFlags.is_scheduled = true;
}
if(options.isOutgoing) { if(options.isOutgoing) {
message.pFlags.is_outgoing = true; message.pFlags.is_outgoing = true;
@ -2418,9 +2416,20 @@ export class AppMessagesManager {
const mid = appMessagesIdsManager.generateMessageId(message.id); const mid = appMessagesIdsManager.generateMessageId(message.id);
message.mid = mid; message.mid = mid;
if(message.grouped_id) { if(isMessage) {
const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = new Map()); if(options.isScheduled) {
storage.set(mid, message); message.pFlags.is_scheduled = true;
}
if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = new Map());
storage.set(mid, message);
}
if(message.via_bot_id) {
// ! WARNING
message.viaBotId = message.via_bot_id as any;
}
} }
const dialog = this.getDialogOnly(peerId); const dialog = this.getDialogOnly(peerId);
@ -2441,7 +2450,7 @@ export class AppMessagesManager {
if(message.reply_to.reply_to_top_id) message.reply_to.reply_to_top_id = appMessagesIdsManager.generateMessageId(message.reply_to.reply_to_top_id); if(message.reply_to.reply_to_top_id) message.reply_to.reply_to_top_id = appMessagesIdsManager.generateMessageId(message.reply_to.reply_to_top_id);
} }
if(message.replies) { if(isMessage && message.replies) {
if(message.replies.max_id) message.replies.max_id = appMessagesIdsManager.generateMessageId(message.replies.max_id); if(message.replies.max_id) message.replies.max_id = appMessagesIdsManager.generateMessageId(message.replies.max_id);
if(message.replies.read_max_id) message.replies.read_max_id = appMessagesIdsManager.generateMessageId(message.replies.read_max_id); if(message.replies.read_max_id) message.replies.read_max_id = appMessagesIdsManager.generateMessageId(message.replies.read_max_id);
} }
@ -2452,17 +2461,18 @@ export class AppMessagesManager {
} }
//storage.generateIndex(message); //storage.generateIndex(message);
const myId = appUsersManager.getSelf().id; const myId = appUsersManager.getSelf().id.toPeerId();
const fwdHeader = isMessage && (message as Message.message).fwd_from as MessageFwdHeader;
message.peerId = peerId; message.peerId = peerId;
if(peerId === myId/* && !message.from_id && !message.fwd_from */) { if(peerId === myId/* && !message.from_id && !message.fwd_from */) {
message.fromId = message.fwd_from ? (message.fwd_from.from_id ? appPeersManager.getPeerId(message.fwd_from.from_id) : 0) : myId; message.fromId = fwdHeader ? (fwdHeader.from_id ? appPeersManager.getPeerId(fwdHeader.from_id) : NULL_PEER_ID) : myId;
} else { } else {
//message.fromId = message.pFlags.post || (!message.pFlags.out && !message.from_id) ? peerId : appPeersManager.getPeerId(message.from_id); //message.fromId = message.pFlags.post || (!message.pFlags.out && !message.from_id) ? peerId : appPeersManager.getPeerId(message.from_id);
message.fromId = message.pFlags.post || !message.from_id ? peerId : appPeersManager.getPeerId(message.from_id); message.fromId = message.pFlags.post || !message.from_id ? peerId : appPeersManager.getPeerId(message.from_id);
} }
const fwdHeader = message.fwd_from as MessageFwdHeader;
if(fwdHeader) { if(fwdHeader) {
//if(peerId === myID) { //if(peerId === myID) {
if(fwdHeader.saved_from_msg_id) fwdHeader.saved_from_msg_id = appMessagesIdsManager.generateMessageId(fwdHeader.saved_from_msg_id); if(fwdHeader.saved_from_msg_id) fwdHeader.saved_from_msg_id = appMessagesIdsManager.generateMessageId(fwdHeader.saved_from_msg_id);
@ -2490,17 +2500,20 @@ export class AppMessagesManager {
} }
} }
if(message.via_bot_id > 0) {
message.viaBotId = message.via_bot_id;
}
const mediaContext: ReferenceContext = { const mediaContext: ReferenceContext = {
type: 'message', type: 'message',
peerId, peerId,
messageId: mid messageId: mid
}; };
if(message.media) { if(isMessage) {
const entities = message.entities;
if(entities && entities.find(entity => entity._ === 'messageEntitySpoiler')) {
message.media = {_: 'messageMediaUnsupported'};
}
}
if(isMessage && message.media) {
switch(message.media._) { switch(message.media._) {
case 'messageMediaEmpty': { case 'messageMediaEmpty': {
delete message.media; delete message.media;
@ -2509,12 +2522,12 @@ export class AppMessagesManager {
case 'messageMediaPhoto': { case 'messageMediaPhoto': {
if(message.media.ttl_seconds) { if(message.media.ttl_seconds) {
message.media = {_: 'messageMediaUnsupportedWeb'}; message.media = {_: 'messageMediaUnsupported'};
} else { } else {
message.media.photo = appPhotosManager.savePhoto(message.media.photo, mediaContext); message.media.photo = appPhotosManager.savePhoto(message.media.photo, mediaContext);
} }
if(!message.media.photo) { // * found this bug on test DC if(!(message.media as MessageMedia.messageMediaPhoto).photo) { // * found this bug on test DC
delete message.media; delete message.media;
} }
@ -2530,7 +2543,7 @@ export class AppMessagesManager {
case 'messageMediaDocument': { case 'messageMediaDocument': {
if(message.media.ttl_seconds) { if(message.media.ttl_seconds) {
message.media = {_: 'messageMediaUnsupportedWeb'}; message.media = {_: 'messageMediaUnsupported'};
} else { } else {
message.media.document = appDocsManager.saveDoc(message.media.document, mediaContext); // 11.04.2020 warning message.media.document = appDocsManager.saveDoc(message.media.document, mediaContext); // 11.04.2020 warning
} }
@ -2550,13 +2563,20 @@ export class AppMessagesManager {
break; */ break; */
case 'messageMediaInvoice': { case 'messageMediaInvoice': {
message.media = {_: 'messageMediaUnsupportedWeb'}; message.media = {_: 'messageMediaUnsupported'};
break;
}
case 'messageMediaUnsupported': {
message.message = '';
delete message.entities;
delete message.totalEntities;
break; break;
} }
} }
} }
if(message.action) { if(!isMessage && message.action) {
const action = message.action as MessageAction; const action = message.action as MessageAction;
let migrateFrom: PeerId; let migrateFrom: PeerId;
let migrateTo: PeerId; let migrateTo: PeerId;
@ -2674,12 +2694,14 @@ export class AppMessagesManager {
case 'messageActionPhoneCall': case 'messageActionPhoneCall':
// @ts-ignore // @ts-ignore
action.type = action.type =
(message.pFlags.out ? 'out_' : 'in_') + (action.pFlags.video ? 'video_' : '') +
(action.duration !== undefined ? (message.pFlags.out ? 'out_' : 'in_') : '') +
( (
action.reason._ === 'phoneCallDiscardReasonMissed' || action.duration !== undefined ? 'ok' : (
action.reason._ === 'phoneCallDiscardReasonBusy' action.reason._ === 'phoneCallDiscardReasonMissed'
? 'missed' ? 'missed'
: 'ok' : 'cancelled'
)
); );
break; break;
} }
@ -2702,7 +2724,7 @@ export class AppMessagesManager {
message.rReply = this.getRichReplyText(message); message.rReply = this.getRichReplyText(message);
} */ } */
if(message.message && message.message.length && !message.totalEntities) { if(isMessage && message.message.length && !message.totalEntities) {
this.wrapMessageEntities(message); this.wrapMessageEntities(message);
} }
@ -2839,6 +2861,11 @@ export class AppMessagesManager {
break; break;
} }
case 'messageMediaUnsupported': {
addPart(UNSUPPORTED_LANG_PACK_KEY);
break;
}
default: default:
//messageText += media._; //messageText += media._;
///////this.log.warn('Got unknown media type!', message); ///////this.log.warn('Got unknown media type!', message);
@ -4314,8 +4341,9 @@ export class AppMessagesManager {
return; return;
} }
this.scheduleHandleNewDialogs(peerId); (update as any).ignoreExisting = true;
set.add(update); set.add(update);
this.scheduleHandleNewDialogs(peerId);
} }
return; return;
@ -4342,27 +4370,32 @@ export class AppMessagesManager {
this.updateMessageRepliesIfNeeded(message); this.updateMessageRepliesIfNeeded(message);
} }
if(historyStorage.history.findSlice(message.mid)) { // * so message can exist if reloadConversation came back earlier with mid
return false; const ignoreExisting: boolean = (update as any).ignoreExisting;
} const isExisting = !!historyStorage.history.findSlice(message.mid);
if(isExisting) {
// * catch situation with disconnect. if message's id is lower than we already have (in bottom end slice), will sort it if(!ignoreExisting) {
const firstSlice = historyStorage.history.first; return false;
if(firstSlice.isEnd(SliceEnd.Bottom)) {
let i = 0;
for(const length = firstSlice.length; i < length; ++i) {
if(message.mid > firstSlice[i]) {
break;
}
} }
firstSlice.splice(i, 0, message.mid);
} else { } else {
historyStorage.history.unshift(message.mid); // * catch situation with disconnect. if message's id is lower than we already have (in bottom end slice), will sort it
} const firstSlice = historyStorage.history.first;
if(firstSlice.isEnd(SliceEnd.Bottom)) {
let i = 0;
for(const length = firstSlice.length; i < length; ++i) {
if(message.mid > firstSlice[i]) {
break;
}
}
if(historyStorage.count !== null) { firstSlice.splice(i, 0, message.mid);
historyStorage.count++; } else {
historyStorage.history.unshift(message.mid);
}
if(historyStorage.count !== null) {
historyStorage.count++;
}
} }
if(this.mergeReplyKeyboard(historyStorage, message)) { if(this.mergeReplyKeyboard(historyStorage, message)) {
@ -4414,7 +4447,7 @@ export class AppMessagesManager {
const inboxUnread = !message.pFlags.out && message.pFlags.unread; const inboxUnread = !message.pFlags.out && message.pFlags.unread;
if(dialog) { if(dialog) {
if(inboxUnread) { if(inboxUnread && message.mid > dialog.top_message) {
const releaseUnreadCount = this.dialogsStorage.prepareDialogUnreadCountModifying(dialog); const releaseUnreadCount = this.dialogsStorage.prepareDialogUnreadCountModifying(dialog);
++dialog.unread_count; ++dialog.unread_count;
@ -4426,7 +4459,9 @@ export class AppMessagesManager {
releaseUnreadCount(); releaseUnreadCount();
} }
this.setDialogTopMessage(message, dialog); if(message.mid >= dialog.top_message) {
this.setDialogTopMessage(message, dialog);
}
} }
if(inboxUnread/* && ($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE) */) { if(inboxUnread/* && ($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE) */) {
@ -4505,7 +4540,7 @@ export class AppMessagesManager {
} */ } */
const isTopMessage = dialog && dialog.top_message === mid; const isTopMessage = dialog && dialog.top_message === mid;
if((message as Message.message).clear_history) { if((message as Message.messageService).clear_history) {
if(isTopMessage) { if(isTopMessage) {
rootScope.dispatchEvent('dialog_flush', {peerId}); rootScope.dispatchEvent('dialog_flush', {peerId});
} }
@ -5873,6 +5908,10 @@ export class AppMessagesManager {
public isDialogUnread(dialog: Dialog) { public isDialogUnread(dialog: Dialog) {
return !!this.getDialogUnreadCount(dialog); return !!this.getDialogUnreadCount(dialog);
} }
public canForward(message: Message.message | Message.messageService) {
return !(message as Message.message).pFlags.noforwards && !appPeersManager.noForwards(message.peerId);
}
} }
const appMessagesManager = new AppMessagesManager(); const appMessagesManager = new AppMessagesManager();

14
src/lib/appManagers/appPeersManager.ts

@ -247,11 +247,7 @@ export class AppPeersManager {
if(!peerId.isUser()) { if(!peerId.isUser()) {
const chatId = peerId.toChatId(); const chatId = peerId.toChatId();
if(!appChatsManager.isChannel(chatId)) { return appChatsManager.getInputPeer(chatId);
return appChatsManager.getChatInputPeer(chatId);
} else {
return appChatsManager.getChannelInputPeer(chatId);
}
} }
const userId = peerId.toUserId(); const userId = peerId.toUserId();
@ -314,6 +310,14 @@ export class AppPeersManager {
return 'ChatList.Context.DeleteChat'; return 'ChatList.Context.DeleteChat';
} }
} }
public noForwards(peerId: PeerId) {
if(peerId.isUser()) return false;
else {
const chat = appChatsManager.getChatTyped(peerId.toChatId());
return !!(chat as Chat.chat).pFlags?.noforwards;
}
}
} }
export type IsPeerType = 'isChannel' | 'isMegagroup' | 'isAnyGroup' | 'isBroadcast' | 'isBot' | 'isContact' | 'isUser' | 'isAnyChat'; export type IsPeerType = 'isChannel' | 'isMegagroup' | 'isAnyGroup' | 'isBroadcast' | 'isBot' | 'isContact' | 'isUser' | 'isAnyChat';

18
src/lib/appManagers/appProfileManager.ts

@ -12,7 +12,7 @@
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import { tsNow } from "../../helpers/date"; import { tsNow } from "../../helpers/date";
import { numberThousandSplitter } from "../../helpers/number"; import { numberThousandSplitter } from "../../helpers/number";
import { ChannelParticipantsFilter, ChannelsChannelParticipants, ChannelParticipant, Chat, ChatFull, ChatParticipants, ChatPhoto, ExportedChatInvite, InputChannel, InputFile, InputFileLocation, PhotoSize, SendMessageAction, Update, UserFull, UserProfilePhoto } from "../../layer"; import { ChannelParticipantsFilter, ChannelsChannelParticipants, ChannelParticipant, Chat, ChatFull, ChatParticipants, ChatPhoto, ExportedChatInvite, InputChannel, InputFile, SendMessageAction, Update, UserFull } from "../../layer";
import { LangPackKey, i18n } from "../langPack"; import { LangPackKey, i18n } from "../langPack";
//import apiManager from '../mtproto/apiManager'; //import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker'; import apiManager from '../mtproto/mtprotoworker';
@ -20,7 +20,7 @@ import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope"; import rootScope from "../rootScope";
import SearchIndex from "../searchIndex"; import SearchIndex from "../searchIndex";
import apiUpdatesManager from "./apiUpdatesManager"; import apiUpdatesManager from "./apiUpdatesManager";
import appChatsManager, { Channel } from "./appChatsManager"; import appChatsManager from "./appChatsManager";
import appMessagesIdsManager from "./appMessagesIdsManager"; import appMessagesIdsManager from "./appMessagesIdsManager";
import appNotificationsManager from "./appNotificationsManager"; import appNotificationsManager from "./appNotificationsManager";
import appPeersManager from "./appPeersManager"; import appPeersManager from "./appPeersManager";
@ -111,7 +111,7 @@ export class AppProfileManager {
if(photo) { if(photo) {
const hasChatPhoto = photo._ !== 'chatPhotoEmpty'; const hasChatPhoto = photo._ !== 'chatPhotoEmpty';
const hasFullChatPhoto = fullChat.chat_photo?._ !== 'photoEmpty'; const hasFullChatPhoto = fullChat.chat_photo?._ !== 'photoEmpty';
if(hasChatPhoto !== hasFullChatPhoto || (photo as ChatPhoto.chatPhoto).photo_id !== fullChat.chat_photo.id) { if(hasChatPhoto !== hasFullChatPhoto || (photo as ChatPhoto.chatPhoto).photo_id !== fullChat.chat_photo?.id) {
updated = true; updated = true;
} }
} }
@ -168,10 +168,11 @@ export class AppProfileManager {
params: { params: {
id: appUsersManager.getUserInput(id) id: appUsersManager.getUserInput(id)
}, },
processResult: (userFull) => { processResult: (usersUserFull) => {
const user = userFull.user as User; appChatsManager.saveApiChats(usersUserFull.chats, true);
appUsersManager.saveApiUser(user, true); appUsersManager.saveApiUsers(usersUserFull.users);
const userFull = usersUserFull.full_user;
const peerId = id.toPeerId(false); const peerId = id.toPeerId(false);
if(userFull.profile_photo) { if(userFull.profile_photo) {
userFull.profile_photo = appPhotosManager.savePhoto(userFull.profile_photo, {type: 'profilePhoto', peerId}); userFull.profile_photo = appPhotosManager.savePhoto(userFull.profile_photo, {type: 'profilePhoto', peerId});
@ -186,7 +187,7 @@ export class AppProfileManager {
settings: userFull.notify_settings settings: userFull.notify_settings
}); });
rootScope.dispatchEvent('user_full_update', id); this.usersFull[id] = userFull;
/* if(userFull.bot_info) { /* if(userFull.bot_info) {
userFull.bot_info = this.saveBotInfo(userFull.bot_info) as any; userFull.bot_info = this.saveBotInfo(userFull.bot_info) as any;
@ -194,7 +195,8 @@ export class AppProfileManager {
//appMessagesManager.savePinnedMessage(id, userFull.pinned_msg_id); //appMessagesManager.savePinnedMessage(id, userFull.pinned_msg_id);
return this.usersFull[id] = userFull; rootScope.dispatchEvent('user_full_update', id);
return userFull;
} }
}); });
} }

6
src/lib/appManagers/appStateManager.ts

@ -176,7 +176,7 @@ const ALL_KEYS = Object.keys(STATE_INIT) as any as Array<keyof State>;
const REFRESH_KEYS = ['contactsList', 'stateCreatedTime', const REFRESH_KEYS = ['contactsList', 'stateCreatedTime',
'maxSeenMsgId', 'filters', 'topPeers'] as any as Array<keyof State>; 'maxSeenMsgId', 'filters', 'topPeers'] as any as Array<keyof State>;
export type StatePeerType = 'recentSearch' | 'topPeer' | 'dialog' | 'contact' | 'topMessage'; export type StatePeerType = 'recentSearch' | 'topPeer' | 'dialog' | 'contact' | 'topMessage' | 'self';
//const REFRESH_KEYS_WEEK = ['dialogs', 'allDialogsLoaded', 'updates', 'pinnedOrders'] as any as Array<keyof State>; //const REFRESH_KEYS_WEEK = ['dialogs', 'allDialogsLoaded', 'updates', 'pinnedOrders'] as any as Array<keyof State>;
@ -213,6 +213,10 @@ export class AppStateManager extends EventListenerBase<{
constructor() { constructor() {
super(); super();
this.loadSavedState(); this.loadSavedState();
rootScope.addEventListener('user_auth', () => {
this.requestPeerSingle(rootScope.myId, 'self');
});
} }
public loadSavedState(): Promise<State> { public loadSavedState(): Promise<State> {

18
src/lib/appManagers/appStickersManager.ts

@ -26,10 +26,12 @@ export type MyStickerSetInput = {
access_hash?: StickerSet.stickerSet['access_hash'] access_hash?: StickerSet.stickerSet['access_hash']
}; };
export type MyMessagesStickerSet = MessagesStickerSet.messagesStickerSet;
export class AppStickersManager { export class AppStickersManager {
private storage = new AppStorage<Record<Long, MessagesStickerSet>, typeof DATABASE_STATE>(DATABASE_STATE, 'stickerSets'); private storage = new AppStorage<Record<Long, MyMessagesStickerSet>, typeof DATABASE_STATE>(DATABASE_STATE, 'stickerSets');
private getStickerSetPromises: {[setId: Long]: Promise<MessagesStickerSet>} = {}; private getStickerSetPromises: {[setId: Long]: Promise<MyMessagesStickerSet>} = {};
private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<Document[]>} = {}; private getStickersByEmoticonsPromises: {[emoticon: string]: Promise<Document[]>} = {};
private greetingStickers: Document.document[]; private greetingStickers: Document.document[];
@ -41,8 +43,9 @@ export class AppStickersManager {
rootScope.addMultipleEventsListeners({ rootScope.addMultipleEventsListeners({
updateNewStickerSet: (update) => { updateNewStickerSet: (update) => {
this.saveStickerSet(update.stickerset, update.stickerset.set.id); const stickerSet = update.stickerset as MyMessagesStickerSet;
rootScope.dispatchEvent('stickers_installed', update.stickerset.set); this.saveStickerSet(stickerSet, stickerSet.set.id);
rootScope.dispatchEvent('stickers_installed', stickerSet.set);
} }
}); });
@ -92,7 +95,7 @@ export class AppStickersManager {
overwrite: boolean, overwrite: boolean,
useCache: boolean, useCache: boolean,
saveById: boolean saveById: boolean
}> = {}): Promise<MessagesStickerSet> { }> = {}): Promise<MyMessagesStickerSet> {
const id = set.id; const id = set.id;
if(this.getStickerSetPromises[id]) { if(this.getStickerSetPromises[id]) {
return this.getStickerSetPromises[id]; return this.getStickerSetPromises[id];
@ -111,8 +114,9 @@ export class AppStickersManager {
try { try {
const stickerSet = await apiManager.invokeApi('messages.getStickerSet', { const stickerSet = await apiManager.invokeApi('messages.getStickerSet', {
stickerset: this.getStickerSetInput(set) stickerset: this.getStickerSetInput(set),
}); hash: 0
}) as MyMessagesStickerSet;
const saveById = params.saveById ? id : stickerSet.set.id; const saveById = params.saveById ? id : stickerSet.set.id;
this.saveStickerSet(stickerSet, saveById); this.saveStickerSet(stickerSet, saveById);

6
src/lib/appManagers/appUsersManager.ts

@ -80,7 +80,11 @@ export class AppUsersManager {
const userId = update.user_id; const userId = update.user_id;
const user = this.users[userId]; const user = this.users[userId];
if(user) { if(user) {
this.forceUserOnline(userId); if((user.photo as UserProfilePhoto.userProfilePhoto)?.photo_id === (update.photo as UserProfilePhoto.userProfilePhoto).photo_id) {
return;
}
this.forceUserOnline(userId, update.date);
if(update.photo._ === 'userProfilePhotoEmpty') { if(update.photo._ === 'userProfilePhotoEmpty') {
delete user.photo; delete user.photo;

98
src/lib/calls/callConnectionInstanceBase.ts

@ -0,0 +1,98 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { safeAssign } from "../../helpers/object";
import { logger } from "../logger";
import createDataChannel from "./helpers/createDataChannel";
import createPeerConnection from "./helpers/createPeerConnection";
import LocalConferenceDescription from "./localConferenceDescription";
import StreamManager from "./streamManager";
import { Ssrc } from "./types";
export type CallConnectionInstanceOptions = {
streamManager: StreamManager,
connection?: RTCPeerConnection,
log?: ReturnType<typeof logger>
};
export default abstract class CallConnectionInstanceBase {
public connection: RTCPeerConnection;
public streamManager: StreamManager;
public dataChannel: RTCDataChannel;
public description: LocalConferenceDescription;
public sources: {
audio: Ssrc,
video?: Ssrc,
};
protected negotiating: Promise<void>;
protected log: ReturnType<typeof logger>;
constructor(options: CallConnectionInstanceOptions) {
safeAssign(this, options);
if(!this.log) {
this.log = this.connection?.log || logger('CALL-CONNECTION-BASE');
}
this.sources = {} as any;
}
public createPeerConnection(config?: RTCConfiguration) {
return this.connection || (this.connection = createPeerConnection(config, this.log.bindPrefix('connection')).connection);
}
public createDataChannel(dict?: RTCDataChannelInit) {
return this.dataChannel || (this.dataChannel = createDataChannel(this.connection, dict, this.log.bindPrefix('data')));
}
public createDescription() {
return this.description || (this.description = new LocalConferenceDescription(this.connection));
}
public appendStreamToConference() {
return this.streamManager.appendToConference(this.description);
}
public closeConnection() {
const {connection} = this;
if(!connection) {
return;
}
try {
connection.log('close');
connection.close();
} catch(e) {
this.log.error(e);
}
}
public closeConnectionAndStream(stopStream: boolean) {
this.closeConnection();
stopStream && this.streamManager.stop();
}
protected abstract negotiateInternal(): CallConnectionInstanceBase['negotiating'];
public negotiate() {
let promise = this.negotiating;
if(promise) {
return promise;
}
return this.negotiating = this.negotiateInternal().finally(() => {
this.negotiating = undefined;
});
}
public sendDataChannelData(data: any) {
if(this.dataChannel.readyState !== 'open') {
return;
}
this.dataChannel.send(JSON.stringify(data));
}
}

222
src/lib/calls/callInstanceBase.ts

@ -0,0 +1,222 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import EventListenerBase, { EventListenerListeners } from "../../helpers/eventListenerBase";
import noop from "../../helpers/noop";
import { logger } from "../logger";
import getAudioConstraints from "./helpers/getAudioConstraints";
import getStreamCached from "./helpers/getStreamCached";
import getVideoConstraints from "./helpers/getVideoConstraints";
import LocalConferenceDescription from "./localConferenceDescription";
import StreamManager, { StreamItem } from "./streamManager";
export type TryAddTrackOptions = {
stream: MediaStream,
track: MediaStreamTrack,
type: StreamItem['type'],
source?: string
};
export default abstract class CallInstanceBase<E extends EventListenerListeners> extends EventListenerBase<E> {
protected log: ReturnType<typeof logger>;
protected outputDeviceId: string;
protected player: HTMLElement;
protected elements: Map<string, HTMLMediaElement>;
protected audio: HTMLAudioElement;
// protected fixedSafariAudio: boolean;
protected getStream: ReturnType<typeof getStreamCached>;
constructor() {
super(false);
const player = this.player = document.createElement('div');
player.classList.add('call-player');
player.style.display = 'none';
document.body.append(player);
this.elements = new Map();
// possible Safari fix
const audio = this.audio = new Audio();
audio.autoplay = true;
audio.volume = 1.0;
this.player.append(audio);
this.elements.set('audio', audio);
this.fixSafariAudio();
this.getStream = getStreamCached();
}
public get isSharingAudio() {
return !!this.streamManager.hasInputTrackKind('audio');
}
public get isSharingVideo() {
return !!this.streamManager.hasInputTrackKind('video');
}
public abstract get isMuted(): boolean;
public abstract get isClosing(): boolean;
public fixSafariAudio() {
// if(this.fixedSafariAudio) return;
this.audio.play().catch(noop);
// this.fixedSafariAudio = true;
}
public requestAudioSource(muted: boolean) {
return this.requestInputSource(true, false, muted);
}
public requestInputSource(audio: boolean, video: boolean, muted: boolean) {
const {streamManager} = this;
if(streamManager) {
const isAudioGood = !audio || this.isSharingAudio;
const isVideoGood = !video || this.isSharingVideo;
if(isAudioGood && isVideoGood) {
return Promise.resolve();
}
}
const constraints: MediaStreamConstraints = {
audio: audio && getAudioConstraints(),
video: video && getVideoConstraints()
};
return this.getStream({
constraints,
muted
}).then(stream => {
if(stream.getVideoTracks().length) {
this.saveInputVideoStream(stream, 'main');
}
this.onInputStream(stream);
});
}
public getElement(endpoint: number | string) {
return this.elements.get('' + endpoint);
}
public abstract get streamManager(): StreamManager;
public abstract get description(): LocalConferenceDescription;
public abstract toggleMuted(): Promise<void>;
public cleanup() {
this.player.textContent = '';
this.player.remove();
this.elements.clear();
// can have no connectionInstance but streamManager with input stream
this.streamManager.stop();
super.cleanup();
}
public onTrack(event: RTCTrackEvent) {
this.tryAddTrack({
stream: event.streams[0],
track: event.track,
type: 'output'
});
}
public saveInputVideoStream(stream: MediaStream, type?: string) {
const track = stream.getVideoTracks()[0];
this.tryAddTrack({
stream,
track,
type: 'input',
source: type || 'main'
});
}
public tryAddTrack({stream, track, type, source}: TryAddTrackOptions) {
if(!source) {
source = StreamManager.getSource(stream, type);
}
this.log('tryAddTrack', stream, track, type, source);
const isOutput = type === 'output';
const {player, elements, streamManager} = this;
const tagName = track.kind as StreamItem['kind'];
const isVideo = tagName === 'video';
const elementEndpoint = isVideo ? source : tagName;
let element = elements.get(elementEndpoint);
if(isVideo) {
track.addEventListener('ended', () => {
this.log('[track] onended');
elements.delete(elementEndpoint);
// element.remove();
}, {once: true});
}
if(isOutput) {
streamManager.addTrack(stream, track, type);
}
const useStream = isVideo ? stream : streamManager.outputStream;
if(!element) {
element = document.createElement(tagName);
element.autoplay = true;
element.srcObject = useStream;
element.volume = 1.0;
if((element as any).sinkId !== 'undefined') {
const {outputDeviceId} = this;
if(outputDeviceId) {
(element as any).setSinkId(outputDeviceId);
}
}
if(!isVideo) {
player.appendChild(element);
}
// audio.play();
elements.set(elementEndpoint, element);
} else {
if(element.paused) {
element.play().catch(noop);
}
if(element.srcObject !== useStream) {
element.srcObject = useStream;
}
}
return source;
}
public setMuted(muted?: boolean) {
this.streamManager.inputStream.getAudioTracks().forEach((track) => {
if(track?.kind === 'audio') {
track.enabled = muted === undefined ? !track.enabled : !muted;
}
});
}
protected onInputStream(stream: MediaStream): void {
if(!this.isClosing) {
const {streamManager, description} = this;
streamManager.addStream(stream, 'input');
if(description) {
streamManager.appendToConference(description);
}
}
}
}

17
src/lib/calls/callState.ts

@ -0,0 +1,17 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
enum CALL_STATE {
CONNECTED,
CONNECTING,
EXCHANGING_KEYS,
PENDING,
REQUESTING,
CLOSING,
CLOSED
}
export default CALL_STATE;

371
src/lib/calls/groupCallConnectionInstance.ts

@ -0,0 +1,371 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { forEachReverse } from "../../helpers/array";
import throttle from "../../helpers/schedulers/throttle";
import { Updates, PhoneJoinGroupCall, PhoneJoinGroupCallPresentation, Update } from "../../layer";
import apiUpdatesManager from "../appManagers/apiUpdatesManager";
import appGroupCallsManager, { GroupCallConnectionType, JoinGroupCallJsonPayload } from "../appManagers/appGroupCallsManager";
import apiManager from "../mtproto/apiManager";
import rootScope from "../rootScope";
import CallConnectionInstanceBase, { CallConnectionInstanceOptions } from "./callConnectionInstanceBase";
import GroupCallInstance from "./groupCallInstance";
import filterServerCodecs from "./helpers/filterServerCodecs";
import fixLocalOffer from "./helpers/fixLocalOffer";
import processMediaSection from "./helpers/processMediaSection";
import { ConferenceEntry } from "./localConferenceDescription";
import SDP from "./sdp";
import SDPMediaSection from "./sdp/mediaSection";
import { WebRTCLineType } from "./sdpBuilder";
import { UpdateGroupCallConnectionData } from "./types";
export default class GroupCallConnectionInstance extends CallConnectionInstanceBase {
private groupCall: GroupCallInstance;
public updateConstraints?: boolean;
private type: GroupCallConnectionType;
private options: {
type: Extract<GroupCallConnectionType, 'main'>,
isMuted?: boolean,
joinVideo?: boolean,
rejoin?: boolean
} | {
type: Extract<GroupCallConnectionType, 'presentation'>,
};
private updateConstraintsInterval: number;
public negotiateThrottled: () => void;
constructor(options: CallConnectionInstanceOptions & {
groupCall: GroupCallConnectionInstance['groupCall'],
type: GroupCallConnectionInstance['type'],
options: GroupCallConnectionInstance['options'],
}) {
super(options);
this.negotiateThrottled = throttle(this.negotiate.bind(this), 0, false);
}
public createPeerConnection() {
return this.connection || super.createPeerConnection({
iceServers: [],
iceTransportPolicy: 'all',
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require',
iceCandidatePoolSize: 0,
// sdpSemantics: "unified-plan",
// extmapAllowMixed: true,
});
}
public createDataChannel() {
if(this.dataChannel) {
return this.dataChannel;
}
const dataChannel = super.createDataChannel();
dataChannel.addEventListener('open', () => {
this.maybeUpdateRemoteVideoConstraints();
});
dataChannel.addEventListener('close', () => {
if(this.updateConstraintsInterval) {
clearInterval(this.updateConstraintsInterval);
this.updateConstraintsInterval = undefined;
}
});
return dataChannel;
}
public createDescription() {
if(this.description) {
return this.description;
}
const description = super.createDescription();
/* const perType = 0;
const types = ['audio' as const, 'video' as const];
const count = types.length * perType;
const init: RTCRtpTransceiverInit = {direction: 'recvonly'};
types.forEach(type => {
for(let i = 0; i < perType; ++i) {
description.createEntry(type).createTransceiver(connection, init);
}
}); */
return description;
}
public appendStreamToConference() {
super.appendStreamToConference();/* .then(() => {
currentGroupCall.connections.main.negotiating = false;
this.startNegotiation({
type: type,
isMuted: muted,
rejoin
});
}); */
}
private async invokeJoinGroupCall(localSdp: SDP, mainChannels: SDPMediaSection[], options: GroupCallConnectionInstance['options']) {
const {groupCall, description} = this;
const groupCallId = groupCall.id;
const processedChannels = mainChannels.map(section => {
const processed = processMediaSection(localSdp, section);
this.sources[processed.entry.type as 'video' | 'audio'] = processed.entry;
return processed;
});
let promise: Promise<Updates>;
const audioChannel = processedChannels.find(channel => channel.media.mediaType === 'audio');
const videoChannel = processedChannels.find(channel => channel.media.mediaType === 'video');
let {source, params} = audioChannel || {};
const useChannel = videoChannel || audioChannel;
const channels: {[type in WebRTCLineType]?: typeof audioChannel} = {
audio: audioChannel,
video: videoChannel
};
description.entries.forEach(entry => {
if(entry.direction === 'sendonly') {
const channel = channels[entry.type];
if(!channel) return;
description.setEntrySource(entry, channel.sourceGroups || channel.source);
description.setEntryPeerId(entry, rootScope.myId);
}
});
// overwrite ssrc with audio in video params
if(params !== useChannel.params) {
const data: JoinGroupCallJsonPayload = JSON.parse(useChannel.params.data);
// data.ssrc = source || data.ssrc - 1; // audio channel can be missed in screensharing
if(source) data.ssrc = source;
else delete data.ssrc;
params = {
_: 'dataJSON',
data: JSON.stringify(data)
};
}
const groupCallInput = appGroupCallsManager.getGroupCallInput(groupCallId);
if(options.type === 'main') {
const request: PhoneJoinGroupCall = {
call: groupCallInput,
join_as: {_: 'inputPeerSelf'},
params,
muted: options.isMuted,
video_stopped: !options.joinVideo
};
promise = apiManager.invokeApi('phone.joinGroupCall', request);
this.log(`[api] joinGroupCall id=${groupCallId}`, request);
} else {
const request: PhoneJoinGroupCallPresentation = {
call: groupCallInput,
params,
};
promise = apiManager.invokeApi('phone.joinGroupCallPresentation', request);
this.log(`[api] joinGroupCallPresentation id=${groupCallId}`, request);
}
const updates = await promise;
apiUpdatesManager.processUpdateMessage(updates);
const update = (updates as Updates.updates).updates.find(update => update._ === 'updateGroupCallConnection') as Update.updateGroupCallConnection;
const data: UpdateGroupCallConnectionData = JSON.parse(update.params.data);
data.audio = data.audio || groupCall.connections.main.description.audio;
description.setData(data);
filterServerCodecs(mainChannels, data);
return data;
}
protected async negotiateInternal() {
const {connection, description} = this;
const isNewConnection = connection.iceConnectionState === 'new' && !description.getEntryByMid('0').source;
const log = this.log.bindPrefix('startNegotiation');
log('start');
const originalOffer = await connection.createOffer({iceRestart: false});
if(isNewConnection && this.dataChannel) {
const dataChannelEntry = description.createEntry('application');
dataChannelEntry.setDirection('sendrecv');
}
const {sdp: localSdp, offer} = fixLocalOffer({
offer: originalOffer,
data: description
});
log('[sdp] setLocalDescription', offer.sdp);
await connection.setLocalDescription(offer);
const mainChannels = localSdp.media.filter(media => {
return media.mediaType !== 'application' && media.isSending;
});
if(isNewConnection) {
try {
await this.invokeJoinGroupCall(localSdp, mainChannels, this.options);
} catch(e) {
this.log.error('[tdweb] joinGroupCall error', e);
}
}
/* if(!data) {
log('abort 0');
this.closeConnectionAndStream(connection, streamManager);
return;
} */
/* if(connection.iceConnectionState !== 'new') {
log(`abort 1 connectionState=${connection.iceConnectionState}`);
this.closeConnectionAndStream(connection, streamManager);
return;
} */
/* if(this.currentGroupCall !== currentGroupCall || connectionHandler.connection !== connection) {
log('abort', this.currentGroupCall, currentGroupCall);
this.closeConnectionAndStream(connection, streamManager);
return;
} */
const isAnswer = true;
// const _bundleMids = bundleMids.slice();
const entriesToDelete: ConferenceEntry[] = [];
const bundle = localSdp.bundle;
forEachReverse(bundle, (mid, idx, arr) => {
const entry = description.getEntryByMid(mid);
if(entry.shouldBeSkipped(isAnswer)) {
arr.splice(idx, 1);
entriesToDelete.push(entry);
}
});
/* forEachReverse(description.entries, (entry, idx, arr) => {
const mediaSection = _parsedSdp.media.find(section => section.oa.get('mid').oa === entry.mid);
const deleted = !mediaSection;
// const deleted = !_bundleMids.includes(entry.mid); // ! can't use it because certain mid can be missed in bundle
if(deleted) {
arr.splice(idx, 1);
}
}); */
const entries = localSdp.media.map((section) => {
const mid = section.mid;
let entry = description.getEntryByMid(mid);
if(!entry) {
entry = new ConferenceEntry(mid, section.mediaType);
entry.setDirection('inactive');
}
return entry;
});
const answerDescription: RTCSessionDescriptionInit = {
type: 'answer',
sdp: description.generateSdp({
bundle,
entries,
isAnswer
})
};
entriesToDelete.forEach(entry => {
description.deleteEntry(entry);
});
log(`[sdp] setRemoteDescription signaling=${connection.signalingState} ice=${connection.iceConnectionState} gathering=${connection.iceGatheringState} connection=${connection.connectionState}`, answerDescription.sdp);
await connection.setRemoteDescription(answerDescription);
log('end');
}
public negotiate() {
let promise = this.negotiating;
if(promise) {
return promise;
}
promise = super.negotiate();
if(this.updateConstraints) {
promise.then(() => {
this.maybeUpdateRemoteVideoConstraints();
this.updateConstraints = false;
});
}
return promise;
}
public maybeUpdateRemoteVideoConstraints() {
if(this.dataChannel.readyState !== 'open') {
return;
}
this.log('maybeUpdateRemoteVideoConstraints');
// * https://github.com/TelegramMessenger/tgcalls/blob/6f2746e04c9b040f8c8dfc64d916a1853d09c4ce/tgcalls/group/GroupInstanceCustomImpl.cpp#L2549
type VideoConstraints = {minHeight?: number, maxHeight: number};
const obj: {
colibriClass: 'ReceiverVideoConstraints',
constraints: {[endpoint: string]: VideoConstraints},
defaultConstraints: VideoConstraints,
onStageEndpoints: string[]
} = {
colibriClass: 'ReceiverVideoConstraints',
constraints: {},
defaultConstraints: {maxHeight: 0},
onStageEndpoints: []
};
for(const entry of this.description.entries) {
if(entry.direction !== 'recvonly' || entry.type !== 'video') {
continue;
}
const {endpoint} = entry;
obj.onStageEndpoints.push(endpoint);
obj.constraints[endpoint] = {
minHeight: 180,
maxHeight: 720
};
}
this.sendDataChannelData(obj);
if(!obj.onStageEndpoints.length) {
if(this.updateConstraintsInterval) {
clearInterval(this.updateConstraintsInterval);
this.updateConstraintsInterval = undefined;
}
} else if(!this.updateConstraintsInterval) {
this.updateConstraintsInterval = window.setInterval(this.maybeUpdateRemoteVideoConstraints.bind(this), 5000);
}
}
public addInputVideoStream(stream: MediaStream) {
// const {sources} = this;
// if(sources?.video) {
// const source = this.sources.video.source;
// stream.source = '' + source;
this.groupCall.saveInputVideoStream(stream, this.type);
// }
this.streamManager.addStream(stream, 'input');
this.appendStreamToConference(); // replace sender track
}
}

492
src/lib/calls/groupCallInstance.ts

@ -0,0 +1,492 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { IS_SAFARI } from "../../environment/userAgent";
import { indexOfAndSplice } from "../../helpers/array";
import { safeAssign } from "../../helpers/object";
import throttle from "../../helpers/schedulers/throttle";
import { GroupCall, GroupCallParticipant, Updates } from "../../layer";
import apiUpdatesManager from "../appManagers/apiUpdatesManager";
import appGroupCallsManager, { GroupCallConnectionType, GroupCallId, GroupCallOutputSource } from "../appManagers/appGroupCallsManager";
import appPeersManager from "../appManagers/appPeersManager";
import { logger } from "../logger";
import apiManager from "../mtproto/apiManager";
import { NULL_PEER_ID } from "../mtproto/mtproto_config";
import rootScope from "../rootScope";
import CallInstanceBase, { TryAddTrackOptions } from "./callInstanceBase";
import GroupCallConnectionInstance from "./groupCallConnectionInstance";
import GROUP_CALL_STATE from "./groupCallState";
import getScreenConstraints from "./helpers/getScreenConstraints";
import getScreenStream from "./helpers/getScreenStream";
import getStream from "./helpers/getStream";
import getVideoConstraints from "./helpers/getVideoConstraints";
import stopTrack from "./helpers/stopTrack";
import localConferenceDescription from "./localConferenceDescription";
import { WebRTCLineType } from "./sdpBuilder";
import StreamManager from "./streamManager";
import { Ssrc } from "./types";
export default class GroupCallInstance extends CallInstanceBase<{
state: (state: GROUP_CALL_STATE) => void,
pinned: (source?: GroupCallOutputSource) => void,
}> {
public id: GroupCallId;
public chatId: ChatId;
public handleUpdateGroupCallParticipants: boolean;
public updatingSdp: boolean;
public isSpeakingMap: Map<any, any>;
public connections: {[k in GroupCallConnectionType]?: GroupCallConnectionInstance};
public groupCall: GroupCall;
public participant: GroupCallParticipant;
// will be set with negotiation
public joined: boolean;
private pinnedSources: Array<GroupCallOutputSource>;
private participantsSsrcs: Map<PeerId, Ssrc[]>;
private hadAutoPinnedSources: Set<GroupCallOutputSource>;
private dispatchPinnedThrottled: () => void;
private startVideoSharingPromise: Promise<void>;
private startScreenSharingPromise: Promise<void>;
constructor(options: {
id: GroupCallInstance['id'],
chatId: GroupCallInstance['chatId'],
isSpeakingMap?: GroupCallInstance['isSpeakingMap'],
connections?: GroupCallInstance['connections']
}) {
super();
safeAssign(this, options);
if(!this.log) {
this.log = logger('GROUP-CALL');
}
if(!this.connections) {
this.connections = {};
}
if(!this.isSpeakingMap) {
this.isSpeakingMap = new Map();
}
this.pinnedSources = [];
this.participantsSsrcs = new Map();
this.hadAutoPinnedSources = new Set();
this.dispatchPinnedThrottled = throttle(() => {
this.dispatchEvent('pinned', this.pinnedSource);
}, 0, false);
this.addEventListener('state', (state) => {
if(state === GROUP_CALL_STATE.CLOSED) {
this.cleanup();
}
});
}
get connectionState() {
return this.connections.main.connection.iceConnectionState;
}
get state() {
const {connectionState} = this;
if(connectionState === 'closed') {
return GROUP_CALL_STATE.CLOSED;
} else if(connectionState !== 'connected' && (!IS_SAFARI || connectionState !== 'completed')) {
return GROUP_CALL_STATE.CONNECTING;
} else {
const {participant} = this;
if(!participant.pFlags.can_self_unmute) {
return GROUP_CALL_STATE.MUTED_BY_ADMIN;
} else if(participant.pFlags.muted) {
return GROUP_CALL_STATE.MUTED;
} else {
return GROUP_CALL_STATE.UNMUTED;
}
}
}
get participants() {
return appGroupCallsManager.getCachedParticipants(this.id);
}
get isSharingScreen() {
return !!this.connections.presentation;
}
get pinnedSource() {
return this.pinnedSources[this.pinnedSources.length - 1];
}
public get isMuted() {
return this.state !== GROUP_CALL_STATE.UNMUTED;
}
public get isClosing() {
const {state} = this;
return state === GROUP_CALL_STATE.CLOSED;
}
public get streamManager(): StreamManager {
return this.connections.main.streamManager;
}
public get description(): localConferenceDescription {
return this.connections.main.description;
}
public pinSource(source: GroupCallOutputSource) {
indexOfAndSplice(this.pinnedSources, source);
this.pinnedSources.push(source);
this.dispatchPinnedThrottled();
}
public unpinSource(source: GroupCallOutputSource) {
this.hadAutoPinnedSources.delete(source);
indexOfAndSplice(this.pinnedSources, source);
this.dispatchPinnedThrottled();
}
public unpinAll() {
this.pinnedSources.length = 0;
this.dispatchPinnedThrottled();
}
public getParticipantByPeerId(peerId: PeerId) {
return NULL_PEER_ID === peerId ? this.participant : this.participants.get(peerId);
}
public toggleMuted() {
return this.requestAudioSource(true).then(() => appGroupCallsManager.toggleMuted());
}
public getElement(endpoint: GroupCallOutputSource) {
return super.getElement(endpoint);
}
public getVideoElementFromParticipantByType(participant: GroupCallParticipant, type: 'video' | 'presentation') {
let source: GroupCallOutputSource;
if(participant.pFlags.self) {
const connectionType: GroupCallConnectionType = type === 'video' ? 'main' : 'presentation';
source = connectionType;
} else {
const codec = participant[type];
source = codec.source_groups[0].sources[0];
}
const element = this.getElement(source) as HTMLVideoElement;
if(!element) return;
const clone = element.cloneNode() as typeof element;
clone.srcObject = element.srcObject;
clone.setAttribute('playsinline', 'true');
clone.muted = true;
return {video: clone, source};
}
public createConnectionInstance(options: {
streamManager: StreamManager,
type: GroupCallConnectionType,
options: GroupCallConnectionInstance['options'],
}) {
return this.connections[options.type] = new GroupCallConnectionInstance({
groupCall: this,
log: this.log.bindPrefix(options.type),
...options
});
}
public changeRaiseHand(raise: boolean) {
return appGroupCallsManager.editParticipant(this.id, this.participant, {raiseHand: raise});
}
public async startScreenSharingInternal() {
try {
const type: GroupCallConnectionType = 'presentation';
const stream = await getScreenStream(getScreenConstraints());
const streamManager = new StreamManager();
const connectionInstance = this.createConnectionInstance({
streamManager,
type,
options: {
type
}
});
const connection = connectionInstance.createPeerConnection();
connection.addEventListener('negotiationneeded', () => {
connectionInstance.negotiate();
});
stream.getVideoTracks()[0].addEventListener('ended', () => {
if(this.connections.presentation) { // maybe user has stopped screensharing through browser's ui
this.stopScreenSharing();
}
}, {once: true});
connectionInstance.createDescription();
connectionInstance.addInputVideoStream(stream);
} catch(err) {
this.log.error('start screen sharing error', err);
}
}
public startScreenSharing() {
return this.startScreenSharingPromise || (this.startScreenSharingPromise = this.startScreenSharingInternal().finally(() => {
this.startScreenSharingPromise = undefined;
}));
}
public stopScreenSharing() {
const connectionInstance = this.connections.presentation;
if(!connectionInstance) {
return Promise.resolve();
}
delete this.connections.presentation;
this.unpinSource('presentation');
connectionInstance.closeConnectionAndStream(true);
delete this.participant.presentation;
appGroupCallsManager.saveApiParticipant(this.id, this.participant);
return apiManager.invokeApi('phone.leaveGroupCallPresentation', {
call: appGroupCallsManager.getGroupCallInput(this.id)
}).then(updates => {
apiUpdatesManager.processUpdateMessage(updates);
});
}
public toggleScreenSharing() {
if(this.isSharingScreen) {
return this.stopScreenSharing();
} else {
return this.startScreenSharing();
}
}
public async startVideoSharingInternal() {
const constraints: MediaStreamConstraints = {
video: getVideoConstraints()
};
try {
const stream = await getStream(constraints, false);
const connectionInstance = this.connections.main;
connectionInstance.addInputVideoStream(stream);
await appGroupCallsManager.editParticipant(this.id, this.participant, {
videoPaused: false,
videoStopped: false
});
} catch(err) {
this.log.error('startVideoSharing error', err, constraints);
}
}
public startVideoSharing() {
return this.startVideoSharingPromise || (this.startVideoSharingPromise = this.startVideoSharingInternal().finally(() => {
this.startVideoSharingPromise = undefined;
}));
}
public async stopVideoSharing() {
const connectionInstance = this.connections.main;
const track = connectionInstance.streamManager.inputStream.getVideoTracks()[0];
if(!track) {
return;
}
stopTrack(track);
connectionInstance.streamManager.appendToConference(connectionInstance.description); // clear sender track
await appGroupCallsManager.editParticipant(this.id, this.participant, {
videoStopped: true
});
}
public toggleVideoSharing() {
if(this.isSharingVideo) {
return this.stopVideoSharing();
} else {
return this.startVideoSharing();
}
}
public async hangUp(discard = false, rejoin = false, isDiscarded = false) {
for(const type in this.connections) {
const connection = this.connections[type as GroupCallConnectionType];
connection.closeConnectionAndStream(!rejoin);
}
this.dispatchEvent('state', this.state);
if(isDiscarded) {
return;
}
if(!rejoin) {
let promise: Promise<Updates>;
const groupCallInput = appGroupCallsManager.getGroupCallInput(this.id);
if(discard) {
this.log(`[api] discardGroupCall id=${this.id}`);
promise = apiManager.invokeApi('phone.discardGroupCall', {
call: groupCallInput
});
} else if(this.joined) {
this.log(`[api] leaveGroupCall id=${this.id}`);
const connectionInstance = this.connections.main;
promise = apiManager.invokeApi('phone.leaveGroupCall', {
call: groupCallInput,
source: connectionInstance.sources.audio.source
});
} else {
this.log(`[api] id=${this.id} payload=null`);
promise = apiManager.invokeApi('phone.joinGroupCall', {
call: groupCallInput,
join_as: {_: 'inputPeerSelf'},
muted: true,
video_stopped: true,
params: {
_: 'dataJSON',
data: ''
}
});
}
const updates = await promise;
apiUpdatesManager.processUpdateMessage(updates);
}
}
public tryAddTrack(options: Omit<TryAddTrackOptions, 'streamManager'>) {
const {description} = this;
const source = super.tryAddTrack(options);
if(options.type === 'output') {
const entry = description.getEntryBySource(+source);
const participant = this.participants.get(entry.peerId);
if(participant) {
rootScope.dispatchEvent('group_call_participant', {groupCallId: this.id, participant});
}
}
return source;
}
public onParticipantUpdate(participant: GroupCallParticipant, doNotDispatchParticipantUpdate?: PeerId) {
const connectionInstance = this.connections.main;
const {connection, description} = connectionInstance;
const peerId = appPeersManager.getPeerId(participant.peer);
const hasLeft = !!participant.pFlags.left;
const oldSsrcs = this.participantsSsrcs.get(peerId) || [];
if(participant.presentation && !hasLeft) {
const {source} = appGroupCallsManager.makeSsrcFromParticipant(participant, 'video', participant.presentation.source_groups, participant.presentation.endpoint);
if(!this.hadAutoPinnedSources.has(source)) {
this.hadAutoPinnedSources.add(source);
this.pinSource(participant.pFlags.self ? 'presentation' : source);
}
}
if(participant.pFlags.self) {
this.participant = participant;
if(connectionInstance.sources.audio.source !== participant.source) {
this.hangUp();
}
let mute = false;
if(!participant.pFlags.can_self_unmute) {
this.stopScreenSharing();
this.stopVideoSharing();
mute = true;
} else if(participant.pFlags.muted) {
mute = true;
}
if(mute) {
this.setMuted(true);
}
if(doNotDispatchParticipantUpdate !== peerId) {
this.dispatchEvent('state', this.state);
}
return;
}
const ssrcs = hasLeft ? [] : appGroupCallsManager.makeSsrcsFromParticipant(participant);
if(!hasLeft) {
this.participantsSsrcs.set(peerId, ssrcs);
} else {
this.participantsSsrcs.delete(peerId);
}
// const TEST_OLD = false;
const modifiedTypes: Set<WebRTCLineType> = new Set();
oldSsrcs.forEach(oldSsrc => {
const oldSource = oldSsrc.source;
const newSsrc = ssrcs.find(ssrc => ssrc.source === oldSource);
if(!newSsrc) {
this.unpinSource(oldSource);
const oldEntry = description.getEntryBySource(oldSource);
if(oldEntry && oldEntry.direction !== 'inactive') {
oldEntry.setDirection('inactive');
modifiedTypes.add(oldEntry.type);
}
}
});
ssrcs.forEach(ssrc => {
let entry = description.getEntryBySource(ssrc.source);
if(entry) {
if(entry.direction === 'inactive') {
entry.setDirection(entry.originalDirection);
modifiedTypes.add(entry.type);
}
return;
}
entry = description.createEntry(ssrc.type);
description.setEntrySource(entry, ssrc.sourceGroups || ssrc.source);
description.setEntryPeerId(entry, peerId);
// if(TEST_OLD) {
// description.bundleMids.push(entry.mid);
// entry.setDirection('recvonly');
// } else {
ssrc.type === 'video' && entry.setEndpoint(ssrc.endpoint);
entry.createTransceiver(connection, {direction: 'recvonly'});
// }
modifiedTypes.add(entry.type);
});
/* if(TEST_OLD) {
this.setRemoteOffer({
connection,
description,
ssrcs
});
} else */if(modifiedTypes.size) {
if(modifiedTypes.has('video')) {
connectionInstance.updateConstraints = true;
}
connectionInstance.negotiateThrottled();
}
}
}

31
src/lib/calls/helpers/createDataChannel.ts

@ -0,0 +1,31 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { Logger, logger } from "../../logger";
export default function createDataChannel(connection: RTCPeerConnection, dict?: RTCDataChannelInit, log?: Logger) {
// return;
if(!log) {
log = logger('RTCDataChannel');
}
const channel = connection.createDataChannel('data', dict);
channel.addEventListener('message', (e) => {
log('onmessage', e);
});
channel.addEventListener('open', () => {
log('onopen');
});
channel.addEventListener('close', () => {
log('onclose');
});
channel.log = log;
return channel;
}

30
src/lib/calls/helpers/createMainStreamManager.ts

@ -0,0 +1,30 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { GROUP_CALL_AMPLITUDE_ANALYSE_INTERVAL_MS } from "../constants";
import StreamManager from "../streamManager";
import getAudioConstraints from "./getAudioConstraints";
import getStream from "./getStream";
import getVideoConstraints from "./getVideoConstraints";
export default async function createMainStreamManager(muted?: boolean, joinVideo?: boolean) {
const constraints: MediaStreamConstraints = {
audio: getAudioConstraints(),
video: joinVideo && getVideoConstraints()
};
const streamManager = new StreamManager(GROUP_CALL_AMPLITUDE_ANALYSE_INTERVAL_MS);
try {
const stream = await getStream(constraints, muted);
streamManager.addStream(stream, 'input');
} catch(err) {
console.error('joinGroupCall getStream error', err, constraints);
streamManager.inputStream = new MediaStream();
}
return streamManager;
}

43
src/lib/calls/helpers/createPeerConnection.ts

@ -0,0 +1,43 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { Logger, logger } from "../../logger";
export default function createPeerConnection(config: RTCConfiguration, log?: Logger) {
if(!log) {
log = logger('RTCPeerConnection');
}
log('constructor');
// @ts-ignore
const connection = new RTCPeerConnection(config);
connection.addEventListener('track', (event) => {
log('ontrack', event);
});
connection.addEventListener('signalingstatechange', () => {
log('onsignalingstatechange', connection.signalingState);
});
connection.addEventListener('connectionstatechange', () => {
log('onconnectionstatechange', connection.connectionState);
});
connection.addEventListener('negotiationneeded', () => { // * will be fired every time input device changes
log('onnegotiationneeded', connection.signalingState);
});
connection.addEventListener('icecandidate', (event) => {
log('onicecandidate', event);
});
connection.addEventListener('iceconnectionstatechange', () => {
log('oniceconnectionstatechange', connection.iceConnectionState);
});
connection.addEventListener('datachannel', () => {
log('ondatachannel');
});
connection.log = log;
return {connection};
}

42
src/lib/calls/helpers/filterServerCodecs.ts

@ -0,0 +1,42 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { forEachReverse } from "../../../helpers/array";
import SDPMediaSection from "../sdp/mediaSection";
import { UpdateGroupCallConnectionData, Codec } from "../types";
export default function filterServerCodecs(mainChannels: SDPMediaSection[], data: UpdateGroupCallConnectionData) {
// ! Need to filter server's extmap for Firefox
const performExtmap = (channel: typeof mainChannels[0]) => {
const out: {[id: string]: string} = {};
const extmap = channel.attributes.get('extmap');
extmap.forEach((extmap) => {
const id = extmap.key.split('/', 1)[0];
out[id] = extmap.value;
});
return out;
};
const codecsToPerform: [Codec, 'audio' | 'video'][] = /* flatten([data, dataPresentation].filter(Boolean).map(data => {
return */['audio' as const, 'video' as const].filter(type => data[type]).map(type => ([data[type], type]));
// }));
codecsToPerform.forEach(([codec, type]) => {
const channel = mainChannels.find(line => line.mediaType === type);
if(!channel) {
return;
}
const extmap = performExtmap(channel);
forEachReverse(codec["rtp-hdrexts"], (value, index, arr) => {
if(extmap[value.id] !== value.uri) {
arr.splice(index, 1);
console.log(`[sdp] filtered extmap:`, value, index, type);
}
});
});
}

101
src/lib/calls/helpers/fixLocalOffer.ts

@ -0,0 +1,101 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { forEachReverse } from "../../../helpers/array";
import { copy } from "../../../helpers/object";
import { ConferenceEntry } from "../localConferenceDescription";
import { parseSdp, addSimulcast } from "../sdp/utils";
import { generateMediaFirstLine, SDPBuilder } from "../sdpBuilder";
import { UpdateGroupCallConnectionData } from "../types";
import parseMediaSectionInfo from "./parseMediaSectionInfo";
export default function fixLocalOffer(options: {
offer: RTCSessionDescriptionInit,
data: UpdateGroupCallConnectionData,
skipAddingMulticast?: boolean
// mids?: string[]
}) {
const {offer, data} = options;
const sdp = parseSdp(offer.sdp);
let hasMunged = false;
if(!options.skipAddingMulticast) {
hasMunged = addSimulcast(sdp) || hasMunged;
}
// const bundleLine = parsedSdp.session.lines.find(line => line.Ha?.key === 'group');
// const bundleMids = bundleLine.value.split(' ').slice(1);
forEachReverse(sdp.media, (section, idx, arr) => {
// const mid = section.oa.get('mid').oa;
// это может случиться при выключении и включении видео. почему-то появится секция уже удалённая
// ! нельзя тут модифицировать локальное описание, будет критовать
/* if(mids && !mids.includes(mid) && !bundleMids.includes(mid)) {
console.error('wtf');
hasMunged = true;
arr.splice(idx, 1);
return;
} */
if(/* section.mediaType !== 'video' || */section.isSending) {
return;
}
if(section.mediaType === 'application') {
return;
}
const mediaLine = section.mediaLine;
const mediaLineParts = mediaLine.mediaLineParts;
const mediaCodecIds = mediaLineParts.ids;
const localMLine = mediaLine.toString();
const codec = data[section.mediaType];
const payloadTypes = codec['payload-types'];
/* forEachReverse(payloadTypes, (payloadType, idx, arr) => {
if(!mediaCodecIds.includes('' + payloadType.id) && section.mediaType === 'video') {
// if(payloadType.name === 'H265') {
console.warn('[sdp] filtered unsupported codec', payloadType, mediaCodecIds, section.mediaType);
arr.splice(idx, 1);
}
}); */
const codecIds = payloadTypes.map(payload => '' + payload.id);
const correctMLine = generateMediaFirstLine(section.mediaType, undefined, codecIds);
if(localMLine !== correctMLine) {
const sectionInfo = parseMediaSectionInfo(sdp, section);
let newData = {...data};
newData.transport = copy(newData.transport);
newData.transport.ufrag = sectionInfo.ufrag;
newData.transport.pwd = sectionInfo.pwd;
newData.transport.fingerprints = [sectionInfo.fingerprint];
newData.transport.candidates = [];
const entry = new ConferenceEntry(sectionInfo.mid, mediaLineParts.type);
entry.setPort(mediaLineParts.port);
sectionInfo.source && entry.setSource(sectionInfo.sourceGroups || sectionInfo.source);
entry.setDirection(section.direction);
const newSdp = new SDPBuilder().addSsrcEntry(entry, newData).finalize();
const newChannel = parseSdp(newSdp).media[0];
arr[idx] = newChannel;
hasMunged = true;
}
});
if(hasMunged) {
const mungedSdp = sdp.toString();
offer.sdp = mungedSdp;
}
return {offer, sdp/* , bundleMids */};
}

22
src/lib/calls/helpers/getAudioConstraints.ts

@ -0,0 +1,22 @@
import constraintSupported, { MyMediaTrackSupportedConstraints } from "../../../environment/constraintSupport";
export default function getAudioConstraints(): MediaTrackConstraints {
const constraints: MediaTrackConstraints = {
channelCount: 2
};
const desirable: (keyof MyMediaTrackSupportedConstraints)[] = [
'noiseSuppression',
'echoCancellation',
'autoGainControl'
];
desirable.forEach(constraint => {
if(constraintSupported(constraint)) {
// @ts-ignore
constraints[constraint] = true;
}
});
return constraints;
}

12
src/lib/calls/helpers/getScreenConstraints.ts

@ -0,0 +1,12 @@
export default function getScreenConstraints(): DisplayMediaStreamConstraints {
return {
video: {
// @ts-ignore
// cursor: 'always',
width: {max: 1920},
height: {max: 1080},
frameRate: {max: 30}
},
audio: true
};
}

4
src/lib/calls/helpers/getScreenStream.ts

@ -0,0 +1,4 @@
export default async function getScreenStream(constraints: DisplayMediaStreamConstraints) {
const screenStream = await navigator.mediaDevices.getDisplayMedia(constraints);
return screenStream;
}

20
src/lib/calls/helpers/getStream.ts

@ -0,0 +1,20 @@
export default async function getStream(constraints: MediaStreamConstraints, muted: boolean) {
// console.log('getStream', constraints);
const stream = await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach(x => {
/* x.onmute = x => {
console.log('track.onmute', x);
};
x.onunmute = x => {
console.log('track.onunmute', x);
}; */
x.enabled = !muted;
});
// console.log('getStream result', stream);
return stream;
}
(window as any).getStream = getStream;

65
src/lib/calls/helpers/getStreamCached.ts

@ -0,0 +1,65 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import getScreenStream from "./getScreenStream";
import getStream from "./getStream";
/**
* ! Use multiple constraints together only with first invoke
*/
export default function getStreamCached() {
const _cache: {
main: Partial<{
audio: Promise<MediaStream>,
video: Promise<MediaStream>
}>,
screen: Partial<{
audio: Promise<MediaStream>,
video: Promise<MediaStream>
}>
} = {
main: {},
screen: {}
};
return async(options: {
isScreen: true,
constraints: DisplayMediaStreamConstraints,
} | {
isScreen?: false,
constraints: MediaStreamConstraints,
muted: boolean
}) => {
const {isScreen, constraints} = options;
const cache = _cache[isScreen ? 'screen' : 'main'];
let promise: Promise<MediaStream> = cache[constraints.audio ? 'audio' : 'video'];
if(!promise) {
promise = (isScreen ? getScreenStream : getStream)(constraints, (options as any).muted);
if(constraints.audio && !cache.audio) cache.audio = promise.finally(() => cache.audio = undefined);
if(constraints.video && !cache.video) cache.video = promise.finally(() => cache.video = undefined);
}
try {
return await promise;
/* let out: Partial<{
audio: MediaStream,
video: MediaStream
}> = {};
await Promise.all([
constraints.audio && cache.audio.then(stream => out.audio = stream),
constraints.video && cache.video.then(stream => out.video = stream)
].filter(Boolean));
return out; */
} catch(err) {
throw err;
}
};
}
(window as any).getStreamCached = getStreamCached;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save