mirror of
https://github.com/urbit/shrub.git
synced 2024-12-24 11:24:21 +03:00
Merge pull request #4415 from urbit/lf/tutorial-fixes
Tutorial: bugfixes
This commit is contained in:
commit
16d83d97ab
@ -122,9 +122,9 @@ module.exports = {
|
|||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
|
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
|
||||||
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
|
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
|
||||||
'process.env.TUTORIAL_CHAT': JSON.stringify('chat-8401'),
|
'process.env.TUTORIAL_CHAT': JSON.stringify('chat-1704'),
|
||||||
'process.env.TUTORIAL_BOOK': JSON.stringify('notebook-9148'),
|
'process.env.TUTORIAL_BOOK': JSON.stringify('book-9695'),
|
||||||
'process.env.TUTORIAL_LINKS': JSON.stringify('link-4353'),
|
'process.env.TUTORIAL_LINKS': JSON.stringify('link-2827'),
|
||||||
})
|
})
|
||||||
|
|
||||||
// new CleanWebpackPlugin(),
|
// new CleanWebpackPlugin(),
|
||||||
|
@ -32,7 +32,7 @@ export default class SettingsApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
putEntry(buc: Key, key: Key, val: Value) {
|
putEntry(buc: Key, key: Key, val: Value) {
|
||||||
this.storeAction({
|
return this.storeAction({
|
||||||
"put-entry": {
|
"put-entry": {
|
||||||
"bucket-key": buc,
|
"bucket-key": buc,
|
||||||
"entry-key": key,
|
"entry-key": key,
|
||||||
|
@ -39,10 +39,11 @@ const commandIndex = function (currentGroup, groups, associations) {
|
|||||||
: !currentGroup; // home workspace or hasn't loaded
|
: !currentGroup; // home workspace or hasn't loaded
|
||||||
const workspace = currentGroup || '/home';
|
const workspace = currentGroup || '/home';
|
||||||
commands.push(result(`Groups: Create`, `/~landscape/new`, 'Groups', null));
|
commands.push(result(`Groups: Create`, `/~landscape/new`, 'Groups', null));
|
||||||
commands.push(result(`Groups: Join`, `/~landscape/join`, 'Groups', null));
|
|
||||||
if (canAdd) {
|
if (canAdd) {
|
||||||
commands.push(result(`Channel: Create`, `/~landscape${workspace}/new`, 'Groups', null));
|
commands.push(result(`Channel: Create`, `/~landscape${workspace}/new`, 'Groups', null));
|
||||||
}
|
}
|
||||||
|
commands.push(result(`Groups: Join`, `/~landscape/join`, 'Groups', null));
|
||||||
|
commands.push(result(`Tutorial`, '/?tutorial=true', 'Null', null));
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,8 @@ import { Box } from "@tlon/indigo-react";
|
|||||||
import { useOutsideClick } from "./useOutsideClick";
|
import { useOutsideClick } from "./useOutsideClick";
|
||||||
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
||||||
import {Portal} from "~/views/components/Portal";
|
import {Portal} from "~/views/components/Portal";
|
||||||
|
import {ModalPortal} from "~/views/components/ModalPortal";
|
||||||
|
import {PropFunc} from "~/types";
|
||||||
|
|
||||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||||
interface UseModalProps {
|
interface UseModalProps {
|
||||||
@ -23,7 +25,8 @@ interface UseModalResult {
|
|||||||
showModal: () => void;
|
showModal: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModal(props: UseModalProps): UseModalResult {
|
export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalResult {
|
||||||
|
const { modal, ...rest } = props;
|
||||||
const innerRef = useRef<HTMLElement>();
|
const innerRef = useRef<HTMLElement>();
|
||||||
const [modalShown, setModalShown] = useState(false);
|
const [modalShown, setModalShown] = useState(false);
|
||||||
|
|
||||||
@ -39,15 +42,13 @@ export function useModal(props: UseModalProps): UseModalResult {
|
|||||||
() =>
|
() =>
|
||||||
!modalShown
|
!modalShown
|
||||||
? null
|
? null
|
||||||
: typeof props.modal === "function"
|
: typeof modal === "function"
|
||||||
? props.modal(dismiss)
|
? modal(dismiss)
|
||||||
: props.modal,
|
: modal,
|
||||||
[modalShown, props.modal, dismiss]
|
[modalShown, modal, dismiss]
|
||||||
);
|
);
|
||||||
|
|
||||||
useOutsideClick(innerRef, dismiss);
|
const modalComponent = useMemo(
|
||||||
|
|
||||||
const modal = useMemo(
|
|
||||||
() =>
|
() =>
|
||||||
!inner ? null : (
|
!inner ? null : (
|
||||||
<Portal>
|
<Portal>
|
||||||
@ -63,7 +64,8 @@ export function useModal(props: UseModalProps): UseModalResult {
|
|||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
spacing="2"
|
spacing="2"
|
||||||
|
dismiss={dismiss}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{inner}
|
{inner}
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
@ -74,6 +76,6 @@ export function useModal(props: UseModalProps): UseModalResult {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
showModal,
|
showModal,
|
||||||
modal,
|
modal: modalComponent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,7 @@ export function useOutsideClick(
|
|||||||
const portalRoot = document.querySelector('#portal-root')!;
|
const portalRoot = document.querySelector('#portal-root')!;
|
||||||
if (
|
if (
|
||||||
ref.current &&
|
ref.current &&
|
||||||
!ref.current.contains(event.target as any) &&
|
!ref.current.contains(event.target as any)
|
||||||
(!portalRoot.contains(ref.current) || portalRoot.contains(event.target as any))
|
|
||||||
) {
|
) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@ function decodePolicy(policy: Enc<GroupPolicy>): GroupPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function decodeTags(tags: Enc<Tags>): Tags {
|
function decodeTags(tags: Enc<Tags>): Tags {
|
||||||
console.log(tags);
|
|
||||||
return _.reduce(
|
return _.reduce(
|
||||||
tags,
|
tags,
|
||||||
(acc, ships, key): Tags => {
|
(acc, ships, key): Tags => {
|
||||||
|
@ -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 {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
@ -20,6 +20,7 @@ import { JoinGroup } from "~/views/landscape/components/JoinGroup";
|
|||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import useLocalState from "~/logic/state/local";
|
import useLocalState from "~/logic/state/local";
|
||||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||||
|
import { useQuery } from "~/logic/lib/useQuery";
|
||||||
import {
|
import {
|
||||||
hasTutorialGroup,
|
hasTutorialGroup,
|
||||||
TUTORIAL_GROUP,
|
TUTORIAL_GROUP,
|
||||||
@ -69,11 +70,25 @@ export default function LaunchApp(props) {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { query } = useQuery();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(query.get('tutorial')) {
|
||||||
|
if(hasTutorialGroup(props)) {
|
||||||
|
nextTutStep();
|
||||||
|
} else {
|
||||||
|
showModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
|
||||||
|
|
||||||
const waiter = useWaitForProps(props);
|
const waiter = useWaitForProps(props);
|
||||||
|
|
||||||
const { modal, showModal } = useModal({
|
const { modal, showModal } = useModal({
|
||||||
|
position: 'relative',
|
||||||
|
maxWidth: '350px',
|
||||||
modal: (dismiss) => {
|
modal: (dismiss) => {
|
||||||
const onDismiss = (e) => {
|
const onDismiss = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -87,7 +102,7 @@ export default function LaunchApp(props) {
|
|||||||
await waiter(hasTutorialGroup);
|
await waiter(hasTutorialGroup);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph =>
|
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph =>
|
||||||
api.graph.join(TUTORIAL_HOST, graph)));
|
props.api.graph.join(TUTORIAL_HOST, graph)));
|
||||||
|
|
||||||
await waiter(p => {
|
await waiter(p => {
|
||||||
return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph &&
|
return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph &&
|
||||||
@ -99,7 +114,10 @@ export default function LaunchApp(props) {
|
|||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Col gapY="2" p="3">
|
<Col maxWidth="350px" gapY="2" p="3">
|
||||||
|
<Box position="absolute" left="-16px" top="-16px">
|
||||||
|
<Icon width="32px" height="32px" color="blue" display="block" icon="LargeBullet" />
|
||||||
|
</Box>
|
||||||
<Text lineHeight="tall" fontWeight="medium">Welcome</Text>
|
<Text lineHeight="tall" fontWeight="medium">Welcome</Text>
|
||||||
<Text lineHeight="tall">
|
<Text lineHeight="tall">
|
||||||
You have been invited to use Landscape, an interface to chat
|
You have been invited to use Landscape, an interface to chat
|
||||||
@ -108,7 +126,7 @@ export default function LaunchApp(props) {
|
|||||||
Would you like a tour of Landscape?
|
Would you like a tour of Landscape?
|
||||||
</Text>
|
</Text>
|
||||||
<Row gapX="2" justifyContent="flex-end">
|
<Row gapX="2" justifyContent="flex-end">
|
||||||
<Button onClick={onDismiss}>Skip</Button>
|
<Button backgroundColor="washedGray" onClick={onDismiss}>Skip</Button>
|
||||||
<StatelessAsyncButton primary onClick={onContinue}>
|
<StatelessAsyncButton primary onClick={onContinue}>
|
||||||
Yes
|
Yes
|
||||||
</StatelessAsyncButton>
|
</StatelessAsyncButton>
|
||||||
@ -116,12 +134,14 @@ export default function LaunchApp(props) {
|
|||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
});
|
});
|
||||||
|
const hasLoaded = useMemo(() => Object.keys(props.contacts).length > 0, [props.contacts]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);
|
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);
|
||||||
if(!seenTutorial && tutorialProgress === 'hidden') {
|
if(hasLoaded && !seenTutorial && tutorialProgress === 'hidden') {
|
||||||
showModal();
|
showModal();
|
||||||
}
|
}
|
||||||
}, [props.settings]);
|
}, [props.settings, hasLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -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 onReadAll = useCallback(() => {
|
const onReadAll = useCallback(() => {
|
||||||
props.api.hark.readAll()
|
props.api.hark.readAll()
|
||||||
@ -49,6 +51,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
|
||||||
@ -74,7 +78,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>
|
||||||
|
@ -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
|
||||||
|
@ -75,7 +75,8 @@ export function GroupSearch<I extends string, V extends FormValues<I>>(props: Gr
|
|||||||
touched: touchedFields,
|
touched: touchedFields,
|
||||||
errors,
|
errors,
|
||||||
initialValues,
|
initialValues,
|
||||||
setFieldValue
|
setFieldValue,
|
||||||
|
setFieldTouched,
|
||||||
} = useFormikContext<V>();
|
} = useFormikContext<V>();
|
||||||
const [inputIdx, setInputIdx] = useState(initialValues[id].length);
|
const [inputIdx, setInputIdx] = useState(initialValues[id].length);
|
||||||
const name = `${id}[${inputIdx}]`;
|
const name = `${id}[${inputIdx}]`;
|
||||||
@ -118,10 +119,12 @@ export function GroupSearch<I extends string, V extends FormValues<I>>(props: Gr
|
|||||||
render={(arrayHelpers) => {
|
render={(arrayHelpers) => {
|
||||||
const onSelect = (a: Association) => {
|
const onSelect = (a: Association) => {
|
||||||
setFieldValue(name, a.group);
|
setFieldValue(name, a.group);
|
||||||
|
setFieldTouched(name, true, false);
|
||||||
setInputIdx(s => s+1);
|
setInputIdx(s => s+1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemove = (idx: number) => {
|
const onRemove = (idx: number) => {
|
||||||
|
setFieldTouched(name, true, false);
|
||||||
setInputIdx(s => s - 1);
|
setInputIdx(s => s - 1);
|
||||||
arrayHelpers.remove(idx);
|
arrayHelpers.remove(idx);
|
||||||
};
|
};
|
||||||
@ -145,6 +148,7 @@ export function GroupSearch<I extends string, V extends FormValues<I>>(props: Gr
|
|||||||
}
|
}
|
||||||
getKey={(a: Association) => a.group}
|
getKey={(a: Association) => a.group}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
onBlur={() => {}}
|
||||||
/>
|
/>
|
||||||
{value?.length > 0 && (
|
{value?.length > 0 && (
|
||||||
value.map((e, idx: number) => {
|
value.map((e, idx: number) => {
|
||||||
|
@ -1,13 +1,35 @@
|
|||||||
import React from "react";
|
import React, { useCallback, UIEvent, MouseEvent, useRef } from "react";
|
||||||
import { Box } from "@tlon/indigo-react";
|
import { Box } from "@tlon/indigo-react";
|
||||||
import { PropFunc } from "~/types/util";
|
import { PropFunc } from "~/types/util";
|
||||||
|
|
||||||
interface ModalOverlayProps {
|
interface ModalOverlayProps {
|
||||||
spacing: PropFunc<typeof Box>["m"];
|
spacing: PropFunc<typeof Box>["m"];
|
||||||
|
dismiss: () => void;
|
||||||
}
|
}
|
||||||
export const ModalOverlay = React.forwardRef(
|
type Props = ModalOverlayProps & PropFunc<typeof Box>;
|
||||||
(props: ModalOverlayProps & PropFunc<typeof Box>, ref) => {
|
export const ModalOverlay = (props: Props) => {
|
||||||
const { spacing, ...rest } = props;
|
const { spacing, ...rest } = props;
|
||||||
|
const ref = useRef<HTMLElement | null>(null);
|
||||||
|
const onClick = useCallback(
|
||||||
|
(e: any) => {
|
||||||
|
if (!(ref as any).current.contains(e.target)) {
|
||||||
|
props.dismiss();
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
[props.dismiss, ref]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback(
|
||||||
|
(e: any) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
props.dismiss();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[props.dismiss, ref]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
backgroundColor="scales.black20"
|
backgroundColor="scales.black20"
|
||||||
@ -15,15 +37,16 @@ export const ModalOverlay = React.forwardRef(
|
|||||||
top="0px"
|
top="0px"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
zIndex={10}
|
|
||||||
position="fixed"
|
position="fixed"
|
||||||
display="flex"
|
display="flex"
|
||||||
|
zIndex={10}
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
p={spacing}
|
p={spacing}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
<Box ref={ref} {...rest} />
|
<Box ref={ref} {...rest} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
@ -47,7 +47,9 @@ const StatusBar = (props) => {
|
|||||||
|
|
||||||
const anchorRef = useRef(null);
|
const anchorRef = useRef(null);
|
||||||
|
|
||||||
useTutorialModal('leap', true, anchorRef.current);
|
const leapHighlight = useTutorialModal('leap', true, anchorRef.current);
|
||||||
|
|
||||||
|
const floatLeap = leapHighlight && window.matchMedia('(max-width: 550px)').matches;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -63,7 +65,7 @@ const StatusBar = (props) => {
|
|||||||
<Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
|
<Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
|
||||||
<Icon icon='Spaces' color='black'/>
|
<Icon icon='Spaces' color='black'/>
|
||||||
</Button>
|
</Button>
|
||||||
<StatusBarItem mr={2} onClick={() => toggleOmnibox()}>
|
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||||
{ !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) &&
|
{ !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) &&
|
||||||
(<Box display="block" right="-8px" top="-8px" position="absolute" >
|
(<Box display="block" right="-8px" top="-8px" position="absolute" >
|
||||||
<Icon color="blue" icon="Bullet" />
|
<Icon color="blue" icon="Bullet" />
|
||||||
|
@ -6,21 +6,25 @@ const Row = styled(_Row)`
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type StatusBarItemProps = Parameters<typeof Row>[0] & { badge?: boolean };
|
type StatusBarItemProps = Parameters<typeof Row>[0] & { badge?: boolean; float?: boolean; };
|
||||||
|
|
||||||
export function StatusBarItem({
|
export function StatusBarItem({
|
||||||
badge,
|
badge,
|
||||||
children,
|
children,
|
||||||
|
float,
|
||||||
...props
|
...props
|
||||||
}: StatusBarItemProps) {
|
}: StatusBarItemProps) {
|
||||||
|
const floatPos = float ? { zIndex: 10, boxShadow: 'rgba(0,0,0,0.2) 0px 0px 0px 999px' } : {};
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
style={{ position: 'relative' }}
|
style={{ position: 'relative', ...floatPos }}
|
||||||
border={1}
|
border={1}
|
||||||
color="washedGray"
|
color="washedGray"
|
||||||
bg="white"
|
bg="white"
|
||||||
px={2}
|
px={2}
|
||||||
overflow='visible'
|
overflow='visible'
|
||||||
|
zIndex={10}
|
||||||
|
boxShadow="1px 1px black"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
46
pkg/interface/src/views/components/Triangle.tsx
Normal file
46
pkg/interface/src/views/components/Triangle.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Box } from "@tlon/indigo-react";
|
||||||
|
import { PropFunc } from "~/types";
|
||||||
|
|
||||||
|
export type Direction = "East" | "South" | "West" | "North";
|
||||||
|
type TriangleProps = PropFunc<typeof Box> & {
|
||||||
|
direction: Direction;
|
||||||
|
color: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const borders = ["Top", "Bottom", "Left", "Right"] as const;
|
||||||
|
|
||||||
|
const directionToBorder = (dir: Direction): typeof borders[number] => {
|
||||||
|
switch (dir) {
|
||||||
|
case "East":
|
||||||
|
return "Left";
|
||||||
|
case "West":
|
||||||
|
return "Right";
|
||||||
|
case "North":
|
||||||
|
return "Bottom";
|
||||||
|
case "South":
|
||||||
|
return "Top";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBorders = (dir: Direction, height: number, color: string) => {
|
||||||
|
const solidBorder = directionToBorder(dir);
|
||||||
|
const transparent = borders.filter((x) => x !== solidBorder);
|
||||||
|
|
||||||
|
return {
|
||||||
|
[`border${solidBorder}`]: `${height}px solid`,
|
||||||
|
[`border${solidBorder}Color`]: color,
|
||||||
|
..._.mapValues(
|
||||||
|
_.keyBy(transparent, (border) => `border${border}`),
|
||||||
|
() => "16px solid transparent"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Triangle({ direction, color, size, ...rest }: TriangleProps) {
|
||||||
|
const borders = getBorders(direction, size, color);
|
||||||
|
|
||||||
|
return <Box width="0px" height="0px" {...borders} {...rest} />;
|
||||||
|
}
|
@ -3,7 +3,6 @@ import { Col, Text, BaseLabel, Label } from "@tlon/indigo-react";
|
|||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { Association, NotificationGraphConfig } from "~/types";
|
import { Association, NotificationGraphConfig } from "~/types";
|
||||||
import { StatelessAsyncToggle } from "~/views/components/StatelessAsyncToggle";
|
import { StatelessAsyncToggle } from "~/views/components/StatelessAsyncToggle";
|
||||||
import {useTutorialModal} from "~/views/components/useTutorialModal";
|
|
||||||
|
|
||||||
interface ChannelNotificationsProps {
|
interface ChannelNotificationsProps {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
@ -27,7 +26,6 @@ export function ChannelNotifications(props: ChannelNotificationsProps) {
|
|||||||
|
|
||||||
const anchorRef = useRef<HTMLElement | null>(null)
|
const anchorRef = useRef<HTMLElement | null>(null)
|
||||||
|
|
||||||
useTutorialModal('notifications', true, anchorRef.current);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col mb="6" gapY="4" flexShrink={0}>
|
<Col mb="6" gapY="4" flexShrink={0}>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef, useCallback } from "react";
|
||||||
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
||||||
import { Col, Box, Text, Row } from "@tlon/indigo-react";
|
import { Col, Box, Text, Row } from "@tlon/indigo-react";
|
||||||
import { ChannelPopoverRoutesSidebar } from "./Sidebar";
|
import { ChannelPopoverRoutesSidebar } from "./Sidebar";
|
||||||
@ -36,9 +36,9 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
|||||||
const overlayRef = useRef<HTMLElement>();
|
const overlayRef = useRef<HTMLElement>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useOutsideClick(overlayRef, () => {
|
const onDismiss = useCallback(() => {
|
||||||
history.push(props.baseUrl);
|
history.push(props.baseUrl);
|
||||||
});
|
}, [history, props.baseUrl]);
|
||||||
|
|
||||||
const handleUnsubscribe = async () => {
|
const handleUnsubscribe = async () => {
|
||||||
const [,,ship,name] = association.resource.split('/');
|
const [,,ship,name] = association.resource.split('/');
|
||||||
@ -62,6 +62,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
spacing={[3, 5, 7]}
|
spacing={[3, 5, 7]}
|
||||||
ref={overlayRef}
|
ref={overlayRef}
|
||||||
|
dismiss={onDismiss}
|
||||||
>
|
>
|
||||||
<Row
|
<Row
|
||||||
flexDirection={["column", "row"]}
|
flexDirection={["column", "row"]}
|
||||||
|
@ -89,7 +89,6 @@ export function GroupSwitcher(props: {
|
|||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
height='48px'
|
height='48px'
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
zIndex="2"
|
|
||||||
position="sticky"
|
position="sticky"
|
||||||
top="0px"
|
top="0px"
|
||||||
pl='3'
|
pl='3'
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useRef, useCallback } from "react";
|
import React, { useRef, useCallback } from "react";
|
||||||
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
|
import { Route, Switch, RouteComponentProps, Link } from "react-router-dom";
|
||||||
import { Box, Row, Col, Icon, Text } from "@tlon/indigo-react";
|
import { Box, Row, Col, Icon, Text } from "@tlon/indigo-react";
|
||||||
import { useOutsideClick } from "~/logic/lib/useOutsideClick";
|
|
||||||
import { HoverBoxLink } from "~/views/components/HoverBox";
|
import { HoverBoxLink } from "~/views/components/HoverBox";
|
||||||
import { Contacts, Contact } from "~/types/contact-update";
|
import { Contacts, Contact } from "~/types/contact-update";
|
||||||
import { Group } from "~/types/group-update";
|
import { Group } from "~/types/group-update";
|
||||||
@ -33,10 +32,9 @@ export function PopoverRoutes(
|
|||||||
const relativeUrl = (url: string) => `${props.baseUrl}/popover${url}`;
|
const relativeUrl = (url: string) => `${props.baseUrl}/popover${url}`;
|
||||||
const innerRef = useRef(null);
|
const innerRef = useRef(null);
|
||||||
|
|
||||||
const onOutsideClick = useCallback(() => {
|
const onDismiss = useCallback(() => {
|
||||||
props.history.push(props.baseUrl);
|
props.history.push(props.baseUrl);
|
||||||
}, [props.history.push, props.baseUrl]);
|
}, [props.history.push, props.baseUrl]);
|
||||||
useOutsideClick(innerRef, onOutsideClick);
|
|
||||||
|
|
||||||
useHashLink();
|
useHashLink();
|
||||||
|
|
||||||
@ -62,6 +60,7 @@ export function PopoverRoutes(
|
|||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
bg="white"
|
bg="white"
|
||||||
|
dismiss={onDismiss}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
display="grid"
|
display="grid"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import _ from 'lodash';
|
||||||
import { Box, Col, Row, Button, Text, Icon, Action } from "@tlon/indigo-react";
|
import { Box, Col, Row, Button, Text, Icon, Action } from "@tlon/indigo-react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { TutorialProgress, tutorialProgress as progress } from "~/types";
|
import { TutorialProgress, tutorialProgress as progress } from "~/types";
|
||||||
@ -13,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",
|
||||||
@ -24,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 }) {
|
||||||
@ -33,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,
|
||||||
@ -44,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(() => {
|
||||||
@ -75,25 +81,36 @@ export function TutorialModal(props: { api: GlobalApi }) {
|
|||||||
if(key === 'bottom' || key === 'left') {
|
if(key === 'bottom' || key === 'left') {
|
||||||
return ['0px', ...value];
|
return ['0px', ...value];
|
||||||
}
|
}
|
||||||
return [null, ...value];
|
return ['unset', ...value];
|
||||||
});
|
});
|
||||||
if(!('bottom' in withMobile)) {
|
if(!('bottom' in withMobile)) {
|
||||||
withMobile.bottom = ['0px', null];
|
withMobile.bottom = ['0px', 'unset'];
|
||||||
}
|
}
|
||||||
if(!('left' in withMobile)) {
|
if(!('left' in withMobile)) {
|
||||||
withMobile.left = ['0px', null];
|
withMobile.left = ['0px', 'unset'];
|
||||||
}
|
}
|
||||||
|
|
||||||
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]);
|
||||||
@ -108,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
|
||||||
@ -148,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>
|
||||||
|
Loading…
Reference in New Issue
Block a user