Media timestamps

Fix hiding audio bar on stop
Fix showing audio bar on waveform click
This commit is contained in:
Eduard Kuzmenko 2023-03-03 19:47:02 +04:00
parent cf9d4e11f1
commit 7cd845c210
21 changed files with 417 additions and 158 deletions

View File

@ -640,7 +640,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
};
private onEnded = (e?: Event) => {
if(!e.isTrusted) {
if(e && !e.isTrusted) {
return;
}
@ -697,7 +697,7 @@ export class AppMediaPlaybackController extends EventListenerBase<{
return this.toggle(false);
};
public stop = (media = this.playingMedia) => {
public stop = (media = this.playingMedia, force?: boolean) => {
if(!media) {
return false;
}
@ -707,7 +707,6 @@ export class AppMediaPlaybackController extends EventListenerBase<{
}
media.currentTime = 0;
simulateEvent(media, 'ended');
if(media === this.playingMedia) {
const details = this.mediaDetails.get(media);
@ -733,6 +732,10 @@ export class AppMediaPlaybackController extends EventListenerBase<{
this.playingMediaType = undefined;
}
if(force) {
this.dispatchEvent('stop');
}
return true;
};

View File

@ -134,6 +134,10 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const onCaptionClick = (e: MouseEvent) => {
const a = findUpTag(e.target, 'A');
if(a.classList.contains('timestamp')) {
return;
}
const spoiler = findUpClassName(e.target, 'spoiler');
if(a instanceof HTMLAnchorElement && (!spoiler || this.content.caption.classList.contains('is-spoiler-visible'))) { // close viewer if it's t.me/ redirect
const onclick = a.getAttribute('onclick');
@ -172,11 +176,19 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
}
onPrevClick = async(target: AppMediaViewerTargetType) => {
this.openMedia(await this.getMessageByPeer(target.peerId, target.mid), target.element, -1);
this.openMedia({
message: await this.getMessageByPeer(target.peerId, target.mid),
target: target.element,
fromRight: -1
});
};
onNextClick = async(target: AppMediaViewerTargetType) => {
this.openMedia(await this.getMessageByPeer(target.peerId, target.mid), target.element, 1);
this.openMedia({
message: await this.getMessageByPeer(target.peerId, target.mid),
target: target.element,
fromRight: 1
});
};
onDeleteClick = () => {
@ -235,8 +247,11 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
const caption = (message as Message.message).message;
let html: Parameters<typeof setInnerHTML>[1] = '';
if(caption) {
const media = getMediaFromMessage(message, true);
html = wrapRichText(caption, {
entities: (message as Message.message).totalEntities
entities: (message as Message.message).totalEntities,
maxMediaTimestamp: ((media as MyDocument)?.type === 'video' && (media as MyDocument).duration) || undefined
});
}
@ -252,8 +267,24 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
return this;
}
public async openMedia(message: MyMessage, target?: HTMLElement, fromRight = 0, reverse = false,
prevTargets: AppMediaViewerTargetType[] = [], nextTargets: AppMediaViewerTargetType[] = []/* , needLoadMore = true */) {
public async openMedia({
message,
target,
fromRight = 0,
reverse = false,
prevTargets = [],
nextTargets = [],
mediaTimestamp
}: {
message: MyMessage,
target?: HTMLElement,
fromRight?: number,
reverse?: boolean,
prevTargets?: AppMediaViewerTargetType[],
nextTargets?: AppMediaViewerTargetType[],
mediaTimestamp?: number
/* , needLoadMore = true */
}) {
if(this.setMoverPromise) return this.setMoverPromise;
const mid = message.mid;
@ -283,7 +314,19 @@ export default class AppMediaViewer extends AppMediaViewerBase<'caption', 'delet
this.wholeDiv.classList.toggle('no-forwards', cantDownloadMessage);
this.setCaption(message);
const promise = super._openMedia(media as MyPhoto | MyDocument, message.date, fromId, fromRight, target, reverse, prevTargets, nextTargets, message/* , needLoadMore */);
const promise = super._openMedia({
media: media as MyPhoto | MyDocument,
timestamp: message.date,
fromId,
fromRight,
target,
reverse,
prevTargets,
nextTargets,
message,
mediaTimestamp
/* , needLoadMore */
});
this.target.mid = mid;
this.target.peerId = message.peerId;
this.target.message = message;

View File

@ -36,11 +36,19 @@ export default class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete
}
onPrevClick = (target: AppMediaViewerAvatarTargetType) => {
this.openMedia(target.photoId, target.element, -1);
this.openMedia({
photoId: target.photoId,
target: target.element,
fromRight: -1
});
};
onNextClick = (target: AppMediaViewerAvatarTargetType) => {
this.openMedia(target.photoId, target.element, 1);
this.openMedia({
photoId: target.photoId,
target: target.element,
fromRight: 1
});
};
onDownloadClick = () => {
@ -50,11 +58,32 @@ export default class AppMediaViewerAvatar extends AppMediaViewerBase<'', 'delete
});
};
public async openMedia(photoId: Photo.photo['id'], target?: HTMLElement, fromRight = 0, prevTargets?: AppMediaViewerAvatarTargetType[], nextTargets?: AppMediaViewerAvatarTargetType[]) {
public async openMedia({
photoId,
target,
fromRight = 0,
prevTargets,
nextTargets
}: {
photoId: Photo.photo['id'],
target?: HTMLElement,
fromRight?: number,
prevTargets?: AppMediaViewerAvatarTargetType[],
nextTargets?: AppMediaViewerAvatarTargetType[]
}) {
if(this.setMoverPromise) return this.setMoverPromise;
const photo = await this.managers.appPhotosManager.getPhoto(photoId);
const ret = super._openMedia(photo, photo.date, this.peerId, fromRight, target, false, prevTargets, nextTargets);
const ret = super._openMedia({
media: photo,
timestamp: photo.date,
fromId: this.peerId,
fromRight,
target,
reverse: false,
prevTargets,
nextTargets
});
this.target.photoId = photo.id;
this.target.photo = photo;

View File

@ -139,7 +139,7 @@ export default class AppMediaViewerBase<
protected lastDragDelta: {x: number, y: number} = this.transform;
protected lastGestureTime: number;
protected clampZoomDebounced: ReturnType<typeof debounce<() => void>>;
ignoreNextClick: boolean;
protected ignoreNextClick: boolean;
get target() {
return this.listLoader.current;
@ -758,6 +758,10 @@ export default class AppMediaViewerBase<
window.addEventListener('keyup', this.onKeyUp);
}
public setMediaTimestamp(timestamp: number) {
this.videoPlayer?.setTimestamp(timestamp);
}
onClick = (e: MouseEvent) => {
if(this.ignoreNextClick) {
this.ignoreNextClick = undefined;
@ -1471,18 +1475,30 @@ export default class AppMediaViewerBase<
});
}
protected async _openMedia(
protected async _openMedia({
media,
timestamp,
fromId,
fromRight,
target,
reverse = false,
prevTargets = [],
nextTargets = [],
message,
mediaTimestamp
}: {
media: MyDocument | MyPhoto,
timestamp: number,
fromId: PeerId | string,
fromRight: number,
target?: HTMLElement,
reverse = false,
prevTargets: TargetType[] = [],
nextTargets: TargetType[] = [],
message?: MyMessage
reverse?: boolean,
prevTargets?: TargetType[],
nextTargets?: TargetType[],
message?: MyMessage,
mediaTimestamp?: number
/* , needLoadMore = true */
) {
}) {
if(this.setMoverPromise) return this.setMoverPromise;
/* if(DEBUG) {
@ -1528,7 +1544,7 @@ export default class AppMediaViewerBase<
const tempId = ++this.tempId;
if(container.firstElementChild) {
container.innerHTML = '';
container.replaceChildren();
}
// ok set
@ -1672,6 +1688,10 @@ export default class AppMediaViewerBase<
video.loop = true;
}
if(mediaTimestamp !== undefined) {
video.currentTime = mediaTimestamp;
}
// if(!video.parentElement) {
div.append(video);
// }

View File

@ -586,7 +586,14 @@ export default class AppSearchSuper {
const message = await this.managers.appMessagesManager.getMessageByPeer(peerId, mid);
new AppMediaViewer()
.setSearchContext(this.copySearchContext(inputFilter))
.openMedia(message, targets[idx].element, 0, false, targets.slice(0, idx), targets.slice(idx + 1));
.openMedia({
message,
target: targets[idx].element,
fromRight: 0,
reverse: false,
prevTargets: targets.slice(0, idx),
nextTargets: targets.slice(idx + 1)
});
};
attachClickEvent(this.tabs.inputMessagesFilterPhotoVideo, onMediaClick.bind(null, 'grid-item', 'grid-item', 'inputMessagesFilterPhotoVideo'), {listenerSetter: this.listenerSetter});

View File

@ -262,7 +262,7 @@ async function wrapVoiceMessage(audioEl: AudioElement) {
let mousedown = false, mousemove = false;
progress.addEventListener('mouseleave', (e) => {
if(mousedown) {
audio.play();
audioEl.togglePlay(undefined, true);
mousedown = false;
}
mousemove = false;
@ -275,7 +275,7 @@ async function wrapVoiceMessage(audioEl: AudioElement) {
e.preventDefault();
if(e.button !== 0) return;
if(!audio.paused) {
audio.pause();
audioEl.togglePlay(undefined, false);
}
scrub(e);
@ -283,7 +283,7 @@ async function wrapVoiceMessage(audioEl: AudioElement) {
});
progress.addEventListener('mouseup', (e) => {
if(mousemove && mousedown) {
audio.play();
audioEl.togglePlay(undefined, true);
mousedown = false;
}
});
@ -491,6 +491,7 @@ export default class AudioElement extends HTMLElement {
private onTypeDisconnect: () => void;
public onLoad: (autoload?: boolean) => void;
public readyPromise: CancellablePromise<void>;
public load: (shouldPlay: boolean, controlledAutoplay?: boolean) => void;
public async render() {
this.classList.add('audio');
@ -562,27 +563,7 @@ export default class AudioElement extends HTMLElement {
onPlay();
}
const togglePlay = (e?: Event, paused = audio.paused) => {
e && cancelEvent(e);
if(paused) {
const hadSearchContext = !!this.searchContext;
if(appMediaPlaybackController.setSearchContext(this.searchContext || {
peerId: NULL_PEER_ID,
inputFilter: {_: 'inputMessagesFilterEmpty'},
useSearch: false
})) {
const [prev, next] = !hadSearchContext ? [] : findMediaTargets(this, this.message.mid/* , this.searchContext.useSearch */);
appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next);
}
audio.play().catch(() => {});
} else {
audio.pause();
}
};
attachClickEvent(toggle, (e) => togglePlay(e), {listenerSetter: this.listenerSetter});
attachClickEvent(toggle, (e) => this.togglePlay(e), {listenerSetter: this.listenerSetter});
this.addAudioListener('ended', () => {
toggle.classList.remove('playing');
@ -599,8 +580,6 @@ export default class AudioElement extends HTMLElement {
});
this.addAudioListener('play', onPlay);
return togglePlay;
};
if(doc.thumbs?.length) {
@ -629,24 +608,16 @@ export default class AudioElement extends HTMLElement {
const autoDownload = doc.type !== 'audio'/* || !this.noAutoDownload */;
onLoad(autoDownload);
const r = (shouldPlay: boolean) => {
const r = this.load = (shouldPlay: boolean, controlledAutoplay?: boolean) => {
this.load = undefined;
if(this.audio.src) {
return;
}
appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid, this.message.pFlags.is_scheduled);
const onDownloadInit = () => {
if(shouldPlay) {
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio
if(IS_SAFARI && !this.audio.autoplay) {
this.audio.autoplay = true;
}
}
};
onDownloadInit();
this.onDownloadInit(shouldPlay);
if(!preloader) {
if(doc.supportsStreaming) {
@ -673,7 +644,7 @@ export default class AudioElement extends HTMLElement {
deferred.cancel();
}, {once: true}) as any;
onDownloadInit();
this.onDownloadInit(shouldPlay);
};
/* if(!this.audio.paused) {
@ -683,7 +654,7 @@ export default class AudioElement extends HTMLElement {
const playListener: any = this.addAudioListener('play', onPlay);
this.readyPromise.then(() => {
this.listenerSetter.remove(playListener);
this.listenerSetter.remove(pauseListener);
pauseListener && this.listenerSetter.remove(pauseListener);
});
} else {
preloader = constructDownloadPreloader();
@ -693,7 +664,7 @@ export default class AudioElement extends HTMLElement {
}
const load = () => {
onDownloadInit();
this.onDownloadInit(shouldPlay);
const download = appDownloadManager.downloadMediaURL({media: doc});
@ -729,7 +700,7 @@ export default class AudioElement extends HTMLElement {
// setTimeout(() => {
// release loaded audio
if(appMediaPlaybackController.willBePlayedMedia === this.audio) {
if(!controlledAutoplay && appMediaPlaybackController.willBePlayedMedia === this.audio) {
this.audio.play();
appMediaPlaybackController.willBePlayed(undefined);
}
@ -755,6 +726,54 @@ export default class AudioElement extends HTMLElement {
}
}
private onDownloadInit(shouldPlay: boolean) {
if(shouldPlay) {
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio
if(IS_SAFARI && !this.audio.autoplay) {
this.audio.autoplay = true;
}
}
}
public togglePlay(e?: Event, paused = this.audio.paused) {
e && cancelEvent(e);
if(paused) {
this.setTargetsIfNeeded();
this.audio.play().catch(() => {});
} else {
this.audio.pause();
}
}
public setTargetsIfNeeded() {
const hadSearchContext = !!this.searchContext;
if(appMediaPlaybackController.setSearchContext(this.searchContext || {
peerId: NULL_PEER_ID,
inputFilter: {_: 'inputMessagesFilterEmpty'},
useSearch: false
})) {
const [prev, next] = !hadSearchContext ? [] : findMediaTargets(this, this.message.mid/* , this.searchContext.useSearch */);
appMediaPlaybackController.setTargets({peerId: this.message.peerId, mid: this.message.mid}, prev, next);
}
}
public playWithTimestamp(timestamp: number) {
this.load?.(true, true);
appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio
this.readyPromise.then(() => {
if(appMediaPlaybackController.willBePlayedMedia !== this.audio && this.audio.paused) {
return;
}
appMediaPlaybackController.willBePlayed(undefined);
this.audio.currentTime = timestamp;
this.togglePlay(undefined, true);
});
}
get addAudioListener() {
return this.listenerSetter.add(this.audio);
}

View File

@ -96,7 +96,12 @@ export async function openAvatarViewer(
peerId,
inputFilter: {_: inputFilter}
})
.openMedia(message, getTarget(), undefined, undefined, prevTargets ? f(prevTargets) : undefined, nextTargets ? f(nextTargets) : undefined);
.openMedia({
message,
target: getTarget(),
prevTargets: prevTargets ? f(prevTargets) : undefined,
nextTargets: nextTargets ? f(nextTargets) : undefined
});
return;
}
@ -112,13 +117,12 @@ export async function openAvatarViewer(
photoId: el.item as string
}));
new AppMediaViewerAvatar(peerId).openMedia(
photo.id,
getTarget(),
undefined,
prevTargets ? f(prevTargets) : undefined,
nextTargets ? f(nextTargets) : undefined
);
new AppMediaViewerAvatar(peerId).openMedia({
photoId: photo.id,
target: getTarget(),
prevTargets: prevTargets ? f(prevTargets) : undefined,
nextTargets: nextTargets ? f(nextTargets) : undefined
});
}
}

