Skip to content

Commit

Permalink
add entry for now and set interval to 5 minutes
Browse files Browse the repository at this point in the history
  • Loading branch information
riknoll committed Sep 21, 2023
1 parent 71ec641 commit 4de7504
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 81 deletions.
55 changes: 14 additions & 41 deletions webapp/src/dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -860,59 +860,27 @@ export function isDontShowDownloadDialogFlagSet() {
}

export async function showTurnBackTimeDialogAsync(header: pxt.workspace.Header, reloadHeader: () => void) {
const history = (await workspace.getScriptHistoryAsync(header)).entries;
const text = await workspace.getTextAsync(header.id);
let history: pxt.workspace.HistoryEntry[] = [];

const getTextAtTimestamp = (timestamp: number) => {
let currentText = {...text};
const newHeader = {
...header
};

for (let i = 0; i < history.length; i++) {
const index = history.length - 1 - i;
const entry = history[index];
currentText = workspace.applyDiff(currentText, entry);
if (entry.timestamp === timestamp) {
const version = index > 0 ? history[index - 1].editorVersion : entry.editorVersion;

// Attempt to update the version in pxt.json
try {
const config = JSON.parse(currentText[pxt.CONFIG_NAME]) as pxt.PackageConfig;
if (config.targetVersions) {
config.targetVersions.target = version;
}
currentText[pxt.CONFIG_NAME] = JSON.stringify(config, null, 4);
}
catch (e) {
}

// Also set version in the header; this is what the compiler actually checks when applying upgrades
newHeader.targetVersion = version;
break;
}
}

return [newHeader, currentText] as [pxt.workspace.Header, pxt.workspace.ScriptText];
if (text?.[pxt.HISTORY_FILE]) {
history = JSON.parse(text[pxt.HISTORY_FILE]).entries;
}

const onTimestampSelect = async (timestamp: number) => {
const loadProject = async (text: pxt.workspace.ScriptText, editorVersion: string) => {
core.hideDialog();

const [newHeader, text] = getTextAtTimestamp(timestamp);
header.targetVersion = newHeader.targetVersion;
header.targetVersion = editorVersion;

await workspace.saveAsync(header, text);
reloadHeader();
}

const onCopySelect = async (timestamp: number) => {
const copyProject = async (text: pxt.workspace.ScriptText, editorVersion: string, timestamp?: number) => {
core.hideDialog();

const [newHeader, text] = getTextAtTimestamp(timestamp);

const newHistory: pxt.workspace.HistoryFile = {
entries: history.slice(0, history.findIndex(e => e.timestamp === timestamp))
entries: timestamp == undefined ? history : history.slice(0, history.findIndex(e => e.timestamp === timestamp))
}

if (text[pxt.HISTORY_FILE]) {
Expand All @@ -936,6 +904,11 @@ export async function showTurnBackTimeDialogAsync(header: pxt.workspace.Header,
} as any
);

const newHeader: pxt.workspace.Header = {
...header,
targetVersion: editorVersion
}

await workspace.duplicateAsync(newHeader, `${newHeader.name} ${dateString} ${timeString}`, text);

invalidate("headers:");
Expand All @@ -950,8 +923,8 @@ export async function showTurnBackTimeDialogAsync(header: pxt.workspace.Header,
<TimeMachine
history={history}
text={text}
onTimestampSelect={onTimestampSelect}
onCopySelect={onCopySelect}
onProjectLoad={loadProject}
onProjectCopy={copyProject}
/>
)
})
Expand Down
106 changes: 86 additions & 20 deletions webapp/src/timeMachine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { hideDialog } from "./core";
import { FocusTrap } from "../../react-common/components/controls/FocusTrap";

interface TimeMachineProps {
onTimestampSelect: (timestamp: number) => void;
onCopySelect: (timestamp: number) => void;
onProjectLoad: (text: pxt.workspace.ScriptText, editorVersion: string, timestamp?: number) => void;
onProjectCopy: (text: pxt.workspace.ScriptText, editorVersion: string, timestamp?: number) => void;
text: pxt.workspace.ScriptText;
history: pxt.workspace.HistoryEntry[];
}
Expand All @@ -18,12 +18,18 @@ interface PendingMessage {
handler: (response: any) => void;
}

interface TimeEntry {
label: string;
timestamp: number;
}

type FrameState = "loading" | "loaded" | "loading-project" | "loaded-project";

export const TimeMachine = (props: TimeMachineProps) => {
const { text, history, onTimestampSelect, onCopySelect } = props;
const { text, history, onProjectLoad, onProjectCopy } = props;

const [selected, setSelected] = React.useState<number>(history[history.length - 1]?.timestamp);
// -1 here is a standin for "now"
const [selected, setSelected] = React.useState<number>(-1);
const [loading, setLoading] = React.useState<FrameState>("loading")

const iframeRef = React.useRef<HTMLIFrameElement>();
Expand Down Expand Up @@ -135,30 +141,47 @@ export const TimeMachine = (props: TimeMachineProps) => {

React.useEffect(() => {
if (loading === "loaded" && importProject.current) {
const previewFiles = applyUntilTimestamp(text, history, history[history.length - 1].timestamp);
importProject.current(previewFiles)
importProject.current(text)
}
}, [loading, importProject.current, history, text]);
}, [loading, importProject.current, text]);

const onTimeSelected = (newValue: number) => {
if (importProject.current) {
setSelected(newValue);
const previewFiles = applyUntilTimestamp(text, history, newValue);
importProject.current(previewFiles)

if (newValue === -1) {
importProject.current(text);
}
else {
const previewFiles = applyUntilTimestamp(text, history, newValue);
importProject.current(previewFiles)
}
}
};

const onGoPressed = React.useCallback(() => {
onTimestampSelect(selected);
}, [selected, onTimestampSelect]);
if (selected === -1) {
hideDialog();
}
else {
const previewFiles = applyUntilTimestamp(text, history, selected);
onProjectLoad(previewFiles, history.find(e => e.timestamp === selected).editorVersion, selected);
}
}, [selected, onProjectLoad]);

const onSaveCopySelect = React.useCallback(() => {
onCopySelect(selected);
}, [selected, onCopySelect]);
if (selected === -1) {
onProjectCopy(text, pxt.appTarget.versions.target);
}
else {
const previewFiles = applyUntilTimestamp(text, history, selected);
onProjectCopy(previewFiles, history.find(e => e.timestamp === selected).editorVersion, selected)
}
}, [selected, onProjectCopy]);

