Merge pull request #959 from openreplay/clickmap-fix

fix(player, frontend): use ScaleMode for screen
refactor(player-ui): remove isClickmap from ReplayPlayer
refactor(player): pass MarkerClick callback on targets ingection
This commit is contained in:
Alex K 2023-01-30 16:02:38 +01:00 committed by GitHub
commit d2bf74f7e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 112 additions and 141 deletions

View file

@ -7,21 +7,15 @@ import { PlayerContext } from 'App/components/Session/playerContext';
import { observer } from 'mobx-react-lite'
function Player() {
const [wrapperHeight, setWrapperHeight] = React.useState(0);
const playerContext = React.useContext(PlayerContext);
const screenWrapper = React.useRef<HTMLDivElement>(null);
const portHeight = playerContext.store.get().portHeight
React.useEffect(() => {
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null;
if (parentElement) {
playerContext.player.attach(parentElement);
playerContext.player.play();
}
}, []);
React.useEffect(() => {
setWrapperHeight(portHeight)
}, [portHeight]);
if (!playerContext.player) return null;
@ -31,7 +25,7 @@ function Player() {
>
<div className={cn("relative flex-1", 'overflow-visible')}>
<Overlay isClickmap />
<div className={cn(stl.screenWrapper, '!overflow-y-scroll')} style={{ height: wrapperHeight, maxHeight: 800 }} ref={screenWrapper} />
<div className={cn(stl.screenWrapper, '!overflow-y-scroll')} style={{ maxHeight: 800 }} ref={screenWrapper} />
</div>
</div>
);

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { createWebPlayer } from 'Player';
import { createClickMapPlayer } from 'Player';
import { makeAutoObservable } from 'mobx';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import PlayerContent from './ThinPlayerContent';
@ -20,11 +20,10 @@ function WebPlayer(props: any) {
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
useEffect(() => {
const [WebPlayerInst, PlayerStore] = createWebPlayer(customSession, (state) =>
const [WebPlayerInst, PlayerStore] = createClickMapPlayer(customSession, (state) =>
makeAutoObservable(state)
);
setContextValue({ player: WebPlayerInst, store: PlayerStore });
WebPlayerInst.setMarkerClick(onMarkerClick)
return () => WebPlayerInst.clean();
}, [session.sessionId]);
@ -35,10 +34,10 @@ function WebPlayer(props: any) {
contextValue.player && contextValue.player.play()
if (isPlayerReady && insights.size > 0) {
setTimeout(() => {
contextValue.player.jump(jumpTimestamp)
contextValue.player.pause()
contextValue.player.scaleFullPage()
setTimeout(() => { contextValue.player.showClickmap(insights) }, 250)
contextValue.player.jump(jumpTimestamp)
contextValue.player.scale()
setTimeout(() => { contextValue.player.showClickmap(insights, onMarkerClick) }, 250)
}, 500)
}
return () => {

View file

@ -13,7 +13,6 @@ interface IProps {
activeTab: string;
jiraConfig: Record<string, any>
fullView?: boolean
isClickmap?: boolean
}
function PlayerBlock(props: IProps) {
@ -24,14 +23,12 @@ function PlayerBlock(props: IProps) {
activeTab,
jiraConfig,
fullView = false,
isClickmap
} = props;
const shouldShowSubHeader = !fullscreen && !fullView && !isClickmap
const shouldShowSubHeader = !fullscreen && !fullView
return (
<div
className={cn(styles.playerBlock, 'flex flex-col', !isClickmap ? 'overflow-x-hidden' : 'overflow-visible')}
style={{ zIndex: isClickmap ? 1 : undefined, minWidth: isClickmap ? '100%' : undefined }}
className={cn(styles.playerBlock, 'flex flex-col', 'overflow-x-hidden')}
>
{shouldShowSubHeader ? (
<SubHeader sessionId={sessionId} disabled={disabled} jiraConfig={jiraConfig} />
@ -39,7 +36,6 @@ function PlayerBlock(props: IProps) {
<Player
activeTab={activeTab}
fullView={fullView}
isClickmap={isClickmap}
/>
</div>
);

View file

@ -17,11 +17,10 @@ interface IProps {
fullscreen: boolean;
activeTab: string;
setActiveTab: (tab: string) => void;
isClickmap: boolean;
session: Session
}
function PlayerContent({ session, fullscreen, activeTab, setActiveTab, isClickmap }: IProps) {
function PlayerContent({ session, fullscreen, activeTab, setActiveTab }: IProps) {
const { store } = React.useContext(PlayerContext)
const {
@ -60,7 +59,7 @@ function PlayerContent({ session, fullscreen, activeTab, setActiveTab, isClickma
style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)' } : undefined}
>
<div className={cn(styles.session, 'relative')} data-fullscreen={fullscreen}>
<PlayerBlock activeTab={activeTab} isClickmap={isClickmap} />
<PlayerBlock activeTab={activeTab} />
</div>
</div>
{activeTab !== '' && (

View file

@ -43,7 +43,6 @@ interface IProps {
nextId: string;
sessionId: string;
activeTab: string;
isClickmap?: boolean;
updateLastPlayedSession: (id: string) => void
}

View file

@ -9,7 +9,6 @@ import withLocationHandlers from 'HOCs/withLocationHandlers';
import { useStore } from 'App/mstore';
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
import { fetchList as fetchMembers } from 'Duck/member';
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
import { observer } from 'mobx-react-lite';
@ -28,7 +27,6 @@ function WebPlayer(props: any) {
fullscreen,
fetchList,
customSession,
isClickmap,
insights,
jumpTimestamp,
onMarkerClick,
@ -41,28 +39,21 @@ function WebPlayer(props: any) {
const [contextValue, setContextValue] = useState<IPlayerContext>(defaultContextValue);
useEffect(() => {
if (!isClickmap) {
fetchList('issues');
}
const usedSession = isClickmap && customSession ? customSession : session;
fetchList('issues');
const [WebPlayerInst, PlayerStore] = createWebPlayer(usedSession, (state) =>
const [WebPlayerInst, PlayerStore] = createWebPlayer(session, (state) =>
makeAutoObservable(state)
);
setContextValue({ player: WebPlayerInst, store: PlayerStore });
if (!isClickmap) {
notesStore.fetchSessionNotes(session.sessionId).then((r) => {
const note = props.query.get('note');
if (note) {
WebPlayerInst.pause();
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r));
setShowNote(true);
}
});
} else {
WebPlayerInst.setMarkerClick(onMarkerClick)
}
notesStore.fetchSessionNotes(session.sessionId).then((r) => {
const note = props.query.get('note');
if (note) {
WebPlayerInst.pause();
setNoteItem(notesStore.getNoteById(parseInt(note, 10), r));
setShowNote(true);
}
})
const jumpToTime = props.query.get('jumpto');
const freeze = props.query.get('freeze')
@ -100,34 +91,29 @@ function WebPlayer(props: any) {
return (
<PlayerContext.Provider value={contextValue}>
<>
{!isClickmap ? (
<PlayerBlockHeader
// @ts-ignore TODO?
activeTab={activeTab}
setActiveTab={setActiveTab}
tabs={TABS}
fullscreen={fullscreen}
<PlayerBlockHeader
// @ts-ignore TODO?
activeTab={activeTab}
setActiveTab={setActiveTab}
tabs={TABS}
fullscreen={fullscreen}
/>
{/* @ts-ignore */}
<PlayerContent
activeTab={activeTab}
fullscreen={fullscreen}
setActiveTab={setActiveTab}
session={session}
/>
<Modal open={showNoteModal} onClose={onNoteClose}>
{showNoteModal ? (
<ReadNote
note={noteItem}
onClose={onNoteClose}
notFound={!noteItem}
/>
) : null}
{/* @ts-ignore */}
<PlayerContent
activeTab={activeTab}
fullscreen={fullscreen}
setActiveTab={setActiveTab}
session={session}
isClickmap={isClickmap}
/>
<Modal open={showNoteModal} onClose={onNoteClose}>
{showNoteModal ? (
<ReadNote
note={noteItem}
onClose={onNoteClose}
notFound={!noteItem}
/>
) : null}
</Modal>
</>
</Modal>
</PlayerContext.Provider>
);
}
@ -146,6 +132,5 @@ export default connect(
toggleFullscreen,
closeBottomBlock,
fetchList,
fetchMembers,
}
)(withLocationHandlers()(observer(WebPlayer)));

View file

@ -27,6 +27,18 @@ export function createWebPlayer(session: Record<string, any>, wrapStore?: (s:IWe
}
export function createClickMapPlayer(session: Record<string, any>, wrapStore?: (s:IWebPlayerStore) => IWebPlayerStore): [IWebPlayer, IWebPlayerStore] {
let store: WebPlayerStore = new SimpleStore<WebState>({
...WebPlayer.INITIAL_STATE,
})
if (wrapStore) {
store = wrapStore(store)
}
const player = new WebPlayer(store, session, false, true)
return [player, store]
}
export function createLiveWebPlayer(session: Record<string, any>, config: RTCIceServer[], wrapStore?: (s:IWebLivePlayerStore) => IWebLivePlayerStore): [IWebLivePlayer, IWebLivePlayerStore] {
let store: WebLivePlayerStore = new SimpleStore<WebLiveState>({
...WebLivePlayer.INITIAL_STATE,

View file

@ -153,15 +153,18 @@ export default class Animator {
}
freeze() {
if (this.store.get().ready) {
// making sure that replay is displayed completely
setTimeout(() => {
this.store.update({ freeze: true })
this.pause()
}, 250)
} else {
setTimeout(() => this.freeze(), 500)
}
return new Promise<void>(res => {
if (this.store.get().ready) {
// making sure that replay is displayed completely
setTimeout(() => {
this.store.update({ freeze: true })
this.pause()
res()
}, 250)
} else {
setTimeout(() => res(this.freeze()), 500)
}
})
}
togglePlay = () => {

View file

@ -12,6 +12,13 @@ export const INITIAL_STATE: State = {
}
export enum ScaleMode {
Embed,
//AdjustParentWidth
AdjustParentHeight,
}
function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] {
// @ts-ignore (IE, Edge)
if (typeof doc.msElementsFromRect === 'function') {
@ -55,9 +62,9 @@ export default class Screen {
private readonly iframe: HTMLIFrameElement;
private readonly screen: HTMLDivElement;
private parentElement: HTMLElement | null = null;
private parentElement: HTMLElement | null = null
constructor(isMobile: boolean) {
constructor(isMobile: boolean, private scaleMode: ScaleMode = ScaleMode.Embed) {
const iframe = document.createElement('iframe');
iframe.className = styles.iframe;
this.iframe = iframe;
@ -79,11 +86,10 @@ export default class Screen {
attach(parentElement: HTMLElement) {
if (this.parentElement) {
this.parentElement = null
console.error("BaseScreen: Trying to attach an attached screen.");
console.warn("BaseScreen: reattaching the screen.");
}
parentElement.appendChild(this.screen);
this.parentElement = parentElement;
/* == For the Inspecting Document content == */
@ -124,6 +130,7 @@ export default class Screen {
private boundingRect: DOMRect | null = null;
private getBoundingClientRect(): DOMRect {
if (this.boundingRect === null) {
// TODO: use this.screen instead in order to separate overlay functionality
return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation?
}
return this.boundingRect
@ -200,48 +207,41 @@ export default class Screen {
if (!this.parentElement) return;
const { offsetWidth, offsetHeight } = this.parentElement;
this.scaleRatio = Math.min(offsetWidth / width, offsetHeight / height);
if (this.scaleRatio > 1) {
this.scaleRatio = 1;
} else {
this.scaleRatio = Math.round(this.scaleRatio * 1e3) / 1e3;
let translate = ""
let posStyles = {}
switch (this.scaleMode) {
case ScaleMode.Embed:
this.scaleRatio = Math.min(offsetWidth / width, offsetHeight / height)
translate = "translate(-50%, -50%)"
break;
case ScaleMode.AdjustParentHeight:
this.scaleRatio = offsetWidth / width
translate = "translate(-50%, 0)"
posStyles = { top: 0 }
break;
}
this.screen.style.transform = `scale(${ this.scaleRatio }) translate(-50%, -50%)`;
this.screen.style.width = width + 'px';
this.screen.style.height = height + 'px';
this.iframe.style.width = width + 'px';
this.iframe.style.height = height + 'px';
this.boundingRect = this.overlay.getBoundingClientRect();
}
scaleFullPage() {
if (!this.parentElement || !this.document) return;
const { width: boxWidth } = this.parentElement.getBoundingClientRect();
const { height, width } = this.document.body.getBoundingClientRect();
this.overlay.remove()
this.scaleRatio = boxWidth/width;
if (this.scaleRatio > 1) {
this.scaleRatio = 1;
} else {
this.scaleRatio = Math.round(this.scaleRatio * 1e3) / 1e3;
}
Object.assign(this.screen.style, {
top: '0',
left: '50%',
if (this.scaleMode === ScaleMode.AdjustParentHeight) {
this.parentElement.style.height = this.scaleRatio * height + 'px'
}
Object.assign(this.screen.style, posStyles, {
height: height + 'px',
width: width + 'px',
transform: `scale(${this.scaleRatio}) translate(-50%, 0)`,
transform: `scale(${this.scaleRatio}) ${translate}`,
})
Object.assign(this.iframe.style, {
width: width + 'px',
height: height + 'px',
})
return height
this.boundingRect = this.overlay.getBoundingClientRect();
}
}

View file

@ -6,7 +6,8 @@ import Player from '../player/Player'
import MessageManager from './MessageManager'
import InspectorController from './addons/InspectorController'
import TargetMarker from './addons/TargetMarker'
import Screen from './Screen/Screen'
import Screen, { ScaleMode } from './Screen/Screen'
// export type State = typeof WebPlayer.INITIAL_STATE
@ -17,7 +18,6 @@ export default class WebPlayer extends Player {
...MessageManager.INITIAL_STATE,
inspectorMode: false,
portHeight: 0,
}
private readonly inspectorController: InspectorController
@ -26,7 +26,7 @@ export default class WebPlayer extends Player {
private targetMarker: TargetMarker
constructor(protected wpState: Store<typeof WebPlayer.INITIAL_STATE>, session: any, live: boolean) {
constructor(protected wpState: Store<typeof WebPlayer.INITIAL_STATE>, session: any, live: boolean, isClickMap = false) {
let initialLists = live ? {} : {
event: session.events || [],
stack: session.stackEvents || [],
@ -40,7 +40,7 @@ export default class WebPlayer extends Player {
) || [],
}
const screen = new Screen(session.isMobile)
const screen = new Screen(session.isMobile, isClickMap ? ScaleMode.AdjustParentHeight : ScaleMode.Embed)
const messageManager = new MessageManager(session, wpState, screen, initialLists)
super(wpState, messageManager)
this.screen = screen
@ -71,20 +71,11 @@ export default class WebPlayer extends Player {
}
scale = () => {
console.log('called scale')
const { width, height } = this.wpState.get()
this.screen.scale({ width, height })
this.inspectorController.scale({ width, height })
// this.updateMarketTargets() ??
}
scaleFullPage = () => {
window.removeEventListener('resize', this.scale)
window.addEventListener('resize', this.scaleFullPage)
const portHeight = this.screen.scaleFullPage()
return this.wpState.update({ portHeight })
this.targetMarker.updateMarkedTargets()
}
// Inspector & marker
@ -119,16 +110,11 @@ export default class WebPlayer extends Player {
}
showClickmap = (...args: Parameters<TargetMarker['injectTargets']>) => {
this.freeze()
if (this.wpState.get().portHeight !== 0) {
this.screen.overlay.remove() // hack. TODO: 1.split Screen functionalities (overlay, mounter) 2. separate ClickMapPlayer class that does not create overlay
this.targetMarker.injectTargets(...args)
this.freeze().then(() => {
this.targetMarker.injectTargets(...args)
} else {
setTimeout(() => this.showClickmap(...args), 500)
}
}
setMarkerClick = (...args: Parameters<TargetMarker['setOnMarkerClick']>) => {
this.targetMarker.setOnMarkerClick(...args)
})
}
toggleUserName = (name?: string) => {

View file

@ -39,7 +39,6 @@ export default class TargetMarker {
private clickMapOverlay: HTMLDivElement | null = null
private clickContainers: HTMLDivElement[] = []
private smallClicks: HTMLDivElement[] = []
private onMarkerClick: (selector: string, innerText: string) => void
static INITIAL_STATE: State = {
markedTargets: null,
activeTargetIndex: 0
@ -50,7 +49,7 @@ export default class TargetMarker {
private readonly store: Store<State>,
) {}
updateMarketTargets() {
updateMarkedTargets() {
const { markedTargets } = this.store.get()
if (markedTargets) {
this.store.update({
@ -137,7 +136,10 @@ export default class TargetMarker {
}
injectTargets(selections: { selector: string, count: number, clickRage?: boolean }[] | null) {
injectTargets(
selections: { selector: string, count: number, clickRage?: boolean }[] | null,
onMarkerClick?: (selector: string, innerText: string) => void,
) {
if (selections) {
const totalCount = selections.reduce((a, b) => {
return a + b.count
@ -183,7 +185,7 @@ export default class TargetMarker {
border.onclick = (e) => {
e.stopPropagation()
const innerText = el.innerText.length > 25 ? `${el.innerText.slice(0, 20)}...` : el.innerText
this.onMarkerClick?.(s.selector, innerText)
onMarkerClick?.(s.selector, innerText)
this.clickContainers.forEach(container => {
if (container.id === containerId) {
container.style.visibility = "visible"
@ -202,7 +204,7 @@ export default class TargetMarker {
overlay.onclick = (e) => {
e.stopPropagation()
this.onMarkerClick('', '')
onMarkerClick?.('', '')
this.clickContainers.forEach(container => {
container.style.visibility = "hidden"
})
@ -228,8 +230,4 @@ export default class TargetMarker {
}
}
setOnMarkerClick(cb: (selector: string) => void) {
this.onMarkerClick = cb
}
}