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. 570
      src/components/chat/bubbles.ts
  11. 15
      src/components/chat/chat.ts
  12. 30
      src/components/chat/contextMenu.ts
  13. 2
      src/components/chat/inlineHelper.ts
  14. 42
      src/components/chat/input.ts
  15. 101
      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. 21
      src/components/groupCall/description.ts
  20. 162
      src/components/groupCall/index.ts
  21. 54
      src/components/groupCall/microphoneIconMini.ts
  22. 30
      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. 17
      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. 35
      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. 2
      src/environment/ctx.ts
  64. 2
      src/environment/emojiSupport.ts
  65. 4
      src/environment/parallaxSupport.ts
  66. 2
      src/environment/touchSupport.ts
  67. 2
      src/environment/webpSupport.ts
  68. 60
      src/helpers/audioAssetPlayer.ts
  69. 28
      src/helpers/dom/attachListNavigation.ts
  70. 14
      src/helpers/dom/getVisibleRect.ts
  71. 23
      src/helpers/eachMinute.ts
  72. 31
      src/helpers/eachTimeout.ts
  73. 6
      src/helpers/eventListenerBase.ts
  74. 116
      src/helpers/fastSmoothScroll.ts
  75. 2
      src/helpers/formatPhoneNumber.ts
  76. 4
      src/helpers/middleware.ts
  77. 84
      src/helpers/movablePanel.ts
  78. 53
      src/lang.ts
  79. 655
      src/layer.d.ts
  80. 13
      src/lib/appManagers/appChatsManager.ts
  81. 75
      src/lib/appManagers/appDialogsManager.ts
  82. 1331
      src/lib/appManagers/appGroupCallsManager.ts
  83. 134
      src/lib/appManagers/appImManager.ts
  84. 141
      src/lib/appManagers/appMessagesManager.ts
  85. 14
      src/lib/appManagers/appPeersManager.ts
  86. 20
      src/lib/appManagers/appProfileManager.ts
  87. 6
      src/lib/appManagers/appStateManager.ts
  88. 18
      src/lib/appManagers/appStickersManager.ts
  89. 6
      src/lib/appManagers/appUsersManager.ts
  90. 98
      src/lib/calls/callConnectionInstanceBase.ts
  91. 222
      src/lib/calls/callInstanceBase.ts
  92. 17
      src/lib/calls/callState.ts
  93. 371
      src/lib/calls/groupCallConnectionInstance.ts
  94. 492
      src/lib/calls/groupCallInstance.ts
  95. 31
      src/lib/calls/helpers/createDataChannel.ts
  96. 30
      src/lib/calls/helpers/createMainStreamManager.ts
  97. 43
      src/lib/calls/helpers/createPeerConnection.ts
  98. 42
      src/lib/calls/helpers/filterServerCodecs.ts
  99. 101
      src/lib/calls/helpers/fixLocalOffer.ts
  100. 22
      src/lib/calls/helpers/getAudioConstraints.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 = { @@ -31,8 +31,10 @@ type AppMediaViewerTargetType = {
peerId: PeerId
};
export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> {
protected btnMenuDelete: HTMLElement;
protected listLoader: SearchListLoader<AppMediaViewerTargetType>;
protected btnMenuForward: ButtonMenuItemOptions;
protected btnMenuDownload: ButtonMenuItemOptions;
protected btnMenuDelete: ButtonMenuItemOptions;
get searchContext() {
return this.listLoader.searchContext;
@ -98,22 +100,21 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet @@ -98,22 +100,21 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
attachClickEvent(this.buttons.delete, this.onDeleteClick);
const buttons: ButtonMenuItemOptions[] = [{
const buttons: ButtonMenuItemOptions[] = [this.btnMenuForward = {
icon: 'forward',
text: 'Forward',
onClick: this.onForwardClick
}, {
}, this.btnMenuDownload = {
icon: 'download',
text: 'MediaViewer.Context.Download',
onClick: this.onDownloadClick
}, {
}, this.btnMenuDelete = {
icon: 'delete danger',
text: 'Delete',
onClick: this.onDeleteClick
}];
this.setBtnMenuToggle(buttons);
this.btnMenuDelete = buttons[buttons.length - 1].element;
// * constructing html end
@ -256,10 +257,20 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet @@ -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 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);
[this.buttons.delete, this.btnMenuDelete].forEach(button => {
[this.buttons.delete, this.btnMenuDelete.element].forEach(button => {
button.classList.toggle('hide', !canDeleteMessage);
});

8
src/components/appMediaViewerBase.ts

@ -610,7 +610,7 @@ export default class AppMediaViewerBase< @@ -610,7 +610,7 @@ export default class AppMediaViewerBase<
let needOpacity = false;
if(target !== this.content.media && !target.classList.contains('profile-avatars-avatar')) {
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)) {
target = this.content.media;
@ -1410,6 +1410,12 @@ export default class AppMediaViewerBase< @@ -1410,6 +1410,12 @@ export default class AppMediaViewerBase<
video.parentElement.classList.add('is-buffering');
}
});
if(this.wholeDiv.classList.contains('no-forwards')) {
video.addEventListener('contextmenu', (e) => {
cancelEvent(e);
});
}
attachCanPlay();
}

13
src/components/appSearchSuper..ts

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

40
src/components/appSelectPeers.ts

@ -24,6 +24,7 @@ import { filterUnique, indexOfAndSplice } from "../helpers/array"; @@ -24,6 +24,7 @@ import { filterUnique, indexOfAndSplice } from "../helpers/array";
import debounce from "../helpers/schedulers/debounce";
import windowSize from "../helpers/windowSize";
import appPeersManager, { IsPeerType } from "../lib/appManagers/appPeersManager";
import { generateDelimiter, SettingSection } from "./sidebarLeft";
type SelectSearchPeerType = 'contacts' | 'dialogs' | 'channelParticipants';
@ -35,11 +36,11 @@ export default class AppSelectPeers { @@ -35,11 +36,11 @@ export default class AppSelectPeers {
handheldsSize: 66,
avatarSize: 48
} */);
public chatsContainer = document.createElement('div');
private chatsContainer = document.createElement('div');
public scrollable: Scrollable;
public selectedScrollable: Scrollable;
private selectedScrollable: Scrollable;
public selectedContainer: HTMLElement;
private selectedContainer: HTMLElement;
public input: HTMLInputElement;
//public selected: {[peerId: PeerId]: HTMLElement} = {};
@ -77,6 +78,8 @@ export default class AppSelectPeers { @@ -77,6 +78,8 @@ export default class AppSelectPeers {
private selfPresence: LangPackKey = 'Presence.YourChat';
private needSwitchList = false;
private sectionNameLangPackKey: LangPackKey;
constructor(options: {
appendTo: AppSelectPeers['appendTo'],
@ -92,7 +95,8 @@ export default class AppSelectPeers { @@ -92,7 +95,8 @@ export default class AppSelectPeers {
placeholder?: AppSelectPeers['placeholder'],
selfPresence?: AppSelectPeers['selfPresence'],
exceptSelf?: AppSelectPeers['exceptSelf'],
filterPeerTypeBy?: AppSelectPeers['filterPeerTypeBy']
filterPeerTypeBy?: AppSelectPeers['filterPeerTypeBy'],
sectionNameLangPackKey?: AppSelectPeers['sectionNameLangPackKey']
}) {
safeAssign(this, options);
@ -139,6 +143,8 @@ export default class AppSelectPeers { @@ -139,6 +143,8 @@ export default class AppSelectPeers {
this.input.type = 'text';
if(this.multiSelect) {
const section = new SettingSection({});
section.innerContainer.classList.add('selector-search-section');
let topContainer = document.createElement('div');
topContainer.classList.add('selector-search-container');
@ -149,7 +155,7 @@ export default class AppSelectPeers { @@ -149,7 +155,7 @@ export default class AppSelectPeers {
topContainer.append(this.selectedContainer);
this.selectedScrollable = new Scrollable(topContainer);
let delimiter = document.createElement('hr');
// let delimiter = document.createElement('hr');
this.selectedContainer.addEventListener('click', (e) => {
if(this.freezed) return;
@ -167,11 +173,18 @@ export default class AppSelectPeers { @@ -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.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.setVirtualContainer(this.list);
@ -208,6 +221,8 @@ export default class AppSelectPeers { @@ -208,6 +221,8 @@ export default class AppSelectPeers {
this.getMoreResults();
};
this.scrollable.container.prepend(generateDelimiter());
this.container.append(this.chatsContainer);
this.appendTo.append(this.container);
@ -565,7 +580,10 @@ export default class AppSelectPeers { @@ -565,7 +580,10 @@ export default class AppSelectPeers {
this.onChange && this.onChange(this.selected.size);
if(scroll) {
this.selectedScrollable.scrollIntoViewNew(this.input, 'center');
this.selectedScrollable.scrollIntoViewNew({
element: this.input,
position: 'center'
});
}
return div;
@ -602,7 +620,11 @@ export default class AppSelectPeers { @@ -602,7 +620,11 @@ export default class AppSelectPeers {
});
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 @@ @@ -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 @@ @@ -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 @@ @@ -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<{ @@ -21,6 +21,7 @@ export default class AutocompleteHelper extends EventListenerBase<{
protected container: HTMLElement;
protected list: HTMLElement;
protected resetTarget: () => void;
protected attach: () => void;
protected detach: () => void;
protected init?(): void;
@ -52,13 +53,21 @@ export default class AutocompleteHelper extends EventListenerBase<{ @@ -52,13 +53,21 @@ export default class AutocompleteHelper extends EventListenerBase<{
this.controller.addHelper(this);
}
public toggleListNavigation(enabled: boolean) {
if(enabled) {
this.attach && this.attach();
} else {
this.detach && this.detach();
}
}
protected onVisible = () => {
if(this.detach) { // it can be so because 'visible' calls before animation's end
this.detach();
}
const list = this.list;
const {detach, resetTarget} = attachListNavigation({
const {attach, detach, resetTarget} = attachListNavigation({
list,
type: this.listType,
onSelect: this.onSelect,
@ -66,6 +75,7 @@ export default class AutocompleteHelper extends EventListenerBase<{ @@ -66,6 +75,7 @@ export default class AutocompleteHelper extends EventListenerBase<{
waitForKey: this.waitForKey
});
this.attach = attach;
this.detach = detach;
this.resetTarget = resetTarget;
if(!IS_MOBILE && !this.navigationItem) {

6
src/components/chat/autocompleteHelperController.ts

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

570
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<{ @@ -57,7 +57,7 @@ export default class Chat extends EventListenerBase<{
public contextMenu: ChatContextMenu;
public search: ChatSearch;
public wasAlreadyUsed = false;
public wasAlreadyUsed: boolean;
// public initPeerId = 0;
public peerId: PeerId;
public threadId: number;
@ -66,11 +66,12 @@ export default class Chat extends EventListenerBase<{ @@ -66,11 +66,12 @@ export default class Chat extends EventListenerBase<{
public log: ReturnType<typeof logger>;
public type: ChatType = 'chat';
public type: ChatType;
public noAutoDownloadMedia: boolean;
public noForwards: boolean;
public inited = false;
public inited: boolean;
constructor(public appImManager: AppImManager,
public appChatsManager: AppChatsManager,
@ -95,6 +96,8 @@ export default class Chat extends EventListenerBase<{ @@ -95,6 +96,8 @@ export default class Chat extends EventListenerBase<{
) {
super();
this.type = 'chat';
this.container = document.createElement('div');
this.container.classList.add('chat', 'tabs-tab');
@ -177,7 +180,7 @@ export default class Chat extends EventListenerBase<{ @@ -177,7 +180,7 @@ export default class Chat extends EventListenerBase<{
// this.initPeerId = peerId;
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.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);
@ -254,7 +257,7 @@ export default class Chat extends EventListenerBase<{ @@ -254,7 +257,7 @@ export default class Chat extends EventListenerBase<{
public setPeer(peerId: PeerId, lastMsgId?: number) {
if(!peerId) {
this.inited = false;
this.inited = undefined;
} else if(!this.inited) {
if(this.init) {
this.init(/* peerId */);
@ -268,6 +271,8 @@ export default class Chat extends EventListenerBase<{ @@ -268,6 +271,8 @@ export default class Chat extends EventListenerBase<{
if(!samePeer) {
rootScope.dispatchEvent('peer_changing', this);
this.peerId = peerId;
this.noForwards = this.appPeersManager.noForwards(peerId);
this.container.classList.toggle('no-forwards', this.noForwards);
} else if(this.setPeerPromise) {
return;
}

30
src/components/chat/contextMenu.ts

@ -24,8 +24,9 @@ import findUpClassName from "../../helpers/dom/findUpClassName"; @@ -24,8 +24,9 @@ import findUpClassName from "../../helpers/dom/findUpClassName";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
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 assumeType from "../../helpers/assumeType";
export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true})[];
@ -40,7 +41,8 @@ export default class ChatContextMenu { @@ -40,7 +41,8 @@ export default class ChatContextMenu {
private isUsernameTarget: boolean;
private peerId: PeerId;
private mid: number;
private message: any;
private message: Message.message | Message.messageService;
private noForwards: boolean;
constructor(private attachTo: HTMLElement,
private chat: Chat,
@ -109,6 +111,7 @@ export default class ChatContextMenu { @@ -109,6 +111,7 @@ export default class ChatContextMenu {
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
this.message = this.chat.getMessage(this.mid);
this.noForwards = !this.appMessagesManager.canForward(this.message);
this.buttons.forEach(button => {
let good: boolean;
@ -176,6 +179,7 @@ export default class ChatContextMenu { @@ -176,6 +179,7 @@ export default class ChatContextMenu {
text: 'MessageScheduleEditTime',
onClick: () => {
this.chat.input.scheduleSending(() => {
assumeType<Message.message>(this.message);
this.appMessagesManager.editMessage(this.message, this.message.message, {
scheduleDate: this.chat.input.scheduleDate,
entities: this.message.entities
@ -203,18 +207,18 @@ export default class ChatContextMenu { @@ -203,18 +207,18 @@ export default class ChatContextMenu {
icon: 'copy',
text: 'Copy',
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',
text: 'Chat.CopySelectedText',
onClick: this.onCopyClick,
verify: () => !!this.message.message && this.isTextSelected
verify: () => !this.noForwards && !!(this.message as Message.message).message && this.isTextSelected
}, {
icon: 'copy',
text: 'Message.Context.Selection.Copy',
onClick: this.onCopyClick,
verify: () => {
if(!this.isSelected) {
if(!this.isSelected || this.noForwards) {
return false;
}
@ -270,19 +274,19 @@ export default class ChatContextMenu { @@ -270,19 +274,19 @@ export default class ChatContextMenu {
icon: 'unpin',
text: 'Message.Context.Unpin',
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',
text: 'MediaViewer.Context.Download',
onClick: () => {
this.appDocsManager.saveDocFile(this.message.media.document);
this.appDocsManager.saveDocFile((this.message as any).media.document);
},
verify: () => {
if(this.message.pFlags.is_outgoing) {
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;
let hasTarget = !!IS_TOUCH_SUPPORTED;
@ -295,7 +299,7 @@ export default class ChatContextMenu { @@ -295,7 +299,7 @@ export default class ChatContextMenu {
text: 'Chat.Poll.Unvote',
onClick: this.onRetractVote,
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;
}/* ,
cancelEvent: true */
@ -304,7 +308,7 @@ export default class ChatContextMenu { @@ -304,7 +308,7 @@ export default class ChatContextMenu {
text: 'Chat.Poll.Stop',
onClick: this.onStopPoll,
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;
}/* ,
cancelEvent: true */
@ -312,7 +316,7 @@ export default class ChatContextMenu { @@ -312,7 +316,7 @@ export default class ChatContextMenu {
icon: 'forward',
text: 'Forward',
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',
text: 'Message.Context.Selection.Forward',
@ -335,7 +339,7 @@ export default class ChatContextMenu { @@ -335,7 +339,7 @@ export default class ChatContextMenu {
icon: 'select',
text: 'Message.Context.Select',
onClick: this.onSelectClick,
verify: () => !this.message.action && !this.isSelected && this.isSelectable,
verify: () => !(this.message as Message.messageService).action && !this.isSelected && this.isSelectable,
notDirect: () => true,
withSelection: true
}, {
@ -470,4 +474,4 @@ export default class ChatContextMenu { @@ -470,4 +474,4 @@ export default class ChatContextMenu {
new PopupDeleteMessages(this.peerId, this.isTargetAGroupedItem ? [this.mid] : this.chat.getMidsByMid(this.mid), this.chat.type);
}
};
}
}

2
src/components/chat/inlineHelper.ts

@ -46,7 +46,9 @@ export default class InlineHelper extends AutocompleteHelper { @@ -46,7 +46,9 @@ export default class InlineHelper extends AutocompleteHelper {
appendTo,
controller,
listType: 'xy',
waitForKey: 'ArrowUp',
onSelect: (target) => {
if(!target) return false; // can happen when there is only button
const {peerId, botId, queryId} = this.list.dataset;
return this.chat.input.getReadyToSend(() => {
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 { @@ -198,6 +198,8 @@ export default class ChatInput {
private previousQuery: string;
private releaseMediaPlayback: () => void;
botStartBtn: HTMLButtonElement;
fakeBotStartBtn: HTMLElement;
constructor(private chat: Chat,
private appMessagesManager: AppMessagesManager,
@ -636,6 +638,14 @@ export default class ChatInput { @@ -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') {
this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId, mids}) => {
if(this.chat.peerId === peerId && mids.includes(this.editMsgId)) {
@ -765,6 +775,31 @@ export default class ChatInput { @@ -765,6 +775,31 @@ export default class ChatInput {
attachClickEvent(this.replyElements.container, this.onHelperClick, {listenerSetter: this.listenerSetter});
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() {
@ -1030,10 +1065,7 @@ export default class ChatInput { @@ -1030,10 +1065,7 @@ export default class ChatInput {
key = 'Message';
}
if(i.key !== key) {
i.key = key;
i.update();
}
i.compareAndUpdate({key});
}
const visible = this.attachMenuButtons.filter(button => {
@ -1539,7 +1571,7 @@ export default class ChatInput { @@ -1539,7 +1571,7 @@ export default class ChatInput {
entities = RichTextProcessor.mergeEntities(entities, RichTextProcessor.parseEntities(_value));
}
value = value.substr(0, caretPos);
value = value.slice(0, caretPos);
if(this.previousQuery === value) {
return;

101
src/components/chat/messageRender.ts

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

53
src/components/chat/selection.ts

@ -332,16 +332,11 @@ class AppSelection { @@ -332,16 +332,11 @@ class AppSelection {
for(const mid of mids) {
const message = this.appMessagesManager.getMessageFromStorage(storage, mid);
if(!cantForward) {
if(message.action) {
cantForward = true;
}
cantForward = !this.appMessagesManager.canForward(message);
}
if(!cantDelete) {
const canDelete = this.appMessagesManager.canDeleteMessage(message);
if(!canDelete) {
cantDelete = true;
}
cantDelete = !this.appMessagesManager.canDeleteMessage(message);
}
if(cantForward && cantDelete) break;
@ -666,6 +661,8 @@ export default class ChatSelection extends AppSelection { @@ -666,6 +661,8 @@ export default class ChatSelection extends AppSelection {
public selectionSendNowBtn: HTMLElement;
public selectionForwardBtn: HTMLElement;
public selectionDeleteBtn: HTMLElement;
selectionLeft: HTMLDivElement;
selectionRight: HTMLDivElement;
constructor(private chat: Chat, private bubbles: ChatBubbles, private input: ChatInput, appMessagesManager: AppMessagesManager) {
super({
@ -822,8 +819,8 @@ export default class ChatSelection extends AppSelection { @@ -822,8 +819,8 @@ export default class ChatSelection extends AppSelection {
}
protected onToggleSelection = (forwards: boolean) => {
let transform = '', borderRadius = '';
if(forwards) {
let transform = '', borderRadius = '', needTranslateX: number;
// if(forwards) {
const p = this.input.rowsWrapper.parentElement;
const fakeSelectionWrapper = p.querySelector('.fake-selection-wrapper');
const fakeRowsWrapper = p.querySelector('.fake-rows-wrapper');
@ -835,16 +832,19 @@ export default class ChatSelection extends AppSelection { @@ -835,16 +832,19 @@ export default class ChatSelection extends AppSelection {
if(widthFrom !== widthTo) {
const scale = (widthTo/* - 8 */) / widthFrom;
const initTranslateX = (widthFrom - widthTo) / 2;
const needTranslateX = fakeSelectionRect.left - fakeRowsRect.left - initTranslateX;
transform = `translateX(${needTranslateX}px) scaleX(${scale})`;
needTranslateX = fakeSelectionRect.left - fakeRowsRect.left - initTranslateX;
if(scale < 1) {
const br = 12;
borderRadius = '' + (br + br * (1 - scale)) + 'px';
if(forwards) {
transform = `translateX(${needTranslateX}px) scaleX(${scale})`;
if(scale < 1) {
const br = 12;
borderRadius = '' + (br + br * (1 - scale)) + 'px';
}
}
//scale = widthTo / widthFrom;
}
}
// }
SetTransition(this.input.rowsWrapper, 'is-centering', forwards, 200);
this.input.rowsWrapper.style.transform = transform;
@ -857,6 +857,8 @@ export default class ChatSelection extends AppSelection { @@ -857,6 +857,8 @@ export default class ChatSelection extends AppSelection {
this.selectionSendNowBtn =
this.selectionForwardBtn =
this.selectionDeleteBtn =
this.selectionLeft =
this.selectionRight =
null;
this.selectedText = undefined;
}
@ -914,13 +916,21 @@ export default class ChatSelection extends AppSelection { @@ -914,13 +916,21 @@ export default class ChatSelection extends AppSelection {
});
}, attachClickOptions);
this.selectionContainer.append(...[
btnCancel,
this.selectionCountEl,
const left = this.selectionLeft = document.createElement('div');
left.classList.add('selection-container-left');
left.append(btnCancel, this.selectionCountEl);
const right = this.selectionRight = document.createElement('div');
right.classList.add('selection-container-right');
right.append(...[
this.selectionSendNowBtn,
this.selectionForwardBtn,
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.append(this.selectionContainer);
@ -928,7 +938,12 @@ export default class ChatSelection extends AppSelection { @@ -928,7 +938,12 @@ export default class ChatSelection extends AppSelection {
void this.selectionInputWrapper.offsetLeft; // reflow
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"; @@ -47,6 +47,8 @@ import AppEditContactTab from "../sidebarRight/tabs/editContact";
import appMediaPlaybackController from "../appMediaPlaybackController";
import { NULL_PEER_ID } from "../../lib/mtproto/mtproto_config";
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};
@ -60,6 +62,7 @@ export default class ChatTopbar { @@ -60,6 +62,7 @@ export default class ChatTopbar {
private chatUtils: HTMLDivElement;
private btnJoin: HTMLButtonElement;
private btnPinned: HTMLButtonElement;
private btnCall: HTMLButtonElement;
private btnGroupCall: HTMLButtonElement;
private btnMute: HTMLButtonElement;
private btnSearch: HTMLButtonElement;
@ -156,12 +159,14 @@ export default class ChatTopbar { @@ -156,12 +159,14 @@ export default class ChatTopbar {
this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null,
this.btnJoin,
this.btnPinned,
this.btnCall,
this.btnGroupCall,
this.btnMute,
this.btnSearch,
this.btnMore
].filter(Boolean));
this.pushButtonToVerify(this.btnCall, this.verifyCallButton.bind(this, 'voice'));
this.pushButtonToVerify(this.btnGroupCall, this.verifyVideoChatButton);
this.chatInfoContainer.append(this.btnBack, this.chatInfo, this.chatUtils);
@ -290,6 +295,14 @@ export default class ChatTopbar { @@ -290,6 +295,14 @@ export default class ChatTopbar {
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() {
this.menuButtons = [{
icon: 'search',
@ -332,6 +345,16 @@ export default class ChatTopbar { @@ -332,6 +345,16 @@ export default class ChatTopbar {
const chatFull = this.appProfileManager.getCachedFullChat(this.peerId.toChatId());
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',
text: 'PeerInfo.Action.LiveStream',
@ -487,6 +510,10 @@ export default class ChatTopbar { @@ -487,6 +510,10 @@ export default class ChatTopbar {
}, {listenerSetter: this.listenerSetter});
}
private onCallClick(type: CallType) {
this.chat.appImManager.callUser(this.peerId.toUserId(), type);
}
private onJoinGroupCallClick = () => {
this.chat.appImManager.joinGroupCall(this.peerId);
};
@ -503,10 +530,12 @@ export default class ChatTopbar { @@ -503,10 +530,12 @@ export default class ChatTopbar {
this.pinnedMessage = new ChatPinnedMessage(this, this.chat, this.appMessagesManager, this.appPeersManager);
this.btnJoin = Button('btn-primary btn-color-primary chat-join hide');
this.btnCall = ButtonIcon('phone');
this.btnGroupCall = ButtonIcon('videochat');
this.btnPinned = ButtonIcon('pinlist');
this.btnMute = ButtonIcon('mute');
this.attachClickEvent(this.btnCall, this.onCallClick.bind(this, 'voice'));
this.attachClickEvent(this.btnGroupCall, this.onJoinGroupCallClick);
this.attachClickEvent(this.btnPinned, () => {
@ -660,7 +689,7 @@ export default class ChatTopbar { @@ -660,7 +689,7 @@ export default class ChatTopbar {
if(this.btnJoin) {
if(this.appPeersManager.isAnyChat(peerId)) {
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);
} else {
this.btnJoin.classList.add('hide');

5
src/components/confirmationPopup.ts

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

21
src/components/groupCall/description.ts

@ -4,9 +4,8 @@ @@ -4,9 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { deepEqual } from "../../helpers/object";
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 I18n, { LangPackKey, FormatterArguments } from "../../lib/langPack";
@ -19,13 +18,15 @@ export default class GroupCallDescriptionElement { @@ -19,13 +18,15 @@ export default class GroupCallDescriptionElement {
});
this.descriptionIntl.element.classList.add('group-call-description');
}
appendTo.append(this.descriptionIntl.element);
public detach() {
this.descriptionIntl.element.remove();
}
public update(instance: GroupCallInstance) {
const {state} = instance;
let key: LangPackKey, args: FormatterArguments;
if(state === GROUP_CALL_STATE.CONNECTING) {
key = 'VoiceChat.Status.Connecting';
@ -35,11 +36,13 @@ export default class GroupCallDescriptionElement { @@ -35,11 +36,13 @@ export default class GroupCallDescriptionElement {
}
const {descriptionIntl} = this;
if(descriptionIntl.key !== key || !deepEqual(descriptionIntl.args, args)) {
descriptionIntl.key = key;
descriptionIntl.args = args;
descriptionIntl.update();
descriptionIntl.compareAndUpdate({
key,
args
});
if(!this.descriptionIntl.element.parentElement) {
this.appendTo.append(this.descriptionIntl.element);
}
}
}

162
src/components/groupCall/index.ts

@ -11,10 +11,9 @@ import customProperties from "../../helpers/dom/customProperties"; @@ -11,10 +11,9 @@ import customProperties from "../../helpers/dom/customProperties";
import { safeAssign } from "../../helpers/object";
import { GroupCall, GroupCallParticipant } from "../../layer";
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 GROUP_CALL_STATE from "../../lib/calls/groupCallState";
import { LangPackKey } from "../../lib/langPack";
import { RLottieColor } from "../../lib/rlottie/rlottiePlayer";
import rootScope from "../../lib/rootScope";
import ButtonIcon from "../buttonIcon";
@ -26,16 +25,16 @@ import GroupCallDescriptionElement from "./description"; @@ -26,16 +25,16 @@ import GroupCallDescriptionElement from "./description";
import GroupCallTitleElement from "./title";
import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../../helpers/dom/fullScreen";
import Scrollable from "../scrollable";
import MovableElement, { MovableState } from "../movableElement";
import { MovableState } from "../movableElement";
import animationIntersector from "../animationIntersector";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import { IS_APPLE_MOBILE } from "../../environment/userAgent";
import mediaSizes, { ScreenSize } from "../../helpers/mediaSizes";
import toggleDisability from "../../helpers/dom/toggleDisability";
import { ripple } from "../ripple";
import throttle from "../../helpers/schedulers/throttle";
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 {
UNMUTED,
@ -118,32 +117,6 @@ let previousState: MovableState = { @@ -118,32 +117,6 @@ let previousState: MovableState = {
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 {
private appGroupCallsManager: AppGroupCallsManager;
private appPeersManager: AppPeersManager;
@ -160,7 +133,7 @@ export default class PopupGroupCall extends PopupElement { @@ -160,7 +133,7 @@ export default class PopupGroupCall extends PopupElement {
private btnExitFullScreen: HTMLButtonElement;
private btnInvite: HTMLButtonElement;
private btnShowColumn: HTMLButtonElement;
private movable: MovableElement;
private movablePanel: MovablePanel;
private buttonsContainer: HTMLDivElement;
private btnFullScreen2: HTMLButtonElement;
private btnVideo: HTMLDivElement;
@ -202,7 +175,6 @@ export default class PopupGroupCall extends PopupElement { @@ -202,7 +175,6 @@ export default class PopupGroupCall extends PopupElement {
const btnInvite = this.btnInvite = ButtonIcon('adduser');
const btnShowColumn = this.btnShowColumn = ButtonIcon('rightpanel ' + className + '-only-big');
this.toggleMovable(!IS_TOUCH_SUPPORTED);
attachClickEvent(btnShowColumn, this.toggleRightColumn, {listenerSetter});
@ -259,45 +231,54 @@ export default class PopupGroupCall extends PopupElement { @@ -259,45 +231,54 @@ export default class PopupGroupCall extends PopupElement {
...options
});
listenerSetter.add(rootScope)('group_call_state', (instance) => {
if(this.instance === instance) {
this.updateInstance();
}
this.movablePanel = new MovablePanel({
listenerSetter,
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) => {
if(this.instance.id === groupCall.id) {
if(this.instance?.id === groupCall.id) {
this.updateInstance();
}
});
listenerSetter.add(rootScope)('group_call_pinned', ({instance}) => {
if(this.instance === instance) {
this.setHasPinned();
}
listenerSetter.add(instance)('pinned', () => {
this.setHasPinned();
});
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', () => {
const {movable} = this;
if(movable) {
previousState = movable.state;
}
const {movablePanel} = this;
previousState = movablePanel.state;
this.groupCallParticipantsVideo.destroy();
this.groupCallParticipants.destroy();
this.groupCallMicrophoneIcon.destroy();
if(movable) {
movable.destroy();
}
movablePanel.destroy();
});
this.toggleRightColumn();
@ -310,21 +291,20 @@ export default class PopupGroupCall extends PopupElement { @@ -310,21 +291,20 @@ export default class PopupGroupCall extends PopupElement {
const buttons = this.buttonsContainer = document.createElement('div');
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({
text: 'VoiceChat.Video.Stream.Video',
callback: this.onVideoClick
// text: 'VoiceChat.Video.Stream.Video',
callback: this.onVideoClick,
icon: 'videocamera_filled'
});
btnVideo.classList.add('tgico-videocamera_filled');
const btnScreen = this.btnScreen = _makeButton({
text: 'VoiceChat.Video.Stream.Screencast',
callback: this.onScreenClick
// text: 'VoiceChat.Video.Stream.Screencast',
callback: this.onScreenClick,
icon: 'sharescreen_filled'
});
btnScreen.classList.add('tgico-sharescreen_filled');
btnScreen.classList.toggle('hide', !IS_SCREEN_SHARING_SUPPORTED);
const btnMute = _makeButton({
@ -337,20 +317,20 @@ export default class PopupGroupCall extends PopupElement { @@ -337,20 +317,20 @@ export default class PopupGroupCall extends PopupElement {
btnMute.append(microphoneIcon.container);
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);
const btnLeave = _makeButton({
text: 'VoiceChat.Leave',
// text: 'VoiceChat.Leave',
isDanger: true,
callback: this.onLeaveClick
callback: this.onLeaveClick,
icon: 'close'
});
btnLeave.classList.add('tgico-close');
buttons.append(btnVideo, btnScreen, btnMute, btnMore, btnLeave);
this.container.append(buttons);
@ -419,36 +399,6 @@ export default class PopupGroupCall extends PopupElement { @@ -419,36 +399,6 @@ export default class PopupGroupCall extends PopupElement {
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 = () => {
this.toggleBigLayout();
const isFull = isFullScreen();
@ -470,7 +420,8 @@ export default class PopupGroupCall extends PopupElement { @@ -470,7 +420,8 @@ export default class PopupGroupCall extends PopupElement {
private toggleBigLayout = () => {
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) {
cancelFullScreen();
@ -519,11 +470,16 @@ export default class PopupGroupCall extends PopupElement { @@ -519,11 +470,16 @@ export default class PopupGroupCall extends PopupElement {
return;
}
const {participant, groupCall} = this.instance;
if(!participant) {
return;
}
this.setTitle();
this.setDescription();
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.groupCallMicrophoneIcon.setState(microphoneButtonState);
}

54
src/components/groupCall/microphoneIconMini.ts

@ -0,0 +1,54 @@ @@ -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'
}]
});
}
}

30
src/components/groupCall/participantVideo.ts

@ -4,15 +4,16 @@ @@ -4,15 +4,16 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { animate } from "../../helpers/animation";
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 { i18n } from "../../lib/langPack";
import PeerTitle from "../peerTitle";
import { getGroupCallParticipantMutedState } from ".";
import GroupCallParticipantMutedIcon from "./participantMutedIcon";
import GroupCallParticipantStatusElement from "./participantStatus";
import GroupCallInstance from "../../lib/calls/groupCallInstance";
import callVideoCanvasBlur from "../call/videoCanvasBlur";
const className = 'group-call-participant-video';
@ -92,33 +93,14 @@ export default class GroupCallParticipantVideoElement { @@ -92,33 +93,14 @@ export default class GroupCallParticipantVideoElement {
this.right.append(this.groupCallParticipantMutedIcon.container);
const className = 'group-call-participant-video';
video.classList.add(className);
video.classList.add(className, 'call-video');
if(video.paused) {
video.play();
}
const canvas = document.createElement('canvas');
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();
}
const canvas = callVideoCanvasBlur(video);
canvas.classList.add(className + '-blur');
this.container.prepend(canvas, video);

15
src/components/groupCall/participantVideos.ts

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

3
src/components/groupCall/participants.ts

@ -14,8 +14,9 @@ import { safeAssign } from "../../helpers/object"; @@ -14,8 +14,9 @@ import { safeAssign } from "../../helpers/object";
import ScrollableLoader from "../../helpers/scrollableLoader";
import { GroupCallParticipant } from "../../layer";
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 GroupCallInstance from "../../lib/calls/groupCallInstance";
import rootScope from "../../lib/rootScope";
import ButtonMenu, { ButtonMenuItemOptions } from "../buttonMenu";
import confirmationPopup from "../confirmationPopup";

2
src/components/groupCall/participantsList.ts

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

17
src/components/groupCall/title.ts

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
import setInnerHTML from "../../helpers/dom/setInnerHTML";
import { GroupCall } from "../../layer";
import { GroupCallInstance } from "../../lib/appManagers/appGroupCallsManager";
import GroupCallInstance from "../../lib/calls/groupCallInstance";
import RichTextProcessor from "../../lib/richtextprocessor";
import PeerTitle from "../peerTitle";
@ -23,10 +23,15 @@ export default class GroupCallTitleElement { @@ -23,10 +23,15 @@ export default class GroupCallTitleElement {
const peerId = instance.chatId.toPeerId(true);
if(groupCall.title) {
setInnerHTML(appendTo, RichTextProcessor.wrapEmojiText(groupCall.title));
} else if(peerTitle.peerId !== peerId) {
peerTitle.peerId = peerId;
peerTitle.update();
appendTo.append(peerTitle.element);
}
} else {
if(peerTitle.peerId !== peerId) {
peerTitle.peerId = peerId;
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? @@ -37,7 +37,13 @@ export function horizontalMenu(tabs: HTMLElement, content: HTMLElement, onClick?
}
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) {

20
src/components/movableElement.ts

@ -24,12 +24,20 @@ export type MovableState = { @@ -24,12 +24,20 @@ export type MovableState = {
const className = 'movable-element';
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<{
resize: () => void
}> {
private minWidth: number;
private minHeight: number;
private element: HTMLElement;
private verifyTouchTarget: (e: TouchEvent | MouseEvent) => boolean;
private top: number;
private left: number;
@ -39,11 +47,7 @@ export default class MovableElement extends EventListenerBase<{ @@ -39,11 +47,7 @@ export default class MovableElement extends EventListenerBase<{
private swipeHandler: SwipeHandler;
private handlers: HTMLElement[];
constructor(options: {
minWidth: MovableElement['minWidth'],
minHeight: MovableElement['minHeight'],
element: MovableElement['element']
}) {
constructor(options: MovableElementOptions) {
super(true);
safeAssign(this, options);
@ -133,11 +137,7 @@ export default class MovableElement extends EventListenerBase<{ @@ -133,11 +137,7 @@ export default class MovableElement extends EventListenerBase<{
},
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()) {
if(this.verifyTouchTarget && !this.verifyTouchTarget(e)) {
return false;
}

14
src/components/peerProfile.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* 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 replaceContent from "../helpers/dom/replaceContent";
import { fastRaf } from "../helpers/schedulers";
@ -56,7 +56,7 @@ export default class PeerProfile { @@ -56,7 +56,7 @@ export default class PeerProfile {
private threadId: number;
constructor(public scrollable: Scrollable) {
if(!PARALLAX_SUPPORTED) {
if(!IS_PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('no-parallax');
}
}
@ -130,7 +130,11 @@ export default class PeerProfile { @@ -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.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) => {
if(!e.isTrusted) {
@ -216,7 +220,7 @@ export default class PeerProfile { @@ -216,7 +220,7 @@ export default class PeerProfile {
if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container);
else this.element.prepend(this.avatars.container);
if(PARALLAX_SUPPORTED) {
if(IS_PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('parallax');
}
@ -224,7 +228,7 @@ export default class PeerProfile { @@ -224,7 +228,7 @@ export default class PeerProfile {
}
}
if(PARALLAX_SUPPORTED) {
if(IS_PARALLAX_SUPPORTED) {
this.scrollable.container.classList.remove('parallax');
}

11
src/components/peerProfileAvatars.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* 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 { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent";
@ -26,8 +26,8 @@ const LOAD_NEAREST = 3; @@ -26,8 +26,8 @@ const LOAD_NEAREST = 3;
export default class PeerProfileAvatars {
private static BASE_CLASS = 'profile-avatars';
private static SCALE = PARALLAX_SUPPORTED ? 2 : 1;
private static TRANSLATE_TEMPLATE = PARALLAX_SUPPORTED ? `translate3d({x}, 0, -1px) scale(${PeerProfileAvatars.SCALE})` : 'translate({x}, 0)';
private static SCALE = IS_PARALLAX_SUPPORTED ? 2 : 1;
private static TRANSLATE_TEMPLATE = IS_PARALLAX_SUPPORTED ? `translate3d({x}, 0, -1px) scale(${PeerProfileAvatars.SCALE})` : 'translate({x}, 0)';
public container: HTMLElement;
public avatars: HTMLElement;
public gradient: HTMLElement;
@ -74,7 +74,10 @@ export default class PeerProfileAvatars { @@ -74,7 +74,10 @@ export default class PeerProfileAvatars {
const checkScrollTop = () => {
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;
}

4
src/components/poll.ts

@ -595,9 +595,9 @@ export default class PollElement extends HTMLElement { @@ -595,9 +595,9 @@ export default class PollElement extends HTMLElement {
* 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);"`;
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;
}

10
src/components/popups/createContact.ts

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

5
src/components/popups/createPoll.ts

@ -380,7 +380,10 @@ export default class PopupCreatePoll extends PopupElement { @@ -380,7 +380,10 @@ export default class PopupCreatePoll extends PopupElement {
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.optionInputFields.push(questionField);

3
src/components/popups/index.ts

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

22
src/components/ripple.ts

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

18
src/components/scrollable.ts

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
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 { cancelEvent } from "../helpers/dom/cancelEvent";
/*
@ -99,18 +99,12 @@ export class ScrollableBase { @@ -99,18 +99,12 @@ export class ScrollableBase {
this.container.append(element);
}
public scrollIntoViewNew(
element: HTMLElement,
position: ScrollLogicalPosition,
margin?: number,
maxDistance?: number,
forceDirection?: FocusDirection,
forceDuration?: number,
axis?: 'x' | 'y',
getNormalSize?: ScrollGetNormalSizeCallback
) {
public scrollIntoViewNew(options: Omit<ScrollOptions, 'container'>) {
//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"; @@ -25,7 +25,7 @@ import AppNewChannelTab from "./tabs/newChannel";
import AppContactsTab from "./tabs/contacts";
import AppArchivedTab from "./tabs/archivedTab";
import AppAddMembersTab from "./tabs/addMembers";
import { i18n_, LangPackKey } from "../../lib/langPack";
import { FormatterArguments, i18n_, LangPackKey } from "../../lib/langPack";
import { ButtonMenuItemOptions } from "../buttonMenu";
import CheckboxField from "../checkboxField";
import { IS_MOBILE_SAFARI } from "../../environment/userAgent";
@ -602,54 +602,68 @@ export class AppSidebarLeft extends SidebarSlider { @@ -602,54 +602,68 @@ export class AppSidebarLeft extends SidebarSlider {
}
}
const className = 'sidebar-left-section';
export class SettingSection {
public container: HTMLElement;
public innerContainer: HTMLElement;
public content: HTMLElement;
public title: HTMLElement;
public caption: HTMLElement;
constructor(options: {
name?: LangPackKey,
nameArgs?: FormatterArguments,
caption?: LangPackKey | true,
noDelimiter?: boolean,
fakeGradientDelimiter?: boolean
}) {
this.container = document.createElement('div');
this.container.classList.add('sidebar-left-section');
fakeGradientDelimiter?: boolean,
noShadow?: boolean
} = {}) {
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) {
this.container.append(generateDelimiter());
this.container.classList.add('with-fake-delimiter');
innerContainer.append(generateDelimiter());
innerContainer.classList.add('with-fake-delimiter');
} else if(!options.noDelimiter) {
const hr = document.createElement('hr');
this.container.append(hr);
innerContainer.append(hr);
} 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) {
this.title = document.createElement('div');
this.title.classList.add('sidebar-left-h2', 'sidebar-left-section-name');
i18n_({element: this.title, key: options.name});
this.content.append(this.title);
const title = this.title = document.createElement('div');
title.classList.add('sidebar-left-h2', className + '-name');
i18n_({element: title, key: options.name, args: options.nameArgs});
content.append(title);
}
container.append(innerContainer);
if(options.caption) {
this.caption = this.generateContentElement();
this.caption.classList.add('sidebar-left-section-caption');
const caption = this.caption = this.generateContentElement();
caption.classList.add(className + '-caption');
container.append(caption);
if(options.caption !== true) {
i18n_({element: this.caption, key: options.caption});
i18n_({element: caption, key: options.caption});
}
}
}
public generateContentElement() {
const content = document.createElement('div');
content.classList.add('sidebar-left-section-content');
this.container.append(content);
content.classList.add(className + '-content');
this.innerContainer.append(content);
return content;
}
}

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

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

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

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

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

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

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

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

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

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

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

@ -82,15 +82,18 @@ export default class AppEditFolderTab extends SliderSuperTab { @@ -82,15 +82,18 @@ export default class AppEditFolderTab extends SliderSuperTab {
this.header.append(this.confirmBtn, this.menuBtn);
const inputSection = new SettingSection({});
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper');
this.nameInputField = new InputField({
label: 'FilterNameInputLabel',
label: 'FilterNameHint',
maxLength: MAX_FOLDER_NAME_LENGTH
});
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 section = new SettingSection({
@ -164,7 +167,7 @@ export default class AppEditFolderTab extends SliderSuperTab { @@ -164,7 +167,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
name: 'exclude_read'
}], 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 excludedFlagsContainer = this.excludePeerIds.container.querySelector('.folder-categories');
@ -255,7 +258,7 @@ export default class AppEditFolderTab extends SliderSuperTab { @@ -255,7 +258,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
}
private onCreateOpen() {
this.caption.style.display = '';
// this.caption.style.display = '';
this.setTitle('FilterNew');
this.menuBtn.classList.add('hide');
this.confirmBtn.classList.remove('hide');
@ -268,7 +271,7 @@ export default class AppEditFolderTab extends SliderSuperTab { @@ -268,7 +271,7 @@ export default class AppEditFolderTab extends SliderSuperTab {
}
private onEditOpen() {
this.caption.style.display = 'none';
// this.caption.style.display = 'none';
this.setTitle(this.type === 'create' ? 'FilterNew' : 'FilterHeaderEdit');
if(this.type === 'edit') {

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

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

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

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

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

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

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

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

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

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import appSidebarLeft from "..";
import appSidebarLeft, { SettingSection } from "..";
import { InputFile } from "../../../layer";
import appChatsManager from "../../../lib/appManagers/appChatsManager";
import Button from "../../button";
@ -31,6 +31,10 @@ export default class AppNewChannelTab extends SliderSuperTab { @@ -31,6 +31,10 @@ export default class AppNewChannelTab extends SliderSuperTab {
this.uploadAvatar = _upload;
});
const section = new SettingSection({
caption: 'Channel.DescriptionHolderDescrpiton'
});
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('input-wrapper');
@ -55,10 +59,6 @@ export default class AppNewChannelTab extends SliderSuperTab { @@ -55,10 +59,6 @@ export default class AppNewChannelTab extends SliderSuperTab {
this.channelNameInputField.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.addEventListener('click', () => {
@ -87,7 +87,8 @@ export default class AppNewChannelTab extends SliderSuperTab { @@ -87,7 +87,8 @@ export default class AppNewChannelTab extends SliderSuperTab {
});
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() {

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

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

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

@ -20,7 +20,8 @@ type InputNotifyKey = Exclude<InputNotifyPeer['_'], 'inputNotifyPeer'>; @@ -20,7 +20,8 @@ type InputNotifyKey = Exclude<InputNotifyPeer['_'], 'inputNotifyPeer'>;
export default class AppNotificationsTab extends SliderSuperTabEventable {
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');
const NotifySection = (options: {

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

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

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

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

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

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

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

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

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

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

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

@ -22,6 +22,8 @@ import PopupPeer from "../../popups/peer"; @@ -22,6 +22,8 @@ import PopupPeer from "../../popups/peer";
import ButtonCorner from "../../buttonCorner";
import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import toggleDisability from "../../../helpers/dom/toggleDisability";
import CheckboxField from "../../checkboxField";
import rootScope from "../../../lib/rootScope";
export default class AppChatTypeTab extends SliderSuperTabEventable {
public chatId: ChatId;
@ -157,5 +159,40 @@ export default class AppChatTypeTab extends SliderSuperTabEventable { @@ -157,5 +159,40 @@ export default class AppChatTypeTab extends SliderSuperTabEventable {
linkInputField.setOriginalValue(originalValue);
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 { @@ -265,6 +265,7 @@ export default class AppEditChatTab extends SliderSuperTab {
});
});
// ! it won't be updated because chatFull will be old
const onChatUpdate = () => {
showChatHistoryCheckboxField.setValueSilently(isChannel && !(chatFull as ChatFull.channelFull).pFlags.hidden_prehistory);
};
@ -275,7 +276,9 @@ export default class AppEditChatTab extends SliderSuperTab { @@ -275,7 +276,9 @@ export default class AppEditChatTab extends SliderSuperTab {
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')) {

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

@ -114,7 +114,10 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -114,7 +114,10 @@ export default class AppSharedMediaTab extends SliderSuperTab {
attachClickEvent(this.closeBtn, (e) => {
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);
animatedCloseIcon.classList.remove('state-back');
} else if(!this.scrollable.isHeavyAnimationInProgress) {

51
src/components/superIcon.ts

@ -1,3 +1,9 @@ @@ -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 { safeAssign } from "../helpers/object";
import { LottieAssetName } from "../lib/rlottie/lottieLoader";
@ -9,6 +15,9 @@ export type SuperRLottieIconGetInfoResult = RLottieIconItemPart; @@ -9,6 +15,9 @@ export type SuperRLottieIconGetInfoResult = RLottieIconItemPart;
export class SuperRLottieIcon<Options extends {
PartState: any,
ColorState?: any,
Items?: {
name: string
}[]
}> extends RLottieIcon {
protected getPart: (state: Options['PartState'], prevState?: Options['PartState']) => SuperRLottieIconGetInfoResult;
protected getColor?: (state: Options['ColorState'], prevState?: Options['ColorState']) => RLottieColor;
@ -20,6 +29,7 @@ export class SuperRLottieIcon<Options extends { @@ -20,6 +29,7 @@ export class SuperRLottieIcon<Options extends {
constructor(options: {
width: number,
height: number,
skipAnimation?: boolean,
getPart: (state: Options['PartState'], prevState?: Options['PartState']) => SuperRLottieIconGetInfoResult,
getColor?: (state: Options['ColorState'], prevState?: Options['ColorState']) => RLottieColor,
}) {
@ -59,37 +69,58 @@ export class SuperRLottieIcon<Options extends { @@ -59,37 +69,58 @@ export class SuperRLottieIcon<Options extends {
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(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;
if(prevState === state) {
return;
return colorState !== undefined ? this.setColorState(colorState) : false;
}
if(colorState !== undefined) {
this.setColorState(colorState, false);
}
this.partState = state;
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;
if(prevState === state) {
return;
return false;
}
this.colorState = state;
const item = this.getItem();
const color = this.getColor(state, prevState);
const invoke = () => {
item.player.setColor(color, renderIfPaused);
};
if(item.player) {
const color = this.getColor(state, prevState);
item.player.setColor(color);
invoke();
} else {
item.onLoadForColor = invoke;
}
return true;
}
public destroy() {

215
src/components/topbarCall.ts

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

2
src/config/app.ts

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

5
src/environment/callSupport.ts

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

2
src/environment/ctx.ts

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
const ctx = typeof(window) !== 'undefined' ? window : self;
export default ctx;
export default ctx;

2
src/environment/emojiSupport.ts

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
const IS_EMOJI_SUPPORTED = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) !== -1/* && false *//* || true */;
export default IS_EMOJI_SUPPORTED;
export default IS_EMOJI_SUPPORTED;

4
src/environment/parallaxSupport.ts

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

2
src/environment/touchSupport.ts

@ -5,4 +5,4 @@ @@ -5,4 +5,4 @@
*/
// @ts-ignore
export const IS_TOUCH_SUPPORTED = ('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch)/* || true */;
export const IS_TOUCH_SUPPORTED = ('ontouchstart' in window) || (window.DocumentTouch && document instanceof DocumentTouch)/* || true */;

2
src/environment/webpSupport.ts

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
const IS_WEBP_SUPPORTED = document.createElement('canvas').toDataURL('image/webp').startsWith('data:image/webp');
export default IS_WEBP_SUPPORTED;
export default IS_WEBP_SUPPORTED;

60
src/helpers/audioAssetPlayer.ts

@ -0,0 +1,60 @@ @@ -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 @@ -47,7 +47,13 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
target.classList.add(ACTIVE_CLASS_NAME);
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 @@ -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 = () => {
if(!attached) return;
attached = false;
// input.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
document.removeEventListener(HANDLE_EVENT, onKeyDown, {capture: true});
list.removeEventListener('mousemove', onMouseMove);
@ -168,13 +187,10 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo @@ -168,13 +187,10 @@ export default function attachListNavigation({list, type, onSelect, once, waitFo
resetTarget();
}
// 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);
attach();
return {
attach,
detach,
resetTarget
};

14
src/helpers/dom/getVisibleRect.ts

@ -4,17 +4,19 @@ @@ -4,17 +4,19 @@
* 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 overflowRect = overflowElement.getBoundingClientRect();
let {top: overflowTop, bottom: overflowBottom} = overflowRect;
// * respect sticky headers
const sticky = overflowElement.querySelector('.sticky');
if(sticky) {
const stickyRect = sticky.getBoundingClientRect();
overflowTop = stickyRect.bottom;
if(lookForSticky) {
const sticky = overflowElement.querySelector('.sticky');
if(sticky) {
const stickyRect = sticky.getBoundingClientRect();
overflowTop = stickyRect.bottom;
}
}
if(rect.top >= overflowBottom
@ -48,3 +50,5 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT @@ -48,3 +50,5 @@ export default function getVisibleRect(element: HTMLElement, overflowElement: HT
overflow
};
}
(window as any).getVisibleRect = getVisibleRect;

23
src/helpers/eachMinute.ts

@ -4,28 +4,9 @@ @@ -4,28 +4,9 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import ctx from "../environment/ctx";
import noop from "./noop";
import eachTimeout from "./eachTimeout";
// It's better to use timeout instead of interval, because interval can be corrupted
export default function eachMinute(callback: () => any, 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, (60 - new Date().getSeconds()) * 1000);
})();
callback = _callback;
return cancel;
return eachTimeout(callback, () => (60 - new Date().getSeconds()) * 1000, runFirst);
}

31
src/helpers/eachTimeout.ts

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

116
src/helpers/fastSmoothScroll.ts

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

2
src/helpers/formatPhoneNumber.ts

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

4
src/helpers/middleware.ts

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

84
src/helpers/movablePanel.ts

@ -0,0 +1,84 @@ @@ -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 = { @@ -6,7 +6,6 @@ const lang = {
"BlockModal.Search.Placeholder": "Block user...",
"DarkMode": "Dark Mode",
"FilterIncludeExcludeInfo": "Choose chats and types of chats that will\nappear and never appear in this folder.",
"FilterNameInputLabel": "Folder Name",
"FilterMenuDelete": "Delete Folder",
"FilterHeaderEdit": "Edit Folder",
"FilterAllGroups": "All Groups",
@ -82,6 +81,8 @@ const lang = { @@ -82,6 +81,8 @@ const lang = {
"Message.Context.Selection.Delete": "Delete selected",
"Message.Context.Selection.Forward": "Forward 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.Disabled": "Disabled",
"Error.PreviewSender.CaptionTooLong": "Caption is too long.",
@ -592,6 +593,27 @@ const lang = { @@ -592,6 +593,27 @@ const lang = {
"ClearButton": "Clear",
"FilterAllChats": "All Chats",
"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
"AccountSettings.Filters": "Chat Folders",
@ -602,8 +624,28 @@ const lang = { @@ -602,8 +624,28 @@ const lang = {
"Alert.Confirm.Discard": "Discard",
"Appearance.Reset": "Reset to Defaults",
"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.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.",
"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.",
@ -642,6 +684,8 @@ const lang = { @@ -642,6 +684,8 @@ const lang = {
"Chat.DropQuickDesc": "in a quick way",
"Chat.DropAsFilesDesc": "without compression",
"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.Channel.UpdatedTitle": "Channel renamed to \"%@\"",
"Chat.Service.Channel.UpdatedPhoto": "Channel photo updated",
@ -700,6 +744,9 @@ const lang = { @@ -700,6 +744,9 @@ const lang = {
"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?"
},
"Chat.Message.ViewChannel": "VIEW CHANNEL",
"Chat.Message.ViewBot": "VIEW BOT",
"Chat.Message.ViewGroup": "VIEW GROUP",
"ChatList.Context.Mute": "Mute",
"ChatList.Context.Unmute": "Unmute",
"ChatList.Context.Pin": "Pin",
@ -710,8 +757,12 @@ const lang = { @@ -710,8 +757,12 @@ const lang = {
"ChatList.Context.LeaveGroup": "Leave Group",
"ChatList.Service.Call.incoming": "Incoming 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.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.Filter.Header": "Create folders for different groups of chats and quickly switch between them.",
"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 { @@ -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 {
return {
_: 'inputPeerChat',
@ -761,6 +765,15 @@ export class AppChatsManager { @@ -761,6 +765,15 @@ export class AppChatsManager {
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();

75
src/lib/appManagers/appDialogsManager.ts

@ -178,6 +178,9 @@ export class AppDialogsManager { @@ -178,6 +178,9 @@ export class AppDialogsManager {
private loadedDialogsAtLeastOnce = false;
private allChatsIntlElement: I18n.IntlElement;
private emptyDialogsPlaceholderSubtitle: I18n.IntlElement;
private updateContactsLengthPromise: Promise<number>;
constructor() {
this.chatsPreloader = putPreloader(null, true);
@ -787,10 +790,7 @@ export class AppDialogsManager { @@ -787,10 +790,7 @@ export class AppDialogsManager {
private changeFiltersAllChatsKey() {
const scrollable = this.folders.menuScrollContainer.firstElementChild;
const key: LangPackKey = scrollable.scrollWidth > scrollable.clientWidth ? 'FilterAllChatsShort' : 'FilterAllChats';
if(this.allChatsIntlElement.key !== key) {
this.allChatsIntlElement.key = key;
this.allChatsIntlElement.update();
}
this.allChatsIntlElement.compareAndUpdate({key});
}
private onFiltersLengthChange() {
@ -986,40 +986,29 @@ export class AppDialogsManager { @@ -986,40 +986,29 @@ export class AppDialogsManager {
return;
}
let placeholder: ReturnType<AppDialogsManager['generateEmptyPlaceholder']>;
let placeholder: ReturnType<AppDialogsManager['generateEmptyPlaceholder']>, type: 'dialogs' | 'folder';
if(!this.filterId) {
placeholder = this.generateEmptyPlaceholder({
title: 'ChatList.Main.EmptyPlaceholder.Title',
classNameType: 'dialogs'
classNameType: type = 'dialogs'
});
placeholderContainer = placeholder.container;
const img = document.createElement('img');
img.classList.add('empty-placeholder-dialogs-icon');
this.emptyDialogsPlaceholderSubtitle = new I18n.IntlElement({
element: placeholder.subtitle
});
Promise.all([
appUsersManager.getContacts().then(users => {
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({
key,
args,
element: placeholder.subtitle
});
}),
this.updateContactsLength(false),
renderImageFromUrlPromise(img, 'assets/img/EmptyChats.svg'),
fastRafPromise()
]).then(() => {
]).then(([usersLength]) => {
placeholderContainer.classList.add('visible');
part.classList.toggle('has-contacts', !!usersLength);
});
placeholderContainer.prepend(img);
@ -1027,7 +1016,7 @@ export class AppDialogsManager { @@ -1027,7 +1016,7 @@ export class AppDialogsManager {
placeholder = this.generateEmptyPlaceholder({
title: 'FilterNoChatsToDisplay',
subtitle: 'FilterNoChatsToDisplayInfo',
classNameType: 'folder'
classNameType: type = 'folder'
});
placeholderContainer = placeholder.container;
@ -1052,6 +1041,40 @@ export class AppDialogsManager { @@ -1052,6 +1041,40 @@ export class AppDialogsManager {
part.append(placeholderContainer);
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() {
@ -1103,6 +1126,8 @@ export class AppDialogsManager { @@ -1103,6 +1126,8 @@ export class AppDialogsManager {
if(ready) {
section.container.classList.toggle('hide', !sortedUserList.list.childElementCount);
}
this.updateContactsLength(true);
};
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 @@ -76,6 +76,9 @@ import appGroupCallsManager, { GroupCallId, MyGroupCall } from './appGroupCallsM
import TopbarCall from '../../components/topbarCall';
import confirmationPopup from '../../components/confirmationPopup';
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!');
@ -244,7 +247,24 @@ export class AppImManager { @@ -244,7 +247,24 @@ export class AppImManager {
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
// ! 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 { @@ -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) {
const chatId = peerId.toChatId();
const hasRights = appChatsManager.hasRights(chatId, 'manage_call');
@ -787,24 +880,7 @@ export class AppImManager { @@ -787,24 +880,7 @@ export class AppImManager {
}
}
const currentGroupCall = appGroupCallsManager.groupCall;
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();
}
}
await this.discardCurrentCall(peerId);
next();
};
@ -1242,11 +1318,14 @@ export class AppImManager { @@ -1242,11 +1318,14 @@ export class AppImManager {
}
this.chats.push(chat);
return chat;
}
private spliceChats(fromIndex: number, justReturn = true, animate?: boolean, spliced?: Chat[]) {
if(fromIndex >= this.chats.length) return;
const chatFrom = this.chat;
if(this.chats.length > 1 && justReturn) {
rootScope.dispatchEvent('peer_changing', this.chat);
}
@ -1255,6 +1334,8 @@ export class AppImManager { @@ -1255,6 +1334,8 @@ export class AppImManager {
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
for(let i = 0; i < spliced.length - 1; ++i) {
appNavigationController.removeByType('chat', true);
@ -1382,20 +1463,23 @@ export class AppImManager { @@ -1382,20 +1463,23 @@ export class AppImManager {
return this.setPeer(peerId, lastMsgId);
}
const chat = this.chat;
if(chat.inited) { // * use first not inited chat
this.createNewChat();
const oldChat = this.chat;
let chat = oldChat;
if(oldChat.inited) { // * use first not inited chat
chat = this.createNewChat();
}
if(type) {
this.chat.setType(type);
chat.setType(type);
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);
}

141
src/lib/appManagers/appMessagesManager.ts

@ -19,7 +19,7 @@ import { randomLong } from "../../helpers/random"; @@ -19,7 +19,7 @@ import { randomLong } from "../../helpers/random";
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 { 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 type { ApiFileManager } from '../mtproto/apiFileManager';
//import apiManager from '../mtproto/apiManager';
@ -1214,6 +1214,7 @@ export class AppMessagesManager { @@ -1214,6 +1214,7 @@ export class AppMessagesManager {
flags: 4,
total_voters: 0,
pFlags: {},
recent_voters: []
});
const {poll, results} = appPollsManager.getPoll(pollId);
@ -2384,7 +2385,7 @@ export class AppMessagesManager { @@ -2384,7 +2385,7 @@ export class AppMessagesManager {
return appMessagesIdsManager.generateMessageId(dialog?.top_message || 0, true);
}
public saveMessage(message: any, options: Partial<{
public saveMessage(message: Message, options: Partial<{
storage: MessagesStorage,
isScheduled: true,
isOutgoing: true,
@ -2406,10 +2407,7 @@ export class AppMessagesManager { @@ -2406,10 +2407,7 @@ export class AppMessagesManager {
const storage = options.storage || this.getMessagesStorage(peerId);
const isChannel = message.peer_id._ === 'peerChannel';
const isBroadcast = isChannel && appChatsManager.isBroadcast(peerId.toChatId());
if(options.isScheduled) {
message.pFlags.is_scheduled = true;
}
const isMessage = message._ === 'message';
if(options.isOutgoing) {
message.pFlags.is_outgoing = true;
@ -2418,9 +2416,20 @@ export class AppMessagesManager { @@ -2418,9 +2416,20 @@ export class AppMessagesManager {
const mid = appMessagesIdsManager.generateMessageId(message.id);
message.mid = mid;
if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = new Map());
storage.set(mid, message);
if(isMessage) {
if(options.isScheduled) {
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);
@ -2441,7 +2450,7 @@ export class AppMessagesManager { @@ -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.replies) {
if(isMessage && message.replies) {
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);
}
@ -2452,17 +2461,18 @@ export class AppMessagesManager { @@ -2452,17 +2461,18 @@ export class AppMessagesManager {
}
//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;
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 {
//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);
}
const fwdHeader = message.fwd_from as MessageFwdHeader;
if(fwdHeader) {
//if(peerId === myID) {
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 { @@ -2490,17 +2500,20 @@ export class AppMessagesManager {
}
}
if(message.via_bot_id > 0) {
message.viaBotId = message.via_bot_id;
}
const mediaContext: ReferenceContext = {
type: 'message',
peerId,
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._) {
case 'messageMediaEmpty': {
delete message.media;
@ -2509,12 +2522,12 @@ export class AppMessagesManager { @@ -2509,12 +2522,12 @@ export class AppMessagesManager {
case 'messageMediaPhoto': {
if(message.media.ttl_seconds) {
message.media = {_: 'messageMediaUnsupportedWeb'};
message.media = {_: 'messageMediaUnsupported'};
} else {
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;
}
@ -2530,7 +2543,7 @@ export class AppMessagesManager { @@ -2530,7 +2543,7 @@ export class AppMessagesManager {
case 'messageMediaDocument': {
if(message.media.ttl_seconds) {
message.media = {_: 'messageMediaUnsupportedWeb'};
message.media = {_: 'messageMediaUnsupported'};
} else {
message.media.document = appDocsManager.saveDoc(message.media.document, mediaContext); // 11.04.2020 warning
}
@ -2550,13 +2563,20 @@ export class AppMessagesManager { @@ -2550,13 +2563,20 @@ export class AppMessagesManager {
break; */
case 'messageMediaInvoice': {
message.media = {_: 'messageMediaUnsupportedWeb'};
message.media = {_: 'messageMediaUnsupported'};
break;
}
case 'messageMediaUnsupported': {
message.message = '';
delete message.entities;
delete message.totalEntities;
break;
}
}
}
if(message.action) {
if(!isMessage && message.action) {
const action = message.action as MessageAction;
let migrateFrom: PeerId;
let migrateTo: PeerId;
@ -2674,12 +2694,14 @@ export class AppMessagesManager { @@ -2674,12 +2694,14 @@ export class AppMessagesManager {
case 'messageActionPhoneCall':
// @ts-ignore
action.type =
(message.pFlags.out ? 'out_' : 'in_') +
(action.pFlags.video ? 'video_' : '') +
(action.duration !== undefined ? (message.pFlags.out ? 'out_' : 'in_') : '') +
(
action.reason._ === 'phoneCallDiscardReasonMissed' ||
action.reason._ === 'phoneCallDiscardReasonBusy'
? 'missed'
: 'ok'
action.duration !== undefined ? 'ok' : (
action.reason._ === 'phoneCallDiscardReasonMissed'
? 'missed'
: 'cancelled'
)
);
break;
}
@ -2702,7 +2724,7 @@ export class AppMessagesManager { @@ -2702,7 +2724,7 @@ export class AppMessagesManager {
message.rReply = this.getRichReplyText(message);
} */
if(message.message && message.message.length && !message.totalEntities) {
if(isMessage && message.message.length && !message.totalEntities) {
this.wrapMessageEntities(message);
}
@ -2838,6 +2860,11 @@ export class AppMessagesManager { @@ -2838,6 +2860,11 @@ export class AppMessagesManager {
break;
}
case 'messageMediaUnsupported': {
addPart(UNSUPPORTED_LANG_PACK_KEY);
break;
}
default:
//messageText += media._;
@ -4314,8 +4341,9 @@ export class AppMessagesManager { @@ -4314,8 +4341,9 @@ export class AppMessagesManager {
return;
}
this.scheduleHandleNewDialogs(peerId);
(update as any).ignoreExisting = true;
set.add(update);
this.scheduleHandleNewDialogs(peerId);
}
return;
@ -4342,27 +4370,32 @@ export class AppMessagesManager { @@ -4342,27 +4370,32 @@ export class AppMessagesManager {
this.updateMessageRepliesIfNeeded(message);
}
if(historyStorage.history.findSlice(message.mid)) {
return false;
}
// * 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;
}
// * so message can exist if reloadConversation came back earlier with mid
const ignoreExisting: boolean = (update as any).ignoreExisting;
const isExisting = !!historyStorage.history.findSlice(message.mid);
if(isExisting) {
if(!ignoreExisting) {
return false;
}
firstSlice.splice(i, 0, message.mid);
} 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) {
historyStorage.count++;
firstSlice.splice(i, 0, message.mid);
} else {
historyStorage.history.unshift(message.mid);
}
if(historyStorage.count !== null) {
historyStorage.count++;
}
}
if(this.mergeReplyKeyboard(historyStorage, message)) {
@ -4414,7 +4447,7 @@ export class AppMessagesManager { @@ -4414,7 +4447,7 @@ export class AppMessagesManager {
const inboxUnread = !message.pFlags.out && message.pFlags.unread;
if(dialog) {
if(inboxUnread) {
if(inboxUnread && message.mid > dialog.top_message) {
const releaseUnreadCount = this.dialogsStorage.prepareDialogUnreadCountModifying(dialog);
++dialog.unread_count;
@ -4426,7 +4459,9 @@ export class AppMessagesManager { @@ -4426,7 +4459,9 @@ export class AppMessagesManager {
releaseUnreadCount();
}
this.setDialogTopMessage(message, dialog);
if(message.mid >= dialog.top_message) {
this.setDialogTopMessage(message, dialog);
}
}
if(inboxUnread/* && ($rootScope.selectedPeerID != peerID || $rootScope.idle.isIDLE) */) {
@ -4505,7 +4540,7 @@ export class AppMessagesManager { @@ -4505,7 +4540,7 @@ export class AppMessagesManager {
} */
const isTopMessage = dialog && dialog.top_message === mid;
if((message as Message.message).clear_history) {
if((message as Message.messageService).clear_history) {
if(isTopMessage) {
rootScope.dispatchEvent('dialog_flush', {peerId});
}
@ -5873,6 +5908,10 @@ export class AppMessagesManager { @@ -5873,6 +5908,10 @@ export class AppMessagesManager {
public isDialogUnread(dialog: 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();

14
src/lib/appManagers/appPeersManager.ts

@ -247,11 +247,7 @@ export class AppPeersManager { @@ -247,11 +247,7 @@ export class AppPeersManager {
if(!peerId.isUser()) {
const chatId = peerId.toChatId();
if(!appChatsManager.isChannel(chatId)) {
return appChatsManager.getChatInputPeer(chatId);
} else {
return appChatsManager.getChannelInputPeer(chatId);
}
return appChatsManager.getInputPeer(chatId);
}
const userId = peerId.toUserId();
@ -314,6 +310,14 @@ export class AppPeersManager { @@ -314,6 +310,14 @@ export class AppPeersManager {
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';

20
src/lib/appManagers/appProfileManager.ts

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
import { MOUNT_CLASS_TO } from "../../config/debug";
import { tsNow } from "../../helpers/date";
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 apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
@ -20,7 +20,7 @@ import { RichTextProcessor } from "../richtextprocessor"; @@ -20,7 +20,7 @@ import { RichTextProcessor } from "../richtextprocessor";
import rootScope from "../rootScope";
import SearchIndex from "../searchIndex";
import apiUpdatesManager from "./apiUpdatesManager";
import appChatsManager, { Channel } from "./appChatsManager";
import appChatsManager from "./appChatsManager";
import appMessagesIdsManager from "./appMessagesIdsManager";
import appNotificationsManager from "./appNotificationsManager";
import appPeersManager from "./appPeersManager";
@ -111,7 +111,7 @@ export class AppProfileManager { @@ -111,7 +111,7 @@ export class AppProfileManager {
if(photo) {
const hasChatPhoto = photo._ !== 'chatPhotoEmpty';
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;
}
}
@ -168,10 +168,11 @@ export class AppProfileManager { @@ -168,10 +168,11 @@ export class AppProfileManager {
params: {
id: appUsersManager.getUserInput(id)
},
processResult: (userFull) => {
const user = userFull.user as User;
appUsersManager.saveApiUser(user, true);
processResult: (usersUserFull) => {
appChatsManager.saveApiChats(usersUserFull.chats, true);
appUsersManager.saveApiUsers(usersUserFull.users);
const userFull = usersUserFull.full_user;
const peerId = id.toPeerId(false);
if(userFull.profile_photo) {
userFull.profile_photo = appPhotosManager.savePhoto(userFull.profile_photo, {type: 'profilePhoto', peerId});
@ -186,7 +187,7 @@ export class AppProfileManager { @@ -186,7 +187,7 @@ export class AppProfileManager {
settings: userFull.notify_settings
});
rootScope.dispatchEvent('user_full_update', id);
this.usersFull[id] = userFull;
/* if(userFull.bot_info) {
userFull.bot_info = this.saveBotInfo(userFull.bot_info) as any;
@ -194,7 +195,8 @@ export class AppProfileManager { @@ -194,7 +195,8 @@ export class AppProfileManager {
//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>; @@ -176,7 +176,7 @@ const ALL_KEYS = Object.keys(STATE_INIT) as any as Array<keyof State>;
const REFRESH_KEYS = ['contactsList', 'stateCreatedTime',
'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>;
@ -213,6 +213,10 @@ export class AppStateManager extends EventListenerBase<{ @@ -213,6 +213,10 @@ export class AppStateManager extends EventListenerBase<{
constructor() {
super();
this.loadSavedState();
rootScope.addEventListener('user_auth', () => {
this.requestPeerSingle(rootScope.myId, 'self');
});
}
public loadSavedState(): Promise<State> {

18
src/lib/appManagers/appStickersManager.ts

@ -26,10 +26,12 @@ export type MyStickerSetInput = { @@ -26,10 +26,12 @@ export type MyStickerSetInput = {
access_hash?: StickerSet.stickerSet['access_hash']
};
export type MyMessagesStickerSet = MessagesStickerSet.messagesStickerSet;
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 greetingStickers: Document.document[];
@ -41,8 +43,9 @@ export class AppStickersManager { @@ -41,8 +43,9 @@ export class AppStickersManager {
rootScope.addMultipleEventsListeners({
updateNewStickerSet: (update) => {
this.saveStickerSet(update.stickerset, update.stickerset.set.id);
rootScope.dispatchEvent('stickers_installed', update.stickerset.set);
const stickerSet = update.stickerset as MyMessagesStickerSet;
this.saveStickerSet(stickerSet, stickerSet.set.id);
rootScope.dispatchEvent('stickers_installed', stickerSet.set);
}
});
@ -92,7 +95,7 @@ export class AppStickersManager { @@ -92,7 +95,7 @@ export class AppStickersManager {
overwrite: boolean,
useCache: boolean,
saveById: boolean
}> = {}): Promise<MessagesStickerSet> {
}> = {}): Promise<MyMessagesStickerSet> {
const id = set.id;
if(this.getStickerSetPromises[id]) {
return this.getStickerSetPromises[id];
@ -111,8 +114,9 @@ export class AppStickersManager { @@ -111,8 +114,9 @@ export class AppStickersManager {
try {
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;
this.saveStickerSet(stickerSet, saveById);

6
src/lib/appManagers/appUsersManager.ts

@ -80,7 +80,11 @@ export class AppUsersManager { @@ -80,7 +80,11 @@ export class AppUsersManager {
const userId = update.user_id;
const user = this.users[userId];
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') {
delete user.photo;

98
src/lib/calls/callConnectionInstanceBase.ts

@ -0,0 +1,98 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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;
}

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

Loading…
Cancel
Save