Browse Source

Fix saving poll results to state

Release poll if it's deleted
Added thumbs for audio
Fix sending voice message to wrong chat
Fix missed avatars in messages from group
Fix clickable links in input
master
morethanwords 3 years ago
parent
commit
40a2e087d6
  1. 135
      src/components/appMediaPlaybackController.ts
  2. 276
      src/components/appMediaViewer.ts
  3. 16
      src/components/appSearchSuper..ts
  4. 208
      src/components/audio.ts
  5. 2
      src/components/avatar.ts
  6. 19
      src/components/chat/bubbles.ts
  7. 12
      src/components/chat/input.ts
  8. 7
      src/components/chat/selection.ts
  9. 7
      src/components/poll.ts
  10. 35
      src/components/preloader.ts
  11. 16
      src/components/wrappers.ts
  12. 8
      src/helpers/cancellablePromise.ts
  13. 26
      src/lib/appManagers/appMessagesManager.ts
  14. 60
      src/lib/appManagers/appPollsManager.ts
  15. 4
      src/lib/richtextprocessor.ts
  16. 390
      src/scss/partials/_audio.scss
  17. 8
      src/scss/partials/_chatBubble.scss
  18. 17
      src/scss/partials/_document.scss
  19. 1
      src/scss/partials/_poll.scss
  20. 4
      src/scss/style.scss

135
src/components/appMediaPlaybackController.ts

