diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/Reply.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/Reply.tsx
index 915a97037e4..75f33500f48 100644
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/Reply.tsx
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/Reply.tsx
@@ -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';
@@ -73,3 +73,22 @@ export const Reply = ({ reply }: ReplyProps) => {
>
);
};
+
+export const SkeletonReply = props => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/index.tsx
index 44cc32b55ff..c53921050a7 100644
--- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/index.tsx
+++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/screens/Comments/Dialog/index.tsx
@@ -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(, 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;
@@ -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);
@@ -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,
@@ -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 }}
>
{
editing={editing}
setEditing={setEditing}
/>
-
+ {replies.length ? (
+ setRepliesRendered(true)}
+ />
+ ) : null}
>
@@ -331,13 +346,45 @@ const CommentBody = ({ comment, editing, setEditing }) => {
);
};
-const Replies = ({ replies }) => (
- <>
- {replies.map(reply =>
- reply ? : 'Loading...'
- )}
- >
-);
+const Replies = ({ replies, repliesRenderedCallback }) => {
+ const [isAnimating, setAnimating] = React.useState(true);
+ const repliesLoaded = !!replies[0];
+
+ /** Wait another 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();
+ }, [repliesLoaded, isAnimating, repliesRenderedCallback]);
+
+ return (
+ setAnimating(false)}
+ >
+ {repliesLoaded ? (
+ <>
+ {replies.map(reply => (
+
+ ))}
+ >
+ ) : (
+
+ )}
+
+ );
+};
const AddReply = ({ comment }) => {
const { actions } = useOvermind();
diff --git a/packages/components/src/components/SkeletonText/index.tsx b/packages/components/src/components/SkeletonText/index.tsx
new file mode 100644
index 00000000000..d2a3e6d4ad6
--- /dev/null
+++ b/packages/components/src/components/SkeletonText/index.tsx
@@ -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 {}
+
+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%;
+ `;
+});
diff --git a/packages/components/src/components/SkeletonText/skeleton-text.stories.tsx b/packages/components/src/components/SkeletonText/skeleton-text.stories.tsx
new file mode 100644
index 00000000000..2f26fd4fb36
--- /dev/null
+++ b/packages/components/src/components/SkeletonText/skeleton-text.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import { SkeletonText } from '.';
+
+export default {
+ title: 'components/SkeletonText',
+ component: SkeletonText,
+};
+
+export const Basic = () => ;
diff --git a/packages/components/src/components/ThemeProvider/index.tsx b/packages/components/src/components/ThemeProvider/index.tsx
index 4dc24a849ec..61f208224ab 100644
--- a/packages/components/src/components/ThemeProvider/index.tsx
+++ b/packages/components/src/components/ThemeProvider/index.tsx
@@ -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
@@ -35,9 +44,12 @@ export const makeTheme = (vsCodeTheme = {}, name?: string) => {
colors: polyfilledVSCodeColors,
});
+ const type = guessType(vsCodeTheme);
+
if (name) {
return {
name,
+ type,
...theme,
};
}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index 86781964882..de85b9de87d 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -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';