Merge branch 'release/next-userspace' into la/more-fix

This commit is contained in:
Matilde Park 2021-02-08 22:33:39 -05:00
commit 5530274b78
44 changed files with 801 additions and 208 deletions

View File

@ -1,5 +1,6 @@
/- *resource
/+ store=contact-store, contact, default-agent, verb, dbug, pull-hook
/+ store=contact-store, contact, default-agent, verb, dbug, pull-hook, agentio
/+ grpl=group
~% %contact-pull-hook-top ..part ~
|%
+$ card card:agent:gall
@ -23,11 +24,26 @@
def ~(. (default-agent this %|) bowl)
dep ~(. (default:pull-hook this config) bowl)
con ~(. contact bowl)
io ~(. agentio bowl)
grp ~(. grpl bowl)
::
++ on-init on-init:def
++ on-init
^- (quip card _this)
:_ this
(poke-self:pass:io noun+!>(%upgrade))^~
++ on-save !>(~)
++ on-load on-load:def
++ on-poke on-poke:def
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?. ?=(%noun mark) (on-poke:def mark vase)
:_ this
%+ murn ~(tap in scry-groups:grp)
|= rid=resource
?: =(our.bowl entity.rid) ~
`(poke-self:pass:io pull-hook-action+!>([%add [entity .]:rid]))
::
++ on-peek on-peek:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def

View File

@ -31,7 +31,10 @@
^- (quip card _this)
=/ old !<(versioned-state old-vase)
?- -.old
%0 [~ this(state old)]
%0
:_ this(state old)
=- [%pass / %agent [our dap]:bowl %poke -]~
settings-event+!>([%put-entry %tutorial %seen b+%|])
==
::
++ on-poke

View File

@ -23,6 +23,11 @@
^- card
(poke [our.bowl app] cage)
::
++ poke-self
|= =cage
^- card
(poke-our dap.bowl cage)
::
++ arvo
|= =note-arvo
^- card

View File

@ -1,4 +1,5 @@
const path = require('path');
const webpack = require('webpack');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const urbitrc = require('./urbitrc');
@ -117,7 +118,15 @@ module.exports = {
devtool: 'inline-source-map',
devServer: devServer,
plugins: [
new UrbitShipPlugin(urbitrc)
new UrbitShipPlugin(urbitrc),
new webpack.DefinePlugin({
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
'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('link-4353'),
})
// new CleanWebpackPlugin(),
// new HtmlWebpackPlugin({
// title: 'Hot Module Replacement',

View File

@ -56,8 +56,13 @@ module.exports = {
new CleanWebpackPlugin(),
new webpack.DefinePlugin({
'process.env.LANDSCAPE_STREAM': JSON.stringify(process.env.LANDSCAPE_STREAM),
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH)
})
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH),
'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'),
'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'),
}),
// new HtmlWebpackPlugin({
// title: 'Hot Module Replacement',
// template: './public/index.html',

View File

@ -74,6 +74,7 @@ export default class MetadataApi extends BaseApi<StoreState> {
tempChannel.subscribe(window.ship, "metadata-pull-hook", `/preview${group}`,
(err) => {
console.error(err);
reject(err);
tempChannel.delete();
},

View File

@ -0,0 +1,55 @@
import _ from "lodash";
export const alignY = ["top", "bottom"] as const;
export type AlignY = typeof alignY[number];
export const alignX = ["left", "right"] as const;
export type AlignX = typeof alignX[number];
export function getRelativePosition(
relativeTo: HTMLElement | null,
alignX: AlignX | AlignX[],
alignY: AlignY | AlignY[],
offsetX: number = 0,
offsetY: number = 0
) {
const rect = relativeTo?.getBoundingClientRect();
if (!rect) {
return {};
}
const bounds = {
top: rect.top - offsetY,
left: rect.left - offsetX,
bottom: document.documentElement.clientHeight - rect.bottom - offsetY,
right: document.documentElement.clientWidth - rect.right - offsetX,
};
const alignXArr = _.isArray(alignX) ? alignX : [alignX];
const alignYArr = _.isArray(alignY) ? alignY : [alignY];
return {
..._.reduce(
alignXArr,
(acc, a, idx) => ({
...acc,
[a]: _.zipWith(
[...Array(idx), `${bounds[a]}px`],
acc[a] || [],
(a, b) => a || b || null
),
}),
{}
),
..._.reduce(
alignYArr,
(acc, a, idx) => ({
...acc,
[a]: _.zipWith(
[...Array(idx), `${bounds[a]}px`],
acc[a] || [],
(a, b) => a || b || null
),
}),
{}
),
} as Record<AlignY | AlignX, string[]>;
}

View File

@ -23,10 +23,10 @@ export const Sigil = memo(
size,
svgClass = '',
icon = false,
padded = false
padding = 0
}) => {
const padding = icon && padded ? '2px' : '0px';
const innerSize = icon && padded ? Number(size) - 4 : size;
const innerSize = Number(size) - 2*padding;
const paddingPx = `${padding}px`;
const foregroundColor = foreground
? foreground
: foregroundFromBackground(color);
@ -45,7 +45,7 @@ export const Sigil = memo(
borderRadius={icon ? '1' : '0'}
flexBasis={size}
backgroundColor={color}
padding={padding}
padding={paddingPx}
className={classes}
>
{sigil({

View File

@ -0,0 +1,133 @@
import { TutorialProgress, Associations } from "~/types";
import { AlignX, AlignY } from "~/logic/lib/relativePosition";
export const MODAL_WIDTH = 256;
export const MODAL_HEIGHT = 180;
export const MODAL_WIDTH_PX = `${MODAL_WIDTH}px`;
export const MODAL_HEIGHT_PX = `${MODAL_HEIGHT}px`;
export const TUTORIAL_HOST = process.env.TUTORIAL_HOST!;
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!;
interface StepDetail {
title: string;
description: string;
url: string;
alignX: AlignX | AlignX[];
alignY: AlignY | AlignY[];
offsetX: number;
offsetY: number;
}
export function hasTutorialGroup(props: { associations: Associations }) {
return (
`/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` in props.associations.groups
);
}
export const progressDetails: Record<TutorialProgress, StepDetail> = {
hidden: {} as any,
done: {
title: "End",
description:
"This tutorial is finished. Would you like to leave Beginner Island?",
url: "/",
alignX: "right",
alignY: "top",
offsetX: MODAL_WIDTH + 8,
offsetY: 0,
},
start: {
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,
},
"group-desc": {
title: "What's a group",
description:
"A group contains members and tends to be centered around a topic or multiple topics.",
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
alignX: "left",
alignY: "top",
offsetX: MODAL_WIDTH + 8,
offsetY: MODAL_HEIGHT / 2 - 8,
},
channels: {
title: "Channels",
description:
"Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!",
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
alignY: "top",
alignX: "right",
offsetX: MODAL_WIDTH + 8,
offsetY: -8,
},
chat: {
title: "Chat",
description:
"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",
alignX: "right",
offsetX: 0,
offsetY: -32,
},
link: {
title: "Collection",
description:
"A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have its own comment thread.",
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`,
alignY: "top",
alignX: "right",
offsetX: 0,
offsetY: -32,
},
publish: {
title: "Notebook",
description:
"Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.",
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`,
alignY: "top",
alignX: "right",
offsetX: 0,
offsetY: -32,
},
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`,
alignY: "top",
alignX: "right",
offsetX: 0,
offsetY: -32,
},
profile: {
title: "Profile",
description:
"Your profile is customizable and can be shared with other ships. Enter as much or as little information as youd like.",
url: `/~profile/~${window.ship}`,
alignY: "top",
alignX: "right",
offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -120 + MODAL_HEIGHT / 2,
},
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",
offsetX: 0,
offsetY: -32,
},
};

