Audio improvements
Changelogs
This commit is contained in:
parent
377b893add
commit
bf0a86abe9
6
CHANGELOG.md
Normal file
6
CHANGELOG.md
Normal file
@ -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.
|
@ -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",
|
||||
|
@ -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();
|
||||
|
@ -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 = (() => {
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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,11 +197,16 @@ 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;
|
||||
//media.append(source);
|
||||
@ -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 {
|
||||
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 {
|
||||
}
|
||||
}/* , 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 {
|
||||
}/* , {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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
if('mediaSession' in navigator) {
|
||||
this.setNewMediadata(message);
|
||||
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(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 {
|
||||
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 {
|
||||
|
||||
public toggle(play?: boolean) {
|
||||
if(!this.playingMedia) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(play === undefined) {
|
||||
@ -433,7 +542,7 @@ class AppMediaPlaybackController {
|
||||
}
|
||||
|
||||
if(this.playingMedia.paused !== play) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(play) {
|
||||
@ -441,6 +550,8 @@ class AppMediaPlaybackController {
|
||||
} else {
|
||||
this.playingMedia.pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public play = () => {
|
||||
@ -453,21 +564,45 @@ class AppMediaPlaybackController {
|
||||
|
||||
public stop = () => {
|
||||
const media = this.playingMedia;
|
||||
if(media) {
|
||||
if(!media.paused) {
|
||||
media.pause();
|
||||
}
|
||||
|
||||
media.currentTime = 0;
|
||||
simulateEvent(media, 'ended');
|
||||
|
||||
// this.playingMedia = undefined;
|
||||
if(!media) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!media.paused) {
|
||||
media.pause();
|
||||
}
|
||||
|
||||
media.currentTime = 0;
|
||||
simulateEvent(media, 'ended');
|
||||
|
||||
const details = this.mediaDetails.get(media);
|
||||
if(details?.clean) {
|
||||
media.src = '';
|
||||
const peerId = details.peerId;
|
||||
const s = details.isScheduled ? this.scheduled : this.media;
|
||||
const storage = s.get(peerId);
|
||||
if(storage) {
|
||||
storage.delete(details.mid);
|
||||
|
||||
if(!storage.size) {
|
||||
s.delete(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
media.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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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();
|
||||
|
File diff suppressed because it is too large
Load Diff
57
src/components/appMediaViewerAvatar.ts
Normal file
57
src/components/appMediaViewerAvatar.ts
Normal file
@ -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
Normal file
1522
src/components/appMediaViewerBase.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -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";
|
||||
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";
|
||||
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;
|
||||
|
||||
|
@ -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) {
|
||||
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) {
|
||||
const scrubTime = offsetX / availW /* width */ * audio.duration;
|
||||
audio.currentTime = scrubTime;
|
||||
}
|
||||
});
|
||||
}, noop);
|
||||
|
||||
return () => {
|
||||
progress.remove();
|
||||
@ -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) {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
}
|
||||
} else if(uploading) {
|
||||
this.dataset.isOutgoing = '1';
|
||||
this.preloader.attach(downloadDiv, false);
|
||||
//onLoad();
|
||||
}
|
||||
|
@ -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";
|
||||
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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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) => {
|
||||
cancelEvent(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);
|
||||
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();
|
||||
}, {listenerSetter: this.topbar.listenerSetter});
|
||||
});
|
||||
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 {
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
@ -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";
|
||||
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 {
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
.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 {
|
||||
|
||||
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 {
|
||||
//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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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) {
|
||||
|
@ -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);
|
||||
}; */
|
||||
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;
|
||||
|
||||
//classNames.push(`is-pinned-${className}-shown`);
|
||||
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);
|
||||
|
||||
attachClickEvent(this.close, (e) => {
|
||||
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);
|
||||
|
||||
divAndCaption.container.append(this.wrapper/* , this.close */);
|
||||
|
||||
attachClickEvent(this.btnClose, (e) => {
|
||||
cancelEvent(e);
|
||||
|
||||
((onClose ? onClose() : null) || Promise.resolve(true)).then(needClose => {
|
||||
@ -74,27 +85,29 @@ 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 isFloating = (this.floating || mediaSizes.isMobile) && !hide;
|
||||
const scrollTop = isFloating || this.divAndCaption.container.classList.contains('is-floating') ? scrollable.scrollTop : undefined;
|
||||
|
||||
const scrollTop = mediaSizes.isMobile /* && !appImManager.scrollable.isScrolledDown */ ? 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;
|
||||
|
||||
if(scrollTop !== undefined && active.length <= maxActive/* && !scrollable.isScrolledDown */) {
|
||||
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);
|
||||
|
@ -257,40 +257,49 @@ 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() => {
|
||||
if(appPeersManager.canPinMessage(this.topbar.peerId)) {
|
||||
new PopupPinMessage(this.topbar.peerId, this.pinnedMid, true);
|
||||
} else {
|
||||
new PopupPinMessage(this.topbar.peerId, 0, true);
|
||||
}
|
||||
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 {
|
||||
new PopupPinMessage(this.topbar.peerId, 0, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
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 {
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
protected listenerSetter: ListenerSetter;
|
||||
protected appMessagesManager: AppMessagesManager;
|
||||
protected isScheduled: boolean;
|
||||
protected listenElement: HTMLElement;
|
||||
|
||||
protected onToggleSelection: (forwards: boolean) => void;
|
||||
@ -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 {
|
||||
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 {
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
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,22 +143,38 @@ 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');
|
||||
blurActiveElement();
|
||||
if(container) {
|
||||
cancelEvent(e);
|
||||
|
||||
if(findUpClassName(e.target, 'progress-line')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mid = +container.dataset.mid;
|
||||
const peerId = +container.dataset.peerId;
|
||||
@ -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 {
|
||||
},
|
||||
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 {
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
});
|
||||
};
|
||||
|
||||
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
Normal file
357
src/components/peerProfile.ts
Normal file
@ -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
Normal file
332
src/components/peerProfileAvatars.ts
Normal file
@ -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;
|
||||
};
|
||||
}
|
@ -164,12 +164,15 @@ export default class PopupElement {
|
||||
rootScope.isOverlayActive = true;
|
||||
animationIntersector.checkAnimations(true);
|
||||
|
||||
this.listenerSetter.add(document.body)('keydown', (e) => {
|
||||
if(this.confirmShortcutIsSendShortcut ? isSendShortcutPressed(e) : e.key === 'Enter') {
|
||||
simulateClickEvent(this.btnConfirmOnEnter);
|
||||
cancelEvent(e);
|
||||
}
|
||||
});
|
||||
// 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 = () => {
|
||||
|
@ -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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
let percents = (value - this.min) / (this.max - this.min);
|
||||
percents = clamp(percents, 0, 1);
|
||||
|
||||
this.filled.style.width = (percents * 100) + '%';
|
||||
//this.filled.style.transform = 'scaleX(' + scaleX + ')';
|
||||
// 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) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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 => {
|
||||
|
@ -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 {
|
||||
}
|
||||
});
|
||||
|
||||
mediaSizes.addEventListener('resize', () => {
|
||||
this.setColumnProportion();
|
||||
});
|
||||
|
||||
this.sharedMediaTab = new AppSharedMediaTab(this);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
//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;
|
||||
|
@ -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 {
|
||||
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 {
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.AppSharedMediaTab = AppSharedMediaTab);
|
||||
// MOUNT_CLASS_TO && (MOUNT_CLASS_TO.AppSharedMediaTab = AppSharedMediaTab);
|
||||
|
@ -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';
|
||||
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
|
||||
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
|
||||
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,109 +209,129 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
|
||||
ctx.arc(canvas.width / 2, canvas.height / 2, canvas.width / 2, 0, Math.PI * 2);
|
||||
ctx.clip(); */
|
||||
|
||||
const clear = () => {
|
||||
(appImManager.chat.setPeerPromise || Promise.resolve()).finally(() => {
|
||||
if(isInDOM(globalVideo)) {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalVideo.removeEventListener('play', onPlay);
|
||||
globalVideo.removeEventListener('timeupdate', throttledTimeUpdate);
|
||||
globalVideo.removeEventListener('pause', onPaused);
|
||||
globalVideo.removeEventListener('ended', onEnded);
|
||||
});
|
||||
};
|
||||
|
||||
const onFrame = () => {
|
||||
ctx.drawImage(globalVideo, 0, 0);
|
||||
|
||||
const offset = roundVideoCircumference - globalVideo.currentTime / globalVideo.duration * roundVideoCircumference;
|
||||
circle.style.strokeDashoffset = '' + offset;
|
||||
|
||||
return !globalVideo.paused;
|
||||
};
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
if(!globalVideo.duration) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isInDOM(globalVideo)) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
globalVideo.removeEventListener('play', onPlay);
|
||||
globalVideo.removeEventListener('timeupdate', onTimeUpdate);
|
||||
globalVideo.removeEventListener('pause', onPaused);
|
||||
globalVideo.removeEventListener('ended', onEnded);
|
||||
});
|
||||
};
|
||||
if(globalVideo.paused) {
|
||||
onFrame();
|
||||
}
|
||||
|
||||
spanTime.innerText = (globalVideo.duration - globalVideo.currentTime + '').toHHMMSS(false);
|
||||
};
|
||||
|
||||
const onFrame = () => {
|
||||
ctx.drawImage(globalVideo, 0, 0);
|
||||
|
||||
const offset = roundVideoCircumference - globalVideo.currentTime / globalVideo.duration * roundVideoCircumference;
|
||||
circle.style.strokeDashoffset = '' + offset;
|
||||
|
||||
return !globalVideo.paused;
|
||||
};
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
if(!globalVideo.duration) return;
|
||||
|
||||
if(!isInDOM(globalVideo)) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
spanTime.innerText = (globalVideo.duration - globalVideo.currentTime + '').toHHMMSS(false);
|
||||
};
|
||||
|
||||
const onPlay = () => {
|
||||
video.classList.add('hide');
|
||||
divRound.classList.remove('is-paused');
|
||||
animateSingle(onFrame, canvas);
|
||||
|
||||
if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) {
|
||||
preloader.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onPaused = () => {
|
||||
if(!isInDOM(globalVideo)) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
divRound.classList.add('is-paused');
|
||||
};
|
||||
|
||||
const onEnded = () => {
|
||||
video.classList.remove('hide');
|
||||
divRound.classList.add('is-paused');
|
||||
|
||||
video.currentTime = 0;
|
||||
spanTime.innerText = ('' + globalVideo.duration).toHHMMSS(false);
|
||||
|
||||
if(globalVideo.currentTime) {
|
||||
globalVideo.currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
globalVideo.addEventListener('play', onPlay);
|
||||
globalVideo.addEventListener('timeupdate', onTimeUpdate);
|
||||
globalVideo.addEventListener('pause', onPaused);
|
||||
globalVideo.addEventListener('ended', onEnded);
|
||||
|
||||
attachClickEvent(canvas, (e) => {
|
||||
cancelEvent(e);
|
||||
|
||||
// ! костыль
|
||||
if(preloader && !preloader.detached) {
|
||||
preloader.onClick();
|
||||
}
|
||||
|
||||
// ! can't use it here. on Safari iOS video won't start.
|
||||
/* if(globalVideo.readyState < 2) {
|
||||
return;
|
||||
} */
|
||||
|
||||
if(globalVideo.paused) {
|
||||
if(appMediaPlaybackController.setSearchContext(searchContext)) {
|
||||
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid});
|
||||
const throttledTimeUpdate = throttleWithRaf(onTimeUpdate);
|
||||
|
||||
const onPlay = () => {
|
||||
video.classList.add('hide');
|
||||
divRound.classList.remove('is-paused');
|
||||
animateSingle(onFrame, canvas);
|
||||
|
||||
if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) {
|
||||
preloader.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onPaused = () => {
|
||||
if(!isInDOM(globalVideo)) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
divRound.classList.add('is-paused');
|
||||
};
|
||||
|
||||
const onEnded = () => {
|
||||
video.classList.remove('hide');
|
||||
divRound.classList.add('is-paused');
|
||||
|
||||
video.currentTime = 0;
|
||||
spanTime.innerText = ('' + globalVideo.duration).toHHMMSS(false);
|
||||
|
||||
if(globalVideo.currentTime) {
|
||||
globalVideo.currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
globalVideo.addEventListener('play', onPlay);
|
||||
globalVideo.addEventListener('timeupdate', throttledTimeUpdate);
|
||||
globalVideo.addEventListener('pause', onPaused);
|
||||
globalVideo.addEventListener('ended', onEnded);
|
||||
|
||||
attachClickEvent(canvas, (e) => {
|
||||
cancelEvent(e);
|
||||
|
||||
// ! костыль
|
||||
if(preloader && !preloader.detached) {
|
||||
preloader.onClick();
|
||||
}
|
||||
|
||||
globalVideo.play();
|
||||
// ! can't use it here. on Safari iOS video won't start.
|
||||
/* if(globalVideo.readyState < 2) {
|
||||
return;
|
||||
} */
|
||||
|
||||
if(globalVideo.paused) {
|
||||
if(appMediaPlaybackController.setSearchContext(searchContext)) {
|
||||
const [prev, next] = findMediaTargets(divRound, searchContext.useSearch);
|
||||
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid}, prev, next);
|
||||
}
|
||||
|
||||
globalVideo.play();
|
||||
} else {
|
||||
globalVideo.pause();
|
||||
}
|
||||
});
|
||||
|
||||
if(globalVideo.paused) {
|
||||
if(globalVideo.duration && globalVideo.currentTime !== globalVideo.duration && globalVideo.currentTime > 0) {
|
||||
onFrame();
|
||||
onTimeUpdate();
|
||||
video.classList.add('hide');
|
||||
} else {
|
||||
onPaused();
|
||||
}
|
||||
} else {
|
||||
globalVideo.pause();
|
||||
onPlay();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if(globalVideo.paused) {
|
||||
if(globalVideo.duration && globalVideo.currentTime !== globalVideo.duration && globalVideo.currentTime > 0) {
|
||||
onFrame();
|
||||
onTimeUpdate();
|
||||
video.classList.add('hide');
|
||||
} else {
|
||||
onPaused();
|
||||
}
|
||||
if(message.pFlags.is_outgoing) {
|
||||
(divRound as any).onLoad = onLoad;
|
||||
divRound.dataset.isOutgoing = '1';
|
||||
} else {
|
||||
onPlay();
|
||||
onLoad();
|
||||
}
|
||||
} else {
|
||||
video.autoplay = true; // для safari
|
||||
@ -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
|
||||
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
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
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];
|
||||
|
@ -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
Normal file
5
src/environment/parallaxSupport.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { IS_FIREFOX } from "./userAgent";
|
||||
|
||||
const PARALLAX_SUPPORTED = !IS_FIREFOX && false;
|
||||
|
||||
export default PARALLAX_SUPPORTED;
|
33
src/helpers/avatarListLoader.ts
Normal file
33
src/helpers/avatarListLoader.ts
Normal file
@ -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,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>() {
|
||||
|
||||
}; */
|
||||
|
||||
deferred.finally(() => {
|
||||
deferred.catch(noop).finally(() => {
|
||||
deferred.notify = deferred.notifyAll = deferred.lastNotify = null;
|
||||
deferred.listeners.length = 0;
|
||||
|
||||
|
@ -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,
|
||||
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,
|
||||
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,
|
||||
// * 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
Normal file
25
src/helpers/filterChatPhotosMessages.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -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 {}> {
|
||||
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 {}> {
|
||||
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 {}> {
|
||||
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 {}> {
|
||||
}
|
||||
|
||||
this.current = item;
|
||||
this.onJump && this.onJump(item, length > 0);
|
||||
dispatchJump && this.onJump && this.onJump(item, length > 0);
|
||||
return this.current;
|
||||
}
|
||||
|
||||
// нет смысла делать проверку для reverse и loadMediaPromise
|
||||
|
@ -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
Normal file
161
src/helpers/searchListLoader.ts
Normal file
@ -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
1
src/layer.d.ts
vendored
@ -830,6 +830,7 @@ export namespace Message {
|
||||
pinned?: true,
|
||||
unread?: true,
|
||||
is_outgoing?: true,
|
||||
is_scheduled?: true,
|
||||
}>,
|
||||
id: number,
|
||||
from_id?: Peer,
|
||||
|
@ -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";
|
||||
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 {
|
||||
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 {
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
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 {
|
||||
historyStorage.history.delete(tempId);
|
||||
|
||||
delete this.pendingByRandomId[randomId];
|
||||
delete storage[tempId];
|
||||
storage.delete(tempId);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -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 {
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
this.wrapMessageEntities(message);
|
||||
}
|
||||
|
||||
storage[mid] = message;
|
||||
storage.set(mid, message);
|
||||
});
|
||||
|
||||
/* if(groups) {
|
||||
@ -3253,6 +3259,140 @@ export class AppMessagesManager {
|
||||
});
|
||||
}
|
||||
|
||||
public filterMessagesByInputFilter(inputFilter: MyInputMessagesFilter, history: number[], storage: MessagesStorage, limit: number) {
|
||||
const foundMsgs: MyMessage[] = [];
|
||||
if(!history.length) {
|
||||
return foundMsgs;
|
||||
}
|
||||
|
||||
let filtering = true;
|
||||
const neededContents: Partial<{
|
||||
[messageMediaType in MessageMedia['_']]: boolean
|
||||
}> & Partial<{
|
||||
avatar: boolean,
|
||||
url: boolean
|
||||
}> = {},
|
||||
neededDocTypes: MyDocument['type'][] = [],
|
||||
excludeDocTypes: MyDocument['type'][] = []/* ,
|
||||
neededFlags: string[] = [] */;
|
||||
|
||||
switch(inputFilter) {
|
||||
case 'inputMessagesFilterPhotos':
|
||||
neededContents['messageMediaPhoto'] = true;
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterPhotoVideo':
|
||||
neededContents['messageMediaPhoto'] = true;
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('video');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterVideo':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('video');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterDocument':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
excludeDocTypes.push('video');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterVoice':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('voice');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterRoundVoice':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('round', 'voice');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterRoundVideo':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('round');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterMusic':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('audio');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterUrl':
|
||||
neededContents['url'] = true;
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterChatPhotos':
|
||||
neededContents['avatar'] = true;
|
||||
break;
|
||||
|
||||
/* case 'inputMessagesFilterPinned':
|
||||
neededFlags.push('pinned');
|
||||
break; */
|
||||
|
||||
/* case 'inputMessagesFilterMyMentions':
|
||||
neededContents['mentioned'] = true;
|
||||
break; */
|
||||
|
||||
default:
|
||||
filtering = false;
|
||||
break;
|
||||
/* return Promise.resolve({
|
||||
count: 0,
|
||||
next_rate: 0,
|
||||
history: [] as number[]
|
||||
}); */
|
||||
}
|
||||
|
||||
if(!filtering) {
|
||||
return foundMsgs;
|
||||
}
|
||||
|
||||
for(let i = 0, length = history.length; i < length; ++i) {
|
||||
const message: Message.message | Message.messageService = storage.get(history[i]);
|
||||
if(!message) continue;
|
||||
|
||||
//|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ === 'messageEntityMention'));
|
||||
|
||||
let found = false;
|
||||
if(message._ === 'message') {
|
||||
if(message.media && neededContents[message.media._]/* && !message.fwd_from */) {
|
||||
const doc = (message.media as MessageMedia.messageMediaDocument).document as MyDocument;
|
||||
if((neededDocTypes.length && !neededDocTypes.includes(doc.type))
|
||||
|| excludeDocTypes.includes(doc.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
found = true;
|
||||
} else if(neededContents['url'] && message.message) {
|
||||
const goodEntities = ['messageEntityTextUrl', 'messageEntityUrl'];
|
||||
if((message.totalEntities as MessageEntity[]).find(e => goodEntities.includes(e._)) || RichTextProcessor.matchUrl(message.message)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
} else if(neededContents['avatar'] &&
|
||||
message.action &&
|
||||
([
|
||||
'messageActionChannelEditPhoto' as const,
|
||||
'messageActionChatEditPhoto' as const,
|
||||
'messageActionChannelEditVideo' as const,
|
||||
'messageActionChatEditVideo' as const
|
||||
] as MessageAction['_'][]).includes(message.action._)) {
|
||||
found = true;
|
||||
}/* else if(neededFlags.find(flag => message.pFlags[flag])) {
|
||||
found = true;
|
||||
} */
|
||||
|
||||
if(found) {
|
||||
foundMsgs.push(message);
|
||||
if(foundMsgs.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundMsgs;
|
||||
}
|
||||
|
||||
public getSearch({peerId, query, inputFilter, maxId, limit, nextRate, backLimit, threadId, folderId, minDate, maxDate}: {
|
||||
peerId?: number,
|
||||
maxId?: number,
|
||||
@ -3283,7 +3423,7 @@ export class AppMessagesManager {
|
||||
minDate = minDate ? minDate / 1000 | 0 : 0;
|
||||
maxDate = maxDate ? maxDate / 1000 | 0 : 0;
|
||||
|
||||
const foundMsgs: Message.message[] = [];
|
||||
let foundMsgs: MyMessage[] = [];
|
||||
|
||||
//this.log('search', maxId);
|
||||
|
||||
@ -3304,124 +3444,7 @@ export class AppMessagesManager {
|
||||
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[] = []/* ,
|
||||
neededFlags: string[] = [] */;
|
||||
|
||||
switch(inputFilter._) {
|
||||
case 'inputMessagesFilterPhotos':
|
||||
neededContents['messageMediaPhoto'] = true;
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterPhotoVideo':
|
||||
neededContents['messageMediaPhoto'] = true;
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('video');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterVideo':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('video');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterDocument':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
excludeDocTypes.push('video');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterVoice':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('voice');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterRoundVoice':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('round', 'voice');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterRoundVideo':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('round');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterMusic':
|
||||
neededContents['messageMediaDocument'] = true;
|
||||
neededDocTypes.push('audio');
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterUrl':
|
||||
neededContents['url'] = true;
|
||||
break;
|
||||
|
||||
case 'inputMessagesFilterChatPhotos':
|
||||
neededContents['avatar'] = true;
|
||||
break;
|
||||
|
||||
/* case 'inputMessagesFilterPinned':
|
||||
neededFlags.push('pinned');
|
||||
break; */
|
||||
|
||||
/* case 'inputMessagesFilterMyMentions':
|
||||
neededContents['mentioned'] = true;
|
||||
break; */
|
||||
|
||||
default:
|
||||
filtering = false;
|
||||
break;
|
||||
/* return Promise.resolve({
|
||||
count: 0,
|
||||
next_rate: 0,
|
||||
history: [] as number[]
|
||||
}); */
|
||||
}
|
||||
|
||||
if(filtering) {
|
||||
const storage = this.getMessagesStorage(peerId);
|
||||
for(let i = 0, length = history.length; i < length; i++) {
|
||||
const message = storage[history.slice[i]];
|
||||
|
||||
if(!message) continue;
|
||||
|
||||
//|| (neededContents['mentioned'] && message.totalEntities.find((e: any) => e._ === 'messageEntityMention'));
|
||||
|
||||
let found = false;
|
||||
if(message.media && neededContents[message.media._] && !message.fwd_from) {
|
||||
if(message.media._ === 'messageMediaDocument') {
|
||||
if((neededDocTypes.length && !neededDocTypes.includes(message.media.document.type))
|
||||
|| excludeDocTypes.includes(message.media.document.type)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
found = true;
|
||||
} else if(neededContents['url'] && message.message) {
|
||||
const goodEntities = ['messageEntityTextUrl', 'messageEntityUrl'];
|
||||
if((message.totalEntities as MessageEntity[]).find(e => goodEntities.includes(e._)) || RichTextProcessor.matchUrl(message.message)) {
|
||||
found = true;
|
||||
}
|
||||
} else if(neededContents['avatar'] && message.action && ['messageActionChannelEditPhoto', 'messageActionChatEditPhoto', 'messageActionChannelEditVideo', 'messageActionChatEditVideo'].includes(message.action._)) {
|
||||
found = true;
|
||||
}/* else if(neededFlags.find(flag => message.pFlags[flag])) {
|
||||
found = true;
|
||||
} */
|
||||
|
||||
if(found) {
|
||||
foundMsgs.push(message);
|
||||
if(foundMsgs.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foundMsgs = this.filterMessagesByInputFilter(inputFilter._, storage.history.slice, this.getMessagesStorage(peerId), limit);
|
||||
}
|
||||
|
||||
if(foundMsgs.length) {
|
||||
@ -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 {
|
||||
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 {
|
||||
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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
appNotificationsManager.cancel('msg' + messageId);
|
||||
appNotificationsManager.cancel('msg' + mid);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4557,14 +4581,14 @@ export class AppMessagesManager {
|
||||
const messages = update.messages.map(id => appMessagesIdsManager.generateMessageId(id));
|
||||
|
||||
const 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 {
|
||||
} 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 {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
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)) {
|
||||
|
@ -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<{
|
||||
|
||||
public storage = stateStorage;
|
||||
|
||||
public newVersion: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadSavedState();
|
||||
@ -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
|
||||
|
@ -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;
|
||||
|
||||
private stopAndScrubTimeout = 0;
|
||||
private progressRAF = 0;
|
||||
protected filledLoad: HTMLDivElement;
|
||||
|
||||
constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) {
|
||||
super(1000 / 60 / 1000, 0, 0, 1);
|
||||
protected progressRAF = 0;
|
||||
|
||||
if(streamable) {
|
||||
protected media: HTMLMediaElement;
|
||||
protected streamable: boolean;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.media.paused && this.media.play();
|
||||
onMouseUp: (e) => {
|
||||
cancelEvent(e.event);
|
||||
wasPlaying && 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 {
|
||||
this.progressRAF = window.requestAnimationFrame(r);
|
||||
};
|
||||
|
||||
onProgress = (e: Event) => {
|
||||
protected onTimeUpdate = () => {
|
||||
if(this.media.paused) {
|
||||
this.setProgress();
|
||||
|
||||
if(this.streamable) {
|
||||
this.setLoadProgress();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected onProgress = (e: Event) => {
|
||||
this.setLoadProgress();
|
||||
};
|
||||
|
||||
protected scrub(e: MouseEvent) {
|
||||
protected scrub(e: GrabEvent) {
|
||||
const scrubTime = super.scrub(e);
|
||||
this.media.currentTime = scrubTime;
|
||||
return scrubTime;
|
||||
@ -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 {
|
||||
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<{
|
||||
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<{
|
||||
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;
|
||||
}
|
||||
|
@ -364,7 +364,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
|
||||
|
||||
/// #if !MTPROTO_SW
|
||||
private registerWorker() {
|
||||
//return;
|
||||
// return;
|
||||
|
||||
const worker = new MTProtoWorker();
|
||||
//const worker = window;
|
||||
|
@ -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 = {
|
||||
'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,
|
||||
|
@ -120,7 +120,9 @@ export default class DialogsStorage {
|
||||
for(let i = 0, length = dialogs.length; i < length; ++i) {
|
||||
const dialog = dialogs[i];
|
||||
if(dialog) {
|
||||
dialog.top_message = this.appMessagesIdsManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog
|
||||
// if(dialog.peerId !== SERVICE_PEER_ID) {
|
||||
dialog.top_message = this.appMessagesIdsManager.getServerMessageId(dialog.top_message); // * fix outgoing message to avoid copying dialog
|
||||
// }
|
||||
|
||||
if(dialog.topMessage) {
|
||||
this.appMessagesManager.saveMessages([dialog.topMessage]);
|
||||
@ -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 {
|
||||
|
||||
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 {
|
||||
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) {
|
||||
mid = this.appMessagesIdsManager.generateMessageId(dialog.top_message);//dialog.top_message;
|
||||
if(wasDialogBefore?.top_message && !this.appMessagesManager.getMessageByPeer(peerId, wasDialogBefore.top_message).deleted) {
|
||||
mid = wasDialogBefore.top_message;
|
||||
} else {
|
||||
mid = this.appMessagesIdsManager.generateMessageId(dialog.top_message);//dialog.top_message;
|
||||
}
|
||||
|
||||
message = this.appMessagesManager.getMessageByPeer(peerId, mid);
|
||||
} else {
|
||||
mid = this.appMessagesManager.generateTempMessageId(peerId);
|
||||
@ -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
Normal file
20
src/scripts/generate_changelog.js
Normal file
@ -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'));
|
||||
});
|
@ -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"},
|
||||
|
@ -450,6 +450,12 @@
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
&-description:not(:empty) {
|
||||
&:before {
|
||||
content: " • ";
|
||||
}
|
||||
}
|
||||
|
||||
&-time,
|
||||
&-subtitle {
|
||||
font-size: .875rem;
|
||||
|
@ -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 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.rp {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
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;
|
||||
|
@ -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 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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 @@
|
||||
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 @@
|
||||
}
|
||||
|
||||
.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) {
|
||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, .15);
|
||||
@include respond-to(handhelds) {
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
|
||||
/* &-utils {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
} */
|
||||
}
|
||||
|
||||
.pinned-container-content {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
/* &:before {
|
||||
content: " ";
|
||||
height: 1px;
|
||||
background-color: var(--border-color);
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
} */
|
||||
|
||||
&:before {
|
||||
width: 100%;
|
||||
content: " ";
|
||||
height: 52px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
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 @@
|
||||
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 @@
|
||||
}
|
||||
|
||||
.pinned-message {
|
||||
display: none;
|
||||
// display: none;
|
||||
display: flex;
|
||||
width: auto;
|
||||
|
||||
&-content {
|
||||
@ -373,6 +414,7 @@
|
||||
}
|
||||
|
||||
&:not(.is-floating) {
|
||||
margin-right: 1rem;
|
||||
//width: 15.5rem;
|
||||
|
||||
/* .pinned-message-content {
|
||||
@ -382,6 +424,7 @@
|
||||
.pinned-message-close {
|
||||
display: flex;
|
||||
margin-right: .75rem;
|
||||
order: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-pinlist {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.chat.type-discussion & {
|
||||
.pinned-container-close {
|
||||
display: none !important;
|
||||
@ -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 @@
|
||||
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 @@
|
||||
|
||||
&-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;
|
||||
overflow: hidden;
|
||||
max-width: 240px;
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,31 +4,67 @@
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
.topbar {
|
||||
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;
|
||||
@mixin sidebar-transform() {
|
||||
@include respond-to(medium-screens) {
|
||||
transition: transform var(--transition-standard-out);
|
||||
|
||||
&.is-pinned-floating {
|
||||
&.is-pinned-audio-shown, &.is-pinned-message-shown:not(.hide-pinned) {
|
||||
margin-bottom: 52px;
|
||||
/* & + .bubbles {
|
||||
margin-top: 52px;
|
||||
} */
|
||||
|
||||
& ~ .drops-container {
|
||||
--pinned-floating-height: 52px;
|
||||
}
|
||||
body.is-right-column-shown & {
|
||||
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
|
||||
transition: transform var(--transition-standard-in);
|
||||
}
|
||||
|
||||
&.is-pinned-message-shown:not(.hide-pinned):not(.is-pinned-audio-shown) {
|
||||
.pinned-message {
|
||||
display: flex;
|
||||
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%;
|
||||
z-index: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
&.is-pinned-audio-floating {
|
||||
--pinned-floating-height: var(--pinned-audio-height);
|
||||
}
|
||||
|
||||
&.is-pinned-message-floating {
|
||||
--pinned-floating-height: var(--pinned-message-height);
|
||||
}
|
||||
|
||||
&.is-pinned-audio-floating.is-pinned-message-floating {
|
||||
--pinned-floating-height: calc(var(--pinned-audio-height) + var(--pinned-message-height));
|
||||
|
||||
.pinned-message {
|
||||
top: calc(var(--topbar-height) + var(--pinned-audio-height));
|
||||
|
||||
&:before {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* &:before {
|
||||
box-shadow: none;
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +92,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pinned-container-wrapper-utils {
|
||||
@include sidebar-transform();
|
||||
}
|
||||
|
||||
.sidebar-close-button {
|
||||
position: absolute;
|
||||
}
|
||||
@ -92,7 +132,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.peer-title, .info {
|
||||
.peer-title,
|
||||
.info {
|
||||
@include text-overflow();
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
@ -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) {
|
||||
@ -224,4 +254,4 @@
|
||||
margin-top: 1px;
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,8 +380,17 @@ video::-webkit-media-controls-enclosure {
|
||||
}
|
||||
|
||||
@include animation-level(2) {
|
||||
&.with-transition .progress-line__filled {
|
||||
transition: width .2s;
|
||||
&.with-transition {
|
||||
.progress-line__filled {
|
||||
transition: width .2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.use-transform {
|
||||
.progress-line__filled {
|
||||
width: 100%;
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -664,8 +664,11 @@
|
||||
|
||||
#search-container {
|
||||
.search-super-content-music {
|
||||
.audio:not(.audio-show-progress) .audio-time {
|
||||
display: none;
|
||||
.audio:not(.audio-show-progress) {
|
||||
.audio-time,
|
||||
.audio-description:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 @@
|
||||
//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 @@
|
||||
} */
|
||||
}
|
||||
|
||||
.btn-menu &, &.is-square {
|
||||
.btn-menu &,
|
||||
&.is-square {
|
||||
--ripple-duration: .2s;
|
||||
}
|
||||
|
||||
&__circle.hiding, &__square.hiding {
|
||||
&__circle.hiding,
|
||||
&__square.hiding {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
@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 {
|
||||
}
|
||||
}
|
||||
|
||||
.missing-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
&-path {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
max-height: 23.5rem;
|
||||
/* height: auto; */
|
||||
|
Loading…
x
Reference in New Issue
Block a user