Tutorial: bring into line with designs

This commit is contained in:
Liam Fitzgerald 2021-02-11 16:13:23 +10:00
parent 1f1747cb67
commit fe22d33696
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
5 changed files with 192 additions and 72 deletions

View File

@ -1,8 +1,9 @@
import { TutorialProgress, Associations } from "~/types"; import { TutorialProgress, Associations } from "~/types";
import { AlignX, AlignY } from "~/logic/lib/relativePosition"; import { AlignX, AlignY } from "~/logic/lib/relativePosition";
import { Direction } from "~/views/components/Triangle";
export const MODAL_WIDTH = 256; export const MODAL_WIDTH = 256;
export const MODAL_HEIGHT = 180; export const MODAL_HEIGHT = 256;
export const MODAL_WIDTH_PX = `${MODAL_WIDTH}px`; export const MODAL_WIDTH_PX = `${MODAL_WIDTH}px`;
export const MODAL_HEIGHT_PX = `${MODAL_HEIGHT}px`; export const MODAL_HEIGHT_PX = `${MODAL_HEIGHT}px`;
@ -20,6 +21,7 @@ interface StepDetail {
alignY: AlignY | AlignY[]; alignY: AlignY | AlignY[];
offsetX: number; offsetX: number;
offsetY: number; offsetY: number;
arrow: Direction;
} }
export function hasTutorialGroup(props: { associations: Associations }) { export function hasTutorialGroup(props: { associations: Associations }) {
@ -28,8 +30,36 @@ export function hasTutorialGroup(props: { associations: Associations }) {
); );
} }
export const getTrianglePosition = (dir: Direction) => {
const midY = `${MODAL_HEIGHT / 2 - 8}px`;
const midX = `${MODAL_WIDTH / 2 - 8}px`;
switch(dir) {
case 'East':
return {
top: midY,
right: '-32px'
};
case 'West':
return {
top: midY,
left: '-32px'
}
case 'North':
return {
top: '-32px',
left: midX
};
case 'South':
return {
bottom: '-32px',
left: midX
};
}
}
export const progressDetails: Record<TutorialProgress, StepDetail> = { export const progressDetails: Record<TutorialProgress, StepDetail> = {
hidden: {} as any, hidden: {} as any,
exit: {} as any,
done: { done: {
title: "End", title: "End",
description: description:
@ -41,14 +71,15 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
offsetY: 0, offsetY: 0,
}, },
start: { start: {
title: "New group added", title: "New Group added",
description: description:
"We just added you to the Beginner island group to show you around. This group is public, but other groups can be private", "We just added you to the Beginner island group to show you around. This group is public, but other groups can be private",
url: "/", url: "/",
alignX: "right", alignX: "right",
alignY: "top", alignY: "top",
offsetX: MODAL_WIDTH + 8, arrow: "West",
offsetY: 0, offsetX: MODAL_WIDTH + 24,
offsetY: 64,
}, },
"group-desc": { "group-desc": {
title: "What's a group", title: "What's a group",
@ -57,7 +88,8 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`, url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
alignX: "left", alignX: "left",
alignY: "top", alignY: "top",
offsetX: MODAL_WIDTH + 8, arrow: "East",
offsetX: MODAL_WIDTH + 24,
offsetY: MODAL_HEIGHT / 2 - 8, offsetY: MODAL_HEIGHT / 2 - 8,
}, },
channels: { channels: {
@ -67,7 +99,8 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`, url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
alignY: "top", alignY: "top",
alignX: "right", alignX: "right",
offsetX: MODAL_WIDTH + 8, arrow: "West",
offsetX: MODAL_WIDTH + 24,
offsetY: -8, offsetY: -8,
}, },
chat: { chat: {
@ -76,9 +109,10 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
"Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen", "Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen",
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`, url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
alignY: "top", alignY: "top",
arrow: "North",
alignX: "right", alignX: "right",
offsetX: 0, offsetY: -56,
offsetY: -32, offsetX: -8,
}, },
link: { link: {
title: "Collection", title: "Collection",
@ -87,8 +121,9 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`, url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`,
alignY: "top", alignY: "top",
alignX: "right", alignX: "right",
offsetX: 0, arrow: "North",
offsetY: -32, offsetX: -8,
offsetY: -56,
}, },
publish: { publish: {
title: "Notebook", title: "Notebook",
@ -97,18 +132,19 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`, url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`,
alignY: "top", alignY: "top",
alignX: "right", alignX: "right",
offsetX: 0, arrow: "North",
offsetY: -32, offsetX: -8,
offsetY: -56,
}, },
notifications: { notifications: {
title: "Notifications", title: "Notifications",
description: description: "You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.",
"Subscribing to a channel will send you notifications when there are new updates. You will also receive a notification when someone mentions your name in a channel.", url: '/~notifications',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}/settings#notifications`,
alignY: "top", alignY: "top",
alignX: "right", alignX: "left",
offsetX: 0, arrow: "North",
offsetY: -32, offsetX: (MODAL_WIDTH / 2) - 16,
offsetY: -48,
}, },
profile: { profile: {
title: "Profile", title: "Profile",
@ -117,6 +153,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
url: `/~profile/~${window.ship}`, url: `/~profile/~${window.ship}`,
alignY: "top", alignY: "top",
alignX: "right", alignX: "right",
arrow: "South",
offsetX: -300 + MODAL_WIDTH / 2, offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -120 + MODAL_HEIGHT / 2, offsetY: -120 + MODAL_HEIGHT / 2,
}, },
@ -127,7 +164,8 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
url: `/~profile/~${window.ship}`, url: `/~profile/~${window.ship}`,
alignY: "top", alignY: "top",
alignX: "left", alignX: "left",
offsetX: 0, arrow: "North",
offsetY: -32, offsetX: 0.3 *MODAL_HEIGHT,
offsetY: -48,
}, },
}; };

