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. 303
      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. 78
      src/components/chat/audio.ts
  13. 93
      src/components/chat/bubbles.ts
  14. 75
      src/components/chat/pinnedContainer.ts
  15. 31
      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. 3
      src/components/popups/index.ts
  21. 41
      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. 49
      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. 277
      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. 45
      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. 296
      src/scss/partials/_chatPinned.scss
  53. 88
      src/scss/partials/_chatTopbar.scss
  54. 13
      src/scss/partials/_ckin.scss
  55. 5
      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,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 @@ @@ -13,7 +13,8 @@
"profile": "webpack --profile --json > stats.json --config webpack.prod.js",
"profile:dev": "webpack --profile --json > stats.json --config webpack.dev.js",
"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": "",
"license": "GPL-3.0-only",

4
src/components/animationIntersector.ts

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

303
src/components/appMediaPlaybackController.ts

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

1656
src/components/appMediaViewer.ts

File diff suppressed because it is too large Load Diff

57
src/components/appMediaViewerAvatar.ts

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

65
src/components/audio.ts

@ -8,14 +8,13 @@ import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; @@ -8,14 +8,13 @@ import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
import { wrapPhoto } from "./wrappers";
import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer";
import appMediaPlaybackController, { MediaItem } from "./appMediaPlaybackController";
import { DocumentAttribute } from "../layer";
import appMediaPlaybackController, { MediaItem, MediaSearchContext } from "./appMediaPlaybackController";
import { DocumentAttribute, Message } from "../layer";
import mediaSizes from "../helpers/mediaSizes";
import { IS_SAFARI } from "../environment/userAgent";
import appMessagesManager from "../lib/appManagers/appMessagesManager";
import rootScope from "../lib/rootScope";
import './middleEllipsis';
import { SearchSuperContext } from "./appSearchSuper.";
import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent";
import LazyLoadQueue from "./lazyLoadQueue";
@ -81,7 +80,7 @@ function wrapVoiceMessage(audioEl: AudioElement) { @@ -81,7 +80,7 @@ function wrapVoiceMessage(audioEl: AudioElement) {
audioEl.classList.add('is-voice');
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) {
audioEl.classList.add('is-out');
@ -218,7 +217,7 @@ function wrapVoiceMessage(audioEl: AudioElement) { @@ -218,7 +217,7 @@ function wrapVoiceMessage(audioEl: AudioElement) {
const scrubTime = offsetX / availW /* width */ * audio.duration;
audio.currentTime = scrubTime;
}
});
}, noop);
return () => {
progress.remove();
@ -234,7 +233,7 @@ function wrapAudio(audioEl: AudioElement) { @@ -234,7 +233,7 @@ function wrapAudio(audioEl: AudioElement) {
const withTime = audioEl.withTime;
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 descriptionEl = document.createElement('div');
@ -336,13 +335,40 @@ function constructDownloadPreloader(tryAgainOnFail = true) { @@ -336,13 +335,40 @@ function constructDownloadPreloader(tryAgainOnFail = true) {
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 {
public audio: HTMLAudioElement;
public preloader: ProgressivePreloader;
public message: any;
public message: Message.message;
public withTime = false;
public voiceAsMusic = false;
public searchContext: SearchSuperContext;
public searchContext: MediaSearchContext;
public showSender = false;
public noAutoDownload: boolean;
public lazyLoadQueue: LazyLoadQueue;
@ -356,7 +382,10 @@ export default class AudioElement extends HTMLElement { @@ -356,7 +382,10 @@ export default class AudioElement extends HTMLElement {
public render() {
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 isVoice = !this.voiceAsMusic && isRealVoice;
const isOutgoing = this.message.pFlags.is_outgoing;
@ -395,7 +424,7 @@ export default class AudioElement extends HTMLElement { @@ -395,7 +424,7 @@ export default class AudioElement extends HTMLElement {
const onLoad = this.onLoad = (autoload: boolean) => {
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>();
if(this.audio.readyState >= 2) this.readyPromise.resolve();
@ -421,18 +450,7 @@ export default class AudioElement extends HTMLElement { @@ -421,18 +450,7 @@ export default class AudioElement extends HTMLElement {
if(paused) {
if(appMediaPlaybackController.setSearchContext(this.searchContext)) {
let prev: MediaItem[], next: MediaItem[];
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);
}
const [prev, next] = findAudioTargets(this, this.searchContext.useSearch);
appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next);
}
@ -493,7 +511,7 @@ export default class AudioElement extends HTMLElement { @@ -493,7 +511,7 @@ export default class AudioElement extends HTMLElement {
return;
}
appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid);
appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid, this.message.pFlags.is_scheduled);
const onDownloadInit = () => {
if(shouldPlay) {
@ -591,6 +609,7 @@ export default class AudioElement extends HTMLElement { @@ -591,6 +609,7 @@ export default class AudioElement extends HTMLElement {
}
}
} else if(uploading) {
this.dataset.isOutgoing = '1';
this.preloader.attach(downloadDiv, false);
//onLoad();
}

3
src/components/avatar.ts

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

8
src/components/buttonIcon.ts

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

78
src/components/chat/audio.ts

@ -17,46 +17,84 @@ import replaceContent from "../../helpers/dom/replaceContent"; @@ -17,46 +17,84 @@ import replaceContent from "../../helpers/dom/replaceContent";
import PeerTitle from "../peerTitle";
import { i18n } from "../../lib/langPack";
import { formatFullSentTime } from "../../helpers/date";
import { MediaProgressLine, VolumeSelector } from "../../lib/mediaPlayer";
import ButtonIcon from "../buttonIcon";
export default class ChatAudio extends PinnedContainer {
private toggleEl: HTMLElement;
private progressLine: MediaProgressLine;
private volumeSelector: VolumeSelector;
constructor(protected topbar: ChatTopbar, protected chat: Chat, protected appMessagesManager: AppMessagesManager) {
super(
super({
topbar,
chat,
topbar.listenerSetter,
'audio',
new DivAndCaption(
listenerSetter: topbar.listenerSetter,
className: 'audio',
divAndCaption: new DivAndCaption(
'pinned-audio',
(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment) => {
replaceContent(this.divAndCaption.title, title);
replaceContent(this.divAndCaption.subtitle, subtitle);
}
),
() => {
if(this.toggleEl.classList.contains('flip-icon')) {
appMediaPlaybackController.toggle();
}
}
);
onClose: () => {
appMediaPlaybackController.stop();
},
floating: true
});
this.divAndCaption.border.remove();
this.toggleEl = document.createElement('button');
this.toggleEl.classList.add('pinned-audio-ico', 'tgico', 'btn-icon');
attachClickEvent(this.toggleEl, (e) => {
const prevEl = ButtonIcon('pprevious active', {noRipple: true});
const nextEl = ButtonIcon('nnext active', {noRipple: true});
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);
appMediaPlaybackController.toggle();
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();
});
this.wrapper.prepend(this.wrapper.firstElementChild, prevEl, this.toggleEl, nextEl);
this.wrapper.prepend(this.toggleEl);
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.topbar.listenerSetter.add(rootScope)('audio_play', (e) => {
const {doc, mid, peerId} = e;
const progressWrapper = document.createElement('div');
progressWrapper.classList.add('pinned-audio-progress-wrapper');
this.progressLine = new MediaProgressLine(undefined, undefined, true, true);
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;
const message = appMessagesManager.getMessageByPeer(peerId, mid);
if(doc.type === 'voice' || doc.type === 'round') {
title = new PeerTitle({peerId: message.fromId}).element;
@ -67,12 +105,14 @@ export default class ChatAudio extends PinnedContainer { @@ -67,12 +105,14 @@ export default class ChatAudio extends PinnedContainer {
subtitle = doc.audioPerformer || i18n('AudioUnknownArtist');
}
this.progressLine.setMedia(media);
this.fill(title, subtitle, message);
this.toggleEl.classList.add('flip-icon');
this.toggle(false);
});
this.topbar.listenerSetter.add(rootScope)('audio_pause', () => {
this.topbar.listenerSetter.add(rootScope)('media_pause', () => {
this.toggleEl.classList.remove('flip-icon');
});
}

93
src/components/chat/bubbles.ts

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

75
src/components/chat/pinnedContainer.ts

@ -12,50 +12,61 @@ import { ripple } from "../ripple"; @@ -12,50 +12,61 @@ import { ripple } from "../ripple";
import ListenerSetter from "../../helpers/listenerSetter";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
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 CLASSNAME_BASE = 'pinned-container';
const HEIGHT = 52;
export default class PinnedContainer {
private close: HTMLElement;
public wrapperUtils: HTMLElement;
public btnClose: HTMLElement;
protected wrapper: HTMLElement;
constructor(
protected topbar: ChatTopbar,
protected chat: Chat,
public listenerSetter: ListenerSetter,
protected className: string,
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 topbar: ChatTopbar;
protected chat: Chat;
protected listenerSetter: ListenerSetter;
public className: string;
public divAndCaption: DivAndCaption<(title: string | HTMLElement | DocumentFragment, subtitle: string | HTMLElement | DocumentFragment, message?: any) => void>;
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.title.classList.add(CLASSNAME_BASE + '-title');
divAndCaption.subtitle.classList.add(CLASSNAME_BASE + '-subtitle');
divAndCaption.content.classList.add(CLASSNAME_BASE + '-content');
this.close = document.createElement('button');
this.close.classList.add(CLASSNAME_BASE + '-close', `pinned-${className}-close`, 'btn-icon', 'tgico-close');
//divAndCaption.container.prepend(this.close);
this.btnClose = document.createElement('button');
this.btnClose.classList.add(CLASSNAME_BASE + '-close', `pinned-${className}-close`, 'btn-icon', 'tgico-close');
this.wrapper = document.createElement('div');
this.wrapper.classList.add(CLASSNAME_BASE + '-wrapper');
this.wrapper.append(...Array.from(divAndCaption.container.children));
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);
((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => {
@ -74,15 +85,16 @@ export default class PinnedContainer { @@ -74,15 +85,16 @@ export default class PinnedContainer {
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 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);
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 maxActive = hide ? 0 : 1;
@ -91,10 +103,11 @@ export default class PinnedContainer { @@ -91,10 +103,11 @@ export default class PinnedContainer {
scrollable.scrollTop = scrollTop + ((hide ? -1 : 1) * HEIGHT);
}
this.topbar.setFloating();
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.mid = '' + message.mid;
this.divAndCaption.fill(title, subtitle, message);

31
src/components/chat/pinnedMessage.ts

@ -257,7 +257,14 @@ export default class ChatPinnedMessage { @@ -257,7 +257,14 @@ export default class ChatPinnedMessage {
constructor(private topbar: ChatTopbar, private chat: Chat, private appMessagesManager: AppMessagesManager, private appPeersManager: AppPeersManager) {
this.listenerSetter = new ListenerSetter();
this.pinnedMessageContainer = new PinnedContainer(topbar, chat, this.listenerSetter, 'message', new ReplyContainer('pinned-message'), async() => {
const dAC = new ReplyContainer('pinned-message');
this.pinnedMessageContainer = new PinnedContainer({
topbar,
chat,
listenerSetter: this.listenerSetter,
className: 'message',
divAndCaption: dAC,
onClose: async() => {
if(appPeersManager.canPinMessage(this.topbar.peerId)) {
new PopupPinMessage(this.topbar.peerId, this.pinnedMid, true);
} else {
@ -265,32 +272,34 @@ export default class ChatPinnedMessage { @@ -265,32 +272,34 @@ export default class ChatPinnedMessage {
}
return false;
}
});
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.pinnedMessageContainer.divAndCaption.subtitle.append(this.animatedSubtitle.container);
dAC.subtitle.append(this.animatedSubtitle.container);
this.animatedMedia = new AnimatedSuper();
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.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.pinnedMessageContainer.divAndCaption.container.prepend(this.btnOpen);
this.pinnedMessageContainer.wrapperUtils.prepend(this.btnOpen);
attachClickEvent(this.btnOpen, (e) => {
cancelEvent(e);
this.topbar.openPinned(true);
}, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope)('peer_pinned_messages', (e) => {
const peerId = e.peerId;
this.listenerSetter.add(rootScope)('peer_pinned_messages', ({peerId}) => {
if(peerId === this.topbar.peerId) {
//this.wasPinnedIndex = 0;
//setTimeout(() => {
@ -310,9 +319,7 @@ export default class ChatPinnedMessage { @@ -310,9 +319,7 @@ export default class ChatPinnedMessage {
}
});
this.listenerSetter.add(rootScope)('peer_pinned_hidden', (e) => {
const {peerId, maxId} = e;
this.listenerSetter.add(rootScope)('peer_pinned_hidden', ({peerId}) => {
if(peerId === this.topbar.peerId) {
this.pinnedMessageContainer.toggle(this.hidden = true);
}

12
src/components/chat/selection.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* 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 ChatInput from "./input";
import type Chat from "./chat";
@ -51,6 +51,7 @@ class AppSelection { @@ -51,6 +51,7 @@ class AppSelection {
protected listenerSetter: ListenerSetter;
protected appMessagesManager: AppMessagesManager;
protected isScheduled: boolean;
protected listenElement: HTMLElement;
protected onToggleSelection: (forwards: boolean) => void;
@ -79,7 +80,8 @@ class AppSelection { @@ -79,7 +80,8 @@ class AppSelection {
verifyTouchLongPress?: AppSelection['verifyTouchLongPress'],
targetLookupClassName: string,
lookupBetweenParentClassName: string,
lookupBetweenElementsQuery: string
lookupBetweenElementsQuery: string,
isScheduled?: AppSelection['isScheduled']
}) {
safeAssign(this, options);
@ -326,8 +328,9 @@ class AppSelection { @@ -326,8 +328,9 @@ class AppSelection {
cantDelete = !size,
cantSend = !size;
for(const [peerId, mids] of this.selectedMids) {
const storage = this.isScheduled ? this.appMessagesManager.getScheduledMessagesStorage(peerId) : this.appMessagesManager.getMessagesStorage(peerId);
for(const mid of mids) {
const message = this.appMessagesManager.getMessageByPeer(peerId, mid);
const message = this.appMessagesManager.getMessageFromStorage(storage, mid);
if(!cantForward) {
if(message.action) {
cantForward = true;
@ -690,7 +693,8 @@ export default class ChatSelection extends AppSelection { @@ -690,7 +693,8 @@ export default class ChatSelection extends AppSelection {
verifyTouchLongPress: () => !this.chat.input.recording,
targetLookupClassName: 'bubble',
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"; @@ -43,6 +43,7 @@ import PopupPeer from "../popups/peer";
import generateVerifiedIcon from "../generateVerifiedIcon";
import { fastRaf } from "../../helpers/schedulers";
import AppEditContactTab from "../sidebarRight/tabs/editContact";
import appMediaPlaybackController from "../appMediaPlaybackController";
export default class ChatTopbar {
public container: HTMLDivElement;
@ -87,6 +88,7 @@ export default class ChatTopbar { @@ -87,6 +88,7 @@ export default class ChatTopbar {
this.container = document.createElement('div');
this.container.classList.add('sidebar-header', 'topbar');
this.container.dataset.floating = '0';
this.btnBack = ButtonIcon('left sidebar-close-button', {noRipple: true});
@ -141,16 +143,28 @@ export default class ChatTopbar { @@ -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);
if(this.chatAudio) {
this.container.append(this.chatAudio.divAndCaption.container, this.chatUtils);
}
// * construction end
// * fix topbar overflow section
this.listenerSetter.add(window)('resize', this.onResize);
mediaSizes.addEventListener('changeScreen', this.onChangeScreen);
this.listenerSetter.add(mediaSizes)('changeScreen', this.onChangeScreen);
attachClickEvent(this.container, (e) => {
const container: HTMLElement = findUpClassName(e.target, 'pinned-container');
@ -158,6 +172,10 @@ export default class ChatTopbar { @@ -158,6 +172,10 @@ export default class ChatTopbar {
if(container) {
cancelEvent(e);
if(findUpClassName(e.target, 'progress-line')) {
return;
}
const mid = +container.dataset.mid;
const peerId = +container.dataset.peerId;
if(container.classList.contains('pinned-message')) {
@ -165,7 +183,13 @@ export default class ChatTopbar { @@ -165,7 +183,13 @@ export default class ChatTopbar {
this.pinnedMessage.followPinnedMessage(mid);
//}
} 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 {
if(mediaSizes.activeScreen === ScreenSize.medium && document.body.classList.contains(LEFT_COLUMN_ACTIVE_CLASSNAME)) {
@ -350,7 +374,7 @@ export default class ChatTopbar { @@ -350,7 +374,7 @@ export default class ChatTopbar {
},
verify: () => {
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',
@ -448,17 +472,13 @@ export default class ChatTopbar { @@ -448,17 +472,13 @@ export default class ChatTopbar {
}
});
this.listenerSetter.add(rootScope)('peer_typings', (e) => {
const {peerId} = e;
this.listenerSetter.add(rootScope)('peer_typings', ({peerId}) => {
if(this.peerId === peerId) {
this.setPeerStatus();
}
});
this.listenerSetter.add(rootScope)('user_update', (e) => {
const userId = e;
this.listenerSetter.add(rootScope)('user_update', (userId) => {
if(this.peerId === userId) {
this.setPeerStatus();
}
@ -510,20 +530,20 @@ export default class ChatTopbar { @@ -510,20 +530,20 @@ export default class ChatTopbar {
private onResize = () => {
this.setUtilsWidth(true);
this.setFloating();
};
private onChangeScreen = (from: ScreenSize, to: ScreenSize) => {
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.setUtilsWidth(true);
this.onResize();
};
public destroy() {
//this.chat.log.error('Topbar destroying');
this.listenerSetter.removeAll();
mediaSizes.removeEventListener('changeScreen', this.onChangeScreen);
window.clearInterval(this.setPeerStatusInterval);
if(this.pinnedMessage) {
@ -713,6 +733,16 @@ export default class ChatTopbar { @@ -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) => {
if(!this.subtitle) return;

357
src/components/peerProfile.ts

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

3
src/components/popups/index.ts

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

41
src/components/rangeSelector.ts

@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
import { clamp } from "../helpers/number";
import attachGrabListeners, { GrabEvent } from "../helpers/dom/attachGrabListeners";
import { safeAssign } from "../helpers/object";
export default class RangeSelector {
public container: HTMLDivElement;
@ -25,10 +26,33 @@ export default class RangeSelector { @@ -25,10 +26,33 @@ export default class RangeSelector {
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.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');
}
@ -39,7 +63,7 @@ export default class RangeSelector { @@ -39,7 +63,7 @@ export default class RangeSelector {
seek.classList.add('progress-line__seek');
//seek.setAttribute('max', '0');
seek.type = 'range';
seek.step = '' + step;
seek.step = '' + this.step;
seek.min = '' + this.min;
seek.max = '' + this.max;
seek.value = '' + value;
@ -108,14 +132,19 @@ export default class RangeSelector { @@ -108,14 +132,19 @@ export default class RangeSelector {
let percents = (value - this.min) / (this.max - this.min);
percents = clamp(percents, 0, 1);
// using scaleX and width even with vertical because it will be rotated
if(this.useTransform) {
this.filled.style.transform = `scaleX(${percents})`;
} else {
this.filled.style.width = (percents * 100) + '%';
//this.filled.style.transform = 'scaleX(' + scaleX + ')';
}
}
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)) {
value -= this.step / 10;

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

@ -49,7 +49,11 @@ export class RangeSettingSelector { @@ -49,7 +49,11 @@ export class RangeSettingSelector {
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.setHandlers({
onScrub: value => {

15
src/components/sidebarRight/index.ts

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

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

@ -4,711 +4,26 @@ @@ -4,711 +4,26 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import appImManager from "../../../lib/appManagers/appImManager";
import appMessagesManager, { AppMessagesManager, MyMessage } from "../../../lib/appManagers/appMessagesManager";
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 appMessagesManager from "../../../lib/appManagers/appMessagesManager";
import appUsersManager from "../../../lib/appManagers/appUsersManager";
import rootScope from "../../../lib/rootScope";
import AppSearchSuper, { SearchSuperType } from "../../appSearchSuper.";
import AvatarElement, { openAvatarViewer } from "../../avatar";
import SidebarSlider, { SliderSuperTab } from "../../slider";
import CheckboxField from "../../checkboxField";
import appSidebarRight from "..";
import { TransitionSlider } from "../../transition";
import appNotificationsManager from "../../../lib/appManagers/appNotificationsManager";
import AppEditChatTab from "./editChat";
import PeerTitle from "../../peerTitle";
import AppEditContactTab from "./editContact";
import appChatsManager, { Channel } from "../../../lib/appManagers/appChatsManager";
import { Chat, Message, MessageAction, ChatFull, Photo } from "../../../layer";
import appChatsManager from "../../../lib/appManagers/appChatsManager";
import Button from "../../button";
import ButtonIcon from "../../buttonIcon";
import I18n, { i18n, LangPackKey } from "../../../lib/langPack";
import { generateDelimiter, SettingSection } from "../../sidebarLeft";
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 { i18n, LangPackKey } from "../../../lib/langPack";
import { toastNew } from "../../toast";
import AppAddMembersTab from "../../sidebarLeft/tabs/addMembers";
import PopupPickUser from "../../popups/pickUser";
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 { cancelEvent } from "../../../helpers/dom/cancelEvent";
import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import replaceContent from "../../../helpers/dom/replaceContent";
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;
}
}
import PeerProfile from "../../peerProfile";
// TODO: отредактированное сообщение не изменится
export default class AppSharedMediaTab extends SliderSuperTab {
@ -803,16 +118,16 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -803,16 +118,16 @@ export default class AppSharedMediaTab extends SliderSuperTab {
transition(0);
animatedCloseIcon.classList.remove('state-back');
} else if(!this.scrollable.isHeavyAnimationInProgress) {
appSidebarRight.onCloseBtnClick();
this.slider.onCloseBtnClick();
}
});
attachClickEvent(this.editBtn, (e) => {
let tab: AppEditChatTab | AppEditContactTab;
if(this.peerId < 0) {
tab = new AppEditChatTab(appSidebarRight);
tab = new AppEditChatTab(this.slider);
} else {
tab = new AppEditContactTab(appSidebarRight);
tab = new AppEditContactTab(this.slider);
}
if(tab) {
@ -838,6 +153,21 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -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.searchSuper = new AppSearchSuper({
@ -1120,4 +450,4 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -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);

49
src/components/wrappers.ts

@ -19,8 +19,8 @@ import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager'; @@ -19,8 +19,8 @@ import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager';
import LottieLoader from '../lib/lottieLoader';
import webpWorkerController from '../lib/webp/webpWorkerController';
import animationIntersector from './animationIntersector';
import appMediaPlaybackController from './appMediaPlaybackController';
import AudioElement from './audio';
import appMediaPlaybackController, { MediaSearchContext } from './appMediaPlaybackController';
import AudioElement, { findAudioTargets as findMediaTargets } from './audio';
import ReplyContainer from './chat/replyContainer';
import { Layouter, RectPart } from './groupedLayout';
import LazyLoadQueue from './lazyLoadQueue';
@ -48,6 +48,7 @@ import IS_WEBP_SUPPORTED from '../environment/webpSupport'; @@ -48,6 +48,7 @@ import IS_WEBP_SUPPORTED from '../environment/webpSupport';
import MEDIA_MIME_TYPES_SUPPORTED from '../environment/mediaMimeTypesSupport';
import { MiddleEllipsisElement } from './middleEllipsis';
import { joinElementsWith } from '../lib/langPack';
import throttleWithRaf from '../helpers/schedulers/throttleWithRaf';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
@ -92,7 +93,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -92,7 +93,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
loadPromises?: Promise<any>[],
noAutoDownload?: boolean,
size?: PhotoSize,
searchContext?: SearchSuperContext
searchContext?: MediaSearchContext,
}) {
const isAlbumItem = !(boxWidth && boxHeight);
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 @@ -168,12 +169,11 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
video.setAttribute('playsinline', 'true');
video.muted = true;
if(doc.type === 'round') {
const globalVideo = appMediaPlaybackController.addMedia(message.peerId, doc, message.mid, !noAutoDownload) as HTMLVideoElement;
const divRound = document.createElement('div');
divRound.classList.add('media-round', 'z-depth-1');
divRound.dataset.mid = '' + message.mid;
divRound.dataset.peerId = '' + message.peerId;
(divRound as any).message = message;
const size = mediaSizes.active.round;
const halfSize = size.width / 2;
@ -209,6 +209,9 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -209,6 +209,9 @@ 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.clip(); */
const onLoad = () => {
const message: Message.message = (divRound as any).message;
const globalVideo = appMediaPlaybackController.addMedia(message, !noAutoDownload) as HTMLVideoElement;
const clear = () => {
(appImManager.chat.setPeerPromise || Promise.resolve()).finally(() => {
if(isInDOM(globalVideo)) {
@ -216,7 +219,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -216,7 +219,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}
globalVideo.removeEventListener('play', onPlay);
globalVideo.removeEventListener('timeupdate', onTimeUpdate);
globalVideo.removeEventListener('timeupdate', throttledTimeUpdate);
globalVideo.removeEventListener('pause', onPaused);
globalVideo.removeEventListener('ended', onEnded);
});
@ -232,16 +235,24 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -232,16 +235,24 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
};
const onTimeUpdate = () => {
if(!globalVideo.duration) return;
if(!globalVideo.duration) {
return;
}
if(!isInDOM(globalVideo)) {
clear();
return;
}
if(globalVideo.paused) {
onFrame();
}
spanTime.innerText = (globalVideo.duration - globalVideo.currentTime + '').toHHMMSS(false);
};
const throttledTimeUpdate = throttleWithRaf(onTimeUpdate);
const onPlay = () => {
video.classList.add('hide');
divRound.classList.remove('is-paused');
@ -274,7 +285,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -274,7 +285,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
};
globalVideo.addEventListener('play', onPlay);
globalVideo.addEventListener('timeupdate', onTimeUpdate);
globalVideo.addEventListener('timeupdate', throttledTimeUpdate);
globalVideo.addEventListener('pause', onPaused);
globalVideo.addEventListener('ended', onEnded);
@ -293,7 +304,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -293,7 +304,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
if(globalVideo.paused) {
if(appMediaPlaybackController.setSearchContext(searchContext)) {
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid});
const [prev, next] = findMediaTargets(divRound, searchContext.useSearch);
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next);
}
globalVideo.play();
@ -313,6 +325,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -313,6 +325,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
} else {
onPlay();
}
};
if(message.pFlags.is_outgoing) {
(divRound as any).onLoad = onLoad;
divRound.dataset.isOutgoing = '1';
} else {
onLoad();
}
} else {
video.autoplay = true; // для safari
}
@ -456,7 +476,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -456,7 +476,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
}
if(doc.type === 'round') {
appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid);
appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid, message.pFlags.is_scheduled);
}
renderImageFromUrl(video, cacheContext.url);
@ -515,7 +535,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS @@ -515,7 +535,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
fontWeight?: number,
voiceAsMusic?: boolean,
showSender?: boolean,
searchContext?: SearchSuperContext,
searchContext?: MediaSearchContext,
loadPromises?: Promise<any>[],
noAutoDownload?: boolean,
lazyLoadQueue?: LazyLoadQueue
@ -526,8 +546,6 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS @@ -526,8 +546,6 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
const uploading = message.pFlags.is_outgoing && message.media?.preloader;
if(doc.type === 'audio' || doc.type === 'voice' || doc.type === 'round') {
const audioElement = new AudioElement();
audioElement.dataset.mid = '' + message.mid;
audioElement.dataset.peerId = '' + message.peerId;
audioElement.withTime = withTime;
audioElement.message = message;
audioElement.noAutoDownload = noAutoDownload;
@ -1609,7 +1627,7 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo @@ -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,
message: any,
messageDiv: HTMLElement,
@ -1619,7 +1637,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, @@ -1619,7 +1637,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
loadPromises?: Promise<any>[],
noAutoDownload?: boolean,
lazyLoadQueue?: LazyLoadQueue,
searchContext?: SearchSuperContext
searchContext?: MediaSearchContext,
useSearch?: boolean,
}) {
let nameContainer: HTMLElement;
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'; @@ -16,7 +16,7 @@ export const MAIN_DOMAIN = 'web.telegram.org';
const App = {
id: 1025907,
hash: '452b0359b988148995f22ff0f4229750',
version: '0.8.5',
version: '0.8.6',
langPackVersion: '0.3.3',
langPack: 'macos',
langPackCode: 'en',

5
src/environment/parallaxSupport.ts

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

14
src/helpers/dom/attachGrabListeners.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* 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,
onStart: (position: GrabEvent) => void,
@ -12,13 +12,13 @@ export default function attachGrabListeners(element: HTMLElement, @@ -12,13 +12,13 @@ export default function attachGrabListeners(element: HTMLElement,
onEnd?: (position: GrabEvent) => void) {
// * Mouse
const onMouseMove = (event: MouseEvent) => {
onMove({x: event.pageX, y: event.pageY});
onMove({x: event.pageX, y: event.pageY, event});
};
const onMouseUp = (event: MouseEvent) => {
document.removeEventListener('mousemove', onMouseMove);
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) => {
@ -27,7 +27,7 @@ export default function attachGrabListeners(element: HTMLElement, @@ -27,7 +27,7 @@ export default function attachGrabListeners(element: HTMLElement,
return;
}
onStart({x: event.pageX, y: event.pageY});
onStart({x: event.pageX, y: event.pageY, event});
onMouseMove(event);
document.addEventListener('mousemove', onMouseMove);
@ -39,17 +39,17 @@ export default function attachGrabListeners(element: HTMLElement, @@ -39,17 +39,17 @@ export default function attachGrabListeners(element: HTMLElement,
// * Touch
const onTouchMove = (event: TouchEvent) => {
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) => {
document.removeEventListener('touchmove', onTouchMove);
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) => {
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);
document.addEventListener('touchmove', onTouchMove, {passive: false});

25
src/helpers/filterChatPhotosMessages.ts

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

4
src/helpers/object.ts

@ -59,9 +59,9 @@ export function defineNotNumerableProperties(obj: {[key: string]: any}, names: s @@ -59,9 +59,9 @@ export function defineNotNumerableProperties(obj: {[key: string]: any}, names: s
//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 [];
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);
else return ids.sort((a, b) => b - a);
}

161
src/helpers/searchListLoader.ts

@ -0,0 +1,161 @@ @@ -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 { @@ -830,6 +830,7 @@ export namespace Message {
pinned?: true,
unread?: true,
is_outgoing?: true,
is_scheduled?: true,
}>,
id: number,
from_id?: Peer,

33
src/lib/appManagers/apiUpdatesManager.ts

@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
//import apiManager from '../mtproto/apiManager';
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 apiManager from '../mtproto/mtprotoworker';
import rootScope from '../rootScope';
@ -22,6 +22,8 @@ import appPeersManager from "./appPeersManager"; @@ -22,6 +22,8 @@ import appPeersManager from "./appPeersManager";
import appStateManager from './appStateManager';
import serverTimeManager from '../mtproto/serverTimeManager';
import assumeType from '../../helpers/assumeType';
import noop from '../../helpers/noop';
import RichTextProcessor from '../richtextprocessor';
type UpdatesState = {
pendingPtsUpdates: (Update & {pts: number, pts_count: number})[],
@ -633,6 +635,8 @@ export class ApiUpdatesManager { @@ -633,6 +635,8 @@ export class ApiUpdatesManager {
appStateManager.getState().then(_state => {
const state = _state.updates;
const newVersion = appStateManager.newVersion/* || '0.8.6' */;
//rootScope.broadcast('state_synchronizing');
if(!state || !state.pts || !state.date || !state.seq) {
this.log('will get new state');
@ -679,6 +683,33 @@ export class ApiUpdatesManager { @@ -679,6 +683,33 @@ export class ApiUpdatesManager {
// this.updatesState.syncLoading.then(() => {
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 { @@ -461,7 +461,7 @@ export class AppDialogsManager {
appStateManager.getState().then((state) => {
return this.onStateLoaded(state);
}).then(() => {
//return;
// return;
const isLoadedMain = appMessagesManager.dialogsStorage.isDialogsLoaded(0);
const isLoadedArchive = appMessagesManager.dialogsStorage.isDialogsLoaded(1);

16
src/lib/appManagers/appImManager.ts

@ -876,22 +876,6 @@ export class AppImManager { @@ -876,22 +876,6 @@ export class AppImManager {
private init() {
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) {
this.attachDragAndDropListeners();
}

8
src/lib/appManagers/appMessagesIdsManager.ts

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

277
src/lib/appManagers/appMessagesManager.ts

@ -109,10 +109,7 @@ export type PinnedStorage = Partial<{ @@ -109,10 +109,7 @@ export type PinnedStorage = Partial<{
count: number,
maxId: number
}>;
export type MessagesStorage = {
//generateIndex: (message: any) => void
[mid: string]: any
};
export type MessagesStorage = Map<number, any>;
export type MyMessageActionType = Message.messageService['action']['_'];
@ -1586,7 +1583,7 @@ export class AppMessagesManager { @@ -1586,7 +1583,7 @@ export class AppMessagesManager {
fromId: peerId
} as Message.messageService;
this.getMessagesStorage(peerId)[maxId] = message;
this.getMessagesStorage(peerId).set(maxId, message);
return message;
}
@ -1628,7 +1625,7 @@ export class AppMessagesManager { @@ -1628,7 +1625,7 @@ export class AppMessagesManager {
historyStorage.history.delete(tempId);
delete this.pendingByRandomId[randomId];
delete storage[tempId];
storage.delete(tempId);
return true;
}
@ -1936,17 +1933,17 @@ export class AppMessagesManager { @@ -1936,17 +1933,17 @@ export class AppMessagesManager {
return promise;
}
public getMessageFromStorage(storage: MessagesStorage, messageId: number) {
return storage && storage[messageId] || {
public getMessageFromStorage(storage: MessagesStorage, mid: number) {
return storage && storage.get(mid) || {
_: 'messageEmpty',
id: messageId,
id: mid,
deleted: true,
pFlags: {}
};
}
private createMessageStorage() {
const storage: MessagesStorage = {} as any;
const storage: MessagesStorage = new Map();
/* let num = 0;
Object.defineProperty(storage, 'num', {
@ -1977,7 +1974,7 @@ export class AppMessagesManager { @@ -1977,7 +1974,7 @@ export class AppMessagesManager {
continue;
}
const message = this.messagesStorageByPeerId[peerId][messageId];
const message = this.messagesStorageByPeerId[peerId].get(messageId);
if(message) {
return message;
}
@ -2085,15 +2082,27 @@ export class AppMessagesManager { @@ -2085,15 +2082,27 @@ export class AppMessagesManager {
}
return this.doFlushHistory(appPeersManager.getInputPeerById(peerId), justClear, revoke).then(() => {
delete this.historiesStorage[peerId];
delete this.messagesStorageByPeerId[peerId];
delete this.scheduledMessagesStorage[peerId];
delete this.threadsStorage[peerId];
delete this.searchesStorage[peerId];
delete this.pinnedMessages[peerId];
delete this.pendingAfterMsgs[peerId];
delete this.pendingTopMsgs[peerId];
delete this.needSingleMessages[peerId];
[
this.historiesStorage,
this.threadsStorage,
this.searchesStorage,
this.pinnedMessages,
this.pendingAfterMsgs,
this.pendingTopMsgs,
this.needSingleMessages
].forEach(s => {
delete s[peerId];
});
[
this.messagesStorageByPeerId,
this.scheduledMessagesStorage
].forEach(s => {
const ss = s[peerId];
if(ss) {
ss.clear();
}
});
if(justClear) {
rootScope.dispatchEvent('dialog_flush', {peerId});
@ -2166,12 +2175,11 @@ export class AppMessagesManager { @@ -2166,12 +2175,11 @@ export class AppMessagesManager {
if(!affectedHistory.offset) {
const storage = this.getMessagesStorage(peerId);
for(const mid in storage) {
const message = storage[mid];
storage.forEach((message) => {
if(message.pFlags.pinned) {
delete message.pFlags.pinned;
}
}
});
rootScope.dispatchEvent('peer_pinned_messages', {peerId, unpinAll: true});
delete this.pinnedMessages[peerId];
@ -2186,8 +2194,7 @@ export class AppMessagesManager { @@ -2186,8 +2194,7 @@ export class AppMessagesManager {
public getAlbumText(grouped_id: string) {
const group = this.groupedMessagesStorage[grouped_id];
let foundMessages = 0, message: string, totalEntities: MessageEntity[], entities: MessageEntity[];
for(const i in group) {
const m = group[i];
for(const [mid, m] of group) {
if(m.message) {
if(++foundMessages > 1) break;
message = m.message;
@ -2219,8 +2226,7 @@ export class AppMessagesManager { @@ -2219,8 +2226,7 @@ export class AppMessagesManager {
const out: MyMessage[] = [];
if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id];
for(const mid in storage) {
const message = storage[mid];
for(const [mid, message] of storage) {
if(verify(message)) {
out.push(message);
}
@ -2276,8 +2282,8 @@ export class AppMessagesManager { @@ -2276,8 +2282,8 @@ export class AppMessagesManager {
message.mid = mid;
if(message.grouped_id) {
const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = {});
storage[mid] = message;
const storage = this.groupedMessagesStorage[message.grouped_id] ?? (this.groupedMessagesStorage[message.grouped_id] = new Map());
storage.set(mid, message);
}
const dialog = this.getDialogOnly(peerId);
@ -2527,7 +2533,7 @@ export class AppMessagesManager { @@ -2527,7 +2533,7 @@ export class AppMessagesManager {
this.wrapMessageEntities(message);
}
storage[mid] = message;
storage.set(mid, message);
});
/* if(groups) {
@ -3253,70 +3259,24 @@ export class AppMessagesManager { @@ -3253,70 +3259,24 @@ export class AppMessagesManager {
});
}
public getSearch({peerId, query, inputFilter, maxId, limit, nextRate, backLimit, threadId, folderId, minDate, maxDate}: {
peerId?: number,
maxId?: number,
limit?: number,
nextRate?: number,
backLimit?: number,
threadId?: number,
folderId?: number,
query?: string,
inputFilter?: {
_: MyInputMessagesFilter
},
minDate?: number,
maxDate?: number
}): Promise<{
count: number,
next_rate: number,
offset_id_offset: number,
history: MyMessage[]
}> {
if(!peerId) peerId = 0;
if(!query) query = '';
if(!inputFilter) inputFilter = {_: 'inputMessagesFilterEmpty'};
if(limit === undefined) limit = 20;
if(!nextRate) nextRate = 0;
if(!backLimit) backLimit = 0;
minDate = minDate ? minDate / 1000 | 0 : 0;
maxDate = maxDate ? maxDate / 1000 | 0 : 0;
const foundMsgs: Message.message[] = [];
//this.log('search', maxId);
if(backLimit) {
limit += backLimit;
public filterMessagesByInputFilter(inputFilter: MyInputMessagesFilter, history: number[], storage: MessagesStorage, limit: number) {
const foundMsgs: MyMessage[] = [];
if(!history.length) {
return foundMsgs;
}
//const beta = inputFilter._ === 'inputMessagesFilterPinned' && !backLimit;
const beta = false;
let storage: {
count?: number;
history: SlicedArray;
};
// * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений
if(peerId && !backLimit && !maxId && !query && limit !== 1 && !threadId/* && inputFilter._ !== 'inputMessagesFilterPinned' */) {
storage = beta ?
this.getSearchStorage(peerId, inputFilter._) as any :
this.getHistoryStorage(peerId);
let filtering = true;
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[] = []/* ,
const neededContents: Partial<{
[messageMediaType in MessageMedia['_']]: boolean
}> & Partial<{
avatar: boolean,
url: boolean
}> = {},
neededDocTypes: MyDocument['type'][] = [],
excludeDocTypes: MyDocument['type'][] = []/* ,
neededFlags: string[] = [] */;
switch(inputFilter._) {
switch(inputFilter) {
case 'inputMessagesFilterPhotos':
neededContents['messageMediaPhoto'] = true;
break;
@ -3383,23 +3343,24 @@ export class AppMessagesManager { @@ -3383,23 +3343,24 @@ export class AppMessagesManager {
}); */
}
if(filtering) {
const storage = this.getMessagesStorage(peerId);
for(let i = 0, length = history.length; i < length; i++) {
const message = storage[history.slice[i]];
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.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)) {
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) {
@ -3407,7 +3368,15 @@ export class AppMessagesManager { @@ -3407,7 +3368,15 @@ export class AppMessagesManager {
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._)) {
}
} 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;
@ -3420,8 +3389,62 @@ export class AppMessagesManager { @@ -3420,8 +3389,62 @@ export class AppMessagesManager {
}
}
}
return foundMsgs;
}
public getSearch({peerId, query, inputFilter, maxId, limit, nextRate, backLimit, threadId, folderId, minDate, maxDate}: {
peerId?: number,
maxId?: number,
limit?: number,
nextRate?: number,
backLimit?: number,
threadId?: number,
folderId?: number,
query?: string,
inputFilter?: {
_: MyInputMessagesFilter
},
minDate?: number,
maxDate?: number
}): Promise<{
count: number,
next_rate: number,
offset_id_offset: number,
history: MyMessage[]
}> {
if(!peerId) peerId = 0;
if(!query) query = '';
if(!inputFilter) inputFilter = {_: 'inputMessagesFilterEmpty'};
if(limit === undefined) limit = 20;
if(!nextRate) nextRate = 0;
if(!backLimit) backLimit = 0;
minDate = minDate ? minDate / 1000 | 0 : 0;
maxDate = maxDate ? maxDate / 1000 | 0 : 0;
let foundMsgs: MyMessage[] = [];
//this.log('search', maxId);
if(backLimit) {
limit += backLimit;
}
//const beta = inputFilter._ === 'inputMessagesFilterPinned' && !backLimit;
const beta = false;
let storage: {
count?: number;
history: SlicedArray;
};
// * костыль для limit 1, если нужно и получить сообщение, и узнать количество сообщений
if(peerId && !backLimit && !maxId && !query && limit !== 1 && !threadId/* && inputFilter._ !== 'inputMessagesFilterPinned' */) {
storage = beta ?
this.getSearchStorage(peerId, inputFilter._) as any :
this.getHistoryStorage(peerId);
foundMsgs = this.filterMessagesByInputFilter(inputFilter._, storage.history.slice, this.getMessagesStorage(peerId), limit);
}
if(foundMsgs.length) {
@ -3789,10 +3812,11 @@ export class AppMessagesManager { @@ -3789,10 +3812,11 @@ export class AppMessagesManager {
apiPromise.finally(() => {
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) {
this.readHistory(peerId, historyStorage.readMaxId, threadId, true);
if(readMaxId > maxId) {
this.readHistory(peerId, readMaxId, threadId, true);
}
});
@ -4011,7 +4035,7 @@ export class AppMessagesManager { @@ -4011,7 +4035,7 @@ export class AppMessagesManager {
const isLocalThreadUpdate = update._ === 'updateNewDiscussionMessage';
// * 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 threadId = threadKey ? +threadKey.split('_')[1] : undefined;
@ -4200,7 +4224,7 @@ export class AppMessagesManager { @@ -4200,7 +4224,7 @@ export class AppMessagesManager {
const peerId = this.getMessagePeer(message);
const mid = appMessagesIdsManager.generateMessageId(message.id);
const storage = this.getMessagesStorage(peerId);
if(storage[mid] === undefined) {
if(!storage.has(mid)) {
return;
}
@ -4269,12 +4293,12 @@ export class AppMessagesManager { @@ -4269,12 +4293,12 @@ export class AppMessagesManager {
}
for(let i = 0, length = history.length; i < length; i++) {
const messageId = history[i];
if(messageId > maxId) {
const mid = history[i];
if(mid > maxId) {
continue;
}
const message: MyMessage = storage[messageId];
const message: MyMessage = storage.get(mid);
if(message.pFlags.out !== isOut) {
continue;
@ -4309,7 +4333,7 @@ export class AppMessagesManager { @@ -4309,7 +4333,7 @@ export class AppMessagesManager {
}
}
appNotificationsManager.cancel('msg' + messageId);
appNotificationsManager.cancel('msg' + mid);
}
}
@ -4557,14 +4581,14 @@ export class AppMessagesManager { @@ -4557,14 +4581,14 @@ export class AppMessagesManager {
const messages = update.messages.map(id => appMessagesIdsManager.generateMessageId(id));
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();
getMissingPromise.finally(() => {
const werePinned = update.pFlags?.pinned;
if(werePinned) {
for(const mid of messages) {
//storage.history.push(mid);
const message = storage[mid];
const message = storage.get(mid);
message.pFlags.pinned = true;
}
@ -4577,7 +4601,7 @@ export class AppMessagesManager { @@ -4577,7 +4601,7 @@ export class AppMessagesManager {
} else {
for(const mid of messages) {
//storage.history.findAndSplice(_mid => _mid === mid);
const message = storage[mid];
const message = storage.get(mid);
delete message.pFlags.pinned;
}
}
@ -4772,7 +4796,7 @@ export class AppMessagesManager { @@ -4772,7 +4796,7 @@ export class AppMessagesManager {
}
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];
//this.log.warn(callbacks, tempId);
if(callbacks !== undefined) {
@ -4787,10 +4811,10 @@ export class AppMessagesManager { @@ -4787,10 +4811,10 @@ export class AppMessagesManager {
// set cached url to media
if(message.media) {
if(message.media.photo) {
const {photo: newPhoto, document: newDoc} = message.media as any;
if(newPhoto) {
const photo = appPhotosManager.getPhoto('' + tempId);
if(/* photo._ !== 'photoEmpty' */photo) {
const newPhoto = message.media.photo as MyPhoto;
const newPhotoSize = newPhoto.sizes[newPhoto.sizes.length - 1];
const cacheContext = appDownloadManager.getCacheContext(newPhoto, newPhotoSize.type);
const oldCacheContext = appDownloadManager.getCacheContext(photo, 'full');
@ -4802,11 +4826,10 @@ export class AppMessagesManager { @@ -4802,11 +4826,10 @@ export class AppMessagesManager {
const fileName = getFileNameByLocation(downloadOptions.location);
appDownloadManager.fakeDownload(fileName, oldCacheContext.url);
}
} else if(message.media.document) {
} else if(newDoc) {
const doc = appDocsManager.getDoc('' + tempId);
if(doc) {
if(/* doc._ !== 'documentEmpty' && */doc.type && doc.type !== 'sticker') {
const newDoc = message.media.document;
const cacheContext = appDownloadManager.getCacheContext(newDoc);
const oldCacheContext = appDownloadManager.getCacheContext(doc);
Object.assign(cacheContext, oldCacheContext);
@ -4815,18 +4838,18 @@ export class AppMessagesManager { @@ -4815,18 +4838,18 @@ export class AppMessagesManager {
appDownloadManager.fakeDownload(fileName, oldCacheContext.url);
}
}
} else if(message.media.poll) {
} else if((message.media as MessageMedia.messageMediaPoll).poll) {
delete appPollsManager.polls[tempId];
delete appPollsManager.results[tempId];
}
}
const tempMessage = this.getMessageFromStorage(storage, tempId);
delete storage[tempId];
storage.delete(tempId);
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) {
@ -4926,12 +4949,16 @@ export class AppMessagesManager { @@ -4926,12 +4949,16 @@ export class AppMessagesManager {
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[]> {
if(!this.canSendToPeer(peerId)) return Promise.resolve([]);
const storage = this.getScheduledMessagesStorage(peerId);
if(Object.keys(storage).length) {
return Promise.resolve(Object.keys(storage).map(id => +id));
if(storage.size) {
return Promise.resolve([...storage.keys()]);
}
return apiManager.invokeApiSingle('messages.getScheduledHistory', {
@ -4944,7 +4971,7 @@ export class AppMessagesManager { @@ -4944,7 +4971,7 @@ export class AppMessagesManager {
const storage = this.getScheduledMessagesStorage(peerId);
this.saveMessages(historyResult.messages, {storage, isScheduled: true});
return Object.keys(storage).map(id => +id);
return [...storage.keys()];
}
return [];
@ -5441,19 +5468,19 @@ export class AppMessagesManager { @@ -5441,19 +5468,19 @@ export class AppMessagesManager {
if(groupedId) {
const groupedStorage = this.groupedMessagesStorage[groupedId];
if(groupedStorage) {
delete groupedStorage[mid];
groupedStorage.delete(mid);
if(!history.albums) history.albums = {};
(history.albums[groupedId] || (history.albums[groupedId] = new Set())).add(mid);
if(!Object.keys(groupedStorage).length) {
if(!groupedStorage.size) {
delete history.albums;
delete this.groupedMessagesStorage[groupedId];
}
}
}
delete storage[mid];
storage.delete(mid);
const peerMessagesToHandle = this.newMessagesToHandle[peerId];
if(peerMessagesToHandle && peerMessagesToHandle.has(mid)) {

5
src/lib/appManagers/appStateManager.ts

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

239
src/lib/mediaPlayer.ts

@ -16,67 +16,77 @@ import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle"; @@ -16,67 +16,77 @@ import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle";
import EventListenerBase from "../helpers/eventListenerBase";
import rootScope from "./rootScope";
import findUpClassName from "../helpers/dom/findUpClassName";
import { GrabEvent } from "../helpers/dom/attachGrabListeners";
import { attachClickEvent } from "../helpers/dom/clickEvent";
export class MediaProgressLine extends RangeSelector {
private filledLoad: HTMLDivElement;
protected filledLoad: HTMLDivElement;
private stopAndScrubTimeout = 0;
private progressRAF = 0;
protected progressRAF = 0;
constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) {
super(1000 / 60 / 1000, 0, 0, 1);
protected media: HTMLMediaElement;
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.classList.add('progress-line__filled', 'progress-line__loaded');
this.container.prepend(this.filledLoad);
//this.setLoadProgress();
} else if(this.filledLoad) {
this.filledLoad.classList.toggle('hide', !streamable);
}
this.media = media;
this.streamable = streamable;
if(!media.paused || media.currentTime > 0) {
this.onPlay();
}
let wasPlaying = false;
this.setSeekMax();
this.setListeners();
this.setHandlers({
onMouseDown: () => {
//super.onMouseDown(e);
//Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик
if(this.stopAndScrubTimeout) { // возможно лишнее
clearTimeout(this.stopAndScrubTimeout);
}
this.stopAndScrubTimeout = window.setTimeout(() => {
!this.media.paused && this.media.pause();
this.stopAndScrubTimeout = 0;
}, 150);
wasPlaying = !this.media.paused;
wasPlaying && this.media.pause();
},
onMouseUp: () => {
//super.onMouseUp(e);
if(this.stopAndScrubTimeout) {
clearTimeout(this.stopAndScrubTimeout);
this.stopAndScrubTimeout = 0;
onMouseUp: (e) => {
cancelEvent(e.event);
wasPlaying && this.media.play();
}
this.media.paused && this.media.play();
}
})
});
}
onLoadedData = () => {
protected onLoadedData = () => {
this.max = this.media.duration;
this.seek.setAttribute('max', '' + this.max);
};
onEnded = () => {
protected onEnded = () => {
this.setProgress();
};
onPlay = () => {
protected onPlay = () => {
let r = () => {
this.setProgress();
@ -94,11 +104,21 @@ export class MediaProgressLine extends RangeSelector { @@ -94,11 +104,21 @@ export class MediaProgressLine extends RangeSelector {
this.progressRAF = window.requestAnimationFrame(r);
};
onProgress = (e: Event) => {
protected onTimeUpdate = () => {
if(this.media.paused) {
this.setProgress();
if(this.streamable) {
this.setLoadProgress();
}
}
};
protected scrub(e: MouseEvent) {
protected onProgress = (e: Event) => {
this.setLoadProgress();
};
protected scrub(e: GrabEvent) {
const scrubTime = super.scrub(e);
this.media.currentTime = scrubTime;
return scrubTime;
@ -148,6 +168,7 @@ export class MediaProgressLine extends RangeSelector { @@ -148,6 +168,7 @@ export class MediaProgressLine extends RangeSelector {
super.setListeners();
this.media.addEventListener('ended', this.onEnded);
this.media.addEventListener('play', this.onPlay);
this.media.addEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.addEventListener('progress', this.onProgress);
}
@ -157,19 +178,90 @@ export class MediaProgressLine extends RangeSelector { @@ -157,19 +178,90 @@ export class MediaProgressLine extends RangeSelector {
this.media.removeEventListener('loadeddata', this.onLoadedData);
this.media.removeEventListener('ended', this.onEnded);
this.media.removeEventListener('play', this.onPlay);
this.media.removeEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.removeEventListener('progress', this.onProgress);
if(this.stopAndScrubTimeout) {
clearTimeout(this.stopAndScrubTimeout);
}
if(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<{
toggleControls: (show: boolean) => void
}> {
@ -239,73 +331,10 @@ export default class VideoPlayer extends EventListenerBase<{ @@ -239,73 +331,10 @@ export default class VideoPlayer extends EventListenerBase<{
timeDuration = player.querySelector('#time-duration') as HTMLElement;
timeDuration.innerHTML = String(video.duration | 0).toHHMMSS();
const volumeDiv = document.createElement('div');
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 volumeSelector = new VolumeSelector(this.listenerSetter);
const leftControls = player.querySelector('.left-controls');
leftControls.insertBefore(volumeDiv, timeElapsed.parentElement);
leftControls.insertBefore(volumeSelector.btn, timeElapsed.parentElement);
Array.from(toggle).forEach((button) => {
this.listenerSetter.add(button)('click', () => {
@ -360,13 +389,13 @@ export default class VideoPlayer extends EventListenerBase<{ @@ -360,13 +389,13 @@ export default class VideoPlayer extends EventListenerBase<{
if(e.code === 'KeyF') {
this.toggleFullScreen(fullScreenButton);
} else if(e.code === 'KeyM') {
onMuteClick();
appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
} else if(e.code === 'Space') {
this.togglePlay();
} else if(e.altKey && e.code === 'Equal') {
this.video.playbackRate += 0.25;
appMediaPlaybackController.playbackRate += .25;
} else if(e.altKey && e.code === 'Minus') {
this.video.playbackRate -= 0.25;
appMediaPlaybackController.playbackRate -= .25;
} else {
good = false;
}

2
src/lib/mtproto/mtprotoworker.ts

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

7
src/lib/rootScope.ts

@ -59,7 +59,7 @@ export type BroadcastEvents = { @@ -59,7 +59,7 @@ export type BroadcastEvents = {
'message_edit': {storage: MessagesStorage, peerId: number, mid: 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_read': void,
'messages_downloaded': {peerId: number, mids: number[]},
@ -75,8 +75,9 @@ export type BroadcastEvents = { @@ -75,8 +75,9 @@ export type BroadcastEvents = {
'stickers_installed': StickerSet.stickerSet,
'stickers_deleted': StickerSet.stickerSet,
'audio_play': {doc: MyDocument, mid: number, peerId: number},
'audio_pause': void,
'media_play': {doc: MyDocument, message: Message.message, media: HTMLMediaElement},
'media_pause': void,
'media_playback_params': {volume: number, muted: boolean, playbackRate: number},
'state_cleared': void,
'state_synchronized': number | void,

45
src/lib/storages/dialogs.ts

@ -120,7 +120,9 @@ export default class DialogsStorage { @@ -120,7 +120,9 @@ export default class DialogsStorage {
for(let i = 0, length = dialogs.length; i < length; ++i) {
const dialog = dialogs[i];
if(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) {
this.appMessagesManager.saveMessages([dialog.topMessage]);
@ -358,19 +360,20 @@ export default class DialogsStorage { @@ -358,19 +360,20 @@ export default class DialogsStorage {
} */
public setDialogToState(dialog: Dialog) {
const historyStorage = this.appMessagesManager.getHistoryStorage(dialog.peerId);
const messagesStorage = this.appMessagesManager.getMessagesStorage(dialog.peerId);
const {peerId, pts} = dialog;
const historyStorage = this.appMessagesManager.getHistoryStorage(peerId);
const messagesStorage = this.appMessagesManager.getMessagesStorage(peerId);
const history = historyStorage.history.slice;
let incomingMessage: any;
let incomingMessage: MyMessage;
for(let i = 0, length = history.length; i < length; ++i) {
const mid = history[i];
const message: MyMessage = this.appMessagesManager.getMessageFromStorage(messagesStorage, mid);
if(!message.pFlags.is_outgoing) {
if(!message.pFlags.is_outgoing/* || peerId === SERVICE_PEER_ID */) {
incomingMessage = message;
const fromId = message.viaBotId || message.fromId;
if(fromId !== dialog.peerId) {
this.appStateManager.requestPeer(fromId, 'topMessage_' + dialog.peerId, 1);
if(fromId !== peerId) {
this.appStateManager.requestPeer(fromId, 'topMessage_' + peerId, 1);
}
break;
@ -379,16 +382,26 @@ export default class DialogsStorage { @@ -379,16 +382,26 @@ export default class DialogsStorage {
dialog.topMessage = incomingMessage;
if(dialog.peerId < 0 && dialog.pts) {
const newPts = this.apiUpdatesManager.getChannelState(-dialog.peerId, dialog.pts).pts;
// DO NOT TOUCH THESE LINES, SOME REAL MAGIC HERE.
// * 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;
}
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) {
const filter = this.appMessagesManager.filtersStorage.filters[id];
@ -538,9 +551,16 @@ export default class DialogsStorage { @@ -538,9 +551,16 @@ export default class DialogsStorage {
const peerText = this.appPeersManager.getPeerSearchText(peerId);
this.dialogsIndex.indexObject(peerId, peerText);
let mid: number, message;
const wasDialogBefore = this.getDialogOnly(peerId);
let mid: number, message: MyMessage;
if(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);
} else {
mid = this.appMessagesManager.generateTempMessageId(peerId);
@ -573,9 +593,8 @@ export default class DialogsStorage { @@ -573,9 +593,8 @@ export default class DialogsStorage {
}
}
const wasDialogBefore = this.getDialogOnly(peerId);
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_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 @@ @@ -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 @@ @@ -69,6 +69,7 @@
{"name": "random_id", "type": "string"},
{"name": "unread", "type": "true"},
{"name": "is_outgoing", "type": "true"},
{"name": "is_scheduled", "type": "true"},
{"name": "rReply", "type": "string"},
{"name": "viaBotId", "type": "number"},
{"name": "clear_history", "type": "boolean"},

6
src/scss/partials/_audio.scss

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

6
src/scss/partials/_button.scss

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

4
src/scss/partials/_chat.scss

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

7
src/scss/partials/_chatBubble.scss

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

296
src/scss/partials/_chatPinned.scss

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

88
src/scss/partials/_chatTopbar.scss

@ -4,31 +4,67 @@ @@ -4,31 +4,67 @@
* 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-height: 3.5rem;
--pinned-floating-height: 0px;
--pinned-audio-height: 52px;
--pinned-message-height: 52px;
width: 100%;
background-color: #fff;
user-select: none;
box-shadow: 0px 1px 5px -1px rgba(0, 0, 0, .21);
z-index: 1;
min-height: 3.5rem;
max-height: 3.5rem;
&.is-pinned-floating {
&.is-pinned-audio-shown, &.is-pinned-message-shown:not(.hide-pinned) {
margin-bottom: 52px;
/* & + .bubbles {
margin-top: 52px;
} */
min-height: var(--height);
max-height: var(--height);
margin-bottom: var(--pinned-floating-height);
position: relative;
&:before {
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;
}
& ~ .drops-container {
--pinned-floating-height: 52px;
&.is-pinned-audio-floating {
--pinned-floating-height: var(--pinned-audio-height);
}
&.is-pinned-message-floating {
--pinned-floating-height: var(--pinned-message-height);
}
&.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) {
&.is-pinned-audio-floating.is-pinned-message-floating {
--pinned-floating-height: calc(var(--pinned-audio-height) + var(--pinned-message-height));
.pinned-message {
display: flex;
top: calc(var(--topbar-height) + var(--pinned-audio-height));
&:before {
border-top: none;
}
/* &:before {
box-shadow: none;
} */
}
}
@ -56,6 +92,10 @@ @@ -56,6 +92,10 @@
}
}
.pinned-container-wrapper-utils {
@include sidebar-transform();
}
.sidebar-close-button {
position: absolute;
}
@ -92,7 +132,8 @@ @@ -92,7 +132,8 @@
}
}
.peer-title, .info {
.peer-title,
.info {
@include text-overflow();
line-height: var(--line-height);
}
@ -145,18 +186,7 @@ @@ -145,18 +186,7 @@
right: 0px;
padding-right: inherit; */
@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;
}
}
@include sidebar-transform();
@include respond-to(handhelds) {
> .btn-icon:not(.btn-menu-toggle) {

13
src/scss/partials/_ckin.scss

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

5
src/scss/partials/_rightSidebar.scss

@ -664,10 +664,13 @@ @@ -664,10 +664,13 @@
#search-container {
.search-super-content-music {
.audio:not(.audio-show-progress) .audio-time {
.audio:not(.audio-show-progress) {
.audio-time,
.audio-description:before {
display: none;
}
}
}
.search-group.is-short {
li:nth-child(n + 4) {

16
src/scss/partials/_ripple.scss

@ -9,7 +9,12 @@ @@ -9,7 +9,12 @@
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 {
width: 100%;
height: 100%;
@ -59,7 +64,8 @@ @@ -59,7 +64,8 @@
//overflow: hidden;
pointer-events: none;
.btn-menu &, .c-ripple.is-square & {
.btn-menu &,
.c-ripple.is-square & {
animation-name: ripple-effect-handhelds;
//animation-timing-function: ease-out;
animation-duration: .2s;
@ -74,11 +80,13 @@ @@ -74,11 +80,13 @@
} */
}
.btn-menu &, &.is-square {
.btn-menu &,
&.is-square {
--ripple-duration: .2s;
}
&__circle.hiding, &__square.hiding {
&__circle.hiding,
&__square.hiding {
opacity: 0;
}
}

11
src/scss/style.scss

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

Loading…
Cancel
Save