Browse Source

Fix avatar duplicate

Fix media viewer list again
Supported media browser controls
master
morethanwords 3 years ago
parent
commit
344b70dc3e
  1. 311
      src/components/appMediaPlaybackController.ts
  2. 254
      src/components/appMediaViewer.ts
  3. 28
      src/components/audio.ts
  4. 5
      src/components/chat/bubbles.ts
  5. 2
      src/components/lazyLoadQueue.ts
  6. 2
      src/components/sidebarRight/tabs/groupPermissions.ts
  7. 142
      src/components/sidebarRight/tabs/sharedMedia.ts
  8. 9
      src/components/wrappers.ts
  9. 202
      src/helpers/listLoader.ts
  10. 53
      src/helpers/scrollableLoader.ts
  11. 2
      src/helpers/userAgent.ts
  12. 10
      src/lib/appManagers/appPhotosManager.ts
  13. 2
      src/lib/idb.ts
  14. 2
      src/lib/mtproto/apiManager.ts
  15. 2
      src/lib/mtproto/authorizer.ts
  16. 2
      src/lib/mtproto/networker.ts
  17. 5
      src/pages/pageSignQR.ts

311
src/components/appMediaPlaybackController.ts

@ -8,12 +8,19 @@ import rootScope from "../lib/rootScope"; @@ -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"; @@ -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 = (() => { @@ -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 { @@ -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 { @@ -49,20 +58,41 @@ class AppMediaPlaybackController {
} = {};
public willBePlayedMedia: HTMLMediaElement;
public searchContext: SearchSuperContext;
private currentPeerId: number;
private prevMid: number;
private nextMid: number;
private searchContext: SearchSuperContext;
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 { @@ -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 { @@ -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.next();
};
this.resolveWaitingForLoadMedia(this.currentPeerId, this.nextMid);
public toggle(play?: boolean) {
if(!this.playingMedia) {
return;
}
setTimeout(() => {
media.play()//.catch(() => {});
}, 0);
if(play === undefined) {
play = this.playingMedia.paused;
}
};
private loadSiblingsMedia(offsetPeerId: number, offsetMid: number) {
const {playingMedia, searchContext} = this;
if(!searchContext) {
if(this.playingMedia.paused !== play) {
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);
[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) {
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 { @@ -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);
}
}

254
src/components/appMediaViewer.ts

@ -30,11 +30,9 @@ import appSidebarRight from "./sidebarRight"; @@ -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"; @@ -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; @@ -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 @@ -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 @@ -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([]); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
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);
}
loadMore: (anchor, older, loadCount) => {
if(this.peerId < 0 || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет.
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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 = { @@ -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 @@ -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 @@ -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 @@ -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;

28
src/components/audio.ts

@ -9,7 +9,7 @@ import { RichTextProcessor } from "../lib/richtextprocessor"; @@ -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"; @@ -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 { @@ -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 { @@ -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 { @@ -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();
});

5
src/components/chat/bubbles.ts

@ -295,11 +295,10 @@ export default class ChatBubbles { @@ -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 {

2
src/components/lazyLoadQueue.ts

@ -87,7 +87,7 @@ export class LazyLoadQueueBase { @@ -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 */);
}
}

2
src/components/sidebarRight/tabs/groupPermissions.ts

@ -8,7 +8,7 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent"; @@ -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";

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

@ -5,7 +5,7 @@ @@ -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"; @@ -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"; @@ -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) => { @@ -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;
}
this.previous.push(this.current, ...items);
} else {
items = this.previous.splice(this.previous.length + length, -length);
item = items.shift();
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.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 { @@ -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 { @@ -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 { @@ -429,6 +338,7 @@ class PeerProfileAvatars {
this.processItem(listLoader.current);
// listLoader.loaded
listLoader.load(true);
}

9
src/components/wrappers.ts

@ -277,7 +277,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -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 @@ -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;

202
src/helpers/listLoader.ts

@ -1,53 +1,149 @@ @@ -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

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

2
src/helpers/userAgent.ts

@ -14,7 +14,7 @@ export const ctx = typeof(window) !== 'undefined' ? window : self; @@ -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;

10
src/lib/appManagers/appPhotosManager.ts

@ -137,9 +137,17 @@ export class AppPhotosManager { @@ -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
};
});

2
src/lib/idb.ts

@ -141,7 +141,7 @@ export default class IDBStorage<T extends Database<any>> { @@ -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);
}

2
src/lib/mtproto/apiManager.ts

@ -275,7 +275,7 @@ export class ApiManager { @@ -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;
}

2
src/lib/mtproto/authorizer.ts

@ -194,7 +194,7 @@ export class Authorizer { @@ -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;
}

2
src/lib/mtproto/networker.ts

@ -1262,7 +1262,7 @@ export default class MTPNetworker { @@ -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
src/pages/pageSignQR.ts

@ -5,6 +5,7 @@ @@ -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() => { @@ -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…
Cancel
Save