@ -11,6 +11,9 @@ import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromi
import { isSafari } from "../helpers/userAgent"; import { 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 type { SearchSuperContext } from "./appSearchSuper.";
import { copy, deepEqual } from "../helpers/object";
// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда // TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда
@ -18,9 +21,17 @@ import appDownloadManager from "../lib/appManagers/appDownloadManager";
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка // TODO: Safari: попробовать замаскировать подгрузку последнего чанка
// TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце // TODO: Safari: пофиксить момент, когда заканчивается песня и пытаешься включить её заново - прогресс сразу в конце
type MediaItem = {mid: number, peerId: number};
type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement; type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement;
type MediaType = 'voice' | 'audio' | 'round'; const SHOULD_USE_SAFARI_FIX = (() => {
try {
return isSafari && +navigator.userAgent.match(/ Version\/(\d+)/)[1] < 14;
} catch(err) {
return false;
}
})();
class AppMediaPlaybackController { class AppMediaPlaybackController {
private container: HTMLElement; private container: HTMLElement;
@ -29,7 +40,7 @@ class AppMediaPlaybackController {
[mid: string]: HTMLMediaElement [mid: string]: HTMLMediaElement
} }
} = {}; } = {};
private playingMedia: HTMLMediaElement; public playingMedia: HTMLMediaElement;
private waitingMediaForLoad: { private waitingMediaForLoad: {
[peerId: string]: { [peerId: string]: {
@ -38,11 +49,15 @@ class AppMediaPlaybackController {
} = {}; } = {};
public willBePlayedMedia: HTMLMediaElement; public willBePlayedMedia: HTMLMediaElement;
public searchContext: SearchSuperContext;
private currentPeerId: number; private currentPeerId: number;
private prevMid: number; private prevMid: number;
private nextMid: number; private nextMid: number;
private prev: 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;';
@ -63,6 +78,7 @@ class AppMediaPlaybackController {
//media.muted = true; //media.muted = true;
} }
media.dataset.peerId = '' + peerId;
media.dataset.mid = '' + mid; media.dataset.mid = '' + mid;
media.dataset.type = doc.type; media.dataset.type = doc.type;
@ -72,30 +88,11 @@ class AppMediaPlaybackController {
this.container.append(media); this.container.append(media);
media.addEventListener('playing', () => { media.addEventListener('play', this.onPlay);
this.currentPeerId = peerId;
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
if(this.playingMedia !== media) {
if(this.playingMedia && !this.playingMedia.paused) {
this.playingMedia.pause();
}
this.playingMedia = media;
this.loadSiblingsMedia(peerId, doc.type as MediaType, mid);
}
// audio_pause не успеет сработать без таймаута
setTimeout(() => {
rootScope.dispatchEvent('audio_play', {peerId, doc, mid});
}, 0);
});
media.addEventListener('pause', this.onPause); media.addEventListener('pause', this.onPause);
media.addEventListener('ended', this.onEnded); media.addEventListener('ended', this.onEnded);
const onError = (e: Event) => { /* const onError = (e: Event) => {
//console.log('appMediaPlaybackController: video onError', e); //console.log('appMediaPlaybackController: video onError', e);
if(this.nextMid === mid) { if(this.nextMid === mid) {
@ -107,7 +104,7 @@ class AppMediaPlaybackController {
} }
}; };
media.addEventListener('error', onError); media.addEventListener('error', onError); */
const deferred = deferredPromise<void>(); const deferred = deferredPromise<void>();
if(autoload) { if(autoload) {
@ -122,14 +119,16 @@ class AppMediaPlaybackController {
//console.log('will set media url:', media, doc, doc.type, doc.url); //console.log('will set media url:', media, doc, doc.type, doc.url);
((!doc.supportsStreaming ? appDocsManager.downloadDoc(doc) : Promise.resolve()) as Promise<any>).then(() => { ((!doc.supportsStreaming ? appDocsManager.downloadDoc(doc) : Promise.resolve()) as Promise<any>).then(() => {
if(doc.type === 'audio' && doc.supportsStreaming && isSafari) { if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) {
this.handleSafariStreamable(media); this.handleSafariStreamable(media);
} }
// setTimeout(() => {
const cacheContext = appDownloadManager.getCacheContext(doc); const cacheContext = appDownloadManager.getCacheContext(doc);
media.src = cacheContext.url; media.src = cacheContext.url;
// }, doc.supportsStreaming ? 500e3 : 0);
}); });
}, onError); }/* , onError */);
return storage[mid] = media; return storage[mid] = media;
} }
@ -163,7 +162,11 @@ class AppMediaPlaybackController {
} }
public resolveWaitingForLoadMedia(peerId: number, mid: number) { public resolveWaitingForLoadMedia(peerId: number, mid: number) {
const storage = this.waitingMediaForLoad[peerId] ?? (this.waitingMediaForLoad[peerId] = {}); const storage = this.waitingMediaForLoad[peerId];
if(!storage) {
return;
}
const promise = storage[mid]; const promise = storage[mid];
if(promise) { if(promise) {
promise.resolve(); promise.resolve();
@ -184,6 +187,37 @@ class AppMediaPlaybackController {
media.safariBuffering = value; media.safariBuffering = value;
} }
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 previousMedia = this.playingMedia;
if(previousMedia !== media) {
if(previousMedia) {
if(!previousMedia.paused) {
previousMedia.pause();
}
// reset media
previousMedia.currentTime = 0;
simulateEvent(previousMedia, 'ended');
}
this.playingMedia = media;
this.loadSiblingsMedia(peerId, mid);
}
// audio_pause не успеет сработать без таймаута
setTimeout(() => {
const message = appMessagesManager.getMessageByPeer(peerId, mid);
rootScope.dispatchEvent('audio_play', {peerId, doc: message.media.document, mid});
}, 0);
};
onPause = (e?: Event) => { onPause = (e?: Event) => {
/* const target = e.target as HTMLMediaElement; /* const target = e.target as HTMLMediaElement;
if(!isInDOM(target)) { if(!isInDOM(target)) {
@ -196,6 +230,10 @@ class AppMediaPlaybackController {
}; };
onEnded = (e?: Event) => { onEnded = (e?: Event) => {
if(!e.isTrusted) {
return;
}
this.onPause(e); this.onPause(e);
//console.log('on media end'); //console.log('on media end');
@ -215,35 +253,28 @@ class AppMediaPlaybackController {
} }
}; };
private loadSiblingsMedia(peerId: number, type: MediaType, mid: number) { private loadSiblingsMedia(offsetPeerId: number, offsetMid: number) {
const media = this.playingMedia; const {playingMedia, searchContext} = this;
this.prevMid = this.nextMid = 0; if(!searchContext) {
return;
}
return appMessagesManager.getSearch({ return appMessagesManager.getSearch({
peerId, ...searchContext,
query: '', maxId: offsetMid,
inputFilter: {
//_: type === 'audio' ? 'inputMessagesFilterMusic' : (type === 'round' ? 'inputMessagesFilterRoundVideo' : 'inputMessagesFilterVoice')
_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'
},
maxId: mid,
limit: 3, limit: 3,
backLimit: 2 backLimit: 2,
}).then(value => { }).then(value => {
if(this.playingMedia !== media) { if(this.playingMedia !== playingMedia || this.searchContext !== searchContext) {
return; return;
} }
for(const {mid: m} of value.history) { const idx = Math.max(0, value.history.findIndex(message => message.peerId === offsetPeerId && message.mid === offsetMid));
if(m > mid) { const prev = value.history.slice(Math.max(0, idx));
this.nextMid = m; const next = value.history.slice(0, idx);
} else if(m < mid) {
this.prevMid = m;
break;
}
}
[this.prevMid, this.nextMid].filter(Boolean).forEach(mid => { [this.prevMid, this.nextMid].filter(Boolean).forEach(mid => {
const peerId = searchContext.peerId;
const message = appMessagesManager.getMessageByPeer(peerId, mid); const message = appMessagesManager.getMessageByPeer(peerId, mid);
this.addMedia(peerId, message.media.document, mid, false); this.addMedia(peerId, message.media.document, mid, false);
}); });
@ -270,6 +301,16 @@ class AppMediaPlaybackController {
public willBePlayed(media: HTMLMediaElement) { public willBePlayed(media: HTMLMediaElement) {
this.willBePlayedMedia = media; this.willBePlayedMedia = media;
} }
public setSearchContext(context: SearchSuperContext) {
if(deepEqual(this.searchContext, context)) {
return;
}
this.searchContext = copy(context); // {_: type === 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice'}
this.prev.length = 0;
this.next.length = 0;
}
} }
const appMediaPlaybackController = new AppMediaPlaybackController(); const appMediaPlaybackController = new AppMediaPlaybackController();

276
src/components/appMediaViewer.ts

@ -53,6 +53,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";
const ZOOM_STEP = 0.5; const ZOOM_STEP = 0.5;
const ZOOM_INITIAL_VALUE = 1; const ZOOM_INITIAL_VALUE = 1;
@ -65,7 +66,148 @@ const ZOOM_MAX_VALUE = 4;
const MEDIA_VIEWER_CLASSNAME = 'media-viewer'; const MEDIA_VIEWER_CLASSNAME = 'media-viewer';
class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType extends string, TargetType extends object> { 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 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(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 currentMessageId = 0;
public searchContext: SearchSuperContext;
constructor(options: Omit<MediaQueueLoaderOptions<Item>, 'getLoadPromise'> = {}) {
super({
...options,
getLoadPromise: (older, anchor, loadCount) => {
const backLimit = older ? 0 : loadCount;
let maxId = this.currentMessageId;
if(anchor) maxId = anchor.mid;
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1);
return appMessagesManager.getSearch({
...this.searchContext,
maxId,
limit: backLimit ? 0 : loadCount,
backLimit
}).then(value => {
/* if(DEBUG) {
this.log('loaded more media by maxId:', maxId, value, older, this.reverse);
} */
if(value.next_rate) {
this.searchContext.nextRate = value.next_rate;
}
return value.history as any;
});
}
});
}
public setSearchContext(context: SearchSuperContext) {
this.searchContext = context;
}
}
class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType extends string, TargetType extends {element: HTMLElement}> {
protected wholeDiv: HTMLElement; protected wholeDiv: HTMLElement;
protected overlaysDiv: HTMLElement; protected overlaysDiv: HTMLElement;
protected author: {[k in 'container' | 'avatarEl' | 'nameEl' | 'date']: HTMLElement} = {} as any; protected author: {[k in 'container' | 'avatarEl' | 'nameEl' | 'date']: HTMLElement} = {} as any;
@ -78,7 +220,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
protected preloader: ProgressivePreloader = null; protected preloader: ProgressivePreloader = null;
protected preloaderStreamable: ProgressivePreloader = null; protected preloaderStreamable: ProgressivePreloader = null;
protected lastTarget: HTMLElement = null; protected target: TargetType = null;
protected prevTargets: TargetType[] = []; protected prevTargets: TargetType[] = [];
protected nextTargets: TargetType[] = []; protected nextTargets: TargetType[] = [];
//protected targetContainer: HTMLElement = null; //protected targetContainer: HTMLElement = null;
@ -254,6 +396,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
const target = this.prevTargets.pop(); const target = this.prevTargets.pop();
if(target) { if(target) {
this.nextTargets.unshift(this.target);
this.onPrevClick(target); this.onPrevClick(target);
} else { } else {
this.buttons.prev.style.display = 'none'; this.buttons.prev.style.display = 'none';
@ -266,6 +409,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
let target = this.nextTargets.shift(); let target = this.nextTargets.shift();
if(target) { if(target) {
this.prevTargets.push(this.target);
this.onNextClick(target); this.onNextClick(target);
} else { } else {
this.buttons.next.style.display = 'none'; this.buttons.next.style.display = 'none';
@ -418,9 +562,9 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
this.lazyLoadQueue.clear(); this.lazyLoadQueue.clear();
const promise = this.setMoverToTarget(this.lastTarget, true).then(({onAnimationEnd}) => onAnimationEnd); const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd);
this.lastTarget = null; this.target = null;
this.prevTargets = []; this.prevTargets = [];
this.nextTargets = []; this.nextTargets = [];
this.loadedAllMediaUp = this.loadedAllMediaDown = false; this.loadedAllMediaUp = this.loadedAllMediaDown = false;
@ -1209,7 +1353,7 @@ class AppMediaViewerBase<ContentAdditionType extends string, ButtonsAdditionType
const useContainerAsTarget = !target || target === container; const useContainerAsTarget = !target || target === container;
if(useContainerAsTarget) target = container; if(useContainerAsTarget) target = container;
this.lastTarget = target; this.target = {element: target} as any;
const tempId = ++this.tempId; const tempId = ++this.tempId;
if(this.needLoadMore) { if(this.needLoadMore) {
@ -1563,6 +1707,8 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
protected searchContext: SearchSuperContext; protected searchContext: SearchSuperContext;
protected btnMenuDelete: HTMLElement; protected btnMenuDelete: HTMLElement;
protected queueLoader: MediaSearchQueueLoader<AppMediaViewerTargetType>;
constructor() { constructor() {
super(['delete', 'forward']); super(['delete', 'forward']);
@ -1570,6 +1716,24 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
stub.classList.add(MEDIA_VIEWER_CLASSNAME + '-stub'); stub.classList.add(MEDIA_VIEWER_CLASSNAME + '-stub');
this.content.main.prepend(stub); */ this.content.main.prepend(stub); */
this.queueLoader = new MediaSearchQueueLoader({
prevTargets: this.prevTargets,
nextTargets: this.nextTargets,
generateItem: (item) => {
const isForDocument = this.searchContext.inputFilter._ === 'inputMessagesFilterDocument';
const {mid, peerId} = item;
const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(item);
if(!media) return;
if(isForDocument && !AppMediaViewer.isMediaCompatibleForDocumentViewer(media)) {
return;
}
return {element: null as HTMLElement, mid, peerId};
}
});
this.content.caption = document.createElement('div'); this.content.caption = document.createElement('div');
this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption'/* , 'media-viewer-stub' */); this.content.caption.classList.add(MEDIA_VIEWER_CLASSNAME + '-caption'/* , 'media-viewer-stub' */);
@ -1665,18 +1829,16 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
} */ } */
onPrevClick = (target: AppMediaViewerTargetType) => { onPrevClick = (target: AppMediaViewerTargetType) => {
this.nextTargets.unshift({element: this.lastTarget, mid: this.currentMessageId, peerId: this.currentPeerId});
this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, -1); this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, -1);
}; };
onNextClick = (target: AppMediaViewerTargetType) => { onNextClick = (target: AppMediaViewerTargetType) => {
this.prevTargets.push({element: this.lastTarget, mid: this.currentMessageId, peerId: this.currentPeerId});
this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, 1); this.openMedia(appMessagesManager.getMessageByPeer(target.peerId, target.mid), target.element, 1);
}; };
onDeleteClick = () => { onDeleteClick = () => {
new PopupDeleteMessages(this.currentPeerId, [this.currentMessageId], 'chat', () => { new PopupDeleteMessages(this.currentPeerId, [this.currentMessageId], 'chat', () => {
this.lastTarget = this.content.media; this.target = {element: this.content.media} as any;
this.close(); this.close();
}); });
}; };
@ -1732,94 +1894,7 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
// нет смысла делать проверку для reverse и loadMediaPromise // нет смысла делать проверку для reverse и loadMediaPromise
protected loadMoreMedia = (older = true) => { protected loadMoreMedia = (older = true) => {
//if(!older && this.reverse) return; return this.queueLoader.loadMoreMedia(older);
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;
const backLimit = older ? 0 : loadCount;
let maxId = this.currentMessageId;
let anchor: AppMediaViewerTargetType;
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];
}
if(anchor) maxId = anchor.mid;
if(!older) maxId = appMessagesIdsManager.incrementMessageId(maxId, 1);
const promise = appMessagesManager.getSearch({
peerId: this.searchContext.peerId,
query: this.searchContext.query,
inputFilter: {
_: this.searchContext.inputFilter
},
maxId,
limit: backLimit ? 0 : loadCount,
backLimit,
threadId: this.searchContext.threadId,
folderId: this.searchContext.folderId,
nextRate: this.searchContext.nextRate,
minDate: this.searchContext.minDate,
maxDate: this.searchContext.maxDate
}).then(value => {
if(DEBUG) {
this.log('loaded more media by maxId:', maxId, value, older, this.reverse);
}
if(value.next_rate) {
this.searchContext.nextRate = value.next_rate;
}
if(value.history.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 ? value.history.forEach.bind(value.history) : forEachReverse.bind(null, value.history);
const isForDocument = this.searchContext.inputFilter === 'inputMessagesFilterDocument';
method((message: Message.message) => {
const {mid, peerId} = message;
const media: MyPhoto | MyDocument = appMessagesManager.getMediaFromMessage(message);
if(!media) return;
if(isForDocument && !AppMediaViewer.isMediaCompatibleForDocumentViewer(media)) {
return;
}
const t = {element: null as HTMLElement, mid, peerId};
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.buttons.prev.classList.toggle('hide', !this.prevTargets.length);
this.buttons.next.classList.toggle('hide', !this.nextTargets.length);
}, () => {}).then(() => {
if(older) this.loadMediaPromiseDown = null;
else this.loadMediaPromiseUp = null;
});
if(older) this.loadMediaPromiseDown = promise;
else this.loadMediaPromiseUp = promise;
return promise;
}; };
private setCaption(message: Message.message) { private setCaption(message: Message.message) {
@ -1870,6 +1945,8 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
this.currentPeerId = message.peerId; this.currentPeerId = message.peerId;
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.peerId = message.peerId;
return promise; return promise;
} }
@ -1905,12 +1982,10 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe
} }
onPrevClick = (target: AppMediaViewerAvatarTargetType) => { onPrevClick = (target: AppMediaViewerAvatarTargetType) => {
this.nextTargets.unshift({element: this.lastTarget, photoId: this.currentPhotoId});
this.openMedia(target.photoId, target.element, -1); this.openMedia(target.photoId, target.element, -1);
}; };
onNextClick = (target: AppMediaViewerAvatarTargetType) => { onNextClick = (target: AppMediaViewerAvatarTargetType) => {
this.prevTargets.push({element: this.lastTarget, photoId: this.currentPhotoId});
this.openMedia(target.photoId, target.element, 1); this.openMedia(target.photoId, target.element, 1);
}; };
@ -1963,6 +2038,9 @@ export class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete', AppMe
this.currentPhotoId = photo.id; this.currentPhotoId = photo.id;
return super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets); const ret = super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets);
this.target.photoId = photo.id;
return ret;
} }
} }

