mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
Tutorial: bring into line with designs
This commit is contained in:
parent
1f1747cb67
commit
fe22d33696
@ -1,8 +1,9 @@
|
||||
import { TutorialProgress, Associations } from "~/types";
|
||||
import { AlignX, AlignY } from "~/logic/lib/relativePosition";
|
||||
import { Direction } from "~/views/components/Triangle";
|
||||
|
||||
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_HEIGHT_PX = `${MODAL_HEIGHT}px`;
|
||||
|
||||
@ -20,6 +21,7 @@ interface StepDetail {
|
||||
alignY: AlignY | AlignY[];
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
arrow: Direction;
|
||||
}
|
||||
|
||||
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> = {
|
||||
hidden: {} as any,
|
||||
exit: {} as any,
|
||||
done: {
|
||||
title: "End",
|
||||
description:
|
||||
@ -41,14 +71,15 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||
offsetY: 0,
|
||||
},
|
||||
start: {
|
||||
title: "New group added",
|
||||
title: "New Group added",
|
||||
description:
|
||||
"We just added you to the Beginner island group to show you around. This group is public, but other groups can be private",
|
||||
url: "/",
|
||||
alignX: "right",
|
||||
alignY: "top",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
offsetY: 0,
|
||||
arrow: "West",
|
||||
offsetX: MODAL_WIDTH + 24,
|
||||
offsetY: 64,
|
||||
},
|
||||
"group-desc": {
|
||||
title: "What's a group",
|
||||
@ -57,7 +88,8 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
alignX: "left",
|
||||
alignY: "top",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
arrow: "East",
|
||||
offsetX: MODAL_WIDTH + 24,
|
||||
offsetY: MODAL_HEIGHT / 2 - 8,
|
||||
},
|
||||
channels: {
|
||||
@ -67,7 +99,8 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: MODAL_WIDTH + 8,
|
||||
arrow: "West",
|
||||
offsetX: MODAL_WIDTH + 24,
|
||||
offsetY: -8,
|
||||
},
|
||||
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",
|
||||
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
|
||||
alignY: "top",
|
||||
arrow: "North",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
offsetY: -56,
|
||||
offsetX: -8,
|
||||
},
|
||||
link: {
|
||||
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}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
arrow: "North",
|
||||
offsetX: -8,
|
||||
offsetY: -56,
|
||||
},
|
||||
publish: {
|
||||
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}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
arrow: "North",
|
||||
offsetX: -8,
|
||||
offsetY: -56,
|
||||
},
|
||||
notifications: {
|
||||
title: "Notifications",
|
||||
description:
|
||||
"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: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}/settings#notifications`,
|
||||
description: "You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.",
|
||||
url: '/~notifications',
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
alignX: "left",
|
||||
arrow: "North",
|
||||
offsetX: (MODAL_WIDTH / 2) - 16,
|
||||
offsetY: -48,
|
||||
},
|
||||
profile: {
|
||||
title: "Profile",
|
||||
@ -117,6 +153,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||
url: `/~profile/~${window.ship}`,
|
||||
alignY: "top",
|
||||
alignX: "right",
|
||||
arrow: "South",
|
||||
offsetX: -300 + MODAL_WIDTH / 2,
|
||||
offsetY: -120 + MODAL_HEIGHT / 2,
|
||||
},
|
||||
@ -127,7 +164,8 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
|
||||
url: `/~profile/~${window.ship}`,
|
||||
alignY: "top",
|
||||
alignX: "left",
|
||||
offsetX: 0,
|
||||
offsetY: -32,
|
||||
arrow: "North",
|
||||
offsetX: 0.3 *MODAL_HEIGHT,
|
||||
offsetY: -48,
|
||||
},
|
||||
};
|
||||
|
@ -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];
|
||||
interface LocalUpdateSetDark {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useState, useRef } from "react";
|
||||
import _ from 'lodash';
|
||||
import { Box, Col, Text, Row } from "@tlon/indigo-react";
|
||||
import { Link, Switch, Route } from "react-router-dom";
|
||||
@ -12,11 +12,13 @@ import { Dropdown } from "~/views/components/Dropdown";
|
||||
import { Formik } from "formik";
|
||||
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||
import GroupSearch from "~/views/components/GroupSearch";
|
||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
||||
|
||||
const baseUrl = "/~notifications";
|
||||
|
||||
const HeaderLink = (
|
||||
props: PropFunc<typeof Text> & { view?: string; current: string }
|
||||
const HeaderLink = React.forwardRef((
|
||||
props: PropFunc<typeof Text> & { view?: string; current: string },
|
||||
ref
|
||||
) => {
|
||||
const { current, view, ...textProps } = props;
|
||||
const to = view ? `${baseUrl}/${view}` : baseUrl;
|
||||
@ -24,10 +26,10 @@ const HeaderLink = (
|
||||
|
||||
return (
|
||||
<Link to={to}>
|
||||
<Text px="2" {...textProps} gray={!active} />
|
||||
<Text ref={ref} px="2" {...textProps} gray={!active} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
interface NotificationFilter {
|
||||
groups: string[];
|
||||
@ -37,15 +39,17 @@ export default function NotificationsScreen(props: any) {
|
||||
const relativePath = (p: string) => baseUrl + p;
|
||||
|
||||
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
||||
const onSubmit = async (values: { groups: string }) => {
|
||||
setFilter({ groups: values.groups ? [values.groups] : [] });
|
||||
const onSubmit = async ({ groups } : NotificationFilter) => {
|
||||
setFilter({ groups });
|
||||
};
|
||||
const groupFilterDesc =
|
||||
filter.groups.length === 0
|
||||
? "All"
|
||||
: filter.groups
|
||||
.map((g) => props.associations?.groups?.[g]?.metadata?.title)
|
||||
.join(", ");
|
||||
.join(", ");
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
useTutorialModal('notifications', true, anchorRef.current);
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
@ -71,7 +75,7 @@ export default function NotificationsScreen(props: any) {
|
||||
<Text>Updates</Text>
|
||||
<Row>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="">
|
||||
<HeaderLink ref={anchorRef} current={view} view="">
|
||||
Inbox
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
|
@ -54,13 +54,14 @@ export function Profile(props: any) {
|
||||
height="100%"
|
||||
width="100%">
|
||||
<Box
|
||||
ref={anchorRef}
|
||||
maxWidth="600px"
|
||||
width="100%">
|
||||
{ ship === `~${window.ship}` ? (
|
||||
<SetStatus ship={ship} contact={contact} api={props.api} />
|
||||
) : null
|
||||
}
|
||||
<Row ref={anchorRef} width="100%" height="300px">
|
||||
<Row width="100%" height="300px">
|
||||
{cover}
|
||||
</Row>
|
||||
<Row
|
||||
|
@ -14,10 +14,13 @@ import {
|
||||
MODAL_HEIGHT,
|
||||
TUTORIAL_HOST,
|
||||
TUTORIAL_GROUP,
|
||||
getTrianglePosition,
|
||||
} from "~/logic/lib/tutorialModal";
|
||||
import { getRelativePosition } from "~/logic/lib/relativePosition";
|
||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import {Triangle} from "~/views/components/Triangle";
|
||||
import {ModalOverlay} from "~/views/components/ModalOverlay";
|
||||
|
||||
const localSelector = selectLocalState([
|
||||
"tutorialProgress",
|
||||
@ -25,6 +28,7 @@ const localSelector = selectLocalState([
|
||||
"prevTutStep",
|
||||
"tutorialRef",
|
||||
"hideTutorial",
|
||||
"set"
|
||||
]);
|
||||
|
||||
export function TutorialModal(props: { api: GlobalApi }) {
|
||||
@ -34,10 +38,12 @@ export function TutorialModal(props: { api: GlobalApi }) {
|
||||
nextTutStep,
|
||||
prevTutStep,
|
||||
hideTutorial,
|
||||
set: setLocalState
|
||||
} = useLocalState(localSelector);
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
arrow,
|
||||
alignX,
|
||||
alignY,
|
||||
offsetX,
|
||||
@ -45,23 +51,22 @@ export function TutorialModal(props: { api: GlobalApi }) {
|
||||
} = progressDetails[tutorialProgress];
|
||||
|
||||
const [coords, setCoords] = useState({});
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const next = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
const next = useCallback( () => {
|
||||
const idx = progress.findIndex((p) => p === tutorialProgress);
|
||||
const { url } = progressDetails[progress[idx + 1]];
|
||||
history.push(url);
|
||||
nextTutStep();
|
||||
history.push(url);
|
||||
},
|
||||
[nextTutStep, history, tutorialProgress, setCoords]
|
||||
);
|
||||
const prev = useCallback(() => {
|
||||
const idx = progress.findIndex((p) => p === tutorialProgress);
|
||||
history.push(progressDetails[progress[idx - 1]].url);
|
||||
prevTutStep();
|
||||
history.push(progressDetails[progress[idx - 1]].url);
|
||||
}, [prevTutStep, history, tutorialProgress]);
|
||||
|
||||
const updatePos = useCallback(() => {
|
||||
@ -87,14 +92,25 @@ export function TutorialModal(props: { api: GlobalApi }) {
|
||||
|
||||
if (newCoords) {
|
||||
setCoords(withMobile);
|
||||
} else {
|
||||
setCoords({});
|
||||
|
||||
}
|
||||
}, [tutorialRef]);
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
const dismiss = useCallback(async () => {
|
||||
hideTutorial();
|
||||
props.api.settings.putEntry("tutorial", "seen", true);
|
||||
await props.api.settings.putEntry('tutorial', 'seen', true);
|
||||
}, [hideTutorial, props.api]);
|
||||
|
||||
const bailExit = useCallback(() => {
|
||||
setPaused(false);
|
||||
}, []);
|
||||
|
||||
const tryExit = useCallback(() => {
|
||||
setPaused(true);
|
||||
}, []);
|
||||
|
||||
const leaveGroup = useCallback(async () => {
|
||||
await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP);
|
||||
}, [props.api]);
|
||||
@ -109,27 +125,81 @@ export function TutorialModal(props: { api: GlobalApi }) {
|
||||
) {
|
||||
const interval = setInterval(updatePos, 100);
|
||||
return () => {
|
||||
setCoords({});
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, [tutorialRef, tutorialProgress, updatePos]);
|
||||
|
||||
// manually center final window
|
||||
useEffect(() => {
|
||||
if (tutorialProgress === "done") {
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const left = ["0px", `${(innerWidth - MODAL_WIDTH) / 2}px`];
|
||||
const top = [null, `${(innerHeight - MODAL_HEIGHT) / 2}px`];
|
||||
const bottom = ["0px", null];
|
||||
setCoords({ top, left, bottom });
|
||||
}
|
||||
}, [tutorialProgress]);
|
||||
const triPos = getTrianglePosition(arrow);
|
||||
|
||||
if (tutorialProgress === 'done') {
|
||||
return (
|
||||
<Portal>
|
||||
<ModalOverlay dismiss={dismiss} borderRadius="2" maxWidth="270px" backgroundColor="white">
|
||||
<Col p="2" bg="lightBlue">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
if (tutorialProgress === "hidden") {
|
||||
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 (
|
||||
<Portal>
|
||||
<Box
|
||||
@ -149,40 +219,47 @@ export function TutorialModal(props: { api: GlobalApi }) {
|
||||
borderRadius="2"
|
||||
p="2"
|
||||
bg="lightBlue"
|
||||
|
||||
>
|
||||
<Triangle
|
||||
{...triPos}
|
||||
position="absolute"
|
||||
size={16}
|
||||
color="lightBlue"
|
||||
direction={arrow}
|
||||
height="0px"
|
||||
width="0px"
|
||||
/>
|
||||
|
||||
<Box
|
||||
right="8px"
|
||||
top="8px"
|
||||
position="absolute"
|
||||
cursor="pointer"
|
||||
onClick={dismiss}
|
||||
onClick={tryExit}
|
||||
>
|
||||
<Icon icon="X" />
|
||||
</Box>
|
||||
<Text lineHeight="tall" fontWeight="medium">
|
||||
{title}
|
||||
</Text>
|
||||
<Col mb="1">
|
||||
<Text lineHeight="tall" fontWeight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize="0" gray>
|
||||
{progressIdx} of {progress.length - 2}
|
||||
</Text>
|
||||
</Col>
|
||||
|
||||
<Text lineHeight="tall">{description}</Text>
|
||||
{tutorialProgress !== "done" ? (
|
||||
<Row justifyContent="space-between">
|
||||
<Action bg="transparent" onClick={prev}>
|
||||
<Icon icon="ArrowWest" />
|
||||
</Action>
|
||||
<Text>
|
||||
{progressIdx}/{progress.length - 1}
|
||||
</Text>
|
||||
<Action bg="transparent" onClick={next}>
|
||||
<Icon icon="ArrowEast" />
|
||||
</Action>
|
||||
</Row>
|
||||
) : (
|
||||
<Row justifyContent="space-between">
|
||||
<StatelessAsyncButton primary onClick={leaveGroup}>
|
||||
Leave Group
|
||||
</StatelessAsyncButton>
|
||||
<Button onClick={dismiss}>Later</Button>
|
||||
</Row>
|
||||
)}
|
||||
<Row gapX="2" mt="2" justifyContent="flex-end">
|
||||
{ progressIdx > 1 && (
|
||||
<Button bg="washedGray" onClick={prev}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button primary onClick={next}>
|
||||
Next
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Box>
|
||||
</Portal>
|
||||
|
Loading…
Reference in New Issue
Block a user