Fix avatar duplicate
Fix media viewer list again Supported media browser controls
This commit is contained in:
parent
2b25774775
commit
344b70dc3e
@ -8,12 +8,19 @@ import rootScope from "../lib/rootScope";
|
||||
import appMessagesManager from "../lib/appManagers/appMessagesManager";
|
||||
import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
|
||||
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
|
||||
import { isSafari } from "../helpers/userAgent";
|
||||
import { isApple, isSafari } from "../helpers/userAgent";
|
||||
import { MOUNT_CLASS_TO } from "../config/debug";
|
||||
import appDownloadManager from "../lib/appManagers/appDownloadManager";
|
||||
import simulateEvent from "../helpers/dom/dispatchEvent";
|
||||
import type { SearchSuperContext } from "./appSearchSuper.";
|
||||
import { copy, deepEqual } from "../helpers/object";
|
||||
import { DocumentAttribute, Message, MessageMedia, PhotoSize } from "../layer";
|
||||
import appPhotosManager from "../lib/appManagers/appPhotosManager";
|
||||
import { isTouchSupported } from "../helpers/touchSupport";
|
||||
import appAvatarsManager from "../lib/appManagers/appAvatarsManager";
|
||||
import appPeersManager from "../lib/appManagers/appPeersManager";
|
||||
import I18n from "../lib/langPack";
|
||||
import { SearchListLoader } from "./appMediaViewer";
|
||||
|
||||
// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
|
||||
|
||||
@ -21,7 +28,7 @@ import { copy, deepEqual } from "../helpers/object";
|
||||
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка
|
||||
// TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце
|
||||
|
||||
type MediaItem = {mid: number, peerId: number};
|
||||
export type MediaItem = {mid: number, peerId: number};
|
||||
|
||||
type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement;
|
||||
|
||||
@ -33,6 +40,8 @@ const SHOULD_USE_SAFARI_FIX = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
const SEEK_OFFSET = 10;
|
||||
|
||||
class AppMediaPlaybackController {
|
||||
private container: HTMLElement;
|
||||
private media: {
|
||||
@ -40,7 +49,7 @@ class AppMediaPlaybackController {
|
||||
[mid: string]: HTMLMediaElement
|
||||
}
|
||||
} = {};
|
||||
public playingMedia: HTMLMediaElement;
|
||||
private playingMedia: HTMLMediaElement;
|
||||
|
||||
private waitingMediaForLoad: {
|
||||
[peerId: string]: {
|
||||
@ -49,20 +58,41 @@ class AppMediaPlaybackController {
|
||||
} = {};
|
||||
|
||||
public willBePlayedMedia: HTMLMediaElement;
|
||||
public searchContext: SearchSuperContext;
|
||||
private searchContext: SearchSuperContext;
|
||||
|
||||
private currentPeerId: number;
|
||||
private prevMid: number;
|
||||
private nextMid: number;
|
||||
|
||||
private prev: MediaItem[] = [];
|
||||
private next: MediaItem[] = [];
|
||||
private listLoader: SearchListLoader<MediaItem>;
|
||||
|
||||
constructor() {
|
||||
this.container = document.createElement('div');
|
||||
//this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;';
|
||||
this.container.style.cssText = 'display: none;';
|
||||
document.body.append(this.container);
|
||||
|
||||
if(navigator.mediaSession) {
|
||||
navigator.mediaSession.setActionHandler('play', this.play);
|
||||
navigator.mediaSession.setActionHandler('pause', this.pause);
|
||||
navigator.mediaSession.setActionHandler('stop', this.stop);
|
||||
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
|
||||
const media = this.playingMedia
|
||||
if(media) {
|
||||
media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET));
|
||||
}
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('seekforward', (details) => {
|
||||
const media = this.playingMedia
|
||||
if(media) {
|
||||
media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET));
|
||||
}
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
||||
const media = this.playingMedia
|
||||
if(media) {
|
||||
media.currentTime = details.seekTime;
|
||||
}
|
||||
});
|
||||
navigator.mediaSession.setActionHandler('previoustrack', this.previous);
|
||||
navigator.mediaSession.setActionHandler('nexttrack', this.next);
|
||||
}
|
||||
}
|
||||
|
||||
public addMedia(peerId: number, doc: MyDocument, mid: number, autoload = true): HTMLMediaElement {
|
||||
@ -187,33 +217,126 @@ class AppMediaPlaybackController {
|
||||
media.safariBuffering = value;
|
||||
}
|
||||
|
||||
private async setNewMediadata(message: Message.message) {
|
||||
const playingMedia = this.playingMedia;
|
||||
const doc = (message.media as MessageMedia.messageMediaDocument).document as MyDocument;
|
||||
|
||||
const artwork: MediaImage[] = [];
|
||||
|
||||
const isVoice = doc.type === 'voice' || doc.type === 'round';
|
||||
let title = '', artist = '';
|
||||
|
||||
if(doc.thumbs?.length) {
|
||||
const size = doc.thumbs[doc.thumbs.length - 1];
|
||||
if(!(size as PhotoSize.photoStrippedSize).bytes) {
|
||||
const cacheContext = appDownloadManager.getCacheContext(doc, size.type);
|
||||
|
||||
if(cacheContext.url) {
|
||||
artwork.push({
|
||||
src: cacheContext.url,
|
||||
sizes: `${(size as PhotoSize.photoSize).w}x${(size as PhotoSize.photoSize).h}`,
|
||||
type: 'image/jpeg'
|
||||
});
|
||||
} else {
|
||||
const download = appPhotosManager.preloadPhoto(doc, size);
|
||||
download.then(() => {
|
||||
if(this.playingMedia !== playingMedia || !cacheContext.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setNewMediadata(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if(isVoice) {
|
||||
const peerId = message.fromId || message.peerId;
|
||||
const peerPhoto = appPeersManager.getPeerPhoto(peerId);
|
||||
const result = appAvatarsManager.loadAvatar(peerId, peerPhoto, 'photo_small');
|
||||
if(result.cached) {
|
||||
const url = await result.loadPromise;
|
||||
artwork.push({
|
||||
src: url,
|
||||
sizes: '160x160',
|
||||
type: 'image/jpeg'
|
||||
});
|
||||
} else {
|
||||
result.loadPromise.then((url) => {
|
||||
if(this.playingMedia !== playingMedia || !url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setNewMediadata(message);
|
||||
});
|
||||
}
|
||||
|
||||
title = appPeersManager.getPeerTitle(peerId, true, false);
|
||||
artist = I18n.format(doc.type === 'voice' ? 'AttachAudio' : 'AttachRound', true);
|
||||
}
|
||||
|
||||
if(!isVoice) {
|
||||
const attribute = doc.attributes.find(attribute => attribute._ === 'documentAttributeAudio') as DocumentAttribute.documentAttributeAudio;
|
||||
title = attribute && attribute.title || doc.file_name;
|
||||
artist = attribute && attribute.performer;
|
||||
}
|
||||
|
||||
if(!artwork.length) {
|
||||
if(isApple) {
|
||||
if(isTouchSupported) {
|
||||
artwork.push({
|
||||
src: `assets/img/apple-touch-icon-precomposed.png`,
|
||||
sizes: '180x180',
|
||||
type: 'image/png'
|
||||
});
|
||||
} else {
|
||||
artwork.push({
|
||||
src: `assets/img/apple-touch-icon.png`,
|
||||
sizes: '180x180',
|
||||
type: 'image/png'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
[72, 96, 144, 192, 256, 384, 512].forEach(size => {
|
||||
const sizes = `${size}x${size}`;
|
||||
artwork.push({
|
||||
src: `assets/img/android-chrome-${sizes}.png`,
|
||||
sizes,
|
||||
type: 'image/png'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = new MediaMetadata({
|
||||
title,
|
||||
artist,
|
||||
artwork
|
||||
});
|
||||
|
||||
navigator.mediaSession.metadata = metadata;
|
||||
}
|
||||
|
||||
onPlay = (e?: Event) => {
|
||||
const media = e.target as HTMLMediaElement;
|
||||
const peerId = +media.dataset.peerId;
|
||||
const mid = +media.dataset.mid;
|
||||
this.currentPeerId = peerId;
|
||||
|
||||
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
|
||||
|
||||
const message = appMessagesManager.getMessageByPeer(peerId, mid);
|
||||
|
||||
const previousMedia = this.playingMedia;
|
||||
if(previousMedia !== media) {
|
||||
if(previousMedia) {
|
||||
if(!previousMedia.paused) {
|
||||
previousMedia.pause();
|
||||
}
|
||||
|
||||
// reset media
|
||||
previousMedia.currentTime = 0;
|
||||
simulateEvent(previousMedia, 'ended');
|
||||
}
|
||||
this.stop();
|
||||
|
||||
this.playingMedia = media;
|
||||
this.loadSiblingsMedia(peerId, mid);
|
||||
|
||||
if('mediaSession' in navigator) {
|
||||
this.setNewMediadata(message);
|
||||
}
|
||||
}
|
||||
|
||||
// audio_pause не успеет сработать без таймаута
|
||||
setTimeout(() => {
|
||||
const message = appMessagesManager.getMessageByPeer(peerId, mid);
|
||||
rootScope.dispatchEvent('audio_play', {peerId, doc: message.media.document, mid});
|
||||
}, 0);
|
||||
};
|
||||
@ -238,65 +361,80 @@ class AppMediaPlaybackController {
|
||||
|
||||
//console.log('on media end');
|
||||
|
||||
if(this.nextMid) {
|
||||
const media = this.media[this.currentPeerId][this.nextMid];
|
||||
|
||||
/* if(isSafari) {
|
||||
media.autoplay = true;
|
||||
} */
|
||||
|
||||
this.resolveWaitingForLoadMedia(this.currentPeerId, this.nextMid);
|
||||
|
||||
setTimeout(() => {
|
||||
media.play()//.catch(() => {});
|
||||
}, 0);
|
||||
}
|
||||
this.next();
|
||||
};
|
||||
|
||||
private loadSiblingsMedia(offsetPeerId: number, offsetMid: number) {
|
||||
const {playingMedia, searchContext} = this;
|
||||
if(!searchContext) {
|
||||
public toggle(play?: boolean) {
|
||||
if(!this.playingMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
return appMessagesManager.getSearch({
|
||||
...searchContext,
|
||||
maxId: offsetMid,
|
||||
limit: 3,
|
||||
backLimit: 2,
|
||||
}).then(value => {
|
||||
if(this.playingMedia !== playingMedia || this.searchContext !== searchContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = Math.max(0, value.history.findIndex(message => message.peerId === offsetPeerId && message.mid === offsetMid));
|
||||
const prev = value.history.slice(Math.max(0, idx));
|
||||
const next = value.history.slice(0, idx);
|
||||
if(play === undefined) {
|
||||
play = this.playingMedia.paused;
|
||||
}
|
||||
|
||||
[this.prevMid, this.nextMid].filter(Boolean).forEach(mid => {
|
||||
const peerId = searchContext.peerId;
|
||||
const message = appMessagesManager.getMessageByPeer(peerId, mid);
|
||||
this.addMedia(peerId, message.media.document, mid, false);
|
||||
});
|
||||
|
||||
//console.log('loadSiblingsAudio', audio, type, mid, value, this.prevMid, this.nextMid);
|
||||
});
|
||||
}
|
||||
|
||||
public toggle() {
|
||||
if(!this.playingMedia) return;
|
||||
if(this.playingMedia.paused !== play) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.playingMedia.paused) {
|
||||
if(play) {
|
||||
this.playingMedia.play();
|
||||
} else {
|
||||
this.playingMedia.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public pause() {
|
||||
if(!this.playingMedia || this.playingMedia.paused) return;
|
||||
this.playingMedia.pause();
|
||||
}
|
||||
public play = () => {
|
||||
return this.toggle(true);
|
||||
};
|
||||
|
||||
public pause = () => {
|
||||
return this.toggle(false);
|
||||
};
|
||||
|
||||
public stop = () => {
|
||||
const media = this.playingMedia;
|
||||
if(media) {
|
||||
if(!media.paused) {
|
||||
media.pause();
|
||||
}
|
||||
|
||||
media.currentTime = 0;
|
||||
simulateEvent(media, 'ended');
|
||||
|
||||
// this.playingMedia = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
public playItem = (item: MediaItem) => {
|
||||
const {peerId, mid} = item;
|
||||
const media = this.media[peerId][mid];
|
||||
|
||||
/* if(isSafari) {
|
||||
media.autoplay = true;
|
||||
} */
|
||||
|
||||
this.resolveWaitingForLoadMedia(peerId, mid);
|
||||
|
||||
setTimeout(() => {
|
||||
media.play()//.catch(() => {});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
public next = () => {
|
||||
this.listLoader.go(1);
|
||||
};
|
||||
|
||||
public previous = () => {
|
||||
const media = this.playingMedia;
|
||||
if(media && media.currentTime > 5) {
|
||||
media.currentTime = 0;
|
||||
this.toggle(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.listLoader.go(-1);
|
||||
};
|
||||
|
||||
public willBePlayed(media: HTMLMediaElement) {
|
||||
this.willBePlayedMedia = media;
|
||||
@ -304,12 +442,43 @@ class AppMediaPlaybackController {
|
||||
|
||||
public setSearchContext(context: SearchSuperContext) {
|
||||
if(deepEqual(this.searchContext, context)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.searchContext = copy(context); // {_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'}
|
||||
this.prev.length = 0;
|
||||
this.next.length = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
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};
|
||||
},
|
||||
onJump: (item, older) => {
|
||||
this.playItem(item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.listLoader.reset();
|
||||
}
|
||||
|
||||
const reverse = this.searchContext.folderId !== undefined ? false : true;
|
||||
if(prev) {
|
||||
this.listLoader.setTargets(prev, next, reverse);
|
||||
} else {
|
||||
this.listLoader.reverse = reverse;
|
||||
}
|
||||
|
||||
this.listLoader.setSearchContext(this.searchContext);
|
||||
this.listLoader.current = current;
|
||||
|
||||
this.listLoader.load(true);
|
||||
this.listLoader.load(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,11 +30,9 @@ import appSidebarRight from "./sidebarRight";
|
||||
import SwipeHandler from "./swipeHandler";
|
||||
import { ONE_DAY } from "../helpers/date";
|
||||
import { SearchSuperContext } from "./appSearchSuper.";
|
||||
import DEBUG from "../config/debug";
|
||||
import appNavigationController from "./appNavigationController";
|
||||
import { Message } from "../layer";
|
||||
import { forEachReverse } from "../helpers/array";
|
||||
import AppSharedMediaTab from "./sidebarRight/tabs/sharedMedia";
|
||||
import AppSharedMediaTab, { filterChatPhotosMessages } from "./sidebarRight/tabs/sharedMedia";
|
||||
import findUpClassName from "../helpers/dom/findUpClassName";
|
||||
import renderImageFromUrl, { renderImageFromUrlPromise } from "../helpers/dom/renderImageFromUrl";
|
||||
import getVisibleRect from "../helpers/dom/getVisibleRect";
|
||||
@ -53,7 +51,7 @@ import { attachClickEvent } from "../helpers/dom/clickEvent";
|
||||
import PopupDeleteMessages from "./popups/deleteMessages";
|
||||
import RangeSelector from "./rangeSelector";
|
||||
import windowSize from "../helpers/windowSize";
|
||||
import { safeAssign } from "../helpers/object";
|
||||
import ListLoader, { ListLoaderOptions } from "../helpers/listLoader";
|
||||
|
||||
const ZOOM_STEP = 0.5;
|
||||
const ZOOM_INITIAL_VALUE = 1;
|
||||
@ -66,129 +64,22 @@ const ZOOM_MAX_VALUE = 4;
|
||||
|
||||
const MEDIA_VIEWER_CLASSNAME = 'media-viewer';
|
||||
|
||||
type MediaQueueLoaderOptions<Item extends {}> = {
|
||||
prevTargets?: MediaQueueLoader<Item>['prevTargets'],
|
||||
nextTargets?: MediaQueueLoader<Item>['nextTargets'],
|
||||
onLoadedMore?: MediaQueueLoader<Item>['onLoadedMore'],
|
||||
generateItem?: MediaQueueLoader<Item>['generateItem'],
|
||||
getLoadPromise?: MediaQueueLoader<Item>['getLoadPromise'],
|
||||
reverse?: MediaQueueLoader<Item>['reverse']
|
||||
};
|
||||
|
||||
class MediaQueueLoader<Item extends {}> {
|
||||
public target: Item = false as any;
|
||||
public prevTargets: Item[] = [];
|
||||
public nextTargets: Item[] = [];
|
||||
|
||||
public loadMediaPromiseUp: Promise<void> = null;
|
||||
public loadMediaPromiseDown: Promise<void> = null;
|
||||
public loadedAllMediaUp = false;
|
||||
public loadedAllMediaDown = false;
|
||||
|
||||
public reverse = false; // reverse means next = higher msgid
|
||||
|
||||
protected generateItem: (item: Item) => Item = (item) => item;
|
||||
protected getLoadPromise: (older: boolean, anchor: Item, loadCount: number) => Promise<Item[]>;
|
||||
protected onLoadedMore: () => void;
|
||||
|
||||
constructor(options: MediaQueueLoaderOptions<Item> = {}) {
|
||||
safeAssign(this, options);
|
||||
}
|
||||
|
||||
public setTargets(prevTargets: Item[], nextTargets: Item[], reverse: boolean) {
|
||||
this.prevTargets = prevTargets;
|
||||
this.nextTargets = nextTargets;
|
||||
this.reverse = reverse;
|
||||
this.loadedAllMediaUp = this.loadedAllMediaDown = false;
|
||||
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.prevTargets = [];
|
||||
this.nextTargets = [];
|
||||
this.loadedAllMediaUp = this.loadedAllMediaDown = false;
|
||||
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null;
|
||||
}
|
||||
|
||||
// нет смысла делать проверку для reverse и loadMediaPromise
|
||||
public loadMoreMedia = (older = true) => {
|
||||
//if(!older && this.reverse) return;
|
||||
|
||||
if(older && this.loadedAllMediaDown) return Promise.resolve();
|
||||
else if(!older && this.loadedAllMediaUp) return Promise.resolve();
|
||||
|
||||
if(older && this.loadMediaPromiseDown) return this.loadMediaPromiseDown;
|
||||
else if(!older && this.loadMediaPromiseUp) return this.loadMediaPromiseUp;
|
||||
|
||||
const loadCount = 50;
|
||||
|
||||
let anchor: Item;
|
||||
if(older) {
|
||||
anchor = this.reverse ? this.prevTargets[0] : this.nextTargets[this.nextTargets.length - 1];
|
||||
} else {
|
||||
anchor = this.reverse ? this.nextTargets[this.nextTargets.length - 1] : this.prevTargets[0];
|
||||
}
|
||||
|
||||
const promise = this.getLoadPromise(older, anchor, loadCount).then(items => {
|
||||
if((older && this.loadMediaPromiseDown !== promise) || (!older && this.loadMediaPromiseUp !== promise)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(items.length < loadCount) {
|
||||
/* if(this.reverse) {
|
||||
if(older) this.loadedAllMediaUp = true;
|
||||
else this.loadedAllMediaDown = true;
|
||||
} else { */
|
||||
if(older) this.loadedAllMediaDown = true;
|
||||
else this.loadedAllMediaUp = true;
|
||||
//}
|
||||
}
|
||||
|
||||
const method: any = older ? items.forEach.bind(items) : forEachReverse.bind(null, items);
|
||||
method((item: Item) => {
|
||||
const t = this.generateItem(item);
|
||||
if(!t) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(older) {
|
||||
if(this.reverse) this.prevTargets.unshift(t);
|
||||
else this.nextTargets.push(t);
|
||||
} else {
|
||||
if(this.reverse) this.nextTargets.push(t);
|
||||
else this.prevTargets.unshift(t);
|
||||
}
|
||||
});
|
||||
|
||||
this.onLoadedMore && this.onLoadedMore();
|
||||
}, () => {}).then(() => {
|
||||
if(older) this.loadMediaPromiseDown = null;
|
||||
else this.loadMediaPromiseUp = null;
|
||||
});
|
||||
|
||||
if(older) this.loadMediaPromiseDown = promise;
|
||||
else this.loadMediaPromiseUp = promise;
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
class MediaSearchQueueLoader<Item extends {mid: number, peerId: number}> extends MediaQueueLoader<Item> {
|
||||
export class SearchListLoader<Item extends {mid: number, peerId: number}> extends ListLoader<Item> {
|
||||
public searchContext: SearchSuperContext;
|
||||
|
||||
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> = {}) {
|
||||
constructor(options: Omit<ListLoaderOptions<Item>, 'loadMore'> = {}) {
|
||||
super({
|
||||
...options,
|
||||
getLoadPromise: (older, anchor, loadCount) => {
|
||||
loadMore: (anchor, older, loadCount) => {
|
||||
const backLimit = older ? 0 : loadCount;
|
||||
let maxId = this.target?.mid;
|
||||
let maxId = this.current?.mid;
|
||||
|
||||
if(anchor) maxId = anchor.mid;
|
||||
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1);
|
||||
|
||||
return appMessagesManager.getSearch({
|
||||
...this.searchContext,
|
||||
peerId: anchor?.peerId,
|
||||
peerId: this.searchContext.peerId || anchor?.peerId,
|
||||
maxId,
|
||||
limit: backLimit ? 0 : loadCount,
|
||||
backLimit
|
||||
@ -197,11 +88,15 @@ class MediaSearchQueueLoader<Item extends {mid: number, peerId: number}> extends
|
||||
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 value.history as any;
|
||||
return {count: value.count, items: value.history};
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -211,37 +106,45 @@ class MediaSearchQueueLoader<Item extends {mid: number, peerId: number}> extends
|
||||
this.searchContext = context;
|
||||
|
||||
if(this.searchContext.folderId !== undefined) {
|
||||
this.loadedAllMediaUp = true;
|
||||
this.loadedAllUp = true;
|
||||
|
||||
if(this.searchContext.nextRate === undefined) {
|
||||
this.loadedAllMediaDown = true;
|
||||
this.loadedAllDown = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.searchContext.inputFilter._ === 'inputMessagesFilterChatPhotos') {
|
||||
this.loadedAllUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
public reset() {
|
||||
super.reset();
|
||||
this.searchContext = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class MediaAvatarQueueLoader<Item extends {photoId: string}> extends MediaQueueLoader<Item> {
|
||||
class AvatarListLoader<Item extends {photoId: string}> extends ListLoader<Item> {
|
||||
private peerId: number;
|
||||
|
||||
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> & {peerId: number}) {
|
||||
constructor(options: Omit<ListLoaderOptions<Item>, 'loadMore'> & {peerId: number}) {
|
||||
super({
|
||||
...options,
|
||||
getLoadPromise: (older, anchor, loadCount) => {
|
||||
if(this.peerId < 0) return Promise.resolve([]); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
|
||||
loadMore: (anchor, older, loadCount) => {
|
||||
if(this.peerId < 0 || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
|
||||
|
||||
return appPhotosManager.getUserPhotos(this.peerId, anchor?.photoId, loadCount).then(value => {
|
||||
const idx = value.photos.indexOf(this.target.photoId);
|
||||
if(idx !== -1) {
|
||||
value.photos.splice(idx, 1);
|
||||
}
|
||||
|
||||
return value.photos.map(photoId => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -259,8 +162,6 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
protected preloader: ProgressivePreloader = null;
|
||||
protected preloaderStreamable: ProgressivePreloader = null;
|
||||
|
||||
protected prevTargets: TargetType[] = [];
|
||||
protected nextTargets: TargetType[] = [];
|
||||
//protected targetContainer: HTMLElement = null;
|
||||
//protected loadMore: () => void = null;
|
||||
|
||||
@ -268,8 +169,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
|
||||
protected isFirstOpen = true;
|
||||
|
||||
protected reverse = false; // reverse means next = higher msgid
|
||||
protected needLoadMore = true;
|
||||
// protected needLoadMore = true;
|
||||
|
||||
protected pageEl = document.getElementById('page-chats') as HTMLDivElement;
|
||||
|
||||
@ -302,14 +202,14 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
protected ctrlKeyDown: boolean;
|
||||
|
||||
get target() {
|
||||
return this.queueLoader.target;
|
||||
return this.listLoader.current;
|
||||
}
|
||||
|
||||
set target(value) {
|
||||
this.queueLoader.target = value;
|
||||
this.listLoader.current = value;
|
||||
}
|
||||
|
||||
constructor(protected queueLoader: MediaQueueLoader<TargetType>,
|
||||
constructor(protected listLoader: ListLoader<TargetType>,
|
||||
topButtons: Array<keyof AppMediaViewerBase<ContentAdditionType, ButtonsAdditionType, TargetType>['buttons']>) {
|
||||
this.log = logger('AMV');
|
||||
this.preloader = new ProgressivePreloader();
|
||||
@ -432,30 +332,13 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
el.addEventListener('click', this.close.bind(this));
|
||||
});
|
||||
|
||||
this.buttons.prev.addEventListener('click', (e) => {
|
||||
cancelEvent(e);
|
||||
if(this.setMoverPromise) return;
|
||||
|
||||
const target = this.prevTargets.pop();
|
||||
if(target) {
|
||||
this.nextTargets.unshift(this.target);
|
||||
this.onPrevClick(target);
|
||||
} else {
|
||||
this.buttons.prev.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
this.buttons.next.addEventListener('click', (e) => {
|
||||
cancelEvent(e);
|
||||
if(this.setMoverPromise) return;
|
||||
|
||||
let target = this.nextTargets.shift();
|
||||
if(target) {
|
||||
this.prevTargets.push(this.target);
|
||||
this.onNextClick(target);
|
||||
} else {
|
||||
this.buttons.next.style.display = 'none';
|
||||
}
|
||||
([[-1, this.buttons.prev], [1, this.buttons.next]] as [number, HTMLElement][]).forEach(([moveLength, button]) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
cancelEvent(e);
|
||||
if(this.setMoverPromise) return;
|
||||
|
||||
this.listLoader.go(moveLength);
|
||||
});
|
||||
});
|
||||
|
||||
this.buttons.zoom.addEventListener('click', () => {
|
||||
@ -467,6 +350,11 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
|
||||
this.wholeDiv.addEventListener('click', this.onClick);
|
||||
|
||||
this.listLoader.onJump = (item, older) => {
|
||||
if(older) this.onNextClick(item);
|
||||
else this.onPrevClick(item);
|
||||
};
|
||||
|
||||
if(isTouchSupported) {
|
||||
const swipeHandler = new SwipeHandler({
|
||||
element: this.wholeDiv,
|
||||
@ -606,9 +494,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
|
||||
const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd);
|
||||
|
||||
this.target = false as any;
|
||||
this.prevTargets.length = 0;
|
||||
this.nextTargets.length = 0;
|
||||
this.listLoader.reset();
|
||||
this.setMoverPromise = null;
|
||||
this.tempId = -1;
|
||||
(window as any).appMediaViewer = undefined;
|
||||
@ -1353,7 +1239,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
}
|
||||
|
||||
protected async _openMedia(media: any, timestamp: number, fromId: number, fromRight: number, target?: HTMLElement, reverse = false,
|
||||
prevTargets: TargetType[] = [], nextTargets: TargetType[] = [], needLoadMore = true) {
|
||||
prevTargets: TargetType[] = [], nextTargets: TargetType[] = []/* , needLoadMore = true */) {
|
||||
if(this.setMoverPromise) return this.setMoverPromise;
|
||||
|
||||
/* if(DEBUG) {
|
||||
@ -1367,12 +1253,9 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
|
||||
if(this.isFirstOpen) {
|
||||
//this.targetContainer = targetContainer;
|
||||
this.prevTargets = prevTargets;
|
||||
this.nextTargets = nextTargets;
|
||||
this.reverse = reverse;
|
||||
this.needLoadMore = needLoadMore;
|
||||
// this.needLoadMore = needLoadMore;
|
||||
this.isFirstOpen = false;
|
||||
this.queueLoader.setTargets(this.prevTargets, this.nextTargets, this.reverse);
|
||||
this.listLoader.setTargets(prevTargets, nextTargets, reverse);
|
||||
(window as any).appMediaViewer = this;
|
||||
//this.loadMore = loadMore;
|
||||
|
||||
@ -1389,8 +1272,8 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
//if(prevTarget && (!prevTarget.parentElement || !this.isElementVisible(this.targetContainer, prevTarget))) prevTarget = null;
|
||||
//if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null;
|
||||
|
||||
this.buttons.prev.classList.toggle('hide', !this.prevTargets.length);
|
||||
this.buttons.next.classList.toggle('hide', !this.nextTargets.length);
|
||||
this.buttons.prev.classList.toggle('hide', !this.listLoader.previous.length);
|
||||
this.buttons.next.classList.toggle('hide', !this.listLoader.next.length);
|
||||
|
||||
const container = this.content.media;
|
||||
const useContainerAsTarget = !target || target === container;
|
||||
@ -1399,16 +1282,6 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
|
||||
this.target = {element: target} as any;
|
||||
const tempId = ++this.tempId;
|
||||
|
||||
if(this.needLoadMore) {
|
||||
if(this.nextTargets.length < 20) {
|
||||
this.queueLoader.loadMoreMedia(!this.reverse);
|
||||
}
|
||||
|
||||
if(this.prevTargets.length < 20) {
|
||||
this.queueLoader.loadMoreMedia(this.reverse);
|
||||
}
|
||||
}
|
||||
|
||||
if(container.firstElementChild) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
@ -1746,16 +1619,15 @@ type AppMediaViewerTargetType = {
|
||||
};
|
||||
export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> {
|
||||
protected btnMenuDelete: HTMLElement;
|
||||
|
||||
protected queueLoader: MediaSearchQueueLoader<AppMediaViewerTargetType>;
|
||||
protected listLoader: SearchListLoader<AppMediaViewerTargetType>;
|
||||
|
||||
get searchContext() {
|
||||
return this.queueLoader.searchContext;
|
||||
return this.listLoader.searchContext;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(new MediaSearchQueueLoader({
|
||||
generateItem: (item) => {
|
||||
super(new SearchListLoader({
|
||||
processItem: (item) => {
|
||||
const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument';
|
||||
const {mid, peerId} = item;
|
||||
const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(item);
|
||||
@ -1950,13 +1822,13 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
|
||||
}
|
||||
|
||||
public setSearchContext(context: SearchSuperContext) {
|
||||
this.queueLoader.setSearchContext(context);
|
||||
this.listLoader.setSearchContext(context);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async openMedia(message: any, target?: HTMLElement, fromRight = 0, reverse = false,
|
||||
prevTargets: AppMediaViewer['prevTargets'] = [], nextTargets: AppMediaViewer['prevTargets'] = [], needLoadMore = true) {
|
||||
prevTargets: AppMediaViewerTargetType[] = [], nextTargets: AppMediaViewerTargetType[] = []/* , needLoadMore = true */) {
|
||||
if(this.setMoverPromise) return this.setMoverPromise;
|
||||
|
||||
const mid = message.mid;
|
||||
@ -1971,7 +1843,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
|
||||
});
|
||||
|
||||
this.setCaption(message);
|
||||
const promise = super._openMedia(media, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, needLoadMore);
|
||||
const promise = super._openMedia(media, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets/* , needLoadMore */);
|
||||
this.target.mid = mid;
|
||||
this.target.peerId = message.peerId;
|
||||
|
||||
@ -1988,7 +1860,7 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe
|
||||
public peerId: number;
|
||||
|
||||
constructor(peerId: number) {
|
||||
super(new MediaAvatarQueueLoader({peerId}), [/* 'delete' */]);
|
||||
super(new AvatarListLoader({peerId}), [/* 'delete' */]);
|
||||
|
||||
this.peerId = peerId;
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { RichTextProcessor } from "../lib/richtextprocessor";
|
||||
import { formatDate, wrapPhoto } from "./wrappers";
|
||||
import ProgressivePreloader from "./preloader";
|
||||
import { MediaProgressLine } from "../lib/mediaPlayer";
|
||||
import appMediaPlaybackController from "./appMediaPlaybackController";
|
||||
import appMediaPlaybackController, { MediaItem } from "./appMediaPlaybackController";
|
||||
import { DocumentAttribute } from "../layer";
|
||||
import mediaSizes from "../helpers/mediaSizes";
|
||||
import { isSafari } from "../helpers/userAgent";
|
||||
@ -21,13 +21,14 @@ import { formatDateAccordingToToday } from "../helpers/date";
|
||||
import { cancelEvent } from "../helpers/dom/cancelEvent";
|
||||
import { attachClickEvent, detachClickEvent } from "../helpers/dom/clickEvent";
|
||||
import LazyLoadQueue from "./lazyLoadQueue";
|
||||
import { deferredPromise } from "../helpers/cancellablePromise";
|
||||
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
|
||||
import ListenerSetter, { Listener } from "../helpers/listenerSetter";
|
||||
import noop from "../helpers/noop";
|
||||
import findUpClassName from "../helpers/dom/findUpClassName";
|
||||
|
||||
rootScope.addEventListener('messages_media_read', ({mids, peerId}) => {
|
||||
mids.forEach(mid => {
|
||||
(Array.from(document.querySelectorAll('audio-element[message-id="' + mid + '"][peer-id="' + peerId + '"].is-unread')) as AudioElement[]).forEach(elem => {
|
||||
(Array.from(document.querySelectorAll('audio-element[data-mid="' + mid + '"][data-peer-id="' + peerId + '"].is-unread')) as AudioElement[]).forEach(elem => {
|
||||
elem.classList.remove('is-unread');
|
||||
});
|
||||
});
|
||||
@ -371,7 +372,7 @@ export default class AudioElement extends HTMLElement {
|
||||
private listenerSetter = new ListenerSetter();
|
||||
private onTypeDisconnect: () => void;
|
||||
public onLoad: (autoload?: boolean) => void;
|
||||
readyPromise: import("/Users/kuzmenko/Documents/projects/tweb/src/helpers/cancellablePromise").CancellablePromise<void>;
|
||||
private readyPromise: CancellablePromise<void>;
|
||||
|
||||
public render() {
|
||||
this.classList.add('audio');
|
||||
@ -429,7 +430,22 @@ export default class AudioElement extends HTMLElement {
|
||||
e && cancelEvent(e);
|
||||
|
||||
if(paused) {
|
||||
appMediaPlaybackController.setSearchContext(this.searchContext);
|
||||
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);
|
||||
}
|
||||
|
||||
appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next);
|
||||
}
|
||||
|
||||
audio.play().catch(() => {});
|
||||
} else {
|
||||
audio.pause();
|
||||
@ -444,7 +460,7 @@ export default class AudioElement extends HTMLElement {
|
||||
});
|
||||
|
||||
this.addAudioListener('timeupdate', () => {
|
||||
if(appMediaPlaybackController.playingMedia !== audio || appMediaPlaybackController.isSafariBuffering(audio)) return;
|
||||
if((!audio.currentTime && audio.paused) || appMediaPlaybackController.isSafariBuffering(audio)) return;
|
||||
audioTimeDiv.innerText = getTimeStr();
|
||||
});
|
||||
|
||||
|
@ -295,11 +295,10 @@ export default class ChatBubbles {
|
||||
}
|
||||
|
||||
if(message.media?.document) {
|
||||
const element = bubble.querySelector(`audio-element[message-id="${tempId}"], .document[data-doc-id="${tempId}"]`) as HTMLElement;
|
||||
const element = bubble.querySelector(`audio-element[data-mid="${tempId}"], .document[data-doc-id="${tempId}"]`) as HTMLElement;
|
||||
if(element) {
|
||||
if(element instanceof AudioElement) {
|
||||
element.setAttribute('doc-id', message.media.document.id);
|
||||
element.setAttribute('message-id', '' + mid);
|
||||
element.dataset.mid = '' + mid;
|
||||
element.message = message;
|
||||
element.onLoad(true);
|
||||
} else {
|
||||
|
@ -87,7 +87,7 @@ export class LazyLoadQueueBase {
|
||||
//await item.load(item.div);
|
||||
await this.loadItem(item);
|
||||
} catch(err) {
|
||||
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(err)) {
|
||||
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(err as string)) {
|
||||
this.log.error('loadMediaQueue error:', err/* , item */);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent";
|
||||
import findUpTag from "../../../helpers/dom/findUpTag";
|
||||
import replaceContent from "../../../helpers/dom/replaceContent";
|
||||
import ListenerSetter from "../../../helpers/listenerSetter";
|
||||
import ScrollableLoader from "../../../helpers/listLoader";
|
||||
import ScrollableLoader from "../../../helpers/scrollableLoader";
|
||||
import { ChannelParticipant, Chat, ChatBannedRights, Update } from "../../../layer";
|
||||
import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager";
|
||||
import appDialogsManager from "../../../lib/appManagers/appDialogsManager";
|
||||
|
@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import appImManager from "../../../lib/appManagers/appImManager";
|
||||
import appMessagesManager, { AppMessagesManager } from "../../../lib/appManagers/appMessagesManager";
|
||||
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";
|
||||
@ -31,8 +31,6 @@ import Row from "../../row";
|
||||
import { copyTextToClipboard } from "../../../helpers/clipboard";
|
||||
import { toast, toastNew } from "../../toast";
|
||||
import { fastRaf } from "../../../helpers/schedulers";
|
||||
import { safeAssign } from "../../../helpers/object";
|
||||
import { forEachReverse } from "../../../helpers/array";
|
||||
import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
|
||||
import renderImageFromUrl from "../../../helpers/dom/renderImageFromUrl";
|
||||
import SwipeHandler from "../../swipeHandler";
|
||||
@ -50,6 +48,8 @@ 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(() => {
|
||||
@ -60,115 +60,20 @@ let setText = (text: string, row: Row) => {
|
||||
|
||||
const PARALLAX_SUPPORTED = !isFirefox;
|
||||
|
||||
type ListLoaderResult<T> = {count: number, items: any[]};
|
||||
class ListLoader<T> {
|
||||
public current: T;
|
||||
public previous: T[] = [];
|
||||
public next: T[] = [];
|
||||
public count: number;
|
||||
|
||||
public tempId = 0;
|
||||
public loadMore: (anchor: T, older: boolean) => Promise<ListLoaderResult<T>>;
|
||||
public processItem: (item: any) => false | T;
|
||||
public onJump: (item: T, older: boolean) => void;
|
||||
public loadCount = 50;
|
||||
public reverse = false; // reverse means next = higher msgid
|
||||
|
||||
public loadedAllUp = false;
|
||||
public loadedAllDown = false;
|
||||
public loadPromiseUp: Promise<void>;
|
||||
public loadPromiseDown: Promise<void>;
|
||||
|
||||
constructor(options: {
|
||||
loadMore: ListLoader<T>['loadMore'],
|
||||
loadCount: ListLoader<T>['loadCount'],
|
||||
processItem?: ListLoader<T>['processItem'],
|
||||
onJump: ListLoader<T>['onJump'],
|
||||
}) {
|
||||
safeAssign(this, options);
|
||||
|
||||
|
||||
}
|
||||
|
||||
get index() {
|
||||
return this.count !== undefined ? this.previous.length : -1;
|
||||
}
|
||||
|
||||
public go(length: number) {
|
||||
let items: T[], item: T;
|
||||
if(length > 0) {
|
||||
items = this.next.splice(0, length);
|
||||
item = items.pop();
|
||||
if(!item) {
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
this.previous.push(this.current, ...items);
|
||||
} else {
|
||||
items = this.previous.splice(this.previous.length + length, -length);
|
||||
item = items.shift();
|
||||
if(!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.next.unshift(...items, this.current);
|
||||
}
|
||||
|
||||
this.current = item;
|
||||
this.onJump(item, length > 0);
|
||||
}
|
||||
|
||||
public load(older: boolean) {
|
||||
if(older && this.loadedAllDown) return Promise.resolve();
|
||||
else if(!older && this.loadedAllUp) return Promise.resolve();
|
||||
|
||||
if(older && this.loadPromiseDown) return this.loadPromiseDown;
|
||||
else if(!older && this.loadPromiseUp) return this.loadPromiseUp;
|
||||
|
||||
/* const loadCount = 50;
|
||||
const backLimit = older ? 0 : loadCount; */
|
||||
|
||||
let anchor: T;
|
||||
if(older) {
|
||||
anchor = this.reverse ? this.previous[0] : this.next[this.next.length - 1];
|
||||
} else {
|
||||
anchor = this.reverse ? this.next[this.next.length - 1] : this.previous[0];
|
||||
}
|
||||
|
||||
const promise = this.loadMore(anchor, older).then(result => {
|
||||
if(result.items.length < this.loadCount) {
|
||||
if(older) this.loadedAllDown = true;
|
||||
else this.loadedAllUp = true;
|
||||
}
|
||||
|
||||
if(this.count === undefined) {
|
||||
this.count = result.count || result.items.length;
|
||||
}
|
||||
|
||||
const method = older ? result.items.forEach.bind(result.items) : forEachReverse.bind(null, result.items);
|
||||
method((item: any) => {
|
||||
const processed = this.processItem ? this.processItem(item) : item;
|
||||
|
||||
if(!processed) return;
|
||||
|
||||
if(older) {
|
||||
if(this.reverse) this.previous.unshift(processed);
|
||||
else this.next.push(processed);
|
||||
} else {
|
||||
if(this.reverse) this.next.push(processed);
|
||||
else this.previous.unshift(processed);
|
||||
}
|
||||
});
|
||||
}, () => {}).then(() => {
|
||||
if(older) this.loadPromiseDown = null;
|
||||
else this.loadPromiseUp = null;
|
||||
});
|
||||
|
||||
if(older) this.loadPromiseDown = promise;
|
||||
else this.loadPromiseUp = promise;
|
||||
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class PeerProfileAvatars {
|
||||
@ -361,15 +266,17 @@ class PeerProfileAvatars {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadCount = 50;
|
||||
const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader<string | Message.messageService>({
|
||||
loadCount,
|
||||
loadMore: (anchor, older) => {
|
||||
loadCount: 50,
|
||||
loadMore: (anchor, older, loadCount) => {
|
||||
if(!older) return Promise.resolve({count: undefined, items: []});
|
||||
|
||||
if(peerId > 0) {
|
||||
return appPhotosManager.getUserPhotos(peerId, (anchor || listLoader.current) as any, loadCount).then(result => {
|
||||
const maxId: string = (anchor || listLoader.current) as any;
|
||||
return appPhotosManager.getUserPhotos(peerId, maxId, loadCount).then(value => {
|
||||
return {
|
||||
count: result.count,
|
||||
items: result.photos
|
||||
count: value.count,
|
||||
items: value.photos
|
||||
};
|
||||
});
|
||||
} else {
|
||||
@ -391,6 +298,8 @@ class PeerProfileAvatars {
|
||||
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 => {
|
||||
@ -429,6 +338,7 @@ class PeerProfileAvatars {
|
||||
|
||||
this.processItem(listLoader.current);
|
||||
|
||||
// listLoader.loaded
|
||||
listLoader.load(true);
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
|
||||
} */
|
||||
|
||||
if(globalVideo.paused) {
|
||||
appMediaPlaybackController.setSearchContext(searchContext);
|
||||
if(appMediaPlaybackController.setSearchContext(searchContext)) {
|
||||
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid});
|
||||
}
|
||||
|
||||
globalVideo.play();
|
||||
} else {
|
||||
globalVideo.pause();
|
||||
@ -522,8 +525,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
|
||||
const uploading = message.pFlags.is_outgoing && message.media?.preloader;
|
||||
if(doc.type === 'audio' || doc.type === 'voice') {
|
||||
const audioElement = new AudioElement();
|
||||
audioElement.setAttribute('message-id', '' + message.mid);
|
||||
audioElement.setAttribute('peer-id', '' + message.peerId);
|
||||
audioElement.dataset.mid = '' + message.mid;
|
||||
audioElement.dataset.peerId = '' + message.peerId;
|
||||
audioElement.withTime = withTime;
|
||||
audioElement.message = message;
|
||||
audioElement.noAutoDownload = noAutoDownload;
|
||||
|
@ -1,53 +1,149 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import Scrollable from "../components/scrollable";
|
||||
import { safeAssign } from "./object";
|
||||
|
||||
export default class ScrollableLoader {
|
||||
public loading = false;
|
||||
private scrollable: Scrollable;
|
||||
private getPromise: () => Promise<any>;
|
||||
private promise: Promise<any>;
|
||||
private loaded = false;
|
||||
|
||||
constructor(options: {
|
||||
scrollable: ScrollableLoader['scrollable'],
|
||||
getPromise: ScrollableLoader['getPromise']
|
||||
}) {
|
||||
safeAssign(this, options);
|
||||
|
||||
options.scrollable.onScrolledBottom = () => {
|
||||
this.load();
|
||||
};
|
||||
}
|
||||
|
||||
public load() {
|
||||
if(this.loaded) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if(this.loading) {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.promise = this.getPromise().then(done => {
|
||||
this.loading = false;
|
||||
this.promise = undefined;
|
||||
|
||||
if(done) {
|
||||
this.loaded = true;
|
||||
this.scrollable.onScrolledBottom = null;
|
||||
} else {
|
||||
this.scrollable.checkForTriggers();
|
||||
}
|
||||
}, () => {
|
||||
this.promise = undefined;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
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 ListLoaderResult<T extends {}> = {count: number, items: any[]};
|
||||
export default class ListLoader<T extends {}> {
|
||||
public current: T;
|
||||
public previous: T[] = [];
|
||||
public next: T[] = [];
|
||||
public count: number;
|
||||
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 loadCount = 50;
|
||||
protected loadWhenLeft = 20;
|
||||
|
||||
public onJump: (item: T, older: boolean) => void;
|
||||
public onLoadedMore: () => void;
|
||||
|
||||
protected loadedAllUp = false;
|
||||
protected loadedAllDown = false;
|
||||
protected loadPromiseUp: Promise<void>;
|
||||
protected loadPromiseDown: Promise<void>;
|
||||
|
||||
constructor(options: ListLoaderOptions<T>) {
|
||||
safeAssign(this, options);
|
||||
}
|
||||
|
||||
public setTargets(previous: T[], next: T[], reverse: boolean) {
|
||||
this.previous = previous;
|
||||
this.next = next;
|
||||
this.reverse = reverse;
|
||||
}
|
||||
|
||||
public get index() {
|
||||
return this.count !== undefined ? this.previous.length : -1;
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.current = undefined;
|
||||
this.previous = [];
|
||||
this.next = [];
|
||||
this.loadedAllUp = this.loadedAllDown = false;
|
||||
this.loadPromiseUp = this.loadPromiseDown = null;
|
||||
}
|
||||
|
||||
public go(length: number) {
|
||||
let items: T[], item: T;
|
||||
if(length > 0) {
|
||||
items = this.next.splice(0, length);
|
||||
item = items.pop();
|
||||
if(!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.previous.push(this.current, ...items);
|
||||
} else {
|
||||
items = this.previous.splice(this.previous.length + length, -length);
|
||||
item = items.shift();
|
||||
if(!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.next.unshift(...items, this.current);
|
||||
}
|
||||
|
||||
if(this.next.length < this.loadWhenLeft) {
|
||||
this.load(!this.reverse);
|
||||
}
|
||||
|
||||
if(this.previous.length < this.loadWhenLeft) {
|
||||
this.load(this.reverse);
|
||||
}
|
||||
|
||||
this.current = item;
|
||||
this.onJump && this.onJump(item, length > 0);
|
||||
}
|
||||
|
||||
// нет смысла делать проверку для reverse и loadMediaPromise
|
||||
public load(older: boolean) {
|
||||
if(older && this.loadedAllDown) return Promise.resolve();
|
||||
else if(!older && this.loadedAllUp) return Promise.resolve();
|
||||
|
||||
if(older && this.loadPromiseDown) return this.loadPromiseDown;
|
||||
else if(!older && this.loadPromiseUp) return this.loadPromiseUp;
|
||||
|
||||
let anchor: T;
|
||||
if(older) {
|
||||
anchor = this.reverse ? this.previous[0] : this.next[this.next.length - 1];
|
||||
} else {
|
||||
anchor = this.reverse ? this.next[this.next.length - 1] : this.previous[0];
|
||||
}
|
||||
|
||||
const promise = this.loadMore(anchor, older, this.loadCount).then(result => {
|
||||
if((older && this.loadPromiseDown !== promise) || (!older && this.loadPromiseUp !== promise)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(result.items.length < this.loadCount) {
|
||||
if(older) this.loadedAllDown = true;
|
||||
else this.loadedAllUp = true;
|
||||
}
|
||||
|
||||
if(this.count === undefined) {
|
||||
this.count = result.count || result.items.length;
|
||||
}
|
||||
|
||||
const method = older ? result.items.forEach.bind(result.items) : forEachReverse.bind(null, result.items);
|
||||
method((item: any) => {
|
||||
const processed = this.processItem ? this.processItem(item) : item;
|
||||
|
||||
if(!processed) return;
|
||||
|
||||
if(older) {
|
||||
if(this.reverse) this.previous.unshift(processed);
|
||||
else this.next.push(processed);
|
||||
} else {
|
||||
if(this.reverse) this.next.push(processed);
|
||||
else this.previous.unshift(processed);
|
||||
}
|
||||
});
|
||||
|
||||
this.onLoadedMore && this.onLoadedMore();
|
||||
}, () => {}).then(() => {
|
||||
if(older) this.loadPromiseDown = null;
|
||||
else this.loadPromiseUp = null;
|
||||
});
|
||||
|
||||
if(older) this.loadPromiseDown = promise;
|
||||
else this.loadPromiseUp = promise;
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
53
src/helpers/scrollableLoader.ts
Normal file
53
src/helpers/scrollableLoader.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* https://github.com/morethanwords/tweb
|
||||
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
||||
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import Scrollable from "../components/scrollable";
|
||||
import { safeAssign } from "./object";
|
||||
|
||||
export default class ScrollableLoader {
|
||||
public loading = false;
|
||||
private scrollable: Scrollable;
|
||||
private getPromise: () => Promise<any>;
|
||||
private promise: Promise<any>;
|
||||
private loaded = false;
|
||||
|
||||
constructor(options: {
|
||||
scrollable: ScrollableLoader['scrollable'],
|
||||
getPromise: ScrollableLoader['getPromise']
|
||||
}) {
|
||||
safeAssign(this, options);
|
||||
|
||||
options.scrollable.onScrolledBottom = () => {
|
||||
this.load();
|
||||
};
|
||||
}
|
||||
|
||||
public load() {
|
||||
if(this.loaded) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if(this.loading) {
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.promise = this.getPromise().then(done => {
|
||||
this.loading = false;
|
||||
this.promise = undefined;
|
||||
|
||||
if(done) {
|
||||
this.loaded = true;
|
||||
this.scrollable.onScrolledBottom = null;
|
||||
} else {
|
||||
this.scrollable.checkForTriggers();
|
||||
}
|
||||
}, () => {
|
||||
this.promise = undefined;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ export const ctx = typeof(window) !== 'undefined' ? window : self;
|
||||
// https://stackoverflow.com/a/58065241
|
||||
export const isAppleMobile = (/iPad|iPhone|iPod/.test(navigator.platform) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
|
||||
!ctx.MSStream;
|
||||
!(ctx as any).MSStream;
|
||||
|
||||
export const isSafari = !!('safari' in ctx) || !!(userAgent && (/\b(iPad|iPhone|iPod)\b/.test(userAgent) || (!!userAgent.match('Safari') && !userAgent.match('Chrome'))))/* || true */;
|
||||
export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
|
@ -137,9 +137,17 @@ export class AppPhotosManager {
|
||||
photosResult.photos[idx] = this.savePhoto(photo, {type: 'profilePhoto', peerId: userId});
|
||||
return photo.id;
|
||||
});
|
||||
|
||||
// ! WARNING !
|
||||
if(maxId !== '0' && maxId) {
|
||||
const idx = photoIds.indexOf(maxId);
|
||||
if(idx !== -1) {
|
||||
photoIds.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
count: (photosResult as PhotosPhotos.photosPhotosSlice).count || photosResult.photos.length,
|
||||
count: (photosResult as PhotosPhotos.photosPhotosSlice).count || photoIds.length,
|
||||
photos: photoIds
|
||||
};
|
||||
});
|
||||
|
@ -141,7 +141,7 @@ export default class IDBStorage<T extends Database<any>> {
|
||||
return Promise.reject();
|
||||
}
|
||||
} catch(error) {
|
||||
this.log.error('error opening db', error.message)
|
||||
this.log.error('error opening db', (error as Error).message);
|
||||
this.storageIsAvailable = false;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ export class ApiManager {
|
||||
|
||||
networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options);
|
||||
} catch(error) {
|
||||
this.log('Get networker error', error, error.stack);
|
||||
this.log('Get networker error', error, (error as Error).stack);
|
||||
delete this.gettingNetworkers[getKey];
|
||||
throw error;
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export class Authorizer {
|
||||
rsaKeysManager.prepare();
|
||||
deserializer = await promise;
|
||||
} catch(error) {
|
||||
this.log.error('req_pq error', error.message);
|
||||
this.log.error('req_pq error', (error as Error).message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
@ -1262,7 +1262,7 @@ export default class MTPNetworker {
|
||||
try {
|
||||
result.body = deserializer.fetchObject('Object', field + '[body]');
|
||||
} catch(e) {
|
||||
this.log.error('parse error', e.message, e.stack);
|
||||
this.log.error('parse error', (e as Error).message, (e as Error).stack);
|
||||
result.body = {
|
||||
_: 'parse_error',
|
||||
error: e
|
||||
|
@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { DcId } from '../types';
|
||||
import type { ApiError } from '../lib/mtproto/apiManager';
|
||||
import apiManager from '../lib/mtproto/mtprotoworker';
|
||||
import Page from './page';
|
||||
import serverTimeManager from '../lib/mtproto/serverTimeManager';
|
||||
@ -200,10 +201,10 @@ let onFirstMount = async() => {
|
||||
await pause(diff > FETCH_INTERVAL ? 1e3 * FETCH_INTERVAL : 1e3 * diff | 0);
|
||||
}
|
||||
} catch(err) {
|
||||
switch(err.type) {
|
||||
switch((err as ApiError).type) {
|
||||
case 'SESSION_PASSWORD_NEEDED':
|
||||
console.warn('pageSignQR: SESSION_PASSWORD_NEEDED');
|
||||
err.handled = true;
|
||||
(err as ApiError).handled = true;
|
||||
import('./pagePassword').then(m => m.default.mount());
|
||||
stop = true;
|
||||
cachedPromise = null;
|
||||
|
Loading…
x
Reference in New Issue
Block a user