openreplay/frontend/app/components/Session_/Highlight/HighlightPanel.tsx
Delirium 2cd96b0df0
Highlight UI (#2951)
* ui: start highlight ui

* ui: tag items

* ui: connecting highlights to notes api...

* Highlight feature refinements (#2948)

* ui: move clips player to foss, connect notes api to hl

* ui: tune note/hl editing, prevent zoom slider body from jumping around

* ui: safe check for tag

* ui: fix thumbnail gen

* ui: fix thumbnail gen

* ui: make player modal wider, add shadow

* ui: custom warn barge for clips

* ui: swap icon for note event wrapper

* ui: rm other, fix cancel

* ui: moving around creation modal

* ui: bg tint

* ui: rm disabled for text btn

* ui: fix ownership sorting

* ui: close player on bg click

* ui: fix query, fix min distance for default range

* ui: move hl list header out of list comp

* ui: spot list header segmented size

* Various improvements in highlights (#2955)

* ui: update hl in hlPanel comp

* ui: rm debug

* ui: fix icons file

---------

Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
2025-01-24 09:59:54 +01:00

266 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { Button, Checkbox, Input, Tag } from 'antd';
import { X } from 'lucide-react';
import { TAGS, iTag, tagProps } from 'App/services/NotesService';
import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { PlayerContext } from 'Components/Session/playerContext';
import { observer } from 'mobx-react-lite';
import { shortDurationFromMs } from 'App/date';
import { toast } from 'react-toastify';
function maskDuration(input: string): string {
const digits = input.replace(/\D/g, '');
const limitedDigits = digits.slice(0, 4);
if (limitedDigits.length <= 2) {
return limitedDigits;
}
return `${limitedDigits.slice(0, 2)}:${limitedDigits.slice(2)}`;
}
const duration = new RegExp(/(\d{2}):(\d{2})/);
function HighlightPanel({ onClose }: { onClose: () => void }) {
const { uiPlayerStore, notesStore, sessionStore } = useStore();
const editNote = notesStore.editNote;
const [message, setMessage] = React.useState(editNote?.message ?? '');
const [isPublic, setIsPublic] = React.useState(editNote?.isPublic ?? false);
const { store, player } = React.useContext(PlayerContext);
const currentTime = store.get().time;
const startTsStr = shortDurationFromMs(
editNote?.startAt ?? uiPlayerStore.highlightSelection.startTs
);
const endTsStr = shortDurationFromMs(
editNote?.endAt ?? uiPlayerStore.highlightSelection.endTs
);
const [startTs, setStartTs] = React.useState(startTsStr);
const [endTs, setEndTs] = React.useState(endTsStr);
const [tag, setTag] = React.useState(editNote?.tag ?? TAGS[0]);
const onStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newState = maskDuration(e.target.value)
setStartTs(newState);
if (duration.test(newState)) {
const [_, minutes, seconds] = duration.exec(newState) ?? [];
const newTime = (parseInt(minutes) * 60 + parseInt(seconds))*1000;
const sessLength = store.get().endTime;
uiPlayerStore.toggleHighlightSelection({
enabled: true,
range: [Math.min(newTime, sessLength), uiPlayerStore.highlightSelection.endTs],
})
}
};
const onEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newState = maskDuration(e.target.value)
setEndTs(newState);
if (duration.test(newState)) {
const [_, minutes, seconds] = duration.exec(newState) ?? [];
const newTime = (parseInt(minutes) * 60 + parseInt(seconds))*1000;
const sessLength = store.get().endTime;
uiPlayerStore.toggleHighlightSelection({
enabled: true,
range: [uiPlayerStore.highlightSelection.startTs, Math.min(newTime, sessLength)],
})
}
};
const playing = store.get().playing;
React.useEffect(() => {
player.pause();
const time = store.get().time;
const endTime = store.get().endTime;
const distance = Math.max(endTime / 40, 2500);
uiPlayerStore.toggleHighlightSelection({
enabled: true,
range: [Math.max(time - distance, 0), Math.min(time + distance, endTime)],
});
return () => {
uiPlayerStore.toggleHighlightSelection({
enabled: false,
});
notesStore.setEditNote(null)
};
}, []);
React.useEffect(() => {
const startStr = shortDurationFromMs(
uiPlayerStore.highlightSelection.startTs
);
const endStr = shortDurationFromMs(uiPlayerStore.highlightSelection.endTs);
setStartTs(startStr);
setEndTs(endStr);
}, [
uiPlayerStore.highlightSelection.startTs,
uiPlayerStore.highlightSelection.endTs,
]);
React.useEffect(() => {
player.pause();
}, [playing]);
const addTag = (newTag: iTag) => {
setTag(newTag);
};
const tagActive = (checkedTag: iTag) => {
return tag === checkedTag;
};
const onSave = async () => {
try {
notesStore.setSaving(true)
const playerContainer = document.querySelector('iframe')?.contentWindow?.document.body;
let thumbnail;
if (playerContainer) {
thumbnail = await elementToImage(playerContainer);
}
const note = {
message,
tag: tag,
isPublic,
timestamp: parseInt(currentTime, 10),
startAt: parseInt(uiPlayerStore.highlightSelection.startTs, 10),
endAt: parseInt(uiPlayerStore.highlightSelection.endTs, 10),
thumbnail,
}
if (editNoteId) {
await notesStore.updateNote(editNoteId, note);
toast.success('Highlight updated');
} else {
const sessionId = sessionStore.current.sessionId;
await notesStore.addNote(sessionId, note);
toast.success('Highlight saved. Find it in Home > Highlights');
}
onClose();
} catch (e) {
toast.error('Failed to save highlight');
} finally {
notesStore.setSaving(false);
}
}
return (
<div
className={'w-full p-4 flex flex-col gap-4'}
style={{ width: 270 }}
onClick={(e) => e.stopPropagation()}
>
<div className={'flex items-center gap-2'}>
<Icon name="chat-square-quote" color="inherit" size={16} />
<h3 className={'text-xl font-semibold'}>
{editNote ? 'Edit ' : ''}Highlight
</h3>
<div className={'cursor-pointer ml-auto'} onClick={onClose}>
<X size={18} strokeWidth={2} />
</div>
</div>
<div className="text-sm text-neutral-500">
Save key moments from sessions. Access them anytime on the Highlights
page to share with your team.
</div>
<div>
<Input.TextArea
onChange={(e) => setMessage(e.target.value)}
placeholder={'Enter Comments'}
maxLength={200}
rows={6}
value={message}
className="rounded-lg"
autoFocus
/>
<div className={'text-disabled-text text-sm'}>
{message.length}/200 characters remaining
</div>
</div>
<div className={'flex items-center gap-2'}>
<div>
<div className={'font-semibold'}>From</div>
<Input
value={startTs}
onChange={onStartChange}
className="rounded-lg"
/>
</div>
<div>
<div className={'font-semibold'}>To</div>
<Input value={endTs} onChange={onEndChange} className="rounded-lg" />
</div>
</div>
<div className={'flex items-center gap-2 flex-wrap'}>
{TAGS.map((tag) => (
<Tag
onClick={() => addTag(tag)}
key={tag}
className="cursor-pointer rounded-lg hover:bg-indigo-50 mr-0"
color={tagProps[tag]}
bordered={false}
>
<div className={'flex items-center gap-1 text-sm'}>
{tagActive(tag) ? (
<Icon name="check-circle-fill" color="inherit" size={13} />
) : null}
{tag}
</div>
</Tag>
))}
</div>
<div>
<Checkbox
onChange={(e) => setIsPublic(e.target.checked)}
value={isPublic}
className="ms-2"
>
Visible to team members
</Checkbox>
</div>
<div className={'flex items-center gap-2'}>
<Button
onClick={onSave}
type={'primary'}
loading={notesStore.isSaving}
className="font-medium"
>
<Icon name="chat-square-quote" color="inherit" size={14} /> {editNote ? 'Update ' : 'Save '}
Highlight
</Button>
<Button onClick={onClose} type="text" className="font-medium">
Cancel
</Button>
</div>
</div>
);
}
window.__debugElementToImage = (el) => elementToImage(el).then(img => {
const a = document.createElement('a');
a.href = img;
a.download = 'highlight.png';
a.click();
});
function elementToImage(el) {
return import('html2canvas').then(({ default: html2canvas }) => {
return html2canvas(
el,
{
scale: 1,
allowTaint: true,
useCORS: false,
logging: true,
foreignObjectRendering: false,
height: 900,
width: 1200,
x: 0,
y: 0,
}
).then((canvas) => {
return canvas.toDataURL('img/png');
}).catch(e => {
console.log(e);
return undefined
});
})
}
export default observer(HighlightPanel);