Merge pull request #4461 from urbit/lf/tutorial-revive

tutorial: reenable
This commit is contained in:
matildepark 2021-03-04 12:24:09 -05:00 committed by GitHub
commit 1dcdfed9c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 161 additions and 80 deletions

View File

@ -1,5 +1,5 @@
/- *settings
/+ verb, dbug, default-agent
/+ verb, dbug, default-agent, agentio
|%
+$ card card:agent:gall
+$ versioned-state
@ -20,10 +20,14 @@
+* this .
do ~(. +> bol)
def ~(. (default-agent this %|) bol)
io ~(. agentio bol)
::
++ on-init
^- (quip card _this)
`this
=^ cards state
(put-entry:do %tutorial %seen b+|)
[cards this]
::
++ on-save !>(state)
::

View File

@ -127,11 +127,11 @@ module.exports = {
plugins: [
new UrbitShipPlugin(urbitrc),
new webpack.DefinePlugin({
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
'process.env.TUTORIAL_HOST': JSON.stringify('~difmex-passed'),
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
'process.env.TUTORIAL_CHAT': JSON.stringify('chat-1704'),
'process.env.TUTORIAL_BOOK': JSON.stringify('book-9695'),
'process.env.TUTORIAL_LINKS': JSON.stringify('link-2827'),
'process.env.TUTORIAL_CHAT': JSON.stringify('introduce-yourself-7010'),
'process.env.TUTORIAL_BOOK': JSON.stringify('guides-9684'),
'process.env.TUTORIAL_LINKS': JSON.stringify('community-articles-2143'),
})
// new CleanWebpackPlugin(),

View File

@ -61,12 +61,12 @@ module.exports = {
new webpack.DefinePlugin({
'process.env.LANDSCAPE_STREAM': JSON.stringify(process.env.LANDSCAPE_STREAM),
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(GIT_DESC),
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
'process.env.TUTORIAL_HOST': JSON.stringify('~difmex-passed'),
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
'process.env.TUTORIAL_CHAT': JSON.stringify('chat-8401'),
'process.env.TUTORIAL_BOOK': JSON.stringify('notebook-9148'),
'process.env.TUTORIAL_LINKS': JSON.stringify('links-4353'),
})
'process.env.TUTORIAL_CHAT': JSON.stringify('introduce-yourself-7010'),
'process.env.TUTORIAL_BOOK': JSON.stringify('guides-9684'),
'process.env.TUTORIAL_LINKS': JSON.stringify('community-articles-2143'),
}),
// new HtmlWebpackPlugin({
// title: 'Hot Module Replacement',
// template: './public/index.html',

Binary file not shown.

View File

@ -10,7 +10,7 @@
"@reach/tabs": "^0.10.5",
"@tlon/indigo-dark": "^1.0.6",
"@tlon/indigo-light": "^1.0.6",
"@tlon/indigo-react": "1.2.17",
"@tlon/indigo-react": "^1.2.19",
"@tlon/sigil-js": "^1.4.3",
"@urbit/api": "file:../npm/api",
"aws-sdk": "^2.830.0",

View File

@ -79,7 +79,7 @@ const otherIndex = function(config) {
messages: result('Messages', '/~landscape/messages', 'messages', null),
logout: result('Log Out', '/~/logout', 'logout', null)
};
other.push(result('Tutorial', '/?tutorial=true', 'tutorial', null));
for(let cat of config.categories) {
if(idx[cat]) {
other.push(idx[cat]);

View File

@ -12,6 +12,7 @@ export const TUTORIAL_GROUP = process.env.TUTORIAL_GROUP!;
export const TUTORIAL_CHAT = process.env.TUTORIAL_CHAT!;
export const TUTORIAL_BOOK = process.env.TUTORIAL_BOOK!;
export const TUTORIAL_LINKS = process.env.TUTORIAL_LINKS!;
export const TUTORIAL_GROUP_RESOURCE = `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` ;
interface StepDetail {
title: string;
@ -26,7 +27,7 @@ interface StepDetail {
export function hasTutorialGroup(props: { associations: Associations }) {
return (
`/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` in props.associations.groups
TUTORIAL_GROUP_RESOURCE in props.associations.groups
);
}
@ -90,7 +91,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
alignY: 'top',
arrow: 'East',
offsetX: MODAL_WIDTH + 24,
offsetY: MODAL_HEIGHT / 2 - 8
offsetY: 80,
},
channels: {
title: 'Channels',
@ -143,7 +144,7 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
alignY: 'top',
alignX: 'left',
arrow: 'North',
offsetX: (MODAL_WIDTH / 2) - 16,
offsetX: 0,
offsetY: -48
},
profile: {
@ -155,17 +156,17 @@ export const progressDetails: Record<TutorialProgress, StepDetail> = {
alignX: 'right',
arrow: 'South',
offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -120 + MODAL_HEIGHT / 2
offsetY: -60,
},
leap: {
title: 'Leap',
description:
'Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.',
url: `/~profile/~${window.ship}`,
alignY: 'top',
alignX: 'left',
arrow: 'North',
offsetX: 0.3 *MODAL_HEIGHT,
offsetY: -48
}
alignY: "top",
alignX: "left",
arrow: "North",
offsetX: 76,
offsetY: -48,
},
};

View File

@ -23,7 +23,11 @@ export interface SettingsState {
remoteContentPolicy: RemoteContentPolicy;
leap: {
categories: LeapCategories[];
}
};
tutorial: {
seen: boolean;
joined?: number;
};
set: (fn: (state: SettingsState) => void) => void
};
@ -59,6 +63,10 @@ const useSettingsState = create<SettingsStateZus>((set) => ({
leap: {
categories: leapCategories,
},
tutorial: {
seen: false,
joined: undefined
},
set: (fn: (state: SettingsState) => void) => set(produce(fn))
}));

View File

@ -13,6 +13,7 @@ import Tile from './components/tiles/tile';
import Groups from './components/Groups';
import ModalButton from './components/ModalButton';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { StarIcon } from '~/views/components/StarIcon';
import { writeText } from '~/logic/lib/util';
import { useModal } from "~/logic/lib/useModal";
import { NewGroup } from "~/views/landscape/components/NewGroup";
@ -45,6 +46,7 @@ const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
export default function LaunchApp(props) {
const history = useHistory();
const [hashText, setHashText] = useState(props.baseHash);
const [exitingTut, setExitingTut] = useState(false);
const hashBox = (
<Box
position={["relative", "absolute"]}
@ -103,10 +105,11 @@ export default function LaunchApp(props) {
e.stopPropagation();
if(!hasTutorialGroup(props)) {
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
await props.api.settings.putEntry('tutorial', 'joined', Date.now());
await waiter(hasTutorialGroup);
await Promise.all(
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph =>
props.api.graph.join(TUTORIAL_HOST, graph)));
props.api.graph.joinGraph(TUTORIAL_HOST, graph)));
await waiter(p => {
return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph &&
@ -117,26 +120,39 @@ export default function LaunchApp(props) {
nextTutStep();
dismiss();
}
return (
<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">
You have been invited to use Landscape, an interface to chat
and interact with communities
<br />
Would you like a tour of Landscape?
</Text>
<Row gapX="2" justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={onDismiss}>Skip</Button>
<StatelessAsyncButton primary onClick={onContinue}>
Yes
</StatelessAsyncButton>
</Row>
</Col>
)}
return exitingTut ? (
<Col maxWidth="350px" p="3">
<Icon icon="Info" fill="black"></Icon>
<Text my="3" lineHeight="tall">
You can always restart the tutorial by typing "tutorial" in Leap
</Text>
<Row gapX="2" justifyContent="flex-end">
<Button primary onClick={onDismiss}>Ok</Button>
</Row>
</Col>
) : (
<Col maxWidth="350px" p="3">
<Box position="absolute" left="-16px" top="-16px">
<StarIcon width="32px" height="32px" color="blue" display="block" />
</Box>
<Text mb="3" lineHeight="tall" fontWeight="medium">Welcome</Text>
<Text mb="3" lineHeight="tall">
You have been invited to use Landscape, an interface to chat
and interact with communities
<br />
Would you like a tour of Landscape?
</Text>
<Row gapX="2" justifyContent="flex-end">
<Button
backgroundColor="washedGray"
onClick={() => setExitingTut(true)}
>Skip</Button>
<StatelessAsyncButton primary onClick={onContinue}>
Yes
</StatelessAsyncButton>
</Row>
</Col>
)}
});
const hasLoaded = useMemo(() => Object.keys(props.contacts).length > 0, [props.contacts]);

View File

@ -2,21 +2,26 @@ import React, { useRef } from 'react';
import { Box, Text, Col } from '@tlon/indigo-react';
import f from 'lodash/fp';
import _ from 'lodash';
import moment from 'moment';
import { Associations, Association, Unreads, UnreadStats } from '@urbit/api';
import { alphabeticalOrder } from '~/logic/lib/util';
import { getUnreadCount, getNotificationCount } from '~/logic/lib/hark';
import Tile from '../components/tiles/tile';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { TUTORIAL_HOST, TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE } from '~/logic/lib/tutorialModal';
import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings';
interface GroupsProps {
associations: Associations;
}
const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title);
a.group === TUTORIAL_GROUP_RESOURCE
? -1
: b.group === TUTORIAL_GROUP_RESOURCE
? 1
: alphabeticalOrder(a.metadata.title, b.metadata.title);
const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) =>
f.flow(
@ -72,6 +77,7 @@ interface GroupProps {
unreads: number;
first: boolean;
}
const selectJoined = (s: SettingsState) => s.tutorial.joined;
function Group(props: GroupProps) {
const { path, title, unreads, updates, first = false } = props;
const anchorRef = useRef<HTMLElement>(null);
@ -79,14 +85,18 @@ function Group(props: GroupProps) {
useTutorialModal(
'start',
isTutorialGroup,
anchorRef.current
anchorRef
);
const { hideUnreads } = useSettingsState(selectCalmState)
const joined = useSettingsState(selectJoined);
return (
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
<Col height="100%" justifyContent="space-between">
<Text>{title}</Text>
{!hideUnreads && (<Col>
{isTutorialGroup && joined &&
(<Text>{Math.floor(moment.duration(moment(joined).add(14, 'days').diff(moment())).as('days'))} days remaining</Text>)
}
{updates > 0 &&
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
}

View File

@ -51,7 +51,7 @@ export default function NotificationsScreen(props: any): ReactElement {
.map(g => props.associations?.groups?.[g]?.metadata?.title)
.join(', ');
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('notifications', true, anchorRef.current);
useTutorialModal('notifications', true, anchorRef);
return (
<Switch>
<Route
@ -74,7 +74,8 @@ export default function NotificationsScreen(props: any): ReactElement {
borderBottom="1"
borderBottomColor="washedGray"
>
<Text>Updates</Text>
<Text ref={anchorRef}>Notifications</Text>
<Row
justifyContent="space-between"
>

View File

@ -47,7 +47,7 @@ export function Profile(props: any): ReactElement {
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef.current);
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef);
return (
<Center

View File

@ -0,0 +1,27 @@
import React from "react";
import css, { SystemStyleObject } from "@styled-system/css";
import styled from "styled-components";
import { BaseSVG } from "@tlon/indigo-react";
import { PropFunc } from "~/types";
type StarIconProps = PropFunc<typeof BaseSVG> & SvgProps;
interface SvgProps {
color?: string;
}
const Svg = styled(BaseSVG)(({ color }: SvgProps) =>
css({
"& > *": {
fill: typeof color === "undefined" ? "inherit" : color || "black",
},
flexShrink: 0,
} as SystemStyleObject)
);
export function StarIcon(props: StarIconProps) {
return (
<Svg {...props}>
<path d="M9.72024 1.7095c.043-.97631 1.31686-1.317634 1.84226-.49363l1.595 2.50165c.3931.61651 1.2933.61651 1.6864 0l1.595-2.50164c.5254-.824012 1.7993-.482689 1.8423.49362l.1305 2.96401c.0322.73046.8118 1.18056 1.4604.84319l2.6322-1.36896c.867-.45092 1.7995.48159 1.3486 1.3486L22.484 8.12852c-.3374.64867.1127 1.42827.8431 1.46044l2.9641.13054c.9763.04299 1.3176 1.3168.4936 1.8422l-2.5017 1.5951c-.6165.3931-.6165 1.2933 0 1.6863l2.5017 1.5951c.824.5254.4827 1.7992-.4936 1.8422l-2.9641.1306c-.7304.0321-1.1805.8117-.8431 1.4604l1.3689 2.6322c.4509.867-.4816 1.7995-1.3486 1.3486l-2.6322-1.369c-.6486-.3374-1.4282.1127-1.4604.8432l-.1305 2.964c-.043.9763-1.3169 1.3176-1.8423.4936l-1.595-2.5016c-.3931-.6165-1.2933-.6165-1.6864 0l-1.595 2.5016c-.5254.824-1.79926.4827-1.84226-.4936l-.13053-2.964c-.03217-.7305-.81177-1.1806-1.46045-.8432l-2.63218 1.369c-.867.4509-1.79951-.4816-1.3486-1.3486l1.36897-2.6322c.33736-.6487-.11274-1.4283-.84319-1.4604l-2.96402-.1306c-.976304-.043-1.317628-1.3168-.49362-1.8422l2.50165-1.5951c.6165-.393.6165-1.2932 0-1.6863l-2.50165-1.5951c-.824006-.5254-.482684-1.79921.49362-1.8422l2.96402-.13054c.73045-.03217 1.18056-.81177.84319-1.46044L4.14848 5.49634c-.45091-.86701.4816-1.79952 1.3486-1.3486L8.12926 5.5167c.64868.33737 1.42828-.11273 1.46045-.84319l.13053-2.96401z" />
</Svg>
);
}

View File

@ -48,7 +48,7 @@ const StatusBar = (props) => {
const anchorRef = useRef(null);
const leapHighlight = useTutorialModal('leap', true, anchorRef.current);
const leapHighlight = useTutorialModal('leap', true, anchorRef);
const floatLeap = leapHighlight && window.matchMedia('(max-width: 550px)').matches;

View File

@ -100,7 +100,7 @@ export function Omnibox(props: OmniboxProps) {
const initialResults = useMemo(() => {
return new Map(SEARCHED_CATEGORIES.map((category) => {
if (category === 'other') {
return ['other', index.get('other')];
return ['other', index.get('other').filter(({ app }) => app !== 'tutorial')];
}
return [category, []];
}));
@ -133,6 +133,7 @@ export function Omnibox(props: OmniboxProps) {
if (defaultApps.includes(app.toLowerCase())
|| app === 'profile'
|| app === 'messages'
|| app === 'tutorial'
|| app === 'Links'
|| app === 'Terminal'
|| app === 'home'

View File

@ -58,7 +58,10 @@ export class OmniboxResult extends Component {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />;
} else if (icon === 'messages') {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Users' mr='2' size='18px' color={iconFill} />;
} else {
} else if (icon === 'tutorial') {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Tutorial' mr='2' size='18px' color={iconFill} />;
}
else {
graphic = <Icon display='inline-block' icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />;
}

View File

@ -1,21 +1,25 @@
import { useEffect } from 'react';
import { TutorialProgress } from '@urbit/api';
import useLocalState, { selectLocalState } from '~/logic/state/local';
import { useEffect, MutableRefObject } from "react";
import { TutorialProgress } from "@urbit/api";
import useLocalState, { selectLocalState } from "~/logic/state/local";
const localSelector = selectLocalState(['tutorialProgress', 'setTutorialRef']);
export function useTutorialModal(
onProgress: TutorialProgress,
show: boolean,
anchorRef: HTMLElement | null
anchorRef: MutableRefObject<HTMLElement | null>
) {
const { tutorialProgress, setTutorialRef } = useLocalState(localSelector);
useEffect(() => {
if (show && onProgress === tutorialProgress && anchorRef) {
setTutorialRef(anchorRef);
if (show && (onProgress === tutorialProgress) && anchorRef?.current) {
setTutorialRef(anchorRef.current);
}
}, [onProgress, tutorialProgress, show, anchorRef]);
return () => {
console.log(tutorialProgress);
}
}, [tutorialProgress, show, anchorRef]);
return show && onProgress === tutorialProgress;
}

View File

@ -20,7 +20,7 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
useTutorialModal(
'group-desc',
resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
anchorRef.current
anchorRef
);
return (
<Col {...rest} ref={anchorRef} gapY="4">

View File

@ -22,6 +22,7 @@ import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { getModuleIcon } from '~/logic/lib/util';
import { FormError } from '~/views/components/FormError';
import { GroupSummary } from './GroupSummary';
import {TUTORIAL_GROUP_RESOURCE} from '~/logic/lib/tutorialModal';
const formSchema = Yup.object({
group: Yup.string()
@ -72,6 +73,9 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
const onConfirm = useCallback(async (group: string) => {
const [,,ship,name] = group.split('/');
if(group === TUTORIAL_GROUP_RESOURCE) {
await api.settings.putEntry('tutorial', 'joined', Date.now());
}
await api.groups.join(ship, name);
try {
await waiter((p: JoinGroupProps) => {

View File

@ -66,7 +66,7 @@ export function Sidebar(props: SidebarProps): ReactElement {
const isAdmin = (role === 'admin') || (workspace?.type === 'home');
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('channels', true, anchorRef.current);
useTutorialModal('channels', true, anchorRef);
return (
<ScrollbarLessCol

View File

@ -49,7 +49,7 @@ export function SidebarItem(props: {
useTutorialModal(
mod as any,
groupPath === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
anchorRef.current
anchorRef
);
const app = apps[appName];
const isUnmanaged = groups?.[groupPath]?.hidden || false;

View File

@ -112,7 +112,8 @@ export function TutorialModal(props: { api: GlobalApi }) {
const leaveGroup = useCallback(async () => {
await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP);
}, [props.api]);
await dismiss();
}, [props.api, dismiss]);
const progressIdx = progress.findIndex(p => p === tutorialProgress);
@ -137,19 +138,19 @@ export function TutorialModal(props: { api: GlobalApi }) {
return (
<Portal>
<ModalOverlay dismiss={dismiss} borderRadius="2" maxWidth="270px" backgroundColor="white">
<Col p="2" bg="lightBlue">
<Col mb="1">
<Col p="3" bg="lightBlue">
<Col mb="3">
<Text lineHeight="tall" fontWeight="bold">
Tutorial Finished
</Text>
<Text fontSize="0" gray>
{progressIdx} of {progress.length - 1}
{progressIdx} of {progress.length - 2}
</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">
<Row mt="3" gapX="2" justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={dismiss}>
Later
</Button>
@ -170,8 +171,8 @@ export function TutorialModal(props: { api: GlobalApi }) {
if(paused) {
return (
<ModalOverlay dismiss={bailExit} borderRadius="2" maxWidth="270px" backgroundColor="white">
<Col p="2">
<Col mb="1">
<Col p="3">
<Col mb="3">
<Text lineHeight="tall" fontWeight="bold">
End Tutorial Now?
</Text>
@ -179,7 +180,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
<Text lineHeight="tall">
You can always restart the tutorial by typing "tutorial" in Leap.
</Text>
<Row mt="4" gapX="2" justifyContent="flex-end">
<Row mt="3" gapX="2" justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={bailExit}>
Cancel
</Button>
@ -204,8 +205,9 @@ export function TutorialModal(props: { api: GlobalApi }) {
{...coords}
bg="white"
zIndex={50}
height={MODAL_HEIGHT_PX}
width={['100%', MODAL_WIDTH_PX]}
display="flex"
flexDirection="column"
width={["100%", MODAL_WIDTH_PX]}
borderRadius="2"
>
<Col
@ -214,7 +216,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
height="100%"
width="100%"
borderRadius="2"
p="2"
p="3"
bg="lightBlue"
>
@ -229,15 +231,15 @@ export function TutorialModal(props: { api: GlobalApi }) {
/>
<Box
right="8px"
top="8px"
right="16px"
top="16px"
position="absolute"
cursor="pointer"
onClick={tryExit}
>
<Icon icon="X" />
</Box>
<Col mb="1">
<Col mb="3">
<Text lineHeight="tall" fontWeight="bold">
{title}
</Text>
@ -247,7 +249,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
</Col>
<Text lineHeight="tall">{description}</Text>
<Row gapX="2" mt="2" justifyContent="flex-end">
<Row gapX="2" mt="3" justifyContent="flex-end">
{ progressIdx > 1 && (
<Button bg="washedGray" onClick={prev}>
Back