View File

@ -1,5 +1,6 @@
import { useEffect, RefObject, useRef, useState } from "react";
import _ from "lodash";
import usePreviousValue from "./usePreviousValue";
export function distanceToBottom(el: HTMLElement) {
const { scrollTop, scrollHeight, clientHeight } = el;
@ -11,28 +12,41 @@ export function distanceToBottom(el: HTMLElement) {
export function useLazyScroll(
ref: RefObject<HTMLElement>,
margin: number,
count: number,
loadMore: () => Promise<boolean>
) {
const [isDone, setIsDone] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const oldCount = usePreviousValue(count);
const loadUntil = (el: HTMLElement) => {
if (!isDone && distanceToBottom(el) < margin) {
setIsLoading(true);
return loadMore().then((done) => {
setIsLoading(false);
if (done) {
setIsDone(true);
return Promise.resolve();
}
return loadUntil(el);
});
}
setIsLoading(false);
return Promise.resolve();
};
useEffect(() => {
if((oldCount > count) && ref.current) {
loadUntil(ref.current);
}
}, [count]);
useEffect(() => {
if (!ref.current) {
return;
}
setIsDone(false);
const scroll = ref.current;
const loadUntil = (el: HTMLElement) => {
if (!isDone && distanceToBottom(el) < margin) {
return loadMore().then((done) => {
if (done) {
setIsDone(true);
return Promise.resolve();
}
return loadUntil(el);
});
}
return Promise.resolve();
};
loadUntil(scroll);
const onScroll = (e: Event) => {
@ -40,12 +54,13 @@ export function useLazyScroll(
loadUntil(el);
};
ref.current.addEventListener("scroll", onScroll);
ref.current.addEventListener("scroll", onScroll, { passive: true });
return () => {
ref.current?.removeEventListener("scroll", onScroll);
};
}, [ref?.current]);
}, [ref?.current, count]);
return isDone;
return { isDone, isLoading };
}

View File

@ -60,6 +60,8 @@ export function useModal(props: UseModalProps): UseModalResult {
display="flex"
alignItems="stretch"
flexDirection="column"
spacing="2"
>
{inner}
</ModalOverlay>

View File

@ -0,0 +1,20 @@
import { useRef } from "react";
import { Primitive } from "~/types";
export default function usePreviousValue<T extends Primitive>(value: T): T {
const prev = useRef<T | null>(null);
const curr = useRef<T | null>(null);
if (prev?.current !== curr?.current) {
prev.current = curr?.current;
}
if (curr.current !== value) {
curr.current = value;
}
return prev.current!;
}

View File

@ -350,7 +350,12 @@ function archive(json: any, state: HarkState) {
const [archived, unarchived] = _.partition(timebox, (idxNotif) =>
notifIdxEqual(index, idxNotif.index)
);
state.notifications.set(time, unarchived);
if(unarchived.length === 0) {
console.log('deleting entire timebox');
state.notifications.delete(time);
} else {
state.notifications.set(time, unarchived);
}
const newlyRead = archived.filter(x => !x.notification.read).length;
updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead);
}

View File

@ -1,13 +1,21 @@
import React, { ReactNode } from "react";
import f from 'lodash/fp';
import create, { State } from 'zustand';
import { persist } from 'zustand/middleware';
import produce from 'immer';
import { BackgroundConfig, RemoteContentPolicy } from "~/types/local-update";
import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from "~/types/local-update";
export interface LocalState extends State {
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: RemoteContentPolicy;
tutorialProgress: TutorialProgress;
tutorialRef: HTMLElement | null,
hideTutorial: () => void;
nextTutStep: () => void;
prevTutStep: () => void;
setTutorialRef: (el: HTMLElement | null) => void;
dark: boolean;
background: BackgroundConfig;
omniboxShown: boolean;
@ -15,12 +23,35 @@ export interface LocalState extends State {
toggleOmnibox: () => void;
set: (fn: (state: LocalState) => void) => void
};
export const selectLocalState =
<K extends keyof LocalState>(keys: K[]) => f.pick<LocalState, K>(keys);
const useLocalState = create<LocalState>(persist((set, get) => ({
dark: false,
background: undefined,
hideAvatars: false,
hideNicknames: false,
tutorialProgress: 'hidden',
tutorialRef: null,
setTutorialRef: (el: HTMLElement | null) => set(produce(state => {
state.tutorialRef = el;
})),
hideTutorial: () => set(produce(state => {
state.tutorialProgress = 'hidden';
state.tutorialRef = null;
})),
nextTutStep: () => set(produce(state => {
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
if(currIdx < tutorialProgress.length) {
state.tutorialProgress = tutorialProgress[currIdx + 1];
}
})),
prevTutStep: () => set(produce(state => {
const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress)
if(currIdx > 0) {
state.tutorialProgress = tutorialProgress[currIdx - 1];
}
})),
remoteContentPolicy: {
imageShown: true,
audioShown: true,
@ -41,7 +72,10 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
})),
set: fn => set(produce(fn))
}), {
blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
blacklist: [
'suspendedFocus', 'toggleOmnibox', 'omniboxShown', 'tutorialProgress',
'prevTutStep', 'nextTutStep', 'tutorialRef', 'setTutorialRef'
],
name: 'localReducer'
}));

