diff --git a/src/core/client/stream/components/Comment/Comment.css b/src/core/client/stream/components/Comment/Comment.css index abbd6ac69..c4bf8c532 100644 --- a/src/core/client/stream/components/Comment/Comment.css +++ b/src/core/client/stream/components/Comment/Comment.css @@ -1,3 +1,3 @@ .footer { - margin-top: calc(1.5 * var(--spacing-unit)); + margin-top: var(--spacing-unit); } diff --git a/src/core/client/stream/components/Comment/Comment.tsx b/src/core/client/stream/components/Comment/Comment.tsx index 5afe6f61c..4dbda55bf 100644 --- a/src/core/client/stream/components/Comment/Comment.tsx +++ b/src/core/client/stream/components/Comment/Comment.tsx @@ -3,7 +3,7 @@ import { StatelessComponent } from "react"; import { Typography } from "talk-ui/components"; import * as styles from "./Comment.css"; -import PermalinkContainer from "../../containers/PermalinkContainer"; +import PermalinkButtonContainer from "../../containers/PermalinkButtonContainer"; import Timestamp from "./Timestamp"; import TopBar from "./TopBar"; import Username from "./Username"; @@ -27,7 +27,7 @@ const Comment: StatelessComponent = props => { {props.body}
- +
); diff --git a/src/core/client/stream/components/Permalink/Permalink.tsx b/src/core/client/stream/components/Permalink/Permalink.tsx deleted file mode 100644 index 40500904e..000000000 --- a/src/core/client/stream/components/Permalink/Permalink.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Localized } from "fluent-react/compat"; -import React from "react"; -import { Button, ButtonIcon, Popover } from "talk-ui/components"; -import PermalinkPopover from "./PermalinkPopover"; - -interface PermalinkProps { - commentID: string; - assetURL: string | null; -} - -class Permalink extends React.Component { - public render() { - const { commentID, assetURL } = this.props; - return ( - ( - - )} - > - {({ toggleVisibility, forwardRef }) => ( - - )} - - ); - } -} - -export default Permalink; diff --git a/src/core/client/stream/components/Permalink/PermalinkPopover.tsx b/src/core/client/stream/components/Permalink/PermalinkPopover.tsx deleted file mode 100644 index 32495a902..000000000 --- a/src/core/client/stream/components/Permalink/PermalinkPopover.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Localized } from "fluent-react/compat"; -import React, { CSSProperties } from "react"; -import CopyToClipboard from "react-copy-to-clipboard"; -import { Button, ClickOutside, Flex, TextField } from "talk-ui/components"; - -interface InnerProps { - permalinkUrl: string; - style?: CSSProperties; - toggleVisibility: () => void; -} - -interface State { - copied: boolean; -} - -class PermalinkPopover extends React.Component { - public state: State = { - copied: false, - }; - - public onCopy = async () => { - await this.toggleCopied(); - setTimeout(() => { - this.toggleCopied(); - }, 800); - }; - - public toggleCopied = () => { - this.setState((state: State) => ({ - copied: !state.copied, - })); - }; - - public render() { - const { permalinkUrl, toggleVisibility } = this.props; - const { copied } = this.state; - return ( - - - - - - - - - ); - } -} - -export default PermalinkPopover; diff --git a/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx b/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx new file mode 100644 index 000000000..339e1780a --- /dev/null +++ b/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx @@ -0,0 +1,63 @@ +import { Localized } from "fluent-react/compat"; +import React from "react"; +import { oncePerFrame } from "talk-common/utils"; +import { Button, ButtonIcon, ClickOutside, Popover } from "talk-ui/components"; + +import PermalinkPopover from "./PermalinkPopover"; + +interface PermalinkProps { + commentID: string; + assetURL: string | null; +} + +class Permalink extends React.Component { + // Helpers that prevents calling toggleVisibility more then once per frame. + // In essence this means we'll only process an event only once. + // This might happen, when clicking on the button which will + // cause its onClick to happen as well as onClickOutside. + private toggleVisibilityOncePerFrame = oncePerFrame( + (toggleVisibility: () => void) => toggleVisibility() + ); + + public render() { + const { commentID, assetURL } = this.props; + const popoverID = "permalink-popover"; + return ( + ( + + this.toggleVisibilityOncePerFrame(toggleVisibility) + } + > + + + )} + > + {({ toggleVisibility, forwardRef, visible }) => ( + + )} + + ); + } +} + +export default Permalink; diff --git a/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css new file mode 100644 index 000000000..e6cb4ee63 --- /dev/null +++ b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.css @@ -0,0 +1,7 @@ +.root { + width: 300px; +} + +.textField { + flex-grow: 1; +} diff --git a/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx new file mode 100644 index 000000000..218bd78f6 --- /dev/null +++ b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx @@ -0,0 +1,60 @@ +import { Localized } from "fluent-react/compat"; +import React from "react"; +import CopyToClipboard from "react-copy-to-clipboard"; + +import { Button, Flex, TextField } from "talk-ui/components"; + +import * as styles from "./PermalinkPopover.css"; + +interface InnerProps { + permalinkURL: string; + toggleVisibility: () => void; +} + +interface State { + copied: boolean; +} + +class PermalinkPopover extends React.Component { + public state: State = { + copied: false, + }; + + private onCopy = async () => { + await this.toggleCopied(); + setTimeout(() => { + this.toggleCopied(); + }, 800); + }; + + private toggleCopied = () => { + this.setState((state: State) => ({ + copied: !state.copied, + })); + }; + + public render() { + const { permalinkURL } = this.props; + const { copied } = this.state; + return ( + + + + + + + ); + } +} + +export default PermalinkPopover; diff --git a/src/core/client/stream/components/PermalinkButton/index.ts b/src/core/client/stream/components/PermalinkButton/index.ts new file mode 100644 index 000000000..de6d61c1c --- /dev/null +++ b/src/core/client/stream/components/PermalinkButton/index.ts @@ -0,0 +1 @@ +export { default } from "./PermalinkButton"; diff --git a/src/core/client/stream/components/Permalink/PermalinkView.css b/src/core/client/stream/components/PermalinkView.css similarity index 100% rename from src/core/client/stream/components/Permalink/PermalinkView.css rename to src/core/client/stream/components/PermalinkView.css diff --git a/src/core/client/stream/components/Permalink/PermalinkView.tsx b/src/core/client/stream/components/PermalinkView.tsx similarity index 90% rename from src/core/client/stream/components/Permalink/PermalinkView.tsx rename to src/core/client/stream/components/PermalinkView.tsx index ae9f7f6ae..10c2f8470 100644 --- a/src/core/client/stream/components/Permalink/PermalinkView.tsx +++ b/src/core/client/stream/components/PermalinkView.tsx @@ -1,9 +1,9 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; +import React, { StatelessComponent } from "react"; import Logo from "talk-stream/components/Logo"; import { Button, Flex, Typography } from "talk-ui/components"; -import CommentContainer from "../../containers/CommentContainer"; + +import CommentContainer from "../containers/CommentContainer"; import * as styles from "./PermalinkView.css"; export interface PermalinkViewProps { diff --git a/src/core/client/stream/containers/PermalinkContainer.tsx b/src/core/client/stream/containers/PermalinkButtonContainer.tsx similarity index 61% rename from src/core/client/stream/containers/PermalinkContainer.tsx rename to src/core/client/stream/containers/PermalinkButtonContainer.tsx index 1176426c5..fd1509117 100644 --- a/src/core/client/stream/containers/PermalinkContainer.tsx +++ b/src/core/client/stream/containers/PermalinkButtonContainer.tsx @@ -1,9 +1,9 @@ import React, { StatelessComponent } from "react"; import { graphql } from "react-relay"; import { withLocalStateContainer } from "talk-framework/lib/relay"; -import { AppQueryLocal as Local } from "talk-stream/__generated__/AppQueryLocal.graphql"; +import { PermalinkButtonContainerLocal as Local } from "talk-stream/__generated__/PermalinkButtonContainerLocal.graphql"; -import Permalink from "../components/Permalink/Permalink"; +import PermalinkButton from "../components/PermalinkButton"; interface InnerProps { local: Local; @@ -15,13 +15,13 @@ export const PermalinkContainer: StatelessComponent = ({ commentID, }) => { return local.assetURL ? ( - + ) : null; }; const enhanced = withLocalStateContainer( graphql` - fragment PermalinkContainerLocal on Local { + fragment PermalinkButtonContainerLocal on Local { assetURL } ` diff --git a/src/core/client/stream/containers/PermalinkViewContainer.tsx b/src/core/client/stream/containers/PermalinkViewContainer.tsx index b59e40588..ac2c824a9 100644 --- a/src/core/client/stream/containers/PermalinkViewContainer.tsx +++ b/src/core/client/stream/containers/PermalinkViewContainer.tsx @@ -10,7 +10,7 @@ import { SetCommentIDMutation, withSetCommentIDMutation, } from "talk-stream/mutations"; -import PermalinkView from "../components/Permalink/PermalinkView"; +import PermalinkView from "../components/PermalinkView"; interface PermalinkViewContainerProps { data: Data; @@ -22,9 +22,7 @@ class PermalinkViewContainer extends React.Component< PermalinkViewContainerProps > { private showAllComments = () => { - const { local } = this.props; - window.location.href = local.assetURL!; - // mutation + this.props.setCommentID({ id: null }); }; public render() { const { data, local } = this.props; diff --git a/src/core/client/stream/mutations/SetCommentIDMutation.ts b/src/core/client/stream/mutations/SetCommentIDMutation.ts index 8f6b6caf2..a29a70016 100644 --- a/src/core/client/stream/mutations/SetCommentIDMutation.ts +++ b/src/core/client/stream/mutations/SetCommentIDMutation.ts @@ -3,15 +3,15 @@ import { createMutationContainer } from "talk-framework/lib/relay"; import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; export interface SetCommentIDInput { - commentID: string | null; + id: string | null; } -export type SetCommentIDMutation = (input: SetCommentIDInput) => void; +export type SetCommentIDMutation = (input: SetCommentIDInput) => Promise; async function commit(environment: Environment, input: SetCommentIDInput) { return commitLocalUpdate(environment, store => { const record = store.get(LOCAL_ID)!; - record.setValue(input.commentID, "commentID"); + record.setValue(input.id, "commentID"); }); } diff --git a/src/core/client/stream/mutations/SetNetworkStatusMutation.ts b/src/core/client/stream/mutations/SetNetworkStatusMutation.ts index e5c5d0145..87d15e83f 100644 --- a/src/core/client/stream/mutations/SetNetworkStatusMutation.ts +++ b/src/core/client/stream/mutations/SetNetworkStatusMutation.ts @@ -8,7 +8,9 @@ export interface SetNetworkStatusInput { isOffline: boolean; } -export type SetNetworkStatusMutation = (input: SetNetworkStatusInput) => void; +export type SetNetworkStatusMutation = ( + input: SetNetworkStatusInput +) => Promise; async function commit(environment: Environment, input: SetNetworkStatusInput) { return commitLocalUpdate(environment, store => { diff --git a/src/core/client/stream/queries/__snapshots__/AppQuery.spec.tsx.snap b/src/core/client/stream/queries/__snapshots__/AppQuery.spec.tsx.snap index ffc62c435..87dfae59d 100644 --- a/src/core/client/stream/queries/__snapshots__/AppQuery.spec.tsx.snap +++ b/src/core/client/stream/queries/__snapshots__/AppQuery.spec.tsx.snap @@ -19,7 +19,7 @@ exports[`renders loading 1`] = ` `; exports[`renders permalink view 1`] = ` - `; diff --git a/src/core/client/ui/components/ClickOutside/ClickOutside.tsx b/src/core/client/ui/components/ClickOutside/ClickOutside.tsx index b23f7ff3c..18714f282 100644 --- a/src/core/client/ui/components/ClickOutside/ClickOutside.tsx +++ b/src/core/client/ui/components/ClickOutside/ClickOutside.tsx @@ -2,7 +2,7 @@ import React from "react"; import { findDOMNode } from "react-dom"; export interface ClickOutsideProps { - onClickOutside: () => void; + onClickOutside: (e?: MouseEvent) => void; children: React.ReactNode; } @@ -13,7 +13,7 @@ class ClickOutside extends React.Component { const { onClickOutside } = this.props; if (!e || !this.domNode!.contains(e.target as HTMLInputElement)) { // tslint:disable-next-line:no-unused-expression - onClickOutside && onClickOutside(); + onClickOutside && onClickOutside(e); } }; diff --git a/src/core/client/ui/components/Popover/Popover.css b/src/core/client/ui/components/Popover/Popover.css index 89037c5f3..a47f820a4 100644 --- a/src/core/client/ui/components/Popover/Popover.css +++ b/src/core/client/ui/components/Popover/Popover.css @@ -6,7 +6,7 @@ border-radius: 1px; color: #222; display: flex; - padding: 6px 10px; + padding: calc(0.5 * var(--spacing-unit)); &:after, &::before { diff --git a/src/core/client/ui/components/Popover/Popover.tsx b/src/core/client/ui/components/Popover/Popover.tsx index 1122c73db..3123422ee 100644 --- a/src/core/client/ui/components/Popover/Popover.tsx +++ b/src/core/client/ui/components/Popover/Popover.tsx @@ -27,10 +27,21 @@ type Placement = | "left" | "left-start"; +interface BodyRenderProps { + toggleVisibility: () => void; + visible: boolean; +} + +interface ChildrenRenderProps { + toggleVisibility: () => void; + forwardRef?: RefHandler; + visible: boolean; +} + interface PopoverProps { - body: (props: RenderProps) => React.ReactNode | React.ReactElement; - children: (props: RenderProps) => React.ReactNode; - description?: string; + body: (props: BodyRenderProps) => React.ReactNode | React.ReactElement; + children: (props: ChildrenRenderProps) => React.ReactNode; + description: string; id: string; onClose?: () => void; className?: string; @@ -41,11 +52,6 @@ interface State { visible: false; } -interface RenderProps { - toggleVisibility: () => void; - forwardRef?: RefHandler; -} - class Popover extends React.Component { public state: State = { visible: false, @@ -97,24 +103,20 @@ class Popover extends React.Component { children({ forwardRef: props.ref, toggleVisibility: this.toggleVisibility, + visible: this.state.visible, }) } - - {(props: PopperArrowProps) => - visible && ( -
- {description} + + {(props: PopperArrowProps) => ( +
+ {description} + {visible && (
{ {typeof body === "function" ? body({ toggleVisibility: this.toggleVisibility, + visible: this.state.visible, }) : body}
-
- ) - } + )} +
+ )}
); diff --git a/src/core/client/ui/components/TextField/TextField.css b/src/core/client/ui/components/TextField/TextField.css index ca9fc25cc..8fbf2c7db 100644 --- a/src/core/client/ui/components/TextField/TextField.css +++ b/src/core/client/ui/components/TextField/TextField.css @@ -4,5 +4,5 @@ border: 1px solid #979797; box-sizing: border-box; border-radius: 1px; - padding: 5px 20px; + padding: calc(0.5 * var(--spacing-unit)); } diff --git a/src/core/common/utils/index.ts b/src/core/common/utils/index.ts index e91489017..b5b0a0be6 100644 --- a/src/core/common/utils/index.ts +++ b/src/core/common/utils/index.ts @@ -1,2 +1,3 @@ export { default as timeout } from "./timeout"; export { default as pascalCase } from "./pascalCase"; +export { default as oncePerFrame } from "./oncePerFrame"; diff --git a/src/core/common/utils/oncePerFrame.ts b/src/core/common/utils/oncePerFrame.ts new file mode 100644 index 000000000..a5e59bd9b --- /dev/null +++ b/src/core/common/utils/oncePerFrame.ts @@ -0,0 +1,18 @@ +/** + * Function decorator that prevents calling `fn` more then once per frame. + * If called more than once, the last return value gets returned. + */ +const oncePerFrame = any>(fn: T) => { + let toggledThisFrame = false; + let lastResult: any = null; + return ((...args: any[]) => { + if (toggledThisFrame) { + return lastResult; + } + toggledThisFrame = true; + lastResult = fn(...args); + setTimeout(() => (toggledThisFrame = false), 0); + }) as T; +}; + +export default oncePerFrame;