From 35d49f0b98341ea0a1821c1c13efc45b30f482c9 Mon Sep 17 00:00:00 2001 From: DexxDing <82929929+DexxDing@users.noreply.github.com> Date: Tue, 1 Nov 2022 01:17:35 +1100 Subject: [PATCH] Implement: Inactive activity indicator on progress bar (#1039) * update utils.ts: add a tool function to detect inactive periods * update Controller.svelte: add a fixed div element as an indicator * update Controller.svelte: add one blank space at the end * update Controller.svelte: add a variable inactivePeriods and use util function to get inactive periods * update Controller.svelte: add width property for inactive activity indicators * update Controller.svelte: combine calculation value with indicator UI * update utils.ts: fix error https://github.com/HurricaHjz/rrweb_2120_ga_3/pull/5#discussion_r1008677230 and add comments update Controller.svelte: apply Zihan's suggestion https://github.com/HurricaHjz/rrweb_2120_ga_3/pull/5#discussion_r1008678403 * update Controller.svelte: make the color of indicator customizable update index.d.ts: add type definition for the color option Co-authored-by: u7149141 Co-authored-by: Jerry Zhang Co-authored-by: fengyun5264 <115444501+fengyun5264@users.noreply.github.com> Co-authored-by: Zihan Meng Co-authored-by: HurricaHjz <105645379+HurricaHjz@users.noreply.github.com> Co-authored-by: u6924169 Co-authored-by: Majia0712 <55265314+MengZihan712@users.noreply.github.com> --- packages/rrweb-player/src/Controller.svelte | 128 ++++++++++++++------ packages/rrweb-player/src/Player.svelte | 3 + packages/rrweb-player/src/utils.ts | 40 ++++++ packages/rrweb-player/typings/index.d.ts | 5 + 4 files changed, 142 insertions(+), 34 deletions(-) diff --git a/packages/rrweb-player/src/Controller.svelte b/packages/rrweb-player/src/Controller.svelte index 61c3d56a..b46f32b2 100644 --- a/packages/rrweb-player/src/Controller.svelte +++ b/packages/rrweb-player/src/Controller.svelte @@ -12,7 +12,7 @@ createEventDispatcher, afterUpdate, } from 'svelte'; - import { formatTime } from './utils'; + import { formatTime, getInactivePeriods } from './utils'; import Switch from './components/Switch.svelte'; const dispatch = createEventDispatcher(); @@ -24,6 +24,7 @@ export let speedOption: number[]; export let speed = speedOption.length ? speedOption[0] : 1; export let tags: Record = {}; + export let inactiveColor: string; let currentTime = 0; $: { @@ -59,6 +60,21 @@ background: string; position: string; }; + + /** + * Calculate the tag position (percent) to be displayed on the progress bar. + * @param startTime - The start time of the session. + * @param endTime - The end time of the session. + * @param tagTime - The time of the tag. + * @returns The position of the tag. unit: percentage + */ + function position(startTime: number, endTime: number, tagTime: number) { + const sessionDuration = endTime - startTime; + const eventDuration = endTime - tagTime; + const eventPosition = 100 - (eventDuration / sessionDuration) * 100; + return eventPosition.toFixed(2); + } + let customEvents: CustomEvent[]; $: customEvents = (() => { const { context } = replayer.service.state; @@ -67,15 +83,6 @@ const end = context.events[totalEvents - 1].timestamp; const customEvents: CustomEvent[] = []; - // calculate tag position. - const position = (startTime: number, endTime: number, tagTime: number) => { - const sessionDuration = endTime - startTime; - const eventDuration = endTime - tagTime; - const eventPosition = 100 - (eventDuration / sessionDuration) * 100; - - return eventPosition.toFixed(2); - }; - // loop through all the events and find out custom event. context.events.forEach((event) => { /** @@ -95,6 +102,43 @@ return customEvents; })(); + let inactivePeriods: { + name: string; + background: string; + position: string; + width: string; + }[]; + $: inactivePeriods = (() => { + try { + const { context } = replayer.service.state; + const totalEvents = context.events.length; + const start = context.events[0].timestamp; + const end = context.events[totalEvents - 1].timestamp; + const periods = getInactivePeriods(context.events); + // calculate the indicator width. + const getWidth = ( + startTime: number, + endTime: number, + tagStart: number, + tagEnd: number, + ) => { + const sessionDuration = endTime - startTime; + const eventDuration = tagEnd - tagStart; + const width = (eventDuration / sessionDuration) * 100; + return width.toFixed(2); + }; + return periods.map((period) => ({ + name: 'inactive period', + background: inactiveColor, + position: `${position(start, end, period[0])}%`, + width: `${getWidth(start, end, period[0], period[1])}%`, + })); + } catch (e) { + // For safety concern, if there is any error, the main function won't be affected. + return []; + } + })(); + const loopTimer = () => { stopTimer(); @@ -174,11 +218,11 @@ }; export const playRange = ( - timeOffset: number, - endTimeOffset: number, - startLooping: boolean = false, - afterHook: undefined | (() => void) = undefined, - ) => { + timeOffset: number, + endTimeOffset: number, + startLooping: boolean = false, + afterHook: undefined | (() => void) = undefined, + ) => { if (startLooping) { loop = { start: timeOffset, @@ -193,7 +237,6 @@ replayer.play(timeOffset); }; - const handleProgressClick = (event: MouseEvent) => { if (speedState === 'skipping') { return; @@ -207,7 +250,7 @@ percent = 1; } const timeOffset = meta.totalTime * percent; - finished = false + finished = false; goto(timeOffset); }; @@ -230,18 +273,18 @@ export const triggerUpdateMeta = () => { return Promise.resolve().then(() => { meta = replayer.getMetaData(); - }) - } + }); + }; onMount(() => { playerState = replayer.service.state.value; - speedState = replayer.speedService.state.value ; + speedState = replayer.speedService.state.value; replayer.on( 'state-change', (states: { player?: PlayerMachineState; speed?: SpeedMachineState }) => { const { player, speed } = states; if (player?.value && playerState !== player.value) { - playerState = player.value ; + playerState = player.value; switch (playerState) { case 'playing': loopTimer(); @@ -254,7 +297,7 @@ } } if (speed?.value && speedState !== speed.value) { - speedState = speed.value ; + speedState = speed.value; } }, ); @@ -384,17 +427,27 @@ class="rr-progress" class:disabled={speedState === 'skipping'} bind:this={progress} - on:click={handleProgressClick}> + on:click={handleProgressClick} + >
+ style="width: {percentage}" + /> + {#each inactivePeriods as period} +
+ {/each} {#each customEvents as event}
+ {event.position};" + /> {/each}
@@ -411,7 +464,8 @@ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" - height="16"> + height="16" + > + 12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" + /> {:else} + height="16" + > + 512l-388.66944-233.32864 0 466.65728z" + /> {/if} @@ -447,7 +504,8 @@ {/each} @@ -455,7 +513,8 @@ id="skip" bind:checked={skipInactive} disabled={speedState === 'skipping'} - label="skip inactive" /> + label="skip inactive" + />
diff --git a/packages/rrweb-player/src/Player.svelte b/packages/rrweb-player/src/Player.svelte index 634a74f3..5050bd5c 100644 --- a/packages/rrweb-player/src/Player.svelte +++ b/packages/rrweb-player/src/Player.svelte @@ -22,6 +22,8 @@ export let speed = 1; export let showController = true; export let tags: Record = {}; + // color of inactive periods indicator + export let inactiveColor = '#D4D4D4'; let replayer: Replayer; @@ -229,6 +231,7 @@ {speedOption} {skipInactive} {tags} + {inactiveColor} on:fullscreen={() => toggleFullscreen()} /> {/if} diff --git a/packages/rrweb-player/src/utils.ts b/packages/rrweb-player/src/utils.ts index bee0bde5..796d1810 100644 --- a/packages/rrweb-player/src/utils.ts +++ b/packages/rrweb-player/src/utils.ts @@ -15,6 +15,9 @@ declare global { } } +import { EventType, IncrementalSource } from 'rrweb'; +import type { eventWithTime } from 'rrweb/typings/types'; + export function inlineCss(cssObj: Record): string { let style = ''; Object.keys(cssObj).forEach((key) => { @@ -141,3 +144,40 @@ export function typeOf( // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return map[toString.call(obj)]; } + +/** + * Forked from 'rrweb' replay/index.ts. The original function is not exported. + * Determine whether the event is a user interaction event + * @param event - event to be determined + * @returns true if the event is a user interaction event + */ +function isUserInteraction(event: eventWithTime): boolean { + if (event.type !== EventType.IncrementalSnapshot) { + return false; + } + return ( + event.data.source > IncrementalSource.Mutation && + event.data.source <= IncrementalSource.Input + ); +} + +// Forked from 'rrweb' replay/index.ts. A const threshold of inactive time. +const SKIP_TIME_THRESHOLD = 10 * 1000; + +/** + * Get periods of time when no user interaction happened from a list of events. + * @param events - all events + * @returns periods of time consist with [start time, end time] + */ +export function getInactivePeriods(events: eventWithTime[]) { + const inactivePeriods: [number, number][] = []; + let lastActiveTime = events[0].timestamp; + for (const event of events) { + if (!isUserInteraction(event)) continue; + if (event.timestamp - lastActiveTime > SKIP_TIME_THRESHOLD) { + inactivePeriods.push([lastActiveTime, event.timestamp]); + } + lastActiveTime = event.timestamp; + } + return inactivePeriods; +} diff --git a/packages/rrweb-player/typings/index.d.ts b/packages/rrweb-player/typings/index.d.ts index 6d15307b..b6d980b0 100644 --- a/packages/rrweb-player/typings/index.d.ts +++ b/packages/rrweb-player/typings/index.d.ts @@ -50,6 +50,11 @@ export type RRwebPlayerOptions = { * @defaultValue `{}` */ tags?: Record; + /** + * Customize the color of inactive periods indicator in the progress bar with a valid CSS color string. + * @defaultValue `#D4D4D4` + */ + inactiveColor?: string; } & Partial; };