16
src/components/appSearchSuper..ts

@ -56,7 +56,7 @@ import { attachClickEvent, simulateClickEvent } from "../helpers/dom/clickEvent"
export type SearchSuperType = MyInputMessagesFilter/* | 'members' */; export type SearchSuperType = MyInputMessagesFilter/* | 'members' */;
export type SearchSuperContext = { export type SearchSuperContext = {
peerId: number, peerId: number,
inputFilter: MyInputMessagesFilter, inputFilter: {_: MyInputMessagesFilter},
query?: string, query?: string,
maxId?: number, maxId?: number,
folderId?: number, folderId?: number,
@ -453,6 +453,7 @@ export default class AppSearchSuper {
const onMediaClick = (className: string, targetClassName: string, inputFilter: MyInputMessagesFilter, e: MouseEvent) => { const onMediaClick = (className: string, targetClassName: string, inputFilter: MyInputMessagesFilter, e: MouseEvent) => {
const target = findUpClassName(e.target as HTMLDivElement, className); const target = findUpClassName(e.target as HTMLDivElement, className);
if(!target) return;
const mid = +target.dataset.mid; const mid = +target.dataset.mid;
if(!mid) { if(!mid) {
@ -1240,16 +1241,11 @@ export default class AppSearchSuper {
//let loadCount = history.length ? 50 : 15; //let loadCount = history.length ? 50 : 15;
return this.loadPromises[type] = appMessagesManager.getSearch({ return this.loadPromises[type] = appMessagesManager.getSearch({
peerId: this.searchContext.peerId, ...this.searchContext,
query: this.searchContext.query,
inputFilter: {_: type}, inputFilter: {_: type},
maxId, maxId,
limit: loadCount, limit: loadCount,
nextRate: this.nextRates[type] ?? (this.nextRates[type] = 0), nextRate: this.nextRates[type] ?? (this.nextRates[type] = 0)
threadId: this.searchContext.threadId,
folderId: this.searchContext.folderId,
minDate: this.searchContext.minDate,
maxDate: this.searchContext.maxDate
}).then(value => { }).then(value => {
history.push(...value.history.map(m => ({mid: m.mid, peerId: m.peerId}))); history.push(...value.history.map(m => ({mid: m.mid, peerId: m.peerId})));
@ -1541,7 +1537,7 @@ export default class AppSearchSuper {
private copySearchContext(newInputFilter: MyInputMessagesFilter) { private copySearchContext(newInputFilter: MyInputMessagesFilter) {
const context = copy(this.searchContext); const context = copy(this.searchContext);
context.inputFilter = newInputFilter; context.inputFilter = {_: newInputFilter};
context.nextRate = this.nextRates[newInputFilter]; context.nextRate = this.nextRates[newInputFilter];
return context; return context;
} }
@ -1558,7 +1554,7 @@ export default class AppSearchSuper {
this.searchContext = { this.searchContext = {
peerId: peerId || 0, peerId: peerId || 0,
query: query || '', query: query || '',
inputFilter: this.mediaTab.inputFilter, inputFilter: {_: this.mediaTab.inputFilter},
threadId, threadId,
folderId, folderId,
minDate, minDate,

208
src/components/audio.ts

@ -6,7 +6,7 @@
import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
import { RichTextProcessor } from "../lib/richtextprocessor"; import { RichTextProcessor } from "../lib/richtextprocessor";
import { formatDate } 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 from "./appMediaPlaybackController";
@ -20,13 +20,14 @@ import { SearchSuperContext } from "./appSearchSuper.";
import { formatDateAccordingToToday } from "../helpers/date"; 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 { deferredPromise } from "../helpers/cancellablePromise";
import ListenerSetter, { Listener } from "../helpers/listenerSetter";
import noop from "../helpers/noop";
rootScope.addEventListener('messages_media_read', e => { rootScope.addEventListener('messages_media_read', ({mids, peerId}) => {
const {mids, peerId} = e;
mids.forEach(mid => { mids.forEach(mid => {
(Array.from(document.querySelectorAll('audio-element[message-id="' + mid + '"][peer-id="' + peerId + '"]')) as AudioElement[]).forEach(elem => { (Array.from(document.querySelectorAll('audio-element[message-id="' + mid + '"][peer-id="' + peerId + '"].is-unread')) as AudioElement[]).forEach(elem => {
//console.log('updating avatar:', elem);
elem.classList.remove('is-unread'); elem.classList.remove('is-unread');
}); });
}); });
@ -182,7 +183,7 @@ function wrapVoiceMessage(audioEl: AudioElement) {
start(); start();
} }
audioEl.addAudioListener('playing', () => { audioEl.addAudioListener('play', () => {
if(isUnread && !isOut && audioEl.classList.contains('is-unread')) { if(isUnread && !isOut && audioEl.classList.contains('is-unread')) {
audioEl.classList.remove('is-unread'); audioEl.classList.remove('is-unread');
appMessagesManager.readMessages(audioEl.message.peerId, [audioEl.message.mid]); appMessagesManager.readMessages(audioEl.message.peerId, [audioEl.message.mid]);
@ -319,7 +320,7 @@ function wrapAudio(audioEl: AudioElement) {
launched = false; launched = false;
}); });
const onPlaying = () => { const onPlay = () => {
if(!launched) { if(!launched) {
audioEl.classList.add('audio-show-progress'); audioEl.classList.add('audio-show-progress');
launched = true; launched = true;
@ -330,10 +331,10 @@ function wrapAudio(audioEl: AudioElement) {
} }
}; };
audioEl.addAudioListener('playing', onPlaying); audioEl.addAudioListener('play', onPlay);
if(!audioEl.audio.paused || audioEl.audio.currentTime > 0) { if(!audioEl.audio.paused || audioEl.audio.currentTime > 0) {
onPlaying(); onPlay();
} }
return () => { return () => {
@ -346,6 +347,15 @@ function wrapAudio(audioEl: AudioElement) {
return onLoad; return onLoad;
} }
function constructDownloadPreloader(tryAgainOnFail = true) {
const preloader = new ProgressivePreloader({cancelable: true, tryAgainOnFail});
preloader.construct();
preloader.circle.setAttributeNS(null, 'r', '23');
preloader.totalLength = 143.58203125;
return preloader;
}
export default class AudioElement extends HTMLElement { export default class AudioElement extends HTMLElement {
public audio: HTMLAudioElement; public audio: HTMLAudioElement;
public preloader: ProgressivePreloader; public preloader: ProgressivePreloader;
@ -355,20 +365,18 @@ export default class AudioElement extends HTMLElement {
public searchContext: SearchSuperContext; public searchContext: SearchSuperContext;
public showSender = false; public showSender = false;
public noAutoDownload: boolean; public noAutoDownload: boolean;
public lazyLoadQueue: LazyLoadQueue;
public loadPromises: Promise<any>[];
private attachedHandlers: {[name: string]: any[]} = {}; 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>;
constructor() {
super();
// элемент создан
}
public render() { public render() {
this.classList.add('audio'); this.classList.add('audio');
const doc = this.message.media.document || this.message.media.webpage.document; const doc: MyDocument = this.message.media.document || this.message.media.webpage.document;
const isRealVoice = doc.type === 'voice'; const isRealVoice = doc.type === 'voice';
const isVoice = !this.voiceAsMusic && isRealVoice; const isVoice = !this.voiceAsMusic && isRealVoice;
const isOutgoing = this.message.pFlags.is_outgoing; const isOutgoing = this.message.pFlags.is_outgoing;
@ -376,15 +384,21 @@ export default class AudioElement extends HTMLElement {
const durationStr = String(doc.duration | 0).toHHMMSS(); const durationStr = String(doc.duration | 0).toHHMMSS();
this.innerHTML = `<div class="audio-toggle audio-ico"> this.innerHTML = `
<div class="part one" x="0" y="0" fill="#fff"></div> <div class="audio-toggle audio-ico">
<div class="part two" x="0" y="0" fill="#fff"></div> <div class="audio-play-icon">
</div>`; <div class="part one" x="0" y="0" fill="#fff"></div>
<div class="part two" x="0" y="0" fill="#fff"></div>
</div>
</div>`;
const toggle = this.firstElementChild as HTMLElement;
const downloadDiv = document.createElement('div'); const downloadDiv = document.createElement('div');
downloadDiv.classList.add('audio-download'); downloadDiv.classList.add('audio-download');
if(uploading) { if(uploading) {
this.classList.add('is-outgoing');
this.append(downloadDiv); this.append(downloadDiv);
} }
@ -400,31 +414,37 @@ export default class AudioElement extends HTMLElement {
this.onTypeDisconnect = onTypeLoad(); this.onTypeDisconnect = onTypeLoad();
const toggle = this.querySelector('.audio-toggle') as HTMLDivElement;
const getTimeStr = () => String(audio.currentTime | 0).toHHMMSS() + (isVoice ? (' / ' + durationStr) : ''); const getTimeStr = () => String(audio.currentTime | 0).toHHMMSS() + (isVoice ? (' / ' + durationStr) : '');
const onPlaying = () => { const onPlay = () => {
audioTimeDiv.innerText = getTimeStr(); audioTimeDiv.innerText = getTimeStr();
toggle.classList.toggle('playing', !audio.paused); toggle.classList.toggle('playing', !audio.paused);
}; };
if(!audio.paused || (audio.currentTime > 0 && audio.currentTime !== audio.duration)) { if(!audio.paused || (audio.currentTime > 0 && audio.currentTime !== audio.duration)) {
onPlaying(); onPlay();
} }
attachClickEvent(toggle, (e) => { const togglePlay = (e?: Event, paused = audio.paused) => {
cancelEvent(e); e && cancelEvent(e);
if(audio.paused) audio.play().catch(() => {});
else audio.pause(); if(paused) {
}); appMediaPlaybackController.setSearchContext(this.searchContext);
audio.play().catch(() => {});
} else {
audio.pause();
}
};
attachClickEvent(toggle, (e) => togglePlay(e), {listenerSetter: this.listenerSetter});
this.addAudioListener('ended', () => { this.addAudioListener('ended', () => {
toggle.classList.remove('playing'); toggle.classList.remove('playing');
audioTimeDiv.innerText = durationStr;
}); });
this.addAudioListener('timeupdate', () => { this.addAudioListener('timeupdate', () => {
if(appMediaPlaybackController.isSafariBuffering(audio)) return; if(appMediaPlaybackController.playingMedia !== audio || appMediaPlaybackController.isSafariBuffering(audio)) return;
audioTimeDiv.innerText = getTimeStr(); audioTimeDiv.innerText = getTimeStr();
}); });
@ -432,7 +452,9 @@ export default class AudioElement extends HTMLElement {
toggle.classList.remove('playing'); toggle.classList.remove('playing');
}); });
this.addAudioListener('playing', onPlaying); this.addAudioListener('play', onPlay);
return togglePlay;
}; };
if(!isOutgoing) { if(!isOutgoing) {
@ -442,9 +464,7 @@ export default class AudioElement extends HTMLElement {
if(isRealVoice) { if(isRealVoice) {
if(!preloader) { if(!preloader) {
preloader = new ProgressivePreloader({ preloader = constructDownloadPreloader();
cancelable: true
});
} }
const load = () => { const load = () => {
@ -468,7 +488,7 @@ export default class AudioElement extends HTMLElement {
return {download}; return {download};
}; };
preloader.construct(); // preloader.construct();
preloader.setManual(); preloader.setManual();
preloader.attach(downloadDiv); preloader.attach(downloadDiv);
preloader.setDownloadFunction(load); preloader.setDownloadFunction(load);
@ -487,35 +507,89 @@ export default class AudioElement extends HTMLElement {
onLoad(false); onLoad(false);
} }
if(doc.thumbs) {
const imgs: HTMLImageElement[] = [];
const wrapped = wrapPhoto({
photo: doc,
message: null,
container: toggle,
boxWidth: 48,
boxHeight: 48,
loadPromises: this.loadPromises,
withoutPreloader: true,
lazyLoadQueue: this.lazyLoadQueue
});
toggle.style.width = toggle.style.height = '';
if(wrapped.images.thumb) imgs.push(wrapped.images.thumb);
if(wrapped.images.full) imgs.push(wrapped.images.full);
this.classList.add('audio-with-thumb');
imgs.forEach(img => img.classList.add('audio-thumb'));
}
//if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано //if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано
//onLoad(); //onLoad();
//} else { //} else {
const r = (e: Event) => { const r = (e: Event) => {
if(!this.audio) { if(!this.audio) {
onLoad(false); const togglePlay = onLoad(false);
} }
if(this.audio.src) { if(this.audio.src) {
return; return;
} }
//onLoad();
//cancelEvent(e);
appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid);
appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid);
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio
if(isSafari) {
this.audio.autoplay = true;
}
// togglePlay(undefined, true);
this.readyPromise = deferredPromise<void>();
if(this.audio.readyState >= 2) this.readyPromise.resolve();
else {
this.addAudioListener('canplay', () => this.readyPromise.resolve(), {once: true});
}
if(!preloader) { if(!preloader) {
if(doc.supportsStreaming) { if(doc.supportsStreaming) {
preloader = new ProgressivePreloader({ this.classList.add('corner-download');
cancelable: false
}); let pauseListener: Listener;
const onPlay = () => {
const preloader = constructDownloadPreloader(false);
const deferred = deferredPromise<void>();
deferred.notifyAll({done: 75, total: 100});
deferred.catch(() => {
this.audio.pause();
appMediaPlaybackController.willBePlayed(undefined);
});
deferred.cancel = () => {
deferred.cancel = noop;
const err = new Error();
(err as any).type = 'CANCELED';
deferred.reject(err);
};
preloader.attach(downloadDiv, false, deferred);
pauseListener = this.addAudioListener('pause', () => {
deferred.cancel();
}, {once: true}) as any;
};
preloader.attach(downloadDiv, false); /* if(!this.audio.paused) {
} else { onPlay();
preloader = new ProgressivePreloader({ } */
cancelable: true
});
const playListener: any = this.addAudioListener('play', onPlay);
this.readyPromise.then(() => {
this.listenerSetter.remove(playListener);
this.listenerSetter.remove(pauseListener);
});
} else {
const load = () => { const load = () => {
const download = getDownloadPromise(); const download = getDownloadPromise();
preloader.attach(downloadDiv, false, download); preloader.attach(downloadDiv, false, download);
@ -527,17 +601,9 @@ export default class AudioElement extends HTMLElement {
} }
} }
if(isSafari) {
this.audio.autoplay = true;
this.audio.play().catch(() => {});
}
this.append(downloadDiv); this.append(downloadDiv);
new Promise<void>((resolve) => { this.readyPromise.then(() => {
if(this.audio.readyState >= 2) resolve();
else this.addAudioListener('canplay', resolve);
}).then(() => {
downloadDiv.classList.add('downloaded'); downloadDiv.classList.add('downloaded');
setTimeout(() => { setTimeout(() => {
downloadDiv.remove(); downloadDiv.remove();
@ -547,14 +613,14 @@ export default class AudioElement extends HTMLElement {
// release loaded audio // release loaded audio
if(appMediaPlaybackController.willBePlayedMedia === this.audio) { if(appMediaPlaybackController.willBePlayedMedia === this.audio) {
this.audio.play(); this.audio.play();
appMediaPlaybackController.willBePlayedMedia = null; appMediaPlaybackController.willBePlayed(undefined);
} }
//}, 10e3); //}, 10e3);
}); });
}; };
if(!this.audio?.src) { if(!this.audio?.src) {
attachClickEvent(this, r, {once: true, capture: true, passive: false}); attachClickEvent(toggle, r, {once: true, capture: true, passive: false});
} }
//} //}
} }
@ -564,15 +630,8 @@ export default class AudioElement extends HTMLElement {
} }
} }
/* connectedCallback() { get addAudioListener() {
// браузер вызывает этот метод при добавлении элемента в документ return this.listenerSetter.add(this.audio);
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
} */
public addAudioListener(name: string, callback: any) {
if(!this.attachedHandlers[name]) this.attachedHandlers[name] = [];
this.attachedHandlers[name].push(callback);
this.audio.addEventListener(name, callback);
} }
disconnectedCallback() { disconnectedCallback() {
@ -580,21 +639,18 @@ export default class AudioElement extends HTMLElement {
return; return;
} }
// браузер вызывает этот метод при удалении элемента из документа
// (может вызываться много раз, если элемент многократно добавляется/удаляется)
if(this.onTypeDisconnect) { if(this.onTypeDisconnect) {
this.onTypeDisconnect(); this.onTypeDisconnect();
this.onTypeDisconnect = null; this.onTypeDisconnect = null;
} }
for(let name in this.attachedHandlers) { if(this.readyPromise) {
for(let callback of this.attachedHandlers[name]) { this.readyPromise.reject();
this.audio.removeEventListener(name, callback);
}
delete this.attachedHandlers[name];
} }
this.listenerSetter.removeAll();
this.listenerSetter = null;
this.preloader = null; this.preloader = null;
} }
} }

2
src/components/avatar.ts

@ -82,7 +82,7 @@ export async function openAvatarViewer(target: HTMLElement, peerId: number, midd
new AppMediaViewer() new AppMediaViewer()
.setSearchContext({ .setSearchContext({
peerId, peerId,
inputFilter, inputFilter: {_: inputFilter},
}) })
.openMedia(message, getTarget(), undefined, undefined, prevTargets ? f(prevTargets) : undefined, nextTargets ? f(nextTargets) : undefined); .openMedia(message, getTarget(), undefined, undefined, prevTargets ? f(prevTargets) : undefined, nextTargets ? f(nextTargets) : undefined);

19
src/components/chat/bubbles.ts

@ -944,7 +944,7 @@ export default class ChatBubbles {
} }
//this.log('chatInner click:', target); //this.log('chatInner click:', target);
const isVideoComponentElement = target.tagName === 'SPAN'; const isVideoComponentElement = target.tagName === 'SPAN' && !target.classList.contains('emoji');
/* if(isVideoComponentElement) { /* if(isVideoComponentElement) {
const video = target.parentElement.querySelector('video') as HTMLElement; const video = target.parentElement.querySelector('video') as HTMLElement;
if(video) { if(video) {
@ -1054,7 +1054,7 @@ export default class ChatBubbles {
.setSearchContext({ .setSearchContext({
threadId: this.chat.threadId, threadId: this.chat.threadId,
peerId: this.peerId, peerId: this.peerId,
inputFilter: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo' inputFilter: {_: documentDiv ? 'inputMessagesFilterDocument' : 'inputMessagesFilterPhotoVideo'}
}) })
.openMedia(message, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1)); .openMedia(message, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1));
@ -2775,6 +2775,11 @@ export default class ChatBubbles {
group: CHAT_ANIMATION_GROUP, group: CHAT_ANIMATION_GROUP,
loadPromises, loadPromises,
noAutoDownload: this.chat.noAutoDownloadMedia, noAutoDownload: this.chat.noAutoDownloadMedia,
searchContext: isRound ? {
peerId: this.peerId,
inputFilter: {_: 'inputMessagesFilterRoundVoice'},
threadId: this.chat.threadId
} : undefined
}); });
} }
} else { } else {
@ -2786,7 +2791,12 @@ export default class ChatBubbles {
chat: this.chat, chat: this.chat,
loadPromises, loadPromises,
noAutoDownload: this.chat.noAutoDownloadMedia, noAutoDownload: this.chat.noAutoDownloadMedia,
lazyLoadQueue: this.lazyLoadQueue lazyLoadQueue: this.lazyLoadQueue,
searchContext: doc.type === 'voice' || doc.type === 'audio' ? {
peerId: this.peerId,
inputFilter: {_: doc.type === 'voice' ? 'inputMessagesFilterRoundVoice' : 'inputMessagesFilterMusic'},
threadId: this.chat.threadId
} : undefined
}); });
if(newNameContainer) { if(newNameContainer) {
@ -2879,7 +2889,8 @@ export default class ChatBubbles {
let savedFrom = ''; let savedFrom = '';
const needName = ((peerId < 0 && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId; // const needName = ((peerId < 0 && (peerId !== message.fromId || our)) && message.fromId !== rootScope.myId) || message.viaBotId;
const needName = (message.fromId !== rootScope.myId && peerId < 0 && !this.appPeersManager.isBroadcast(peerId)) || message.viaBotId;
if(needName || message.fwd_from || message.reply_to_mid) { // chat if(needName || message.fwd_from || message.reply_to_mid) { // chat
let title: HTMLElement | DocumentFragment; let title: HTMLElement | DocumentFragment;

12
src/components/chat/input.ts

@ -388,7 +388,7 @@ export default class ChatInput {
onClick: () => { onClick: () => {
new PopupCreatePoll(this.chat).show(); new PopupCreatePoll(this.chat).show();
}, },
verify: (peerId, threadId) => this.appMessagesManager.canSendToPeer(peerId, threadId, 'send_polls') verify: (peerId, threadId) => this.appMessagesManager.canSendToPeer(peerId, threadId, 'send_polls') && peerId < 0
}]; }];
this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons); this.attachMenu = ButtonMenuToggle({noRipple: true, listenerSetter: this.listenerSetter}, 'top-left', this.attachMenuButtons);
@ -577,6 +577,9 @@ export default class ChatInput {
this.recorder.ondataavailable = (typedArray: Uint8Array) => { this.recorder.ondataavailable = (typedArray: Uint8Array) => {
if(this.recordCanceled) return; if(this.recordCanceled) return;
const {peerId, threadId} = this.chat;
const replyToMsgId = this.replyToMsgId;
const duration = (Date.now() - this.recordStartTime) / 1000 | 0; const duration = (Date.now() - this.recordStartTime) / 1000 | 0;
const dataBlob = new Blob([typedArray], {type: 'audio/ogg'}); const dataBlob = new Blob([typedArray], {type: 'audio/ogg'});
/* const fileName = new Date().toISOString() + ".opus"; /* const fileName = new Date().toISOString() + ".opus";
@ -588,7 +591,6 @@ export default class ChatInput {
opusDecodeController.setKeepAlive(false); opusDecodeController.setKeepAlive(false);
let peerId = this.chat.peerId;
// тут objectURL ставится уже с audio/wav // тут objectURL ставится уже с audio/wav
this.appMessagesManager.sendFile(peerId, dataBlob, { this.appMessagesManager.sendFile(peerId, dataBlob, {
isVoiceMessage: true, isVoiceMessage: true,
@ -596,8 +598,8 @@ export default class ChatInput {
duration, duration,
waveform: result.waveform, waveform: result.waveform,
objectURL: result.url, objectURL: result.url,
replyToMsgId: this.replyToMsgId, replyToMsgId,
threadId: this.chat.threadId, threadId,
clearDraft: true clearDraft: true
}); });
@ -1464,7 +1466,7 @@ export default class ChatInput {
this.recordTimeEl.innerText = formatted; this.recordTimeEl.innerText = formatted;
window.requestAnimationFrame(r); fastRaf(r);
}; };
r(); r();

7
src/components/chat/selection.ts

@ -516,7 +516,7 @@ export class SearchSelection extends AppSelection {
appMessagesManager, appMessagesManager,
listenElement: searchSuper.container, listenElement: searchSuper.container,
listenerSetter: new ListenerSetter(), listenerSetter: new ListenerSetter(),
verifyTarget: (e, target) => !!target, verifyTarget: (e, target) => !!target && this.isSelecting,
getElementFromTarget: (target) => findUpClassName(target, 'search-super-item'), getElementFromTarget: (target) => findUpClassName(target, 'search-super-item'),
targetLookupClassName: 'search-super-item', targetLookupClassName: 'search-super-item',
lookupBetweenParentClassName: 'tabs-tab', lookupBetweenParentClassName: 'tabs-tab',
@ -560,6 +560,11 @@ export class SearchSelection extends AppSelection {
this.updateElementSelection(element, this.isMidSelected(peerId, mid)); this.updateElementSelection(element, this.isMidSelected(peerId, mid));
}; };
public toggleByMid = (peerId: number, mid: number) => {
const element = this.searchSuper.mediaTab.contentTab.querySelector(`.search-super-item[data-peer-id="${peerId}"][data-mid="${mid}"]`) as HTMLElement;
this.toggleByElement(element);
};
protected onUpdateContainer = (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => { protected onUpdateContainer = (cantForward: boolean, cantDelete: boolean, cantSend: boolean) => {
const length = this.length(); const length = this.length();
replaceContent(this.selectionCountEl, i18n('messages', [length])); replaceContent(this.selectionCountEl, i18n('messages', [length]));

7
src/components/poll.ts

@ -430,9 +430,12 @@ export default class PollElement extends HTMLElement {
// const width = mediaSizes.active.poll.width; // const width = mediaSizes.active.poll.width;
// this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left // this.maxLength = width + tailLength + this.maxOffset + -13.7; // 13 - position left
if(poll.chosenIndexes.length || this.isClosed) { const canVote = !(poll.chosenIndexes.length || this.isClosed);
if(!canVote || this.isPublic) {
this.performResults(results, poll.chosenIndexes, false); this.performResults(results, poll.chosenIndexes, false);
} else if(!this.isClosed) { }
if(canVote) {
this.setVotersCount(results); this.setVotersCount(results);
attachClickEvent(this, this.clickHandler); attachClickEvent(this, this.clickHandler);
} }

35
src/components/preloader.ts

@ -16,7 +16,7 @@ const TRANSITION_TIME = 200;
export default class ProgressivePreloader { export default class ProgressivePreloader {
public preloader: HTMLDivElement; public preloader: HTMLDivElement;
private circle: SVGCircleElement; public circle: SVGCircleElement;
private cancelSvg: SVGSVGElement; private cancelSvg: SVGSVGElement;
private downloadSvg: HTMLElement; private downloadSvg: HTMLElement;
@ -33,7 +33,7 @@ export default class ProgressivePreloader {
public loadFunc: (e?: Event) => {download: CancellablePromise<any>}; public loadFunc: (e?: Event) => {download: CancellablePromise<any>};
private totalLength: number; public totalLength: number;
constructor(options?: Partial<{ constructor(options?: Partial<{
isUpload: ProgressivePreloader['isUpload'], isUpload: ProgressivePreloader['isUpload'],
@ -85,6 +85,12 @@ export default class ProgressivePreloader {
</svg> </svg>
</div>`; </div>`;
if(this.streamable) {
this.totalLength = 118.61124420166016;
} else {
this.totalLength = 149.82473754882812;
}
if(this.cancelable) { if(this.cancelable) {
this.preloader.innerHTML += ` this.preloader.innerHTML += `
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 24 24">
@ -147,7 +153,7 @@ export default class ProgressivePreloader {
const startTime = Date.now(); const startTime = Date.now();
const onEnd = (err: Error) => { const onEnd = (err: Error) => {
promise.notify = null; promise.notify = promise.notifyAll = null;
if(tempId !== this.tempId) { if(tempId !== this.tempId) {
return; return;
@ -205,10 +211,6 @@ export default class ProgressivePreloader {
} }
public attach(elem: Element, reset = false, promise?: CancellablePromise<any>) { public attach(elem: Element, reset = false, promise?: CancellablePromise<any>) {
if(promise/* && false */) {
this.attachPromise(promise);
}
//return; //return;
this.detached = false; this.detached = false;
@ -224,19 +226,24 @@ export default class ProgressivePreloader {
this.preloader.classList.remove('manual'); this.preloader.classList.remove('manual');
} }
const useRafs = isInDOM(this.preloader) ? 1 : 2;
if(this.preloader.parentElement !== elem) { if(this.preloader.parentElement !== elem) {
elem[this.attachMethod](this.preloader); elem[this.attachMethod](this.preloader);
} }
fastRaf(() => { if(promise/* && false */) {
this.attachPromise(promise);
}
// fastRaf(() => {
//console.log('[PP]: attach after rAF', this.detached, performance.now()); //console.log('[PP]: attach after rAF', this.detached, performance.now());
if(this.detached) { // if(this.detached) {
return; // return;
} // }
SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME); SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME, undefined, useRafs);
}); // });
if(this.cancelable && reset) { if(this.cancelable && reset) {
this.setProgress(0); this.setProgress(0);
@ -272,7 +279,7 @@ export default class ProgressivePreloader {
} }
public setProgress(percents: number) { public setProgress(percents: number) {
if(!isInDOM(this.circle)) { if(!this.totalLength && !isInDOM(this.circle)) {
return; return;
} }

16
src/components/wrappers.ts

@ -70,7 +70,7 @@ mediaSizes.addEventListener('changeScreen', (from, to) => {
} }
}); });
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, noAutoDownload, size}: { export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, noAutoDownload, size, searchContext}: {
doc: MyDocument, doc: MyDocument,
container?: HTMLElement, container?: HTMLElement,
message?: any, message?: any,
@ -87,7 +87,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
withoutPreloader?: boolean, withoutPreloader?: boolean,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
noAutoDownload?: boolean, noAutoDownload?: boolean,
size?: PhotoSize size?: PhotoSize,
searchContext?: SearchSuperContext
}) { }) {
const isAlbumItem = !(boxWidth && boxHeight); const isAlbumItem = !(boxWidth && boxHeight);
const canAutoplay = (doc.type !== 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem)) const canAutoplay = (doc.type !== 'video' || (doc.size <= MAX_VIDEO_AUTOPLAY_SIZE && !isAlbumItem))
@ -276,6 +277,7 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
} */ } */
if(globalVideo.paused) { if(globalVideo.paused) {
appMediaPlaybackController.setSearchContext(searchContext);
globalVideo.play(); globalVideo.play();
} else { } else {
globalVideo.pause(); globalVideo.pause();
@ -525,6 +527,8 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
audioElement.withTime = withTime; audioElement.withTime = withTime;
audioElement.message = message; audioElement.message = message;
audioElement.noAutoDownload = noAutoDownload; audioElement.noAutoDownload = noAutoDownload;
audioElement.lazyLoadQueue = lazyLoadQueue;
audioElement.loadPromises = loadPromises;
if(voiceAsMusic) audioElement.voiceAsMusic = voiceAsMusic; if(voiceAsMusic) audioElement.voiceAsMusic = voiceAsMusic;
if(searchContext) audioElement.searchContext = searchContext; if(searchContext) audioElement.searchContext = searchContext;
@ -1588,7 +1592,7 @@ export function wrapAlbum({groupId, attachmentDiv, middleware, uploading, lazyLo
}); });
} }
export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue}: { export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble, messageDiv, chat, loadPromises, noAutoDownload, lazyLoadQueue, searchContext}: {
albumMustBeRenderedFull: boolean, albumMustBeRenderedFull: boolean,
message: any, message: any,
messageDiv: HTMLElement, messageDiv: HTMLElement,
@ -1597,7 +1601,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
chat: Chat, chat: Chat,
loadPromises?: Promise<any>[], loadPromises?: Promise<any>[],
noAutoDownload?: boolean, noAutoDownload?: boolean,
lazyLoadQueue?: LazyLoadQueue lazyLoadQueue?: LazyLoadQueue,
searchContext?: SearchSuperContext
}) { }) {
let nameContainer: HTMLElement; let nameContainer: HTMLElement;
const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid]; const mids = albumMustBeRenderedFull ? chat.getMidsByMid(message.mid) : [message.mid];
@ -1611,7 +1616,8 @@ export function wrapGroupedDocuments({albumMustBeRenderedFull, message, bubble,
message, message,
loadPromises, loadPromises,
noAutoDownload, noAutoDownload,
lazyLoadQueue lazyLoadQueue,
searchContext
}); });
const container = document.createElement('div'); const container = document.createElement('div');

8
src/helpers/cancellablePromise.ts

@ -30,7 +30,6 @@ export function deferredPromise<T>() {
deferredHelper.listeners.forEach((callback: any) => callback(...args)); deferredHelper.listeners.forEach((callback: any) => callback(...args));
}, },
lastNotify: undefined,
listeners: [], listeners: [],
addNotifyListener: (callback: (...args: any[]) => void) => { addNotifyListener: (callback: (...args: any[]) => void) => {
if(deferredHelper.lastNotify) { if(deferredHelper.lastNotify) {
@ -43,14 +42,14 @@ export function deferredPromise<T>() {
let deferred: CancellablePromise<T> = new Promise<T>((resolve, reject) => { let deferred: CancellablePromise<T> = new Promise<T>((resolve, reject) => {
deferredHelper.resolve = (value: T) => { deferredHelper.resolve = (value: T) => {
if(deferred.isFulfilled) return; if(deferred.isFulfilled || deferred.isRejected) return;
deferred.isFulfilled = true; deferred.isFulfilled = true;
resolve(value); resolve(value);
}; };
deferredHelper.reject = (...args: any[]) => { deferredHelper.reject = (...args: any[]) => {
if(deferred.isRejected) return; if(deferred.isRejected || deferred.isFulfilled) return;
deferred.isRejected = true; deferred.isRejected = true;
reject(...args); reject(...args);
@ -64,9 +63,8 @@ export function deferredPromise<T>() {
}; */ }; */
deferred.finally(() => { deferred.finally(() => {
deferred.notify = null; deferred.notify = deferred.notifyAll = deferred.lastNotify = null;
deferred.listeners.length = 0; deferred.listeners.length = 0;
deferred.lastNotify = null;
if(deferred.cancel) { if(deferred.cancel) {
deferred.cancel = () => {}; deferred.cancel = () => {};

26
src/lib/appManagers/appMessagesManager.ts

@ -321,6 +321,18 @@ export class AppMessagesManager {
} }
}); });
rootScope.addEventListener('poll_update', ({poll}) => {
const set = appPollsManager.pollToMessages[poll.id];
if(set) {
for(const key of set) {
const [peerId, mid] = key.split('_');
const message = this.getMessageByPeer(+peerId, +mid);
this.setDialogToStateIfMessageIsTop(message);
}
}
});
appStateManager.getState().then(state => { appStateManager.getState().then(state => {
if(state.maxSeenMsgId) { if(state.maxSeenMsgId) {
this.maxSeenId = state.maxSeenMsgId; this.maxSeenId = state.maxSeenMsgId;
@ -2359,7 +2371,9 @@ export class AppMessagesManager {
break; break;
case 'messageMediaPoll': case 'messageMediaPoll':
message.media.poll = appPollsManager.savePoll(message.media.poll, message.media.results); const result = appPollsManager.savePoll(message.media.poll, message.media.results, message);
message.media.poll = result.poll;
message.media.results = result.results;
break; break;
case 'messageMediaDocument': case 'messageMediaDocument':
if(message.media.ttl_seconds) { if(message.media.ttl_seconds) {
@ -2616,7 +2630,7 @@ export class AppMessagesManager {
break; break;
} }
case 'messageMediaDocument': { case 'messageMediaDocument': {
const document = media.document; const document = media.document as MyDocument;
if(document.type === 'video') { if(document.type === 'video') {
addPart('AttachVideo', undefined, message.message); addPart('AttachVideo', undefined, message.message);
@ -2630,6 +2644,10 @@ export class AppMessagesManager {
addPart(undefined, ((plain ? document.stickerEmojiRaw : document.stickerEmoji) || '')); addPart(undefined, ((plain ? document.stickerEmojiRaw : document.stickerEmoji) || ''));
addPart('AttachSticker'); addPart('AttachSticker');
text = ''; text = '';
} else if(document.type === 'audio') {
const attribute = document.attributes.find(attribute => attribute._ === 'documentAttributeAudio' && (attribute.title || attribute.performer)) as DocumentAttribute.documentAttributeAudio;
const f = '🎵' + ' ' + (attribute ? [attribute.title, attribute.performer].filter(Boolean).join(' - ') : document.file_name);
addPart(undefined, plain ? f : RichTextProcessor.wrapEmojiText(f), message.message);
} else { } else {
addPart(undefined, plain ? document.file_name : RichTextProcessor.wrapEmojiText(document.file_name), message.message); addPart(undefined, plain ? document.file_name : RichTextProcessor.wrapEmojiText(document.file_name), message.message);
} }
@ -5353,6 +5371,10 @@ export class AppMessagesManager {
const messageKey = appWebPagesManager.getMessageKeyForPendingWebPage(message.peerId, message.mid, isScheduled); const messageKey = appWebPagesManager.getMessageKeyForPendingWebPage(message.peerId, message.mid, isScheduled);
appWebPagesManager.deleteWebPageFromPending(media.webpage, messageKey); appWebPagesManager.deleteWebPageFromPending(media.webpage, messageKey);
} }
if((media as MessageMedia.messageMediaPoll).poll) {
appPollsManager.updatePollToMessage(message as Message.message, false);
}
} }
} }

60
src/lib/appManagers/appPollsManager.ts

@ -6,7 +6,7 @@
import { MOUNT_CLASS_TO } from "../../config/debug"; import { MOUNT_CLASS_TO } from "../../config/debug";
import { copy } from "../../helpers/object"; import { copy } from "../../helpers/object";
import { InputMedia, MessageEntity } from "../../layer"; import { InputMedia, Message, MessageEntity, MessageMedia } from "../../layer";
import { logger, LogTypes } from "../logger"; import { logger, LogTypes } from "../logger";
import apiManager from "../mtproto/mtprotoworker"; import apiManager from "../mtproto/mtprotoworker";
import { RichTextProcessor } from "../richtextprocessor"; import { RichTextProcessor } from "../richtextprocessor";
@ -80,6 +80,7 @@ export type Poll = {
export class AppPollsManager { export class AppPollsManager {
public polls: {[id: string]: Poll} = {}; public polls: {[id: string]: Poll} = {};
public results: {[id: string]: PollResults} = {}; public results: {[id: string]: PollResults} = {};
public pollToMessages: {[id: string]: Set<string>} = {};
private log = logger('POLLS', LogTypes.Error); private log = logger('POLLS', LogTypes.Error);
@ -93,27 +94,35 @@ export class AppPollsManager {
return; return;
} }
poll = this.savePoll(poll, update.results as any); let results = update.results;
rootScope.dispatchEvent('poll_update', {poll, results: update.results as any}); const ret = this.savePoll(poll, results as any);
poll = ret.poll;
results = ret.results;
rootScope.dispatchEvent('poll_update', {poll, results: results as any});
} }
}); });
} }
public savePoll(poll: Poll, results: PollResults) { public savePoll(poll: Poll, results: PollResults, message?: Message.message) {
if(message) {
this.updatePollToMessage(message, true);
}
const id = poll.id; const id = poll.id;
if(this.polls[id]) { if(this.polls[id]) {
poll = Object.assign(this.polls[id], poll); poll = Object.assign(this.polls[id], poll);
this.saveResults(poll, results); results = this.saveResults(poll, results);
return poll; } else {
} this.polls[id] = poll;
this.polls[id] = poll; poll.rQuestion = RichTextProcessor.wrapEmojiText(poll.question);
poll.rReply = RichTextProcessor.wrapEmojiText('📊') + ' ' + (poll.rQuestion || 'poll');
poll.chosenIndexes = [];
results = this.saveResults(poll, results);
}
poll.rQuestion = RichTextProcessor.wrapEmojiText(poll.question); return {poll, results};
poll.rReply = RichTextProcessor.wrapEmojiText('📊') + ' ' + (poll.rQuestion || 'poll');
poll.chosenIndexes = [];
this.saveResults(poll, results);
return poll;
} }
public saveResults(poll: Poll, results: PollResults) { public saveResults(poll: Poll, results: PollResults) {
@ -133,6 +142,8 @@ export class AppPollsManager {
}); });
} }
} }
return results;
} }
public getPoll(pollId: string): {poll: Poll, results: PollResults} { public getPoll(pollId: string): {poll: Poll, results: PollResults} {
@ -162,6 +173,29 @@ export class AppPollsManager {
}; };
} }
public updatePollToMessage(message: Message.message, add: boolean) {
const {id} = (message.media as MessageMedia.messageMediaPoll).poll;
let set = this.pollToMessages[id];
if(!add && !set) {
return;
}
if(!set) {
set = this.pollToMessages[id] = new Set();
}
const key = message.peerId + '_' + message.mid;
if(add) set.add(key);
else set.delete(key);
if(!add && !set.size) {
delete this.polls[id];
delete this.results[id];
delete this.pollToMessages[id];
}
}
public sendVote(message: any, optionIds: number[]): Promise<void> { public sendVote(message: any, optionIds: number[]): Promise<void> {
const poll: Poll = message.media.poll; const poll: Poll = message.media.poll;

4
src/lib/richtextprocessor.ts

@ -640,6 +640,10 @@ namespace RichTextProcessor {
onclick = 'showMaskedAlert'; onclick = 'showMaskedAlert';
} }
if(options.wrappingDraft) {
onclick = undefined;
}
const href = (currentContext || typeof electronHelpers === 'undefined') const href = (currentContext || typeof electronHelpers === 'undefined')
? encodeEntities(url) ? encodeEntities(url)
: `javascript:electronHelpers.openExternal('${encodeEntities(url)}');`; : `javascript:electronHelpers.openExternal('${encodeEntities(url)}');`;

390
src/scss/partials/_audio.scss

@ -8,30 +8,65 @@
position: relative; position: relative;
padding-left: 67px; padding-left: 67px;
overflow: visible!important; overflow: visible!important;
height: 54px; height: 3.375rem;
user-select: none; user-select: none;
/* @include respond-to(handhelds) { /* @include respond-to(handhelds) {
padding-left: 45px; padding-left: 45px;
} */ } */
&-toggle, &-download { &-toggle,
&-download {
overflow: hidden; overflow: hidden;
border-radius: 50%; border-radius: 50%;
background-color: var(--primary-color); background-color: var(--primary-color);
align-items: center; align-items: center;
} }
&-toggle { &.corner-download {
.audio-download {
width: 1.375rem;
height: 1.375rem;
margin: 2rem 2rem 0;
background: none;
display: flex !important;
top: 0;
}
.preloader-container {
border-radius: inherit;
background-color: var(--primary-color);
}
.preloader-path-new {
stroke-width: .25rem;
}
}
&-play-icon {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: rotate(-119deg); transform: rotate(-119deg);
overflow: hidden;
max-width: 100%;
max-height: 100%;
border-radius: inherit;
@include animation-level(2) { @include animation-level(2) {
transition: transform .25s ease-in-out; transition: transform .25s ease-in-out;
} }
}
&-toggle {
.part { .part {
position: absolute; position: absolute;
background-color: white; background-color: white;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@include animation-level(2) { @include animation-level(2) {
transition: clip-path .25s ease-in-out; transition: clip-path .25s ease-in-out;
@ -185,160 +220,160 @@
); );
} }
} }
}
&-toggle.playing { &.playing .audio-play-icon {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
&-toggle:not(.playing) { &:not(.playing) {
.part { .part {
@include respond-to(not-handhelds) { @include respond-to(not-handhelds) {
height: 136px; height: 136px;
width: 136px; width: 136px;
} }
@include respond-to(handhelds) { @include respond-to(handhelds) {
height: 92px; height: 92px;
width: 92px; width: 92px;
} }
&.one { &.one {
clip-path: polygon( clip-path: polygon(
43.77666% 55.85251%, 43.77666% 55.85251%,
43.77874% 55.46331%, 43.77874% 55.46331%,
43.7795% 55.09177%, 43.7795% 55.09177%,
43.77934% 54.74844%, 43.77934% 54.74844%,
43.77855% 54.44389%, 43.77855% 54.44389%,
43.77741% 54.18863%, 43.77741% 54.18863%,
43.77625% 53.99325%, 43.77625% 53.99325%,
43.77533% 53.86828%, 43.77533% 53.86828%,
43.77495% 53.82429%, 43.77495% 53.82429%,
43.77518% 53.55329%, 43.77518% 53.55329%,
43.7754% 53.2823%, 43.7754% 53.2823%,
43.77563% 53.01131%, 43.77563% 53.01131%,
43.77585% 52.74031%, 43.77585% 52.74031%,
43.77608% 52.46932%, 43.77608% 52.46932%,
43.7763% 52.19832%, 43.7763% 52.19832%,
43.77653% 51.92733%, 43.77653% 51.92733%,
43.77675% 51.65633%, 43.77675% 51.65633%,
43.77653% 51.38533%, 43.77653% 51.38533%,
43.7763% 51.11434%, 43.7763% 51.11434%,
43.77608% 50.84334%, 43.77608% 50.84334%,
43.77585% 50.57235%, 43.77585% 50.57235%,
43.77563% 50.30136%, 43.77563% 50.30136%,
43.7754% 50.03036%, 43.7754% 50.03036%,
43.77518% 49.75936%, 43.77518% 49.75936%,
43.77495% 49.48837%, 43.77495% 49.48837%,
44.48391% 49.4885%, 44.48391% 49.4885%,
45.19287% 49.48865%, 45.19287% 49.48865%,
45.90183% 49.48878%, 45.90183% 49.48878%,
46.61079% 49.48892%, 46.61079% 49.48892%,
47.31975% 49.48906%, 47.31975% 49.48906%,
48.0287% 49.4892%, 48.0287% 49.4892%,
48.73766% 49.48934%, 48.73766% 49.48934%,
49.44662% 49.48948%, 49.44662% 49.48948%,
50.72252% 49.48934%, 50.72252% 49.48934%,
51.99842% 49.4892%, 51.99842% 49.4892%,
53.27432% 49.48906%, 53.27432% 49.48906%,
54.55022% 49.48892%, 54.55022% 49.48892%,
55.82611% 49.48878%, 55.82611% 49.48878%,
57.10201% 49.48865%, 57.10201% 49.48865%,
58.3779% 49.4885%, 58.3779% 49.4885%,
59.6538% 49.48837%, 59.6538% 49.48837%,
59.57598% 49.89151%, 59.57598% 49.89151%,
59.31883% 50.28598%, 59.31883% 50.28598%,
58.84686% 50.70884%, 58.84686% 50.70884%,
58.12456% 51.19714%, 58.12456% 51.19714%,
57.11643% 51.78793%, 57.11643% 51.78793%,
55.78697% 52.51828%, 55.78697% 52.51828%,
54.10066% 53.42522%, 54.10066% 53.42522%,
52.02202% 54.54581%, 52.02202% 54.54581%,
49.96525% 55.66916%, 49.96525% 55.66916%,
48.3319% 56.57212%, 48.3319% 56.57212%,
47.06745% 57.27347%, 47.06745% 57.27347%,
46.11739% 57.79191%, 46.11739% 57.79191%,
45.42719% 58.14619%, 45.42719% 58.14619%,
44.94235% 58.35507%, 44.94235% 58.35507%,
44.60834% 58.43725%, 44.60834% 58.43725%,
44.37066% 58.41149%, 44.37066% 58.41149%,
44.15383% 58.27711%, 44.15383% 58.27711%,
43.99617% 58.0603%, 43.99617% 58.0603%,
43.88847% 57.77578%, 43.88847% 57.77578%,
43.82151% 57.43825%, 43.82151% 57.43825%,
43.78608% 57.06245%, 43.78608% 57.06245%,
43.77304% 56.66309%, 43.77304% 56.66309%,
43.773% 56.25486% 43.773% 56.25486%
); );
} }
&.two { &.two {
clip-path: polygon( clip-path: polygon(
43.77666% 43.83035%, 43.77666% 43.83035%,
43.77874% 44.21955%, 43.77874% 44.21955%,
43.7795% 44.59109%, 43.7795% 44.59109%,
43.77934% 44.93442%, 43.77934% 44.93442%,
43.77855% 45.23898%, 43.77855% 45.23898%,
43.77741% 45.49423%, 43.77741% 45.49423%,
43.77625% 45.68961%, 43.77625% 45.68961%,
43.77533% 45.81458%, 43.77533% 45.81458%,
43.77495% 45.85858%, 43.77495% 45.85858%,
43.77518% 46.12957%, 43.77518% 46.12957%,
43.7754% 46.40056%, 43.7754% 46.40056%,
43.77563% 46.67156%, 43.77563% 46.67156%,
43.77585% 46.94255%, 43.77585% 46.94255%,
43.77608% 47.21355%, 43.77608% 47.21355%,
43.7763% 47.48454%, 43.7763% 47.48454%,
43.77653% 47.75554%, 43.77653% 47.75554%,
43.77675% 48.02654%, 43.77675% 48.02654%,
43.77653% 48.29753%, 43.77653% 48.29753%,
43.7763% 48.56852%, 43.7763% 48.56852%,
43.77608% 48.83952%, 43.77608% 48.83952%,
43.77585% 49.11051%, 43.77585% 49.11051%,
43.77563% 49.38151%, 43.77563% 49.38151%,
43.7754% 49.65251%, 43.7754% 49.65251%,
43.77518% 49.9235%, 43.77518% 49.9235%,
43.77495% 50.1945%, 43.77495% 50.1945%,
44.48391% 50.19436%, 44.48391% 50.19436%,
45.19287% 50.19422%, 45.19287% 50.19422%,
45.90183% 50.19408%, 45.90183% 50.19408%,
46.61079% 50.19394%, 46.61079% 50.19394%,
47.31975% 50.1938%, 47.31975% 50.1938%,
48.0287% 50.19366%, 48.0287% 50.19366%,
48.73766% 50.19353%, 48.73766% 50.19353%,
49.44662% 50.19338%, 49.44662% 50.19338%,
50.72252% 50.19353%, 50.72252% 50.19353%,
51.99842% 50.19366%, 51.99842% 50.19366%,
53.27432% 50.1938%, 53.27432% 50.1938%,
54.55022% 50.19394%, 54.55022% 50.19394%,
55.82611% 50.19408%, 55.82611% 50.19408%,
57.10201% 50.19422%, 57.10201% 50.19422%,
58.3779% 50.19436%, 58.3779% 50.19436%,
59.6538% 50.1945%, 59.6538% 50.1945%,
59.57598% 49.79136%, 59.57598% 49.79136%,
59.31883% 49.39688%, 59.31883% 49.39688%,
58.84686% 48.97402%, 58.84686% 48.97402%,
58.12456% 48.48572%, 58.12456% 48.48572%,
57.11643% 47.89493%, 57.11643% 47.89493%,
55.78697% 47.16458%, 55.78697% 47.16458%,
54.10066% 46.25764%, 54.10066% 46.25764%,
52.02202% 45.13705%, 52.02202% 45.13705%,
49.96525% 44.01371%, 49.96525% 44.01371%,
48.3319% 43.11074%, 48.3319% 43.11074%,
47.06745% 42.4094%, 47.06745% 42.4094%,
46.11739% 41.89096%, 46.11739% 41.89096%,
45.42719% 41.53667%, 45.42719% 41.53667%,
44.94235% 41.3278%, 44.94235% 41.3278%,
44.60834% 41.24561%, 44.60834% 41.24561%,
44.37066% 41.27137%, 44.37066% 41.27137%,
44.15383% 41.40575%, 44.15383% 41.40575%,
43.99617% 41.62256%, 43.99617% 41.62256%,
43.88847% 41.90709%, 43.88847% 41.90709%,
43.82151% 42.24461%, 43.82151% 42.24461%,
43.78608% 42.62041%, 43.78608% 42.62041%,
43.77304% 43.01978%, 43.77304% 43.01978%,
43.773% 43.428% 43.773% 43.428%
); );
}
} }
} }
} }
@ -366,7 +401,8 @@
opacity: 1; opacity: 1;
} }
&.active, .audio.is-unread:not(.is-out) & { &.active,
.audio.is-unread:not(.is-out) & {
opacity: 1; opacity: 1;
} }
} }
@ -392,7 +428,8 @@
margin-bottom: -2px; margin-bottom: -2px;
} }
&-ico, &-download { &-ico,
&-download {
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
} }
@ -400,8 +437,6 @@
.part { .part {
height: 112px !important; height: 112px !important;
width: 112px !important; width: 112px !important;
position: absolute;
background-color: white;
@include respond-to(handhelds) { @include respond-to(handhelds) {
width: 100px !important; width: 100px !important;
@ -415,8 +450,9 @@
color: var(--primary-text-color); color: var(--primary-text-color);
} }
&-time, &-subtitle { &-time,
font-size: 14px; &-subtitle {
font-size: .875rem;
color: var(--secondary-text-color); color: var(--secondary-text-color);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -424,7 +460,7 @@
display: flex; display: flex;
@include respond-to(handhelds) { @include respond-to(handhelds) {
font-size: 12px; font-size: .75rem;
} }
} }
@ -434,17 +470,19 @@
.audio-time { .audio-time {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 4px; margin-right: .25rem;
} }
} }
// * for audio // * for audio
&-title, &-subtitle { &-title,
&-subtitle {
margin-left: -1px; margin-left: -1px;
} }
// * for audio // * for audio
&-title, &:not(.audio-show-progress) &-subtitle { &-title,
&:not(.audio-show-progress) &-subtitle {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
max-width: 100%; max-width: 100%;
@ -490,6 +528,32 @@
--border-radius: 4px; --border-radius: 4px;
--thumb-size: .75rem; --thumb-size: .75rem;
flex: 1 1 auto; flex: 1 1 auto;
margin-left: 5px; margin: 0 6px 0 5px; // margin-right due to overflow
}
&-with-thumb {
.audio-play-icon {
z-index: 1;
background-color: transparent;
@include animation-level(2) {
transition: transform .25 ease-in-out, background-color .2s ease-in-out;
}
.part {
background-color: #fff !important;
}
&:not(:last-child) {
background-color: rgba(0, 0, 0, .3);
}
}
.media-photo {
border-radius: inherit;
object-fit: cover;
width: inherit;
height: inherit;
}
} }
} }

8
src/scss/partials/_chatBubble.scss

@ -1079,7 +1079,8 @@ $bubble-margin: .25rem;
@include respond-to(handhelds) { @include respond-to(handhelds) {
.document, .document,
.audio { .audio {
&-ico, &-download { &-ico,
&-download {
height: 2.25rem; height: 2.25rem;
width: 2.25rem; width: 2.25rem;
} }
@ -1087,8 +1088,6 @@ $bubble-margin: .25rem;
} }
.audio { .audio {
padding-right: 2px;
$parent: ".audio"; $parent: ".audio";
#{$parent} { #{$parent} {
&-title { &-title {
@ -2209,7 +2208,8 @@ $bubble-margin: .25rem;
} }
&-toggle, &-toggle,
&-download { &-download,
&.corner-download .preloader-container {
background-color: var(--message-out-primary-color); background-color: var(--message-out-primary-color);
} }

17
src/scss/partials/_document.scss

@ -56,12 +56,14 @@
} }
} }
&-ico, &-download { &-ico,
&-download {
font-size: 1.125rem; font-size: 1.125rem;
background-size: contain; background-size: contain;
} }
&-ico, &-name { &-ico,
&-name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -129,7 +131,8 @@
overflow: hidden; overflow: hidden;
} }
&-name, &-size { &-name,
&-size {
line-height: var(--line-height); line-height: var(--line-height);
} }
@ -152,7 +155,8 @@
} }
} }
.document, .audio { .document,
.audio {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -160,7 +164,8 @@
position: relative; position: relative;
user-select: none; user-select: none;
&-ico, &-download { &-ico,
&-download {
position: absolute; position: absolute;
left: 0; left: 0;
width: 3.375rem; width: 3.375rem;
@ -185,7 +190,7 @@
} }
} }
.preloader-container:not(.preloader-streamable) { &:not(.corner-download) .preloader-container:not(.preloader-streamable) {
transform: scale(1) !important; transform: scale(1) !important;
} }
} }

1
src/scss/partials/_poll.scss

@ -37,6 +37,7 @@ poll-element {
margin-top: 2px; margin-top: 2px;
margin-bottom: 5px; margin-bottom: 5px;
display: flex; display: flex;
align-items: center;
position: relative; position: relative;
// @include respond-to(handhelds) { // @include respond-to(handhelds) {

4
src/scss/style.scss

@ -862,11 +862,11 @@ not screen and ( min-resolution: 192dpi),
not screen and ( min-resolution: 2dppx) { not screen and ( min-resolution: 2dppx) {
html:not(.is-safari) { html:not(.is-safari) {
span.emoji { span.emoji {
margin-right: 5px; margin-right: 5px !important;
} }
avatar-element span.emoji { avatar-element span.emoji {
margin-right: 0; margin-right: 0 !important;
} }
} }
} }

Loading…
Cancel
Save