Browse Source

Audio improvements

Changelogs
master
morethanwords 3 years ago
parent
commit
bf0a86abe9
  1. 6
      CHANGELOG.md
  2. 3
      package.json
  3. 4
      src/components/animationIntersector.ts
  4. 311
      src/components/appMediaPlaybackController.ts
  5. 1656
      src/components/appMediaViewer.ts
  6. 57
      src/components/appMediaViewerAvatar.ts
  7. 1522
      src/components/appMediaViewerBase.ts
  8. 3
      src/components/appSearchSuper..ts
  9. 65
      src/components/audio.ts
  10. 3
      src/components/avatar.ts
  11. 8
      src/components/buttonIcon.ts
  12. 80
      src/components/chat/audio.ts
  13. 93
      src/components/chat/bubbles.ts
  14. 75
      src/components/chat/pinnedContainer.ts
  15. 43
      src/components/chat/pinnedMessage.ts
  16. 12
      src/components/chat/selection.ts
  17. 56
      src/components/chat/topbar.ts
  18. 357
      src/components/peerProfile.ts
  19. 332
      src/components/peerProfileAvatars.ts
  20. 15
      src/components/popups/index.ts
  21. 43
      src/components/rangeSelector.ts
  22. 6
      src/components/sidebarLeft/tabs/generalSettings.ts
  23. 15
      src/components/sidebarRight/index.ts
  24. 720
      src/components/sidebarRight/tabs/sharedMedia.ts
  25. 195
      src/components/wrappers.ts
  26. 2
      src/config/app.ts
  27. 5
      src/environment/parallaxSupport.ts
  28. 33
      src/helpers/avatarListLoader.ts
  29. 4
      src/helpers/cancellablePromise.ts
  30. 14
      src/helpers/dom/attachGrabListeners.ts
  31. 25
      src/helpers/filterChatPhotosMessages.ts
  32. 29
      src/helpers/listLoader.ts
  33. 4
      src/helpers/object.ts
  34. 161
      src/helpers/searchListLoader.ts
  35. 1
      src/layer.d.ts
  36. 33
      src/lib/appManagers/apiUpdatesManager.ts
  37. 2
      src/lib/appManagers/appDialogsManager.ts
  38. 16
      src/lib/appManagers/appImManager.ts
  39. 8
      src/lib/appManagers/appMessagesIdsManager.ts
  40. 377
      src/lib/appManagers/appMessagesManager.ts
  41. 5
      src/lib/appManagers/appStateManager.ts
  42. 239
      src/lib/mediaPlayer.ts
  43. 2
      src/lib/mtproto/mtprotoworker.ts
  44. 7
      src/lib/rootScope.ts
  45. 49
      src/lib/storages/dialogs.ts
  46. 20
      src/scripts/generate_changelog.js
  47. 1
      src/scripts/in/schema_additional_params.json
  48. 6
      src/scss/partials/_audio.scss
  49. 6
      src/scss/partials/_button.scss
  50. 4
      src/scss/partials/_chat.scss
  51. 7
      src/scss/partials/_chatBubble.scss
  52. 300
      src/scss/partials/_chatPinned.scss
  53. 94
      src/scss/partials/_chatTopbar.scss
  54. 15
      src/scss/partials/_ckin.scss
  55. 7
      src/scss/partials/_rightSidebar.scss
  56. 16
      src/scss/partials/_ripple.scss
  57. 11
      src/scss/style.scss

6
CHANGELOG.md

@ -0,0 +1,6 @@
### 0.8.6
* Added changelogs.
* Audio player improvements: seek, next/previous buttons, volume controls. Changing volume in the video player will affect audio as well.
* Fixed inability to delete multiple items from pending scheduled messages.
* Fixed accidentally deleting media messages after removing their captions.
* Fixed editing album captions.

3
package.json

@ -13,7 +13,8 @@
"profile": "webpack --profile --json > stats.json --config webpack.prod.js", "profile": "webpack --profile --json > stats.json --config webpack.prod.js",
"profile:dev": "webpack --profile --json > stats.json --config webpack.dev.js", "profile:dev": "webpack --profile --json > stats.json --config webpack.dev.js",
"whybundled": "npm run profile && whybundled stats.json", "whybundled": "npm run profile && whybundled stats.json",
"generate-mtproto-types": "node ./src/scripts/generate_mtproto_types.js src/" "generate-mtproto-types": "node ./src/scripts/generate_mtproto_types.js src/",
"generate-changelog": "node ./src/scripts/generate_changelog.js"
}, },
"author": "", "author": "",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",

4
src/components/animationIntersector.ts

