Browse Source

Picture-in-Picture

master
Eduard Kuzmenko 3 years ago
parent
commit
28ea417d0a
  1. 165
      src/components/appMediaPlaybackController.ts
  2. 122
      src/components/appMediaViewerBase.ts
  3. 2
      src/components/audio.ts
  4. 3
      src/components/chat/audio.ts
  5. 16
      src/components/chat/bubbles.ts
  6. 180
      src/components/mediaProgressLine.ts
  7. 4
      src/components/popups/newMedia.ts
  8. 84
      src/components/volumeSelector.ts
  9. 10
      src/components/wrappers.ts
  10. 8
      src/helpers/dom/createVideo.ts
  11. 13
      src/helpers/searchListLoader.ts
  12. 294
      src/lib/mediaPlayer.ts
  13. 2
      src/scss/partials/_chat.scss
  14. 10
      src/scss/partials/_chatBubble.scss
  15. 5
      src/scss/partials/_ckin.scss
  16. 4
      src/scss/partials/_poll.scss
  17. 2
      src/scss/partials/popups/_datePicker.scss
  18. 2
      src/scss/partials/popups/_joinChatInvite.scss
  19. 7
      src/scss/tgico.scss

165
src/components/appMediaPlaybackController.ts

@ -23,6 +23,7 @@ import SearchListLoader from "../helpers/searchListLoader";
import { onMediaLoad } from "../helpers/files"; import { onMediaLoad } from "../helpers/files";
import copy from "../helpers/object/copy"; import copy from "../helpers/object/copy";
import deepEqual from "../helpers/object/deepEqual"; import deepEqual from "../helpers/object/deepEqual";
import ListenerSetter from "../helpers/listenerSetter";
// TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню // TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню
// TODO: Safari: попробовать замаскировать подгрузку последнего чанка // TODO: Safari: попробовать замаскировать подгрузку последнего чанка
@ -92,6 +93,8 @@ export class AppMediaPlaybackController {
audio: 1 audio: 1
}; };
private pip: HTMLVideoElement;
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;';
@ -100,14 +103,14 @@ export class AppMediaPlaybackController {
if(navigator.mediaSession) { if(navigator.mediaSession) {
const actions: {[action in MediaSessionAction]?: MediaSessionActionHandler} = { const actions: {[action in MediaSessionAction]?: MediaSessionActionHandler} = {
play: this.play, play: this.browserPlay,
pause: this.pause, pause: this.browserPause,
stop: this.stop, stop: this.browserStop,
seekbackward: this.seekBackward, seekbackward: this.browserSeekBackward,
seekforward: this.seekForward, seekforward: this.browserSeekForward,
seekto: this.seekTo, seekto: this.browserSeekTo,
previoustrack: this.previous, previoustrack: this.browserPrevious,
nexttrack: this.next nexttrack: this.browserNext
}; };
for(const action in actions) { for(const action in actions) {
@ -178,22 +181,19 @@ export class AppMediaPlaybackController {
}; };
} }
public seekBackward = (details: MediaSessionActionDetails) => { public seekBackward = (details: MediaSessionActionDetails, media = this.playingMedia) => {
const media = this.playingMedia;
if(media) { if(media) {
media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET)); media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET));
} }
}; };
public seekForward = (details: MediaSessionActionDetails) => { public seekForward = (details: MediaSessionActionDetails, media = this.playingMedia) => {
const media = this.playingMedia;
if(media) { if(media) {
media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET)); media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET));
} }
}; };
public seekTo = (details: MediaSessionActionDetails) => { public seekTo = (details: MediaSessionActionDetails, media = this.playingMedia) => {
const media = this.playingMedia;
if(media) { if(media) {
media.currentTime = details.seekTime; media.currentTime = details.seekTime;
} }
@ -393,6 +393,10 @@ export class AppMediaPlaybackController {
} }
private async setNewMediadata(message: Message.message, playingMedia = this.playingMedia) { private async setNewMediadata(message: Message.message, playingMedia = this.playingMedia) {
if(document.pictureInPictureElement) {
return;
}
await onMediaLoad(playingMedia, undefined, false); // have to wait for load, otherwise on macOS won't set await onMediaLoad(playingMedia, undefined, false); // have to wait for load, otherwise on macOS won't set
const doc = appMessagesManager.getMediaFromMessage(message) as MyDocument; const doc = appMessagesManager.getMediaFromMessage(message) as MyDocument;
@ -493,6 +497,13 @@ export class AppMediaPlaybackController {
navigator.mediaSession.metadata = metadata; navigator.mediaSession.metadata = metadata;
} }
public setCurrentMediadata() {
const {playingMedia} = this;
if(!playingMedia) return;
const message = this.getMessageByMedia(playingMedia);
this.setNewMediadata(message, playingMedia);
}
private getMessageByMedia(media: HTMLMediaElement): Message.message { private getMessageByMedia(media: HTMLMediaElement): Message.message {
const details = this.mediaDetails.get(media); const details = this.mediaDetails.get(media);
const {peerId, mid} = details; const {peerId, mid} = details;
@ -500,6 +511,21 @@ export class AppMediaPlaybackController {
return message; return message;
} }
public getPlayingDetails() {
const {playingMedia} = this;
if(!playingMedia) {
return;
}
const message = this.getMessageByMedia(playingMedia);
return {
doc: appMessagesManager.getMediaFromMessage(message),
message,
media: playingMedia,
playbackParams: this.getPlaybackParams()
};
}
private onPlay = (e?: Event) => { private onPlay = (e?: Event) => {
const media = e.target as HTMLMediaElement; const media = e.target as HTMLMediaElement;
const details = this.mediaDetails.get(media); const details = this.mediaDetails.get(media);
@ -507,6 +533,11 @@ export class AppMediaPlaybackController {
//console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media); //console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media);
const pip = this.pip;
if(pip) {
pip.pause();
}
const message = this.getMessageByMedia(media); const message = this.getMessageByMedia(media);
const previousMedia = this.playingMedia; const previousMedia = this.playingMedia;
@ -550,21 +581,6 @@ export class AppMediaPlaybackController {
}, 0); }, 0);
}; };
public getPlayingDetails() {
const {playingMedia} = this;
if(!playingMedia) {
return;
}
const message = this.getMessageByMedia(playingMedia);
return {
doc: appMessagesManager.getMediaFromMessage(message),
message,
media: playingMedia,
playbackParams: this.getPlaybackParams()
};
}
private onPause = (e?: Event) => { private onPause = (e?: Event) => {
/* const target = e.target as HTMLMediaElement; /* const target = e.target as HTMLMediaElement;
if(!isInDOM(target)) { if(!isInDOM(target)) {
@ -573,6 +589,10 @@ export class AppMediaPlaybackController {
return; return;
} */ } */
// if(this.pip) {
// this.pip.play();
// }
rootScope.dispatchEvent('media_pause'); rootScope.dispatchEvent('media_pause');
}; };
@ -594,23 +614,27 @@ export class AppMediaPlaybackController {
} }
}; };
public toggle(play?: boolean) { // public get pip() {
if(!this.playingMedia) { // return document.pictureInPictureElement as HTMLVideoElement;
// }
public toggle(play?: boolean, media = this.playingMedia) {
if(!media) {
return false; return false;
} }
if(play === undefined) { if(play === undefined) {
play = this.playingMedia.paused; play = media.paused;
} }
if(this.playingMedia.paused !== play) { if(media.paused !== play) {
return false; return false;
} }
if(play) { if(play) {
this.playingMedia.play(); media.play();
} else { } else {
this.playingMedia.pause(); media.pause();
} }
return true; return true;
@ -624,8 +648,7 @@ export class AppMediaPlaybackController {
return this.toggle(false); return this.toggle(false);
}; };
public stop = () => { public stop = (media = this.playingMedia) => {
const media = this.playingMedia;
if(!media) { if(!media) {
return false; return false;
} }
@ -637,6 +660,7 @@ export class AppMediaPlaybackController {
media.currentTime = 0; media.currentTime = 0;
simulateEvent(media, 'ended'); simulateEvent(media, 'ended');
if(media === this.playingMedia) {
const details = this.mediaDetails.get(media); const details = this.mediaDetails.get(media);
if(details?.clean) { if(details?.clean) {
media.src = ''; media.src = '';
@ -658,6 +682,7 @@ export class AppMediaPlaybackController {
this.playingMedia = undefined; this.playingMedia = undefined;
this.playingMediaType = undefined; this.playingMediaType = undefined;
}
return true; return true;
}; };
@ -690,22 +715,45 @@ export class AppMediaPlaybackController {
} }
}; };
private bindBrowserCallback(cb: (video: HTMLVideoElement, details: MediaSessionActionDetails) => void) {
const handler: MediaSessionActionHandler = (details) => {
cb(this.pip, details);
};
return handler;
}
public browserPlay = this.bindBrowserCallback((video) => this.toggle(true, video));
public browserPause = this.bindBrowserCallback((video) => this.toggle(false, video));
public browserStop = this.bindBrowserCallback((video) => this.stop(video));
public browserSeekBackward = this.bindBrowserCallback((video, details) => this.seekBackward(details, video));
public browserSeekForward = this.bindBrowserCallback((video, details) => this.seekForward(details, video));
public browserSeekTo = this.bindBrowserCallback((video, details) => this.seekTo(details, video));
public browserNext = this.bindBrowserCallback((video) => video || this.next());
public browserPrevious = this.bindBrowserCallback((video) => video ? this.seekToStart(video) : this.previous());
public next = () => { public next = () => {
return this.go(1); return this.go(1);
}; };
public previous = () => { public previous = () => {
const media = this.playingMedia; if(this.seekToStart(this.playingMedia)) {
// if(media && (media.currentTime > 5 || !this.listLoader.getPrevious().length)) {
if(media && media.currentTime > 5) {
media.currentTime = 0;
this.toggle(true);
return; return;
} }
return this.go(-1); return this.go(-1);
}; };
public seekToStart(media: HTMLMediaElement) {
if(media?.currentTime > 5) {
media.currentTime = 0;
this.toggle(true, media);
return true;
}
return false;
}
public willBePlayed(media: HTMLMediaElement) { public willBePlayed(media: HTMLMediaElement) {
this.willBePlayedMedia = media; this.willBePlayedMedia = media;
} }
@ -802,7 +850,7 @@ export class AppMediaPlaybackController {
else this.playingMedia = undefined; else this.playingMedia = undefined;
this.toggleSwitchers(false); this.toggleSwitchers(false);
return () => { return (playPaused = wasPlaying) => {
this.toggleSwitchers(true); this.toggleSwitchers(true);
if(playingMedia) { if(playingMedia) {
@ -817,7 +865,7 @@ export class AppMediaPlaybackController {
this.stop(); this.stop();
} }
if(wasPlaying) { if(playPaused) {
this.play(); this.play();
} }
}; };
@ -826,6 +874,35 @@ export class AppMediaPlaybackController {
public toggleSwitchers(enabled: boolean) { public toggleSwitchers(enabled: boolean) {
this.lockedSwitchers = !enabled; this.lockedSwitchers = !enabled;
} }
public setPictureInPicture(video: HTMLVideoElement) {
this.pip = video;
// let wasPlaying = this.pause();
const listenerSetter = new ListenerSetter();
listenerSetter.add(video)('leavepictureinpicture', () => {
if(this.pip !== video) {
return;
}
this.pip = undefined;
// if(wasPlaying) {
// this.play();
// }
listenerSetter.removeAll();
}, {once: true});
listenerSetter.add(video)('play', () => {
this.pause();
// if(this.pause()) {
// listenerSetter.add(video)('pause', () => {
// this.play();
// }, {once: true});
// }
});
}
} }
const appMediaPlaybackController = new AppMediaPlaybackController(); const appMediaPlaybackController = new AppMediaPlaybackController();

122
src/components/appMediaViewerBase.ts

@ -14,7 +14,7 @@ import { logger } from "../lib/logger";
import VideoPlayer from "../lib/mediaPlayer"; import VideoPlayer from "../lib/mediaPlayer";
import rootScope from "../lib/rootScope"; import rootScope from "../lib/rootScope";
import animationIntersector from "./animationIntersector"; import animationIntersector from "./animationIntersector";
import appMediaPlaybackController from "./appMediaPlaybackController"; import appMediaPlaybackController, { AppMediaPlaybackController } from "./appMediaPlaybackController";
import AvatarElement from "./avatar"; import AvatarElement from "./avatar";
import ButtonIcon from "./buttonIcon"; import ButtonIcon from "./buttonIcon";
import { ButtonMenuItemOptions } from "./buttonMenu"; import { ButtonMenuItemOptions } from "./buttonMenu";
@ -44,6 +44,8 @@ import RichTextProcessor from "../lib/richtextprocessor";
import { NULL_PEER_ID } from "../lib/mtproto/mtproto_config"; import { NULL_PEER_ID } from "../lib/mtproto/mtproto_config";
import { isFullScreen } from "../helpers/dom/fullScreen"; import { isFullScreen } from "../helpers/dom/fullScreen";
import { attachClickEvent } from "../helpers/dom/clickEvent"; import { attachClickEvent } from "../helpers/dom/clickEvent";
import SearchListLoader from "../helpers/searchListLoader";
import createVideo from "../helpers/dom/createVideo";
const ZOOM_STEP = 0.5; const ZOOM_STEP = 0.5;
const ZOOM_INITIAL_VALUE = 1; const ZOOM_INITIAL_VALUE = 1;
@ -114,6 +116,7 @@ export default class AppMediaViewerBase<
protected zoomSwipeY = 0; protected zoomSwipeY = 0;
protected ctrlKeyDown: boolean; protected ctrlKeyDown: boolean;
protected releaseSingleMedia: ReturnType<AppMediaPlaybackController['setSingleMedia']>;
get target() { get target() {
return this.listLoader.current; return this.listLoader.current;
@ -426,14 +429,11 @@ export default class AppMediaViewerBase<
const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd); const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd);
this.listLoader.reset(); this.listLoader.reset();
(this.listLoader as any).cleanup && (this.listLoader as any).cleanup(); (this.listLoader as SearchListLoader<any>).cleanup && (this.listLoader as SearchListLoader<any>).cleanup();
this.setMoverPromise = null; this.setMoverPromise = null;
this.tempId = -1; this.tempId = -1;
if((window as any).appMediaViewer === this) {
(window as any).appMediaViewer = undefined; (window as any).appMediaViewer = undefined;
if(this.zoomSwipeHandler) {
this.zoomSwipeHandler.removeListeners();
this.zoomSwipeHandler = undefined;
} }
/* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) { /* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) {
@ -442,19 +442,48 @@ export default class AppMediaViewerBase<
}); });
} */ } */
window.removeEventListener('keydown', this.onKeyDown); this.removeGlobalListeners();
window.removeEventListener('keyup', this.onKeyUp);
window.removeEventListener('wheel', this.onWheel, {capture: true}); this.zoomSwipeHandler = undefined;
promise.finally(() => { promise.finally(() => {
this.wholeDiv.remove(); this.wholeDiv.remove();
rootScope.isOverlayActive = false; this.toggleOverlay(false);
animationIntersector.checkAnimations(false);
}); });
return promise; return promise;
} }
protected toggleOverlay(active: boolean) {
rootScope.isOverlayActive = active;
animationIntersector.checkAnimations(active);
}
protected toggleGlobalListeners(active: boolean) {
if(active) this.setGlobalListeners();
else this.removeGlobalListeners();
}
protected removeGlobalListeners() {
if(this.zoomSwipeHandler) {
this.zoomSwipeHandler.removeListeners();
}
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
window.removeEventListener('wheel', this.onWheel, {capture: true});
}
protected setGlobalListeners() {
if(this.isZooming()) {
this.zoomSwipeHandler.setListeners();
}
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true});
}
onClick = (e: MouseEvent) => { onClick = (e: MouseEvent) => {
if(this.setMoverAnimationPromise) return; if(this.setMoverAnimationPromise) return;
@ -770,7 +799,7 @@ export default class AppMediaViewerBase<
mediaElement = new Image(); mediaElement = new Image();
src = target.src; src = target.src;
} else if(target instanceof HTMLVideoElement) { } else if(target instanceof HTMLVideoElement) {
mediaElement = document.createElement('video'); mediaElement = createVideo();
mediaElement.src = target.src; mediaElement.src = target.src;
} else if(target instanceof SVGSVGElement) { } else if(target instanceof SVGSVGElement) {
const clipId = target.dataset.clipId; const clipId = target.dataset.clipId;
@ -878,10 +907,7 @@ export default class AppMediaViewerBase<
mover.classList.add('hiding'); mover.classList.add('hiding');
} }
this.wholeDiv.classList.add('backwards'); this.toggleWholeActive(false);
setTimeout(() => {
this.wholeDiv.classList.remove('active');
}, 0);
//return ret; //return ret;
@ -969,6 +995,17 @@ export default class AppMediaViewerBase<
return ret; return ret;
} }
protected toggleWholeActive(active: boolean) {
if(active) {
this.wholeDiv.classList.add('active');
} else {
this.wholeDiv.classList.add('backwards');
setTimeout(() => {
this.wholeDiv.classList.remove('active');
}, 0);
}
}
protected setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) { protected setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) {
/* let media = aspecter.firstElementChild; /* let media = aspecter.firstElementChild;
let proportion: number; let proportion: number;
@ -1209,15 +1246,15 @@ export default class AppMediaViewerBase<
this.moveTheMover(this.content.mover, fromRight === 1); this.moveTheMover(this.content.mover, fromRight === 1);
this.setNewMover(); this.setNewMover();
} else { } else {
rootScope.isOverlayActive = true; this.toggleOverlay(true);
window.addEventListener('keydown', this.onKeyDown); this.setGlobalListeners();
window.addEventListener('keyup', this.onKeyUp);
if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true}); if(!this.wholeDiv.parentElement) {
const mainColumns = document.getElementById('main-columns'); this.pageEl.insertBefore(this.wholeDiv, document.getElementById('main-columns'));
this.pageEl.insertBefore(this.wholeDiv, mainColumns);
void this.wholeDiv.offsetLeft; // reflow void this.wholeDiv.offsetLeft; // reflow
this.wholeDiv.classList.add('active'); }
animationIntersector.checkAnimations(true);
this.toggleWholeActive(true);
if(!IS_MOBILE_SAFARI) { if(!IS_MOBILE_SAFARI) {
appNavigationController.pushItem({ appNavigationController.pushItem({
@ -1285,7 +1322,7 @@ export default class AppMediaViewerBase<
const useController = message && media.type !== 'gif'; const useController = message && media.type !== 'gif';
const video = /* useController ? const video = /* useController ?
appMediaPlaybackController.addMedia(message, false, true) as HTMLVideoElement : appMediaPlaybackController.addMedia(message, false, true) as HTMLVideoElement :
*/document.createElement('video'); */createVideo({pip: useController});
const set = () => this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => { const set = () => this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => {
//return; // set and don't move //return; // set and don't move
@ -1366,6 +1403,32 @@ export default class AppMediaViewerBase<
streamable: supportsStreaming, streamable: supportsStreaming,
onPlaybackRackMenuToggle: (open) => { onPlaybackRackMenuToggle: (open) => {
this.wholeDiv.classList.toggle('hide-caption', !!open); this.wholeDiv.classList.toggle('hide-caption', !!open);
},
onPip: (pip) => {
if(!pip && (window as any).appMediaViewer !== this) {
this.releaseSingleMedia = undefined;
this.close();
return;
}
const mover = this.moversContainer.lastElementChild as HTMLElement;
mover.classList.toggle('hiding', pip);
this.toggleWholeActive(!pip);
this.toggleOverlay(!pip);
this.toggleGlobalListeners(!pip);
if(useController) {
if(pip) {
// appMediaPlaybackController.toggleSwitchers(true);
this.releaseSingleMedia(false);
this.releaseSingleMedia = undefined;
appMediaPlaybackController.setPictureInPicture(video);
} else {
this.releaseSingleMedia = appMediaPlaybackController.setSingleMedia(video, message as Message.message);
}
}
} }
}); });
player.addEventListener('toggleControls', (show) => { player.addEventListener('toggleControls', (show) => {
@ -1374,7 +1437,7 @@ export default class AppMediaViewerBase<
this.addEventListener('setMoverBefore', () => { this.addEventListener('setMoverBefore', () => {
this.wholeDiv.classList.remove('has-video-controls'); this.wholeDiv.classList.remove('has-video-controls');
this.videoPlayer.removeListeners(); this.videoPlayer.cleanup();
this.videoPlayer = undefined; this.videoPlayer = undefined;
}, {once: true}); }, {once: true});
@ -1465,10 +1528,13 @@ export default class AppMediaViewerBase<
// * have to set options (especially playbackRate) after src // * have to set options (especially playbackRate) after src
// * https://github.com/videojs/video.js/issues/2516 // * https://github.com/videojs/video.js/issues/2516
if(useController) { if(useController) {
const rollback = appMediaPlaybackController.setSingleMedia(video, message as Message.message); this.releaseSingleMedia = appMediaPlaybackController.setSingleMedia(video, message as Message.message);
this.addEventListener('setMoverBefore', () => { this.addEventListener('setMoverBefore', () => {
rollback(); if(this.releaseSingleMedia) {
this.releaseSingleMedia();
this.releaseSingleMedia = undefined;
}
}, {once: true}); }, {once: true});
} }

2
src/components/audio.ts

@ -7,7 +7,6 @@
import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager";
import { wrapPhoto } from "./wrappers"; import { wrapPhoto } from "./wrappers";
import ProgressivePreloader from "./preloader"; import ProgressivePreloader from "./preloader";
import { MediaProgressLine } from "../lib/mediaPlayer";
import appMediaPlaybackController, { MediaItem, MediaSearchContext } from "./appMediaPlaybackController"; import appMediaPlaybackController, { MediaItem, MediaSearchContext } from "./appMediaPlaybackController";
import { DocumentAttribute, Message } from "../layer"; import { DocumentAttribute, Message } from "../layer";
import mediaSizes from "../helpers/mediaSizes"; import mediaSizes from "../helpers/mediaSizes";
@ -32,6 +31,7 @@ import formatBytes from "../helpers/formatBytes";
import { animateSingle } from "../helpers/animation"; import { animateSingle } from "../helpers/animation";
import clamp from "../helpers/number/clamp"; import clamp from "../helpers/number/clamp";
import toHHMMSS from "../helpers/string/toHHMMSS"; import toHHMMSS from "../helpers/string/toHHMMSS";
import MediaProgressLine from "./mediaProgressLine";
rootScope.addEventListener('messages_media_read', ({mids, peerId}) => { rootScope.addEventListener('messages_media_read', ({mids, peerId}) => {
mids.forEach(mid => { mids.forEach(mid => {

3
src/components/chat/audio.ts

@ -17,10 +17,11 @@ import replaceContent from "../../helpers/dom/replaceContent";
import PeerTitle from "../peerTitle"; import PeerTitle from "../peerTitle";
import { i18n } from "../../lib/langPack"; import { i18n } from "../../lib/langPack";
import { formatFullSentTime } from "../../helpers/date"; import { formatFullSentTime } from "../../helpers/date";
import { MediaProgressLine, VolumeSelector } from "../../lib/mediaPlayer";
import ButtonIcon from "../buttonIcon"; import ButtonIcon from "../buttonIcon";
import { MyDocument } from "../../lib/appManagers/appDocsManager"; import { MyDocument } from "../../lib/appManagers/appDocsManager";
import { Message } from "../../layer"; import { Message } from "../../layer";
import MediaProgressLine from "../mediaProgressLine";
import VolumeSelector from "../volumeSelector";
export default class ChatAudio extends PinnedContainer { export default class ChatAudio extends PinnedContainer {
private toggleEl: HTMLElement; private toggleEl: HTMLElement;

16
src/components/chat/bubbles.ts

@ -3391,6 +3391,9 @@ export default class ChatBubbles {
previewResizer.append(preview); previewResizer.append(preview);
} }
let quoteTextDiv = document.createElement('div');
quoteTextDiv.classList.add('quote-text');
const doc = webpage.document as MyDocument; const doc = webpage.document as MyDocument;
if(doc) { if(doc) {
if(doc.type === 'gif' || doc.type === 'video' || doc.type === 'round') { if(doc.type === 'gif' || doc.type === 'video' || doc.type === 'round') {
@ -3422,18 +3425,23 @@ export default class ChatBubbles {
autoDownloadSize: this.chat.autoDownload.file, autoDownloadSize: this.chat.autoDownload.file,
lazyLoadQueue: this.lazyLoadQueue, lazyLoadQueue: this.lazyLoadQueue,
loadPromises, loadPromises,
sizeType: 'documentName' sizeType: 'documentName',
searchContext: {
useSearch: false,
peerId: this.peerId,
inputFilter: {
_: 'inputMessagesFilterEmpty'
}
}
}); });
preview.append(docDiv); preview.append(docDiv);
preview.classList.add('preview-with-document'); preview.classList.add('preview-with-document');
quoteTextDiv.classList.add('has-document');
//messageDiv.classList.add((webpage.type || 'document') + '-message'); //messageDiv.classList.add((webpage.type || 'document') + '-message');
//doc = null; //doc = null;
} }
} }
let quoteTextDiv = document.createElement('div');
quoteTextDiv.classList.add('quote-text');
if(previewResizer) { if(previewResizer) {
quoteTextDiv.append(previewResizer); quoteTextDiv.append(previewResizer);
} }

180
src/components/mediaProgressLine.ts

@ -0,0 +1,180 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { GrabEvent } from "../helpers/dom/attachGrabListeners";
import appMediaPlaybackController from "./appMediaPlaybackController";
import RangeSelector from "./rangeSelector";
export default class MediaProgressLine extends RangeSelector {
protected filledLoad: HTMLDivElement;
protected progressRAF = 0;
protected media: HTMLMediaElement;
protected streamable: boolean;
constructor(media?: HTMLAudioElement | HTMLVideoElement, streamable?: boolean, withTransition?: boolean, useTransform?: boolean) {
super({
step: 1000 / 60 / 1000,
min: 0,
max: 1,
withTransition,
useTransform
}, 0);
if(media) {
this.setMedia(media, streamable);
}
}
public setMedia(media: HTMLMediaElement, streamable = false) {
if(this.media) {
this.removeListeners();
}
if(streamable && !this.filledLoad) {
this.filledLoad = document.createElement('div');
this.filledLoad.classList.add('progress-line__filled', 'progress-line__loaded');
this.container.prepend(this.filledLoad);
//this.setLoadProgress();
} else if(this.filledLoad) {
this.filledLoad.classList.toggle('hide', !streamable);
}
this.media = media;
this.streamable = streamable;
if(!media.paused || media.currentTime > 0) {
this.onPlay();
}
let wasPlaying = false;
this.setSeekMax();
this.setListeners();
this.setHandlers({
onMouseDown: () => {
wasPlaying = !this.media.paused;
wasPlaying && this.media.pause();
},
onMouseUp: (e) => {
// cancelEvent(e.event);
wasPlaying && this.media.play();
}
});
}
protected onLoadedData = () => {
this.max = this.media.duration;
this.seek.setAttribute('max', '' + this.max);
};
protected onEnded = () => {
this.setProgress();
};
protected onPlay = () => {
let r = () => {
this.setProgress();
this.progressRAF = this.media.paused ? 0 : window.requestAnimationFrame(r);
};
if(this.progressRAF) {
window.cancelAnimationFrame(this.progressRAF);
}
if(this.streamable) {
this.setLoadProgress();
}
this.progressRAF = window.requestAnimationFrame(r);
};
protected onTimeUpdate = () => {
if(this.media.paused) {
this.setProgress();
if(this.streamable) {
this.setLoadProgress();
}
}
};
protected onProgress = (e: Event) => {
this.setLoadProgress();
};
protected scrub(e: GrabEvent) {
const scrubTime = super.scrub(e);
this.media.currentTime = scrubTime;
return scrubTime;
}
protected setLoadProgress() {
if(appMediaPlaybackController.isSafariBuffering(this.media)) return;
const buf = this.media.buffered;
const numRanges = buf.length;
const currentTime = this.media.currentTime;
let nearestStart = 0, end = 0;
for(let i = 0; i < numRanges; ++i) {
const start = buf.start(i);
if(currentTime >= start && start >= nearestStart) {
nearestStart = start;
end = buf.end(i);
}
//console.log('onProgress range:', i, buf.start(i), buf.end(i), this.media);
}
//console.log('onProgress correct range:', nearestStart, end, this.media);
const percents = this.media.duration ? end / this.media.duration : 0;
this.filledLoad.style.width = (percents * 100) + '%';
//this.filledLoad.style.transform = 'scaleX(' + percents + ')';
}
protected setSeekMax() {
this.max = this.media.duration || 0;
if(this.max > 0) {
this.onLoadedData();
} else {
this.media.addEventListener('loadeddata', this.onLoadedData);
}
}
public setProgress() {
if(appMediaPlaybackController.isSafariBuffering(this.media)) return;
const currentTime = this.media.currentTime;
super.setProgress(currentTime);
}
public setListeners() {
super.setListeners();
this.media.addEventListener('ended', this.onEnded);
this.media.addEventListener('play', this.onPlay);
this.media.addEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.addEventListener('progress', this.onProgress);
}
public removeListeners() {
super.removeListeners();
if(this.media) {
this.media.removeEventListener('loadeddata', this.onLoadedData);
this.media.removeEventListener('ended', this.onEnded);
this.media.removeEventListener('play', this.onPlay);
this.media.removeEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.removeEventListener('progress', this.onProgress);
}
if(this.progressRAF) {
window.cancelAnimationFrame(this.progressRAF);
this.progressRAF = 0;
}
}
}

4
src/components/popups/newMedia.ts

@ -25,6 +25,7 @@ import { attachClickEvent } from "../../helpers/dom/clickEvent";
import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport';
import getGifDuration from "../../helpers/getGifDuration"; import getGifDuration from "../../helpers/getGifDuration";
import replaceContent from "../../helpers/dom/replaceContent"; import replaceContent from "../../helpers/dom/replaceContent";
import createVideo from "../../helpers/dom/createVideo";
type SendFileParams = Partial<{ type SendFileParams = Partial<{
file: File, file: File,
@ -275,13 +276,12 @@ export default class PopupNewMedia extends PopupElement {
let promise: Promise<void>; let promise: Promise<void>;
if(isVideo) { if(isVideo) {
const video = document.createElement('video'); const video = createVideo();
const source = document.createElement('source'); const source = document.createElement('source');
source.src = params.objectURL = URL.createObjectURL(file); source.src = params.objectURL = URL.createObjectURL(file);
video.autoplay = true; video.autoplay = true;
video.controls = false; video.controls = false;
video.muted = true; video.muted = true;
video.setAttribute('playsinline', 'true');
video.addEventListener('timeupdate', () => { video.addEventListener('timeupdate', () => {
video.pause(); video.pause();

84
src/components/volumeSelector.ts

@ -0,0 +1,84 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import cancelEvent from "../helpers/dom/cancelEvent";
import { attachClickEvent } from "../helpers/dom/clickEvent";
import ListenerSetter from "../helpers/listenerSetter";
import rootScope from "../lib/rootScope";
import appMediaPlaybackController from "./appMediaPlaybackController";
import RangeSelector from "./rangeSelector";
export default class VolumeSelector extends RangeSelector {
private static ICONS = ['volume_off', 'volume_mute', 'volume_down', 'volume_up'];
public btn: HTMLElement;
protected icon: HTMLSpanElement;
constructor(protected listenerSetter: ListenerSetter, protected vertical = false) {
super({
step: 0.01,
min: 0,
max: 1,
vertical
}, 1);
this.setListeners();
this.setHandlers({
onScrub: currentTime => {
const value = Math.max(Math.min(currentTime, 1), 0);
//console.log('volume scrub:', currentTime, value);
appMediaPlaybackController.muted = false;
appMediaPlaybackController.volume = value;
},
/* onMouseUp: (e) => {
cancelEvent(e.event);
} */
});
const className = 'player-volume';
const btn = this.btn = document.createElement('div');
btn.classList.add('btn-icon', className);
const icon = this.icon = document.createElement('span');
icon.classList.add(className + '__icon');
btn.append(icon, this.container);
attachClickEvent(icon, this.onMuteClick, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope)('media_playback_params', this.setVolume);
this.setVolume();
}
private onMuteClick = (e?: Event) => {
e && cancelEvent(e);
appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
};
private setVolume = () => {
// const volume = video.volume;
const {volume, muted} = appMediaPlaybackController;
let d: string;
let iconIndex: number;
if(!volume || muted) {
iconIndex = 0;
} else if(volume > .5) {
iconIndex = 3;
} else if(volume > 0 && volume < .25) {
iconIndex = 1;
} else {
iconIndex = 2;
}
VolumeSelector.ICONS.forEach(icon => this.icon.classList.remove('tgico-' + icon));
this.icon.classList.add('tgico-' + VolumeSelector.ICONS[iconIndex]);
if(!this.mousedown) {
this.setProgress(muted ? 0 : volume);
}
};
}

10
src/components/wrappers.ts

@ -58,6 +58,7 @@ import Row from './row';
import { ChatAutoDownloadSettings } from '../helpers/autoDownload'; import { ChatAutoDownloadSettings } from '../helpers/autoDownload';
import formatBytes from '../helpers/formatBytes'; import formatBytes from '../helpers/formatBytes';
import toHHMMSS from '../helpers/string/toHHMMSS'; import toHHMMSS from '../helpers/string/toHHMMSS';
import createVideo from '../helpers/dom/createVideo';
const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB
@ -183,9 +184,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
let preloader: ProgressivePreloader; // it must be here, otherwise will get error before initialization in round onPlay let preloader: ProgressivePreloader; // it must be here, otherwise will get error before initialization in round onPlay
const video = document.createElement('video'); const video = createVideo();
video.classList.add('media-video'); video.classList.add('media-video');
video.setAttribute('playsinline', 'true');
video.muted = true; video.muted = true;
if(doc.type === 'round') { if(doc.type === 'round') {
const divRound = document.createElement('div'); const divRound = document.createElement('div');
@ -1650,8 +1650,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
if(asStatic) { if(asStatic) {
media = new Image(); media = new Image();
} else { } else {
media = document.createElement('video'); media = createVideo();
media.setAttribute('playsinline', 'true');
(media as HTMLVideoElement).muted = true; (media as HTMLVideoElement).muted = true;
if(play) { if(play) {
@ -1783,8 +1782,7 @@ export async function wrapStickerSetThumb({set, lazyLoadQueue, container, group,
} else { } else {
let media: HTMLElement; let media: HTMLElement;
if(set.pFlags.videos) { if(set.pFlags.videos) {
media = document.createElement('video'); media = createVideo();
media.setAttribute('playsinline', 'true');
(media as HTMLVideoElement).autoplay = true; (media as HTMLVideoElement).autoplay = true;
(media as HTMLVideoElement).muted = true; (media as HTMLVideoElement).muted = true;
(media as HTMLVideoElement).loop = true; (media as HTMLVideoElement).loop = true;

8
src/helpers/dom/createVideo.ts

@ -0,0 +1,8 @@
export default function createVideo(options: {
pip?: boolean
} = {}) {
const video = document.createElement('video');
if(!options.pip) video.disablePictureInPicture = true;
video.setAttribute('playsinline', 'true');
return video;
}

13
src/helpers/searchListLoader.ts

@ -173,9 +173,10 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
this.loadedAllUp = true; this.loadedAllUp = true;
} }
// if(!this.searchContext.useSearch) { // it should've been noSearch instead...
// this.loadedAllDown = this.loadedAllUp = true; if(this.searchContext.useSearch !== false) {
// } this.loadedAllDown = this.loadedAllUp = true;
}
if(this.otherSideLoader) { if(this.otherSideLoader) {
this.otherSideLoader.setSearchContext(context); this.otherSideLoader.setSearchContext(context);
@ -270,7 +271,11 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
protected setLoaded(down: boolean, value: boolean) { protected setLoaded(down: boolean, value: boolean) {
const changed = super.setLoaded(down, value); const changed = super.setLoaded(down, value);
if(changed && this.otherSideLoader && value/* && (this.reverse ? this.loadedAllUp : this.loadedAllDown) */) { if(changed &&
this.otherSideLoader &&
value &&
this.searchContext?.useSearch !== false/* &&
(this.reverse ? this.loadedAllUp : this.loadedAllDown) */) {
const reverse = this.loadedAllUp; const reverse = this.loadedAllUp;
this.otherSideLoader.setSearchContext({ this.otherSideLoader.setSearchContext({
...this.searchContext, ...this.searchContext,

294
src/lib/mediaPlayer.ts

@ -5,263 +5,19 @@
*/ */
import appMediaPlaybackController from "../components/appMediaPlaybackController"; import appMediaPlaybackController from "../components/appMediaPlaybackController";
import { IS_APPLE_MOBILE } from "../environment/userAgent"; import { IS_APPLE_MOBILE, IS_MOBILE } from "../environment/userAgent";
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import RangeSelector from "../components/rangeSelector";
import { onMediaLoad } from "../helpers/files"; import { onMediaLoad } from "../helpers/files";
import cancelEvent from "../helpers/dom/cancelEvent"; import cancelEvent from "../helpers/dom/cancelEvent";
import ListenerSetter from "../helpers/listenerSetter"; import ListenerSetter from "../helpers/listenerSetter";
import ButtonMenu from "../components/buttonMenu"; import ButtonMenu from "../components/buttonMenu";
import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle"; import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle";
import rootScope from "./rootScope"; import rootScope from "./rootScope";
import { GrabEvent } from "../helpers/dom/attachGrabListeners";
import { attachClickEvent } from "../helpers/dom/clickEvent";
import ControlsHover from "../helpers/dom/controlsHover"; import ControlsHover from "../helpers/dom/controlsHover";
import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../helpers/dom/fullScreen"; import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../helpers/dom/fullScreen";
import toHHMMSS from "../helpers/string/toHHMMSS"; import toHHMMSS from "../helpers/string/toHHMMSS";
import MediaProgressLine from "../components/mediaProgressLine";
export class MediaProgressLine extends RangeSelector { import VolumeSelector from "../components/volumeSelector";
protected filledLoad: HTMLDivElement;
protected progressRAF = 0;
protected media: HTMLMediaElement;
protected streamable: boolean;
constructor(media?: HTMLAudioElement | HTMLVideoElement, streamable?: boolean, withTransition?: boolean, useTransform?: boolean) {
super({
step: 1000 / 60 / 1000,
min: 0,
max: 1,
withTransition,
useTransform
}, 0);
if(media) {
this.setMedia(media, streamable);
}
}
public setMedia(media: HTMLMediaElement, streamable = false) {
if(this.media) {
this.removeListeners();
}
if(streamable && !this.filledLoad) {
this.filledLoad = document.createElement('div');
this.filledLoad.classList.add('progress-line__filled', 'progress-line__loaded');
this.container.prepend(this.filledLoad);
//this.setLoadProgress();
} else if(this.filledLoad) {
this.filledLoad.classList.toggle('hide', !streamable);
}
this.media = media;
this.streamable = streamable;
if(!media.paused || media.currentTime > 0) {
this.onPlay();
}
let wasPlaying = false;
this.setSeekMax();
this.setListeners();
this.setHandlers({
onMouseDown: () => {
wasPlaying = !this.media.paused;
wasPlaying && this.media.pause();
},
onMouseUp: (e) => {
// cancelEvent(e.event);
wasPlaying && this.media.play();
}
});
}
protected onLoadedData = () => {
this.max = this.media.duration;
this.seek.setAttribute('max', '' + this.max);
};
protected onEnded = () => {
this.setProgress();
};
protected onPlay = () => {
let r = () => {
this.setProgress();
this.progressRAF = this.media.paused ? 0 : window.requestAnimationFrame(r);
};
if(this.progressRAF) {
window.cancelAnimationFrame(this.progressRAF);
}
if(this.streamable) {
this.setLoadProgress();
}
this.progressRAF = window.requestAnimationFrame(r);
};
protected onTimeUpdate = () => {
if(this.media.paused) {
this.setProgress();
if(this.streamable) {
this.setLoadProgress();
}
}
};
protected onProgress = (e: Event) => {
this.setLoadProgress();
};
protected scrub(e: GrabEvent) {
const scrubTime = super.scrub(e);
this.media.currentTime = scrubTime;
return scrubTime;
}
protected setLoadProgress() {
if(appMediaPlaybackController.isSafariBuffering(this.media)) return;
const buf = this.media.buffered;
const numRanges = buf.length;
const currentTime = this.media.currentTime;
let nearestStart = 0, end = 0;
for(let i = 0; i < numRanges; ++i) {
const start = buf.start(i);
if(currentTime >= start && start >= nearestStart) {
nearestStart = start;
end = buf.end(i);
}
//console.log('onProgress range:', i, buf.start(i), buf.end(i), this.media);
}
//console.log('onProgress correct range:', nearestStart, end, this.media);
const percents = this.media.duration ? end / this.media.duration : 0;
this.filledLoad.style.width = (percents * 100) + '%';
//this.filledLoad.style.transform = 'scaleX(' + percents + ')';
}
protected setSeekMax() {
this.max = this.media.duration || 0;
if(this.max > 0) {
this.onLoadedData();
} else {
this.media.addEventListener('loadeddata', this.onLoadedData);
}
}
public setProgress() {
if(appMediaPlaybackController.isSafariBuffering(this.media)) return;
const currentTime = this.media.currentTime;
super.setProgress(currentTime);
}
public setListeners() {
super.setListeners();
this.media.addEventListener('ended', this.onEnded);
this.media.addEventListener('play', this.onPlay);
this.media.addEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.addEventListener('progress', this.onProgress);
}
public removeListeners() {
super.removeListeners();
if(this.media) {
this.media.removeEventListener('loadeddata', this.onLoadedData);
this.media.removeEventListener('ended', this.onEnded);
this.media.removeEventListener('play', this.onPlay);
this.media.removeEventListener('timeupdate', this.onTimeUpdate);
this.streamable && this.media.removeEventListener('progress', this.onProgress);
}
if(this.progressRAF) {
window.cancelAnimationFrame(this.progressRAF);
this.progressRAF = 0;
}
}
}
export class VolumeSelector extends RangeSelector {
private static ICONS = ['volume_off', 'volume_mute', 'volume_down', 'volume_up'];
public btn: HTMLElement;
protected icon: HTMLSpanElement;
constructor(protected listenerSetter: ListenerSetter, protected vertical = false) {
super({
step: 0.01,
min: 0,
max: 1,
vertical
}, 1);
this.setListeners();
this.setHandlers({
onScrub: currentTime => {
const value = Math.max(Math.min(currentTime, 1), 0);
//console.log('volume scrub:', currentTime, value);
appMediaPlaybackController.muted = false;
appMediaPlaybackController.volume = value;
},
/* onMouseUp: (e) => {
cancelEvent(e.event);
} */
});
const className = 'player-volume';
const btn = this.btn = document.createElement('div');
btn.classList.add('btn-icon', className);
const icon = this.icon = document.createElement('span');
icon.classList.add(className + '__icon');
btn.append(icon, this.container);
attachClickEvent(icon, this.onMuteClick, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope)('media_playback_params', this.setVolume);
this.setVolume();
}
private onMuteClick = (e?: Event) => {
e && cancelEvent(e);
appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
};
private setVolume = () => {
// const volume = video.volume;
const {volume, muted} = appMediaPlaybackController;
let d: string;
let iconIndex: number;
if(!volume || muted) {
iconIndex = 0;
} else if(volume > .5) {
iconIndex = 3;
} else if(volume > 0 && volume < .25) {
iconIndex = 1;
} else {
iconIndex = 2;
}
VolumeSelector.ICONS.forEach(icon => this.icon.classList.remove('tgico-' + icon));
this.icon.classList.add('tgico-' + VolumeSelector.ICONS[iconIndex]);
if(!this.mousedown) {
this.setProgress(muted ? 0 : volume);
}
};
}
export default class VideoPlayer extends ControlsHover { export default class VideoPlayer extends ControlsHover {
private static PLAYBACK_RATES = [0.5, 1, 1.5, 2]; private static PLAYBACK_RATES = [0.5, 1, 1.5, 2];
@ -274,18 +30,21 @@ export default class VideoPlayer extends ControlsHover {
protected listenerSetter: ListenerSetter; protected listenerSetter: ListenerSetter;
protected playbackRateButton: HTMLElement; protected playbackRateButton: HTMLElement;
protected pipButton: HTMLElement;
/* protected videoParent: HTMLElement; /* protected videoParent: HTMLElement;
protected videoWhichChild: number; */ protected videoWhichChild: number; */
protected onPlaybackRackMenuToggle?: (open: boolean) => void; protected onPlaybackRackMenuToggle?: (open: boolean) => void;
protected onPip?: (pip: boolean) => void;
constructor({video, play = false, streamable = false, duration, onPlaybackRackMenuToggle}: { constructor({video, play = false, streamable = false, duration, onPlaybackRackMenuToggle, onPip}: {
video: HTMLVideoElement, video: HTMLVideoElement,
play?: boolean, play?: boolean,
streamable?: boolean, streamable?: boolean,
duration?: number, duration?: number,
onPlaybackRackMenuToggle?: (open: boolean) => void onPlaybackRackMenuToggle?: VideoPlayer['onPlaybackRackMenuToggle'],
onPip: VideoPlayer['onPip']
}) { }) {
super(); super();
@ -294,6 +53,7 @@ export default class VideoPlayer extends ControlsHover {
this.wrapper.classList.add('ckin__player'); this.wrapper.classList.add('ckin__player');
this.onPlaybackRackMenuToggle = onPlaybackRackMenuToggle; this.onPlaybackRackMenuToggle = onPlaybackRackMenuToggle;
this.onPip = onPip;
this.listenerSetter = new ListenerSetter(); this.listenerSetter = new ListenerSetter();
@ -347,6 +107,7 @@ export default class VideoPlayer extends ControlsHover {
if(skin === 'default') { if(skin === 'default') {
this.playbackRateButton = this.wrapper.querySelector('.playback-rate') as HTMLElement; this.playbackRateButton = this.wrapper.querySelector('.playback-rate') as HTMLElement;
this.pipButton = this.wrapper.querySelector('.pip') as HTMLElement;
const toggle = wrapper.querySelectorAll('.toggle') as NodeListOf<HTMLElement>; const toggle = wrapper.querySelectorAll('.toggle') as NodeListOf<HTMLElement>;
const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement; const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement;
@ -366,6 +127,25 @@ export default class VideoPlayer extends ControlsHover {
}); });
}); });
if(this.pipButton) {
listenerSetter.add(this.pipButton)('click', () => {
this.video.requestPictureInPicture();
});
const onPip = (pip: boolean) => {
this.wrapper.style.visibility = pip ? 'hidden': '';
this.onPip(pip);
};
listenerSetter.add(video)('enterpictureinpicture', () => {
onPip(true);
});
listenerSetter.add(video)('leavepictureinpicture', () => {
onPip(false);
});
}
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
listenerSetter.add(video)('click', () => { listenerSetter.add(video)('click', () => {
this.togglePlay(); this.togglePlay();
@ -407,17 +187,6 @@ export default class VideoPlayer extends ControlsHover {
}); });
} }
/* player.addEventListener('click', (e) => {
if(e.target !== player) {
return;
}
this.togglePlay();
}); */
/* video.addEventListener('play', () => {
}); */
listenerSetter.add(video)('dblclick', () => { listenerSetter.add(video)('dblclick', () => {
if(!IS_TOUCH_SUPPORTED) { if(!IS_TOUCH_SUPPORTED) {
this.toggleFullScreen(); this.toggleFullScreen();
@ -492,6 +261,7 @@ export default class VideoPlayer extends ControlsHover {
</div> </div>
<div class="right-controls"> <div class="right-controls">
<button class="btn-icon ${skin}__button btn-menu-toggle playback-rate night" title="Playback Rate"></button> <button class="btn-icon ${skin}__button btn-menu-toggle playback-rate night" title="Playback Rate"></button>
${!IS_MOBILE && document.pictureInPictureEnabled ? `<button class="btn-icon ${skin}__button pip tgico-pip" title="Picture-in-Picture"></button>` : ''}
<button class="btn-icon ${skin}__button fullscreen tgico-fullscreen" title="Full Screen"></button> <button class="btn-icon ${skin}__button fullscreen tgico-fullscreen" title="Full Screen"></button>
</div> </div>
</div> </div>
@ -593,10 +363,10 @@ export default class VideoPlayer extends ControlsHover {
} }
} }
public removeListeners() { public cleanup() {
super.cleanup(); super.cleanup();
this.listenerSetter.removeAll(); this.listenerSetter.removeAll();
this.progress.removeListeners(); this.progress.removeListeners();
this.onPlaybackRackMenuToggle = undefined; this.onPlaybackRackMenuToggle = this.onPip = undefined;
} }
} }

2
src/scss/partials/_chat.scss

@ -338,7 +338,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
font-size: 2rem; font-size: 2rem;
&:before { &:before {
font-weight: bold; font-weight: var(--font-weight-bold);
} }
} }

10
src/scss/partials/_chatBubble.scss

@ -543,6 +543,7 @@ $bubble-beside-button-width: 38px;
&.webpage { &.webpage {
.preview-with-document { .preview-with-document {
margin-bottom: 0 !important; margin-bottom: 0 !important;
min-width: 100%;
} }
.document { .document {
@ -552,6 +553,15 @@ $bubble-beside-button-width: 38px;
padding-left: 44px; padding-left: 44px;
} }
} }
.has-document {
display: flex;
flex-direction: column;
.preview-resizer {
order: 1;
}
}
} }
.preview-resizer { .preview-resizer {

5
src/scss/partials/_ckin.scss

@ -87,7 +87,6 @@
padding: 0; padding: 0;
position: absolute; position: absolute;
opacity: 1; opacity: 1;
visibility: visible;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate3d(-50%, -50%, 0) scale(1); transform: translate3d(-50%, -50%, 0) scale(1);
@ -98,7 +97,7 @@
} }
@include animation-level(2) { @include animation-level(2) {
transition: visibility var(--layer-transition), opacity var(--layer-transition); transition: opacity var(--layer-transition);
} }
@include respond-to(handhelds) { @include respond-to(handhelds) {
@ -110,7 +109,6 @@
&:not(.played) { &:not(.played) {
.default__button--big { .default__button--big {
opacity: 0; opacity: 0;
visibility: hidden;
} }
} }
@ -205,7 +203,6 @@
&.is-playing, &:not(.played) { &.is-playing, &:not(.played) {
.default__button--big { .default__button--big {
opacity: 0; opacity: 0;
visibility: hidden;
} }
.toggle:not(.default__button--big) { .toggle:not(.default__button--big) {

4
src/scss/partials/_poll.scss

@ -122,7 +122,7 @@ poll-element {
border-radius: 50%; border-radius: 50%;
height: 16px; height: 16px;
width: 16px; width: 16px;
font-weight: bold; font-weight: var(--font-weight-bold);
font-size: .75rem; font-size: .75rem;
opacity: 1; opacity: 1;
display: flex; display: flex;
@ -132,7 +132,7 @@ poll-element {
&:before { &:before {
content: $tgico-check; content: $tgico-check;
//margin-left: 1px; //margin-left: 1px;
font-weight: bold; font-weight: var(--font-weight-bold);
} }
} }

2
src/scss/partials/popups/_datePicker.scss

@ -192,7 +192,7 @@
} }
&-day { &-day {
font-weight: bold; font-weight: var(--font-weight-bold);
color: var(--primary-text-color) !important; color: var(--primary-text-color) !important;
font-size: 14px !important; font-size: 14px !important;
} }

2
src/scss/partials/popups/_joinChatInvite.scss

@ -17,7 +17,7 @@
} }
.chat-title { .chat-title {
font-weight: bold; font-weight: var(--font-weight-bold);
margin: .75rem 0 .25rem; margin: .75rem 0 .25rem;
line-height: var(--line-height); line-height: var(--line-height);
} }

7
src/scss/tgico.scss

@ -21,3 +21,10 @@ $tgico-font-path: "assets/fonts" !default;
@import "tgico/style"; @import "tgico/style";
@import "tgico/variables"; @import "tgico/variables";
.tgico-phone_filled {
&:before {
content: $tgico-endcall_filled;
transform: rotate(-135deg);
}
}

Loading…
Cancel
Save