const url = `${window.location.origin + window.location.pathname}?timeMachine=1&controller=1&skillsMap=1&noproject=1&nocookiebanner=1`;

const buckets: {[index: string]: pxt.workspace.HistoryEntry[]} = {};
const buckets: {[index: string]: TimeEntry[]} = {};

for (const entry of history) {
const date = new Date(entry.timestamp);
Expand All @@ -175,10 +198,29 @@ export const TimeMachine = (props: TimeMachineProps) => {
buckets[key] = [];
}

buckets[key].push(entry);
buckets[key].push({
label: formatTime(entry.timestamp),
timestamp: entry.timestamp
});
}

const nowEntry = {
label: lf("Now"),
timestamp: -1
};

const sortedBuckets = Object.keys(buckets).sort((a, b) => parseInt(b) - parseInt(a));
for (const bucket of sortedBuckets) {
buckets[bucket].sort((a, b) => b.timestamp - a.timestamp);
}

if (!sortedBuckets.length || !isToday(parseInt(sortedBuckets[0]))) {
buckets[Date.now()] = [nowEntry]
}
else {
buckets[sortedBuckets[0]].unshift(nowEntry)
}

return createPortal(
<FocusTrap className="time-machine" onEscape={hideDialog}>
<div className="time-machine-header">
Expand Down Expand Up @@ -237,14 +279,14 @@ export const TimeMachine = (props: TimeMachineProps) => {
{formatDate(parseInt(date))}
</TreeItemBody>
<Tree role="group">
{...buckets[date].sort((a, b) => b.timestamp - a.timestamp).map(entry =>
{...buckets[date].map(entry =>
<TreeItem
key={entry.timestamp}
onClick={() => onTimeSelected(entry.timestamp)}
className={selected === entry.timestamp ? "selected" : undefined}
>
<TreeItemBody>
{formatTime(entry.timestamp)}
{entry.label}
</TreeItemBody>
</TreeItem>
)}
Expand All @@ -260,12 +302,28 @@ export const TimeMachine = (props: TimeMachineProps) => {
}

function applyUntilTimestamp(text: pxt.workspace.ScriptText, history: pxt.workspace.HistoryEntry[], timestamp: number) {
let currentText = { ...text };
let currentText = text;

for (let i = 0; i < history.length; i++) {
const entry = history[history.length - 1 - i];
const index = history.length - 1 - i;
const entry = history[index];
currentText = workspace.applyDiff(currentText, entry);
if (entry.timestamp === timestamp) break;
if (entry.timestamp === timestamp) {
const version = index > 0 ? history[index - 1].editorVersion : entry.editorVersion;

// Attempt to update the version in pxt.json
try {
const config = JSON.parse(currentText[pxt.CONFIG_NAME]) as pxt.PackageConfig;
if (config.targetVersions) {
config.targetVersions.target = version;
}
currentText[pxt.CONFIG_NAME] = JSON.stringify(config, null, 4);
}
catch (e) {
}

break;
}
}

return currentText;
Expand Down Expand Up @@ -341,4 +399,12 @@ function formatFullDate(time: number) {
const formattedTime = formatTime(time);

return lf("{id:date,time}{0}, {1}", formattedDate, formattedTime);
}

function isToday(time: number) {
const now = new Date();
const date = new Date(time);
return now.getFullYear() === date.getFullYear() &&
now.getMonth() === date.getMonth() &&
now.getDate() == date.getDate();
}
40 changes: 20 additions & 20 deletions webapp/src/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import * as dmp from "diff-match-patch";
import U = pxt.Util;
import Cloud = pxt.Cloud;

const differ = new dmp.diff_match_patch();
// 5 minutes
const historyInterval = 1000 * 60 * 5;

// Avoid importing entire crypto-js
/* eslint-disable import/no-internal-modules */
Expand Down Expand Up @@ -601,11 +602,23 @@ export async function saveAsync(h: Header, text?: ScriptText, fromCloudSync?: bo
};
}

const top = history.entries[history.entries.length - 1];
let shouldCombine = false;
if (history.entries.length > 1) {
const currentTime = Date.now();
const topTime = history.entries[history.entries.length - 1].timestamp;
const prevTime = history.entries[history.entries.length - 2].timestamp;

if (top && canCombine(top, diff)) {
top.timestamp = diff.timestamp;
diff.changes.forEach(change => top.changes.push(change));
if (currentTime - topTime < historyInterval && topTime - prevTime < historyInterval) {
shouldCombine = true;
}
}

if (shouldCombine) {
// Roll back the last diff and create a new one
const prevText = applyDiff(previous.text, history.entries.pop());

const diff = diffScriptText(prevText, toWrite);
history.entries.push(diff);
}
else {
history.entries.push(diff);
Expand Down Expand Up @@ -689,32 +702,19 @@ function diffScriptText(oldVersion: pxt.workspace.ScriptText, newVersion: pxt.wo
}

function diffText(a: string, b: string) {
const differ = new dmp.diff_match_patch();
return differ.patch_make(a, b);
}

function patchText(patch: unknown, a: string) {
const differ = new dmp.diff_match_patch();
return differ.patch_apply(patch as any, a)[0]
}

export function applyDiff(text: ScriptText, history: pxt.workspace.HistoryEntry) {
return pxt.workspace.applyDiff(text, history, patchText);
}

function canCombine(oldEntry: pxt.workspace.HistoryEntry, newEntry: pxt.workspace.HistoryEntry) {
if (newEntry.timestamp - oldEntry.timestamp > 3000) return false;

let fileNames: pxt.Map<boolean> = {};

for (const change of oldEntry.changes) {
fileNames[change.filename] = true;
}

for (const change of newEntry.changes) {
if (fileNames[change.filename]) return false;
}
return true;
}

export function importAsync(h: Header, text: ScriptText, isCloud = false) {
h.path = computePath(h)
return forceSaveAsync(h, text, isCloud)
Expand Down

0 comments on commit 4de7504

Please sign in to comment.