View File

@ -13,3 +13,4 @@ export * from './metadata-update';
export * from './noun';
export * from './s3-update';
export * from './workspace';
export * from './util';

View File

@ -1,3 +1,6 @@
export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'notifications', 'profile', 'leap', 'done'] as const;
export type TutorialProgress = typeof tutorialProgress[number];
interface LocalUpdateSetDark {
setDark: boolean;
}
@ -48,4 +51,4 @@ export type LocalUpdate =
| LocalUpdateHideAvatars
| LocalUpdateHideNicknames
| LocalUpdateSetOmniboxShown
| RemoteContentPolicy;
| RemoteContentPolicy;

View File

@ -1,4 +1,5 @@
import { Icon } from "@tlon/indigo-react";
export type PropFunc<T extends (...args: any[]) => any> = Parameters<T>[0];
export type Primitive = string | number | undefined | symbol | null | boolean;
export type IconRef = PropFunc<typeof Icon>['icon'];

View File

@ -20,6 +20,7 @@ import { Content } from './landscape/components/Content';
import StatusBar from './components/StatusBar';
import Omnibox from './components/leap/Omnibox';
import ErrorBoundary from '~/views/components/ErrorBoundary';
import { TutorialModal } from '~/views/landscape/components/TutorialModal';
import GlobalStore from '~/logic/store/store';
import GlobalSubscription from '~/logic/subscription/global';
@ -149,6 +150,7 @@ class App extends React.Component {
</Helmet>
<Root background={background}>
<Router>
<TutorialModal api={this.api} />
<ErrorBoundary>
<StatusBarWithRouter
props={this.props}

View File

@ -137,7 +137,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
color={`#${color}`}
classes={sigilClass}
icon
padded
padding={2}
/>;
return (

View File

@ -279,7 +279,7 @@ export const MessageWithSigil = (props) => {
color={color}
classes={sigilClass}
icon
padded
padding={2}
/>
);

View File

@ -1,19 +1,33 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import f from 'lodash/fp';
import _ from 'lodash';
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
import { Col, Button, Box, Row, Icon, Text } from '@tlon/indigo-react';
import './css/custom.css';
import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import Welcome from './components/welcome';
import Groups from './components/Groups';
import ModalButton from './components/ModalButton';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { writeText } from '~/logic/lib/util';
import { useModal } from "~/logic/lib/useModal";
import { NewGroup } from "~/views/landscape/components/NewGroup";
import { JoinGroup } from "~/views/landscape/components/JoinGroup";
import { Helmet } from 'react-helmet';
import useLocalState from "~/logic/state/local";
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import {
hasTutorialGroup,
TUTORIAL_GROUP,
TUTORIAL_HOST,
TUTORIAL_BOOK,
TUTORIAL_CHAT,
TUTORIAL_LINKS
} from '~/logic/lib/tutorialModal';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -23,7 +37,10 @@ const ScrollbarLessBox = styled(Box)`
}
`;
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep']);
export default function LaunchApp(props) {
const history = useHistory();
const [hashText, setHashText] = useState(props.baseHash);
const hashBox = (
<Box
@ -51,13 +68,68 @@ export default function LaunchApp(props) {
<Text color="gray">{hashText || props.baseHash}</Text>
</Box>
);
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
const waiter = useWaitForProps(props);
const { modal, showModal } = useModal({
modal: (dismiss) => {
const onDismiss = (e) => {
e.stopPropagation();
props.api.settings.putEntry('tutorial', 'seen', true);
dismiss();
};
const onContinue = async (e) => {
e.stopPropagation();
if(!hasTutorialGroup(props)) {
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
await waiter(hasTutorialGroup);
await Promise.all(
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph =>
api.graph.join(TUTORIAL_HOST, graph)));
await waiter(p => {
return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph &&
`/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}` in p.associations.graph &&
`/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}` in p.associations.graph;
});
}
nextTutStep();
dismiss();
}
return (
<Col gapY="2" p="3">
<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 onClick={onDismiss}>Skip</Button>
<StatelessAsyncButton primary onClick={onContinue}>
Yes
</StatelessAsyncButton>
</Row>
</Col>
)}
});
useEffect(() => {
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);
if(!seenTutorial && tutorialProgress === 'hidden') {
showModal();
}
}, [props.settings]);
return (
<>
<Helmet defer={false}>
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
</Helmet>
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
<Welcome firstTime={props.launch.firstTime} api={props.api} />
{modal}
<Box
mx='2'
display='grid'

View File

@ -1,4 +1,4 @@
import React from "react";
import React, {useRef} from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import f from "lodash/fp";
import _ from "lodash";
@ -7,6 +7,8 @@ import { Associations, Association, Unreads, UnreadStats } from "~/types";
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";
interface GroupsProps {
associations: Associations;
@ -73,8 +75,15 @@ interface GroupProps {
}
function Group(props: GroupProps) {
const { path, title, unreads, updates, first = false } = props;
const anchorRef = useRef<HTMLElement>(null);
const isTutorialGroup = path === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`;
useTutorialModal(
'start',
isTutorialGroup,
anchorRef.current
);
return (
<Tile to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
<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>
<Col>

View File

@ -22,43 +22,45 @@ const SquareBox = styled(Box)`
`;
const routeList = defaultApps.map(a => `/~${a}`);
export default class Tile extends React.Component {
render() {
const { bg, to, href, p, boxShadow, gridColumnStart, ...props } = this.props;
const Tile = React.forwardRef((props, ref) => {
const { bg, to, href, p, boxShadow, gridColumnStart, ...rest } = props;
let childElement = (
<Box p={typeof p === 'undefined' ? 2 : p} width="100%" height="100%">
{props.children}
</Box>
);
if (to) {
if (routeList.indexOf(to) !== -1 || to === '/~profile' || to.startsWith('/~landscape/')) {
childElement= (<Link to={to}>{childElement}</Link>);
} else {
childElement= (<a href={to}>{childElement}</a>);
}
let childElement = (
<Box p={typeof p === 'undefined' ? 2 : p} width="100%" height="100%">
{props.children}
</Box>
);
if (to) {
if (routeList.indexOf(to) !== -1 || to === '/~profile' || to.startsWith('/~landscape/')) {
childElement= (<Link to={to}>{childElement}</Link>);
} else {
childElement= (<a href={to}>{childElement}</a>);
}
return (
<SquareBox
borderRadius={2}
overflow="hidden"
bg={bg || "white"}
color={props?.color || 'washedGray'}
boxShadow={boxShadow || '0 0 0px 1px inset'}
style={{ gridColumnStart }}
>
<Box
{...props}
height="100%"
width="100%"
>
{childElement}
</Box>
</SquareBox>
);
}
}
return (
<SquareBox
ref={ref}
position="relative"
borderRadius={2}
overflow="hidden"
bg={bg || "white"}
color={props?.color || 'washedGray'}
boxShadow={boxShadow || '0 0 0px 1px inset'}
style={{ gridColumnStart }}
>
<Box
{...rest}
height="100%"
width="100%"
>
{childElement}
</Box>
</SquareBox>
);
});
export default Tile;

View File

@ -1,49 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Box, Text } from '@tlon/indigo-react';
export default class Welcome extends React.Component {
constructor() {
super();
this.state = {
show: true
};
this.disableWelcome = this.disableWelcome.bind(this);
}
disableWelcome() {
this.props.api.launch.changeFirstTime(false);
this.setState({ show: false });
}
render() {
const firstTime = this.props.firstTime;
return (firstTime && this.state.show) ? (
<Box
bg='white'
border={1}
margin={3}
padding={3}
display='flex'
flexDirection='column'
alignItems='flex-start'
>
<Text>Welcome. This virtual computer belongs to you completely. The Urbit ID you used to boot it is yours as well.</Text>
<Text pt={2}>Since your ID and OS belong to you, its up to you to keep them safe. Be sure your ID is somewhere you wont lose it and you keep your OS on a machine you trust.</Text>
<Text pt={2}>Urbit OS is designed to keep your data secure and hard to lose. But the system is still young so dont put anything critical in here just yet.</Text>
<Text pt={2}>To begin exploring, you should probably pop into a chat and verify there are signs of life in this new place. If you were invited by a friend, you probably already have access to a few groups.</Text>
<Text pt={2}>If you don't know where to go, feel free to <Link className="no-underline bb b--black b--gray1-d dib" to="/~landscape/join/~bitbet-bolbel/urbit-community">join the Urbit Community group</Link>.
</Text>
<Text pt={2}>Have fun!</Text>
<Text pt={2} className='pointer bb'
onClick={(() => {
this.disableWelcome();
})}
>
Close this note
</Text>
</Box>
) : null;
}
}

View File

@ -106,7 +106,7 @@ export function LinkResource(props: LinkResourceProps) {
return (
<Col alignItems="center" overflowY="auto" width="100%">
<Col width="100%" p={3} maxWidth="768px">
<Link to={resourceUrl}><Text bold>{"<- Back"}</Text></Link>
<Link to={resourceUrl}><Text px={3} bold>{"<- Back"}</Text></Link>
<LinkItem
contacts={contacts}
key={node.post.index}
@ -133,6 +133,7 @@ export function LinkResource(props: LinkResourceProps) {
history={props.history}
baseUrl={`${resourceUrl}/${props.match.params.index}`}
group={group}
px={3}
/>
</Col>
</Col>

View File

@ -185,7 +185,7 @@ const GraphNode = ({
icon
color={`#000000`}
classes="mix-blend-diff"
padded
padding={2}
/>
) : <Box style={{ width: '16px' }}></Box>;

View File

@ -1,7 +1,7 @@
import React, { useEffect, useCallback, useRef, useState } from "react";
import f from "lodash/fp";
import _ from "lodash";
import { Icon, Col, Center, Row, Box, Text, Anchor, Rule } from "@tlon/indigo-react";
import { Icon, Col, Center, Row, Box, Text, Anchor, Rule, LoadingSpinner } from "@tlon/indigo-react";
import moment from "moment";
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, joinProgress, JoinRequests, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
@ -38,6 +38,7 @@ function filterNotification(associations: Associations, groups: string[]) {
export default function Inbox(props: {
notifications: Notifications;
notificationsSize: number;
archive: Notifications;
groups: Groups;
showArchive?: boolean;
@ -103,7 +104,12 @@ export default function Inbox(props: {
return api.hark.getMore();
}, [api]);
const loadedAll = useLazyScroll(scrollRef, 0.2, loadMore);
const { isDone, isLoading } = useLazyScroll(
scrollRef,
0.2,
_.flatten(notifications).length,
loadMore
);
return (
@ -126,11 +132,17 @@ export default function Inbox(props: {
/>
);
})}
{loadedAll && (
{isDone && (
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="washedGray" width="100%" height="96px">
<Text gray fontSize="1">No more notifications</Text>
</Center>
)}
{isLoading && (
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="washedGray" width="100%" height="96px">
<LoadingSpinner />
</Center>
)}
</Col>
);
}

View File

@ -1,4 +1,4 @@
import React, {useEffect} from "react";
import React, {useEffect, useRef} from "react";
import { Sigil } from "~/logic/lib/sigil";
import { ViewProfile } from './ViewProfile';
import { EditProfile } from './EditProfile';
@ -15,6 +15,7 @@ import {
} from "@tlon/indigo-react";
import useLocalState from "~/logic/state/local";
import { useHistory } from "react-router-dom";
import {useTutorialModal} from "~/views/components/useTutorialModal";
export function Profile(props: any) {
@ -41,7 +42,11 @@ export function Profile(props: any) {
const image = (!hideAvatars && contact?.avatar)
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
: <Sigil ship={ship} size={96} color={hexColor} />;
: <Sigil padding={24} ship={ship} size={128} color={hexColor} />;
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef.current);
return (
<Center
@ -55,7 +60,7 @@ export function Profile(props: any) {
<SetStatus ship={ship} contact={contact} api={props.api} />
) : null
}
<Row width="100%" height="300px">
<Row ref={anchorRef} width="100%" height="300px">
{cover}
</Row>
<Row
@ -64,7 +69,7 @@ export function Profile(props: any) {
width="100%"
>
<Center width="100%" marginTop="-48px">
<Box height='96px' width='96px' borderRadius="2" overflow="hidden">
<Box height='128px' width='128px' borderRadius="2" overflow="hidden">
{image}
</Box>
</Center>

View File

@ -1,4 +1,5 @@
import React, {useEffect, useState} from "react";
import _ from 'lodash';
import { Sigil } from "~/logic/lib/sigil";
import {
@ -25,10 +26,21 @@ export function ViewProfile(props: any) {
useEffect(() => {
(async () => {
setPreviews(
await Promise.all((contact?.groups || []).map(g => api.metadata.preview(g)))
_.compact(
await Promise.all(
(contact?.groups || []).map(g => api.metadata.preview(g)
.catch(() => null)
)
)
)
);
})();
}, [contact?.groups])
return () => {
setPreviews([]);
}
}, [ship]);
return (
<>
<Row
@ -63,7 +75,7 @@ export function ViewProfile(props: any) {
{ (contact?.groups || []).length > 0 && (
<Col gapY="3" my="3" alignItems="center">
<Text fontWeight="medium">Pinned Groups</Text>
{previews.length > 0 ? (
{previews.length === 0 ? (
<LoadingSpinner />
) : (
<Row justifyContent="center" gapX="3">

View File

@ -48,7 +48,7 @@ export default function Author(props: AuthorProps) {
width={16}
/>
) : (
<Sigil ship={ship} size={16} color={color} icon padded />
<Sigil ship={ship} size={16} color={color} icon padding={2} />
);
return (

View File

@ -11,7 +11,8 @@ import { createPost, createBlankNodeWithChildPost } from '~/logic/api/graph';
import { getLatestCommentRevision } from '~/logic/lib/publish';
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import { getUnreadCount } from '~/logic/lib/hark';
import {isWriter} from '~/logic/lib/group';
import { PropFunc } from '~/types/util';
import { isWriter } from '~/logic/lib/group';
interface CommentsProps {
comments: GraphNode;
@ -25,8 +26,19 @@ interface CommentsProps {
group: Group;
}
export function Comments(props: CommentsProps) {
const { association, comments, ship, name, api, history, baseUrl, group } = props;
export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
const {
association,
comments,
ship,
name,
editCommentId,
api,
history,
baseUrl,
group,
...rest
} = props;
const onSubmit = async (
{ comment },
@ -53,7 +65,7 @@ export function Comments(props: CommentsProps) {
actions: FormikHelpers<{ comment: string }>
) => {
try {
const commentNode = comments.children.get(bigInt(props.editCommentId))!;
const commentNode = comments.children.get(bigInt(editCommentId))!;
const [idx, _] = getLatestCommentRevision(commentNode);
const content = tokenizeMessage(comment);
@ -71,8 +83,8 @@ export function Comments(props: CommentsProps) {
};
let commentContent = null;
if (props.editCommentId) {
const commentNode = comments.children.get(bigInt(props.editCommentId));
if (editCommentId) {
const commentNode = comments.children.get(bigInt(editCommentId));
const [_, post] = getLatestCommentRevision(commentNode);
commentContent = post.contents.reduce((val, curr) => {
if ('text' in curr) {
@ -91,22 +103,20 @@ export function Comments(props: CommentsProps) {
const children = Array.from(comments.children);
useEffect(() => {
return () => {
api.hark.markCountAsRead(association, parentIndex, 'comment')
api.hark.markCountAsRead(association, parentIndex, 'comment');
};
}, [comments.post.index])
}, [comments.post.index]);
const readCount = children.length - getUnreadCount(props?.unreads, association.resource, parentIndex);
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';
return (
<Col>
<Col {...rest}>
{( !props.editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
{( !!props.editCommentId ? (
{( props.editCommentId ? (
<CommentInput
onSubmit={onEdit}
label='Edit Comment'
@ -127,7 +137,7 @@ export function Comments(props: CommentsProps) {
unread={i >= readCount}
baseUrl={props.baseUrl}
group={group}
pending={idx.toString() === props.editCommentId}
pending={idx.toString() === editCommentId}
/>
);
})}

View File

@ -11,9 +11,7 @@ import { Box, Col } from "@tlon/indigo-react";
import { useOutsideClick } from "~/logic/lib/useOutsideClick";
import { useLocation } from "react-router-dom";
import { Portal } from "./Portal";
type AlignY = "top" | "bottom";
type AlignX = "left" | "right";
import { getRelativePosition, AlignY, AlignX } from "~/logic/lib/relativePosition";
interface DropdownProps {
children: ReactNode;
@ -44,46 +42,11 @@ export function Dropdown(props: DropdownProps) {
const [coords, setCoords] = useState({});
const updatePos = useCallback(() => {
const rect = anchorRef.current?.getBoundingClientRect();
if (rect) {
const bounds = {
top: rect.top,
left: rect.left,
bottom: document.documentElement.clientHeight - rect.bottom,
right: document.documentElement.clientWidth - rect.right,
};
const alignX = _.isArray(props.alignX) ? props.alignX : [props.alignX];
const alignY = _.isArray(props.alignY) ? props.alignY : [props.alignY];
let newCoords = {
..._.reduce(
alignX,
(acc, a, idx) => ({
...acc,
[a]: _.zipWith(
[...Array(idx), `${bounds[a]}px`],
acc[a] || [],
(a, b) => a || b || null
),
}),
{}
),
..._.reduce(
alignY,
(acc, a, idx) => ({
...acc,
[a]: _.zipWith(
[...Array(idx), `${bounds[a]}px`],
acc[a] || [],
(a, b) => a || b || null
),
}),
{}
),
};
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY);
if(newCoords) {
setCoords(newCoords);
}
}, [setCoords, anchorRef.current]);
}, [setCoords, anchorRef.current, props.alignY, props.alignX]);
useEffect(() => {
if (!open) {

View File

@ -21,12 +21,12 @@ interface HoverBoxLinkProps {
to: string;
}
export const HoverBoxLink = ({
export const HoverBoxLink = React.forwardRef(({
to,
children,
...rest
}: HoverBoxLinkProps & PropFunc<typeof HoverBox>) => (
<Link to={to}>
}: HoverBoxLinkProps & PropFunc<typeof HoverBox>, ref) => (
<Link ref={ref} to={to}>
<HoverBox {...rest}>{children}</HoverBox>
</Link>
);
));

View File

@ -162,7 +162,8 @@ export function ShipSearch<I extends string, V extends Value<I>>(
<FieldArray
name={id}
render={(arrayHelpers) => {
const onAdd = () => {
const onAdd = (ship: string) => {
setFieldValue(name(), ship);
inputIdx.current += 1;
arrayHelpers.push("");
};

View File

@ -1,6 +1,7 @@
import React, {
useState,
useEffect
useEffect,
useRef
} from 'react';
import {
@ -18,6 +19,7 @@ import { StatusBarItem } from './StatusBarItem';
import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from "~/logic/lib/util";
import { SetStatusBarModal } from './SetStatusBarModal';
import { useTutorialModal } from './useTutorialModal';
import useLocalState from '~/logic/state/local';
@ -43,6 +45,10 @@ const StatusBar = (props) => {
style={{ objectFit: 'cover' }} />
) : <Sigil ship={ship} size={16} color={color} icon />;
const anchorRef = useRef(null);
useTutorialModal('leap', true, anchorRef.current);
return (
<Box
display='grid'
@ -64,7 +70,7 @@ const StatusBar = (props) => {
</Box>
)}
<Icon icon='LeapArrow'/>
<Text ml={2} color='black'>
<Text ref={anchorRef} ml={2} color='black'>
Leap
</Text>
<Text display={['none', 'inline']} ml={2} color='gray'>

View File

@ -51,7 +51,7 @@ export class OmniboxResult extends Component {
graphic = <Icon display="inline-block" verticalAlign="middle" icon='SignOut' mr='2' size='18px' color={iconFill} />;
} else if (icon === 'profile') {
text = text.startsWith('Profile') ? window.ship : text;
graphic = <Sigil color={color} classes='dib flex-shrink-0 v-mid mr2' ship={text} size={18} icon padded />;
graphic = <Sigil color={color} classes='dib flex-shrink-0 v-mid mr2' ship={text} size={18} icon padding={2} />;
} else if (icon === 'home') {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Home' mr='2' size='18px' color={iconFill} />;
} else if (icon === 'notifications') {

View File

@ -0,0 +1,21 @@
import { useEffect } from "react";
import { TutorialProgress } from "~/types";
import useLocalState, { selectLocalState } from "~/logic/state/local";
const localSelector = selectLocalState(["tutorialProgress", "setTutorialRef"]);
export function useTutorialModal(
onProgress: TutorialProgress,
show: boolean,
anchorRef: HTMLElement | null
) {
const { tutorialProgress, setTutorialRef } = useLocalState(localSelector);
useEffect(() => {
if (show && onProgress === tutorialProgress && anchorRef) {
setTutorialRef(anchorRef);
}
}, [onProgress, tutorialProgress, show, anchorRef]);
return show && onProgress === tutorialProgress;
}

View File

@ -1,8 +1,9 @@
import React from "react";
import React, {useRef} from "react";
import { Col, Text, BaseLabel, Label } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Association, NotificationGraphConfig } from "~/types";
import { StatelessAsyncToggle } from "~/views/components/StatelessAsyncToggle";
import {useTutorialModal} from "~/views/components/useTutorialModal";
interface ChannelNotificationsProps {
api: GlobalApi;
@ -24,9 +25,13 @@ export function ChannelNotifications(props: ChannelNotificationsProps) {
await api.hark[func](rid, "/");
};
const anchorRef = useRef<HTMLElement | null>(null)
useTutorialModal('notifications', true, anchorRef.current);
return (
<Col mb="6" gapY="4" flexShrink={0}>
<Text id="notifications" fontSize="2" fontWeight="bold">
<Text ref={anchorRef} id="notifications" fontSize="2" fontWeight="bold">
Channel Notifications
</Text>
<BaseLabel display="flex" cursor="pointer">

View File

@ -1,19 +1,28 @@
import React, { ReactNode } from "react";
import React, { ReactNode, useRef } from "react";
import { Metadata } from "~/types";
import { Col, Row, Text } from "@tlon/indigo-react";
import { MetadataIcon } from "./MetadataIcon";
import { useTutorialModal } from "~/views/components/useTutorialModal";
import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal";
interface GroupSummaryProps {
metadata: Metadata;
memberCount: number;
channelCount: number;
resource?: string;
children?: ReactNode;
}
export function GroupSummary(props: GroupSummaryProps) {
const { channelCount, memberCount, metadata, children } = props;
const { channelCount, memberCount, metadata, resource, children } = props;
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal(
"group-desc",
resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
anchorRef.current
);
return (
<Col maxWidth="500px" gapY="4">
<Col ref={anchorRef} maxWidth="300px" gapY="4">
<Row gapX="2" width="100%">
<MetadataIcon
borderRadius="1"
@ -41,7 +50,7 @@ export function GroupSummary(props: GroupSummaryProps) {
</Col>
</Row>
<Row width="100%">
{metadata.description &&
{metadata.description &&
<Text
width="100%"
fontSize="1"

View File

@ -202,6 +202,7 @@ export function GroupsPane(props: GroupsPaneProps) {
memberCount={memberCount}
channelCount={channelCount}
metadata={groupAssociation.metadata}
resource={groupAssociation.group}
/>
} else {
summary = (<Box p="4"><Text fontSize="0" color='gray'>

View File

@ -113,7 +113,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
const members = group ? Array.from(groups[group]?.members).map(s => `~${s}`) : undefined;
return (
<Col overflowY="auto" p={3}>
<Col overflowY="auto" p={3} backgroundColor="white">
<Box pb='3' display={['block', 'none']} onClick={() => history.push(props.baseUrl)}>
<Text fontSize='0' bold>{'<- Back'}</Text>
</Box>

View File

@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useRef } from 'react';
import styled from 'styled-components';
import {
Col
@ -19,6 +19,7 @@ import { getGroupFromWorkspace } from '~/logic/lib/workspace';
import { SidebarAppConfigs } from './types';
import { SidebarList } from './SidebarList';
import { roleForShip } from '~/logic/lib/group';
import {useTutorialModal} from '~/views/components/useTutorialModal';
const ScrollbarLessCol = styled(Col)`
scrollbar-width: none !important;
@ -64,8 +65,12 @@ export function Sidebar(props: SidebarProps) {
const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined;
const isAdmin = (role === 'admin') || (workspace?.type === 'home');
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('channels', true, anchorRef.current);
return (
<ScrollbarLessCol
ref={anchorRef}
display={display}
width="100%"
gridRow="1/2"

View File

@ -1,4 +1,4 @@
import React from "react";
import React, {useRef} from "react";
import _ from 'lodash';
import { Icon, Row, Box, Text, BaseImage } from "@tlon/indigo-react";
@ -9,6 +9,8 @@ import { Groups, Association } from "~/types";
import { Sigil } from '~/logic/lib/sigil';
import urbitOb from 'urbit-ob';
import { getModuleIcon, getItemTitle, uxToHex } from "~/logic/lib/util";
import {useTutorialModal} from "~/views/components/useTutorialModal";
import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal";
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
switch (props.status) {
@ -41,6 +43,12 @@ export function SidebarItem(props: {
const mod = association?.metadata?.module || appName;
const rid = association?.resource
const groupPath = association?.group;
const anchorRef = useRef<HTMLElement | null>(null)
useTutorialModal(
mod as any,
groupPath === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
anchorRef.current
);
const app = apps[appName];
const isUnmanaged = groups?.[groupPath]?.hidden || false;
if (!app) {
@ -76,7 +84,7 @@ export function SidebarItem(props: {
if (props.contacts?.[title] && props.contacts[title].avatar) {
img = <BaseImage src={props.contacts[title].avatar} width='16px' height='16px' borderRadius={2}/>;
} else {
img = <Sigil ship={title} color={`#${uxToHex(props.contacts?.[title]?.color || '0x0')}`} icon padded size={16}/>
img = <Sigil ship={title} color={`#${uxToHex(props.contacts?.[title]?.color || '0x0')}`} icon padding={2} size={16}/>
}
if (props.contacts?.[title] && props.contacts[title].nickname) {
title = props.contacts[title].nickname;
@ -87,6 +95,7 @@ export function SidebarItem(props: {
return (
<HoverBoxLink
ref={anchorRef}
to={to}
bg="white"
bgActive="washedGray"

View File

@ -0,0 +1,189 @@
import React, { useState, useEffect, useCallback } from "react";
import { Box, Col, Row, Button, Text, Icon, Action } from "@tlon/indigo-react";
import { useHistory } from "react-router-dom";
import { TutorialProgress, tutorialProgress as progress } from "~/types";
import { Portal } from "~/views/components/Portal";
import useLocalState, { selectLocalState } from "~/logic/state/local";
import {
progressDetails,
MODAL_HEIGHT_PX,
MODAL_WIDTH_PX,
MODAL_WIDTH,
MODAL_HEIGHT,
TUTORIAL_HOST,
TUTORIAL_GROUP,
} from "~/logic/lib/tutorialModal";
import { getRelativePosition } from "~/logic/lib/relativePosition";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
import GlobalApi from "~/logic/api/global";
const localSelector = selectLocalState([
"tutorialProgress",
"nextTutStep",
"prevTutStep",
"tutorialRef",
"hideTutorial",
]);
export function TutorialModal(props: { api: GlobalApi }) {
const {
tutorialProgress,
tutorialRef,
nextTutStep,
prevTutStep,
hideTutorial,
} = useLocalState(localSelector);
const {
title,
description,
alignX,
alignY,
offsetX,
offsetY,
} = progressDetails[tutorialProgress];
const [coords, setCoords] = useState({});
const history = useHistory();
const next = useCallback(
(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation();
const idx = progress.findIndex((p) => p === tutorialProgress);
const { url } = progressDetails[progress[idx + 1]];
history.push(url);
nextTutStep();
},
[nextTutStep, history, tutorialProgress, setCoords]
);
const prev = useCallback(() => {
const idx = progress.findIndex((p) => p === tutorialProgress);
history.push(progressDetails[progress[idx - 1]].url);
prevTutStep();
}, [prevTutStep, history, tutorialProgress]);
const updatePos = useCallback(() => {
const newCoords = getRelativePosition(
tutorialRef,
alignX,
alignY,
offsetX,
offsetY
);
const withMobile: any = _.mapValues(newCoords, (value: string[], key: string) => {
if(key === 'bottom' || key === 'left') {
return ['0px', ...value];
}
return [null, ...value];
});
if(!('bottom' in withMobile)) {
withMobile.bottom = ['0px', null];
}
if(!('left' in withMobile)) {
withMobile.left = ['0px', null];
}
if (newCoords) {
setCoords(withMobile);
}
}, [tutorialRef]);
const dismiss = useCallback(() => {
hideTutorial();
props.api.settings.putEntry("tutorial", "seen", true);
}, [hideTutorial, props.api]);
const leaveGroup = useCallback(async () => {
await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP);
}, [props.api]);
const progressIdx = progress.findIndex((p) => p === tutorialProgress);
useEffect(() => {
if (
tutorialProgress !== "hidden" &&
tutorialProgress !== "done" &&
tutorialRef
) {
const interval = setInterval(updatePos, 100);
return () => {
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]);
if (tutorialProgress === "hidden") {
return null;
}
return (
<Portal>
<Box
position="fixed"
{...coords}
bg="white"
zIndex={50}
height={MODAL_HEIGHT_PX}
width={["100%", MODAL_WIDTH_PX]}
borderRadius="2"
>
<Col
position="relative"
justifyContent="space-between"
height="100%"
width="100%"
borderRadius="2"
p="2"
bg="lightBlue"
>
<Box
right="8px"
top="8px"
position="absolute"
cursor="pointer"
onClick={dismiss}
>
<Icon icon="X" />
</Box>
<Text lineHeight="tall" fontWeight="medium">
{title}
</Text>
<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>
)}
</Col>
</Box>
</Portal>
);
}