Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Element, Menu, Stack } from '@codesandbox/components';
import { Element, Menu, Stack, SkeletonText } from '@codesandbox/components';
import css from '@styled-system/css';
import { CommentFragment } from 'app/graphql/types';
import { useOvermind } from 'app/overmind';
Expand Down Expand Up @@ -73,3 +73,22 @@ export const Reply = ({ reply }: ReplyProps) => {
</>
);
};

export const SkeletonReply = props => (
<Element marginX={4} paddingTop={6} {...props}>
<Stack align="center" gap={2} marginBottom={4}>
<SkeletonText style={{ width: '32px', height: '32px' }} />

<Stack direction="vertical" gap={1}>
<SkeletonText style={{ width: '120px', height: '14px' }} />
<SkeletonText style={{ width: '120px', height: '14px' }} />
</Stack>
</Stack>

<Stack direction="vertical" gap={1} marginBottom={6}>
<SkeletonText style={{ width: '100%', height: '14px' }} />
<SkeletonText style={{ width: '100%', height: '14px' }} />
<SkeletonText style={{ width: '100%', height: '14px' }} />
</Stack>
</Element>
);
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ import ReactDOM from 'react-dom';
import { AvatarBlock } from '../components/AvatarBlock';
import { EditComment } from '../components/EditComment';
import { Markdown } from './Markdown';
import { Reply } from './Reply';
import { Reply, SkeletonReply } from './Reply';
import { useScrollTop } from './use-scroll-top';

export const CommentDialog = props =>
ReactDOM.createPortal(<Dialog {...props} />, document.body);

const DIALOG_WIDTH = 420;
const DIALOG_TRANSITION_DURATION = 0.25;
const REPLY_TRANSITION_DELAY = 0.25;

