Merge branch 'master' into opencollective

This commit is contained in:
Rodrigo Pombo 2019-03-17 01:57:31 -03:00 committed by GitHub
commit b64ed7974c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 3545 additions and 843 deletions

2
.storybook/addons.js Normal file
View File

@ -0,0 +1,2 @@
import "@storybook/addon-actions/register";
import "@storybook/addon-links/register";

9
.storybook/config.js Normal file
View File

@ -0,0 +1,9 @@
import { configure } from "@storybook/react";
const req = require.context("../src", true, /\.story\.js$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

View File

@ -10,13 +10,15 @@
"netlify-auth-providers": "^1.0.0-alpha5",
"opencollective-postinstall": "^2.0.2",
"prismjs": "^1.15.0",
"react": "^16.8.1",
"react-dom": "^16.8.1",
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-scripts": "2.1.3",
"react-swipeable": "^4.3.2",
"react-use": "^5.2.2",
"workerize-loader": "^1.0.4",
"opencollective": "^1.0.3"
"opencollective": "^1.0.3",
"rebound": "^0.1.0",
"workerize-loader": "^1.0.4"
},
"scripts": {
"start": "craco start",
@ -26,7 +28,9 @@
"test-cra": "react-scripts test",
"test": "run-s test-prettier test-cra",
"eject": "react-scripts eject",
"postinstall": "opencollective-postinstall"
"postinstall": "opencollective-postinstall",
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public"
},
"eslintConfig": {
"extends": "react-app"
@ -38,6 +42,12 @@
"not op_mini all"
],
"devDependencies": {
"@babel/core": "^7.3.4",
"@storybook/addon-actions": "^4.1.13",
"@storybook/addon-links": "^4.1.13",
"@storybook/addons": "^4.1.13",
"@storybook/react": "^4.1.13",
"babel-loader": "8.0.4",
"npm-run-all": "^4.1.5",
"prettier": "^1.16.4"
},

View File

