Insights refinements
This commit is contained in:
@@ -2,13 +2,12 @@ import { LocationDescriptor } from "history";
|
|||||||
import { observer, useObserver } from "mobx-react";
|
import { observer, useObserver } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
|
import { Link, useRouteMatch } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Document from "~/models/Document";
|
import Document from "~/models/Document";
|
||||||
import DocumentMeta from "~/components/DocumentMeta";
|
import DocumentMeta from "~/components/DocumentMeta";
|
||||||
import DocumentViews from "~/components/DocumentViews";
|
|
||||||
import Popover from "~/components/Popover";
|
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
|
import { documentUrl, documentInsightsUrl } from "~/utils/routeHelpers";
|
||||||
import Fade from "./Fade";
|
import Fade from "./Fade";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,41 +20,32 @@ type Props = {
|
|||||||
function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
|
function DocumentMetaWithViews({ to, isDraft, document, ...rest }: Props) {
|
||||||
const { views } = useStores();
|
const { views } = useStores();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const match = useRouteMatch();
|
||||||
const documentViews = useObserver(() => views.inDocument(document.id));
|
const documentViews = useObserver(() => views.inDocument(document.id));
|
||||||
const totalViewers = documentViews.length;
|
const totalViewers = documentViews.length;
|
||||||
const onlyYou = totalViewers === 1 && documentViews[0].user.id;
|
const onlyYou = totalViewers === 1 && documentViews[0].user.id;
|
||||||
const viewsLoadedOnMount = React.useRef(totalViewers > 0);
|
const viewsLoadedOnMount = React.useRef(totalViewers > 0);
|
||||||
|
|
||||||
const popover = usePopoverState({
|
const insightsUrl = documentInsightsUrl(document);
|
||||||
gutter: 8,
|
|
||||||
placement: "bottom",
|
|
||||||
modal: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Wrapper = viewsLoadedOnMount.current ? React.Fragment : Fade;
|
const Wrapper = viewsLoadedOnMount.current ? React.Fragment : Fade;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Meta document={document} to={to} replace {...rest}>
|
<Meta document={document} to={to} replace {...rest}>
|
||||||
{totalViewers && !isDraft ? (
|
{totalViewers && !isDraft ? (
|
||||||
<PopoverDisclosure {...popover}>
|
<Wrapper>
|
||||||
{(props) => (
|
•
|
||||||
<Wrapper>
|
<Link
|
||||||
•
|
to={match.url === insightsUrl ? documentUrl(document) : insightsUrl}
|
||||||
<a {...props}>
|
>
|
||||||
{t("Viewed by")}{" "}
|
{t("Viewed by")}{" "}
|
||||||
{onlyYou
|
{onlyYou
|
||||||
? t("only you")
|
? t("only you")
|
||||||
: `${totalViewers} ${
|
: `${totalViewers} ${
|
||||||
totalViewers === 1 ? t("person") : t("people")
|
totalViewers === 1 ? t("person") : t("people")
|
||||||
}`}
|
}`}
|
||||||
</a>
|
</Link>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)}
|
|
||||||
</PopoverDisclosure>
|
|
||||||
) : null}
|
) : null}
|
||||||
<Popover {...popover} width={300} aria-label={t("Viewers")} tabIndex={0}>
|
|
||||||
<DocumentViews document={document} isOpen={popover.visible} />
|
|
||||||
</Popover>
|
|
||||||
</Meta>
|
</Meta>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,15 @@ import useEventListener from "./useEventListener";
|
|||||||
export default function useTextSelection() {
|
export default function useTextSelection() {
|
||||||
const [selection, setSelection] = React.useState<string>("");
|
const [selection, setSelection] = React.useState<string>("");
|
||||||
|
|
||||||
const handleMouse = React.useCallback(() => {
|
useEventListener(
|
||||||
const selection = window.getSelection();
|
"selectionchange",
|
||||||
const text = selection?.toString();
|
() => {
|
||||||
setSelection(text ?? "");
|
const selection = window.getSelection();
|
||||||
}, []);
|
const text = selection?.toString();
|
||||||
|
setSelection(text ?? "");
|
||||||
useEventListener("mousemove", handleMouse);
|
},
|
||||||
useEventListener("mouseup", handleMouse);
|
document
|
||||||
|
);
|
||||||
|
|
||||||
return selection;
|
return selection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,11 +47,13 @@ function Insights() {
|
|||||||
<Heading>{t("Stats")}</Heading>
|
<Heading>{t("Stats")}</Heading>
|
||||||
<Text type="secondary" size="small">
|
<Text type="secondary" size="small">
|
||||||
<List>
|
<List>
|
||||||
<li>
|
{stats.total.words > 0 && (
|
||||||
{t(`{{ count }} minute read`, {
|
<li>
|
||||||
count: stats.total.readingTime,
|
{t(`{{ count }} minute read`, {
|
||||||
})}
|
count: stats.total.readingTime,
|
||||||
</li>
|
})}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
<li>{t(`{{ count }} words`, { count: stats.total.words })}</li>
|
<li>{t(`{{ count }} words`, { count: stats.total.words })}</li>
|
||||||
<li>
|
<li>
|
||||||
{t(`{{ count }} characters`, {
|
{t(`{{ count }} characters`, {
|
||||||
@@ -80,24 +82,6 @@ function Insights() {
|
|||||||
</List>
|
</List>
|
||||||
</Text>
|
</Text>
|
||||||
</Content>
|
</Content>
|
||||||
<Content column>
|
|
||||||
<Heading>{t("Views")}</Heading>
|
|
||||||
<Text type="secondary" size="small">
|
|
||||||
{documentViews.length <= 1
|
|
||||||
? t("No one else has viewed yet")
|
|
||||||
: t(`Viewed {{ count }} times by {{ teamMembers }} people`, {
|
|
||||||
count: documentViews.reduce(
|
|
||||||
(memo, view) => memo + view.count,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
teamMembers: documentViews.length,
|
|
||||||
})}
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
<ListSpacing>
|
|
||||||
<DocumentViews document={document} isOpen />
|
|
||||||
</ListSpacing>
|
|
||||||
</Content>
|
|
||||||
<Content column>
|
<Content column>
|
||||||
<Heading>{t("Collaborators")}</Heading>
|
<Heading>{t("Collaborators")}</Heading>
|
||||||
<Text type="secondary" size="small">
|
<Text type="secondary" size="small">
|
||||||
@@ -129,6 +113,26 @@ function Insights() {
|
|||||||
/>
|
/>
|
||||||
</ListSpacing>
|
</ListSpacing>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content column>
|
||||||
|
<Heading>{t("Views")}</Heading>
|
||||||
|
<Text type="secondary" size="small">
|
||||||
|
{documentViews.length <= 1
|
||||||
|
? t("No one else has viewed yet")
|
||||||
|
: t(`Viewed {{ count }} times by {{ teamMembers }} people`, {
|
||||||
|
count: documentViews.reduce(
|
||||||
|
(memo, view) => memo + view.count,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
teamMembers: documentViews.length,
|
||||||
|
})}
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
{documentViews.length > 1 && (
|
||||||
|
<ListSpacing>
|
||||||
|
<DocumentViews document={document} isOpen />
|
||||||
|
</ListSpacing>
|
||||||
|
)}
|
||||||
|
</Content>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
@@ -145,7 +149,7 @@ function useTextStats(text: string, selectedText: string) {
|
|||||||
words: numTotalWords,
|
words: numTotalWords,
|
||||||
characters: text.length,
|
characters: text.length,
|
||||||
emoji: matches.length ?? 0,
|
emoji: matches.length ?? 0,
|
||||||
readingTime: Math.floor(numTotalWords / 200),
|
readingTime: Math.max(1, Math.floor(numTotalWords / 200)),
|
||||||
},
|
},
|
||||||
selected: {
|
selected: {
|
||||||
words: countWords(selectedText),
|
words: countWords(selectedText),
|
||||||
@@ -176,7 +180,7 @@ const List = styled("ul")`
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: ${(props) => props.theme.textTertiary};
|
color: ${(props) => props.theme.textTertiary};
|
||||||
width: 8px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { m } from "framer-motion";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { BackIcon } from "outline-icons";
|
import { BackIcon } from "outline-icons";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import styled, { useTheme } from "styled-components";
|
import styled, { useTheme } from "styled-components";
|
||||||
import breakpoint from "styled-components-breakpoint";
|
import breakpoint from "styled-components-breakpoint";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import Flex from "~/components/Flex";
|
import Flex from "~/components/Flex";
|
||||||
import Scrollable from "~/components/Scrollable";
|
import Scrollable from "~/components/Scrollable";
|
||||||
import ResizeBorder from "~/components/Sidebar/components/ResizeBorder";
|
import ResizeBorder from "~/components/Sidebar/components/ResizeBorder";
|
||||||
|
import Tooltip from "~/components/Tooltip";
|
||||||
import usePersistedState from "~/hooks/usePersistedState";
|
import usePersistedState from "~/hooks/usePersistedState";
|
||||||
|
|
||||||
type Props = React.HTMLAttributes<HTMLDivElement> & {
|
type Props = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
@@ -19,6 +21,7 @@ type Props = React.HTMLAttributes<HTMLDivElement> & {
|
|||||||
|
|
||||||
function RightSidebar({ title, onClose, children, border, className }: Props) {
|
function RightSidebar({ title, onClose, children, border, className }: Props) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [width, setWidth] = usePersistedState(
|
const [width, setWidth] = usePersistedState(
|
||||||
"rightSidebarWidth",
|
"rightSidebarWidth",
|
||||||
theme.sidebarWidth
|
theme.sidebarWidth
|
||||||
@@ -100,12 +103,14 @@ function RightSidebar({ title, onClose, children, border, className }: Props) {
|
|||||||
<Position style={style} column>
|
<Position style={style} column>
|
||||||
<Header>
|
<Header>
|
||||||
<Title>{title}</Title>
|
<Title>{title}</Title>
|
||||||
<Button
|
<Tooltip tooltip={t("Close")} shortcut="Esc" delay={500}>
|
||||||
icon={<ForwardIcon />}
|
<Button
|
||||||
onClick={onClose}
|
icon={<ForwardIcon />}
|
||||||
borderOnHover
|
onClick={onClose}
|
||||||
neutral
|
borderOnHover
|
||||||
/>
|
neutral
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Header>
|
</Header>
|
||||||
<Scrollable topShadow>{children}</Scrollable>
|
<Scrollable topShadow>{children}</Scrollable>
|
||||||
<ResizeBorder
|
<ResizeBorder
|
||||||
|
|||||||
@@ -445,16 +445,16 @@
|
|||||||
"{{ count }} words selected_plural": "{{ count }} words selected",
|
"{{ count }} words selected_plural": "{{ count }} words selected",
|
||||||
"{{ count }} characters selected": "{{ count }} character selected",
|
"{{ count }} characters selected": "{{ count }} character selected",
|
||||||
"{{ count }} characters selected_plural": "{{ count }} characters selected",
|
"{{ count }} characters selected_plural": "{{ count }} characters selected",
|
||||||
"Views": "Views",
|
|
||||||
"No one else has viewed yet": "No one else has viewed yet",
|
|
||||||
"Viewed {{ count }} times by {{ teamMembers }} people": "Viewed {{ count }} time by {{ teamMembers }} people",
|
|
||||||
"Viewed {{ count }} times by {{ teamMembers }} people_plural": "Viewed {{ count }} times by {{ teamMembers }} people",
|
|
||||||
"Collaborators": "Collaborators",
|
"Collaborators": "Collaborators",
|
||||||
"Created": "Created",
|
"Created": "Created",
|
||||||
"Last updated": "Last updated",
|
"Last updated": "Last updated",
|
||||||
"Creator": "Creator",
|
"Creator": "Creator",
|
||||||
"Last edited": "Last edited",
|
"Last edited": "Last edited",
|
||||||
"Previously edited": "Previously edited",
|
"Previously edited": "Previously edited",
|
||||||
|
"Views": "Views",
|
||||||
|
"No one else has viewed yet": "No one else has viewed yet",
|
||||||
|
"Viewed {{ count }} times by {{ teamMembers }} people": "Viewed {{ count }} time by {{ teamMembers }} people",
|
||||||
|
"Viewed {{ count }} times by {{ teamMembers }} people_plural": "Viewed {{ count }} times by {{ teamMembers }} people",
|
||||||
"Sorry, it looks like you don’t have permission to access the document": "Sorry, it looks like you don’t have permission to access the document",
|
"Sorry, it looks like you don’t have permission to access the document": "Sorry, it looks like you don’t have permission to access the document",
|
||||||
"Sorry, the last change could not be persisted – please reload the page": "Sorry, the last change could not be persisted – please reload the page",
|
"Sorry, the last change could not be persisted – please reload the page": "Sorry, the last change could not be persisted – please reload the page",
|
||||||
"This template will be permanently deleted in <2></2> unless restored.": "This template will be permanently deleted in <2></2> unless restored.",
|
"This template will be permanently deleted in <2></2> unless restored.": "This template will be permanently deleted in <2></2> unless restored.",
|
||||||
@@ -465,6 +465,7 @@
|
|||||||
"Deleted by {{userName}}": "Deleted by {{userName}}",
|
"Deleted by {{userName}}": "Deleted by {{userName}}",
|
||||||
"Observing {{ userName }}": "Observing {{ userName }}",
|
"Observing {{ userName }}": "Observing {{ userName }}",
|
||||||
"Backlinks": "Backlinks",
|
"Backlinks": "Backlinks",
|
||||||
|
"Close": "Close",
|
||||||
"Anyone with the link <1></1>can view this document": "Anyone with the link <1></1>can view this document",
|
"Anyone with the link <1></1>can view this document": "Anyone with the link <1></1>can view this document",
|
||||||
"Share": "Share",
|
"Share": "Share",
|
||||||
"Share this document": "Share this document",
|
"Share this document": "Share this document",
|
||||||
|
|||||||
Reference in New Issue
Block a user