Insights refinements

This commit is contained in:
Tom Moor
2022-11-13 14:47:20 -05:00
parent 3880a956a3
commit b60c66316a
5 changed files with 71 additions and 70 deletions

View File

@@ -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) => ( &nbsp;&nbsp;
<Wrapper> <Link
&nbsp;&nbsp; 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>
); );
} }

View File

@@ -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;
} }

View File

@@ -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;
} }
`; `;

View File

@@ -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

View File

@@ -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 dont have permission to access the document": "Sorry, it looks like you dont have permission to access the document", "Sorry, it looks like you dont have permission to access the document": "Sorry, it looks like you dont 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",