@ -1,17 +1,26 @@
import easing from "./easing";
const MULTIPLY = "multiply";
/* eslint-disable */
function mergeResults(results) {
function mergeResults(results, composite) {
const firstResult = results[0];
if (results.length < 2) {
return firstResult;
}
if (Array.isArray(firstResult)) {
// console.log("merge", results);
return firstResult.map((_, i) => {
return mergeResults(results.map(result => result[i]));
return mergeResults(results.map(result => result[i]), composite);
});
} else {
return Object.assign({}, ...results);
const merged = Object.assign({}, ...results);
if (composite === MULTIPLY) {
const opacities = results.map(x => x.opacity).filter(x => x != null);
if (opacities.length !== 0) {
merged.opacity = opacities.reduce((a, b) => a * b);
}
}
return merged;
}
}
@ -19,7 +28,7 @@ const airframe = {
parallel: ({ children: fns }) => {
return (t, ...args) => {
const styles = fns.map(fn => fn(t, ...args));
const result = mergeResults(styles);
const result = mergeResults(styles, MULTIPLY);
return result;
};
},

View File

@ -3,6 +3,7 @@ import { createAnimation, Stagger } from "./airframe/airframe";
import easing from "./airframe/easing";
const dx = 250;
const offOpacity = 0.6;
/* @jsx createAnimation */
@ -43,7 +44,7 @@ function GrowHeight() {
);
}
function SwitchLines({ filterExit, filterEnter }) {
function SwitchLines({ filterExit, filterEnter, filterFadeOut }) {
return (
<parallel>
<Stagger interval={0.2} filter={filterExit}>
@ -59,6 +60,19 @@ function SwitchLines({ filterExit, filterEnter }) {
<SlideFromRight />
</chain>
</Stagger>
<Stagger interval={0} filter={filterEnter}>
<tween from={{ opacity: offOpacity }} to={{ opacity: 1 }} />
</Stagger>
<Stagger interval={0} filter={filterFadeOut}>
<tween
from={{ opacity: 1 }}
to={{ opacity: offOpacity }}
ease={easing.easeOutCubic}
/>
</Stagger>
<Stagger interval={0} filter={l => !filterEnter(l) && !filterFadeOut(l)}>
<tween from={{ opacity: offOpacity }} to={{ opacity: offOpacity }} />
</Stagger>
</parallel>
);
}
@ -68,10 +82,12 @@ export default (
<SwitchLines
filterExit={line => line.left && !line.middle}
filterEnter={line => !line.left && line.middle}
filterFadeOut={line => false}
/>
<SwitchLines
filterExit={line => line.middle && !line.right}
filterEnter={line => !line.middle && line.right}
filterFadeOut={line => !line.left && line.middle}
/>
</chain>
);

View File

@ -37,12 +37,7 @@ function InnerApp({ gitProvider }) {
return <Error error={{ status: 404 }} gitProvider={gitProvider} />;
}
const commits = versions.map(v => v.commit);
const slideLines = versions.map(v => v.lines);
return (
<History commits={commits} slideLines={slideLines} loadMore={loadMore} />
);
return <History versions={versions} loadMore={loadMore} />;
}
function useVersionsLoader(gitProvider) {

View File

@ -21,12 +21,13 @@
.comment-box:after {
border-color: rgba(1, 22, 39, 0);
border-bottom-color: rgb(1, 22, 39);
border-width: 11px;
margin-left: -11px;
border-width: 13px;
margin-left: -13px;
}
.comment-box:before {
border-color: rgba(1, 22, 39, 0);
border-bottom-color: rgb(214, 222, 235, 0.5);
border-width: 14px;
margin-left: -14px;
margin-bottom: 2px;
}

View File

@ -84,3 +84,26 @@ export function getSlides(codes, language) {
.filter(line => line.middle || line.left || line.right);
});
}
export function getChanges(lines) {
const changes = [];
let currentChange = null;
let i = 0;
const isNewLine = i => !lines[i].left && lines[i].middle;
while (i < lines.length) {
if (isNewLine(i)) {
if (!currentChange) {
currentChange = { start: i };
}
} else {
if (currentChange) {
currentChange.end = i - 1;
changes.push(currentChange);
currentChange = null;
}
}
i++;
}
return changes;
}

View File

@ -1,5 +1,5 @@
import { getLanguage, loadLanguage } from "./language-detector";
import { getSlides } from "./differ";
import { getSlides, getChanges } from "./differ";
import github from "./github-commit-fetcher";
import gitlab from "./gitlab-commit-fetcher";
@ -25,5 +25,9 @@ export async function getVersions(source, params) {
const codes = commits.map(commit => commit.content);
const slides = getSlides(codes, lang);
return commits.map((commit, i) => ({ commit, lines: slides[i] }));
return commits.map((commit, i) => ({
commit,
lines: slides[i],
changes: getChanges(slides[i])
}));
}

View File

@ -1,5 +1,5 @@
import { getLanguage, loadLanguage } from "./language-detector";
import { getSlides } from "./differ";
import { getSlides, getChanges } from "./differ";
const vscode = window.vscode;
@ -43,7 +43,11 @@ async function getVersions(last) {
const codes = commits.map(commit => commit.content);
const slides = getSlides(codes, lang);
return commits.map((commit, i) => ({ commit, lines: slides[i] }));
return commits.map((commit, i) => ({
commit,
lines: slides[i],
changes: getChanges(slides[i])
}));
}
export default {

View File

@ -99,14 +99,13 @@ function CommitList({ commits, currentIndex, selectCommit }) {
);
}
export default function History({ commits, slideLines, loadMore }) {
return (
<Slides slideLines={slideLines} commits={commits} loadMore={loadMore} />
);
export default function History({ versions, loadMore }) {
return <Slides versions={versions} loadMore={loadMore} />;
}
function Slides({ commits, slideLines, loadMore }) {
function Slides({ versions, loadMore }) {
const [current, target, setTarget] = useSliderSpring(0);
const commits = versions.map(v => v.commit);
const setClampedTarget = newTarget => {
setTarget(Math.min(commits.length - 0.75, Math.max(-0.25, newTarget)));
if (newTarget >= commits.length - 5) {
@ -140,12 +139,13 @@ function Slides({ commits, slideLines, loadMore }) {
onSwipedRight={prevSlide}
style={{ height: "100%" }}
>
<Slide time={index - current} lines={slideLines[index]} />
<Slide time={index - current} version={versions[index]} />
</Swipeable>
</React.Fragment>
);
}
// TODO use ./useSpring
function useSliderSpring(initial) {
const [target, setTarget] = useState(initial);
const tension = 0;

View File

@ -3,4 +3,9 @@ import React from "react";
import ReactDOM from "react-dom";
const root = document.getElementById("root");
ReactDOM.render(<App />, root);
ReactDOM.render(
<React.unstable_ConcurrentMode>
<App />
</React.unstable_ConcurrentMode>,
root
);

View File

@ -1,18 +1,104 @@
import React from "react";
import useChildren from "./use-virtual-children";
import "./scroller.css";
import useSpring from "./use-spring";
import { nextIndex, prevIndex, closestIndex, getScrollTop } from "./utils";
const initialState = {
snap: false,
targetTop: 0,
currentTop: 0,
areaIndex: 0
};
export default function Scroller({
items,
getRow,
getRowHeight,
data,
top,
setTop
snapAreas
}) {
const ref = React.useRef();
const height = useHeight(ref);
const reducer = (prevState, action) => {
switch (action.type) {
case "change-area":
if (snapAreas.length === 0) {
return prevState;
}
const { changeIndex, recalculate } = action;
const movingFromUnknownIndex = !prevState.snap || recalculate;
// TODO memo
const heights = items.map((item, i) => getRowHeight(item, i, data));
let newIndex;
if (movingFromUnknownIndex) {
//todo memo
const oldIndex = getAreaIndex(
prevState.targetTop,
snapAreas,
heights,
height
);
newIndex = changeIndex(snapAreas, oldIndex);
} else {
newIndex = changeIndex(snapAreas, prevState.areaIndex);
}
if (newIndex === prevState.areaIndex && !movingFromUnknownIndex) {
return prevState;
}
// TODO memo
let contentHeight = heights.reduce((a, b) => a + b, 0);
const targetTop = getScrollTop(
snapAreas[newIndex],
contentHeight,
height,
heights
);
return {
...prevState,
areaIndex: newIndex,
snap: true,
currentTop: null,
targetTop
};
case "manual-scroll":
const { newTop } = action;
if (newTop === prevState.currentTop && !prevState.snap) {
return prevState;
}
// console.log("manual scroll", newTop);
return {
...prevState,
snap: false,
currentTop: newTop,
targetTop: newTop
};
default:
throw Error();
}
};
const [{ snap, targetTop, currentTop }, dispatch] = React.useReducer(
reducer,
initialState
);
const top = useSpring({
target: targetTop,
current: currentTop,
round: Math.round
});
// console.log("render", targetTop, top);
const children = useChildren({
height,
top,
@ -22,21 +108,78 @@ export default function Scroller({
data
});
React.useEffect(() => {
document.body.addEventListener("keydown", e => {
if (e.keyCode === 38) {
dispatch({ type: "change-area", changeIndex: prevIndex });
e.preventDefault();
} else if (e.keyCode === 40) {
dispatch({ type: "change-area", changeIndex: nextIndex });
e.preventDefault();
}
});
}, []);
// Auto-scroll to closest change when changing versions:
// React.useLayoutEffect(() => {
// dispatch({
// type: "change-area",
// recalculate: true,
// changeIndex: closestIndex
// });
// }, [snapAreas]);
React.useLayoutEffect(() => {
ref.current.scrollTop = top;
}, [top]);
if (snap) {
ref.current.scrollTop = top;
}
}, [snap, top]);
return (
<div
style={{ height: "100%", overflowY: "auto", overflowX: "hidden" }}
className="scroller"
ref={ref}
onScroll={e => setTop(e.target.scrollTop)}
onScroll={e => {
const newTop = e.target.scrollTop;
if (newTop === top) {
return;
}
dispatch({ type: "manual-scroll", newTop });
}}
children={children}
/>
);
}
function getAreaIndex(scrollTop, areas, heights, containerHeight) {
if (areas.length === 0) {
return 0;
}
const scrollMiddle = scrollTop + containerHeight / 2;
let h = 0;
let i = 0;
while (scrollMiddle > h) {
h += heights[i++];
}
const middleRow = i;
const areaCenters = areas.map(a => (a.start + a.end) / 2);
areaCenters.unshift(0);
for (let a = 0; a < areas.length; a++) {
if (middleRow < areaCenters[a + 1]) {
return (
a -
(areaCenters[a + 1] - middleRow) / (areaCenters[a + 1] - areaCenters[a])
);
}
}
return areas.length - 0.9;
}
function useHeight(ref) {
let [height, setHeight] = React.useState(null);
@ -50,7 +193,7 @@ function useHeight(ref) {
return () => {
window.removeEventListener("resize", handleResize);
};
}, [ref.current]);
}, []);
return height;
}

104
src/scroller.story.js Normal file
View File

@ -0,0 +1,104 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import Scroller from "./scroller";
const snapAreas1 = [
{ start: 1, end: 5 },
{ start: 15, end: 26 },
{ start: 50, end: 100 },
{ start: 300, end: 302 }
];
const snapAreas2 = [
{ start: 8, end: 12 },
{ start: 30, end: 32 },
{ start: 550, end: 552 },
{ start: 595, end: 599 }
];
const items = Array(600)
.fill(0)
.map((_, i) => {
const a1 = snapAreas1.find(a => a.start <= i && i <= a.end);
const a2 = snapAreas2.find(a => a.start <= i && i <= a.end);
return {
content: `Row ${i}${
a1
? ` - Area1 [${a1.start}, ${a1.end}]`
: a2
? ` - Area2 [${a2.start}, ${a2.end}]`
: ""
}`,
key: i,
height: 22
};
});
function getRow(item) {
return (
<div key={item.key} style={{ height: item.height }}>
{item.content}
</div>
);
}
function getRowHeight(item) {
return item.height;
}
function BasicScroller({ areas }) {
const [top, setTop] = React.useState(40);
return (
<Scroller
items={items}
snapAreas={areas}
getRow={getRow}
getRowHeight={getRowHeight}
top={top}
setTop={setTop}
/>
);
}
storiesOf("Scroller", module).add("single", () => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "90vh"
}}
>
<div style={{ width: "60%", height: "80vh", border: "1px solid black" }}>
<BasicScroller areas={snapAreas1} />
</div>
</div>
));
function DoubleScroller() {
const [flag, setFlag] = React.useState(false);
return (
<div>
<div style={{ height: "80vh", border: "1px solid black", width: "60vw" }}>
<BasicScroller areas={flag ? snapAreas1 : snapAreas2} />
</div>
<div>
{flag ? "Areas 1" : "Areas 2"}
<button onClick={() => setFlag(flag => !flag)}>Toggle</button>
</div>
</div>
);
}
storiesOf("Scroller", module).add("multiple", () => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "90vh"
}}
>
<DoubleScroller />
</div>
));