export const Dialog: React.FC = () => {
const { state } = useOvermind();
const controller = useAnimation();

const comment = state.comments.currentComment;
const replies = comment.comments;

// This comment doens't exist in the database, it's an optimistic comment
const isNewComment = comment.id === OPTIMISTIC_COMMENT_ID;
Expand All @@ -47,6 +50,8 @@ export const Dialog: React.FC = () => {
// this could rather be `getInitialPosition`
const initialPosition = getInitialPosition(currentCommentPositions);

const [repliesRendered, setRepliesRendered] = React.useState(false);

// reset editing when comment changes
React.useEffect(() => {
setEditing(isNewComment);
Expand All @@ -67,8 +72,13 @@ export const Dialog: React.FC = () => {
currentCommentPositions,
isCodeComment,
isNewComment,
repliesRendered,
]);

React.useEffect(() => {
setRepliesRendered(false);
}, [comment.id]);

const onDragHandlerPan = (deltaX: number, deltaY: number) => {
controller.set((_, target) => ({
x: Number(target.x) + deltaX,
Expand All @@ -84,7 +94,7 @@ export const Dialog: React.FC = () => {
key={isCodeComment ? 'code' : 'global'}
initial={{ ...initialPosition, scale: 0.5, opacity: 0 }}
animate={controller}
transition={{ duration: 0.25 }}
transition={{ duration: DIALOG_TRANSITION_DURATION }}
style={{ position: 'absolute', zIndex: 2 }}
>
<Stack
Expand Down Expand Up @@ -127,7 +137,12 @@ export const Dialog: React.FC = () => {
editing={editing}
setEditing={setEditing}
/>
<Replies replies={comment.comments} />
{replies.length ? (
<Replies
replies={replies}
repliesRenderedCallback={() => setRepliesRendered(true)}
/>
) : null}
</Stack>
<AddReply comment={comment} />
</>
Expand Down Expand Up @@ -331,13 +346,45 @@ const CommentBody = ({ comment, editing, setEditing }) => {
);
};

const Replies = ({ replies }) => (
<>
{replies.map(reply =>
reply ? <Reply reply={reply} key={reply.id} /> : 'Loading...'
)}
</>
);
const Replies = ({ replies, repliesRenderedCallback }) => {
const [isAnimating, setAnimating] = React.useState(true);
const repliesLoaded = !!replies[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const repliesLoaded = !!replies[0];
const repliesLoaded = Boolean(replies[0]);


/** Wait another <delay>ms after the dialog has transitioned into view */
const delay = DIALOG_TRANSITION_DURATION + REPLY_TRANSITION_DELAY;
const REPLY_TRANSITION_DURATION = 0.25;
const SKELETON_HEIGHT = 146;

React.useEffect(() => {
if (repliesLoaded && !isAnimating) repliesRenderedCallback();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (repliesLoaded && !isAnimating) repliesRenderedCallback();
if (repliesLoaded && !isAnimating) {
repliesRenderedCallback();
}

}, [repliesLoaded, isAnimating, repliesRenderedCallback]);

return (
<motion.div
initial={{ height: repliesLoaded ? 0 : SKELETON_HEIGHT }}
animate={{ height: 'auto' }}
transition={{
delay,
duration: REPLY_TRANSITION_DURATION,
}}
style={{
minHeight: repliesLoaded ? 0 : SKELETON_HEIGHT,
overflow: 'visible',
}}
onAnimationComplete={() => setAnimating(false)}
>
{repliesLoaded ? (
<>
{replies.map(reply => (
<Reply reply={reply} key={reply.id} />
))}
</>
) : (
<SkeletonReply />
)}
</motion.div>
);
};

const AddReply = ({ comment }) => {
const { actions } = useOvermind();
Expand Down
52 changes: 52 additions & 0 deletions packages/components/src/components/SkeletonText/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import styled, { css as styledcss, keyframes } from 'styled-components';
import Color from 'color';
import { Element } from '../Element';

// export interface ITextProps extends React.HTMLAttributes<HTMLSpanElement> {}

Comment on lines +5 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// export interface ITextProps extends React.HTMLAttributes<HTMLSpanElement> {}

const pulse = keyframes`
0% { background-position: 100% 50%; }
100% { background-position: -100% 50%; }
`;

export const SkeletonText = styled(Element)(props => {
const color = props.theme.colors.sideBar.border;
const themeType = props.theme.type;

/**
* This is fun,
* We animate the background gradient to create a pulse animation
*
* To support all themes nicely, we can't really pick a value from the theme
* So, we take the sidebar.border and then change it's luminosity
* 14% for background and 16% for the pulse highlight on top
* We need to set the value to 100 - value for light themes
*/

const backgroundLuminosity = themeType === 'light' ? 86 : 14;
const highlightLuminosity = themeType === 'light' ? 88 : 16;

// @ts-ignore - we have multiple versions of color in the app
// which leads to confusing type checks
const [h, s] = Color(color).hsl().color;

const background = Color({ h, s, l: backgroundLuminosity }).string();
const highlight = Color({ h, s, l: highlightLuminosity }).string();

return styledcss`
height: 16px;
width: 200px;
border-radius: 2px;
opacity: 0.7;
animation: ${pulse} 4s linear infinite;
background: linear-gradient(
90deg,
${background} 0%,
${background} 20%,
${highlight} 50%,
${background} 80%,
${background} 100%
);
background-size: 200% 200%;
`;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import { SkeletonText } from '.';

export default {
title: 'components/SkeletonText',
component: SkeletonText,
};

export const Basic = () => <SkeletonText />;
12 changes: 12 additions & 0 deletions packages/components/src/components/ThemeProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export const getThemes = () => {

return results.filter(a => a);
};

const guessType = theme => {
if (theme.type) return theme.type;

if (theme.name && theme.name.toLowerCase().includes('light')) return 'light';

return 'dark';
};

export const makeTheme = (vsCodeTheme = {}, name?: string) => {
// Our interface does not map 1-1 with vscode.
// To add styles that remain themeable, we add
Expand All @@ -35,9 +44,12 @@ export const makeTheme = (vsCodeTheme = {}, name?: string) => {
colors: polyfilledVSCodeColors,
});

const type = guessType(vsCodeTheme);

if (name) {
return {
name,
type,
...theme,
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './components/SearchInput';
export * from './components/Select';
export * from './components/Stats';
export * from './components/Menu';
export * from './components/SkeletonText';
export * from './components/Switch';
export * from './components/Text';
export * from './components/Textarea';
Expand Down