View File

@ -1,4 +1,4 @@
export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'notifications', 'profile', 'leap', 'done'] as const; export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'profile', 'leap', 'notifications', 'done', 'exit'] as const;
export type TutorialProgress = typeof tutorialProgress[number]; export type TutorialProgress = typeof tutorialProgress[number];
interface LocalUpdateSetDark { interface LocalUpdateSetDark {

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState, useRef } from "react";
import _ from 'lodash'; import _ from 'lodash';
import { Box, Col, Text, Row } from "@tlon/indigo-react"; import { Box, Col, Text, Row } from "@tlon/indigo-react";
import { Link, Switch, Route } from "react-router-dom"; import { Link, Switch, Route } from "react-router-dom";
@ -12,11 +12,13 @@ import { Dropdown } from "~/views/components/Dropdown";
import { Formik } from "formik"; import { Formik } from "formik";
import { FormikOnBlur } from "~/views/components/FormikOnBlur"; import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import GroupSearch from "~/views/components/GroupSearch"; import GroupSearch from "~/views/components/GroupSearch";
import {useTutorialModal} from "~/views/components/useTutorialModal";
const baseUrl = "/~notifications"; const baseUrl = "/~notifications";
const HeaderLink = ( const HeaderLink = React.forwardRef((
props: PropFunc<typeof Text> & { view?: string; current: string } props: PropFunc<typeof Text> & { view?: string; current: string },
ref
) => { ) => {
const { current, view, ...textProps } = props; const { current, view, ...textProps } = props;
const to = view ? `${baseUrl}/${view}` : baseUrl; const to = view ? `${baseUrl}/${view}` : baseUrl;
@ -24,10 +26,10 @@ const HeaderLink = (
return ( return (
<Link to={to}> <Link to={to}>
<Text px="2" {...textProps} gray={!active} /> <Text ref={ref} px="2" {...textProps} gray={!active} />
</Link> </Link>
); );
}; });
interface NotificationFilter { interface NotificationFilter {
groups: string[]; groups: string[];
@ -37,8 +39,8 @@ export default function NotificationsScreen(props: any) {
const relativePath = (p: string) => baseUrl + p; const relativePath = (p: string) => baseUrl + p;
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] }); const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
const onSubmit = async (values: { groups: string }) => { const onSubmit = async ({ groups } : NotificationFilter) => {
setFilter({ groups: values.groups ? [values.groups] : [] }); setFilter({ groups });
}; };
const groupFilterDesc = const groupFilterDesc =
filter.groups.length === 0 filter.groups.length === 0
@ -46,6 +48,8 @@ export default function NotificationsScreen(props: any) {
: filter.groups : filter.groups
.map((g) => props.associations?.groups?.[g]?.metadata?.title) .map((g) => props.associations?.groups?.[g]?.metadata?.title)
.join(", "); .join(", ");
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('notifications', true, anchorRef.current);
return ( return (
<Switch> <Switch>
<Route <Route
@ -71,7 +75,7 @@ export default function NotificationsScreen(props: any) {
<Text>Updates</Text> <Text>Updates</Text>
<Row> <Row>
<Box> <Box>
<HeaderLink current={view} view=""> <HeaderLink ref={anchorRef} current={view} view="">
Inbox Inbox
</HeaderLink> </HeaderLink>
</Box> </Box>

View File

@ -54,13 +54,14 @@ export function Profile(props: any) {
height="100%" height="100%"
width="100%"> width="100%">
<Box <Box
ref={anchorRef}
maxWidth="600px" maxWidth="600px"
width="100%"> width="100%">
{ ship === `~${window.ship}` ? ( { ship === `~${window.ship}` ? (
<SetStatus ship={ship} contact={contact} api={props.api} /> <SetStatus ship={ship} contact={contact} api={props.api} />
) : null ) : null
} }
<Row ref={anchorRef} width="100%" height="300px"> <Row width="100%" height="300px">
{cover} {cover}
</Row> </Row>
<Row <Row

View File

@ -14,10 +14,13 @@ import {
MODAL_HEIGHT, MODAL_HEIGHT,
TUTORIAL_HOST, TUTORIAL_HOST,
TUTORIAL_GROUP, TUTORIAL_GROUP,
getTrianglePosition,
} from "~/logic/lib/tutorialModal"; } from "~/logic/lib/tutorialModal";
import { getRelativePosition } from "~/logic/lib/relativePosition"; import { getRelativePosition } from "~/logic/lib/relativePosition";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton"; import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import {Triangle} from "~/views/components/Triangle";
import {ModalOverlay} from "~/views/components/ModalOverlay";
const localSelector = selectLocalState([ const localSelector = selectLocalState([
"tutorialProgress", "tutorialProgress",
@ -25,6 +28,7 @@ const localSelector = selectLocalState([
"prevTutStep", "prevTutStep",
"tutorialRef", "tutorialRef",
"hideTutorial", "hideTutorial",
"set"
]); ]);
export function TutorialModal(props: { api: GlobalApi }) { export function TutorialModal(props: { api: GlobalApi }) {
@ -34,10 +38,12 @@ export function TutorialModal(props: { api: GlobalApi }) {
nextTutStep, nextTutStep,
prevTutStep, prevTutStep,
hideTutorial, hideTutorial,
set: setLocalState
} = useLocalState(localSelector); } = useLocalState(localSelector);
const { const {
title, title,
description, description,
arrow,
alignX, alignX,
alignY, alignY,
offsetX, offsetX,
@ -45,23 +51,22 @@ export function TutorialModal(props: { api: GlobalApi }) {
} = progressDetails[tutorialProgress]; } = progressDetails[tutorialProgress];
const [coords, setCoords] = useState({}); const [coords, setCoords] = useState({});
const [paused, setPaused] = useState(false);
const history = useHistory(); const history = useHistory();
const next = useCallback( const next = useCallback( () => {
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
const idx = progress.findIndex((p) => p === tutorialProgress); const idx = progress.findIndex((p) => p === tutorialProgress);
const { url } = progressDetails[progress[idx + 1]]; const { url } = progressDetails[progress[idx + 1]];
history.push(url);
nextTutStep(); nextTutStep();
history.push(url);
}, },
[nextTutStep, history, tutorialProgress, setCoords] [nextTutStep, history, tutorialProgress, setCoords]
); );
const prev = useCallback(() => { const prev = useCallback(() => {
const idx = progress.findIndex((p) => p === tutorialProgress); const idx = progress.findIndex((p) => p === tutorialProgress);
history.push(progressDetails[progress[idx - 1]].url);
prevTutStep(); prevTutStep();
history.push(progressDetails[progress[idx - 1]].url);
}, [prevTutStep, history, tutorialProgress]); }, [prevTutStep, history, tutorialProgress]);
const updatePos = useCallback(() => { const updatePos = useCallback(() => {
@ -87,14 +92,25 @@ export function TutorialModal(props: { api: GlobalApi }) {
if (newCoords) { if (newCoords) {
setCoords(withMobile); setCoords(withMobile);
} else {
setCoords({});
} }
}, [tutorialRef]); }, [tutorialRef]);
const dismiss = useCallback(() => { const dismiss = useCallback(async () => {
hideTutorial(); hideTutorial();
props.api.settings.putEntry("tutorial", "seen", true); await props.api.settings.putEntry('tutorial', 'seen', true);
}, [hideTutorial, props.api]); }, [hideTutorial, props.api]);
const bailExit = useCallback(() => {
setPaused(false);
}, []);
const tryExit = useCallback(() => {
setPaused(true);
}, []);
const leaveGroup = useCallback(async () => { const leaveGroup = useCallback(async () => {
await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP); await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP);
}, [props.api]); }, [props.api]);
@ -109,27 +125,81 @@ export function TutorialModal(props: { api: GlobalApi }) {
) { ) {
const interval = setInterval(updatePos, 100); const interval = setInterval(updatePos, 100);
return () => { return () => {
setCoords({});
clearInterval(interval); clearInterval(interval);
}; };
} }
return () => {}; return () => {};
}, [tutorialRef, tutorialProgress, updatePos]); }, [tutorialRef, tutorialProgress, updatePos]);
// manually center final window const triPos = getTrianglePosition(arrow);
useEffect(() => {
if (tutorialProgress === "done") { if (tutorialProgress === 'done') {
const { innerWidth, innerHeight } = window; return (
const left = ["0px", `${(innerWidth - MODAL_WIDTH) / 2}px`]; <Portal>
const top = [null, `${(innerHeight - MODAL_HEIGHT) / 2}px`]; <ModalOverlay dismiss={dismiss} borderRadius="2" maxWidth="270px" backgroundColor="white">
const bottom = ["0px", null]; <Col p="2" bg="lightBlue">
setCoords({ top, left, bottom }); <Col mb="1">
<Text lineHeight="tall" fontWeight="bold">
Tutorial Finished
</Text>
<Text fontSize="0" gray>
{progressIdx} of {progress.length - 1}
</Text>
</Col>
<Text lineHeight="tall">
This tutorial is finished. Would you like to leave Beginner Island?
</Text>
<Row mt="2" gapX="2" justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={dismiss}>
Later
</Button>
<StatelessAsyncButton primary destructive onClick={leaveGroup}>
Leave Group
</StatelessAsyncButton>
</Row>
</Col>
</ModalOverlay>
</Portal>
);
} }
}, [tutorialProgress]);
if (tutorialProgress === "hidden") { if (tutorialProgress === "hidden") {
return null; return null;
} }
if(paused) {
return (
<ModalOverlay dismiss={bailExit} borderRadius="2" maxWidth="270px" backgroundColor="white">
<Col p="2">
<Col mb="1">
<Text lineHeight="tall" fontWeight="bold">
End Tutorial Now?
</Text>
</Col>
<Text lineHeight="tall">
You can always restart the tutorial by typing "tutorial" in Leap.
</Text>
<Row mt="4" gapX="2" justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={bailExit}>
Cancel
</Button>
<StatelessAsyncButton primary destructive onClick={dismiss}>
End Tutorial
</StatelessAsyncButton>
</Row>
</Col>
</ModalOverlay>
)
}
if(Object.keys(coords).length === 0) {
return null;
}
return ( return (
<Portal> <Portal>
<Box <Box
@ -149,40 +219,47 @@ export function TutorialModal(props: { api: GlobalApi }) {
borderRadius="2" borderRadius="2"
p="2" p="2"
bg="lightBlue" bg="lightBlue"
> >
<Triangle
{...triPos}
position="absolute"
size={16}
color="lightBlue"
direction={arrow}
height="0px"
width="0px"
/>
<Box <Box
right="8px" right="8px"
top="8px" top="8px"
position="absolute" position="absolute"
cursor="pointer" cursor="pointer"
onClick={dismiss} onClick={tryExit}
> >
<Icon icon="X" /> <Icon icon="X" />
</Box> </Box>
<Text lineHeight="tall" fontWeight="medium"> <Col mb="1">
<Text lineHeight="tall" fontWeight="bold">
{title} {title}
</Text> </Text>
<Text lineHeight="tall">{description}</Text> <Text fontSize="0" gray>
{tutorialProgress !== "done" ? ( {progressIdx} of {progress.length - 2}
<Row justifyContent="space-between">
<Action bg="transparent" onClick={prev}>
<Icon icon="ArrowWest" />
</Action>
<Text>
{progressIdx}/{progress.length - 1}
</Text> </Text>
<Action bg="transparent" onClick={next}> </Col>
<Icon icon="ArrowEast" />
</Action> <Text lineHeight="tall">{description}</Text>
</Row> <Row gapX="2" mt="2" justifyContent="flex-end">
) : ( { progressIdx > 1 && (
<Row justifyContent="space-between"> <Button bg="washedGray" onClick={prev}>
<StatelessAsyncButton primary onClick={leaveGroup}> Back
Leave Group </Button>
</StatelessAsyncButton>
<Button onClick={dismiss}>Later</Button>
</Row>
)} )}
<Button primary onClick={next}>
Next
</Button>
</Row>
</Col> </Col>
</Box> </Box>
</Portal> </Portal>