View File

@ -45,7 +45,7 @@ export default class ChatAudio extends PinnedContainer {
}
),
onClose: () => {
appMediaPlaybackController.stop();
appMediaPlaybackController.stop(undefined, true);
},
floating: true
});

View File

@ -565,8 +565,8 @@ export default class ChatBubbles {
if(element instanceof AudioElement || element.classList.contains('media-round')) {
element.dataset.mid = '' + message.mid;
delete element.dataset.isOutgoing;
(element as any).message = message;
(element as any).onLoad(true);
(element as AudioElement).message = message;
(element as AudioElement).onLoad(true);
} else {
element.dataset.docId = '' + doc.id;
(element as any).doc = doc;
@ -1920,6 +1920,68 @@ export default class ChatBubbles {
return;
}
if(await this.checkTargetForMediaViewer(target, e)) {
return;
}
if(['IMG', 'DIV', 'SPAN'/* , 'A' */].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV');
if(['DIV', 'SPAN'].indexOf(target.tagName) !== -1/* || target.tagName === 'A' */) {
if(target.classList.contains('goto-original')) {
const savedFrom = bubble.dataset.savedFrom;
const [peerId, mid] = savedFrom.split('_');
// //this.log('savedFrom', peerId, msgID);
this.chat.appImManager.setInnerPeer({
peerId: peerId.toPeerId(),
lastMsgId: +mid
});
return;
} else if(target.classList.contains('forward')) {
const mid = +bubble.dataset.mid;
const message = await this.managers.appMessagesManager.getMessageByPeer(this.peerId, mid);
new PopupForward({
[this.peerId]: await this.managers.appMessagesManager.getMidsByMessage(message)
});
// appSidebarRight.forwardTab.open([mid]);
return;
}
let isReplyClick = false;
try {
isReplyClick = !!findUpClassName(e.target, 'reply');
} catch(err) {}
if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) {
const bubbleMid = +bubble.dataset.mid;
this.followStack.push(bubbleMid);
const message = (await this.chat.getMessage(bubbleMid)) as Message.message;
const replyToPeerId = message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : this.peerId;
const replyToMid = message.reply_to.reply_to_msg_id;
this.chat.appImManager.setInnerPeer({
peerId: replyToPeerId,
lastMsgId: replyToMid,
type: this.chat.type,
threadId: this.chat.threadId
});
/* if(this.chat.type === 'discussion') {
this.chat.appImManager.setMessageId(, originalMessageId);
} else {
this.chat.appImManager.setInnerPeer(this.peerId, originalMessageId);
} */
// this.chat.setMessageId(, originalMessageId);
}
}
// console.log('chatInner click', e);
};
public async checkTargetForMediaViewer(target: HTMLElement, e?: Event, mediaTimestamp?: number) {
const bubble = findUpClassName(target, 'bubble');
const documentDiv = findUpClassName(target, 'document-with-thumb');
if((target.tagName === 'IMG' && !target.classList.contains('emoji') && !target.classList.contains('document-thumb')) ||
target.classList.contains('album-item') ||
@ -1929,7 +1991,7 @@ export default class ChatBubbles {
target.classList.contains('canvas-thumbnail')) {
const groupedItem = findUpClassName(target, 'album-item') || findUpClassName(target, 'document-container');
const preloader = (groupedItem || bubble).querySelector<HTMLElement>('.preloader-container');
if(preloader) {
if(preloader && e) {
simulateClickEvent(preloader);
cancelEvent(e);
return;
@ -2027,67 +2089,18 @@ export default class ChatBubbles {
useSearch: this.chat.type !== 'scheduled' && !isSingleMedia,
isScheduled: this.chat.type === 'scheduled'
})
.openMedia(message, targets[idx].element, 0, true, targets.slice(0, idx), targets.slice(idx + 1));
// appMediaViewer.openMedia(message, target as HTMLImageElement);
return;
.openMedia({
message: message,
target: targets[idx].element,
fromRight: 0,
reverse: true,
prevTargets: targets.slice(0, idx),
nextTargets: targets.slice(idx + 1),
mediaTimestamp
});
return true;
}
if(['IMG', 'DIV', 'SPAN'/* , 'A' */].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV');
if(['DIV', 'SPAN'].indexOf(target.tagName) !== -1/* || target.tagName === 'A' */) {
if(target.classList.contains('goto-original')) {
const savedFrom = bubble.dataset.savedFrom;
const [peerId, mid] = savedFrom.split('_');
// //this.log('savedFrom', peerId, msgID);
this.chat.appImManager.setInnerPeer({
peerId: peerId.toPeerId(),
lastMsgId: +mid
});
return;
} else if(target.classList.contains('forward')) {
const mid = +bubble.dataset.mid;
const message = await this.managers.appMessagesManager.getMessageByPeer(this.peerId, mid);
new PopupForward({
[this.peerId]: await this.managers.appMessagesManager.getMidsByMessage(message)
});
// appSidebarRight.forwardTab.open([mid]);
return;
}
let isReplyClick = false;
try {
isReplyClick = !!findUpClassName(e.target, 'reply');
} catch(err) {}
if(isReplyClick && bubble.classList.contains('is-reply')/* || bubble.classList.contains('forwarded') */) {
const bubbleMid = +bubble.dataset.mid;
this.followStack.push(bubbleMid);
const message = (await this.chat.getMessage(bubbleMid)) as Message.message;
const replyToPeerId = message.reply_to.reply_to_peer_id ? getPeerId(message.reply_to.reply_to_peer_id) : this.peerId;
const replyToMid = message.reply_to.reply_to_msg_id;
this.chat.appImManager.setInnerPeer({
peerId: replyToPeerId,
lastMsgId: replyToMid,
type: this.chat.type,
threadId: this.chat.threadId
});
/* if(this.chat.type === 'discussion') {
this.chat.appImManager.setMessageId(, originalMessageId);
} else {
this.chat.appImManager.setInnerPeer(this.peerId, originalMessageId);
} */
// this.chat.setMessageId(, originalMessageId);
}
}
// console.log('chatInner click', e);
};
}
public async onGoDownClick() {
if(!this.followStack.length) {
@ -3219,10 +3232,33 @@ export default class ChatBubbles {
this.onScroll();
const afterSetPromise = Promise.all([setPeerPromise, getHeavyAnimationPromise()]);
const afterSetPromise = Promise.all([
setPeerPromise,
getHeavyAnimationPromise()
]);
afterSetPromise.then(() => { // check whether list isn't full
if(!middleware()) {
return;
}
scrollable.checkForTriggers();
if(options.mediaTimestamp !== undefined) {
// ! :(
const p = cached && !samePeer && liteMode.isAvailable('animations') && this.chat.appImManager.chats.length > 1 ?
pause(400) :
Promise.resolve();
p.then(() => {
return this.getMountedBubble(lastMsgId);
}).then((mounted) => {
if(!middleware() || !mounted) {
return;
}
this.playMediaWithTimestamp(mounted.bubble, options.mediaTimestamp);
});
}
// if(cached) {
// this.onRenderScrollSet();
// }
@ -3267,6 +3303,21 @@ export default class ChatBubbles {
return {cached, promise: setPeerPromise};
}
public playMediaWithTimestamp(bubble: HTMLElement, timestamp: number) {
const attachment = bubble.querySelector<HTMLElement>('.attachment');
if(attachment) {
const media = attachment.querySelector<HTMLElement>('img, video, canvas');
this.checkTargetForMediaViewer(media, undefined, timestamp);
return;
}
const audio = bubble.querySelector<AudioElement>('.audio');
if(audio) {
audio.playWithTimestamp(timestamp);
return;
}
}
private async setFetchReactionsInterval(afterSetPromise: Promise<any>) {
const middleware = this.getMiddleware();
const needReactionsInterval = this.chat.isChannel;
@ -3874,6 +3925,8 @@ export default class ChatBubbles {
customEmojiSize ??= this.chat.appImManager.customEmojiSize;
const doc = (messageMedia as MessageMedia.messageMediaDocument)?.document as MyDocument;
const richText = wrapRichText(messageMessage, {
entities: totalEntities,
passEntities: this.passEntities,
@ -3881,7 +3934,8 @@ export default class ChatBubbles {
lazyLoadQueue: this.lazyLoadQueue,
customEmojiSize,
middleware,
animationGroup: this.chat.animationGroup
animationGroup: this.chat.animationGroup,
maxMediaTimestamp: ((['voice', 'audio', 'video'] as MyDocument['type'][]).includes(doc?.type) && doc.duration) || undefined
});
let canHaveTail = true;

View File

@ -100,6 +100,9 @@ export default class PopupForward extends PopupPickUser {
case 'voice':
action = 'send_voices';
break;
case 'video':
action = 'send_videos';
break;
default:
action = 'send_docs';
break;

View File

@ -34,7 +34,7 @@ import rootScope from '../../lib/rootScope';
import {ThumbCache} from '../../lib/storages/thumbs';
import animationIntersector, {AnimationItemGroup} from '../animationIntersector';
import appMediaPlaybackController, {MediaSearchContext} from '../appMediaPlaybackController';
import {findMediaTargets} from '../audio';
import AudioElement, {findMediaTargets} from '../audio';
import LazyLoadQueue from '../lazyLoadQueue';
import ProgressivePreloader from '../preloader';
import wrapPhoto from './photo';
@ -338,7 +338,8 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH
};
if(message.pFlags.is_outgoing) {
(divRound as any).onLoad = onLoad;
// ! WARNING ! just to type-check
(divRound as any as AudioElement).onLoad = onLoad;
divRound.dataset.isOutgoing = '1';
} else {
onLoad();
@ -567,6 +568,8 @@ export default async function wrapVideo({doc, container, message, boxWidth, boxH
preloader.setDownloadFunction(load);
}
(container as any).preloader = preloader;
/* if(doc.size >= 20e6 && !doc.downloaded) {
let downloadDiv = document.createElement('div');
downloadDiv.classList.add('download');

View File

@ -11,7 +11,7 @@ import parseUriParams from './string/parseUriParams';
export default function addAnchorListener<Params extends {pathnameParams?: any, uriParams?: any}>(options: {
name: 'showMaskedAlert' | 'execBotCommand' | 'searchByHashtag' | 'addstickers' | 'im' |
'resolve' | 'privatepost' | 'addstickers' | 'voicechat' | 'joinchat' | 'join' | 'invoice' |
'addemoji',
'addemoji' | 'setMediaTimestamp',
protocol?: 'tg',
callback: (params: Params, element?: HTMLAnchorElement) => boolean | any,
noPathnameParams?: boolean,

11
src/layer.d.ts vendored
View File

@ -4525,7 +4525,7 @@ export namespace ReplyMarkup {
/**
* @link https://core.telegram.org/type/MessageEntity
*/
export type MessageEntity = MessageEntity.messageEntityUnknown | MessageEntity.messageEntityMention | MessageEntity.messageEntityHashtag | MessageEntity.messageEntityBotCommand | MessageEntity.messageEntityUrl | MessageEntity.messageEntityEmail | MessageEntity.messageEntityBold | MessageEntity.messageEntityItalic | MessageEntity.messageEntityCode | MessageEntity.messageEntityPre | MessageEntity.messageEntityTextUrl | MessageEntity.messageEntityMentionName | MessageEntity.inputMessageEntityMentionName | MessageEntity.messageEntityPhone | MessageEntity.messageEntityCashtag | MessageEntity.messageEntityUnderline | MessageEntity.messageEntityStrike | MessageEntity.messageEntityBlockquote | MessageEntity.messageEntityBankCard | MessageEntity.messageEntitySpoiler | MessageEntity.messageEntityCustomEmoji | MessageEntity.messageEntityEmoji | MessageEntity.messageEntityHighlight | MessageEntity.messageEntityLinebreak | MessageEntity.messageEntityCaret;
export type MessageEntity = MessageEntity.messageEntityUnknown | MessageEntity.messageEntityMention | MessageEntity.messageEntityHashtag | MessageEntity.messageEntityBotCommand | MessageEntity.messageEntityUrl | MessageEntity.messageEntityEmail | MessageEntity.messageEntityBold | MessageEntity.messageEntityItalic | MessageEntity.messageEntityCode | MessageEntity.messageEntityPre | MessageEntity.messageEntityTextUrl | MessageEntity.messageEntityMentionName | MessageEntity.inputMessageEntityMentionName | MessageEntity.messageEntityPhone | MessageEntity.messageEntityCashtag | MessageEntity.messageEntityUnderline | MessageEntity.messageEntityStrike | MessageEntity.messageEntityBlockquote | MessageEntity.messageEntityBankCard | MessageEntity.messageEntitySpoiler | MessageEntity.messageEntityCustomEmoji | MessageEntity.messageEntityEmoji | MessageEntity.messageEntityHighlight | MessageEntity.messageEntityLinebreak | MessageEntity.messageEntityCaret | MessageEntity.messageEntityTimestamp;
export namespace MessageEntity {
export type messageEntityUnknown = {
@ -4684,6 +4684,14 @@ export namespace MessageEntity {
offset?: number,
length?: number
};
export type messageEntityTimestamp = {
_: 'messageEntityTimestamp',
offset?: number,
length?: number,
time?: number,
raw?: string
};
}
/**
@ -11858,6 +11866,7 @@ export interface ConstructorDeclMap {
'messageEntityHighlight': MessageEntity.messageEntityHighlight,
'messageEntityLinebreak': MessageEntity.messageEntityLinebreak,
'messageEntityCaret': MessageEntity.messageEntityCaret,
'messageEntityTimestamp': MessageEntity.messageEntityTimestamp,
'messageActionDiscussionStarted': MessageAction.messageActionDiscussionStarted,
'messageActionChatLeave': MessageAction.messageActionChatLeave,
'messageActionChannelDeletePhoto': MessageAction.messageActionChannelDeletePhoto,

View File

@ -120,7 +120,8 @@ export type ChatSetPeerOptions = {
startParam?: string,
stack?: number,
commentId?: number,
type?: ChatType
type?: ChatType,
mediaTimestamp?: number
};
export type ChatSetInnerPeerOptions = Modify<ChatSetPeerOptions, {
@ -623,6 +624,23 @@ export class AppImManager extends EventListenerBase<{
}
});
addAnchorListener<{}>({
name: 'setMediaTimestamp',
callback: (_, element) => {
const timestamp = +element.dataset.timestamp;
const bubble = findUpClassName(element, 'bubble');
if(bubble) {
this.chat.bubbles.playMediaWithTimestamp(bubble, timestamp);
return;
}
if(findUpClassName(element, 'media-viewer-caption')) {
const appMediaViewer = (window as any).appMediaViewer;
appMediaViewer.setMediaTimestamp(timestamp);
}
}
});
([
['addstickers', INTERNAL_LINK_TYPE.STICKER_SET],
['addemoji', INTERNAL_LINK_TYPE.EMOJI_SET]
@ -707,7 +725,7 @@ export class AppImManager extends EventListenerBase<{
// pathnameParams: [string, string?],
// uriParams: {comment?: number}
pathnameParams: ['c', string, string] | [string, string?],
uriParams: {thread?: string, comment?: string} | {comment?: string, start?: string}
uriParams: {thread?: string, comment?: string, t?: string} | {comment?: string, start?: string, t?: string}
}>({
name: 'im',
callback: async({pathnameParams, uriParams}, element) => {
@ -726,7 +744,8 @@ export class AppImManager extends EventListenerBase<{
post: pathnameParams[2] || pathnameParams[1],
thread,
comment: uriParams.comment,
stack: this.getStackFromElement(element)
stack: this.getStackFromElement(element),
t: uriParams.t
};
} else {
const thread = 'thread' in uriParams ? uriParams.thread : pathnameParams[2] && pathnameParams[1];
@ -737,7 +756,8 @@ export class AppImManager extends EventListenerBase<{
thread,
comment: uriParams.comment,
start: 'start' in uriParams ? uriParams.start : undefined,
stack: this.getStackFromElement(element)
stack: this.getStackFromElement(element),
t: uriParams.t
};
}
@ -765,7 +785,8 @@ export class AppImManager extends EventListenerBase<{
post?: string,
thread?: string,
comment?: string,
phone?: string
phone?: string,
t?: string
}
}>({
name: 'resolve',
@ -1127,7 +1148,8 @@ export class AppImManager extends EventListenerBase<{
commentId,
startParam: link.start,
stack: link.stack,
threadId
threadId,
mediaTimestamp: link.t && +link.t
});
break;
}
@ -1153,7 +1175,8 @@ export class AppImManager extends EventListenerBase<{
peer: chat,
lastMsgId: postId,
threadId,
stack: link.stack
stack: link.stack,
mediaTimestamp: link.t && +link.t
});
break;
}

View File

@ -25,6 +25,7 @@ export namespace InternalLink {
comment?: string,
thread?: string,
start?: string,
t?: string, // media timestamp
stack?: number // local
}
@ -34,6 +35,7 @@ export namespace InternalLink {
post: string,
thread?: string,
comment?: string,
t?: string // media timestamp
stack?: number // local
}

View File

@ -270,8 +270,7 @@ export default class VideoPlayer extends ControlsHover {
}
}
protected togglePlay() {
const isPaused = this.video.paused;
protected togglePlay(isPaused = this.video.paused) {
this.video[isPaused ? 'play' : 'pause']();
}
@ -394,6 +393,11 @@ export default class VideoPlayer extends ControlsHover {
}
}
public setTimestamp(timestamp: number) {
this.video.currentTime = timestamp;
this.togglePlay(true);
}
public cleanup() {
super.cleanup();
this.listenerSetter.removeAll();

View File

@ -69,8 +69,9 @@ export const URL_REG_EXP = URL_PROTOCOL_REG_EXP_PART +
export const URL_PROTOCOL_REG_EXP = new RegExp('^' + URL_PROTOCOL_REG_EXP_PART.slice(0, -1), 'i');
export const URL_ANY_PROTOCOL_REG_EXP = /^((?:[^\/]+?):\/\/|mailto:)/;
export const USERNAME_REG_EXP = '[a-zA-Z\\d_]{5,32}';
export const TIMESTAMP_REG_EXP = '(?:\\s|^)((?:\\d{1,2}:)?(?:[0-5]?[0-9]):(?:[0-5][0-9]))(?:\\s|$)';
export const BOT_COMMAND_REG_EXP = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + USERNAME_REG_EXP + '))?(\\b|$)';
export const FULL_REG_EXP = new RegExp('(^| )(@)(' + USERNAME_REG_EXP + ')|(' + URL_REG_EXP + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + ALPHA_NUMERIC_REG_EXP + ']{2,64})|(^|\\s)' + BOT_COMMAND_REG_EXP, 'i');
export const FULL_REG_EXP = new RegExp('(^| )(@)(' + USERNAME_REG_EXP + ')|(' + URL_REG_EXP + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + ALPHA_NUMERIC_REG_EXP + ']{2,64})|(^|\\s)' + BOT_COMMAND_REG_EXP + '|' + TIMESTAMP_REG_EXP + '', 'i');
export const EMAIL_REG_EXP = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
// const markdownTestRegExp = /[`_*@~]/;
export const MARKDOWN_REG_EXP = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s|\x01)(`|~~|\*\*|__|_-_|\|\|)([^\n]+?)\7([\x01\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m;

View File

@ -16,14 +16,14 @@ import checkBrackets from './checkBrackets';
import getEmojiUnified from './getEmojiUnified';
export default function parseEntities(text: string) {
let match: any;
let match: RegExpMatchArray;
let raw = text;
const entities: MessageEntity[] = [];
let matchIndex;
let rawOffset = 0;
// var start = tsNow()
FULL_REG_EXP.lastIndex = 0;
while((match = raw.match(FULL_REG_EXP))) {
while(match = raw.match(FULL_REG_EXP)) {
matchIndex = rawOffset + match.index;
// console.log('parseEntities match:', match);
@ -101,6 +101,20 @@ export default function parseEntities(text: string) {
length: 1 + match[13].length + (match[14] ? 1 + match[14].length : 0),
unsafe: true
});
} else if(match[16]) { // Media timestamp
const timestamp = match[16];
const splitted: string[] = timestamp.split(':');
const splittedLength = splitted.length;
const hours = splittedLength === 3 ? +splitted[0] : 0;
const minutes = +splitted[splittedLength === 3 ? 1 : 0];
const seconds = +splitted[splittedLength - 1];
entities.push({
_: 'messageEntityTimestamp',
offset: matchIndex + (/\D/.test(match[0][0]) ? 1 : 0),
length: timestamp.length,
raw: timestamp,
time: hours * 3600 + minutes * 60 + seconds
});
}
raw = raw.substr(match.index + match[0].length);

View File

@ -975,6 +975,7 @@ export default function wrapRichText(text: string, options: Partial<{
passEntities: Partial<{
[_ in MessageEntity['_']]: boolean
}>,
maxMediaTimestamp: number,
noEncoding: boolean,
isSelectable: boolean,
@ -1462,6 +1463,20 @@ export default function wrapRichText(text: string, options: Partial<{
break;
}
case 'messageEntityTimestamp': {
if(!options.maxMediaTimestamp || entity.time > options.maxMediaTimestamp) {
break;
}
element = document.createElement('a');
element.classList.add('timestamp');
element.dataset.timestamp = '' + entity.time;
(element as HTMLAnchorElement).href = '#';
element.setAttribute('onclick', 'setMediaTimestamp(this)');
break;
}
}
if(!usedText && partText) {

View File

@ -168,6 +168,15 @@
"params": [
{"name": "unsafe", "type": "boolean"}
]
}, {
"predicate": "messageEntityTimestamp",
"params": [
{"name": "offset", "type": "number"},
{"name": "length", "type": "number"},
{"name": "time", "type": "number"},
{"name": "raw", "type": "string"}
],
"type": "MessageEntity"
}, {
"predicate": "user",
"params": [

View File

@ -2765,6 +2765,7 @@ $bubble-border-radius-big: 12px;
--selection-background-color: var(--message-out-selection-background-color);
--message-time-color: var(--message-out-time-color);
--message-status-color: var(--message-out-status-color);
--link-color: var(--message-primary-color);
.bubble-content {
margin-left: auto;
@ -2807,10 +2808,6 @@ $bubble-border-radius-big: 12px;
}
}
.anchor-url {
color: var(--message-primary-color);
}
/* .bubble-content-wrapper {
> .user-avatar {
left: auto;