@ -60,14 +60,14 @@ export class AnimationIntersector {
} }
}); });
rootScope.addEventListener('audio_play', ({doc}) => { rootScope.addEventListener('media_play', ({doc}) => {
if(doc.type === 'round') { if(doc.type === 'round') {
this.videosLocked = true; this.videosLocked = true;
this.checkAnimations(); this.checkAnimations();
} }
}); });
rootScope.addEventListener('audio_pause', () => { rootScope.addEventListener('media_pause', () => {
if(this.videosLocked) { if(this.videosLocked) {
this.videosLocked = false; this.videosLocked = false;
this.checkAnimations(); this.checkAnimations();

311
src/components/appMediaPlaybackController.ts

@ -20,9 +20,8 @@ import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; import appAvatarsManager from "../lib/appManagers/appAvatarsManager";
import appPeersManager from "../lib/appManagers/appPeersManager"; import appPeersManager from "../lib/appManagers/appPeersManager";
import I18n from "../lib/langPack"; import I18n from "../lib/langPack";
import { SearchListLoader } from "./appMediaViewer"; import SearchListLoader from "../helpers/searchListLoader";
import { onMediaLoad } from "../helpers/files";
// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
// TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню // TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка // TODO: Safari: попробовать замаскировать подгрузку последнего чанка
@ -42,27 +41,44 @@ const SHOULD_USE_SAFARI_FIX = (() => {
const SEEK_OFFSET = 10; const SEEK_OFFSET = 10;
export type MediaSearchContext = SearchSuperContext & Partial<{
isScheduled: boolean,
useSearch: boolean
}>;
type MediaDetails = {
peerId: number,
mid: number,
docId: string,
clean?: boolean,
isScheduled?: boolean,
isSingle?: boolean
};
class AppMediaPlaybackController { class AppMediaPlaybackController {
private container: HTMLElement; private container: HTMLElement;
private media: { private media: Map<number, Map<number, HTMLMediaElement>> = new Map();
[peerId: string]: { private scheduled: AppMediaPlaybackController['media'] = new Map();
[mid: string]: HTMLMediaElement private mediaDetails: Map<HTMLMediaElement, MediaDetails> = new Map();
}
} = {};
private playingMedia: HTMLMediaElement; private playingMedia: HTMLMediaElement;
private waitingMediaForLoad: { private waitingMediaForLoad: Map<number, Map<number, CancellablePromise<void>>> = new Map();
[peerId: string]: { private waitingScheduledMediaForLoad: AppMediaPlaybackController['waitingMediaForLoad'] = new Map();
[mid: string]: CancellablePromise<void>
}
} = {};
private waitingDocumentsForLoad: {[docId: string]: Set<HTMLMediaElement>} = {}; private waitingDocumentsForLoad: {[docId: string]: Set<HTMLMediaElement>} = {};
public willBePlayedMedia: HTMLMediaElement; public willBePlayedMedia: HTMLMediaElement;
private searchContext: SearchSuperContext; private searchContext: MediaSearchContext;
private listLoader: SearchListLoader<MediaItem>; private listLoader: SearchListLoader<MediaItem>;
public volume: number;
public muted: boolean;
public playbackRate: number;
private _volume = 1;
private _muted = false;
private _playbackRate = 1;
private lockedSwitchers: boolean;
constructor() { constructor() {
this.container = document.createElement('div'); this.container = document.createElement('div');
//this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;'; //this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;';
@ -98,34 +114,81 @@ class AppMediaPlaybackController {
} }
} }
}); });
const properties: {[key: PropertyKey]: PropertyDescriptor} = {};
const keys = [
'volume' as const,
'muted' as const,
'playbackRate' as const
];
keys.forEach(key => {
const _key = ('_' + key) as `_${typeof key}`;
properties[key] = {
get: () => this[_key],
set: (value: number | boolean) => {
if(this[_key] === value) {
return;
}
// @ts-ignore
this[_key] = value;
if(this.playingMedia) {
// @ts-ignore
this.playingMedia[key] = value;
}
this.dispatchPlaybackParams();
}
};
});
Object.defineProperties(this, properties);
}
private dispatchPlaybackParams() {
const {volume, muted, playbackRate} = this;
rootScope.dispatchEvent('media_playback_params', {
volume, muted, playbackRate
});
} }
public seekBackward = (details: MediaSessionActionDetails) => { public seekBackward = (details: MediaSessionActionDetails) => {
const media = this.playingMedia const media = this.playingMedia;
if(media) { if(media) {
media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET)); media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET));
} }
}; };
public seekForward = (details: MediaSessionActionDetails) => { public seekForward = (details: MediaSessionActionDetails) => {
const media = this.playingMedia const media = this.playingMedia;
if(media) { if(media) {
media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET)); media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET));
} }
}; };
public seekTo = (details: MediaSessionActionDetails) => { public seekTo = (details: MediaSessionActionDetails) => {
const media = this.playingMedia const media = this.playingMedia;
if(media) { if(media) {
media.currentTime = details.seekTime; media.currentTime = details.seekTime;
} }
}; };
public addMedia(peerId: number, doc: MyDocument, mid: number, autoload = true): HTMLMediaElement { public addMedia(message: Message.message, autoload: boolean, clean?: boolean): HTMLMediaElement {
const storage = this.media[peerId] ?? (this.media[peerId] = {}); const {peerId, mid} = message;
if(storage[mid]) return storage[mid];
const isScheduled = !!message.pFlags.is_scheduled;
const s = isScheduled ? this.scheduled : this.media;
let storage = s.get(message.peerId);
if(!storage) {
s.set(message.peerId, storage = new Map());
}
let media = storage.get(mid);
if(media) {
return media;
}
const media = document.createElement(doc.type === 'round' ? 'video' : 'audio'); const doc: MyDocument = appMessagesManager.getMediaFromMessage(message);
storage.set(mid, media = document.createElement(doc.type === 'round' || doc.type === 'video' ? 'video' : 'audio'));
//const source = document.createElement('source'); //const source = document.createElement('source');
//source.type = doc.type === 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type; //source.type = doc.type === 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type;
@ -134,10 +197,15 @@ class AppMediaPlaybackController {
//media.muted = true; //media.muted = true;
} }
media.dataset.docId = '' + doc.id; const details: MediaDetails = {
media.dataset.peerId = '' + peerId; peerId,
media.dataset.mid = '' + mid; mid,
media.dataset.type = doc.type; docId: doc.id,
clean,
isScheduled: message.pFlags.is_scheduled
};
this.mediaDetails.set(media, details);
//media.autoplay = true; //media.autoplay = true;
media.volume = 1; media.volume = 1;
@ -149,7 +217,6 @@ class AppMediaPlaybackController {
media.addEventListener('pause', this.onPause); media.addEventListener('pause', this.onPause);
media.addEventListener('ended', this.onEnded); media.addEventListener('ended', this.onEnded);
const message: Message.message = appMessagesManager.getMessageByPeer(peerId, mid);
if(doc.type !== 'audio' && message?.pFlags.media_unread && message.fromId !== rootScope.myId) { if(doc.type !== 'audio' && message?.pFlags.media_unread && message.fromId !== rootScope.myId) {
media.addEventListener('timeupdate', () => { media.addEventListener('timeupdate', () => {
appMessagesManager.readMessages(peerId, [mid]); appMessagesManager.readMessages(peerId, [mid]);
@ -174,8 +241,13 @@ class AppMediaPlaybackController {
if(autoload) { if(autoload) {
deferred.resolve(); deferred.resolve();
} else { } else {
const waitingStorage = this.waitingMediaForLoad[peerId] ?? (this.waitingMediaForLoad[peerId] = {}); const w = message.pFlags.is_scheduled ? this.waitingScheduledMediaForLoad : this.waitingMediaForLoad;
waitingStorage[mid] = deferred; let waitingStorage = w.get(peerId);
if(!waitingStorage) {
w.set(peerId, waitingStorage = new Map());
}
waitingStorage.set(mid, deferred);
} }
deferred.then(() => { deferred.then(() => {
@ -196,11 +268,17 @@ class AppMediaPlaybackController {
} }
}/* , onError */); }/* , onError */);
return storage[mid] = media; return media;
}
public getMedia(peerId: number, mid: number, isScheduled?: boolean) {
const s = (isScheduled ? this.scheduled : this.media).get(peerId);
return s?.get(mid);
} }
private onMediaDocumentLoad = (media: HTMLMediaElement) => { private onMediaDocumentLoad = (media: HTMLMediaElement) => {
const doc = appDocsManager.getDoc(media.dataset.docId); const details = this.mediaDetails.get(media);
const doc = appDocsManager.getDoc(details.docId);
if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) { if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) {
this.handleSafariStreamable(media); this.handleSafariStreamable(media);
} }
@ -248,16 +326,21 @@ class AppMediaPlaybackController {
}/* , {once: true} */); }/* , {once: true} */);
} }
public resolveWaitingForLoadMedia(peerId: number, mid: number) { public resolveWaitingForLoadMedia(peerId: number, mid: number, isScheduled?: boolean) {
const storage = this.waitingMediaForLoad[peerId]; const w = isScheduled ? this.waitingScheduledMediaForLoad : this.waitingMediaForLoad;
const storage = w.get(peerId);
if(!storage) { if(!storage) {
return; return;
} }
const promise = storage[mid]; const promise = storage.get(mid);
if(promise) { if(promise) {
promise.resolve(); promise.resolve();
delete storage[mid]; storage.delete(mid);
if(!storage.size) {
w.delete(peerId);
}
} }
} }
@ -274,8 +357,9 @@ class AppMediaPlaybackController {
media.safariBuffering = value; media.safariBuffering = value;
} }
private async setNewMediadata(message: Message.message) { private async setNewMediadata(message: Message.message, playingMedia = this.playingMedia) {
const playingMedia = this.playingMedia; await onMediaLoad(playingMedia, undefined, false); // have to wait for load, otherwise on macOS won't set
const doc = (message.media as MessageMedia.messageMediaDocument).document as MyDocument; const doc = (message.media as MessageMedia.messageMediaDocument).document as MyDocument;
const artwork: MediaImage[] = []; const artwork: MediaImage[] = [];
@ -374,33 +458,58 @@ class AppMediaPlaybackController {
navigator.mediaSession.metadata = metadata; navigator.mediaSession.metadata = metadata;
} }
onPlay = (e?: Event) => { private getMessageByMedia(media: HTMLMediaElement) {
const details = this.mediaDetails.get(media);
const {peerId, mid} = details;
const message = details.isScheduled ? appMessagesManager.getScheduledMessageByPeer(peerId, mid) : appMessagesManager.getMessageByPeer(peerId, mid);
return message;
}
private onPlay = (e?: Event) => {
const media = e.target as HTMLMediaElement; const media = e.target as HTMLMediaElement;
const peerId = +media.dataset.peerId; const details = this.mediaDetails.get(media);
const mid = +media.dataset.mid; const {peerId, mid} = details;
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media); //console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
const message = appMessagesManager.getMessageByPeer(peerId, mid); const message = this.getMessageByMedia(media);
const previousMedia = this.playingMedia; const previousMedia = this.playingMedia;
if(previousMedia !== media) { if(previousMedia !== media) {
this.stop(); this.stop();
this.playingMedia = media; const verify = (element: MediaItem) => element.mid === mid && element.peerId === peerId;
if(!this.listLoader.current || !verify(this.listLoader.current)) {
let idx = this.listLoader.previous.findIndex(verify);
let jumpLength: number;
if(idx !== -1) {
jumpLength = -(this.listLoader.previous.length - idx);
} else {
idx = this.listLoader.next.findIndex(verify);
if(idx !== -1) {
jumpLength = idx + 1;
}
}
if('mediaSession' in navigator) { if(idx !== -1) {
this.setNewMediadata(message); if(jumpLength) {
this.listLoader.go(jumpLength, false);
}
} else {
this.setTargets({peerId, mid});
}
} }
this.setMedia(media, message);
} }
// audio_pause не успеет сработать без таймаута // audio_pause не успеет сработать без таймаута
setTimeout(() => { setTimeout(() => {
rootScope.dispatchEvent('audio_play', {peerId, doc: message.media.document, mid}); rootScope.dispatchEvent('media_play', {doc: appMessagesManager.getMediaFromMessage(message), message, media});
}, 0); }, 0);
}; };
onPause = (e?: Event) => { private onPause = (e?: Event) => {
/* const target = e.target as HTMLMediaElement; /* const target = e.target as HTMLMediaElement;
if(!isInDOM(target)) { if(!isInDOM(target)) {
this.container.append(target); this.container.append(target);
@ -408,10 +517,10 @@ class AppMediaPlaybackController {
return; return;
} */ } */
rootScope.dispatchEvent('audio_pause'); rootScope.dispatchEvent('media_pause');
}; };
onEnded = (e?: Event) => { private onEnded = (e?: Event) => {
if(!e.isTrusted) { if(!e.isTrusted) {
return; return;
} }
@ -425,7 +534,7 @@ class AppMediaPlaybackController {
public toggle(play?: boolean) { public toggle(play?: boolean) {
if(!this.playingMedia) { if(!this.playingMedia) {
return; return false;
} }
if(play === undefined) { if(play === undefined) {
@ -433,7 +542,7 @@ class AppMediaPlaybackController {
} }
if(this.playingMedia.paused !== play) { if(this.playingMedia.paused !== play) {
return; return false;
} }
if(play) { if(play) {
@ -441,6 +550,8 @@ class AppMediaPlaybackController {
} else { } else {
this.playingMedia.pause(); this.playingMedia.pause();
} }
return true;
} }
public play = () => { public play = () => {
@ -453,21 +564,45 @@ class AppMediaPlaybackController {
public stop = () => { public stop = () => {
const media = this.playingMedia; const media = this.playingMedia;
if(media) { if(!media) {
if(!media.paused) { return false;
media.pause(); }
if(!media.paused) {
media.pause();
}
media.currentTime = 0;
simulateEvent(media, 'ended');
const details = this.mediaDetails.get(media);
if(details?.clean) {
media.src = '';
const peerId = details.peerId;
const s = details.isScheduled ? this.scheduled : this.media;
const storage = s.get(peerId);
if(storage) {
storage.delete(details.mid);
if(!storage.size) {
s.delete(peerId);
}
} }
media.currentTime = 0; media.remove();
simulateEvent(media, 'ended');
// this.playingMedia = undefined; this.mediaDetails.delete(media);
} }
this.playingMedia = undefined;
return true;
}; };
public playItem = (item: MediaItem) => { public playItem = (item: MediaItem) => {
const {peerId, mid} = item; const {peerId, mid} = item;
const media = this.media[peerId][mid]; const isScheduled = this.searchContext.isScheduled;
const media = this.getMedia(peerId, mid, isScheduled);
/* if(isSafari) { /* if(isSafari) {
media.autoplay = true; media.autoplay = true;
@ -476,12 +611,12 @@ class AppMediaPlaybackController {
media.play(); media.play();
setTimeout(() => { setTimeout(() => {
this.resolveWaitingForLoadMedia(peerId, mid); this.resolveWaitingForLoadMedia(peerId, mid, isScheduled);
}, 0); }, 0);
}; };
public next = () => { public next = () => {
this.listLoader.go(1); return !this.lockedSwitchers && this.listLoader.go(1);
}; };
public previous = () => { public previous = () => {
@ -492,14 +627,14 @@ class AppMediaPlaybackController {
return; return;
} }
this.listLoader.go(-1); return !this.lockedSwitchers && this.listLoader.go(-1);
}; };
public willBePlayed(media: HTMLMediaElement) { public willBePlayed(media: HTMLMediaElement) {
this.willBePlayedMedia = media; this.willBePlayedMedia = media;
} }
public setSearchContext(context: SearchSuperContext) { public setSearchContext(context: MediaSearchContext) {
if(deepEqual(this.searchContext, context)) { if(deepEqual(this.searchContext, context)) {
return false; return false;
} }
@ -508,20 +643,25 @@ class AppMediaPlaybackController {
return true; return true;
} }
public getSearchContext() {
return this.searchContext;
}
public setTargets(current: MediaItem, prev?: MediaItem[], next?: MediaItem[]) { public setTargets(current: MediaItem, prev?: MediaItem[], next?: MediaItem[]) {
if(!this.listLoader) { if(!this.listLoader) {
this.listLoader = new SearchListLoader({ this.listLoader = new SearchListLoader({
loadCount: 10, loadCount: 10,
loadWhenLeft: 5, loadWhenLeft: 5,
processItem: (item: Message.message) => { processItem: (message: Message.message) => {
const {peerId, mid} = item; this.addMedia(message, false);
this.addMedia(peerId, (item.media as MessageMedia.messageMediaDocument).document as MyDocument, mid, false); return {peerId: message.peerId, mid: message.mid};
return {peerId, mid};
}, },
onJump: (item, older) => { onJump: (item, older) => {
this.playItem(item); this.playItem(item);
} }
}); });
this.listLoader.onEmptied = this.stop;
} else { } else {
this.listLoader.reset(); this.listLoader.reset();
} }
@ -539,6 +679,49 @@ class AppMediaPlaybackController {
this.listLoader.load(true); this.listLoader.load(true);
this.listLoader.load(false); this.listLoader.load(false);
} }
public setMedia(media: HTMLMediaElement, message: Message.message) {
this.playingMedia = media;
this.playingMedia.volume = this.volume;
this.playingMedia.muted = this.muted;
this.playingMedia.playbackRate = this.playbackRate;
if('mediaSession' in navigator) {
this.setNewMediadata(message);
}
}
public setSingleMedia(media: HTMLMediaElement, message: Message.message) {
const playingMedia = this.playingMedia;
const wasPlaying = this.pause();
this.willBePlayed(undefined);
this.setMedia(media, message);
this.toggleSwitchers(false);
return () => {
this.toggleSwitchers(true);
if(this.mediaDetails.get(playingMedia)) {
this.setMedia(playingMedia, this.getMessageByMedia(playingMedia));
} else {
this.next() || this.previous();
}
if(this.playingMedia === media) {
this.stop();
}
if(wasPlaying) {
this.play();
}
};
}
public toggleSwitchers(enabled: boolean) {
this.lockedSwitchers = !enabled;
}
} }
const appMediaPlaybackController = new AppMediaPlaybackController(); const appMediaPlaybackController = new AppMediaPlaybackController();

1656
src/components/appMediaViewer.ts

File diff suppressed because it is too large Load Diff

57
src/components/appMediaViewerAvatar.ts

@ -0,0 +1,57 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import AvatarListLoader from "../helpers/avatarListLoader";
import appImManager from "../lib/appManagers/appImManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager";
import AppMediaViewerBase from "./appMediaViewerBase";
type AppMediaViewerAvatarTargetType = {element: HTMLElement, photoId: string};
export default class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMediaViewerAvatarTargetType> {
public peerId: number;
constructor(peerId: number) {
super(new AvatarListLoader({peerId}), [/* 'delete' */]);
this.peerId = peerId;
this.setBtnMenuToggle([{
icon: 'download',
text: 'MediaViewer.Context.Download',
onClick: this.onDownloadClick
}/* , {
icon: 'delete danger btn-disabled',
text: 'Delete',
onClick: () => {}
} */]);
// * constructing html end
this.setListeners();
}
onPrevClick = (target: AppMediaViewerAvatarTargetType) => {
this.openMedia(target.photoId, target.element, -1);
};
onNextClick = (target: AppMediaViewerAvatarTargetType) => {
this.openMedia(target.photoId, target.element, 1);
};
onDownloadClick = () => {
appPhotosManager.savePhotoFile(appPhotosManager.getPhoto(this.target.photoId), appImManager.chat.bubbles.lazyLoadQueue.queueId);
};
public async openMedia(photoId: string, target?: HTMLElement, fromRight = 0, prevTargets?: AppMediaViewerAvatarTargetType[], nextTargets?: AppMediaViewerAvatarTargetType[]) {
if(this.setMoverPromise) return this.setMoverPromise;
const photo = appPhotosManager.getPhoto(photoId);
const ret = super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets);
this.target.photoId = photo.id;
return ret;
}
}

1522
src/components/appMediaViewerBase.ts

File diff suppressed because it is too large Load Diff

3
src/components/appSearchSuper..ts

@ -4,7 +4,6 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import { months } from "../helpers/date";
import { copy, getObjectKeysAndSort, safeAssign } from "../helpers/object"; import { copy, getObjectKeysAndSort, safeAssign } from "../helpers/object";
import { escapeRegExp, limitSymbols } from "../helpers/string"; import { escapeRegExp, limitSymbols } from "../helpers/string";
import appChatsManager from "../lib/appManagers/appChatsManager"; import appChatsManager from "../lib/appManagers/appChatsManager";
@ -17,7 +16,6 @@ import appUsersManager from "../lib/appManagers/appUsersManager";
import { logger } from "../lib/logger"; import { logger } from "../lib/logger";
import RichTextProcessor from "../lib/richtextprocessor"; import RichTextProcessor from "../lib/richtextprocessor";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import AppMediaViewer from "./appMediaViewer";
import { SearchGroup, SearchGroupType } from "./appSearch"; import { SearchGroup, SearchGroupType } from "./appSearch";
import { horizontalMenu } from "./horizontalMenu"; import { horizontalMenu } from "./horizontalMenu";
import LazyLoadQueue from "./lazyLoadQueue"; import LazyLoadQueue from "./lazyLoadQueue";
@ -51,6 +49,7 @@ import { SearchSelection } from "./chat/selection";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent, simulateClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent, simulateClickEvent } from "../helpers/dom/clickEvent";
import { MyDocument } from "../lib/appManagers/appDocsManager"; import { MyDocument } from "../lib/appManagers/appDocsManager";
import AppMediaViewer from "./appMediaViewer";
//const testScroll = false; //const testScroll = false;

65
src/components/audio.ts

@ -8,14 +8,13 @@ import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
import { wrapPhoto } from "./wrappers"; import { wrapPhoto } from "./wrappers";
import ProgressivePreloader from "./preloader"; import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer"; import { MediaProgressLine } from "../lib/mediaPlayer";
import appMediaPlaybackController, { MediaItem } from "./appMediaPlaybackController"; import appMediaPlaybackController, { MediaItem, MediaSearchContext } from "./appMediaPlaybackController";
import { DocumentAttribute } from "../layer"; import { DocumentAttribute, Message } from "../layer";
import mediaSizes from "../helpers/mediaSizes"; import mediaSizes from "../helpers/mediaSizes";
import { IS_SAFARI } from "../environment/userAgent"; import { IS_SAFARI } from "../environment/userAgent";
import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import './middleEllipsis'; import './middleEllipsis';
import { SearchSuperContext } from "./appSearchSuper.";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
import LazyLoadQueue from "./lazyLoadQueue"; import LazyLoadQueue from "./lazyLoadQueue";
@ -81,7 +80,7 @@ function wrapVoiceMessage(audioEl: AudioElement) {
audioEl.classList.add('is-voice'); audioEl.classList.add('is-voice');
const message = audioEl.message; const message = audioEl.message;
const doc = (message.media.document || message.media.webpage.document) as MyDocument; const doc = appMessagesManager.getMediaFromMessage(message) as MyDocument;
if(message.pFlags.out) { if(message.pFlags.out) {
audioEl.classList.add('is-out'); audioEl.classList.add('is-out');
@ -218,7 +217,7 @@ function wrapVoiceMessage(audioEl: AudioElement) {
const scrubTime = offsetX / availW /* width */ * audio.duration; const scrubTime = offsetX / availW /* width */ * audio.duration;
audio.currentTime = scrubTime; audio.currentTime = scrubTime;
} }
}); }, noop);
return () => { return () => {
progress.remove(); progress.remove();
@ -234,7 +233,7 @@ function wrapAudio(audioEl: AudioElement) {
const withTime = audioEl.withTime; const withTime = audioEl.withTime;
const message = audioEl.message; const message = audioEl.message;
const doc: MyDocument = message.media.document || message.media.webpage.document; const doc: MyDocument = appMessagesManager.getMediaFromMessage(message);
const isVoice = doc.type === 'voice' || doc.type === 'round'; const isVoice = doc.type === 'voice' || doc.type === 'round';
const descriptionEl = document.createElement('div'); const descriptionEl = document.createElement('div');
@ -336,13 +335,40 @@ function constructDownloadPreloader(tryAgainOnFail = true) {
return preloader; return preloader;
} }
export const findAudioTargets = (anchor: HTMLElement, useSearch: boolean) => {
let prev: MediaItem[], next: MediaItem[];
// if(anchor.classList.contains('search-super-item') || !useSearch) {
const container = findUpClassName(anchor, anchor.classList.contains('search-super-item') ? 'tabs-tab' : 'bubbles-inner');
if(container) {
const attr = `:not([data-is-outgoing="1"])`;
const justAudioSelector = `.audio:not(.is-voice)${attr}`;
let selector: string;
if(!anchor.matches(justAudioSelector)) {
selector = `.audio.is-voice${attr}, .media-round${attr}`;
} else {
selector = justAudioSelector;
}
const elements = Array.from(container.querySelectorAll(selector)) as HTMLElement[];
const idx = elements.indexOf(anchor);
const mediaItems: MediaItem[] = elements.map(element => ({peerId: +element.dataset.peerId, mid: +element.dataset.mid}));
prev = mediaItems.slice(0, idx);
next = mediaItems.slice(idx + 1);
}
// }
return [prev, next];
};
export default class AudioElement extends HTMLElement { export default class AudioElement extends HTMLElement {
public audio: HTMLAudioElement; public audio: HTMLAudioElement;
public preloader: ProgressivePreloader; public preloader: ProgressivePreloader;
public message: any; public message: Message.message;
public withTime = false; public withTime = false;
public voiceAsMusic = false; public voiceAsMusic = false;
public searchContext: SearchSuperContext; public searchContext: MediaSearchContext;
public showSender = false; public showSender = false;
public noAutoDownload: boolean; public noAutoDownload: boolean;
public lazyLoadQueue: LazyLoadQueue; public lazyLoadQueue: LazyLoadQueue;
@ -356,7 +382,10 @@ export default class AudioElement extends HTMLElement {
public render() { public render() {
this.classList.add('audio'); this.classList.add('audio');
const doc: MyDocument = this.message.media.document || this.message.media.webpage.document; this.dataset.mid = '' + this.message.mid;
this.dataset.peerId = '' + this.message.peerId;
const doc: MyDocument = appMessagesManager.getMediaFromMessage(this.message);
const isRealVoice = doc.type === 'voice'; const isRealVoice = doc.type === 'voice';
const isVoice = !this.voiceAsMusic && isRealVoice; const isVoice = !this.voiceAsMusic && isRealVoice;
const isOutgoing = this.message.pFlags.is_outgoing; const isOutgoing = this.message.pFlags.is_outgoing;
@ -395,7 +424,7 @@ export default class AudioElement extends HTMLElement {
const onLoad = this.onLoad = (autoload: boolean) => { const onLoad = this.onLoad = (autoload: boolean) => {
this.onLoad = undefined; this.onLoad = undefined;
const audio = this.audio = appMediaPlaybackController.addMedia(this.message.peerId, this.message.media.document || this.message.media.webpage.document, this.message.mid, autoload); const audio = this.audio = appMediaPlaybackController.addMedia(this.message, autoload);
this.readyPromise = deferredPromise<void>(); this.readyPromise = deferredPromise<void>();
if(this.audio.readyState >= 2) this.readyPromise.resolve(); if(this.audio.readyState >= 2) this.readyPromise.resolve();
@ -421,18 +450,7 @@ export default class AudioElement extends HTMLElement {
if(paused) { if(paused) {
if(appMediaPlaybackController.setSearchContext(this.searchContext)) { if(appMediaPlaybackController.setSearchContext(this.searchContext)) {
let prev: MediaItem[], next: MediaItem[]; const [prev, next] = findAudioTargets(this, this.searchContext.useSearch);
const container = findUpClassName(this, this.classList.contains('search-super-item') ? 'tabs-tab' : 'bubbles-inner');
if(container) {
const elements = Array.from(container.querySelectorAll('.audio' + (isVoice ? '.is-voice' : ''))) as AudioElement[];
const idx = elements.indexOf(this);
const mediaItems: MediaItem[] = elements.map(element => ({peerId: +element.dataset.peerId, mid: +element.dataset.mid}));
prev = mediaItems.slice(0, idx);
next = mediaItems.slice(idx + 1);
}
appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next); appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next);
} }
@ -493,7 +511,7 @@ export default class AudioElement extends HTMLElement {
return; return;
} }
appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid); appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid, this.message.pFlags.is_scheduled);
const onDownloadInit = () => { const onDownloadInit = () => {
if(shouldPlay) { if(shouldPlay) {
@ -591,6 +609,7 @@ export default class AudioElement extends HTMLElement {
} }
} }
} else if(uploading) { } else if(uploading) {
this.dataset.isOutgoing = '1';
this.preloader.attach(downloadDiv, false); this.preloader.attach(downloadDiv, false);
//onLoad(); //onLoad();
} }

3
src/components/avatar.ts

@ -7,7 +7,6 @@
import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appProfileManager from "../lib/appManagers/appProfileManager"; import appProfileManager from "../lib/appManagers/appProfileManager";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import AppMediaViewer, { AppMediaViewerAvatar } from "./appMediaViewer";
import { Message } from "../layer"; import { Message } from "../layer";
import appPeersManager from "../lib/appManagers/appPeersManager"; import appPeersManager from "../lib/appManagers/appPeersManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager"; import appPhotosManager from "../lib/appManagers/appPhotosManager";
@ -15,6 +14,8 @@ import type { LazyLoadQueueIntersector } from "./lazyLoadQueue";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import appAvatarsManager from "../lib/appManagers/appAvatarsManager"; import appAvatarsManager from "../lib/appManagers/appAvatarsManager";
import AppMediaViewer from "./appMediaViewer";
import AppMediaViewerAvatar from "./appMediaViewerAvatar";
const onAvatarUpdate = (peerId: number) => { const onAvatarUpdate = (peerId: number) => {
appAvatarsManager.removeFromAvatarsCache(peerId); appAvatarsManager.removeFromAvatarsCache(peerId);

8
src/components/buttonIcon.ts

@ -6,8 +6,12 @@
import Button from "./button"; import Button from "./button";
const ButtonIcon = (className: string, options: Partial<{noRipple: true, onlyMobile: true, asDiv: boolean}> = {}) => { const ButtonIcon = (className?: string, options: Partial<{noRipple: true, onlyMobile: true, asDiv: boolean}> = {}) => {
const button = Button('btn-icon', {icon: className, ...options}); const button = Button('btn-icon', {
icon: className || undefined,
...options
});
return button; return button;
}; };

80
src/components/chat/audio.ts

@ -17,46 +17,84 @@ import replaceContent from "../../helpers/dom/replaceContent";
import PeerTitle from "../peerTitle"; import PeerTitle from "../peerTitle";
import { i18n } from "../../lib/langPack"; import { i18n } from "../../lib/langPack";
import { formatFullSentTime } from "../../helpers/date"; import { formatFullSentTime } from "../../helpers/date";
import { MediaProgressLine, VolumeSelector } from "../../lib/mediaPlayer";
import ButtonIcon from "../buttonIcon";
export default class ChatAudio extends PinnedContainer { export default class ChatAudio extends PinnedContainer {
private toggleEl: HTMLElement; private toggleEl: HTMLElement;
private progressLine: MediaProgressLine;
private volumeSelector: VolumeSelector;
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager) { constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager) {
super( super({
topbar, topbar,
chat, chat,
topbar.listenerSetter, listenerSetter: topbar.listenerSetter,
'audio', className: 'audio',
new DivAndCaption( divAndCaption: new DivAndCaption(
'pinned-audio', 'pinned-audio',
(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment) => { (title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment) => {
replaceContent(this.divAndCaption.title, title); replaceContent(this.divAndCaption.title, title);
replaceContent(this.divAndCaption.subtitle, subtitle); replaceContent(this.divAndCaption.subtitle, subtitle);
} }
), ),
() => { onClose: () => {
if(this.toggleEl.classList.contains('flip-icon')) { appMediaPlaybackController.stop();
appMediaPlaybackController.toggle(); },
} floating: true
} });
);
this.divAndCaption.border.remove(); this.divAndCaption.border.remove();
this.toggleEl = document.createElement('button'); const prevEl = ButtonIcon('pprevious active', {noRipple: true});
this.toggleEl.classList.add('pinned-audio-ico', 'tgico', 'btn-icon'); const nextEl = ButtonIcon('nnext active', {noRipple: true});
attachClickEvent(this.toggleEl, (e) => {
cancelEvent(e); prevEl.innerHTML = `<svg class="missing-icon" viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet"><g><path class="missing-icon-path" d="M6 6h2v12H6zm3.5 6l8.5 6V6z"></path></g></svg>`;
nextEl.innerHTML = `<svg class="missing-icon" viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet"><g><path class="missing-icon-path" d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"></path></g></svg>`;
const attachClick = (elem: HTMLElement, callback: () => void) => {
attachClickEvent(elem, (e) => {
cancelEvent(e);
callback();
}, {listenerSetter: this.topbar.listenerSetter});
};
attachClick(prevEl, () => {
appMediaPlaybackController.previous();
});
attachClick(nextEl, () => {
appMediaPlaybackController.next();
});
this.toggleEl = ButtonIcon('', {noRipple: true});
this.toggleEl.classList.add('active', 'pinned-audio-ico', 'tgico');
attachClick(this.toggleEl, () => {
appMediaPlaybackController.toggle(); appMediaPlaybackController.toggle();
}, {listenerSetter: this.topbar.listenerSetter}); });
this.wrapper.prepend(this.wrapper.firstElementChild, prevEl, this.toggleEl, nextEl);
this.volumeSelector = new VolumeSelector(this.listenerSetter, true);
const volumeProgressLineContainer = document.createElement('div');
volumeProgressLineContainer.classList.add('progress-line-container');
volumeProgressLineContainer.append(this.volumeSelector.container);
const tunnel = document.createElement('div');
tunnel.classList.add('pinned-audio-volume-tunnel');
this.volumeSelector.btn.classList.add('pinned-audio-volume', 'active');
this.volumeSelector.btn.prepend(tunnel);
this.volumeSelector.btn.append(volumeProgressLineContainer);
this.wrapperUtils.prepend(this.volumeSelector.btn);
this.wrapper.prepend(this.toggleEl); const progressWrapper = document.createElement('div');
progressWrapper.classList.add('pinned-audio-progress-wrapper');
this.topbar.listenerSetter.add(rootScope)('audio_play', (e) => { this.progressLine = new MediaProgressLine(undefined, undefined, true, true);
const {doc, mid, peerId} = e; this.progressLine.container.classList.add('pinned-audio-progress');
progressWrapper.append(this.progressLine.container);
this.wrapper.insertBefore(progressWrapper, this.wrapperUtils);
this.topbar.listenerSetter.add(rootScope)('media_play', ({doc, message, media}) => {
let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment; let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment;
const message = appMessagesManager.getMessageByPeer(peerId, mid);
if(doc.type === 'voice' || doc.type === 'round') { if(doc.type === 'voice' || doc.type === 'round') {
title = new PeerTitle({peerId: message.fromId}).element; title = new PeerTitle({peerId: message.fromId}).element;
@ -67,12 +105,14 @@ export default class ChatAudio extends PinnedContainer {
subtitle = doc.audioPerformer || i18n('AudioUnknownArtist'); subtitle = doc.audioPerformer || i18n('AudioUnknownArtist');
} }
this.progressLine.setMedia(media);
this.fill(title, subtitle, message); this.fill(title, subtitle, message);
this.toggleEl.classList.add('flip-icon'); this.toggleEl.classList.add('flip-icon');
this.toggle(false); this.toggle(false);
}); });
this.topbar.listenerSetter.add(rootScope)('audio_pause', () => { this.topbar.listenerSetter.add(rootScope)('media_pause', () => {
this.toggleEl.classList.remove('flip-icon'); this.toggleEl.classList.remove('flip-icon');
}); });
} }

93
src/components/chat/bubbles.ts

@ -21,7 +21,6 @@ import { getObjectKeysAndSort } from "../../helpers/object";
import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport"; import { IS_TOUCH_SUPPORTED } from "../../environment/touchSupport";
import { logger } from "../../lib/logger"; import { logger } from "../../lib/logger";
import rootScope, { BroadcastEvents } from "../../lib/rootScope"; import rootScope, { BroadcastEvents } from "../../lib/rootScope";
import AppMediaViewer from "../appMediaViewer";
import BubbleGroups from "./bubbleGroups"; import BubbleGroups from "./bubbleGroups";
import PopupDatePicker from "../popups/datePicker"; import PopupDatePicker from "../popups/datePicker";
import PopupForward from "../popups/forward"; import PopupForward from "../popups/forward";
@ -75,6 +74,7 @@ import { formatNumber } from "../../helpers/number";
import { SEND_WHEN_ONLINE_TIMESTAMP } from "../../lib/mtproto/constants"; import { SEND_WHEN_ONLINE_TIMESTAMP } from "../../lib/mtproto/constants";
import windowSize from "../../helpers/windowSize"; import windowSize from "../../helpers/windowSize";
import { formatPhoneNumber } from "../../helpers/formatPhoneNumber"; import { formatPhoneNumber } from "../../helpers/formatPhoneNumber";
import AppMediaViewer from "../appMediaViewer";
const USE_MEDIA_TAILS = false; const USE_MEDIA_TAILS = false;
const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([ const IGNORE_ACTIONS: Set<Message.messageService['action']['_']> = new Set([
@ -209,7 +209,7 @@ export default class ChatBubbles {
// will call when sent for update pos // will call when sent for update pos
this.listenerSetter.add(rootScope)('history_update', ({storage, peerId, mid}) => { this.listenerSetter.add(rootScope)('history_update', ({storage, peerId, mid}) => {
if(mid && peerId === this.peerId && this.chat.getMessagesStorage() === storage) { if(this.chat.getMessagesStorage() === storage) {
const bubble = this.bubbles[mid]; const bubble = this.bubbles[mid];
if(!bubble) return; if(!bubble) return;
@ -294,12 +294,11 @@ export default class ChatBubbles {
if(message.media?.document) { if(message.media?.document) {
const element = bubble.querySelector(`audio-element[data-mid="${tempId}"], .document[data-doc-id="${tempId}"], .media-round[data-mid="${tempId}"]`) as HTMLElement; const element = bubble.querySelector(`audio-element[data-mid="${tempId}"], .document[data-doc-id="${tempId}"], .media-round[data-mid="${tempId}"]`) as HTMLElement;
if(element) { if(element) {
if(element.classList.contains('media-round')) { if(element instanceof AudioElement || element.classList.contains('media-round')) {
element.dataset.mid = '' + mid; element.dataset.mid = '' + message.mid;
} else if(element instanceof AudioElement) { delete element.dataset.isOutgoing;
element.dataset.mid = '' + mid; (element as any).message = message;
element.message = message; (element as any).onLoad(true);
element.onLoad(true);
} else { } else {
element.dataset.docId = message.media.document.id; element.dataset.docId = message.media.document.id;
} }
@ -363,7 +362,7 @@ export default class ChatBubbles {
this.listenerSetter.add(rootScope)('message_edit', ({storage, peerId, mid}) => { this.listenerSetter.add(rootScope)('message_edit', ({storage, peerId, mid}) => {
// fastRaf(() => { // fastRaf(() => {
if(peerId !== this.peerId || storage !== this.chat.getMessagesStorage()) return; if(storage !== this.chat.getMessagesStorage()) return;
const message = this.chat.getMessage(mid); const message = this.chat.getMessage(mid);
const mounted = message.grouped_id ? this.getGroupedBubble(message.grouped_id) : this.getMountedBubble(mid); const mounted = message.grouped_id ? this.getGroupedBubble(message.grouped_id) : this.getMountedBubble(mid);
if(!mounted) return; if(!mounted) return;
@ -499,10 +498,8 @@ export default class ChatBubbles {
public constructPeerHelpers() { public constructPeerHelpers() {
// will call when message is sent (only 1) // will call when message is sent (only 1)
this.listenerSetter.add(rootScope)('history_append', (e) => { this.listenerSetter.add(rootScope)('history_append', ({storage, mid}) => {
const {peerId, storage, mid} = e; if(storage !== this.chat.getMessagesStorage()) return;
if(peerId !== this.peerId || storage !== this.chat.getMessagesStorage()) return;
if(!this.scrollable.loadedAll.bottom) { if(!this.scrollable.loadedAll.bottom) {
this.chat.setMessageId(); this.chat.setMessageId();
@ -517,26 +514,20 @@ export default class ChatBubbles {
this.renderNewMessagesByIds(msgIds); this.renderNewMessagesByIds(msgIds);
}); });
this.listenerSetter.add(rootScope)('history_delete', (e) => { this.listenerSetter.add(rootScope)('history_delete', ({peerId, msgs}) => {
const {peerId, msgs} = e;
if(peerId === this.peerId) { if(peerId === this.peerId) {
this.deleteMessagesByIds(Array.from(msgs)); this.deleteMessagesByIds(Array.from(msgs));
} }
}); });
this.listenerSetter.add(rootScope)('dialog_unread', (e) => { this.listenerSetter.add(rootScope)('dialog_unread', ({peerId}) => {
const info = e; if(peerId === this.peerId) {
if(info.peerId === this.peerId) {
this.chat.input.setUnreadCount(); this.chat.input.setUnreadCount();
this.updateUnreadByDialog(); this.updateUnreadByDialog();
} }
}); });
this.listenerSetter.add(rootScope)('dialogs_multiupdate', (e) => { this.listenerSetter.add(rootScope)('dialogs_multiupdate', (dialogs) => {
const dialogs = e;
if(dialogs[this.peerId]) { if(dialogs[this.peerId]) {
this.chat.input.setUnreadCount(); this.chat.input.setUnreadCount();
} }
@ -548,8 +539,7 @@ export default class ChatBubbles {
} }
}); });
this.listenerSetter.add(rootScope)('chat_update', (e) => { this.listenerSetter.add(rootScope)('chat_update', (chatId) => {
const chatId: number = e;
if(this.peerId === -chatId) { if(this.peerId === -chatId) {
const hadRights = this.chatInner.classList.contains('has-rights'); const hadRights = this.chatInner.classList.contains('has-rights');
const hasRights = this.appMessagesManager.canSendToPeer(this.peerId, this.chat.threadId); const hasRights = this.appMessagesManager.canSendToPeer(this.peerId, this.chat.threadId);
@ -586,16 +576,16 @@ export default class ChatBubbles {
} }
}); });
this.listenerSetter.add(rootScope)('message_views', (e) => { this.listenerSetter.add(rootScope)('message_views', ({peerId, views, mid}) => {
if(this.peerId !== e.peerId) return; if(this.peerId !== peerId) return;
fastRaf(() => { fastRaf(() => {
const bubble = this.bubbles[e.mid]; const bubble = this.bubbles[mid];
if(!bubble) return; if(!bubble) return;
const postViewsElements = Array.from(bubble.querySelectorAll('.post-views')) as HTMLElement[]; const postViewsElements = Array.from(bubble.querySelectorAll('.post-views')) as HTMLElement[];
if(postViewsElements.length) { if(postViewsElements.length) {
const str = formatNumber(e.views, 1); const str = formatNumber(views, 1);
let different = false; let different = false;
postViewsElements.forEach(postViews => { postViewsElements.forEach(postViews => {
if(different || postViews.innerHTML !== str) { if(different || postViews.innerHTML !== str) {
@ -813,19 +803,17 @@ export default class ChatBubbles {
public constructScheduledHelpers() { public constructScheduledHelpers() {
const onUpdate = () => { const onUpdate = () => {
this.chat.topbar.setTitle(Object.keys(this.appMessagesManager.getScheduledMessagesStorage(this.peerId)).length); this.chat.topbar.setTitle(this.appMessagesManager.getScheduledMessagesStorage(this.peerId).size);
}; };
this.listenerSetter.add(rootScope)('scheduled_new', (e) => { this.listenerSetter.add(rootScope)('scheduled_new', ({peerId, mid}) => {
const {peerId, mid} = e;
if(peerId !== this.peerId) return; if(peerId !== this.peerId) return;
this.renderNewMessagesByIds([mid]); this.renderNewMessagesByIds([mid]);
onUpdate(); onUpdate();
}); });
this.listenerSetter.add(rootScope)('scheduled_delete', (e) => { this.listenerSetter.add(rootScope)('scheduled_delete', ({peerId, mids}) => {
const {peerId, mids} = e;
if(peerId !== this.peerId) return; if(peerId !== this.peerId) return;
this.deleteMessagesByIds(mids); this.deleteMessagesByIds(mids);
@ -1047,7 +1035,9 @@ export default class ChatBubbles {
.setSearchContext({ .setSearchContext({
threadId: this.chat.threadId, threadId: this.chat.threadId,
peerId: this.peerId, peerId: this.peerId,
inputFilter: {_: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo'} inputFilter: {_: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo'},
useSearch: this.chat.type !== 'scheduled',
isScheduled: this.chat.type === 'scheduled'
}) })
.openMedia(message, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1)); .openMedia(message, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1));
@ -1167,9 +1157,9 @@ export default class ChatBubbles {
public getGroupedBubble(groupId: string) { public getGroupedBubble(groupId: string) {
const group = this.appMessagesManager.groupedMessagesStorage[groupId]; const group = this.appMessagesManager.groupedMessagesStorage[groupId];
for(const mid in group) { for(const [mid] of group) {
if(this.bubbles[mid]) { if(this.bubbles[mid]) {
const maxId = Math.max(...Object.keys(group).map(id => +id)); // * because in scheduled album can be rendered by lowest mid during sending const maxId = Math.max(...group.keys()); // * because in scheduled album can be rendered by lowest mid during sending
return { return {
bubble: this.bubbles[mid], bubble: this.bubbles[mid],
mid: +mid, mid: +mid,
@ -1425,7 +1415,13 @@ export default class ChatBubbles {
//this.log('renderNewMessagesByIDs: messagesQueuePromise after', this.scrollable.isScrolledDown); //this.log('renderNewMessagesByIDs: messagesQueuePromise after', this.scrollable.isScrolledDown);
//this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true, 5000); //this.scrollable.scrollTo(this.scrollable.scrollHeight, 'top', true, true, 5000);
//const bubble = this.bubbles[Math.max(...mids)]; //const bubble = this.bubbles[Math.max(...mids)];
this.scrollToBubbleEnd();
let bubble: HTMLElement;
if(this.chat.type === 'scheduled') {
bubble = this.bubbles[Math.max(...mids)];
}
this.scrollToBubbleEnd(bubble);
//this.scrollable.scrollIntoViewNew(this.chatInner, 'end'); //this.scrollable.scrollIntoViewNew(this.chatInner, 'end');
@ -2142,7 +2138,7 @@ export default class ChatBubbles {
if(message.deleted) return; if(message.deleted) return;
else if(message.grouped_id && albumMustBeRenderedFull) { // will render only last album's message else if(message.grouped_id && albumMustBeRenderedFull) { // will render only last album's message
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
const maxId = Math.max(...Object.keys(storage).map(i => +i)); const maxId = Math.max(...storage.keys());
if(message.mid < maxId) { if(message.mid < maxId) {
return; return;
} }
@ -2508,7 +2504,7 @@ export default class ChatBubbles {
bubble.classList.add('photo'); bubble.classList.add('photo');
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
if(message.grouped_id && Object.keys(storage).length !== 1 && albumMustBeRenderedFull) { if(message.grouped_id && storage.size !== 1 && albumMustBeRenderedFull) {
bubble.classList.add('is-album', 'is-grouped'); bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({ wrapAlbum({
groupId: message.grouped_id, groupId: message.grouped_id,
@ -2591,7 +2587,8 @@ export default class ChatBubbles {
const docDiv = wrapDocument({ const docDiv = wrapDocument({
message, message,
noAutoDownload: this.chat.noAutoDownloadMedia, noAutoDownload: this.chat.noAutoDownloadMedia,
lazyLoadQueue: this.lazyLoadQueue lazyLoadQueue: this.lazyLoadQueue,
loadPromises
}); });
preview.append(docDiv); preview.append(docDiv);
preview.classList.add('preview-with-document'); preview.classList.add('preview-with-document');
@ -2739,7 +2736,7 @@ export default class ChatBubbles {
bubble.classList.add(isRound ? 'round' : 'video'); bubble.classList.add(isRound ? 'round' : 'video');
const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id]; const storage = this.appMessagesManager.groupedMessagesStorage[message.grouped_id];
if(message.grouped_id && Object.keys(storage).length !== 1 && albumMustBeRenderedFull) { if(message.grouped_id && storage.size !== 1 && albumMustBeRenderedFull) {
bubble.classList.add('is-album', 'is-grouped'); bubble.classList.add('is-album', 'is-grouped');
wrapAlbum({ wrapAlbum({
@ -2771,8 +2768,10 @@ export default class ChatBubbles {
searchContext: isRound ? { searchContext: isRound ? {
peerId: this.peerId, peerId: this.peerId,
inputFilter: {_: 'inputMessagesFilterRoundVoice'}, inputFilter: {_: 'inputMessagesFilterRoundVoice'},
threadId: this.chat.threadId threadId: this.chat.threadId,
} : undefined useSearch: !message.pFlags.is_scheduled,
isScheduled: message.pFlags.is_scheduled
} : undefined,
}); });
} }
} else { } else {
@ -2788,8 +2787,10 @@ export default class ChatBubbles {
searchContext: doc.type === 'voice' || doc.type === 'audio' ? { searchContext: doc.type === 'voice' || doc.type === 'audio' ? {
peerId: this.peerId, peerId: this.peerId,
inputFilter: {_: doc.type === 'voice' ? 'inputMessagesFilterRoundVoice' : 'inputMessagesFilterMusic'}, inputFilter: {_: doc.type === 'voice' ? 'inputMessagesFilterRoundVoice' : 'inputMessagesFilterMusic'},
threadId: this.chat.threadId threadId: this.chat.threadId,
} : undefined useSearch: !message.pFlags.is_scheduled,
isScheduled: message.pFlags.is_scheduled
} : undefined,
}); });
if(newNameContainer) { if(newNameContainer) {

75
src/components/chat/pinnedContainer.ts

@ -12,50 +12,61 @@ import { ripple } from "../ripple";
import ListenerSetter from "../../helpers/listenerSetter"; import ListenerSetter from "../../helpers/listenerSetter";
import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../helpers/dom/clickEvent";
import { safeAssign } from "../../helpers/object";
import { Message } from "../../layer";
//const classNames: string[] = [];
const classNames: string[] = ['is-pinned-message-shown', 'is-pinned-audio-shown']; const classNames: string[] = ['is-pinned-message-shown', 'is-pinned-audio-shown'];
const CLASSNAME_BASE = 'pinned-container'; const CLASSNAME_BASE = 'pinned-container';
const HEIGHT = 52; const HEIGHT = 52;
export default class PinnedContainer { export default class PinnedContainer {
private close: HTMLElement; public wrapperUtils: HTMLElement;
public btnClose: HTMLElement;
protected wrapper: HTMLElement; protected wrapper: HTMLElement;
constructor( protected topbar: ChatTopbar;
protected topbar: ChatTopbar, protected chat: Chat;
protected chat: Chat, protected listenerSetter: ListenerSetter;
public listenerSetter: ListenerSetter, public className: string;
protected className: string, public divAndCaption: DivAndCaption<(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment, message?: any) => void>;
public divAndCaption: DivAndCaption<(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment, message?: any) => void>,
onClose?: () => void | Promise<boolean>
) {
/* const prev = this.divAndCaption.fill;
this.divAndCaption.fill = (mid, title, subtitle) => {
this.divAndCaption.container.dataset.mid = '' + mid;
prev(mid, title, subtitle);
}; */
//classNames.push(`is-pinned-${className}-shown`);
protected floating = false;
protected onClose?: () => void | Promise<boolean>;
constructor(options: {
topbar: PinnedContainer['topbar'],
chat: PinnedContainer['chat'],
listenerSetter: PinnedContainer['listenerSetter'],
className: PinnedContainer['className'],
divAndCaption: PinnedContainer['divAndCaption'],
onClose?: PinnedContainer['onClose'],
floating?: PinnedContainer['floating']
}) {
safeAssign(this, options);
const {divAndCaption, className, onClose} = this;
divAndCaption.container.classList.add(CLASSNAME_BASE, 'hide'); divAndCaption.container.classList.add(CLASSNAME_BASE, 'hide');
divAndCaption.title.classList.add(CLASSNAME_BASE + '-title'); divAndCaption.title.classList.add(CLASSNAME_BASE + '-title');
divAndCaption.subtitle.classList.add(CLASSNAME_BASE + '-subtitle'); divAndCaption.subtitle.classList.add(CLASSNAME_BASE + '-subtitle');
divAndCaption.content.classList.add(CLASSNAME_BASE + '-content'); divAndCaption.content.classList.add(CLASSNAME_BASE + '-content');
this.close = document.createElement('button'); this.btnClose = document.createElement('button');
this.close.classList.add(CLASSNAME_BASE + '-close', `pinned-${className}-close`, 'btn-icon', 'tgico-close'); this.btnClose.classList.add(CLASSNAME_BASE + '-close', `pinned-${className}-close`, 'btn-icon', 'tgico-close');
//divAndCaption.container.prepend(this.close);
this.wrapper = document.createElement('div'); this.wrapper = document.createElement('div');
this.wrapper.classList.add(CLASSNAME_BASE + '-wrapper'); this.wrapper.classList.add(CLASSNAME_BASE + '-wrapper');
this.wrapper.append(...Array.from(divAndCaption.container.children));
ripple(this.wrapper); ripple(this.wrapper);
divAndCaption.container.append(this.close, this.wrapper); this.wrapperUtils = document.createElement('div');
this.wrapperUtils.classList.add(CLASSNAME_BASE + '-wrapper-utils');
this.wrapperUtils.append(this.btnClose);
this.wrapper.append(...Array.from(divAndCaption.container.children), this.wrapperUtils);
attachClickEvent(this.close, (e) => { divAndCaption.container.append(this.wrapper/* , this.close */);
attachClickEvent(this.btnClose, (e) => {
cancelEvent(e); cancelEvent(e);
((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => { ((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => {
@ -74,15 +85,16 @@ export default class PinnedContainer {
return; return;
} }
this.divAndCaption.container.classList.toggle('is-floating', mediaSizes.isMobile);
this.topbar.container.classList.toggle('is-pinned-floating', mediaSizes.isMobile);
const scrollable = this.chat.bubbles.scrollable; const scrollable = this.chat.bubbles.scrollable;
const scrollTop = mediaSizes.isMobile /* && !appImManager.scrollable.isScrolledDown */ ? scrollable.scrollTop : undefined; const isFloating = (this.floating || mediaSizes.isMobile) && !hide;
const scrollTop = isFloating || this.divAndCaption.container.classList.contains('is-floating') ? scrollable.scrollTop : undefined;
this.divAndCaption.container.classList.toggle('is-floating', isFloating);
this.divAndCaption.container.classList.toggle('hide', hide); this.divAndCaption.container.classList.toggle('hide', hide);
const className = `is-pinned-${this.className}-shown`;
this.topbar.container.classList.toggle(className, !hide); this.topbar.container.classList.toggle('is-pinned-floating', isFloating);
this.topbar.container.classList.toggle(`is-pinned-${this.className}-shown`, !hide);
const active = classNames.filter(className => this.topbar.container.classList.contains(className)); const active = classNames.filter(className => this.topbar.container.classList.contains(className));
const maxActive = hide ? 0 : 1; const maxActive = hide ? 0 : 1;
@ -91,10 +103,11 @@ export default class PinnedContainer {
scrollable.scrollTop = scrollTop + ((hide ? -1 : 1) * HEIGHT); scrollable.scrollTop = scrollTop + ((hide ? -1 : 1) * HEIGHT);
} }
this.topbar.setFloating();
this.topbar.setUtilsWidth(); this.topbar.setUtilsWidth();
} }
public fill(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment, message: any) { public fill(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment, message: Message.message) {
this.divAndCaption.container.dataset.peerId = '' + message.peerId; this.divAndCaption.container.dataset.peerId = '' + message.peerId;
this.divAndCaption.container.dataset.mid = '' + message.mid; this.divAndCaption.container.dataset.mid = '' + message.mid;
this.divAndCaption.fill(title, subtitle, message); this.divAndCaption.fill(title, subtitle, message);

43
src/components/chat/pinnedMessage.ts

@ -257,40 +257,49 @@ export default class ChatPinnedMessage {
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) { constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
this.pinnedMessageContainer = new PinnedContainer(topbar, chat, this.listenerSetter, 'message', new ReplyContainer('pinned-message'), async() => { const dAC = new ReplyContainer('pinned-message');
if(appPeersManager.canPinMessage(this.topbar.peerId)) { this.pinnedMessageContainer = new PinnedContainer({
new PopupPinMessage(this.topbar.peerId, this.pinnedMid, true); topbar,
} else { chat,
new PopupPinMessage(this.topbar.peerId, 0, true); listenerSetter: this.listenerSetter,
} className: 'message',
divAndCaption: dAC,
onClose: async() => {
if(appPeersManager.canPinMessage(this.topbar.peerId)) {
new PopupPinMessage(this.topbar.peerId, this.pinnedMid, true);
} else {
new PopupPinMessage(this.topbar.peerId, 0, true);
}
return false; return false;
}
}); });
this.pinnedMessageBorder = new PinnedMessageBorder(); this.pinnedMessageBorder = new PinnedMessageBorder();
this.pinnedMessageContainer.divAndCaption.border.replaceWith(this.pinnedMessageBorder.render(1, 0)); dAC.border.replaceWith(this.pinnedMessageBorder.render(1, 0));
this.animatedSubtitle = new AnimatedSuper(); this.animatedSubtitle = new AnimatedSuper();
this.pinnedMessageContainer.divAndCaption.subtitle.append(this.animatedSubtitle.container); dAC.subtitle.append(this.animatedSubtitle.container);
this.animatedMedia = new AnimatedSuper(); this.animatedMedia = new AnimatedSuper();
this.animatedMedia.container.classList.add('pinned-message-media-container'); this.animatedMedia.container.classList.add('pinned-message-media-container');
this.pinnedMessageContainer.divAndCaption.content.prepend(this.animatedMedia.container); dAC.content.prepend(this.animatedMedia.container);
this.animatedCounter = new AnimatedCounter(true); this.animatedCounter = new AnimatedCounter(true);
this.pinnedMessageContainer.divAndCaption.title.append(i18n('PinnedMessage'), ' ', this.animatedCounter.container); dAC.title.append(i18n('PinnedMessage'), ' ', this.animatedCounter.container);
dAC.container.prepend(this.pinnedMessageContainer.btnClose);
this.btnOpen = ButtonIcon('pinlist pinned-container-close pinned-message-pinlist', {noRipple: true}); this.btnOpen = ButtonIcon('pinlist pinned-container-close pinned-message-pinlist', {noRipple: true});
this.pinnedMessageContainer.divAndCaption.container.prepend(this.btnOpen);
this.pinnedMessageContainer.wrapperUtils.prepend(this.btnOpen);
attachClickEvent(this.btnOpen, (e) => { attachClickEvent(this.btnOpen, (e) => {
cancelEvent(e); cancelEvent(e);
this.topbar.openPinned(true); this.topbar.openPinned(true);
}, {listenerSetter: this.listenerSetter}); }, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope)('peer_pinned_messages', (e) => { this.listenerSetter.add(rootScope)('peer_pinned_messages', ({peerId}) => {
const peerId = e.peerId;
if(peerId === this.topbar.peerId) { if(peerId === this.topbar.peerId) {
//this.wasPinnedIndex = 0; //this.wasPinnedIndex = 0;
//setTimeout(() => { //setTimeout(() => {
@ -310,9 +319,7 @@ export default class ChatPinnedMessage {
} }
}); });
this.listenerSetter.add(rootScope)('peer_pinned_hidden', (e) => { this.listenerSetter.add(rootScope)('peer_pinned_hidden', ({peerId}) => {
const {peerId, maxId} = e;
if(peerId === this.topbar.peerId) { if(peerId === this.topbar.peerId) {
this.pinnedMessageContainer.toggle(this.hidden = true); this.pinnedMessageContainer.toggle(this.hidden = true);
} }

12
src/components/chat/selection.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type { AppMessagesManager } from "../../lib/appManagers/appMessagesManager"; import type { AppMessagesManager, MessagesStorage } from "../../lib/appManagers/appMessagesManager";
import type ChatBubbles from "./bubbles"; import type ChatBubbles from "./bubbles";
import type ChatInput from "./input"; import type ChatInput from "./input";
import type Chat from "./chat"; import type Chat from "./chat";
@ -51,6 +51,7 @@ class AppSelection {
protected listenerSetter: ListenerSetter; protected listenerSetter: ListenerSetter;
protected appMessagesManager: AppMessagesManager; protected appMessagesManager: AppMessagesManager;
protected isScheduled: boolean;
protected listenElement: HTMLElement; protected listenElement: HTMLElement;
protected onToggleSelection: (forwards: boolean) => void; protected onToggleSelection: (forwards: boolean) => void;
@ -79,7 +80,8 @@ class AppSelection {
verifyTouchLongPress?: AppSelection['verifyTouchLongPress'], verifyTouchLongPress?: AppSelection['verifyTouchLongPress'],
targetLookupClassName: string, targetLookupClassName: string,
lookupBetweenParentClassName: string, lookupBetweenParentClassName: string,
lookupBetweenElementsQuery: string lookupBetweenElementsQuery: string,
isScheduled?: AppSelection['isScheduled']
}) { }) {
safeAssign(this, options); safeAssign(this, options);
@ -326,8 +328,9 @@ class AppSelection {
cantDelete = !size, cantDelete = !size,
cantSend = !size; cantSend = !size;
for(const [peerId, mids] of this.selectedMids) { for(const [peerId, mids] of this.selectedMids) {
const storage = this.isScheduled ? this.appMessagesManager.getScheduledMessagesStorage(peerId) : this.appMessagesManager.getMessagesStorage(peerId);
for(const mid of mids) { for(const mid of mids) {
const message = this.appMessagesManager.getMessageByPeer(peerId, mid); const message = this.appMessagesManager.getMessageFromStorage(storage, mid);
if(!cantForward) { if(!cantForward) {
if(message.action) { if(message.action) {
cantForward = true; cantForward = true;
@ -690,7 +693,8 @@ export default class ChatSelection extends AppSelection {
verifyTouchLongPress: () => !this.chat.input.recording, verifyTouchLongPress: () => !this.chat.input.recording,
targetLookupClassName: 'bubble', targetLookupClassName: 'bubble',
lookupBetweenParentClassName: 'bubbles-inner', lookupBetweenParentClassName: 'bubbles-inner',
lookupBetweenElementsQuery: '.bubble:not(.is-multiple-documents), .grouped-item' lookupBetweenElementsQuery: '.bubble:not(.is-multiple-documents), .grouped-item',
isScheduled: chat.type === 'scheduled'
}); });
} }

56
src/components/chat/topbar.ts

@ -43,6 +43,7 @@ import PopupPeer from "../popups/peer";
import generateVerifiedIcon from "../generateVerifiedIcon"; import generateVerifiedIcon from "../generateVerifiedIcon";
import { fastRaf } from "../../helpers/schedulers"; import { fastRaf } from "../../helpers/schedulers";
import AppEditContactTab from "../sidebarRight/tabs/editContact"; import AppEditContactTab from "../sidebarRight/tabs/editContact";
import appMediaPlaybackController from "../appMediaPlaybackController";
export default class ChatTopbar { export default class ChatTopbar {
public container: HTMLDivElement; public container: HTMLDivElement;
@ -87,6 +88,7 @@ export default class ChatTopbar {
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('sidebar-header', 'topbar'); this.container.classList.add('sidebar-header', 'topbar');
this.container.dataset.floating = '0';
this.btnBack = ButtonIcon('left sidebar-close-button', {noRipple: true}); this.btnBack = ButtonIcon('left sidebar-close-button', {noRipple: true});
@ -141,16 +143,28 @@ export default class ChatTopbar {
}); });
} }
this.chatUtils.append(...[this.chatAudio ? this.chatAudio.divAndCaption.container : null, this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null, this.btnJoin, this.btnPinned, this.btnMute, this.btnSearch, this.btnMore].filter(Boolean)); this.chatUtils.append(...[
// this.chatAudio ? this.chatAudio.divAndCaption.container : null,
this.pinnedMessage ? this.pinnedMessage.pinnedMessageContainer.divAndCaption.container : null,
this.btnJoin,
this.btnPinned,
this.btnMute,
this.btnSearch,
this.btnMore
].filter(Boolean));
this.container.append(this.btnBack, this.chatInfo, this.chatUtils); this.container.append(this.btnBack, this.chatInfo, this.chatUtils);
if(this.chatAudio) {
this.container.append(this.chatAudio.divAndCaption.container, this.chatUtils);
}
// * construction end // * construction end
// * fix topbar overflow section // * fix topbar overflow section
this.listenerSetter.add(window)('resize', this.onResize); this.listenerSetter.add(window)('resize', this.onResize);
mediaSizes.addEventListener('changeScreen', this.onChangeScreen); this.listenerSetter.add(mediaSizes)('changeScreen', this.onChangeScreen);
attachClickEvent(this.container, (e) => { attachClickEvent(this.container, (e) => {
const container: HTMLElement = findUpClassName(e.target, 'pinned-container'); const container: HTMLElement = findUpClassName(e.target, 'pinned-container');
@ -158,6 +172,10 @@ export default class ChatTopbar {
if(container) { if(container) {
cancelEvent(e); cancelEvent(e);
if(findUpClassName(e.target, 'progress-line')) {
return;
}
const mid = +container.dataset.mid; const mid = +container.dataset.mid;
const peerId = +container.dataset.peerId; const peerId = +container.dataset.peerId;
if(container.classList.contains('pinned-message')) { if(container.classList.contains('pinned-message')) {
@ -165,7 +183,13 @@ export default class ChatTopbar {
this.pinnedMessage.followPinnedMessage(mid); this.pinnedMessage.followPinnedMessage(mid);
//} //}
} else { } else {
this.chat.appImManager.setInnerPeer(peerId, mid); const searchContext = appMediaPlaybackController.getSearchContext();
this.chat.appImManager.setInnerPeer(
peerId,
mid,
searchContext.isScheduled ? 'scheduled' : (searchContext.threadId ? 'discussion' : undefined),
searchContext.threadId
);
} }
} else { } else {
if(mediaSizes.activeScreen === ScreenSize.medium && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) { if(mediaSizes.activeScreen === ScreenSize.medium && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) {
@ -350,7 +374,7 @@ export default class ChatTopbar {
}, },
verify: () => { verify: () => {
const userFull = this.appProfileManager.usersFull[this.peerId]; const userFull = this.appProfileManager.usersFull[this.peerId];
return this.peerId > 0 && userFull && !userFull.pFlags?.blocked; return this.peerId > 0 && this.peerId !== rootScope.myId && userFull && !userFull.pFlags?.blocked;
} }
}, { }, {
icon: 'lockoff', icon: 'lockoff',
@ -448,17 +472,13 @@ export default class ChatTopbar {
} }
}); });
this.listenerSetter.add(rootScope)('peer_typings', (e) => { this.listenerSetter.add(rootScope)('peer_typings', ({peerId}) => {
const {peerId} = e;
if(this.peerId === peerId) { if(this.peerId === peerId) {
this.setPeerStatus(); this.setPeerStatus();
} }
}); });
this.listenerSetter.add(rootScope)('user_update', (e) => { this.listenerSetter.add(rootScope)('user_update', (userId) => {
const userId = e;
if(this.peerId === userId) { if(this.peerId === userId) {
this.setPeerStatus(); this.setPeerStatus();
} }
@ -510,20 +530,20 @@ export default class ChatTopbar {
private onResize = () => { private onResize = () => {
this.setUtilsWidth(true); this.setUtilsWidth(true);
this.setFloating();
}; };
private onChangeScreen = (from: ScreenSize, to: ScreenSize) => { private onChangeScreen = (from: ScreenSize, to: ScreenSize) => {
this.container.classList.toggle('is-pinned-floating', mediaSizes.isMobile); this.container.classList.toggle('is-pinned-floating', mediaSizes.isMobile);
this.chatAudio && this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to === ScreenSize.mobile); // this.chatAudio && this.chatAudio.divAndCaption.container.classList.toggle('is-floating', to === ScreenSize.mobile);
this.pinnedMessage && this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.classList.toggle('is-floating', to === ScreenSize.mobile); this.pinnedMessage && this.pinnedMessage.pinnedMessageContainer.divAndCaption.container.classList.toggle('is-floating', to === ScreenSize.mobile);
this.setUtilsWidth(true); this.onResize();
}; };
public destroy() { public destroy() {
//this.chat.log.error('Topbar destroying'); //this.chat.log.error('Topbar destroying');
this.listenerSetter.removeAll(); this.listenerSetter.removeAll();
mediaSizes.removeEventListener('changeScreen', this.onChangeScreen);
window.clearInterval(this.setPeerStatusInterval); window.clearInterval(this.setPeerStatusInterval);
if(this.pinnedMessage) { if(this.pinnedMessage) {
@ -713,6 +733,16 @@ export default class ChatTopbar {
}); });
}; };
public setFloating = () => {
const containers = [this.chatAudio, this.pinnedMessage && this.pinnedMessage.pinnedMessageContainer].filter(Boolean);
const count = containers.reduce((acc, container) => {
const isFloating = container.divAndCaption.container.classList.contains('is-floating');
this.container.classList.toggle(`is-pinned-${container.className}-floating`, isFloating);
return acc + +isFloating;
}, 0);
this.container.dataset.floating = '' + count;
};
public setPeerStatus = (needClear = false) => { public setPeerStatus = (needClear = false) => {
if(!this.subtitle) return; if(!this.subtitle) return;

357
src/components/peerProfile.ts

@ -0,0 +1,357 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import PARALLAX_SUPPORTED from "../environment/parallaxSupport";
import { copyTextToClipboard } from "../helpers/clipboard";
import replaceContent from "../helpers/dom/replaceContent";
import { fastRaf } from "../helpers/schedulers";
import { User } from "../layer";
import { Channel } from "../lib/appManagers/appChatsManager";
import appImManager from "../lib/appManagers/appImManager";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appNotificationsManager from "../lib/appManagers/appNotificationsManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import appProfileManager from "../lib/appManagers/appProfileManager";
import appUsersManager from "../lib/appManagers/appUsersManager";
import I18n from "../lib/langPack";
import RichTextProcessor from "../lib/richtextprocessor";
import rootScope from "../lib/rootScope";
import AvatarElement from "./avatar";
import CheckboxField from "./checkboxField";
import generateVerifiedIcon from "./generateVerifiedIcon";
import PeerProfileAvatars from "./peerProfileAvatars";
import PeerTitle from "./peerTitle";
import Row from "./row";
import Scrollable from "./scrollable";
import { SettingSection, generateDelimiter } from "./sidebarLeft";
import { toast } from "./toast";
let setText = (text: string, row: Row) => {
//fastRaf(() => {
row.title.innerHTML = text;
row.container.style.display = '';
//});
};
export default class PeerProfile {
public element: HTMLElement;
public avatars: PeerProfileAvatars;
private avatar: AvatarElement;
private section: SettingSection;
private name: HTMLDivElement;
private subtitle: HTMLDivElement;
private bio: Row;
private username: Row;
private phone: Row;
private notifications: Row;
private cleaned: boolean;
private setBioTimeout: number;
private setPeerStatusInterval: number;
private peerId = 0;
private threadId: number;
constructor(public scrollable: Scrollable) {
if(!PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('no-parallax');
}
}
public init() {
this.init = null;
this.element = document.createElement('div');
this.element.classList.add('profile-content');
this.section = new SettingSection({
noDelimiter: true
});
this.avatar = new AvatarElement();
this.avatar.classList.add('profile-avatar', 'avatar-120');
this.avatar.setAttribute('dialog', '1');
this.avatar.setAttribute('clickable', '');
this.name = document.createElement('div');
this.name.classList.add('profile-name');
this.subtitle = document.createElement('div');
this.subtitle.classList.add('profile-subtitle');
this.bio = new Row({
title: ' ',
subtitleLangKey: 'UserBio',
icon: 'info',
clickable: (e) => {
if((e.target as HTMLElement).tagName === 'A') {
return;
}
appProfileManager.getProfileByPeerId(this.peerId).then(full => {
copyTextToClipboard(full.about);
toast(I18n.format('BioCopied', true));
});
}
});
this.bio.title.classList.add('pre-wrap');
this.username = new Row({
title: ' ',
subtitleLangKey: 'Username',
icon: 'username',
clickable: () => {
const peer: Channel | User.user = appPeersManager.getPeer(this.peerId);
copyTextToClipboard('@' + peer.username);
toast(I18n.format('UsernameCopied', true));
}
});
this.phone = new Row({
title: ' ',
subtitleLangKey: 'Phone',
icon: 'phone',
clickable: () => {
const peer: User = appUsersManager.getUser(this.peerId);
copyTextToClipboard('+' + peer.phone);
toast(I18n.format('PhoneCopied', true));
}
});
this.notifications = new Row({
checkboxField: new CheckboxField({toggle: true}),
titleLangKey: 'Notifications',
icon: 'unmute'
});
this.section.content.append(this.phone.container, this.username.container, this.bio.container, this.notifications.container);
this.element.append(this.section.container, generateDelimiter());
this.notifications.checkboxField.input.addEventListener('change', (e) => {
if(!e.isTrusted) {
return;
}
//let checked = this.notificationsCheckbox.checked;
appMessagesManager.mutePeer(this.peerId);
});
rootScope.addEventListener('dialog_notify_settings', (dialog) => {
if(this.peerId === dialog.peerId) {
const muted = appNotificationsManager.isPeerLocalMuted(this.peerId, false);
this.notifications.checkboxField.checked = !muted;
}
});
rootScope.addEventListener('peer_typings', ({peerId}) => {
if(this.peerId === peerId) {
this.setPeerStatus();
}
});
rootScope.addEventListener('peer_bio_edit', (peerId) => {
if(peerId === this.peerId) {
this.setBio(true);
}
});
rootScope.addEventListener('user_update', (userId) => {
if(this.peerId === userId) {
this.setPeerStatus();
}
});
rootScope.addEventListener('contacts_update', (userId) => {
if(this.peerId === userId) {
const user = appUsersManager.getUser(userId);
if(!user.pFlags.self) {
if(user.phone) {
setText(appUsersManager.formatUserPhone(user.phone), this.phone);
} else {
this.phone.container.style.display = 'none';
}
}
}
});
this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3);
}
public setPeerStatus = (needClear = false) => {
if(!this.peerId) return;
const peerId = this.peerId;
appImManager.setPeerStatus(this.peerId, this.subtitle, needClear, true, () => peerId === this.peerId);
};
public cleanupHTML() {
this.bio.container.style.display = 'none';
this.phone.container.style.display = 'none';
this.username.container.style.display = 'none';
this.notifications.container.style.display = '';
this.notifications.checkboxField.checked = true;
if(this.setBioTimeout) {
window.clearTimeout(this.setBioTimeout);
this.setBioTimeout = 0;
}
}
public setAvatar() {
if(this.peerId !== rootScope.myId) {
const photo = appPeersManager.getPeerPhoto(this.peerId);
if(photo) {
const oldAvatars = this.avatars;
this.avatars = new PeerProfileAvatars(this.scrollable);
this.avatars.setPeer(this.peerId);
this.avatars.info.append(this.name, this.subtitle);
this.avatar.remove();
if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container);
else this.element.prepend(this.avatars.container);
if(PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('parallax');
}
return;
}
}
if(PARALLAX_SUPPORTED) {
this.scrollable.container.classList.remove('parallax');
}
if(this.avatars) {
this.avatars.container.remove();
this.avatars = undefined;
}
this.avatar.setAttribute('peer', '' + this.peerId);
this.section.content.prepend(this.avatar, this.name, this.subtitle);
}
public fillProfileElements() {
if(!this.cleaned) return;
this.cleaned = false;
const peerId = this.peerId;
this.cleanupHTML();
this.setAvatar();
// username
if(peerId !== rootScope.myId) {
let username = appPeersManager.getPeerUsername(peerId);
if(username) {
setText(appPeersManager.getPeerUsername(peerId), this.username);
}
const muted = appNotificationsManager.isPeerLocalMuted(peerId, false);
this.notifications.checkboxField.checked = !muted;
} else {
fastRaf(() => {
this.notifications.container.style.display = 'none';
});
}
//let membersLi = this.profileTabs.firstElementChild.children[0] as HTMLLIElement;
if(peerId > 0) {
//membersLi.style.display = 'none';
let user = appUsersManager.getUser(peerId);
if(user.phone && peerId !== rootScope.myId) {
setText(appUsersManager.formatUserPhone(user.phone), this.phone);
}
}/* else {
//membersLi.style.display = appPeersManager.isBroadcast(peerId) ? 'none' : '';
} */
this.setBio();
replaceContent(this.name, new PeerTitle({
peerId,
dialog: true,
}).element);
const peer = appPeersManager.getPeer(peerId);
if(peer?.pFlags?.verified) {
this.name.append(generateVerifiedIcon());
}
this.setPeerStatus(true);
}
public setBio(override?: true) {
if(this.setBioTimeout) {
window.clearTimeout(this.setBioTimeout);
this.setBioTimeout = 0;
}
const peerId = this.peerId;
const threadId = this.threadId;
if(!peerId) {
return;
}
let promise: Promise<boolean>;
if(peerId > 0) {
promise = appProfileManager.getProfile(peerId, override).then(userFull => {
if(this.peerId !== peerId || this.threadId !== threadId) {
//this.log.warn('peer changed');
return false;
}
if(userFull.rAbout && peerId !== rootScope.myId) {
setText(userFull.rAbout, this.bio);
}
//this.log('userFull', userFull);
return true;
});
} else {
promise = appProfileManager.getChatFull(-peerId, override).then((chatFull) => {
if(this.peerId !== peerId || this.threadId !== threadId) {
//this.log.warn('peer changed');
return false;
}
//this.log('chatInfo res 2:', chatFull);
if(chatFull.about) {
setText(RichTextProcessor.wrapRichText(chatFull.about), this.bio);
}
return true;
});
}
promise.then((canSetNext) => {
if(canSetNext) {
this.setBioTimeout = window.setTimeout(() => this.setBio(true), 60e3);
}
});
}
public setPeer(peerId: number, threadId = 0) {
if(this.peerId === peerId && this.threadId === peerId) return;
if(this.init) {
this.init();
}
this.peerId = peerId;
this.threadId = threadId;
this.cleaned = true;
}
}

332
src/components/peerProfileAvatars.ts

@ -0,0 +1,332 @@
import PARALLAX_SUPPORTED from "../environment/parallaxSupport";
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent";
import renderImageFromUrl from "../helpers/dom/renderImageFromUrl";
import filterChatPhotosMessages from "../helpers/filterChatPhotosMessages";
import ListLoader from "../helpers/listLoader";
import { fastRaf } from "../helpers/schedulers";
import { Message, ChatFull, MessageAction, Photo } from "../layer";
import appAvatarsManager from "../lib/appManagers/appAvatarsManager";
import appDownloadManager from "../lib/appManagers/appDownloadManager";
import appMessagesManager, { AppMessagesManager } from "../lib/appManagers/appMessagesManager";
import appPeersManager from "../lib/appManagers/appPeersManager";
import appPhotosManager from "../lib/appManagers/appPhotosManager";
import appProfileManager from "../lib/appManagers/appProfileManager";
import { openAvatarViewer } from "./avatar";
import Scrollable from "./scrollable";
import SwipeHandler from "./swipeHandler";
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)';
public container: HTMLElement;
public avatars: HTMLElement;
public gradient: HTMLElement;
public info: HTMLElement;
public arrowPrevious: HTMLElement;
public arrowNext: HTMLElement;
private tabs: HTMLDivElement;
private listLoader: ListLoader<string | Message.messageService, string | Message.messageService>;
private peerId: number;
constructor(public scrollable: Scrollable) {
this.container = document.createElement('div');
this.container.classList.add(PeerProfileAvatars.BASE_CLASS + '-container');
this.avatars = document.createElement('div');
this.avatars.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatars');
this.gradient = document.createElement('div');
this.gradient.classList.add(PeerProfileAvatars.BASE_CLASS + '-gradient');
this.info = document.createElement('div');
this.info.classList.add(PeerProfileAvatars.BASE_CLASS + '-info');
this.tabs = document.createElement('div');
this.tabs.classList.add(PeerProfileAvatars.BASE_CLASS + '-tabs');
this.arrowPrevious = document.createElement('div');
this.arrowPrevious.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow');
/* const previousIcon = document.createElement('i');
previousIcon.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow-icon', 'tgico-previous');
this.arrowBack.append(previousIcon); */
this.arrowNext = document.createElement('div');
this.arrowNext.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow', PeerProfileAvatars.BASE_CLASS + '-arrow-next');
/* const nextIcon = document.createElement('i');
nextIcon.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow-icon', 'tgico-next');
this.arrowNext.append(nextIcon); */
this.container.append(this.avatars, this.gradient, this.info, this.tabs, this.arrowPrevious, this.arrowNext);
const checkScrollTop = () => {
if(this.scrollable.scrollTop !== 0) {
this.scrollable.scrollIntoViewNew(this.scrollable.container.firstElementChild as HTMLElement, 'start');
return false;
}
return true;
};
const SWITCH_ZONE = 1 / 3;
let cancel = false;
let freeze = false;
attachClickEvent(this.container, async(_e) => {
if(freeze) {
cancelEvent(_e);
return;
}
if(cancel) {
cancel = false;
return;
}
if(!checkScrollTop()) {
return;
}
const rect = this.container.getBoundingClientRect();
// const e = (_e as TouchEvent).touches ? (_e as TouchEvent).touches[0] : _e as MouseEvent;
const e = _e;
const x = e.pageX;
const clickX = x - rect.left;
if((!this.listLoader.previous.length && !this.listLoader.next.length)
|| (clickX > (rect.width * SWITCH_ZONE) && clickX < (rect.width - rect.width * SWITCH_ZONE))) {
const peerId = this.peerId;
const targets: {element: HTMLElement, item: string | Message.messageService}[] = [];
this.listLoader.previous.concat(this.listLoader.current, this.listLoader.next).forEach((item, idx) => {
targets.push({
element: /* null */this.avatars.children[idx] as HTMLElement,
item
});
});
const prevTargets = targets.slice(0, this.listLoader.previous.length);
const nextTargets = targets.slice(this.listLoader.previous.length + 1);
const target = this.avatars.children[this.listLoader.previous.length] as HTMLElement;
freeze = true;
openAvatarViewer(target, peerId, () => peerId === this.peerId, this.listLoader.current, prevTargets, nextTargets);
freeze = false;
} else {
const centerX = rect.right - (rect.width / 2);
const toRight = x > centerX;
// this.avatars.classList.remove('no-transition');
// fastRaf(() => {
this.avatars.classList.add('no-transition');
void this.avatars.offsetLeft; // reflow
let distance: number;
if(this.listLoader.index === 0 && !toRight) distance = this.listLoader.count - 1;
else if(this.listLoader.index === (this.listLoader.count - 1) && toRight) distance = -(this.listLoader.count - 1);
else distance = toRight ? 1 : -1;
this.listLoader.go(distance);
fastRaf(() => {
this.avatars.classList.remove('no-transition');
});
// });
}
});
const cancelNextClick = () => {
cancel = true;
document.body.addEventListener(IS_TOUCH_SUPPORTED ? 'touchend' : 'click', (e) => {
cancel = false;
}, {once: true});
};
let width = 0, x = 0, lastDiffX = 0, lastIndex = 0, minX = 0;
const swipeHandler = new SwipeHandler({
element: this.avatars,
onSwipe: (xDiff, yDiff) => {
lastDiffX = xDiff;
let lastX = x + xDiff * -PeerProfileAvatars.SCALE;
if(lastX > 0) lastX = 0;
else if(lastX < minX) lastX = minX;
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', lastX + 'px');
//console.log(xDiff, yDiff);
return false;
},
verifyTouchTarget: (e) => {
if(!checkScrollTop()) {
cancelNextClick();
cancelEvent(e);
return false;
} else if(this.container.classList.contains('is-single') || freeze) {
return false;
}
return true;
},
onFirstSwipe: () => {
const rect = this.avatars.getBoundingClientRect();
width = rect.width;
minX = -width * (this.tabs.childElementCount - 1);
/* lastIndex = whichChild(this.tabs.querySelector('.active'));
x = -width * lastIndex; */
x = rect.left - this.container.getBoundingClientRect().left;
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', x + 'px');
this.container.classList.add('is-swiping');
this.avatars.classList.add('no-transition');
void this.avatars.offsetLeft; // reflow
},
onReset: () => {
const addIndex = Math.ceil(Math.abs(lastDiffX) / (width / PeerProfileAvatars.SCALE)) * (lastDiffX >= 0 ? 1 : -1);
cancelNextClick();
//console.log(addIndex);
this.avatars.classList.remove('no-transition');
fastRaf(() => {
this.listLoader.go(addIndex);
this.container.classList.remove('is-swiping');
});
}
});
}
public setPeer(peerId: number) {
this.peerId = peerId;
const photo = appPeersManager.getPeerPhoto(peerId);
if(!photo) {
return;
}
const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader({
loadCount: 50,
loadMore: (anchor, older, loadCount) => {
if(!older) return Promise.resolve({count: undefined, items: []});
if(peerId > 0) {
const maxId: string = (anchor || listLoader.current) as any;
return appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => {
return {
count: value.count,
items: value.photos
};
});
} else {
const promises: [Promise<ChatFull>, ReturnType<AppMessagesManager['getSearch']>] = [] as any;
if(!listLoader.current) {
promises.push(appProfileManager.getChatFull(-peerId));
}
promises.push(appMessagesManager.getSearch({
peerId,
maxId: Number.MAX_SAFE_INTEGER,
inputFilter: {
_: 'inputMessagesFilterChatPhotos'
},
limit: loadCount,
backLimit: 0
}));
return Promise.all(promises).then((result) => {
const value = result.pop() as typeof result[1];
filterChatPhotosMessages(value);
if(!listLoader.current) {
const chatFull = result[0];
const message = value.history.findAndSplice(m => {
return ((m as Message.messageService).action as MessageAction.messageActionChannelEditPhoto).photo.id === chatFull.chat_photo.id;
}) as Message.messageService;
listLoader.current = message || appMessagesManager.generateFakeAvatarMessage(this.peerId, chatFull.chat_photo);
}
//console.log('avatars loaded:', value);
return {
count: value.count,
items: value.history
};
});
}
},
processItem: this.processItem,
onJump: (item, older) => {
const id = this.listLoader.index;
//const nextId = Math.max(0, id);
const x = 100 * PeerProfileAvatars.SCALE * id;
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', `-${x}%`);
const activeTab = this.tabs.querySelector('.active');
if(activeTab) activeTab.classList.remove('active');
const tab = this.tabs.children[id] as HTMLElement;
tab.classList.add('active');
}
});
if(photo._ === 'userProfilePhoto') {
listLoader.current = photo.photo_id;
}
this.processItem(listLoader.current);
// listLoader.loaded
listLoader.load(true);
}
public addTab() {
const tab = document.createElement('div');
tab.classList.add(PeerProfileAvatars.BASE_CLASS + '-tab');
this.tabs.append(tab);
if(this.tabs.childElementCount === 1) {
tab.classList.add('active');
}
this.container.classList.toggle('is-single', this.tabs.childElementCount <= 1);
}
public processItem = (photoId: string | Message.messageService) => {
const avatar = document.createElement('div');
avatar.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar');
let photo: Photo.photo;
if(photoId) {
photo = typeof(photoId) === 'string' ?
appPhotosManager.getPhoto(photoId) :
(photoId.action as MessageAction.messageActionChannelEditPhoto).photo as Photo.photo;
}
const img = new Image();
img.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar-image');
img.draggable = false;
if(photo) {
const size = appPhotosManager.choosePhotoSize(photo, 420, 420, false);
appPhotosManager.preloadPhoto(photo, size).then(() => {
const cacheContext = appDownloadManager.getCacheContext(photo, size.type);
renderImageFromUrl(img, cacheContext.url, () => {
avatar.append(img);
});
});
} else {
const photo = appPeersManager.getPeerPhoto(this.peerId);
appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img);
}
this.avatars.append(avatar);
this.addTab();
return photoId;
};
}

15
src/components/popups/index.ts

@ -164,12 +164,15 @@ export default class PopupElement {
rootScope.isOverlayActive = true; rootScope.isOverlayActive = true;
animationIntersector.checkAnimations(true); animationIntersector.checkAnimations(true);
this.listenerSetter.add(document.body)('keydown', (e) => { // cannot add event instantly because keydown propagation will fire it
if(this.confirmShortcutIsSendShortcut ? isSendShortcutPressed(e) : e.key === 'Enter') { setTimeout(() => {
simulateClickEvent(this.btnConfirmOnEnter); this.listenerSetter.add(document.body)('keydown', (e) => {
cancelEvent(e); if(this.confirmShortcutIsSendShortcut ? isSendShortcutPressed(e) : e.key === 'Enter') {
} simulateClickEvent(this.btnConfirmOnEnter);
}); cancelEvent(e);
}
});
}, 0);
} }
public hide = () => { public hide = () => {

43
src/components/rangeSelector.ts

@ -6,6 +6,7 @@
import { clamp } from "../helpers/number"; import { clamp } from "../helpers/number";
import attachGrabListeners, { GrabEvent } from "../helpers/dom/attachGrabListeners"; import attachGrabListeners, { GrabEvent } from "../helpers/dom/attachGrabListeners";
import { safeAssign } from "../helpers/object";
export default class RangeSelector { export default class RangeSelector {
public container: HTMLDivElement; public container: HTMLDivElement;
@ -25,10 +26,33 @@ export default class RangeSelector {
protected decimals: number; protected decimals: number;
constructor(protected step: number, value: number, protected min: number, protected max: number, withTransition = false) { protected step: number;
protected min: number;
protected max: number;
protected withTransition = false;
protected useTransform = false;
protected vertical = false;
constructor(
options: {
step: RangeSelector['step'],
min: RangeSelector['min'],
max: RangeSelector['max'],
withTransition?: RangeSelector['withTransition'],
useTransform?: RangeSelector['useTransform'],
vertical?: RangeSelector['vertical']
},
value = 0
) {
safeAssign(this, options);
this.container = document.createElement('div'); this.container = document.createElement('div');
this.container.classList.add('progress-line'); this.container.classList.add('progress-line');
if(withTransition) {
// there is no sense in using transition with transform, because it is updating every frame
if(this.useTransform) {
this.container.classList.add('use-transform');
} else if(this.withTransition) {
this.container.classList.add('with-transition'); this.container.classList.add('with-transition');
} }
@ -39,7 +63,7 @@ export default class RangeSelector {
seek.classList.add('progress-line__seek'); seek.classList.add('progress-line__seek');
//seek.setAttribute('max', '0'); //seek.setAttribute('max', '0');
seek.type = 'range'; seek.type = 'range';
seek.step = '' + step; seek.step = '' + this.step;
seek.min = '' + this.min; seek.min = '' + this.min;
seek.max = '' + this.max; seek.max = '' + this.max;
seek.value = '' + value; seek.value = '' + value;
@ -108,14 +132,19 @@ export default class RangeSelector {
let percents = (value - this.min) / (this.max - this.min); let percents = (value - this.min) / (this.max - this.min);
percents = clamp(percents, 0, 1); percents = clamp(percents, 0, 1);
this.filled.style.width = (percents * 100) + '%'; // using scaleX and width even with vertical because it will be rotated
//this.filled.style.transform = 'scaleX(' + scaleX + ')'; if(this.useTransform) {
this.filled.style.transform = `scaleX(${percents})`;
} else {
this.filled.style.width = (percents * 100) + '%';
}
} }
protected scrub(event: GrabEvent) { protected scrub(event: GrabEvent) {
const offsetX = clamp(event.x - this.rect.left, 0, this.rect.width); const rectMax = this.vertical ? this.rect.height : this.rect.width;
const offsetAxisValue = clamp(this.vertical ? -(event.y - this.rect.bottom) : event.x - this.rect.left, 0, rectMax);
let value = this.min + (offsetX / this.rect.width * (this.max - this.min)); let value = this.min + (offsetAxisValue / rectMax * (this.max - this.min));
if((value - this.min) < ((this.max - this.min) / 2)) { if((value - this.min) < ((this.max - this.min) / 2)) {
value -= this.step / 10; value -= this.step / 10;

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

@ -49,7 +49,11 @@ export class RangeSettingSelector {
details.append(nameDiv, valueDiv); details.append(nameDiv, valueDiv);
this.range = new RangeSelector(step, initialValue, minValue, maxValue); this.range = new RangeSelector({
step,
min: minValue,
max: maxValue
}, initialValue);
this.range.setListeners(); this.range.setListeners();
this.range.setHandlers({ this.range.setHandlers({
onScrub: value => { onScrub: value => {

15
src/components/sidebarRight/index.ts

@ -14,6 +14,7 @@ export const RIGHT_COLUMN_ACTIVE_CLASSNAME = 'is-right-column-shown';
export class AppSidebarRight extends SidebarSlider { export class AppSidebarRight extends SidebarSlider {
public sharedMediaTab: AppSharedMediaTab; public sharedMediaTab: AppSharedMediaTab;
private isColumnProportionSet = false;
constructor() { constructor() {
super({ super({
@ -28,6 +29,10 @@ export class AppSidebarRight extends SidebarSlider {
} }
}); });
mediaSizes.addEventListener('resize', () => {
this.setColumnProportion();
});
this.sharedMediaTab = new AppSharedMediaTab(this); this.sharedMediaTab = new AppSharedMediaTab(this);
} }
@ -49,6 +54,11 @@ export class AppSidebarRight extends SidebarSlider {
return res; return res;
} */ } */
private setColumnProportion() {
const proportion = this.sidebarEl.scrollWidth / this.sidebarEl.previousElementSibling.scrollWidth;
document.documentElement.style.setProperty('--right-column-proportion', '' + proportion);
}
public toggleSidebar(enable?: boolean, animate?: boolean) { public toggleSidebar(enable?: boolean, animate?: boolean) {
/////this.log('sidebarEl', this.sidebarEl, enable, isElementInViewport(this.sidebarEl)); /////this.log('sidebarEl', this.sidebarEl, enable, isElementInViewport(this.sidebarEl));
@ -73,6 +83,11 @@ export class AppSidebarRight extends SidebarSlider {
//this.selectTab(this.sharedMediaTab); //this.selectTab(this.sharedMediaTab);
} }
if(!this.isColumnProportionSet) {
this.setColumnProportion();
this.isColumnProportionSet = true;
}
const animationPromise = appImManager.selectTab(active ? 1 : 2, animate); const animationPromise = appImManager.selectTab(active ? 1 : 2, animate);
document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, enable); document.body.classList.toggle(RIGHT_COLUMN_ACTIVE_CLASSNAME, enable);
return animationPromise; return animationPromise;

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

@ -4,711 +4,26 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import appImManager from "../../../lib/appManagers/appImManager"; import appMessagesManager from "../../../lib/appManagers/appMessagesManager";
import appMessagesManager, { AppMessagesManager, MyMessage } from "../../../lib/appManagers/appMessagesManager"; import appUsersManager from "../../../lib/appManagers/appUsersManager";
import appPeersManager from "../../../lib/appManagers/appPeersManager";
import appProfileManager from "../../../lib/appManagers/appProfileManager";
import appUsersManager, { User } from "../../../lib/appManagers/appUsersManager";
import { RichTextProcessor } from "../../../lib/richtextprocessor";
import rootScope from "../../../lib/rootScope"; import rootScope from "../../../lib/rootScope";
import AppSearchSuper, { SearchSuperType } from "../../appSearchSuper."; import AppSearchSuper, { SearchSuperType } from "../../appSearchSuper.";
import AvatarElement, { openAvatarViewer } from "../../avatar";
import SidebarSlider, { SliderSuperTab } from "../../slider"; import SidebarSlider, { SliderSuperTab } from "../../slider";
import CheckboxField from "../../checkboxField";
import appSidebarRight from "..";
import { TransitionSlider } from "../../transition"; import { TransitionSlider } from "../../transition";
import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager";
import AppEditChatTab from "./editChat"; import AppEditChatTab from "./editChat";
import PeerTitle from "../../peerTitle"; import PeerTitle from "../../peerTitle";
import AppEditContactTab from "./editContact"; import AppEditContactTab from "./editContact";
import appChatsManager, { Channel } from "../../../lib/appManagers/appChatsManager"; import appChatsManager from "../../../lib/appManagers/appChatsManager";
import { Chat, Message, MessageAction, ChatFull, Photo } from "../../../layer";
import Button from "../../button"; import Button from "../../button";
import ButtonIcon from "../../buttonIcon"; import ButtonIcon from "../../buttonIcon";
import I18n, { i18n, LangPackKey } from "../../../lib/langPack"; import { i18n, LangPackKey } from "../../../lib/langPack";
import { generateDelimiter, SettingSection } from "../../sidebarLeft"; import { toastNew } from "../../toast";
import Row from "../../row";
import { copyTextToClipboard } from "../../../helpers/clipboard";
import { toast, toastNew } from "../../toast";
import { fastRaf } from "../../../helpers/schedulers";
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
import renderImageFromUrl from "../../../helpers/dom/renderImageFromUrl";
import SwipeHandler from "../../swipeHandler";
import { MOUNT_CLASS_TO } from "../../../config/debug";
import AppAddMembersTab from "../../sidebarLeft/tabs/addMembers"; import AppAddMembersTab from "../../sidebarLeft/tabs/addMembers";
import PopupPickUser from "../../popups/pickUser"; import PopupPickUser from "../../popups/pickUser";
import PopupPeer, { PopupPeerButtonCallbackCheckboxes, PopupPeerCheckboxOptions } from "../../popups/peer"; import PopupPeer, { PopupPeerButtonCallbackCheckboxes, PopupPeerCheckboxOptions } from "../../popups/peer";
import Scrollable from "../../scrollable";
import { IS_TOUCH_SUPPORTED } from "../../../environment/touchSupport";
import { IS_FIREFOX } from "../../../environment/userAgent";
import appDownloadManager from "../../../lib/appManagers/appDownloadManager";
import ButtonCorner from "../../buttonCorner"; import ButtonCorner from "../../buttonCorner";
import { cancelEvent } from "../../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../../helpers/dom/clickEvent"; import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import replaceContent from "../../../helpers/dom/replaceContent"; import PeerProfile from "../../peerProfile";
import appAvatarsManager from "../../../lib/appManagers/appAvatarsManager";
import generateVerifiedIcon from "../../generateVerifiedIcon";
import ListLoader from "../../../helpers/listLoader";
import { forEachReverse } from "../../../helpers/array";
let setText = (text: string, row: Row) => {
//fastRaf(() => {
row.title.innerHTML = text;
row.container.style.display = '';
//});
};
const PARALLAX_SUPPORTED = !IS_FIREFOX && false;
export function filterChatPhotosMessages(value: {
count: number;
next_rate: number;
offset_id_offset: number;
history: MyMessage[];
}) {
forEachReverse(value.history, (message, idx, arr) => {
if(!((message as Message.messageService).action as MessageAction.messageActionChatEditPhoto).photo) {
arr.splice(idx, 1);
if(value.count !== undefined) {
--value.count;
}
}
});
}
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)';
public container: HTMLElement;
public avatars: HTMLElement;
public gradient: HTMLElement;
public info: HTMLElement;
public arrowPrevious: HTMLElement;
public arrowNext: HTMLElement;
private tabs: HTMLDivElement;
private listLoader: ListLoader<string | Message.messageService>;
private peerId: number;
constructor(public scrollable: Scrollable) {
this.container = document.createElement('div');
this.container.classList.add(PeerProfileAvatars.BASE_CLASS + '-container');
this.avatars = document.createElement('div');
this.avatars.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatars');
this.gradient = document.createElement('div');
this.gradient.classList.add(PeerProfileAvatars.BASE_CLASS + '-gradient');
this.info = document.createElement('div');
this.info.classList.add(PeerProfileAvatars.BASE_CLASS + '-info');
this.tabs = document.createElement('div');
this.tabs.classList.add(PeerProfileAvatars.BASE_CLASS + '-tabs');
this.arrowPrevious = document.createElement('div');
this.arrowPrevious.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow');
/* const previousIcon = document.createElement('i');
previousIcon.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow-icon', 'tgico-previous');
this.arrowBack.append(previousIcon); */
this.arrowNext = document.createElement('div');
this.arrowNext.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow', PeerProfileAvatars.BASE_CLASS + '-arrow-next');
/* const nextIcon = document.createElement('i');
nextIcon.classList.add(PeerProfileAvatars.BASE_CLASS + '-arrow-icon', 'tgico-next');
this.arrowNext.append(nextIcon); */
this.container.append(this.avatars, this.gradient, this.info, this.tabs, this.arrowPrevious, this.arrowNext);
const checkScrollTop = () => {
if(this.scrollable.scrollTop !== 0) {
this.scrollable.scrollIntoViewNew(this.scrollable.container.firstElementChild as HTMLElement, 'start');
return false;
}
return true;
};
const SWITCH_ZONE = 1 / 3;
let cancel = false;
let freeze = false;
attachClickEvent(this.container, async(_e) => {
if(freeze) {
cancelEvent(_e);
return;
}
if(cancel) {
cancel = false;
return;
}
if(!checkScrollTop()) {
return;
}
const rect = this.container.getBoundingClientRect();
// const e = (_e as TouchEvent).touches ? (_e as TouchEvent).touches[0] : _e as MouseEvent;
const e = _e;
const x = e.pageX;
const clickX = x - rect.left;
if((!this.listLoader.previous.length && !this.listLoader.next.length)
|| (clickX > (rect.width * SWITCH_ZONE) && clickX < (rect.width - rect.width * SWITCH_ZONE))) {
const peerId = this.peerId;
const targets: {element: HTMLElement, item: string | Message.messageService}[] = [];
this.listLoader.previous.concat(this.listLoader.current, this.listLoader.next).forEach((item, idx) => {
targets.push({
element: /* null */this.avatars.children[idx] as HTMLElement,
item
});
});
const prevTargets = targets.slice(0, this.listLoader.previous.length);
const nextTargets = targets.slice(this.listLoader.previous.length + 1);
const target = this.avatars.children[this.listLoader.previous.length] as HTMLElement;
freeze = true;
openAvatarViewer(target, peerId, () => peerId === this.peerId, this.listLoader.current, prevTargets, nextTargets);
freeze = false;
} else {
const centerX = rect.right - (rect.width / 2);
const toRight = x > centerX;
// this.avatars.classList.remove('no-transition');
// fastRaf(() => {
this.avatars.classList.add('no-transition');
void this.avatars.offsetLeft; // reflow
let distance: number;
if(this.listLoader.index === 0 && !toRight) distance = this.listLoader.count - 1;
else if(this.listLoader.index === (this.listLoader.count - 1) && toRight) distance = -(this.listLoader.count - 1);
else distance = toRight ? 1 : -1;
this.listLoader.go(distance);
fastRaf(() => {
this.avatars.classList.remove('no-transition');
});
// });
}
});
const cancelNextClick = () => {
cancel = true;
document.body.addEventListener(IS_TOUCH_SUPPORTED ? 'touchend' : 'click', (e) => {
cancel = false;
}, {once: true});
};
let width = 0, x = 0, lastDiffX = 0, lastIndex = 0, minX = 0;
const swipeHandler = new SwipeHandler({
element: this.avatars,
onSwipe: (xDiff, yDiff) => {
lastDiffX = xDiff;
let lastX = x + xDiff * -PeerProfileAvatars.SCALE;
if(lastX > 0) lastX = 0;
else if(lastX < minX) lastX = minX;
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', lastX + 'px');
//console.log(xDiff, yDiff);
return false;
},
verifyTouchTarget: (e) => {
if(!checkScrollTop()) {
cancelNextClick();
cancelEvent(e);
return false;
} else if(this.container.classList.contains('is-single') || freeze) {
return false;
}
return true;
},
onFirstSwipe: () => {
const rect = this.avatars.getBoundingClientRect();
width = rect.width;
minX = -width * (this.tabs.childElementCount - 1);
/* lastIndex = whichChild(this.tabs.querySelector('.active'));
x = -width * lastIndex; */
x = rect.left - this.container.getBoundingClientRect().left;
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', x + 'px');
this.container.classList.add('is-swiping');
this.avatars.classList.add('no-transition');
void this.avatars.offsetLeft; // reflow
},
onReset: () => {
const addIndex = Math.ceil(Math.abs(lastDiffX) / (width / PeerProfileAvatars.SCALE)) * (lastDiffX >= 0 ? 1 : -1);
cancelNextClick();
//console.log(addIndex);
this.avatars.classList.remove('no-transition');
fastRaf(() => {
this.listLoader.go(addIndex);
this.container.classList.remove('is-swiping');
});
}
});
}
public setPeer(peerId: number) {
this.peerId = peerId;
const photo = appPeersManager.getPeerPhoto(peerId);
if(!photo) {
return;
}
const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader<string | Message.messageService>({
loadCount: 50,
loadMore: (anchor, older, loadCount) => {
if(!older) return Promise.resolve({count: undefined, items: []});
if(peerId > 0) {
const maxId: string = (anchor || listLoader.current) as any;
return appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => {
return {
count: value.count,
items: value.photos
};
});
} else {
const promises: [Promise<ChatFull>, ReturnType<AppMessagesManager['getSearch']>] = [] as any;
if(!listLoader.current) {
promises.push(appProfileManager.getChatFull(-peerId));
}
promises.push(appMessagesManager.getSearch({
peerId,
maxId: Number.MAX_SAFE_INTEGER,
inputFilter: {
_: 'inputMessagesFilterChatPhotos'
},
limit: loadCount,
backLimit: 0
}));
return Promise.all(promises).then((result) => {
const value = result.pop() as typeof result[1];
filterChatPhotosMessages(value);
if(!listLoader.current) {
const chatFull = result[0];
const message = value.history.findAndSplice(m => {
return ((m as Message.messageService).action as MessageAction.messageActionChannelEditPhoto).photo.id === chatFull.chat_photo.id;
}) as Message.messageService;
listLoader.current = message || appMessagesManager.generateFakeAvatarMessage(this.peerId, chatFull.chat_photo);
}
//console.log('avatars loaded:', value);
return {
count: value.count,
items: value.history
};
});
}
},
processItem: this.processItem,
onJump: (item, older) => {
const id = this.listLoader.index;
//const nextId = Math.max(0, id);
const x = 100 * PeerProfileAvatars.SCALE * id;
this.avatars.style.transform = PeerProfileAvatars.TRANSLATE_TEMPLATE.replace('{x}', `-${x}%`);
const activeTab = this.tabs.querySelector('.active');
if(activeTab) activeTab.classList.remove('active');
const tab = this.tabs.children[id] as HTMLElement;
tab.classList.add('active');
}
});
if(photo._ === 'userProfilePhoto') {
listLoader.current = photo.photo_id;
}
this.processItem(listLoader.current);
// listLoader.loaded
listLoader.load(true);
}
public addTab() {
const tab = document.createElement('div');
tab.classList.add(PeerProfileAvatars.BASE_CLASS + '-tab');
this.tabs.append(tab);
if(this.tabs.childElementCount === 1) {
tab.classList.add('active');
}
this.container.classList.toggle('is-single', this.tabs.childElementCount <= 1);
}
public processItem = (photoId: string | Message.messageService) => {
const avatar = document.createElement('div');
avatar.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar');
let photo: Photo.photo;
if(photoId) {
photo = typeof(photoId) === 'string' ?
appPhotosManager.getPhoto(photoId) :
(photoId.action as MessageAction.messageActionChannelEditPhoto).photo as Photo.photo;
}
const img = new Image();
img.classList.add(PeerProfileAvatars.BASE_CLASS + '-avatar-image');
img.draggable = false;
if(photo) {
const size = appPhotosManager.choosePhotoSize(photo, 420, 420, false);
appPhotosManager.preloadPhoto(photo, size).then(() => {
const cacheContext = appDownloadManager.getCacheContext(photo, size.type);
renderImageFromUrl(img, cacheContext.url, () => {
avatar.append(img);
});
});
} else {
const photo = appPeersManager.getPeerPhoto(this.peerId);
appAvatarsManager.putAvatar(avatar, this.peerId, photo, 'photo_big', img);
}
this.avatars.append(avatar);
this.addTab();
return photoId;
};
}
class PeerProfile {
public element: HTMLElement;
public avatars: PeerProfileAvatars;
private avatar: AvatarElement;
private section: SettingSection;
private name: HTMLDivElement;
private subtitle: HTMLDivElement;
private bio: Row;
private username: Row;
private phone: Row;
private notifications: Row;
private cleaned: boolean;
private setBioTimeout: number;
private setPeerStatusInterval: number;
private peerId = 0;
private threadId: number;
constructor(public scrollable: Scrollable) {
if(!PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('no-parallax');
}
}
public init() {
this.init = null;
this.element = document.createElement('div');
this.element.classList.add('profile-content');
this.section = new SettingSection({
noDelimiter: true
});
this.avatar = new AvatarElement();
this.avatar.classList.add('profile-avatar', 'avatar-120');
this.avatar.setAttribute('dialog', '1');
this.avatar.setAttribute('clickable', '');
this.name = document.createElement('div');
this.name.classList.add('profile-name');
this.subtitle = document.createElement('div');
this.subtitle.classList.add('profile-subtitle');
this.bio = new Row({
title: ' ',
subtitleLangKey: 'UserBio',
icon: 'info',
clickable: (e) => {
if((e.target as HTMLElement).tagName === 'A') {
return;
}
appProfileManager.getProfileByPeerId(this.peerId).then(full => {
copyTextToClipboard(full.about);
toast(I18n.format('BioCopied', true));
});
}
});
this.bio.title.classList.add('pre-wrap');
this.username = new Row({
title: ' ',
subtitleLangKey: 'Username',
icon: 'username',
clickable: () => {
const peer: Channel | User = appPeersManager.getPeer(this.peerId);
copyTextToClipboard('@' + peer.username);
toast(I18n.format('UsernameCopied', true));
}
});
this.phone = new Row({
title: ' ',
subtitleLangKey: 'Phone',
icon: 'phone',
clickable: () => {
const peer: User = appUsersManager.getUser(this.peerId);
copyTextToClipboard('+' + peer.phone);
toast(I18n.format('PhoneCopied', true));
}
});
this.notifications = new Row({
checkboxField: new CheckboxField({toggle: true}),
titleLangKey: 'Notifications',
icon: 'unmute'
});
this.section.content.append(this.phone.container, this.username.container, this.bio.container, this.notifications.container);
this.element.append(this.section.container, generateDelimiter());
this.notifications.checkboxField.input.addEventListener('change', (e) => {
if(!e.isTrusted) {
return;
}
//let checked = this.notificationsCheckbox.checked;
appMessagesManager.mutePeer(this.peerId);
});
rootScope.addEventListener('dialog_notify_settings', (dialog) => {
if(this.peerId === dialog.peerId) {
const muted = appNotificationsManager.isPeerLocalMuted(this.peerId, false);
this.notifications.checkboxField.checked = !muted;
}
});
rootScope.addEventListener('peer_typings', ({peerId}) => {
if(this.peerId === peerId) {
this.setPeerStatus();
}
});
rootScope.addEventListener('peer_bio_edit', (peerId) => {
if(peerId === this.peerId) {
this.setBio(true);
}
});
rootScope.addEventListener('user_update', (userId) => {
if(this.peerId === userId) {
this.setPeerStatus();
}
});
rootScope.addEventListener('contacts_update', (userId) => {
if(this.peerId === userId) {
const user = appUsersManager.getUser(userId);
if(!user.pFlags.self) {
if(user.phone) {
setText(appUsersManager.formatUserPhone(user.phone), this.phone);
} else {
this.phone.container.style.display = 'none';
}
}
}
});
this.setPeerStatusInterval = window.setInterval(this.setPeerStatus, 60e3);
}
public setPeerStatus = (needClear = false) => {
if(!this.peerId) return;
const peerId = this.peerId;
appImManager.setPeerStatus(this.peerId, this.subtitle, needClear, true, () => peerId === this.peerId);
};
public cleanupHTML() {
this.bio.container.style.display = 'none';
this.phone.container.style.display = 'none';
this.username.container.style.display = 'none';
this.notifications.container.style.display = '';
this.notifications.checkboxField.checked = true;
if(this.setBioTimeout) {
window.clearTimeout(this.setBioTimeout);
this.setBioTimeout = 0;
}
}
public setAvatar() {
if(this.peerId !== rootScope.myId) {
const photo = appPeersManager.getPeerPhoto(this.peerId);
if(photo) {
const oldAvatars = this.avatars;
this.avatars = new PeerProfileAvatars(this.scrollable);
this.avatars.setPeer(this.peerId);
this.avatars.info.append(this.name, this.subtitle);
this.avatar.remove();
if(oldAvatars) oldAvatars.container.replaceWith(this.avatars.container);
else this.element.prepend(this.avatars.container);
if(PARALLAX_SUPPORTED) {
this.scrollable.container.classList.add('parallax');
}
return;
}
}
if(PARALLAX_SUPPORTED) {
this.scrollable.container.classList.remove('parallax');
}
if(this.avatars) {
this.avatars.container.remove();
this.avatars = undefined;
}
this.avatar.setAttribute('peer', '' + this.peerId);
this.section.content.prepend(this.avatar, this.name, this.subtitle);
}
public fillProfileElements() {
if(!this.cleaned) return;
this.cleaned = false;
const peerId = this.peerId;
this.cleanupHTML();
this.setAvatar();
// username
if(peerId !== rootScope.myId) {
let username = appPeersManager.getPeerUsername(peerId);
if(username) {
setText(appPeersManager.getPeerUsername(peerId), this.username);
}
const muted = appNotificationsManager.isPeerLocalMuted(peerId, false);
this.notifications.checkboxField.checked = !muted;
} else {
fastRaf(() => {
this.notifications.container.style.display = 'none';
});
}
//let membersLi = this.profileTabs.firstElementChild.children[0] as HTMLLIElement;
if(peerId > 0) {
//membersLi.style.display = 'none';
let user = appUsersManager.getUser(peerId);
if(user.phone && peerId !== rootScope.myId) {
setText(appUsersManager.formatUserPhone(user.phone), this.phone);
}
}/* else {
//membersLi.style.display = appPeersManager.isBroadcast(peerId) ? 'none' : '';
} */
this.setBio();
replaceContent(this.name, new PeerTitle({
peerId,
dialog: true,
}).element);
const peer = appPeersManager.getPeer(peerId);
if(peer?.pFlags?.verified) {
this.name.append(generateVerifiedIcon());
}
this.setPeerStatus(true);
}
public setBio(override?: true) {
if(this.setBioTimeout) {
window.clearTimeout(this.setBioTimeout);
this.setBioTimeout = 0;
}
const peerId = this.peerId;
const threadId = this.threadId;
if(!peerId) {
return;
}
let promise: Promise<boolean>;
if(peerId > 0) {
promise = appProfileManager.getProfile(peerId, override).then(userFull => {
if(this.peerId !== peerId || this.threadId !== threadId) {
//this.log.warn('peer changed');
return false;
}
if(userFull.rAbout && peerId !== rootScope.myId) {
setText(userFull.rAbout, this.bio);
}
//this.log('userFull', userFull);
return true;
});
} else {
promise = appProfileManager.getChatFull(-peerId, override).then((chatFull) => {
if(this.peerId !== peerId || this.threadId !== threadId) {
//this.log.warn('peer changed');
return false;
}
//this.log('chatInfo res 2:', chatFull);
if(chatFull.about) {
setText(RichTextProcessor.wrapRichText(chatFull.about), this.bio);
}
return true;
});
}
promise.then((canSetNext) => {
if(canSetNext) {
this.setBioTimeout = window.setTimeout(() => this.setBio(true), 60e3);
}
});
}
public setPeer(peerId: number, threadId = 0) {
if(this.peerId === peerId && this.threadId === peerId) return;
if(this.init) {
this.init();
}
this.peerId = peerId;
this.threadId = threadId;
this.cleaned = true;
}
}
// TODO: отредактированное сообщение не изменится // TODO: отредактированное сообщение не изменится
export default class AppSharedMediaTab extends SliderSuperTab { export default class AppSharedMediaTab extends SliderSuperTab {
@ -803,16 +118,16 @@ export default class AppSharedMediaTab extends SliderSuperTab {
transition(0); transition(0);
animatedCloseIcon.classList.remove('state-back'); animatedCloseIcon.classList.remove('state-back');
} else if(!this.scrollable.isHeavyAnimationInProgress) { } else if(!this.scrollable.isHeavyAnimationInProgress) {
appSidebarRight.onCloseBtnClick(); this.slider.onCloseBtnClick();
} }
}); });
attachClickEvent(this.editBtn, (e) => { attachClickEvent(this.editBtn, (e) => {
let tab: AppEditChatTab | AppEditContactTab; let tab: AppEditChatTab | AppEditContactTab;
if(this.peerId < 0) { if(this.peerId < 0) {
tab = new AppEditChatTab(appSidebarRight); tab = new AppEditChatTab(this.slider);
} else { } else {
tab = new AppEditContactTab(appSidebarRight); tab = new AppEditContactTab(this.slider);
} }
if(tab) { if(tab) {
@ -838,6 +153,21 @@ export default class AppSharedMediaTab extends SliderSuperTab {
} }
}); });
rootScope.addEventListener('history_multiappend', (msgIdsByPeer) => {
for(const peerId in msgIdsByPeer) {
this.renderNewMessages(+peerId, Array.from(msgIdsByPeer[peerId]));
}
});
rootScope.addEventListener('history_delete', ({peerId, msgs}) => {
this.deleteDeletedMessages(peerId, Array.from(msgs));
});
// Calls when message successfully sent and we have an id
rootScope.addEventListener('message_sent', ({message}) => {
this.renderNewMessages(message.peerId, [message.mid]);
});
//this.container.prepend(this.closeBtn.parentElement); //this.container.prepend(this.closeBtn.parentElement);
this.searchSuper = new AppSearchSuper({ this.searchSuper = new AppSearchSuper({
@ -1120,4 +450,4 @@ export default class AppSharedMediaTab extends SliderSuperTab {
} }
} }
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.AppSharedMediaTab = AppSharedMediaTab); // MOUNT_CLASS_TO && (MOUNT_CLASS_TO.AppSharedMediaTab = AppSharedMediaTab);

195
src/components/wrappers.ts

@ -19,8 +19,8 @@ import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager';
import LottieLoader from '../lib/lottieLoader'; import LottieLoader from '../lib/lottieLoader';
import webpWorkerController from '../lib/webp/webpWorkerController'; import webpWorkerController from '../lib/webp/webpWorkerController';
import animationIntersector from './animationIntersector'; import animationIntersector from './animationIntersector';
import appMediaPlaybackController from './appMediaPlaybackController'; import appMediaPlaybackController, { MediaSearchContext } from './appMediaPlaybackController';
import AudioElement from './audio'; import AudioElement, { findAudioTargets as findMediaTargets } from './audio';
import ReplyContainer from './chat/replyContainer'; import ReplyContainer from './chat/replyContainer';
import { Layouter, RectPart } from './groupedLayout'; import { Layouter, RectPart } from './groupedLayout';
import LazyLoadQueue from './lazyLoadQueue'; import LazyLoadQueue from './lazyLoadQueue';
@ -48,6 +48,7 @@ import IS_WEBP_SUPPORTED from '../environment/webpSupport';
import MEDIA_MIME_TYPES_SUPPORTED from '../environment/mediaMimeTypesSupport'; import MEDIA_MIME_TYPES_SUPPORTED from '../environment/mediaMimeTypesSupport';
import { MiddleEllipsisElement } from './middleEllipsis'; import { MiddleEllipsisElement } from './middleEllipsis';
import { joinElementsWith } from '../lib/langPack'; import { joinElementsWith } from '../lib/langPack';
import throttleWithRaf from '../helpers/schedulers/throttleWithRaf';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
@ -92,7 +93,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
noAutoDownload?: boolean, noAutoDownload?: boolean,
size?: PhotoSize, size?: PhotoSize,
searchContext?: SearchSuperContext searchContext?: MediaSearchContext,
}) { }) {
const isAlbumItem = !(boxWidth && boxHeight); const isAlbumItem = !(boxWidth && boxHeight);
const canAutoplay = (doc.type !== 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem)) const canAutoplay = (doc.type !== 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem))
@ -168,12 +169,11 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
video.setAttribute('playsinline', 'true'); video.setAttribute('playsinline', 'true');
video.muted = true; video.muted = true;
if(doc.type === 'round') { if(doc.type === 'round') {
const globalVideo = appMediaPlaybackController.addMedia(message.peerId, doc, message.mid, !noAutoDownload) as HTMLVideoElement;
const divRound = document.createElement('div'); const divRound = document.createElement('div');
divRound.classList.add('media-round', 'z-depth-1'); divRound.classList.add('media-round', 'z-depth-1');
divRound.dataset.mid = '' + message.mid; divRound.dataset.mid = '' + message.mid;
divRound.dataset.peerId = '' + message.peerId; divRound.dataset.peerId = '' + message.peerId;
(divRound as any).message = message;
const size = mediaSizes.active.round; const size = mediaSizes.active.round;
const halfSize = size.width / 2; const halfSize = size.width / 2;
@ -209,109 +209,129 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2); ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2);
ctx.clip(); */ ctx.clip(); */
const clear = () => { const onLoad = () => {
(appImManager.chat.setPeerPromise || Promise.resolve()).finally(() => { const message: Message.message = (divRound as any).message;
if(isInDOM(globalVideo)) { const globalVideo = appMediaPlaybackController.addMedia(message, !noAutoDownload) as HTMLVideoElement;
const clear = () => {
(appImManager.chat.setPeerPromise || Promise.resolve()).finally(() => {
if(isInDOM(globalVideo)) {
return;
}
globalVideo.removeEventListener('play', onPlay);
globalVideo.removeEventListener('timeupdate', throttledTimeUpdate);
globalVideo.removeEventListener('pause', onPaused);
globalVideo.removeEventListener('ended', onEnded);
});
};
const onFrame = () => {
ctx.drawImage(globalVideo, 0, 0);
const offset = roundVideoCircumference - globalVideo.currentTime / globalVideo.duration * roundVideoCircumference;
circle.style.strokeDashoffset = '' + offset;
return !globalVideo.paused;
};
const onTimeUpdate = () => {
if(!globalVideo.duration) {
return; return;
} }
globalVideo.removeEventListener('play', onPlay); if(!isInDOM(globalVideo)) {
globalVideo.removeEventListener('timeupdate', onTimeUpdate); clear();
globalVideo.removeEventListener('pause', onPaused); return;
globalVideo.removeEventListener('ended', onEnded); }
});
};
const onFrame = () => {
ctx.drawImage(globalVideo, 0, 0);
const offset = roundVideoCircumference - globalVideo.currentTime / globalVideo.duration * roundVideoCircumference; if(globalVideo.paused) {
circle.style.strokeDashoffset = '' + offset; onFrame();
}
return !globalVideo.paused; spanTime.innerText = (globalVideo.duration - globalVideo.currentTime + '').toHHMMSS(false);
}; };
const onTimeUpdate = () => { const throttledTimeUpdate = throttleWithRaf(onTimeUpdate);
if(!globalVideo.duration) return;
if(!isInDOM(globalVideo)) { const onPlay = () => {
clear(); video.classList.add('hide');
return; divRound.classList.remove('is-paused');
} animateSingle(onFrame, canvas);
spanTime.innerText = (globalVideo.duration - globalVideo.currentTime + '').toHHMMSS(false); if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) {
}; preloader.onClick();
}
};
const onPlay = () => { const onPaused = () => {
video.classList.add('hide'); if(!isInDOM(globalVideo)) {
divRound.classList.remove('is-paused'); clear();
animateSingle(onFrame, canvas); return;
}
if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) { divRound.classList.add('is-paused');
preloader.onClick(); };
}
};
const onPaused = () => { const onEnded = () => {
if(!isInDOM(globalVideo)) { video.classList.remove('hide');
clear(); divRound.classList.add('is-paused');
return;
}
divRound.classList.add('is-paused'); video.currentTime = 0;
}; spanTime.innerText = ('' + globalVideo.duration).toHHMMSS(false);
const onEnded = () => { if(globalVideo.currentTime) {
video.classList.remove('hide'); globalVideo.currentTime = 0;
divRound.classList.add('is-paused'); }
};
video.currentTime = 0; globalVideo.addEventListener('play', onPlay);
spanTime.innerText = ('' + globalVideo.duration).toHHMMSS(false); globalVideo.addEventListener('timeupdate', throttledTimeUpdate);
globalVideo.addEventListener('pause', onPaused);
globalVideo.addEventListener('ended', onEnded);
if(globalVideo.currentTime) { attachClickEvent(canvas, (e) => {
globalVideo.currentTime = 0; cancelEvent(e);
}
};
globalVideo.addEventListener('play', onPlay); // ! костыль
globalVideo.addEventListener('timeupdate', onTimeUpdate); if(preloader && !preloader.detached) {
globalVideo.addEventListener('pause', onPaused); preloader.onClick();
globalVideo.addEventListener('ended', onEnded); }
attachClickEvent(canvas, (e) => { // ! can't use it here. on Safari iOS video won't start.
cancelEvent(e); /* if(globalVideo.readyState < 2) {
return;
} */
// ! костыль if(globalVideo.paused) {
if(preloader && !preloader.detached) { if(appMediaPlaybackController.setSearchContext(searchContext)) {
preloader.onClick(); const [prev, next] = findMediaTargets(divRound, searchContext.useSearch);
} appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next);
}
// ! can't use it here. on Safari iOS video won't start. globalVideo.play();
/* if(globalVideo.readyState < 2) { } else {
return; globalVideo.pause();
} */ }
});
if(globalVideo.paused) { if(globalVideo.paused) {
if(appMediaPlaybackController.setSearchContext(searchContext)) { if(globalVideo.duration && globalVideo.currentTime !== globalVideo.duration && globalVideo.currentTime > 0) {
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}); onFrame();
onTimeUpdate();
video.classList.add('hide');
} else {
onPaused();
} }
globalVideo.play();
} else { } else {
globalVideo.pause(); onPlay();
} }
}); };
if(globalVideo.paused) { if(message.pFlags.is_outgoing) {
if(globalVideo.duration && globalVideo.currentTime !== globalVideo.duration && globalVideo.currentTime > 0) { (divRound as any).onLoad = onLoad;
onFrame(); divRound.dataset.isOutgoing = '1';
onTimeUpdate();
video.classList.add('hide');
} else {
onPaused();
}
} else { } else {
onPlay(); onLoad();
} }
} else { } else {
video.autoplay = true; // для safari video.autoplay = true; // для safari
@ -456,7 +476,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
} }
if(doc.type === 'round') { if(doc.type === 'round') {
appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid); appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid, message.pFlags.is_scheduled);
} }
renderImageFromUrl(video, cacheContext.url); renderImageFromUrl(video, cacheContext.url);
@ -515,7 +535,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
fontWeight?: number, fontWeight?: number,
voiceAsMusic?: boolean, voiceAsMusic?: boolean,
showSender?: boolean, showSender?: boolean,
searchContext?: SearchSuperContext, searchContext?: MediaSearchContext,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
noAutoDownload?: boolean, noAutoDownload?: boolean,
lazyLoadQueue?: LazyLoadQueue lazyLoadQueue?: LazyLoadQueue
@ -526,8 +546,6 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
const uploading = message.pFlags.is_outgoing && message.media?.preloader; const uploading = message.pFlags.is_outgoing && message.media?.preloader;
if(doc.type === 'audio' || doc.type === 'voice' || doc.type === 'round') { if(doc.type === 'audio' || doc.type === 'voice' || doc.type === 'round') {
const audioElement = new AudioElement(); const audioElement = new AudioElement();
audioElement.dataset.mid = '' + message.mid;
audioElement.dataset.peerId = '' + message.peerId;
audioElement.withTime = withTime; audioElement.withTime = withTime;
audioElement.message = message; audioElement.message = message;
audioElement.noAutoDownload = noAutoDownload; audioElement.noAutoDownload = noAutoDownload;
@ -1609,7 +1627,7 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo
}); });
} }
export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue, searchContext}: { export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue, searchContext, useSearch}: {
albumMustBeRenderedFull: boolean, albumMustBeRenderedFull: boolean,
message: any, message: any,
messageDiv: HTMLElement, messageDiv: HTMLElement,
@ -1619,7 +1637,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
noAutoDownload?: boolean, noAutoDownload?: boolean,
lazyLoadQueue?: LazyLoadQueue, lazyLoadQueue?: LazyLoadQueue,
searchContext?: SearchSuperContext searchContext?: MediaSearchContext,
useSearch?: boolean,
}) { }) {
let nameContainer: HTMLElement; let nameContainer: HTMLElement;
const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid]; const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid];

2
src/config/app.ts

@ -16,7 +16,7 @@ export const MAIN_DOMAIN = 'web.telegram.org';
const App = { const App = {
id: 1025907, id: 1025907,
hash: '452b0359b988148995f22ff0f4229750', hash: '452b0359b988148995f22ff0f4229750',
version: '0.8.5', version: '0.8.6',
langPackVersion: '0.3.3', langPackVersion: '0.3.3',
langPack: 'macos', langPack: 'macos',
langPackCode: 'en', langPackCode: 'en',

5
src/environment/parallaxSupport.ts

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

33
src/helpers/avatarListLoader.ts

@ -0,0 +1,33 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import appPhotosManager from "../lib/appManagers/appPhotosManager";
import ListLoader, { ListLoaderOptions } from "./listLoader";
export default class AvatarListLoader<Item extends {photoId: string}> extends ListLoader<Item, any> {
private peerId: number;
constructor(options: Omit<ListLoaderOptions<Item, any>, 'loadMore'> & {peerId: number}) {
super({
...options,
loadMore: (anchor, older, loadCount) => {
if(this.peerId < 0 || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
const maxId = anchor?.photoId;
return appPhotosManager.getUserPhotos(this.peerId, maxId, loadCount).then(value => {
const items = value.photos.map(photoId => {
return {element: null as HTMLElement, photoId} as any;
});
return {count: value.count, items};
});
}
});
this.loadedAllUp = true;
this.peerId = options.peerId;
}
}

4
src/helpers/cancellablePromise.ts

@ -4,6 +4,8 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import noop from "./noop";
export interface CancellablePromise<T> extends Promise<T> { export interface CancellablePromise<T> extends Promise<T> {
resolve?: (value: T) => void, resolve?: (value: T) => void,
reject?: (...args: any[]) => void, reject?: (...args: any[]) => void,
@ -62,7 +64,7 @@ export function deferredPromise<T>() {
}; */ }; */
deferred.finally(() => { deferred.catch(noop).finally(() => {
deferred.notify = deferred.notifyAll = deferred.lastNotify = null; deferred.notify = deferred.notifyAll = deferred.lastNotify = null;
deferred.listeners.length = 0; deferred.listeners.length = 0;

14
src/helpers/dom/attachGrabListeners.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
export type GrabEvent = {x: number, y: number, isTouch?: boolean}; export type GrabEvent = {x: number, y: number, isTouch?: boolean, event: TouchEvent | MouseEvent};
export default function attachGrabListeners(element: HTMLElement, export default function attachGrabListeners(element: HTMLElement,
onStart: (position: GrabEvent) => void, onStart: (position: GrabEvent) => void,
@ -12,13 +12,13 @@ export default function attachGrabListeners(element: HTMLElement,
onEnd?: (position: GrabEvent) => void) { onEnd?: (position: GrabEvent) => void) {
// * Mouse // * Mouse
const onMouseMove = (event: MouseEvent) => { const onMouseMove = (event: MouseEvent) => {
onMove({x: event.pageX, y: event.pageY}); onMove({x: event.pageX, y: event.pageY, event});
}; };
const onMouseUp = (event: MouseEvent) => { const onMouseUp = (event: MouseEvent) => {
document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mousemove', onMouseMove);
element.addEventListener('mousedown', onMouseDown, {once: true}); element.addEventListener('mousedown', onMouseDown, {once: true});
onEnd && onEnd({x: event.pageX, y: event.pageY}); onEnd && onEnd({x: event.pageX, y: event.pageY, event});
}; };
const onMouseDown = (event: MouseEvent) => { const onMouseDown = (event: MouseEvent) => {
@ -27,7 +27,7 @@ export default function attachGrabListeners(element: HTMLElement,
return; return;
} }
onStart({x: event.pageX, y: event.pageY}); onStart({x: event.pageX, y: event.pageY, event});
onMouseMove(event); onMouseMove(event);
document.addEventListener('mousemove', onMouseMove); document.addEventListener('mousemove', onMouseMove);
@ -39,17 +39,17 @@ export default function attachGrabListeners(element: HTMLElement,
// * Touch // * Touch
const onTouchMove = (event: TouchEvent) => { const onTouchMove = (event: TouchEvent) => {
event.preventDefault(); event.preventDefault();
onMove({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true}); onMove({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true, event});
}; };
const onTouchEnd = (event: TouchEvent) => { const onTouchEnd = (event: TouchEvent) => {
document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchmove', onTouchMove);
element.addEventListener('touchstart', onTouchStart, {passive: false, once: true}); element.addEventListener('touchstart', onTouchStart, {passive: false, once: true});
onEnd && onEnd({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true}); onEnd && onEnd({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true, event});
}; };
const onTouchStart = (event: TouchEvent) => { const onTouchStart = (event: TouchEvent) => {
onStart({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true}); onStart({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true, event});
onTouchMove(event); onTouchMove(event);
document.addEventListener('touchmove', onTouchMove, {passive: false}); document.addEventListener('touchmove', onTouchMove, {passive: false});

25
src/helpers/filterChatPhotosMessages.ts

@ -0,0 +1,25 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { Message, MessageAction } from "../layer";
import type { MyMessage } from "../lib/appManagers/appMessagesManager";
import { forEachReverse } from "./array";
export default function filterChatPhotosMessages(value: {
count: number;
next_rate: number;
offset_id_offset: number;
history: MyMessage[];
}) {
forEachReverse(value.history, (message, idx, arr) => {
if(!((message as Message.messageService).action as MessageAction.messageActionChatEditPhoto).photo) {
arr.splice(idx, 1);
if(value.count !== undefined) {
--value.count;
}
}
});
}

29
src/helpers/listLoader.ts

@ -7,17 +7,17 @@
import { forEachReverse } from "./array"; import { forEachReverse } from "./array";
import { safeAssign } from "./object"; import { safeAssign } from "./object";
export type ListLoaderOptions<T extends {}> = { export type ListLoaderOptions<T extends {}, P extends {}> = {
loadMore: ListLoader<T>['loadMore'], loadMore: ListLoader<T, P>['loadMore'],
loadCount?: ListLoader<T>['loadCount'], loadCount?: ListLoader<T, P>['loadCount'],
loadWhenLeft?: ListLoader<T>['loadWhenLeft'], loadWhenLeft?: ListLoader<T, P>['loadWhenLeft'],
processItem?: ListLoader<T>['processItem'], processItem?: ListLoader<T, P>['processItem'],
onJump?: ListLoader<T>['onJump'], onJump?: ListLoader<T, P>['onJump'],
onLoadedMore?: ListLoader<T>['onLoadedMore'] onLoadedMore?: ListLoader<T, P>['onLoadedMore']
}; };
export type ListLoaderResult<T extends {}> = {count: number, items: any[]}; export type ListLoaderResult<T extends {}> = {count: number, items: any[]};
export default class ListLoader<T extends {}> { export default class ListLoader<T extends {}, P extends {}> {
public current: T; public current: T;
public previous: T[] = []; public previous: T[] = [];
public next: T[] = []; public next: T[] = [];
@ -25,7 +25,7 @@ export default class ListLoader<T extends {}> {
public reverse = false; // reverse means next = higher msgid public reverse = false; // reverse means next = higher msgid
protected loadMore: (anchor: T, older: boolean, loadCount: number) => Promise<ListLoaderResult<T>>; protected loadMore: (anchor: T, older: boolean, loadCount: number) => Promise<ListLoaderResult<T>>;
protected processItem: (item: any) => T; protected processItem: (item: P) => T;
protected loadCount = 50; protected loadCount = 50;
protected loadWhenLeft = 20; protected loadWhenLeft = 20;
@ -37,7 +37,7 @@ export default class ListLoader<T extends {}> {
protected loadPromiseUp: Promise<void>; protected loadPromiseUp: Promise<void>;
protected loadPromiseDown: Promise<void>; protected loadPromiseDown: Promise<void>;
constructor(options: ListLoaderOptions<T>) { constructor(options: ListLoaderOptions<T, P>) {
safeAssign(this, options); safeAssign(this, options);
} }
@ -51,15 +51,15 @@ export default class ListLoader<T extends {}> {
return this.count !== undefined ? this.previous.length : -1; return this.count !== undefined ? this.previous.length : -1;
} }
public reset() { public reset(loadedAll = false) {
this.current = undefined; this.current = undefined;
this.previous = []; this.previous = [];
this.next = []; this.next = [];
this.loadedAllUp = this.loadedAllDown = false; this.loadedAllUp = this.loadedAllDown = loadedAll;
this.loadPromiseUp = this.loadPromiseDown = null; this.loadPromiseUp = this.loadPromiseDown = null;
} }
public go(length: number) { public go(length: number, dispatchJump = true) {
let items: T[], item: T; let items: T[], item: T;
if(length > 0) { if(length > 0) {
items = this.next.splice(0, length); items = this.next.splice(0, length);
@ -88,7 +88,8 @@ export default class ListLoader<T extends {}> {
} }
this.current = item; this.current = item;
this.onJump && this.onJump(item, length > 0); dispatchJump && this.onJump && this.onJump(item, length > 0);
return this.current;
} }
// нет смысла делать проверку для reverse и loadMediaPromise // нет смысла делать проверку для reverse и loadMediaPromise

4
src/helpers/object.ts

@ -59,9 +59,9 @@ export function defineNotNumerableProperties(obj: {[key: string]: any}, names: s
//console.log('defineNotNumerableProperties time:', performance.now() - perf); //console.log('defineNotNumerableProperties time:', performance.now() - perf);
} }
export function getObjectKeysAndSort(object: any, sort: 'asc' | 'desc' = 'asc') { export function getObjectKeysAndSort(object: {[key: string]: any}, sort: 'asc' | 'desc' = 'asc') {
if(!object) return []; if(!object) return [];
const ids = Object.keys(object).map(i => +i); const ids = object instanceof Map ? [...object.keys()] : Object.keys(object).map(i => +i);
if(sort === 'asc') return ids.sort((a, b) => a - b); if(sort === 'asc') return ids.sort((a, b) => a - b);
else return ids.sort((a, b) => b - a); else return ids.sort((a, b) => b - a);
} }

161
src/helpers/searchListLoader.ts

@ -0,0 +1,161 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { MediaSearchContext } from "../components/appMediaPlaybackController";
import type { SearchSuperContext } from "../components/appSearchSuper.";
import type { Message } from "../layer";
import appMessagesIdsManager from "../lib/appManagers/appMessagesIdsManager";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import rootScope from "../lib/rootScope";
import { forEachReverse } from "./array";
import filterChatPhotosMessages from "./filterChatPhotosMessages";
import ListLoader, { ListLoaderOptions } from "./listLoader";
export default class SearchListLoader<Item extends {mid: number, peerId: number}> extends ListLoader<Item, Message.message> {
public searchContext: MediaSearchContext;
public onEmptied: () => void;
constructor(options: Omit<ListLoaderOptions<Item, Message.message>, 'loadMore'> & {onEmptied?: () => void} = {}) {
super({
...options,
loadMore: (anchor, older, loadCount) => {
const backLimit = older ? 0 : loadCount;
let maxId = this.current?.mid;
if(anchor) maxId = anchor.mid;
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1);
return appMessagesManager.getSearch({
...this.searchContext,
peerId: this.searchContext.peerId || anchor?.peerId,
maxId,
limit: backLimit ? 0 : loadCount,
backLimit
}).then(value => {
/* if(DEBUG) {
this.log('loaded more media by maxId:', maxId, value, older, this.reverse);
} */
if(this.searchContext.inputFilter._ === 'inputMessagesFilterChatPhotos') {
filterChatPhotosMessages(value);
}
if(value.next_rate) {
this.searchContext.nextRate = value.next_rate;
}
return {count: value.count, items: value.history};
});
},
processItem: (message) => {
const filtered = this.filterMids([message.mid]);
if(!filtered.length) {
return;
}
return options.processItem(message);
}
});
rootScope.addEventListener('history_delete', this.onHistoryDelete);
rootScope.addEventListener('history_multiappend', this.onHistoryMultiappend);
rootScope.addEventListener('message_sent', this.onMessageSent);
}
protected filterMids(mids: number[]) {
const storage = this.searchContext.isScheduled ?
appMessagesManager.getScheduledMessagesStorage(this.searchContext.peerId) :
appMessagesManager.getMessagesStorage(this.searchContext.peerId);
const filtered = appMessagesManager.filterMessagesByInputFilter(this.searchContext.inputFilter._, mids, storage, mids.length) as Message.message[];
return filtered;
}
protected onHistoryDelete = ({peerId, msgs}: {peerId: number, msgs: Set<number>}) => {
const shouldBeDeleted = (item: Item) => item.peerId === peerId && msgs.has(item.mid);
const filter = (item: Item, idx: number, arr: Item[]) => {
if(shouldBeDeleted(item)) {
arr.splice(idx, 1);
}
};
forEachReverse(this.previous, filter);
forEachReverse(this.next, filter);
if(this.current && shouldBeDeleted(this.current)) {
if(this.go(1)) {
this.previous.splice(this.previous.length - 1, 1);
} else if(this.go(-1)) {
this.next.splice(0, 1);
} else if(this.onEmptied) {
this.onEmptied();
}
}
};
protected onHistoryMultiappend = (obj: {
[peerId: string]: Set<number>;
}) => {
if(this.searchContext.folderId !== undefined) {
return;
}
// because it's reversed
if(!this.loadedAllUp || this.loadPromiseUp) {
return;
}
const mids = obj[this.searchContext.peerId];
if(!mids) {
return;
}
const sorted = Array.from(mids).sort((a, b) => a - b);
const filtered = this.filterMids(sorted);
const targets = filtered.map(message => this.processItem(message)).filter(Boolean);
if(targets.length) {
this.next.push(...targets);
}
};
protected onMessageSent = ({message}: {message: Message.message}) => {
this.onHistoryMultiappend({
[message.peerId]: new Set([message.mid])
});
};
public setSearchContext(context: SearchSuperContext) {
this.searchContext = context;
if(this.searchContext.folderId !== undefined) {
this.loadedAllUp = true;
if(this.searchContext.nextRate === undefined) {
this.loadedAllDown = true;
}
}
if(this.searchContext.inputFilter._ === 'inputMessagesFilterChatPhotos') {
this.loadedAllUp = true;
}
if(!this.searchContext.useSearch) {
this.loadedAllDown = this.loadedAllUp = true;
}
}
public reset() {
super.reset();
this.searchContext = undefined;
}
public cleanup() {
this.reset();
rootScope.removeEventListener('history_delete', this.onHistoryDelete);
rootScope.removeEventListener('history_multiappend', this.onHistoryMultiappend);
rootScope.removeEventListener('message_sent', this.onMessageSent);
this.onEmptied = undefined;
}
}

1
src/layer.d.ts vendored

@ -830,6 +830,7 @@ export namespace Message {
pinned?: true, pinned?: true,
unread?: true, unread?: true,
is_outgoing?: true, is_outgoing?: true,
is_scheduled?: true,
}>, }>,
id: number, id: number,
from_id?: Peer, from_id?: Peer,

33
src/lib/appManagers/apiUpdatesManager.ts

@ -11,7 +11,7 @@
//import apiManager from '../mtproto/apiManager'; //import apiManager from '../mtproto/apiManager';
import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug'; import DEBUG, { MOUNT_CLASS_TO } from '../../config/debug';
import { Message, MessageFwdHeader, Peer, Update, Updates } from '../../layer'; import { Message, MessageEntity, MessageFwdHeader, Peer, Update, Updates } from '../../layer';
import { logger, LogTypes } from '../logger'; import { logger, LogTypes } from '../logger';
import apiManager from '../mtproto/mtprotoworker'; import apiManager from '../mtproto/mtprotoworker';
import rootScope from '../rootScope'; import rootScope from '../rootScope';
@ -22,6 +22,8 @@ import appPeersManager from "./appPeersManager";
import appStateManager from './appStateManager'; import appStateManager from './appStateManager';
import serverTimeManager from '../mtproto/serverTimeManager'; import serverTimeManager from '../mtproto/serverTimeManager';
import assumeType from '../../helpers/assumeType'; import assumeType from '../../helpers/assumeType';
import noop from '../../helpers/noop';
import RichTextProcessor from '../richtextprocessor';
type UpdatesState = { type UpdatesState = {
pendingPtsUpdates: (Update & {pts: number, pts_count: number})[], pendingPtsUpdates: (Update & {pts: number, pts_count: number})[],
@ -633,6 +635,8 @@ export class ApiUpdatesManager {
appStateManager.getState().then(_state => { appStateManager.getState().then(_state => {
const state = _state.updates; const state = _state.updates;
const newVersion = appStateManager.newVersion/* || '0.8.6' */;
//rootScope.broadcast('state_synchronizing'); //rootScope.broadcast('state_synchronizing');
if(!state || !state.pts || !state.date || !state.seq) { if(!state || !state.pts || !state.date || !state.seq) {
this.log('will get new state'); this.log('will get new state');
@ -679,6 +683,33 @@ export class ApiUpdatesManager {
// this.updatesState.syncLoading.then(() => { // this.updatesState.syncLoading.then(() => {
this.setProxy(); this.setProxy();
// }); // });
if(newVersion) {
this.updatesState.syncLoading.then(() => {
fetch('changelogs/' + newVersion + '.md')
.then(res => res.text())
.then(text => {
const pre = `**Telegram was updated to version alpha ${newVersion}**\n\n`;
text = pre + text;
const entities: MessageEntity[] = [];
const message = RichTextProcessor.parseMarkdown(text, entities);
const update: Update.updateServiceNotification = {
_: 'updateServiceNotification',
entities,
message,
type: 'local',
pFlags: {},
inbox_date: Date.now() / 1000 | 0,
media: undefined
};
this.processLocalUpdate(update);
})
.catch(noop);
});
}
}); });
} }
} }

2
src/lib/appManagers/appDialogsManager.ts

@ -461,7 +461,7 @@ export class AppDialogsManager {
appStateManager.getState().then((state) => { appStateManager.getState().then((state) => {
return this.onStateLoaded(state); return this.onStateLoaded(state);
}).then(() => { }).then(() => {
//return; // return;
const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0); const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0);
const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1); const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1);

16
src/lib/appManagers/appImManager.ts

@ -876,22 +876,6 @@ export class AppImManager {
private init() { private init() {
document.addEventListener('paste', this.onDocumentPaste, true); document.addEventListener('paste', this.onDocumentPaste, true);
rootScope.addEventListener('history_multiappend', (msgIdsByPeer) => {
for(const peerId in msgIdsByPeer) {
appSidebarRight.sharedMediaTab.renderNewMessages(+peerId, Array.from(msgIdsByPeer[peerId]));
}
});
rootScope.addEventListener('history_delete', ({peerId, msgs}) => {
appSidebarRight.sharedMediaTab.deleteDeletedMessages(peerId, Array.from(msgs));
});
// Calls when message successfully sent and we have an id
rootScope.addEventListener('message_sent', ({storage, tempId, mid}) => {
const message = appMessagesManager.getMessageFromStorage(storage, mid);
appSidebarRight.sharedMediaTab.renderNewMessages(message.peerId, [mid]);
});
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
this.attachDragAndDropListeners(); this.attachDragAndDropListeners();
} }

8
src/lib/appManagers/appMessagesIdsManager.ts

@ -30,6 +30,10 @@ export class AppMessagesIdsManager {
* * will ignore outgoing offset * * will ignore outgoing offset
*/ */
public getServerMessageId(messageId: number) { public getServerMessageId(messageId: number) {
return this.clearMessageId(messageId, true);
}
public clearMessageId(messageId: number, toServer?: boolean) {
const q = AppMessagesIdsManager.MESSAGE_ID_OFFSET; const q = AppMessagesIdsManager.MESSAGE_ID_OFFSET;
if(messageId < q) { // id 0 -> mid 0xFFFFFFFF, so 0xFFFFFFFF must convert to 0 if(messageId < q) { // id 0 -> mid 0xFFFFFFFF, so 0xFFFFFFFF must convert to 0
return messageId; return messageId;
@ -41,11 +45,11 @@ export class AppMessagesIdsManager {
messageId -= used + 1; messageId -= used + 1;
} }
return (messageId - q) / AppMessagesIdsManager.MESSAGE_ID_INCREMENT; return toServer ? (messageId - q) / AppMessagesIdsManager.MESSAGE_ID_INCREMENT : messageId;
} }
public incrementMessageId(messageId: number, increment: number) { public incrementMessageId(messageId: number, increment: number) {
return this.generateMessageId(appMessagesIdsManager.getServerMessageId(messageId) + increment); return this.generateMessageId(this.getServerMessageId(messageId) + increment);
} }
} }

377
src/lib/appManagers/appMessagesManager.ts

@ -109,10 +109,7 @@ export type PinnedStorage = Partial<{
count: number, count: number,
maxId: number maxId: number
}>; }>;
export type MessagesStorage = { export type MessagesStorage = Map<number, any>;
//generateIndex: (message: any) => void
[mid: string]: any
};
export type MyMessageActionType = Message.messageService['action']['_']; export type MyMessageActionType = Message.messageService['action']['_'];
@ -1586,7 +1583,7 @@ export class AppMessagesManager {
fromId: peerId fromId: peerId
} as Message.messageService; } as Message.messageService;
this.getMessagesStorage(peerId)[maxId] = message; this.getMessagesStorage(peerId).set(maxId, message);
return message; return message;
} }
@ -1628,7 +1625,7 @@ export class AppMessagesManager {
historyStorage.history.delete(tempId); historyStorage.history.delete(tempId);
delete this.pendingByRandomId[randomId]; delete this.pendingByRandomId[randomId];
delete storage[tempId]; storage.delete(tempId);
return true; return true;
} }
@ -1936,17 +1933,17 @@ export class AppMessagesManager {
return promise; return promise;
} }
public getMessageFromStorage(storage: MessagesStorage, messageId: number) { public getMessageFromStorage(storage: MessagesStorage, mid: number) {
return storage && storage[messageId] || { return storage && storage.get(mid) || {
_: 'messageEmpty', _: 'messageEmpty',
id: messageId, id: mid,
deleted: true, deleted: true,
pFlags: {} pFlags: {}
}; };
} }
private createMessageStorage() { private createMessageStorage() {
const storage: MessagesStorage = {} as any; const storage: MessagesStorage = new Map();
/* let num = 0; /* let num = 0;
Object.defineProperty(storage, 'num', { Object.defineProperty(storage, 'num', {
@ -1977,7 +1974,7 @@ export class AppMessagesManager {
continue; continue;
} }
const message = this.messagesStorageByPeerId[peerId][messageId]; const message = this.messagesStorageByPeerId[peerId].get(messageId);
if(message) { if(message) {
return message; return message;
} }
@ -2085,15 +2082,27 @@ export class AppMessagesManager {
} }
return this.doFlushHistory(appPeersManager.getInputPeerById(peerId), justClear, revoke).then(() => { return this.doFlushHistory(appPeersManager.getInputPeerById(peerId), justClear, revoke).then(() => {
delete this.historiesStorage[peerId]; [
delete this.messagesStorageByPeerId[peerId]; this.historiesStorage,
delete this.scheduledMessagesStorage[peerId]; this.threadsStorage,
delete this.threadsStorage[peerId]; this.searchesStorage,
delete this.searchesStorage[peerId]; this.pinnedMessages,
delete this.pinnedMessages[peerId]; this.pendingAfterMsgs,
delete this.pendingAfterMsgs[peerId]; this.pendingTopMsgs,
delete this.pendingTopMsgs[peerId]; this.needSingleMessages
delete this.needSingleMessages[peerId]; ].forEach(s => {
delete s[peerId];
});
[
this.messagesStorageByPeerId,
this.scheduledMessagesStorage
].forEach(s => {
const ss = s[peerId];
if(ss) {
ss.clear();
}
});
if(justClear) { if(justClear) {
rootScope.dispatchEvent('dialog_flush', {peerId}); rootScope.dispatchEvent('dialog_flush', {peerId});
@ -2166,12 +2175,11 @@ export class AppMessagesManager {
if(!affectedHistory.offset) { if(!affectedHistory.offset) {
const storage = this.getMessagesStorage(peerId); const storage = this.getMessagesStorage(peerId);
for(const mid in storage) { storage.forEach((message) => {
const message = storage[mid];
if(message.pFlags.pinned) { if(message.pFlags.pinned) {
delete message.pFlags.pinned; delete message.pFlags.pinned;
} }
} });
rootScope.dispatchEvent('peer_pinned_messages', {peerId, unpinAll: true}); rootScope.dispatchEvent('peer_pinned_messages', {peerId, unpinAll: true});
delete this.pinnedMessages[peerId]; delete this.pinnedMessages[peerId];
@ -2186,8 +2194,7 @@ export class AppMessagesManager {
public getAlbumText(grouped_id: string) { public getAlbumText(grouped_id: string) {
const group = this.groupedMessagesStorage[grouped_id]; const group = this.groupedMessagesStorage[grouped_id];
let foundMessages = 0, message: string, totalEntities: MessageEntity[], entities: MessageEntity[]; let foundMessages = 0, message: string, totalEntities: MessageEntity[], entities: MessageEntity[];
for(const i in group) { for(const [mid, m] of group) {
const m = group[i];
if(m.message) { if(m.message) {
if(++foundMessages > 1) break; if(++foundMessages > 1) break;
message = m.message; message = m.message;
@ -2219,8 +2226,7 @@ export class AppMessagesManager {
const out: MyMessage[] = []; const out: MyMessage[] = [];
if(message.grouped_id) { if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id]; const storage = this.groupedMessagesStorage[message.grouped_id];
for(const mid in storage) { for(const [mid, message] of storage) {
const message = storage[mid];
if(verify(message)) { if(verify(message)) {
out.push(message); out.push(message);
} }
@ -2276,8 +2282,8 @@ export class AppMessagesManager {
message.mid = mid; message.mid = mid;
if(message.grouped_id) { if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = {}); const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = new Map());
storage[mid] = message; storage.set(mid, message);
} }
const dialog = this.getDialogOnly(peerId); const dialog = this.getDialogOnly(peerId);
@ -2527,7 +2533,7 @@ export class AppMessagesManager {
this.wrapMessageEntities(message); this.wrapMessageEntities(message);
} }
storage[mid] = message; storage.set(mid, message);
}); });
/* if(groups) { /* if(groups) {
@ -3253,6 +3259,140 @@ export class AppMessagesManager {
}); });
} }
public filterMessagesByInputFilter(inputFilter: MyInputMessagesFilter, history: number[], storage: MessagesStorage, limit: number) {
const foundMsgs: MyMessage[] = [];
if(!history.length) {
return foundMsgs;
}
let filtering = true;
const neededContents: Partial<{
[messageMediaType in MessageMedia['_']]: boolean
}> & Partial<{
avatar: boolean,
url: boolean
}> = {},
neededDocTypes: MyDocument['type'][] = [],
excludeDocTypes: MyDocument['type'][] = []/* ,
neededFlags: string[] = [] */;
switch(inputFilter) {
case 'inputMessagesFilterPhotos':
neededContents['messageMediaPhoto'] = true;
break;
case 'inputMessagesFilterPhotoVideo':
neededContents['messageMediaPhoto'] = true;
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('video');
break;
case 'inputMessagesFilterVideo':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('video');
break;
case 'inputMessagesFilterDocument':
neededContents['messageMediaDocument'] = true;
excludeDocTypes.push('video');
break;
case 'inputMessagesFilterVoice':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('voice');
break;
case 'inputMessagesFilterRoundVoice':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('round', 'voice');
break;
case 'inputMessagesFilterRoundVideo':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('round');
break;
case 'inputMessagesFilterMusic':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('audio');
break;
case 'inputMessagesFilterUrl':
neededContents['url'] = true;
break;
case 'inputMessagesFilterChatPhotos':
neededContents['avatar'] = true;
break;
/* case 'inputMessagesFilterPinned':
neededFlags.push('pinned');
break; */
/* case 'inputMessagesFilterMyMentions':
neededContents['mentioned'] = true;
break; */
default:
filtering = false;
break;
/* return Promise.resolve({
count: 0,
next_rate: 0,
history: [] as number[]
}); */
}
if(!filtering) {
return foundMsgs;
}
for(let i = 0, length = history.length; i < length; ++i) {
const message: Message.message | Message.messageService = storage.get(history[i]);
if(!message) continue;
//|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ === 'messageEntityMention'));
let found = false;
if(message._ === 'message') {
if(message.media && neededContents[message.media._]/* && !message.fwd_from */) {
const doc = (message.media as MessageMedia.messageMediaDocument).document as MyDocument;
if((neededDocTypes.length && !neededDocTypes.includes(doc.type))
|| excludeDocTypes.includes(doc.type)) {
continue;
}
found = true;
} else if(neededContents['url'] && message.message) {
const goodEntities = ['messageEntityTextUrl', 'messageEntityUrl'];
if((message.totalEntities as MessageEntity[]).find(e => goodEntities.includes(e._)) || RichTextProcessor.matchUrl(message.message)) {
found = true;
}
}
} else if(neededContents['avatar'] &&
message.action &&
([
'messageActionChannelEditPhoto' as const,
'messageActionChatEditPhoto' as const,
'messageActionChannelEditVideo' as const,
'messageActionChatEditVideo' as const
] as MessageAction['_'][]).includes(message.action._)) {
found = true;
}/* else if(neededFlags.find(flag => message.pFlags[flag])) {
found = true;
} */
if(found) {
foundMsgs.push(message);
if(foundMsgs.length >= limit) {
break;
}
}
}
return foundMsgs;
}
public getSearch({peerId, query, inputFilter, maxId, limit, nextRate, backLimit, threadId, folderId, minDate, maxDate}: { public getSearch({peerId, query, inputFilter, maxId, limit, nextRate, backLimit, threadId, folderId, minDate, maxDate}: {
peerId?: number, peerId?: number,
maxId?: number, maxId?: number,
@ -3283,7 +3423,7 @@ export class AppMessagesManager {
minDate = minDate ? minDate / 1000 | 0 : 0; minDate = minDate ? minDate / 1000 | 0 : 0;
maxDate = maxDate ? maxDate / 1000 | 0 : 0; maxDate = maxDate ? maxDate / 1000 | 0 : 0;
const foundMsgs: Message.message[] = []; let foundMsgs: MyMessage[] = [];
//this.log('search', maxId); //this.log('search', maxId);
@ -3304,124 +3444,7 @@ export class AppMessagesManager {
storage = beta ? storage = beta ?
this.getSearchStorage(peerId, inputFilter._) as any : this.getSearchStorage(peerId, inputFilter._) as any :
this.getHistoryStorage(peerId); this.getHistoryStorage(peerId);
let filtering = true; foundMsgs = this.filterMessagesByInputFilter(inputFilter._, storage.history.slice, this.getMessagesStorage(peerId), limit);
const history = /* maxId ? storage.history.slice(storage.history.indexOf(maxId) + 1) : */storage.history;
if(storage !== undefined && history.length) {
const neededContents: {
[messageMediaType: string]: boolean
} = {},
neededDocTypes: string[] = [],
excludeDocTypes: string[] = []/* ,
neededFlags: string[] = [] */;
switch(inputFilter._) {
case 'inputMessagesFilterPhotos':
neededContents['messageMediaPhoto'] = true;
break;
case 'inputMessagesFilterPhotoVideo':
neededContents['messageMediaPhoto'] = true;
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('video');
break;
case 'inputMessagesFilterVideo':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('video');
break;
case 'inputMessagesFilterDocument':
neededContents['messageMediaDocument'] = true;
excludeDocTypes.push('video');
break;
case 'inputMessagesFilterVoice':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('voice');
break;
case 'inputMessagesFilterRoundVoice':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('round', 'voice');
break;
case 'inputMessagesFilterRoundVideo':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('round');
break;
case 'inputMessagesFilterMusic':
neededContents['messageMediaDocument'] = true;
neededDocTypes.push('audio');
break;
case 'inputMessagesFilterUrl':
neededContents['url'] = true;
break;
case 'inputMessagesFilterChatPhotos':
neededContents['avatar'] = true;
break;
/* case 'inputMessagesFilterPinned':
neededFlags.push('pinned');
break; */
/* case 'inputMessagesFilterMyMentions':
neededContents['mentioned'] = true;
break; */
default:
filtering = false;
break;
/* return Promise.resolve({
count: 0,
next_rate: 0,
history: [] as number[]
}); */
}
if(filtering) {
const storage = this.getMessagesStorage(peerId);
for(let i = 0, length = history.length; i < length; i++) {
const message = storage[history.slice[i]];
if(!message) continue;
//|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ === 'messageEntityMention'));
let found = false;
if(message.media && neededContents[message.media._] && !message.fwd_from) {
if(message.media._ === 'messageMediaDocument') {
if((neededDocTypes.length && !neededDocTypes.includes(message.media.document.type))
|| excludeDocTypes.includes(message.media.document.type)) {
continue;
}
}
found = true;
} else if(neededContents['url'] && message.message) {
const goodEntities = ['messageEntityTextUrl', 'messageEntityUrl'];
if((message.totalEntities as MessageEntity[]).find(e => goodEntities.includes(e._)) || RichTextProcessor.matchUrl(message.message)) {
found = true;
}
} else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto', 'messageActionChannelEditVideo', 'messageActionChatEditVideo'].includes(message.action._)) {
found = true;
}/* else if(neededFlags.find(flag => message.pFlags[flag])) {
found = true;
} */
if(found) {
foundMsgs.push(message);
if(foundMsgs.length >= limit) {
break;
}
}
}
}
}
} }
if(foundMsgs.length) { if(foundMsgs.length) {
@ -3789,10 +3812,11 @@ export class AppMessagesManager {
apiPromise.finally(() => { apiPromise.finally(() => {
delete historyStorage.readPromise; delete historyStorage.readPromise;
this.log('readHistory: promise finally', maxId, historyStorage.readMaxId); const {readMaxId} = historyStorage;
this.log('readHistory: promise finally', maxId, readMaxId);
if(historyStorage.readMaxId > maxId) { if(readMaxId > maxId) {
this.readHistory(peerId, historyStorage.readMaxId, threadId, true); this.readHistory(peerId, readMaxId, threadId, true);
} }
}); });
@ -4011,7 +4035,7 @@ export class AppMessagesManager {
const isLocalThreadUpdate = update._ === 'updateNewDiscussionMessage'; const isLocalThreadUpdate = update._ === 'updateNewDiscussionMessage';
// * temporary save the message for info (peerId, reply mids...) // * temporary save the message for info (peerId, reply mids...)
this.saveMessages([message], {storage: {}}); this.saveMessages([message], {storage: new Map()});
const threadKey = this.getThreadKey(message); const threadKey = this.getThreadKey(message);
const threadId = threadKey ? +threadKey.split('_')[1] : undefined; const threadId = threadKey ? +threadKey.split('_')[1] : undefined;
@ -4200,7 +4224,7 @@ export class AppMessagesManager {
const peerId = this.getMessagePeer(message); const peerId = this.getMessagePeer(message);
const mid = appMessagesIdsManager.generateMessageId(message.id); const mid = appMessagesIdsManager.generateMessageId(message.id);
const storage = this.getMessagesStorage(peerId); const storage = this.getMessagesStorage(peerId);
if(storage[mid] === undefined) { if(!storage.has(mid)) {
return; return;
} }
@ -4269,12 +4293,12 @@ export class AppMessagesManager {
} }
for(let i = 0, length = history.length; i < length; i++) { for(let i = 0, length = history.length; i < length; i++) {
const messageId = history[i]; const mid = history[i];
if(messageId > maxId) { if(mid > maxId) {
continue; continue;
} }
const message: MyMessage = storage[messageId]; const message: MyMessage = storage.get(mid);
if(message.pFlags.out !== isOut) { if(message.pFlags.out !== isOut) {
continue; continue;
@ -4309,7 +4333,7 @@ export class AppMessagesManager {
} }
} }
appNotificationsManager.cancel('msg' + messageId); appNotificationsManager.cancel('msg' + mid);
} }
} }
@ -4557,14 +4581,14 @@ export class AppMessagesManager {
const messages = update.messages.map(id => appMessagesIdsManager.generateMessageId(id)); const messages = update.messages.map(id => appMessagesIdsManager.generateMessageId(id));
const storage = this.getMessagesStorage(peerId); const storage = this.getMessagesStorage(peerId);
const missingMessages = messages.filter(mid => !storage[mid]); const missingMessages = messages.filter(mid => !storage.has(mid));
const getMissingPromise = missingMessages.length ? Promise.all(missingMessages.map(mid => this.wrapSingleMessage(peerId, mid))) : Promise.resolve(); const getMissingPromise = missingMessages.length ? Promise.all(missingMessages.map(mid => this.wrapSingleMessage(peerId, mid))) : Promise.resolve();
getMissingPromise.finally(() => { getMissingPromise.finally(() => {
const werePinned = update.pFlags?.pinned; const werePinned = update.pFlags?.pinned;
if(werePinned) { if(werePinned) {
for(const mid of messages) { for(const mid of messages) {
//storage.history.push(mid); //storage.history.push(mid);
const message = storage[mid]; const message = storage.get(mid);
message.pFlags.pinned = true; message.pFlags.pinned = true;
} }
@ -4577,7 +4601,7 @@ export class AppMessagesManager {
} else { } else {
for(const mid of messages) { for(const mid of messages) {
//storage.history.findAndSplice(_mid => _mid === mid); //storage.history.findAndSplice(_mid => _mid === mid);
const message = storage[mid]; const message = storage.get(mid);
delete message.pFlags.pinned; delete message.pFlags.pinned;
} }
} }
@ -4772,7 +4796,7 @@ export class AppMessagesManager {
} }
public finalizePendingMessageCallbacks(storage: MessagesStorage, tempId: number, mid: number) { public finalizePendingMessageCallbacks(storage: MessagesStorage, tempId: number, mid: number) {
const message = this.getMessageFromStorage(storage, mid); const message: Message.message = this.getMessageFromStorage(storage, mid);
const callbacks = this.tempFinalizeCallbacks[tempId]; const callbacks = this.tempFinalizeCallbacks[tempId];
//this.log.warn(callbacks, tempId); //this.log.warn(callbacks, tempId);
if(callbacks !== undefined) { if(callbacks !== undefined) {
@ -4787,10 +4811,10 @@ export class AppMessagesManager {
// set cached url to media // set cached url to media
if(message.media) { if(message.media) {
if(message.media.photo) { const {photo: newPhoto, document: newDoc} = message.media as any;
if(newPhoto) {
const photo = appPhotosManager.getPhoto('' + tempId); const photo = appPhotosManager.getPhoto('' + tempId);
if(/* photo._ !== 'photoEmpty' */photo) { if(/* photo._ !== 'photoEmpty' */photo) {
const newPhoto = message.media.photo as MyPhoto;
const newPhotoSize = newPhoto.sizes[newPhoto.sizes.length - 1]; const newPhotoSize = newPhoto.sizes[newPhoto.sizes.length - 1];
const cacheContext = appDownloadManager.getCacheContext(newPhoto, newPhotoSize.type); const cacheContext = appDownloadManager.getCacheContext(newPhoto, newPhotoSize.type);
const oldCacheContext = appDownloadManager.getCacheContext(photo, 'full'); const oldCacheContext = appDownloadManager.getCacheContext(photo, 'full');
@ -4802,11 +4826,10 @@ export class AppMessagesManager {
const fileName = getFileNameByLocation(downloadOptions.location); const fileName = getFileNameByLocation(downloadOptions.location);
appDownloadManager.fakeDownload(fileName, oldCacheContext.url); appDownloadManager.fakeDownload(fileName, oldCacheContext.url);
} }
} else if(message.media.document) { } else if(newDoc) {
const doc = appDocsManager.getDoc('' + tempId); const doc = appDocsManager.getDoc('' + tempId);
if(doc) { if(doc) {
if(/* doc._ !== 'documentEmpty' && */doc.type && doc.type !== 'sticker') { if(/* doc._ !== 'documentEmpty' && */doc.type && doc.type !== 'sticker') {
const newDoc = message.media.document;
const cacheContext = appDownloadManager.getCacheContext(newDoc); const cacheContext = appDownloadManager.getCacheContext(newDoc);
const oldCacheContext = appDownloadManager.getCacheContext(doc); const oldCacheContext = appDownloadManager.getCacheContext(doc);
Object.assign(cacheContext, oldCacheContext); Object.assign(cacheContext, oldCacheContext);
@ -4815,18 +4838,18 @@ export class AppMessagesManager {
appDownloadManager.fakeDownload(fileName, oldCacheContext.url); appDownloadManager.fakeDownload(fileName, oldCacheContext.url);
} }
} }
} else if(message.media.poll) { } else if((message.media as MessageMedia.messageMediaPoll).poll) {
delete appPollsManager.polls[tempId]; delete appPollsManager.polls[tempId];
delete appPollsManager.results[tempId]; delete appPollsManager.results[tempId];
} }
} }
const tempMessage = this.getMessageFromStorage(storage, tempId); const tempMessage = this.getMessageFromStorage(storage, tempId);
delete storage[tempId]; storage.delete(tempId);
this.handleReleasingMessage(tempMessage, storage); this.handleReleasingMessage(tempMessage, storage);
rootScope.dispatchEvent('message_sent', {storage, tempId, tempMessage, mid}); rootScope.dispatchEvent('message_sent', {storage, tempId, tempMessage, mid, message});
} }
public incrementMaxSeenId(maxId: number) { public incrementMaxSeenId(maxId: number) {
@ -4926,12 +4949,16 @@ export class AppMessagesManager {
return this.scheduledMessagesStorage[peerId] ?? (this.scheduledMessagesStorage[peerId] = this.createMessageStorage()); return this.scheduledMessagesStorage[peerId] ?? (this.scheduledMessagesStorage[peerId] = this.createMessageStorage());
} }
public getScheduledMessageByPeer(peerId: number, mid: number) {
return this.getMessageFromStorage(this.getScheduledMessagesStorage(peerId), mid);
}
public getScheduledMessages(peerId: number): Promise<number[]> { public getScheduledMessages(peerId: number): Promise<number[]> {
if(!this.canSendToPeer(peerId)) return Promise.resolve([]); if(!this.canSendToPeer(peerId)) return Promise.resolve([]);
const storage = this.getScheduledMessagesStorage(peerId); const storage = this.getScheduledMessagesStorage(peerId);
if(Object.keys(storage).length) { if(storage.size) {
return Promise.resolve(Object.keys(storage).map(id => +id)); return Promise.resolve([...storage.keys()]);
} }
return apiManager.invokeApiSingle('messages.getScheduledHistory', { return apiManager.invokeApiSingle('messages.getScheduledHistory', {
@ -4944,7 +4971,7 @@ export class AppMessagesManager {
const storage = this.getScheduledMessagesStorage(peerId); const storage = this.getScheduledMessagesStorage(peerId);
this.saveMessages(historyResult.messages, {storage, isScheduled: true}); this.saveMessages(historyResult.messages, {storage, isScheduled: true});
return Object.keys(storage).map(id => +id); return [...storage.keys()];
} }
return []; return [];
@ -5441,19 +5468,19 @@ export class AppMessagesManager {
if(groupedId) { if(groupedId) {
const groupedStorage = this.groupedMessagesStorage[groupedId]; const groupedStorage = this.groupedMessagesStorage[groupedId];
if(groupedStorage) { if(groupedStorage) {
delete groupedStorage[mid]; groupedStorage.delete(mid);
if(!history.albums) history.albums = {}; if(!history.albums) history.albums = {};
(history.albums[groupedId] || (history.albums[groupedId] = new Set())).add(mid); (history.albums[groupedId] || (history.albums[groupedId] = new Set())).add(mid);
if(!Object.keys(groupedStorage).length) { if(!groupedStorage.size) {
delete history.albums; delete history.albums;
delete this.groupedMessagesStorage[groupedId]; delete this.groupedMessagesStorage[groupedId];
} }
} }
} }
delete storage[mid]; storage.delete(mid);
const peerMessagesToHandle = this.newMessagesToHandle[peerId]; const peerMessagesToHandle = this.newMessagesToHandle[peerId];
if(peerMessagesToHandle && peerMessagesToHandle.has(mid)) { if(peerMessagesToHandle && peerMessagesToHandle.has(mid)) {

5
src/lib/appManagers/appStateManager.ts

@ -174,7 +174,7 @@ const REFRESH_KEYS = ['contactsList', 'stateCreatedTime',
export class AppStateManager extends EventListenerBase<{ export class AppStateManager extends EventListenerBase<{
save: (state: State) => Promise<void>, save: (state: State) => Promise<void>,
peerNeeded: (peerId: number) => void, peerNeeded: (peerId: number) => void,
peerUnneeded: (peerId: number) => void, peerUnneeded: (peerId: number) => void
}> { }> {
public static STATE_INIT = STATE_INIT; public static STATE_INIT = STATE_INIT;
private loaded: Promise<State>; private loaded: Promise<State>;
@ -199,6 +199,8 @@ export class AppStateManager extends EventListenerBase<{
public storage = stateStorage; public storage = stateStorage;
public newVersion: string;
constructor() { constructor() {
super(); super();
this.loadSavedState(); this.loadSavedState();
@ -409,6 +411,7 @@ export class AppStateManager extends EventListenerBase<{
if(state.version !== STATE_VERSION) { if(state.version !== STATE_VERSION) {
this.pushToState('version', STATE_VERSION); this.pushToState('version', STATE_VERSION);
this.newVersion = STATE_VERSION;
} }
// ! probably there is better place for it // ! probably there is better place for it

239
src/lib/mediaPlayer.ts

@ -16,67 +16,77 @@ import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle";
import EventListenerBase from "../helpers/eventListenerBase"; import EventListenerBase from "../helpers/eventListenerBase";
import rootScope from "./rootScope"; import rootScope from "./rootScope";
import findUpClassName from "../helpers/dom/findUpClassName"; import findUpClassName from "../helpers/dom/findUpClassName";
import { GrabEvent } from "../helpers/dom/attachGrabListeners";
import { attachClickEvent } from "../helpers/dom/clickEvent";
export class MediaProgressLine extends RangeSelector { export class MediaProgressLine extends RangeSelector {
private filledLoad: HTMLDivElement; protected filledLoad: HTMLDivElement;
private stopAndScrubTimeout = 0; protected progressRAF = 0;
private progressRAF = 0;
constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) { protected media: HTMLMediaElement;
super(1000 / 60 / 1000, 0, 0, 1); protected streamable: boolean;
if(streamable) { constructor(media?: HTMLAudioElement | HTMLVideoElement, streamable?: boolean, withTransition?: boolean, useTransform?: boolean) {
super({
step: 1000 / 60 / 1000,
min: 0,
max: 1,
withTransition,
useTransform
}, 0);
if(media) {
this.setMedia(media, streamable);
}
}
public setMedia(media: HTMLMediaElement, streamable = false) {
if(this.media) {
this.removeListeners();
}
if(streamable && !this.filledLoad) {
this.filledLoad = document.createElement('div'); this.filledLoad = document.createElement('div');
this.filledLoad.classList.add('progress-line__filled', 'progress-line__loaded'); this.filledLoad.classList.add('progress-line__filled', 'progress-line__loaded');
this.container.prepend(this.filledLoad); this.container.prepend(this.filledLoad);
//this.setLoadProgress(); //this.setLoadProgress();
} else if(this.filledLoad) {
this.filledLoad.classList.toggle('hide', !streamable);
} }
this.media = media;
this.streamable = streamable;
if(!media.paused || media.currentTime > 0) { if(!media.paused || media.currentTime > 0) {
this.onPlay(); this.onPlay();
} }
let wasPlaying = false;
this.setSeekMax(); this.setSeekMax();
this.setListeners(); this.setListeners();
this.setHandlers({ this.setHandlers({
onMouseDown: () => { onMouseDown: () => {
//super.onMouseDown(e); wasPlaying = !this.media.paused;
wasPlaying && this.media.pause();
//Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик
if(this.stopAndScrubTimeout) { // возможно лишнее
clearTimeout(this.stopAndScrubTimeout);
}
this.stopAndScrubTimeout = window.setTimeout(() => {
!this.media.paused && this.media.pause();
this.stopAndScrubTimeout = 0;
}, 150);
}, },
onMouseUp: () => { onMouseUp: (e) => {
//super.onMouseUp(e); cancelEvent(e.event);
wasPlaying && this.media.play();
if(this.stopAndScrubTimeout) {
clearTimeout(this.stopAndScrubTimeout);
this.stopAndScrubTimeout = 0;
}
this.media.paused && this.media.play();
} }
}) });
} }
onLoadedData = () => { protected onLoadedData = () => {
this.max = this.media.duration; this.max = this.media.duration;
this.seek.setAttribute('max', '' + this.max); this.seek.setAttribute('max', '' + this.max);
}; };
onEnded = () => { protected onEnded = () => {
this.setProgress(); this.setProgress();
}; };
onPlay = () => { protected onPlay = () => {
let r = () => { let r = () => {
this.setProgress(); this.setProgress();
@ -94,11 +104,21 @@ export class MediaProgressLine extends RangeSelector {
this.progressRAF = window.requestAnimationFrame(r); this.progressRAF = window.requestAnimationFrame(r);
}; };
onProgress = (e: Event) => { protected onTimeUpdate = () => {
if(this.media.paused) {
this.setProgress();
if(this.streamable) {
this.setLoadProgress();
}
}
};
protected onProgress = (e: Event) => {
this.setLoadProgress(); this.setLoadProgress();
}; };
protected scrub(e: MouseEvent) { protected scrub(e: GrabEvent) {
const scrubTime = super.scrub(e); const scrubTime = super.scrub(e);
this.media.currentTime = scrubTime; this.media.currentTime = scrubTime;
return scrubTime; return scrubTime;
@ -148,6 +168,7 @@ export class MediaProgressLine extends RangeSelector {
super.setListeners(); super.setListeners();
this.media.addEventListener('ended', this.onEnded); this.media.addEventListener('ended', this.onEnded);
this.media.addEventListener('play', this.onPlay); this.media.addEventListener('play', this.onPlay);
this.media.addEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.addEventListener('progress', this.onProgress); this.streamable && this.media.addEventListener('progress', this.onProgress);
} }
@ -157,19 +178,90 @@ export class MediaProgressLine extends RangeSelector {
this.media.removeEventListener('loadeddata', this.onLoadedData); this.media.removeEventListener('loadeddata', this.onLoadedData);
this.media.removeEventListener('ended', this.onEnded); this.media.removeEventListener('ended', this.onEnded);
this.media.removeEventListener('play', this.onPlay); this.media.removeEventListener('play', this.onPlay);
this.media.removeEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.removeEventListener('progress', this.onProgress); this.streamable && this.media.removeEventListener('progress', this.onProgress);
if(this.stopAndScrubTimeout) {
clearTimeout(this.stopAndScrubTimeout);
}
if(this.progressRAF) { if(this.progressRAF) {
window.cancelAnimationFrame(this.progressRAF); window.cancelAnimationFrame(this.progressRAF);
this.progressRAF = 0;
} }
} }
} }
let lastVolume = 1, muted = !lastVolume; export class VolumeSelector extends RangeSelector {
public btn: HTMLElement;
protected volumeSvg: HTMLElement;
constructor(protected listenerSetter: ListenerSetter, protected vertical = false) {
super({
step: 0.01,
min: 0,
max: 1,
vertical
}, 1);
this.setListeners();
this.setHandlers({
onScrub: currentTime => {
const value = Math.max(Math.min(currentTime, 1), 0);
//console.log('volume scrub:', currentTime, value);
appMediaPlaybackController.muted = false;
appMediaPlaybackController.volume = value;
},
onMouseUp: (e) => {
cancelEvent(e.event);
}
});
this.btn = document.createElement('div');
this.btn.classList.add('player-volume');
this.btn.innerHTML = `
<svg class="player-volume__icon" focusable="false" viewBox="0 0 24 24" aria-hidden="true"></svg>
`;
this.btn.classList.add('btn-icon');
this.volumeSvg = this.btn.firstElementChild as HTMLElement;
this.btn.append(this.container);
attachClickEvent(this.volumeSvg, this.onMuteClick, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope)('media_playback_params', this.setVolume);
this.setVolume();
}
private onMuteClick = (e?: Event) => {
e && cancelEvent(e);
appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
};
private setVolume = () => {
// const volume = video.volume;
const {volume, muted} = appMediaPlaybackController;
let d: string;
if(!volume || muted) {
d = `M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z`;
} else if(volume > .5) {
d = `M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z`;
} else if(volume > 0 && volume < .25) {
d = `M7 9v6h4l5 5V4l-5 5H7z`;
} else {
d = `M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z`;
}
try {
this.volumeSvg.innerHTML = `<path d="${d}"></path>`;
} catch(err) {}
if(!this.mousedown) {
this.setProgress(muted ? 0 : volume);
}
};
}
export default class VideoPlayer extends EventListenerBase<{ export default class VideoPlayer extends EventListenerBase<{
toggleControls: (show: boolean) => void toggleControls: (show: boolean) => void
}> { }> {
@ -239,73 +331,10 @@ export default class VideoPlayer extends EventListenerBase<{
timeDuration = player.querySelector('#time-duration') as HTMLElement; timeDuration = player.querySelector('#time-duration') as HTMLElement;
timeDuration.innerHTML = String(video.duration | 0).toHHMMSS(); timeDuration.innerHTML = String(video.duration | 0).toHHMMSS();
const volumeDiv = document.createElement('div'); const volumeSelector = new VolumeSelector(this.listenerSetter);
volumeDiv.classList.add('player-volume');
volumeDiv.innerHTML = `
<svg class="player-volume__icon" focusable="false" viewBox="0 0 24 24" aria-hidden="true"></svg>
`;
const volumeSvg = volumeDiv.firstElementChild as SVGSVGElement;
const onMuteClick = (e?: Event) => {
e && cancelEvent(e);
video.muted = !video.muted;
};
this.listenerSetter.add(volumeSvg)('click', onMuteClick);
const volumeProgress = new RangeSelector(0.01, 1, 0, 1);
volumeProgress.setListeners();
volumeProgress.setHandlers({
onScrub: currentTime => {
const value = Math.max(Math.min(currentTime, 1), 0);
//console.log('volume scrub:', currentTime, value);
video.muted = false;
video.volume = value;
}
});
volumeDiv.append(volumeProgress.container);
const setVolume = () => {
const volume = video.volume;
let d: string;
if(!volume || video.muted) {
d = `M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z`;
} else if(volume > .5) {
d = `M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z`;
} else if(volume > 0 && volume < .25) {
d = `M7 9v6h4l5 5V4l-5 5H7z`;
} else {
d = `M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z`;
}
try {
volumeSvg.innerHTML = `<path d="${d}"></path>`;
} catch(err) {}
if(!volumeProgress.mousedown) {
volumeProgress.setProgress(video.muted ? 0 : volume);
}
};
// не вызовется повторно если на 1 установить 1
this.listenerSetter.add(video)('volumechange', () => {
muted = video.muted;
lastVolume = video.volume;
setVolume();
});
video.volume = lastVolume;
video.muted = muted;
setVolume();
// volume end
const leftControls = player.querySelector('.left-controls'); const leftControls = player.querySelector('.left-controls');
leftControls.insertBefore(volumeDiv, timeElapsed.parentElement); leftControls.insertBefore(volumeSelector.btn, timeElapsed.parentElement);
Array.from(toggle).forEach((button) => { Array.from(toggle).forEach((button) => {
this.listenerSetter.add(button)('click', () => { this.listenerSetter.add(button)('click', () => {
@ -360,13 +389,13 @@ export default class VideoPlayer extends EventListenerBase<{
if(e.code === 'KeyF') { if(e.code === 'KeyF') {
this.toggleFullScreen(fullScreenButton); this.toggleFullScreen(fullScreenButton);
} else if(e.code === 'KeyM') { } else if(e.code === 'KeyM') {
onMuteClick(); appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
} else if(e.code === 'Space') { } else if(e.code === 'Space') {
this.togglePlay(); this.togglePlay();
} else if(e.altKey && e.code === 'Equal') { } else if(e.altKey && e.code === 'Equal') {
this.video.playbackRate += 0.25; appMediaPlaybackController.playbackRate += .25;
} else if(e.altKey && e.code === 'Minus') { } else if(e.altKey && e.code === 'Minus') {
this.video.playbackRate -= 0.25; appMediaPlaybackController.playbackRate -= .25;
} else { } else {
good = false; good = false;
} }

2
src/lib/mtproto/mtprotoworker.ts

@ -364,7 +364,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
/// #if !MTPROTO_SW /// #if !MTPROTO_SW
private registerWorker() { private registerWorker() {
//return; // return;
const worker = new MTProtoWorker(); const worker = new MTProtoWorker();
//const worker = window; //const worker = window;

7
src/lib/rootScope.ts

@ -59,7 +59,7 @@ export type BroadcastEvents = {
'message_edit': {storage: MessagesStorage, peerId: number, mid: number}, 'message_edit': {storage: MessagesStorage, peerId: number, mid: number},
'message_views': {peerId: number, mid: number, views: number}, 'message_views': {peerId: number, mid: number, views: number},
'message_sent': {storage: MessagesStorage, tempId: number, tempMessage: any, mid: number}, 'message_sent': {storage: MessagesStorage, tempId: number, tempMessage: any, mid: number, message: Message.message},
'messages_pending': void, 'messages_pending': void,
'messages_read': void, 'messages_read': void,
'messages_downloaded': {peerId: number, mids: number[]}, 'messages_downloaded': {peerId: number, mids: number[]},
@ -75,8 +75,9 @@ export type BroadcastEvents = {
'stickers_installed': StickerSet.stickerSet, 'stickers_installed': StickerSet.stickerSet,
'stickers_deleted': StickerSet.stickerSet, 'stickers_deleted': StickerSet.stickerSet,
'audio_play': {doc: MyDocument, mid: number, peerId: number}, 'media_play': {doc: MyDocument, message: Message.message, media: HTMLMediaElement},
'audio_pause': void, 'media_pause': void,
'media_playback_params': {volume: number, muted: boolean, playbackRate: number},
'state_cleared': void, 'state_cleared': void,
'state_synchronized': number | void, 'state_synchronized': number | void,

49
src/lib/storages/dialogs.ts

@ -120,7 +120,9 @@ export default class DialogsStorage {
for(let i = 0, length = dialogs.length; i < length; ++i) { for(let i = 0, length = dialogs.length; i < length; ++i) {
const dialog = dialogs[i]; const dialog = dialogs[i];
if(dialog) { if(dialog) {
dialog.top_message = this.appMessagesIdsManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog // if(dialog.peerId !== SERVICE_PEER_ID) {
dialog.top_message = this.appMessagesIdsManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog
// }
if(dialog.topMessage) { if(dialog.topMessage) {
this.appMessagesManager.saveMessages([dialog.topMessage]); this.appMessagesManager.saveMessages([dialog.topMessage]);
@ -358,19 +360,20 @@ export default class DialogsStorage {
} */ } */
public setDialogToState(dialog: Dialog) { public setDialogToState(dialog: Dialog) {
const historyStorage = this.appMessagesManager.getHistoryStorage(dialog.peerId); const {peerId, pts} = dialog;
const messagesStorage = this.appMessagesManager.getMessagesStorage(dialog.peerId); const historyStorage = this.appMessagesManager.getHistoryStorage(peerId);
const messagesStorage = this.appMessagesManager.getMessagesStorage(peerId);
const history = historyStorage.history.slice; const history = historyStorage.history.slice;
let incomingMessage: any; let incomingMessage: MyMessage;
for(let i = 0, length = history.length; i < length; ++i) { for(let i = 0, length = history.length; i < length; ++i) {
const mid = history[i]; const mid = history[i];
const message: MyMessage = this.appMessagesManager.getMessageFromStorage(messagesStorage, mid); const message: MyMessage = this.appMessagesManager.getMessageFromStorage(messagesStorage, mid);
if(!message.pFlags.is_outgoing) { if(!message.pFlags.is_outgoing/* || peerId === SERVICE_PEER_ID */) {
incomingMessage = message; incomingMessage = message;
const fromId = message.viaBotId || message.fromId; const fromId = message.viaBotId || message.fromId;
if(fromId !== dialog.peerId) { if(fromId !== peerId) {
this.appStateManager.requestPeer(fromId, 'topMessage_' + dialog.peerId, 1); this.appStateManager.requestPeer(fromId, 'topMessage_' + peerId, 1);
} }
break; break;
@ -379,16 +382,26 @@ export default class DialogsStorage {
dialog.topMessage = incomingMessage; dialog.topMessage = incomingMessage;
if(dialog.peerId < 0 && dialog.pts) { // DO NOT TOUCH THESE LINES, SOME REAL MAGIC HERE.
const newPts = this.apiUpdatesManager.getChannelState(-dialog.peerId, dialog.pts).pts; // * Read service chat when refreshing page with outgoing & getting new service outgoing message
if(incomingMessage && dialog.read_inbox_max_id >= dialog.top_message) {
dialog.unread_count = 0;
}
dialog.read_inbox_max_id = this.appMessagesIdsManager.clearMessageId(dialog.read_inbox_max_id);
dialog.read_outbox_max_id = this.appMessagesIdsManager.clearMessageId(dialog.read_outbox_max_id);
// CAN TOUCH NOW
if(peerId < 0 && pts) {
const newPts = this.apiUpdatesManager.getChannelState(-peerId, pts).pts;
dialog.pts = newPts; dialog.pts = newPts;
} }
this.storage.set({ this.storage.set({
[dialog.peerId]: dialog [peerId]: dialog
}); });
this.appStateManager.requestPeer(dialog.peerId, 'dialog_' + dialog.peerId, 1); this.appStateManager.requestPeer(peerId, 'dialog_' + peerId, 1);
/* for(let id in this.appMessagesManager.filtersStorage.filters) { /* for(let id in this.appMessagesManager.filtersStorage.filters) {
const filter = this.appMessagesManager.filtersStorage.filters[id]; const filter = this.appMessagesManager.filtersStorage.filters[id];
@ -538,9 +551,16 @@ export default class DialogsStorage {
const peerText = this.appPeersManager.getPeerSearchText(peerId); const peerText = this.appPeersManager.getPeerSearchText(peerId);
this.dialogsIndex.indexObject(peerId, peerText); this.dialogsIndex.indexObject(peerId, peerText);
let mid: number, message; const wasDialogBefore = this.getDialogOnly(peerId);
let mid: number, message: MyMessage;
if(dialog.top_message) { if(dialog.top_message) {
mid = this.appMessagesIdsManager.generateMessageId(dialog.top_message);//dialog.top_message; if(wasDialogBefore?.top_message && !this.appMessagesManager.getMessageByPeer(peerId, wasDialogBefore.top_message).deleted) {
mid = wasDialogBefore.top_message;
} else {
mid = this.appMessagesIdsManager.generateMessageId(dialog.top_message);//dialog.top_message;
}
message = this.appMessagesManager.getMessageByPeer(peerId, mid); message = this.appMessagesManager.getMessageByPeer(peerId, mid);
} else { } else {
mid = this.appMessagesManager.generateTempMessageId(peerId); mid = this.appMessagesManager.generateTempMessageId(peerId);
@ -573,9 +593,8 @@ export default class DialogsStorage {
} }
} }
const wasDialogBefore = this.getDialogOnly(peerId);
dialog.top_message = mid; dialog.top_message = mid;
dialog.unread_count = wasDialogBefore && dialog.read_inbox_max_id === this.appMessagesIdsManager.getServerMessageId(wasDialogBefore.read_inbox_max_id) ? wasDialogBefore.unread_count : dialog.unread_count;
dialog.read_inbox_max_id = this.appMessagesIdsManager.generateMessageId(wasDialogBefore && !dialog.read_inbox_max_id ? wasDialogBefore.read_inbox_max_id : dialog.read_inbox_max_id); dialog.read_inbox_max_id = this.appMessagesIdsManager.generateMessageId(wasDialogBefore && !dialog.read_inbox_max_id ? wasDialogBefore.read_inbox_max_id : dialog.read_inbox_max_id);
dialog.read_outbox_max_id = this.appMessagesIdsManager.generateMessageId(wasDialogBefore && !dialog.read_outbox_max_id ? wasDialogBefore.read_outbox_max_id : dialog.read_outbox_max_id); dialog.read_outbox_max_id = this.appMessagesIdsManager.generateMessageId(wasDialogBefore && !dialog.read_outbox_max_id ? wasDialogBefore.read_outbox_max_id : dialog.read_outbox_max_id);

20
src/scripts/generate_changelog.js

@ -0,0 +1,20 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
// @ts-check
const fs = require('fs');
const text = fs.readFileSync('./CHANGELOG.md').toString('utf-8');
const writeTo = `./public/changelogs/{VERSION}.md`;
const splitted = text.split('\n\n');
splitted.forEach(text => {
text = text.replace(/^\*/gm, '•');
const splitted = text.split('\n');
const firstLine = splitted.shift();
fs.writeFileSync(writeTo.replace('{VERSION}', firstLine.substr(4)), splitted.join('\n'));
});

1
src/scripts/in/schema_additional_params.json

@ -69,6 +69,7 @@
{"name": "random_id", "type": "string"}, {"name": "random_id", "type": "string"},
{"name": "unread", "type": "true"}, {"name": "unread", "type": "true"},
{"name": "is_outgoing", "type": "true"}, {"name": "is_outgoing", "type": "true"},
{"name": "is_scheduled", "type": "true"},
{"name": "rReply", "type": "string"}, {"name": "rReply", "type": "string"},
{"name": "viaBotId", "type": "number"}, {"name": "viaBotId", "type": "number"},
{"name": "clear_history", "type": "boolean"}, {"name": "clear_history", "type": "boolean"},

6
src/scss/partials/_audio.scss

@ -450,6 +450,12 @@
color: var(--primary-text-color); color: var(--primary-text-color);
} }
&-description:not(:empty) {
&:before {
content: "";
}
}
&-time, &-time,
&-subtitle { &-subtitle {
font-size: .875rem; font-size: .875rem;

6
src/scss/partials/_button.scss

@ -27,7 +27,7 @@
border: none; border: none;
padding: .5rem; padding: .5rem;
position: relative; position: relative;
overflow: hidden; // overflow: hidden;
transition: color .15s ease-in-out, opacity .15s ease-in-out, background-color .15s ease-in-out; transition: color .15s ease-in-out, opacity .15s ease-in-out, background-color .15s ease-in-out;
/* kostil */ /* kostil */
@ -35,6 +35,10 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&.rp {
overflow: hidden;
}
&.active { &.active {
color: var(--primary-color); color: var(--primary-color);
} }

4
src/scss/partials/_chat.scss

@ -227,6 +227,10 @@ $chat-helper-size: 36px;
@include animation-level(2) { @include animation-level(2) {
animation: grow-icon .4s forwards ease-in-out !important; animation: grow-icon .4s forwards ease-in-out !important;
} }
@include respond-to(esg-bottom-new) {
margin-right: .125rem !important;
}
} }
&:not(.is-recording) { &:not(.is-recording) {

7
src/scss/partials/_chatBubble.scss

@ -1111,6 +1111,10 @@ $bubble-margin: .25rem;
.audio-subtitle { .audio-subtitle {
margin-top: -1px; margin-top: -1px;
} }
&.corner-download .audio-download {
margin: 1.375rem 1.375rem 0;
}
} }
} }
@ -1887,7 +1891,8 @@ $bubble-margin: .25rem;
background-color: var(--message-highlightning-color); background-color: var(--message-highlightning-color);
font-size: .9375rem; font-size: .9375rem;
padding: .28125rem .625rem; padding: .28125rem .625rem;
line-height: var(--line-height); // line-height: var(--line-height);
line-height: 1.25rem;
border-radius: inherit; border-radius: inherit;
user-select: none; user-select: none;
display: flex; display: flex;

300
src/scss/partials/_chatPinned.scss

@ -103,7 +103,8 @@
} }
body:not(.animation-level-0) & { body:not(.animation-level-0) & {
&-wrapper, &-mark { &-wrapper,
&-mark {
will-change: transform; will-change: transform;
transition: transform .25s ease-in-out; transition: transform .25s ease-in-out;
} }
@ -120,18 +121,14 @@
} }
} }
.pinned-message, .reply { .pinned-message,
.reply {
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
box-sizing: border-box;
margin-right: 1rem;
// max-height: 35px;
position: relative; position: relative;
user-select: none; user-select: none;
/* padding: .25rem; */
/* &.is-media { /* &.is-media {
.emoji:first-child { .emoji:first-child {
@ -152,12 +149,12 @@
color: var(--primary-color); color: var(--primary-color);
} }
&-title, &-subtitle { &-title,
&-subtitle {
font-size: 14px; font-size: 14px;
line-height: var(--line-height); line-height: var(--line-height);
overflow: hidden;
white-space: nowrap; @include text-overflow();
text-overflow: ellipsis;
} }
&-media { &-media {
@ -247,58 +244,93 @@
} }
.pinned-container { .pinned-container {
--container-height: 3.25rem;
display: flex;
justify-content: space-between;
align-items: center;
flex: 0 0 auto; flex: 0 0 auto;
overflow: visible; overflow: visible;
cursor: pointer;
&.is-floating { &.is-floating {
position: absolute !important; position: absolute !important;
top: 3.5rem; top: var(--topbar-height);
right: 0; right: 0;
left: 0; left: 0;
margin: 0; margin: 0;
width: auto; width: auto;
height: 3.25rem; height: var(--container-height);
max-height: 3.25rem; max-height: var(--container-height);
background: var(--surface-color) !important; background-color: var(--surface-color) !important;
// box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, .15);
.pinned-container-close { // box-shadow: 0px 2px 3px 0px rgb(0 0 0 / 10%);
position: absolute;
font-size: 1.4rem;
right: 9px;
display: flex;
}
.pinned-container-wrapper { .pinned-container-wrapper {
order: 0;
padding: 0 1rem; padding: 0 1rem;
height: 100%; height: 100%;
border-radius: 0;
z-index: 0;
@include respond-to(handhelds) {
padding: 0 .5rem;
}
/* &-utils {
position: relative;
z-index: 0;
} */
} }
}
@include respond-to(handhelds) { .pinned-container-content {
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, .15); margin-right: .5rem;
}
&:before { /* &:before {
width: 100%;
content: " "; content: " ";
height: 52px; height: 1px;
left: 0; background-color: var(--border-color);
top: 0; top: 0;
right: 0;
left: 0;
position: absolute;
} */
&:before {
content: " ";
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
position: absolute; position: absolute;
/* box-shadow: inset 0px 2px 3px 0px rgba(0, 0, 0, .15); */ top: 0;
box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, .15); right: 0;
bottom: 0;
left: 0;
} }
// &:before {
// width: 100%;
// content: " ";
// height: 52px;
// left: 0;
// top: 0;
// position: absolute;
// /* box-shadow: inset 0px 2px 3px 0px rgba(0, 0, 0, .15); */
// box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, .15);
// }
} }
&-content { &-content {
width: 100%; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
position: relative;
pointer-events: none;
} }
&-close, .pinned-audio-ico { .btn-icon {
font-size: 1.5rem; font-size: 1.5rem;
display: flex; display: flex;
justify-content: center; justify-content: center;
z-index: 1; // z-index: 1;
flex: 0 0 auto; flex: 0 0 auto;
} }
@ -313,6 +345,14 @@
align-items: center; align-items: center;
padding: .25rem; padding: .25rem;
border-radius: .25rem; border-radius: .25rem;
order: 1;
&-utils {
flex: 0 0 auto;
display: flex;
align-items: center;
position: relative;
}
/* html.no-touch &:hover { /* html.no-touch &:hover {
background-color: var(--light-secondary-text-color); background-color: var(--light-secondary-text-color);
@ -321,7 +361,8 @@
} }
.pinned-message { .pinned-message {
display: none; // display: none;
display: flex;
width: auto; width: auto;
&-content { &-content {
@ -373,6 +414,7 @@
} }
&:not(.is-floating) { &:not(.is-floating) {
margin-right: 1rem;
//width: 15.5rem; //width: 15.5rem;
/* .pinned-message-content { /* .pinned-message-content {
@ -382,6 +424,7 @@
.pinned-message-close { .pinned-message-close {
display: flex; display: flex;
margin-right: .75rem; margin-right: .75rem;
order: 0;
} }
} }
@ -393,10 +436,23 @@
} }
&.is-floating { &.is-floating {
.chat:not(.type-discussion) & { --container-height: var(--pinned-message-height);
/* .chat:not(.type-discussion) & {
.pinned-container-wrapper { .pinned-container-wrapper {
padding-right: 3rem; padding-right: 3rem;
} }
} */
> .btn-icon {
display: none;
}
.pinned-container-wrapper {
padding-left: 1rem;
}
.pinned-container-content {
margin-right: 0;
} }
} }
@ -510,6 +566,10 @@
} }
} }
&-pinlist {
order: 2;
}
.chat.type-discussion & { .chat.type-discussion & {
.pinned-container-close { .pinned-container-close {
display: none !important; display: none !important;
@ -518,11 +578,11 @@
} }
.pinned-audio { .pinned-audio {
display: flex; --progress-height: .25rem;
flex-direction: column;
justify-content: center; &.is-floating {
cursor: pointer; --container-height: var(--pinned-audio-height);
//width: 210px; }
&:not(.is-floating) { &:not(.is-floating) {
padding-right: 1.75rem; padding-right: 1.75rem;
@ -530,14 +590,19 @@
position: relative; position: relative;
} }
&.is-floating .pinned-audio-ico { /* &.is-floating .pinned-audio-ico {
margin-left: -.25rem; margin-left: -.25rem;
} */
.pinned-container-wrapper {
overflow: visible !important;
> .btn-icon {
margin-left: 0 !important;
}
} }
&-ico { &-ico {
color: var(--primary-color);
margin-right: .375rem;
&:before { &:before {
content: $tgico-largeplay; content: $tgico-largeplay;
} }
@ -549,20 +614,147 @@
&-title { &-title {
font-weight: 500; font-weight: 500;
width: 100%;
max-width: 100%;
} }
&-subtitle { &-subtitle {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
&-title, &-subtitle { &-title,
white-space: nowrap; &-subtitle {
text-overflow: ellipsis; font-size: .875rem;
font-size: 14px; line-height: var(--line-height);
line-height: 1.4; width: 100%;
overflow: hidden; max-width: 100%;
max-width: 240px;
@include text-overflow();
}
&-content {
margin-left: .75rem;
}
&-progress {
--border-radius: 0;
--height: var(--progress-height);
--scaleX: 1;
--translateY: .125rem;
position: absolute;
right: 0;
bottom: 0;
left: 0;
transform: scaleX(var(--scaleX)) translateY(var(--translateY));
transform-origin: left center;
transition: transform var(--transition-standard-out);
body.is-right-column-shown & {
@include respond-to(medium-screens) {
--scaleX: calc(1 - var(--right-column-proportion));
}
transition: transform var(--transition-standard-in);
}
@include animation-level(0) {
transition: none !important;
}
&:before {
@include animation-level(2) {
transition: opacity .2s ease-in-out;
}
}
&:not(:hover):before {
opacity: 0;
}
@include hover() {
--translateY: 0;
}
.progress-line__filled {
&:after {
display: none !important;
}
}
&-wrapper {
position: absolute;
height: var(--progress-height);
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
}
}
&-volume {
align-items: center;
position: relative;
width: 2.5rem;
height: 2.5rem;
html.is-touch & {
display: none;
}
&-tunnel {
position: absolute;
top: 100%;
left: 0;
right: 0;
bottom: -1rem;
content: " ";
}
.player-volume__icon {
fill: var(--secondary-text-color);
width: inherit;
height: inherit;
cursor: pointer;
}
&.active .player-volume__icon {
fill: var(--primary-color);
}
.progress-line {
&-container {
position: absolute;
top: 100%;
width: 5rem;
padding: .75rem 1rem;
margin-top: 2.25rem;
transform: rotate(270deg);
border-radius: $border-radius-medium;
background-color: var(--surface-color);
box-shadow: 0px 1px 5px 0px rgb(0 0 0 / 15%);
opacity: 0;
visibility: hidden;
transition: opacity .2s ease-in-out, visibility 0s .2s;
// make a tunnel so volume won't hide during moving the cursor
/* &:before {
position: absolute;
top: -50%;
left: 0;
right: -1rem;
content: " ";
} */
@include animation-level(0) {
transition: none !important;
}
}
}
&:hover, &:active {
.progress-line-container {
opacity: 1;
visibility: visible;
transition: opacity .2s ease-in-out, visibility 0s 0s;
}
}
} }
} }

94
src/scss/partials/_chatTopbar.scss

@ -4,31 +4,67 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
@mixin sidebar-transform() {
@include respond-to(medium-screens) {
transition: transform var(--transition-standard-out);
body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
transition: transform var(--transition-standard-in);
}
body.animation-level-0 & {
transition: none;
}
@content;
}
}
.topbar { .topbar {
--topbar-height: 3.5rem;
--pinned-floating-height: 0px;
--pinned-audio-height: 52px;
--pinned-message-height: 52px;
width: 100%; width: 100%;
background-color: #fff;
user-select: none;
box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .21);
z-index: 1; z-index: 1;
min-height: 3.5rem; min-height: var(--height);
max-height: 3.5rem; max-height: var(--height);
margin-bottom: var(--pinned-floating-height);
&.is-pinned-floating { position: relative;
&.is-pinned-audio-shown, &.is-pinned-message-shown:not(.hide-pinned) {
margin-bottom: 52px;
/* & + .bubbles {
margin-top: 52px;
} */
& ~ .drops-container { &:before {
--pinned-floating-height: 52px; content: " ";
} position: absolute;
} height: calc(var(--topbar-height) + var(--pinned-floating-height));
top: 0;
right: 0;
left: 0;
box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .21);
pointer-events: none;
}
&.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) { &.is-pinned-audio-floating {
.pinned-message { --pinned-floating-height: var(--pinned-audio-height);
display: flex; }
&.is-pinned-message-floating {
--pinned-floating-height: var(--pinned-message-height);
}
&.is-pinned-audio-floating.is-pinned-message-floating {
--pinned-floating-height: calc(var(--pinned-audio-height) + var(--pinned-message-height));
.pinned-message {
top: calc(var(--topbar-height) + var(--pinned-audio-height));
&:before {
border-top: none;
} }
/* &:before {
box-shadow: none;
} */
} }
} }
@ -56,6 +92,10 @@
} }
} }
.pinned-container-wrapper-utils {
@include sidebar-transform();
}
.sidebar-close-button { .sidebar-close-button {
position: absolute; position: absolute;
} }
@ -92,7 +132,8 @@
} }
} }
.peer-title, .info { .peer-title,
.info {
@include text-overflow(); @include text-overflow();
line-height: var(--line-height); line-height: var(--line-height);
} }
@ -145,18 +186,7 @@
right: 0px; right: 0px;
padding-right: inherit; */ padding-right: inherit; */
@include respond-to(medium-screens) { @include sidebar-transform();
transition: transform var(--transition-standard-out);
body.is-right-column-shown & {
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
transition: transform var(--transition-standard-in);
}
body.animation-level-0 & {
transition: none;
}
}
@include respond-to(handhelds) { @include respond-to(handhelds) {
> .btn-icon:not(.btn-menu-toggle) { > .btn-icon:not(.btn-menu-toggle) {

15
src/scss/partials/_ckin.scss

@ -335,7 +335,7 @@ video::-webkit-media-controls-enclosure {
} }
&__filled { &__filled {
padding-right: 1px; // * need because there is border-radius // padding-right: 1px; // * need because there is border-radius
max-width: 100%; max-width: 100%;
&:not(.progress-line__loaded) { &:not(.progress-line__loaded) {
@ -380,8 +380,17 @@ video::-webkit-media-controls-enclosure {
} }
@include animation-level(2) { @include animation-level(2) {
&.with-transition .progress-line__filled { &.with-transition {
transition: width .2s; .progress-line__filled {
transition: width .2s;
}
}
}
&.use-transform {
.progress-line__filled {
width: 100%;
transform-origin: left center;
} }
} }
} }

7
src/scss/partials/_rightSidebar.scss

@ -664,8 +664,11 @@
#search-container { #search-container {
.search-super-content-music { .search-super-content-music {
.audio:not(.audio-show-progress) .audio-time { .audio:not(.audio-show-progress) {
display: none; .audio-time,
.audio-description:before {
display: none;
}
} }
} }

16
src/scss/partials/_ripple.scss

@ -9,7 +9,12 @@
user-select: none; user-select: none;
} }
.rp-overflow, .btn-menu-toggle.rp, .menu-horizontal-div-item.rp, .btn-corner.rp/* , html.is-safari .c-ripple */ { .rp-overflow,
.btn-menu-toggle.rp,
.menu-horizontal-div-item.rp,
.btn-corner.rp,
.pinned-container-wrapper.rp
/* , html.is-safari .c-ripple */ {
.c-ripple { .c-ripple {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -59,7 +64,8 @@
//overflow: hidden; //overflow: hidden;
pointer-events: none; pointer-events: none;
.btn-menu &, .c-ripple.is-square & { .btn-menu &,
.c-ripple.is-square & {
animation-name: ripple-effect-handhelds; animation-name: ripple-effect-handhelds;
//animation-timing-function: ease-out; //animation-timing-function: ease-out;
animation-duration: .2s; animation-duration: .2s;
@ -74,11 +80,13 @@
} */ } */
} }
.btn-menu &, &.is-square { .btn-menu &,
&.is-square {
--ripple-duration: .2s; --ripple-duration: .2s;
} }
&__circle.hiding, &__square.hiding { &__circle.hiding,
&__square.hiding {
opacity: 0; opacity: 0;
} }
} }

11
src/scss/style.scss

@ -232,6 +232,7 @@ html.night {
@import "partials/autocompleteHelper"; @import "partials/autocompleteHelper";
@import "partials/autocompletePeerHelper"; @import "partials/autocompletePeerHelper";
@import "partials/badge"; @import "partials/badge";
@import "partials/ckin";
@import "partials/checkbox"; @import "partials/checkbox";
@import "partials/chatlist"; @import "partials/chatlist";
@import "partials/chat"; @import "partials/chat";
@ -251,7 +252,6 @@ html.night {
@import "partials/leftSidebar"; @import "partials/leftSidebar";
@import "partials/rightSidebar"; @import "partials/rightSidebar";
@import "partials/mediaViewer"; @import "partials/mediaViewer";
@import "partials/ckin";
@import "partials/emojiDropdown"; @import "partials/emojiDropdown";
@import "partials/scrollable"; @import "partials/scrollable";
@import "partials/selector"; @import "partials/selector";
@ -743,6 +743,15 @@ hr {
} }
} }
.missing-icon {
width: 1.5rem;
height: 1.5rem;
&-path {
fill: currentColor;
}
}
.select-wrapper { .select-wrapper {
max-height: 23.5rem; max-height: 23.5rem;
/* height: auto; */ /* height: auto; */

Loading…
Cancel
Save