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";
import appMessagesManager from "../lib/appManagers/appMessagesManager"; import appMessagesManager from "../lib/appManagers/appMessagesManager";
import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise"; 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 { MOUNT_CLASS_TO } from "../config/debug";
import appDownloadManager from "../lib/appManagers/appDownloadManager"; import appDownloadManager from "../lib/appManagers/appDownloadManager";
import simulateEvent from "../helpers/dom/dispatchEvent"; import simulateEvent from "../helpers/dom/dispatchEvent";
import type { SearchSuperContext } from "./appSearchSuper."; import type { SearchSuperContext } from "./appSearchSuper.";
import { copy, deepEqual } from "../helpers/object"; 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: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда // TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
@ -21,7 +28,7 @@ import { copy, deepEqual } from "../helpers/object";
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка // TODO: Safari: попробовать замаскировать подгрузку последнего чанка
// TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце // TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце
type MediaItem = {mid: number, peerId: number}; export type MediaItem = {mid: number, peerId: number};
type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement; type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement;
@ -33,6 +40,8 @@ const SHOULD_USE_SAFARI_FIX = (() => {
} }
})(); })();
const SEEK_OFFSET = 10;
class AppMediaPlaybackController { class AppMediaPlaybackController {
private container: HTMLElement; private container: HTMLElement;
private media: { private media: {
@ -40,7 +49,7 @@ class AppMediaPlaybackController {
[mid: string]: HTMLMediaElement [mid: string]: HTMLMediaElement
} }
} = {}; } = {};
public playingMedia: HTMLMediaElement; private playingMedia: HTMLMediaElement;
private waitingMediaForLoad: { private waitingMediaForLoad: {
[peerId: string]: { [peerId: string]: {
@ -49,20 +58,41 @@ class AppMediaPlaybackController {
} = {}; } = {};
public willBePlayedMedia: HTMLMediaElement; public willBePlayedMedia: HTMLMediaElement;
public searchContext: SearchSuperContext; private searchContext: SearchSuperContext;
private currentPeerId: number;
private prevMid: number;
private nextMid: number;
private prev: MediaItem[] = []; private listLoader: SearchListLoader<MediaItem>;
private next: MediaItem[] = [];
constructor() { constructor() {
this.container = document.createElement('div'); this.container = document.createElement('div');
//this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;'; //this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;';
this.container.style.cssText = 'display: none;'; this.container.style.cssText = 'display: none;';
document.body.append(this.container); 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 { public addMedia(peerId: number, doc: MyDocument, mid: number, autoload = true): HTMLMediaElement {
@ -187,33 +217,126 @@ class AppMediaPlaybackController {
media.safariBuffering = value; 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) => { onPlay = (e?: Event) => {
const media = e.target as HTMLMediaElement; const media = e.target as HTMLMediaElement;
const peerId = +media.dataset.peerId; const peerId = +media.dataset.peerId;
const mid = +media.dataset.mid; const mid = +media.dataset.mid;
this.currentPeerId = peerId;
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media); //console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
const message = appMessagesManager.getMessageByPeer(peerId, mid);
const previousMedia = this.playingMedia; const previousMedia = this.playingMedia;
if(previousMedia !== media) { if(previousMedia !== media) {
if(previousMedia) { this.stop();
if(!previousMedia.paused) {
previousMedia.pause();
}
// reset media
previousMedia.currentTime = 0;
simulateEvent(previousMedia, 'ended');
}
this.playingMedia = media; this.playingMedia = media;
this.loadSiblingsMedia(peerId, mid);
if('mediaSession' in navigator) {
this.setNewMediadata(message);
}
} }
// audio_pause не успеет сработать без таймаута // audio_pause не успеет сработать без таймаута
setTimeout(() => { setTimeout(() => {
const message = appMessagesManager.getMessageByPeer(peerId, mid);
rootScope.dispatchEvent('audio_play', {peerId, doc: message.media.document, mid}); rootScope.dispatchEvent('audio_play', {peerId, doc: message.media.document, mid});
}, 0); }, 0);
}; };
@ -238,65 +361,80 @@ class AppMediaPlaybackController {
//console.log('on media end'); //console.log('on media end');
if(this.nextMid) { this.next();
const media = this.media[this.currentPeerId][this.nextMid]; };
/* if(isSafari) {
media.autoplay = true;
} */
this.resolveWaitingForLoadMedia(this.currentPeerId, this.nextMid); public toggle(play?: boolean) {
if(!this.playingMedia) {
return;
}
setTimeout(() => { if(play === undefined) {
media.play()//.catch(() => {}); play = this.playingMedia.paused;
}, 0);
} }
};
private loadSiblingsMedia(offsetPeerId: number, offsetMid: number) { if(this.playingMedia.paused !== play) {
const {playingMedia, searchContext} = this;
if(!searchContext) {
return; return;
} }
return appMessagesManager.getSearch({ if(play) {
...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) {
this.playingMedia.play(); this.playingMedia.play();
} else { } else {
this.playingMedia.pause(); this.playingMedia.pause();
} }
} }
public pause() { public play = () => {
if(!this.playingMedia || this.playingMedia.paused) return; return this.toggle(true);
this.playingMedia.pause(); };
}
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) { public willBePlayed(media: HTMLMediaElement) {
this.willBePlayedMedia = media; this.willBePlayedMedia = media;
@ -304,12 +442,43 @@ class AppMediaPlaybackController {
public setSearchContext(context: SearchSuperContext) { public setSearchContext(context: SearchSuperContext) {
if(deepEqual(this.searchContext, context)) { if(deepEqual(this.searchContext, context)) {
return; return false;
} }
this.searchContext = copy(context); // {_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'} this.searchContext = copy(context); // {_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'}
this.prev.length = 0; return true;
this.next.length = 0; }
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";
import SwipeHandler from "./swipeHandler"; import SwipeHandler from "./swipeHandler";
import { ONE_DAY } from "../helpers/date"; import { ONE_DAY } from "../helpers/date";
import { SearchSuperContext } from "./appSearchSuper."; import { SearchSuperContext } from "./appSearchSuper.";
import DEBUG from "../config/debug";
import appNavigationController from "./appNavigationController"; import appNavigationController from "./appNavigationController";
import { Message } from "../layer"; import { Message } from "../layer";
import { forEachReverse } from "../helpers/array"; import AppSharedMediaTab, { filterChatPhotosMessages } from "./sidebarRight/tabs/sharedMedia";
import AppSharedMediaTab from "./sidebarRight/tabs/sharedMedia";
import findUpClassName from "../helpers/dom/findUpClassName"; import findUpClassName from "../helpers/dom/findUpClassName";
import renderImageFromUrl, { renderImageFromUrlPromise } from "../helpers/dom/renderImageFromUrl"; import renderImageFromUrl, { renderImageFromUrlPromise } from "../helpers/dom/renderImageFromUrl";
import getVisibleRect from "../helpers/dom/getVisibleRect"; import getVisibleRect from "../helpers/dom/getVisibleRect";
@ -53,7 +51,7 @@ import { attachClickEvent } from "../helpers/dom/clickEvent";
import PopupDeleteMessages from "./popups/deleteMessages"; import PopupDeleteMessages from "./popups/deleteMessages";
import RangeSelector from "./rangeSelector"; import RangeSelector from "./rangeSelector";
import windowSize from "../helpers/windowSize"; import windowSize from "../helpers/windowSize";
import { safeAssign } from "../helpers/object"; import ListLoader, { ListLoaderOptions } from "../helpers/listLoader";
const ZOOM_STEP = 0.5; const ZOOM_STEP = 0.5;
const ZOOM_INITIAL_VALUE = 1; const ZOOM_INITIAL_VALUE = 1;
@ -66,129 +64,22 @@ const ZOOM_MAX_VALUE = 4;
const MEDIA_VIEWER_CLASSNAME = 'media-viewer'; const MEDIA_VIEWER_CLASSNAME = 'media-viewer';
type MediaQueueLoaderOptions<Item extends {}> = { export class SearchListLoader<Item extends {mid: number, peerId: number}> extends ListLoader<Item> {
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> {
public searchContext: SearchSuperContext; public searchContext: SearchSuperContext;
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> = {}) { constructor(options: Omit<ListLoaderOptions<Item>, 'loadMore'> = {}) {
super({ super({
...options, ...options,
getLoadPromise: (older, anchor, loadCount) => { loadMore: (anchor, older, loadCount) => {
const backLimit = older ? 0 : loadCount; const backLimit = older ? 0 : loadCount;
let maxId = this.target?.mid; let maxId = this.current?.mid;
if(anchor) maxId = anchor.mid; if(anchor) maxId = anchor.mid;
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1); if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1);
return appMessagesManager.getSearch({ return appMessagesManager.getSearch({
...this.searchContext, ...this.searchContext,
peerId: anchor?.peerId, peerId: this.searchContext.peerId || anchor?.peerId,
maxId, maxId,
limit: backLimit ? 0 : loadCount, limit: backLimit ? 0 : loadCount,
backLimit 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); this.log('loaded more media by maxId:', maxId, value, older, this.reverse);
} */ } */
if(this.searchContext.inputFilter._ === 'inputMessagesFilterChatPhotos') {
filterChatPhotosMessages(value);
}
if(value.next_rate) { if(value.next_rate) {
this.searchContext.nextRate = 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; this.searchContext = context;
if(this.searchContext.folderId !== undefined) { if(this.searchContext.folderId !== undefined) {
this.loadedAllMediaUp = true; this.loadedAllUp = true;
if(this.searchContext.nextRate === undefined) { 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; private peerId: number;
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> & {peerId: number}) { constructor(options: Omit<ListLoaderOptions<Item>, 'loadMore'> & {peerId: number}) {
super({ super({
...options, ...options,
getLoadPromise: (older, anchor, loadCount) => { loadMore: (anchor, older, loadCount) => {
if(this.peerId < 0) return Promise.resolve([]); // ! это значит, что открыло аватар чата, но следующих фотографий нет. 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 {element: null as HTMLElement, photoId} as any;
}); });
return {count: value.count, items};
}); });
} }
}); });
this.loadedAllUp = true;
this.peerId = options.peerId; this.peerId = options.peerId;
} }
} }
@ -259,8 +162,6 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
protected preloader: ProgressivePreloader = null; protected preloader: ProgressivePreloader = null;
protected preloaderStreamable: ProgressivePreloader = null; protected preloaderStreamable: ProgressivePreloader = null;
protected prevTargets: TargetType[] = [];
protected nextTargets: TargetType[] = [];
//protected targetContainer: HTMLElement = null; //protected targetContainer: HTMLElement = null;
//protected loadMore: () => void = null; //protected loadMore: () => void = null;
@ -268,8 +169,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
protected isFirstOpen = true; 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; protected pageEl = document.getElementById('page-chats') as HTMLDivElement;
@ -302,14 +202,14 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
protected ctrlKeyDown: boolean; protected ctrlKeyDown: boolean;
get target() { get target() {
return this.queueLoader.target; return this.listLoader.current;
} }
set target(value) { 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']>) { topButtons: Array<keyof AppMediaViewerBase<ContentAdditionType, ButtonsAdditionType, TargetType>['buttons']>) {
this.log = logger('AMV'); this.log = logger('AMV');
this.preloader = new ProgressivePreloader(); this.preloader = new ProgressivePreloader();
@ -432,30 +332,13 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
el.addEventListener('click', this.close.bind(this)); el.addEventListener('click', this.close.bind(this));
}); });
this.buttons.prev.addEventListener('click', (e) => { ([[-1, this.buttons.prev], [1, this.buttons.next]] as [number, HTMLElement][]).forEach(([moveLength, button]) => {
cancelEvent(e); button.addEventListener('click', (e) => {
if(this.setMoverPromise) return; cancelEvent(e);
if(this.setMoverPromise) return;
const target = this.prevTargets.pop();
if(target) { this.listLoader.go(moveLength);
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';
}
}); });
this.buttons.zoom.addEventListener('click', () => { this.buttons.zoom.addEventListener('click', () => {
@ -467,6 +350,11 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
this.wholeDiv.addEventListener('click', this.onClick); this.wholeDiv.addEventListener('click', this.onClick);
this.listLoader.onJump = (item, older) => {
if(older) this.onNextClick(item);
else this.onPrevClick(item);
};
if(isTouchSupported) { if(isTouchSupported) {
const swipeHandler = new SwipeHandler({ const swipeHandler = new SwipeHandler({
element: this.wholeDiv, element: this.wholeDiv,
@ -606,9 +494,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd); const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd);
this.target = false as any; this.listLoader.reset();
this.prevTargets.length = 0;
this.nextTargets.length = 0;
this.setMoverPromise = null; this.setMoverPromise = null;
this.tempId = -1; this.tempId = -1;
(window as any).appMediaViewer = undefined; (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, 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(this.setMoverPromise) return this.setMoverPromise;
/* if(DEBUG) { /* if(DEBUG) {
@ -1367,12 +1253,9 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
if(this.isFirstOpen) { if(this.isFirstOpen) {
//this.targetContainer = targetContainer; //this.targetContainer = targetContainer;
this.prevTargets = prevTargets; // this.needLoadMore = needLoadMore;
this.nextTargets = nextTargets;
this.reverse = reverse;
this.needLoadMore = needLoadMore;
this.isFirstOpen = false; this.isFirstOpen = false;
this.queueLoader.setTargets(this.prevTargets, this.nextTargets, this.reverse); this.listLoader.setTargets(prevTargets, nextTargets, reverse);
(window as any).appMediaViewer = this; (window as any).appMediaViewer = this;
//this.loadMore = loadMore; //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(prevTarget && (!prevTarget.parentElement || !this.isElementVisible(this.targetContainer, prevTarget))) prevTarget = null;
//if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null; //if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null;
this.buttons.prev.classList.toggle('hide', !this.prevTargets.length); this.buttons.prev.classList.toggle('hide', !this.listLoader.previous.length);
this.buttons.next.classList.toggle('hide', !this.nextTargets.length); this.buttons.next.classList.toggle('hide', !this.listLoader.next.length);
const container = this.content.media; const container = this.content.media;
const useContainerAsTarget = !target || target === container; const useContainerAsTarget = !target || target === container;
@ -1399,16 +1282,6 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
this.target = {element: target} as any; this.target = {element: target} as any;
const tempId = ++this.tempId; 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) { if(container.firstElementChild) {
container.innerHTML = ''; container.innerHTML = '';
} }
@ -1746,16 +1619,15 @@ type AppMediaViewerTargetType = {
}; };
export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> { export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delete' | 'forward', AppMediaViewerTargetType> {
protected btnMenuDelete: HTMLElement; protected btnMenuDelete: HTMLElement;
protected listLoader: SearchListLoader<AppMediaViewerTargetType>;
protected queueLoader: MediaSearchQueueLoader<AppMediaViewerTargetType>;
get searchContext() { get searchContext() {
return this.queueLoader.searchContext; return this.listLoader.searchContext;
} }
constructor() { constructor() {
super(new MediaSearchQueueLoader({ super(new SearchListLoader({
generateItem: (item) => { processItem: (item) => {
const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument'; const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument';
const {mid, peerId} = item; const {mid, peerId} = item;
const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(item); const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(item);
@ -1950,13 +1822,13 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
} }
public setSearchContext(context: SearchSuperContext) { public setSearchContext(context: SearchSuperContext) {
this.queueLoader.setSearchContext(context); this.listLoader.setSearchContext(context);
return this; return this;
} }
public async openMedia(message: any, target?: HTMLElement, fromRight = 0, reverse = false, 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; if(this.setMoverPromise) return this.setMoverPromise;
const mid = message.mid; const mid = message.mid;
@ -1971,7 +1843,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
}); });
this.setCaption(message); 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.mid = mid;
this.target.peerId = message.peerId; this.target.peerId = message.peerId;
@ -1988,7 +1860,7 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe
public peerId: number; public peerId: number;
constructor(peerId: number) { constructor(peerId: number) {
super(new MediaAvatarQueueLoader({peerId}), [/* 'delete' */]); super(new AvatarListLoader({peerId}), [/* 'delete' */]);
this.peerId = peerId; this.peerId = peerId;

28
src/components/audio.ts

@ -9,7 +9,7 @@ import { RichTextProcessor } from "../lib/richtextprocessor";
import { formatDate, wrapPhoto } from "./wrappers"; import { formatDate, wrapPhoto } from "./wrappers";
import ProgressivePreloader from "./preloader"; import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer"; import { MediaProgressLine } from "../lib/mediaPlayer";
import appMediaPlaybackController from "./appMediaPlaybackController"; import appMediaPlaybackController, { MediaItem } from "./appMediaPlaybackController";
import { DocumentAttribute } from "../layer"; import { DocumentAttribute } from "../layer";
import mediaSizes from "../helpers/mediaSizes"; import mediaSizes from "../helpers/mediaSizes";
import { isSafari } from "../helpers/userAgent"; import { isSafari } from "../helpers/userAgent";
@ -21,13 +21,14 @@ import { formatDateAccordingToToday } from "../helpers/date";
import { cancelEvent } from "../helpers/dom/cancelEvent"; import { cancelEvent } from "../helpers/dom/cancelEvent";
import { attachClickEvent, detachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent, detachClickEvent } from "../helpers/dom/clickEvent";
import LazyLoadQueue from "./lazyLoadQueue"; import LazyLoadQueue from "./lazyLoadQueue";
import { deferredPromise } from "../helpers/cancellablePromise"; import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
import ListenerSetter, { Listener } from "../helpers/listenerSetter"; import ListenerSetter, { Listener } from "../helpers/listenerSetter";
import noop from "../helpers/noop"; import noop from "../helpers/noop";
import findUpClassName from "../helpers/dom/findUpClassName";
rootScope.addEventListener('messages_media_read', ({mids, peerId}) => { rootScope.addEventListener('messages_media_read', ({mids, peerId}) => {
mids.forEach(mid => { 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'); elem.classList.remove('is-unread');
}); });
}); });
@ -371,7 +372,7 @@ export default class AudioElement extends HTMLElement {
private listenerSetter = new ListenerSetter(); private listenerSetter = new ListenerSetter();
private onTypeDisconnect: () => void; private onTypeDisconnect: () => void;
public onLoad: (autoload?: boolean) => void; public onLoad: (autoload?: boolean) => void;
readyPromise: import("/Users/kuzmenko/Documents/projects/tweb/src/helpers/cancellablePromise").CancellablePromise<void>; private readyPromise: CancellablePromise<void>;
public render() { public render() {
this.classList.add('audio'); this.classList.add('audio');
@ -429,7 +430,22 @@ export default class AudioElement extends HTMLElement {
e && cancelEvent(e); e && cancelEvent(e);
if(paused) { 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(() => {}); audio.play().catch(() => {});
} else { } else {
audio.pause(); audio.pause();
@ -444,7 +460,7 @@ export default class AudioElement extends HTMLElement {
}); });
this.addAudioListener('timeupdate', () => { this.addAudioListener('timeupdate', () => {
if(appMediaPlaybackController.playingMedia !== audio || appMediaPlaybackController.isSafariBuffering(audio)) return; if((!audio.currentTime && audio.paused) || appMediaPlaybackController.isSafariBuffering(audio)) return;
audioTimeDiv.innerText = getTimeStr(); audioTimeDiv.innerText = getTimeStr();
}); });

5
src/components/chat/bubbles.ts

@ -295,11 +295,10 @@ export default class ChatBubbles {
} }
if(message.media?.document) { 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) {
if(element instanceof AudioElement) { if(element instanceof AudioElement) {
element.setAttribute('doc-id', message.media.document.id); element.dataset.mid = '' + mid;
element.setAttribute('message-id', '' + mid);
element.message = message; element.message = message;
element.onLoad(true); element.onLoad(true);
} else { } else {

2
src/components/lazyLoadQueue.ts

@ -87,7 +87,7 @@ export class LazyLoadQueueBase {
//await item.load(item.div); //await item.load(item.div);
await this.loadItem(item); await this.loadItem(item);
} catch(err) { } 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 */); this.log.error('loadMediaQueue error:', err/* , item */);
} }
} }

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

@ -8,7 +8,7 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import findUpTag from "../../../helpers/dom/findUpTag"; import findUpTag from "../../../helpers/dom/findUpTag";
import replaceContent from "../../../helpers/dom/replaceContent"; import replaceContent from "../../../helpers/dom/replaceContent";
import ListenerSetter from "../../../helpers/listenerSetter"; import ListenerSetter from "../../../helpers/listenerSetter";
import ScrollableLoader from "../../../helpers/listLoader"; import ScrollableLoader from "../../../helpers/scrollableLoader";
import { ChannelParticipant, Chat, ChatBannedRights, Update } from "../../../layer"; import { ChannelParticipant, Chat, ChatBannedRights, Update } from "../../../layer";
import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager"; import appChatsManager, { ChatRights } from "../../../lib/appManagers/appChatsManager";
import appDialogsManager from "../../../lib/appManagers/appDialogsManager"; import appDialogsManager from "../../../lib/appManagers/appDialogsManager";

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

@ -5,7 +5,7 @@
*/ */
import appImManager from "../../../lib/appManagers/appImManager"; 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 appPeersManager from "../../../lib/appManagers/appPeersManager";
import appProfileManager from "../../../lib/appManagers/appProfileManager"; import appProfileManager from "../../../lib/appManagers/appProfileManager";
import appUsersManager, { User } from "../../../lib/appManagers/appUsersManager"; import appUsersManager, { User } from "../../../lib/appManagers/appUsersManager";
@ -31,8 +31,6 @@ import Row from "../../row";
import { copyTextToClipboard } from "../../../helpers/clipboard"; import { copyTextToClipboard } from "../../../helpers/clipboard";
import { toast, toastNew } from "../../toast"; import { toast, toastNew } from "../../toast";
import { fastRaf } from "../../../helpers/schedulers"; import { fastRaf } from "../../../helpers/schedulers";
import { safeAssign } from "../../../helpers/object";
import { forEachReverse } from "../../../helpers/array";
import appPhotosManager from "../../../lib/appManagers/appPhotosManager"; import appPhotosManager from "../../../lib/appManagers/appPhotosManager";
import renderImageFromUrl from "../../../helpers/dom/renderImageFromUrl"; import renderImageFromUrl from "../../../helpers/dom/renderImageFromUrl";
import SwipeHandler from "../../swipeHandler"; import SwipeHandler from "../../swipeHandler";
@ -50,6 +48,8 @@ import { attachClickEvent } from "../../../helpers/dom/clickEvent";
import replaceContent from "../../../helpers/dom/replaceContent"; import replaceContent from "../../../helpers/dom/replaceContent";
import appAvatarsManager from "../../../lib/appManagers/appAvatarsManager"; import appAvatarsManager from "../../../lib/appManagers/appAvatarsManager";
import generateVerifiedIcon from "../../generateVerifiedIcon"; import generateVerifiedIcon from "../../generateVerifiedIcon";
import ListLoader from "../../../helpers/listLoader";
import { forEachReverse } from "../../../helpers/array";
let setText = (text: string, row: Row) => { let setText = (text: string, row: Row) => {
//fastRaf(() => { //fastRaf(() => {
@ -60,115 +60,20 @@ let setText = (text: string, row: Row) => {
const PARALLAX_SUPPORTED = !isFirefox; const PARALLAX_SUPPORTED = !isFirefox;
type ListLoaderResult<T> = {count: number, items: any[]}; export function filterChatPhotosMessages(value: {
class ListLoader<T> { count: number;
public current: T; next_rate: number;
public previous: T[] = []; offset_id_offset: number;
public next: T[] = []; history: MyMessage[];
public count: number; }) {
forEachReverse(value.history, (message, idx, arr) => {
public tempId = 0; if(!((message as Message.messageService).action as MessageAction.messageActionChatEditPhoto).photo) {
public loadMore: (anchor: T, older: boolean) => Promise<ListLoaderResult<T>>; arr.splice(idx, 1);
public processItem: (item: any) => false | T; if(value.count !== undefined) {
public onJump: (item: T, older: boolean) => void; --value.count;
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;
} }
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 { class PeerProfileAvatars {
@ -361,15 +266,17 @@ class PeerProfileAvatars {
return; return;
} }
const loadCount = 50;
const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader<string | Message.messageService>({ const listLoader: PeerProfileAvatars['listLoader'] = this.listLoader = new ListLoader<string | Message.messageService>({
loadCount, loadCount: 50,
loadMore: (anchor, older) => { loadMore: (anchor, older, loadCount) => {
if(!older) return Promise.resolve({count: undefined, items: []});
if(peerId > 0) { 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 { return {
count: result.count, count: value.count,
items: result.photos items: value.photos
}; };
}); });
} else { } else {
@ -391,6 +298,8 @@ class PeerProfileAvatars {
return Promise.all(promises).then((result) => { return Promise.all(promises).then((result) => {
const value = result.pop() as typeof result[1]; const value = result.pop() as typeof result[1];
filterChatPhotosMessages(value);
if(!listLoader.current) { if(!listLoader.current) {
const chatFull = result[0]; const chatFull = result[0];
const message = value.history.findAndSplice(m => { const message = value.history.findAndSplice(m => {
@ -429,6 +338,7 @@ class PeerProfileAvatars {
this.processItem(listLoader.current); this.processItem(listLoader.current);
// listLoader.loaded
listLoader.load(true); listLoader.load(true);
} }

9
src/components/wrappers.ts

@ -277,7 +277,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
} */ } */
if(globalVideo.paused) { if(globalVideo.paused) {
appMediaPlaybackController.setSearchContext(searchContext); if(appMediaPlaybackController.setSearchContext(searchContext)) {
appMediaPlaybackController.setTargets({peerId: message.peerId, mid: message.mid});
}
globalVideo.play(); globalVideo.play();
} else { } else {
globalVideo.pause(); globalVideo.pause();
@ -522,8 +525,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
const uploading = message.pFlags.is_outgoing && message.media?.preloader; const uploading = message.pFlags.is_outgoing && message.media?.preloader;
if(doc.type === 'audio' || doc.type === 'voice') { if(doc.type === 'audio' || doc.type === 'voice') {
const audioElement = new AudioElement(); const audioElement = new AudioElement();
audioElement.setAttribute('message-id', '' + message.mid); audioElement.dataset.mid = '' + message.mid;
audioElement.setAttribute('peer-id', '' + message.peerId); audioElement.dataset.peerId = '' + message.peerId;
audioElement.withTime = withTime; audioElement.withTime = withTime;
audioElement.message = message; audioElement.message = message;
audioElement.noAutoDownload = noAutoDownload; audioElement.noAutoDownload = noAutoDownload;

202
src/helpers/listLoader.ts

@ -1,53 +1,149 @@
/* /*
* https://github.com/morethanwords/tweb * https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko * Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import Scrollable from "../components/scrollable"; import { forEachReverse } from "./array";
import { safeAssign } from "./object"; import { safeAssign } from "./object";
export default class ScrollableLoader { export type ListLoaderOptions<T extends {}> = {
public loading = false; loadMore: ListLoader<T>['loadMore'],
private scrollable: Scrollable; loadCount?: ListLoader<T>['loadCount'],
private getPromise: () => Promise<any>; loadWhenLeft?: ListLoader<T>['loadWhenLeft'],
private promise: Promise<any>; processItem?: ListLoader<T>['processItem'],
private loaded = false; onJump?: ListLoader<T>['onJump'],
onLoadedMore?: ListLoader<T>['onLoadedMore']
constructor(options: { };
scrollable: ScrollableLoader['scrollable'],
getPromise: ScrollableLoader['getPromise'] export type ListLoaderResult<T extends {}> = {count: number, items: any[]};
}) { export default class ListLoader<T extends {}> {
safeAssign(this, options); public current: T;
public previous: T[] = [];
options.scrollable.onScrolledBottom = () => { public next: T[] = [];
this.load(); public count: number;
}; public reverse = false; // reverse means next = higher msgid
}
protected loadMore: (anchor: T, older: boolean, loadCount: number) => Promise<ListLoaderResult<T>>;
public load() { protected processItem: (item: any) => T;
if(this.loaded) { protected loadCount = 50;
return Promise.resolve(); protected loadWhenLeft = 20;
}
public onJump: (item: T, older: boolean) => void;
if(this.loading) { public onLoadedMore: () => void;
return this.promise;
} protected loadedAllUp = false;
protected loadedAllDown = false;
this.loading = true; protected loadPromiseUp: Promise<void>;
this.promise = this.getPromise().then(done => { protected loadPromiseDown: Promise<void>;
this.loading = false;
this.promise = undefined; constructor(options: ListLoaderOptions<T>) {
safeAssign(this, options);
if(done) { }
this.loaded = true;
this.scrollable.onScrolledBottom = null; public setTargets(previous: T[], next: T[], reverse: boolean) {
} else { this.previous = previous;
this.scrollable.checkForTriggers(); this.next = next;
} this.reverse = reverse;
}, () => { }
this.promise = undefined;
this.loading = false; 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 @@
/*
* 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;
// https://stackoverflow.com/a/58065241 // https://stackoverflow.com/a/58065241
export const isAppleMobile = (/iPad|iPhone|iPod/.test(navigator.platform) || export const isAppleMobile = (/iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) && (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 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; export const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;

10
src/lib/appManagers/appPhotosManager.ts

@ -137,9 +137,17 @@ export class AppPhotosManager {
photosResult.photos[idx] = this.savePhoto(photo, {type: 'profilePhoto', peerId: userId}); photosResult.photos[idx] = this.savePhoto(photo, {type: 'profilePhoto', peerId: userId});
return photo.id; return photo.id;
}); });
// ! WARNING !
if(maxId !== '0' && maxId) {
const idx = photoIds.indexOf(maxId);
if(idx !== -1) {
photoIds.splice(idx, 1);
}
}
return { return {
count: (photosResult as PhotosPhotos.photosPhotosSlice).count || photosResult.photos.length, count: (photosResult as PhotosPhotos.photosPhotosSlice).count || photoIds.length,
photos: photoIds photos: photoIds
}; };
}); });

2
src/lib/idb.ts

@ -141,7 +141,7 @@ export default class IDBStorage<T extends Database<any>> {
return Promise.reject(); return Promise.reject();
} }
} catch(error) { } catch(error) {
this.log.error('error opening db', error.message) this.log.error('error opening db', (error as Error).message);
this.storageIsAvailable = false; this.storageIsAvailable = false;
return Promise.reject(error); return Promise.reject(error);
} }

2
src/lib/mtproto/apiManager.ts

@ -275,7 +275,7 @@ export class ApiManager {
networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options); networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options);
} catch(error) { } catch(error) {
this.log('Get networker error', error, error.stack); this.log('Get networker error', error, (error as Error).stack);
delete this.gettingNetworkers[getKey]; delete this.gettingNetworkers[getKey];
throw error; throw error;
} }

2
src/lib/mtproto/authorizer.ts

@ -194,7 +194,7 @@ export class Authorizer {
rsaKeysManager.prepare(); rsaKeysManager.prepare();
deserializer = await promise; deserializer = await promise;
} catch(error) { } catch(error) {
this.log.error('req_pq error', error.message); this.log.error('req_pq error', (error as Error).message);
throw error; throw error;
} }

2
src/lib/mtproto/networker.ts

@ -1262,7 +1262,7 @@ export default class MTPNetworker {
try { try {
result.body = deserializer.fetchObject('Object', field + '[body]'); result.body = deserializer.fetchObject('Object', field + '[body]');
} catch(e) { } 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 = { result.body = {
_: 'parse_error', _: 'parse_error',
error: e error: e

5
src/pages/pageSignQR.ts

@ -5,6 +5,7 @@
*/ */
import type { DcId } from '../types'; import type { DcId } from '../types';
import type { ApiError } from '../lib/mtproto/apiManager';
import apiManager from '../lib/mtproto/mtprotoworker'; import apiManager from '../lib/mtproto/mtprotoworker';
import Page from './page'; import Page from './page';
import serverTimeManager from '../lib/mtproto/serverTimeManager'; import serverTimeManager from '../lib/mtproto/serverTimeManager';
@ -200,10 +201,10 @@ let onFirstMount = async() => {
await pause(diff > FETCH_INTERVAL ? 1e3 * FETCH_INTERVAL : 1e3 * diff | 0); await pause(diff > FETCH_INTERVAL ? 1e3 * FETCH_INTERVAL : 1e3 * diff | 0);
} }
} catch(err) { } catch(err) {
switch(err.type) { switch((err as ApiError).type) {
case 'SESSION_PASSWORD_NEEDED': case 'SESSION_PASSWORD_NEEDED':
console.warn('pageSignQR: SESSION_PASSWORD_NEEDED'); console.warn('pageSignQR: SESSION_PASSWORD_NEEDED');
err.handled = true; (err as ApiError).handled = true;
import('./pagePassword').then(m => m.default.mount()); import('./pagePassword').then(m => m.default.mount());
stop = true; stop = true;
cachedPromise = null; cachedPromise = null;

Loading…
Cancel
Save