View File

@ -36,8 +36,7 @@ function getLine(line, i, { styles }) {
);
}
function Slide({ lines, styles }) {
const [top, setTop] = React.useState(0);
function Slide({ lines, styles, changes }) {
return (
<pre
style={{
@ -66,16 +65,15 @@ function Slide({ lines, styles }) {
getRow={getLine}
getRowHeight={getLineHeight}
data={{ styles }}
top={top}
setTop={setTop}
snapAreas={changes}
/>
</code>
</pre>
);
}
export default function SlideWrapper({ time, lines }) {
export default function SlideWrapper({ time, version }) {
const { lines, changes } = version;
const styles = animation((time + 1) / 2, lines);
return <Slide lines={lines} styles={styles} />;
return <Slide lines={lines} styles={styles} changes={changes} />;
}

47
src/use-spring.js Normal file
View File

@ -0,0 +1,47 @@
// based on https://github.com/streamich/react-use/blob/master/src/useSpring.ts
import { SpringSystem } from "rebound";
import { useState, useEffect } from "react";
export default function useSpring({
target = 0,
current = null,
tension = 0,
friction = 10,
round = x => x
}) {
const [spring, setSpring] = useState(null);
const [value, setValue] = useState(target);
useEffect(() => {
const listener = {
onSpringUpdate: spring => {
const value = spring.getCurrentValue();
setValue(round(value));
}
};
if (!spring) {
const newSpring = new SpringSystem().createSpring(tension, friction);
newSpring.setCurrentValue(target);
setSpring(newSpring);
newSpring.addListener(listener);
return;
}
return () => {
spring.removeListener(listener);
setSpring(null);
};
}, [tension, friction]);
useEffect(() => {
if (spring) {
spring.setEndValue(target);
if (current != null) {
spring.setCurrentValue(current);
}
}
}, [target, current]);
return value;
}

66
src/use-spring.story.js Normal file
View File

@ -0,0 +1,66 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import useSpring from "./use-spring";
function Test() {
const [{ target, current }, setState] = React.useState({
target: 0,
current: null
});
const value = useSpring({ target, current });
return (
<div>
<div style={{ display: "flex" }}>
<span style={{ flex: 0.3 }}>Target</span>
<input
value={target}
onChange={e =>
setState({
target: +e.target.value,
current: null
})
}
style={{ flex: 1 }}
type="range"
/>
<span style={{ flex: 0.3 }}>{target}</span>
</div>
<div style={{ display: "flex" }}>
<span style={{ flex: 0.3 }}>Current</span>
<input
value={current}
onChange={e =>
setState({
target: +e.target.value,
current: +e.target.value
})
}
style={{ flex: 1 }}
type="range"
/>
<span style={{ flex: 0.3 }}>{current}</span>
</div>
<div style={{ display: "flex" }}>
<span style={{ flex: 0.3 }}>Value</span>
<input value={value} type="range" readOnly style={{ flex: 1 }} />
<span style={{ flex: 0.3 }}>{Math.round(value * 1000) / 1000}</span>
</div>
</div>
);
}
storiesOf("useSpring", module).add("test", () => (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "90vh"
}}
>
<div style={{ width: "60%" }}>
<Test />
</div>
</div>
));

26
src/utils.js Normal file
View File

@ -0,0 +1,26 @@
export function nextIndex(list, currentIndex) {
return Math.min(list.length - 1, Math.floor(currentIndex + 1));
}
export function prevIndex(list, currentIndex) {
return Math.max(0, Math.ceil(currentIndex - 1));
}
export function closestIndex(list, currentIndex) {
return Math.min(Math.max(0, Math.round(currentIndex)), list.length - 1);
}
export function getScrollTop(area, contentHeight, containerHeight, heights) {
const start = heights.slice(0, area.start).reduce((a, b) => a + b, 0);
const end =
start + heights.slice(area.start, area.end + 1).reduce((a, b) => a + b, 0);
const middle = (end + start) / 2;
const halfContainer = containerHeight / 2;
const bestTop =
end - start > containerHeight ? start : middle - halfContainer;
if (bestTop < 0) return 0;
if (bestTop + containerHeight > contentHeight) {
return contentHeight - containerHeight;
}
return bestTop;
}

29
src/utils.test.js Normal file
View File

@ -0,0 +1,29 @@
import { nextIndex, prevIndex } from "./utils";
describe("nextIndex", () => {
const fiveItems = [1, 2, 3, 4, 5];
test("works with middle index", () => {
expect(nextIndex(fiveItems, 2)).toBe(3);
});
test("works with last index", () => {
expect(nextIndex(fiveItems, 4)).toBe(4);
});
test("works with fractions", () => {
expect(nextIndex(fiveItems, 1.1)).toBe(2);
expect(nextIndex(fiveItems, 1.9)).toBe(2);
});
});
describe("prevIndex", () => {
const fiveItems = [1, 2, 3, 4, 5];
test("works with middle index", () => {
expect(prevIndex(fiveItems, 2)).toBe(1);
});
test("works with start index", () => {
expect(prevIndex(fiveItems, 0)).toBe(0);
});
test("works with fractions", () => {
expect(prevIndex(fiveItems, 1.1)).toBe(1);
expect(prevIndex(fiveItems, 1.9)).toBe(1);
});
});

3813
yarn.lock Executable file → Normal file

File diff suppressed because it is too large Load Diff