Merge branch 'next/groups'

This commit is contained in:
Liam Fitzgerald 2021-11-04 16:14:17 -05:00
commit 8135f32816
36 changed files with 178 additions and 838 deletions

View File

@ -1,20 +0,0 @@
name: group-timer
on:
push:
branches:
- 'ops/group-timer'
jobs:
glob:
runs-on: ubuntu-latest
name: "Create and deploy a glob to ~difmex-passed"
steps:
- uses: actions/checkout@v2
with:
lfs: true
- uses: ./.github/actions/glob
with:
ship: 'difmex-passed'
credentials: ${{ secrets.JANEWAY_SERVICE_KEY }}
ssh-sec-key: ${{ secrets.JANEWAY_SSH_SEC_KEY }}
ssh-pub-key: ${{ secrets.JANEWAY_SSH_PUB_KEY }}

View File

@ -24,11 +24,7 @@
def ~(. (default-agent this %|) bol)
io ~(. agentio bol)
::
++ on-init
^- (quip card _this)
=^ cards state
(put-entry:do q.byk.bol %tutorial %seen b+|)
[cards this]
++ on-init on-init:def
::
++ on-save !>(state)
::

View File

@ -111,11 +111,6 @@ module.exports = {
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(GIT_DESC),
'process.env.LANDSCAPE_STORAGE_VERSION': JSON.stringify(Date.now()),
'process.env.LANDSCAPE_LAST_WIPE': JSON.stringify('2021-10-20'),
'process.env.TUTORIAL_HOST': JSON.stringify('~difmex-passed'),
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
'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

@ -75,11 +75,6 @@ module.exports = {
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(GIT_DESC),
'process.env.LANDSCAPE_STORAGE_VERSION': Date.now().toString(),
'process.env.LANDSCAPE_LAST_WIPE': '2021-10-20',
'process.env.TUTORIAL_HOST': JSON.stringify('~difmex-passed'),
'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'),
'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: 'Groups',

View File

@ -93,7 +93,6 @@ 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(const cat of config.categories) {
if(idx[cat]) {
other.push(idx[cat]);

View File

@ -1,173 +0,0 @@
import { Associations } from '@urbit/api';
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
import { TutorialProgress } from '~/types';
import { Direction } from '~/views/components/Triangle';
export const MODAL_WIDTH = 256;
export const MODAL_HEIGHT = 256;
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!;
export const TUTORIAL_GROUP_RESOURCE = `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` ;
interface StepDetail {
title: string;
description: string;
url: string;
alignX: AlignX | AlignX[];
alignY: AlignY | AlignY[];
offsetX: number;
offsetY: number;
arrow?: Direction;
}
export function hasTutorialGroup(props: { associations: Associations }) {
return (
TUTORIAL_GROUP_RESOURCE in props.associations.groups
);
}
export const getTrianglePosition = (dir: Direction) => {
const midY = `${MODAL_HEIGHT / 2 - 8}px`;
const midX = `${MODAL_WIDTH / 2 - 8}px`;
switch(dir) {
case 'East':
return {
top: midY,
right: '-32px'
};
case 'West':
return {
top: midY,
left: '-32px'
};
case 'North':
return {
top: '-32px',
left: midX
};
case 'South':
return {
bottom: '-32px',
left: midX
};
}
};
export const progressDetails: Record<TutorialProgress, StepDetail> = {
hidden: {} as any,
exit: {} as any,
done: {
title: 'End',
description:
'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',
arrow: 'West',
offsetX: MODAL_WIDTH + 24,
offsetY: 64
},
'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',
arrow: 'East',
offsetX: MODAL_WIDTH + 24,
offsetY: 80
},
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',
arrow: 'West',
offsetX: MODAL_WIDTH + 24,
offsetY: -8
},
chat: {
title: 'Chat',
description:
'Chat channels are for messaging within your group. Direct Messages can be accessed from Messages in the top right',
url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`,
alignY: 'top',
arrow: 'North',
alignX: 'right',
offsetY: -56,
offsetX: -8
},
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',
arrow: 'North',
offsetX: -8,
offsetY: -56
},
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',
arrow: 'North',
offsetX: -8,
offsetY: -56
},
notifications: {
title: 'Notifications',
description: 'You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.',
url: '/~notifications',
alignY: 'top',
alignX: 'left',
arrow: 'North',
offsetX: 0,
offsetY: -48
},
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',
arrow: 'South',
offsetX: -300 + MODAL_WIDTH / 2,
offsetY: -4
},
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: 76,
offsetY: -48
}
};

View File

@ -15,11 +15,12 @@ export function useResize<T extends HTMLElement>(
callback(entry, observer);
}
}
let el = ref.current;
const resizeObs = new ResizeObserver(observer);
resizeObs.observe(ref.current, { box: 'border-box' });
resizeObs.observe(el, { box: 'border-box' });
return () => {
resizeObs.unobserve(ref.current);
resizeObs.unobserve(el);
};
}, [callback]);

View File

@ -4,14 +4,11 @@ import { patp2dec } from 'urbit-ob';
import f from 'lodash/fp';
import { Association, Contact, Patp } from '@urbit/api';
import { enableMapSet } from 'immer';
import useSettingsState from '../state/settings';
/* eslint-disable max-lines */
import anyAscii from 'any-ascii';
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
import bigInt, { BigInteger } from 'big-integer';
import { foregroundFromBackground } from '~/logic/lib/sigil';
import { IconRef, Workspace } from '~/types';
import useContactState from '../state/contact';
enableMapSet();
@ -462,13 +459,6 @@ export function pluralize(text: string, isPlural = false, vowel = false) {
return isPlural ? `${text}s` : `${vowel ? 'an' : 'a'} ${text}`;
}
// Hide is an optional second parameter for when this function is used in class components
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
const hideState = useSettingsState(state => state.calm.hideNicknames);
const hideNicknames = typeof hide !== 'undefined' ? hide : hideState;
return Boolean(contact && contact.nickname && !hideNicknames);
}
interface useHoveringInterface {
hovering: boolean;
bind: {
@ -513,21 +503,6 @@ export const svgDataURL = svg => 'data:image/svg+xml;base64,' + btoa(svg);
export const svgBlobURL = svg => URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
export const favicon = () => {
let background = '#ffffff';
const contacts = useContactState.getState().contacts;
if (Object.prototype.hasOwnProperty.call(contacts, `~${window.ship}`)) {
background = `#${uxToHex(contacts[`~${window.ship}`].color)}`;
}
const foreground = foregroundFromBackground(background);
const svg = sigiljs({
patp: window.ship,
renderer: stringRenderer,
size: 16,
colors: [background, foreground]
});
return svg;
};
export function binaryIndexOf(arr: BigInteger[], target: BigInteger): number | undefined {
let leftBound = 0;

View File

@ -1,4 +1,4 @@
import { Contact, deSig, Patp, Rolodex } from '@urbit/api';
import { Contact, deSig, Patp, Rolodex, uxToHex } from '@urbit/api';
import { useCallback } from 'react';
import _ from 'lodash';
import { reduce, reduceNacks } from '../reducers/contact-update';
@ -7,6 +7,8 @@ import {
createSubscription,
reduceStateN
} from './base';
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
import { foregroundFromBackground } from '~/logic/lib/sigil';
export interface ContactState {
contacts: Rolodex;
@ -51,4 +53,21 @@ export function useOurContact() {
return useContact(`~${window.ship}`);
}
export const favicon = () => {
let background = '#ffffff';
const contacts = useContactState.getState().contacts;
if (Object.prototype.hasOwnProperty.call(contacts, `~${window.ship}`)) {
background = `#${uxToHex(contacts[`~${window.ship}`].color)}`;
}
const foreground = foregroundFromBackground(background);
const svg = sigiljs({
patp: window.ship,
renderer: stringRenderer,
size: 16,
colors: [background, foreground]
});
return svg;
};
export default useContactState;

View File

@ -9,7 +9,8 @@ import {
harkBinToId,
decToUd,
unixToDa,
opened
opened,
markEachAsRead
} from '@urbit/api';
import { Poke } from '@urbit/http-api';
import { patp2dec } from 'urbit-ob';
@ -25,6 +26,7 @@ import {
reduceStateN
} from './base';
import { reduce, reduceGraph, reduceGroup } from '../reducers/hark-update';
import useMetadataState from './metadata';
export const HARK_FETCH_MORE_COUNT = 3;
@ -43,6 +45,8 @@ export interface HarkState {
unreads: Unreads;
archiveNote: (bin: HarkBin, lid: HarkLid) => Promise<void>;
readCount: (path: string) => Promise<void>;
readGraph: (graph: string) => Promise<void>;
readGroup: (group: string) => Promise<void>;
}
const useHarkState = createState<HarkState>(
@ -54,6 +58,38 @@ const useHarkState = createState<HarkState>(
poke: async (poke: Poke<any>) => {
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
readGraph: async (graph: string) => {
const prefix = `/graph/${graph.slice(6)}`;
let counts = [] as string[];
let eaches = [] as [string, string][];
Object.entries(get().unreads).forEach(([path, unreads]) => {
if (path.startsWith(prefix)) {
if(unreads.count > 0) {
counts.push(path);
}
unreads.each.forEach(unread => {
eaches.push([path, unread]);
});
}
});
get().set(draft => {
counts.forEach(path => {
draft.unreads[path].count = 0;
});
eaches.forEach(([path, each]) => {
draft.unreads[path].each = [];
});
});
await Promise.all([
...counts.map(path => markCountAsRead({ desk: window.desk, path })),
...eaches.map(([path, each]) => markEachAsRead({ desk: window.desk, path }, each))
].map(pok => api.poke(pok)));
},
readGroup: async (group: string) => {
const graphs =
_.pickBy(useMetadataState.getState().associations.graph, a => a.group === group);
await Promise.all(Object.keys(graphs).map(get().readGraph));
},
readCount: async (path) => {
const poke = markCountAsRead({ desk: (window as any).desk, path });
await pokeOptimisticallyN(useHarkState, poke, [reduce]);

View File

@ -3,7 +3,7 @@ import f from 'lodash/fp';
import React from 'react';
import create, { State } from 'zustand';
import { persist } from 'zustand/middleware';
import { BackgroundConfig, LeapCategories, RemoteContentPolicy, TutorialProgress, tutorialProgress } from '~/types/local-update';
import { BackgroundConfig, LeapCategories, RemoteContentPolicy } from '~/types/local-update';
import airlock from '~/logic/api';
import { bootstrapApi } from '../api/bootstrap';
import { clearStorageMigration, createStorageKey, storageVersion, wait } from '~/logic/lib/util';
@ -15,15 +15,9 @@ export interface LocalState {
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: RemoteContentPolicy;
tutorialProgress: TutorialProgress;
hideGroups: boolean;
hideUtilities: boolean;
tutorialRef: HTMLElement | null,
hideTutorial: () => void;
nextTutStep: () => void;
prevTutStep: () => void;
hideLeapCats: LeapCategories[];
setTutorialRef: (el: HTMLElement | null) => void;
dark: boolean;
mobile: boolean;
breaks: {
@ -62,27 +56,6 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({
hideLeapCats: [],
hideGroups: false,
hideUtilities: 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,
@ -132,8 +105,8 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({
set: fn => set(produce(fn))
}), {
blacklist: [
'suspendedFocus', 'toggleOmnibox', 'omniboxShown', 'tutorialProgress',
'prevTutStep', 'nextTutStep', 'tutorialRef', 'setTutorialRef', 'subscription',
'suspendedFocus', 'toggleOmnibox', 'omniboxShown',
'subscription',
'errorCount', 'breaks'
],
name: createStorageKey('local'),

View File

@ -16,7 +16,7 @@ import {
import { useCallback } from 'react';
import { reduceUpdate } from '../reducers/settings-update';
import airlock from '~/logic/api';
import { getDeskSettings, Value } from '@urbit/api';
import { Contact, getDeskSettings, Value } from '@urbit/api';
import { putEntry } from '@urbit/api/settings';
export interface ShortcutMapping {
@ -49,10 +49,6 @@ export interface SettingsState {
leap: {
categories: LeapCategories[];
};
tutorial: {
seen: boolean;
joined?: number;
};
}
export const selectSettingsState = <K extends keyof (SettingsState & BaseState<SettingsState>)>(keys: K[]) =>
@ -88,10 +84,6 @@ const useSettingsState = createState<SettingsState>(
leap: {
categories: leapCategories
},
tutorial: {
seen: true,
joined: undefined
},
keyboard: {
cycleForward: 'ctrl+\'',
cycleBack: 'ctrl+;',
@ -139,4 +131,11 @@ export function useTheme() {
return useSettingsState(selTheme);
}
// Hide is an optional second parameter for when this function is used in class components
export function useShowNickname(contact: Contact | null, hide?: boolean): boolean {
const hideState = useSettingsState(state => state.calm.hideNicknames);
const hideNicknames = typeof hide !== 'undefined' ? hide : hideState;
return Boolean(contact && contact.nickname && !hideNicknames);
}
export default useSettingsState;

View File

@ -1,10 +1,7 @@
export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'profile', 'leap', 'notifications', 'done', 'exit'] as const;
export const leapCategories = ['mychannel', 'messages', 'updates', 'profile', 'logout'];
export type LeapCategories = typeof leapCategories[number];
export type TutorialProgress = typeof tutorialProgress[number];
interface LocalUpdateSetDark {
setDark: boolean;
}

View File

@ -10,16 +10,15 @@ import { hot } from 'react-hot-loader/root';
import { BrowserRouter as Router, withRouter } from 'react-router-dom';
import styled, { ThemeProvider } from 'styled-components';
import gcpManager from '~/logic/lib/gcpManager';
import { favicon, svgDataURL } from '~/logic/lib/util';
import { svgDataURL } from '~/logic/lib/util';
import withState from '~/logic/lib/withState';
import useContactState from '~/logic/state/contact';
import useContactState, { favicon } from '~/logic/state/contact';
import useLocalState from '~/logic/state/local';
import useSettingsState from '~/logic/state/settings';
import useGraphState from '~/logic/state/graph';
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
import ErrorBoundary from '~/views/components/ErrorBoundary';
import { TutorialModal } from '~/views/landscape/components/TutorialModal';
import './apps/chat/css/custom.css';
import Omnibox from './components/leap/Omnibox';
import StatusBar from './components/StatusBar';
@ -171,7 +170,6 @@ class App extends React.Component {
</Helmet>
<Root>
<Router basename="/apps/landscape">
<TutorialModal />
<ErrorBoundary>
<StatusBarWithRouter
props={this.props}

View File

@ -13,11 +13,11 @@ import { useIdlingState } from '~/logic/lib/idling';
import { Sigil } from '~/logic/lib/sigil';
import { useCopy } from '~/logic/lib/useCopy';
import {
cite, daToUnix, useHovering, useShowNickname, uxToHex
cite, daToUnix, useHovering, uxToHex
} from '~/logic/lib/util';
import { useContact } from '~/logic/state/contact';
import { useDark } from '~/logic/state/join';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import useSettingsState, { selectCalmState, useShowNickname } from '~/logic/state/settings';
import { Dropdown } from '~/views/components/Dropdown';
import ProfileOverlay from '~/views/components/ProfileOverlay';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';

View File

@ -1,27 +1,11 @@
/* eslint-disable max-lines-per-function */
import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
import f from 'lodash/fp';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import { Box, Icon, Row, Text } from '@tlon/indigo-react';
import React, { ReactElement } from 'react';
import { Helmet } from 'react-helmet';
import { Route } from 'react-router-dom';
import styled from 'styled-components';
import {
hasTutorialGroup,
TUTORIAL_BOOK,
TUTORIAL_CHAT,
TUTORIAL_GROUP,
TUTORIAL_HOST,
TUTORIAL_LINKS
} from '~/logic/lib/tutorialModal';
import { useModal } from '~/logic/lib/useModal';
import { useQuery } from '~/logic/lib/useQuery';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import useHarkState from '~/logic/state/hark';
import useLocalState from '~/logic/state/local';
import useMetadataState from '~/logic/state/metadata';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { StarIcon } from '~/views/components/StarIcon';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { JoinGroup } from '~/views/landscape/components/JoinGroup';
import { NewGroup } from '~/views/landscape/components/NewGroup';
import Groups from './components/Groups';
@ -30,9 +14,6 @@ import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import { Invite } from './components/Invite';
import './css/custom.css';
import { join } from '@urbit/api/groups';
import { joinGraph } from '@urbit/api/graph';
import airlock from '~/logic/api';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -42,111 +23,14 @@ const ScrollbarLessBox = styled(Box)`
}
`;
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
interface LaunchAppProps {
connection: string;
}
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const { connection } = props;
const [exitingTut, setExitingTut] = useState(false);
const seen = useSettingsState(s => s?.tutorial?.seen) ?? true;
const associations = useMetadataState(s => s.associations);
const hasLoaded = useMemo(() => Boolean(connection === 'connected'), [connection]);
const notificationsCount = useHarkState(state => state.notificationsCount);
const calmState = useSettingsState(selectCalmState);
const { hideUtilities } = calmState;
const { tutorialProgress, nextTutStep } = useLocalState(tutSelector);
let { hideGroups } = useLocalState(tutSelector);
!hideGroups ? { hideGroups } = calmState : null;
const waiter = useWaitForProps({ ...props, associations });
const { query } = useQuery();
const { modal, showModal } = useModal({
position: 'relative',
maxWidth: '350px',
modal: function modal(dismiss) {
const onDismiss = (e) => {
const { putEntry } = useSettingsState.getState();
e.stopPropagation();
putEntry('tutorial', 'seen', true);
dismiss();
};
const onContinue = async (e) => {
const { putEntry } = useSettingsState.getState();
e.stopPropagation();
if (!hasTutorialGroup({ associations })) {
await airlock.poke(join(TUTORIAL_HOST, TUTORIAL_GROUP));
await putEntry('tutorial', 'joined', Date.now());
await waiter(hasTutorialGroup);
await Promise.all(
[TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => airlock.thread(joinGraph(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 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 &ldquo;tutorial&rdquo; 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 Groups, an interface to chat
and interact with communities
<br />
Would you like a tour of Groups?
</Text>
<Row gapX={2} justifyContent="flex-end">
<Button
backgroundColor="washedGray"
onClick={() => setExitingTut(true)}
>Skip</Button>
<StatelessAsyncButton primary onClick={onContinue}>
Yes
</StatelessAsyncButton>
</Row>
</Col>
);
}
});
useEffect(() => {
if(query.get('tutorial')) {
if (hasTutorialGroup({ associations })) {
if (nextTutStep) {
nextTutStep();
}
} else {
showModal();
}
}
}, [query, showModal]);
useEffect(() => {
if(hasLoaded && !seen && tutorialProgress === 'hidden') {
showModal();
}
}, [seen, hasLoaded]);
const { hideUtilities, hideGroups } = calmState;
return (
<>
@ -157,7 +41,6 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
<Invite />
</Route>
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
{modal}
<Box
mx={2}
display='grid'

View File

@ -1,30 +1,25 @@
import { Box, Col, Text } from '@tlon/indigo-react';
import { Association, Associations, Unreads } from '@urbit/api';
import f from 'lodash/fp';
import moment from 'moment';
import React, { useRef } from 'react';
import React from 'react';
import { getNotificationCount } from '~/logic/lib/hark';
import { TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE, TUTORIAL_HOST } from '~/logic/lib/tutorialModal';
import { alphabeticalOrder } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group';
import useHarkState, { selHarkGraph } from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import useSettingsState, {
selectCalmState
} from '~/logic/state/settings';
import Tile from '../components/tiles/tile';
const sortGroupsAlph = (a: Association, b: Association) =>
a.group === TUTORIAL_GROUP_RESOURCE
? -1
: b.group === TUTORIAL_GROUP_RESOURCE
? 1
: alphabeticalOrder(a.metadata.title, b.metadata.title);
alphabeticalOrder(a.metadata.title, b.metadata.title);
const getGraphUnreads = (associations: Associations) => {
const state = useHarkState.getState();
const selUnread = (graph: string) => {
const { count, each } = selHarkGraph(graph)(state);
const result = count + each.length;
const result = count + each.length;
return result;
};
return (path: string) =>
@ -36,7 +31,10 @@ const getGraphUnreads = (associations: Associations) => {
)(associations.graph);
};
const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) =>
const getGraphNotifications = (
associations: Associations,
unreads: Unreads
) => (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
f.map('resource'),
@ -52,8 +50,11 @@ export default function Groups(props: Parameters<typeof Box>[0]) {
const groups = Object.values(associations?.groups || {})
.filter(e => e?.group in groupState)
.sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || {} as Associations);
const graphNotifications = getGraphNotifications(associations || {} as Associations, unreads);
const graphUnreads = getGraphUnreads(associations || ({} as Associations));
const graphNotifications = getGraphNotifications(
associations || ({} as Associations),
unreads
);
return (
<>
@ -83,37 +84,26 @@ 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<HTMLDivElement>(null);
const isTutorialGroup = path === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`;
useTutorialModal(
'start',
isTutorialGroup,
anchorRef
);
const { hideUnreads } = useSettingsState(selectCalmState);
const joined = useSettingsState(selectJoined);
const days = Math.max(0, Math.floor(moment.duration(moment(joined)
.add(14, 'days')
.diff(moment()))
.as('days'))) || 0;
return (
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? 1 : null}>
<Tile
position="relative"
to={`/~landscape${path}`}
gridColumnStart={first ? 1 : null}
>
<Col height="100%" justifyContent="space-between">
<Text>{title}</Text>
{!hideUnreads && (<Col>
{isTutorialGroup && joined &&
(<Text>{days} day{days !== 1 && 's'} remaining</Text>)
}
{updates > 0 &&
(<Text mt={1} color="blue">{updates} update{updates !== 1 && 's'} </Text>)
}
{unreads > 0 &&
(<Text color="lightGray">{unreads}</Text>)
}
</Col>
{!hideUnreads && (
<Col>
{updates > 0 && (
<Text mt={1} color="blue">
{updates} update{updates !== 1 && 's'}{' '}
</Text>
)}
{unreads > 0 && <Text color="lightGray">{unreads}</Text>}
</Col>
)}
</Col>
</Tile>

View File

@ -1,11 +1,10 @@
import { Action, Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
import React, { ReactElement, ReactNode, useEffect, useRef } from 'react';
import React, { ReactElement, ReactNode, useEffect } from 'react';
import Helmet from 'react-helmet';
import { Link, Route, Switch, useHistory, useLocation } from 'react-router-dom';
import useHarkState from '~/logic/state/hark';
import { Body } from '~/views/components/Body';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { Archive } from './Archive';
import { NewBox } from './NewBox';
@ -41,8 +40,6 @@ export function NavLink({
export default function NotificationsScreen(props: any): ReactElement {
const relativePath = (p: string) => baseUrl + p;
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('notifications', true, anchorRef);
const notificationsCount = useHarkState(state => state.notificationsCount);
const onReadAll = async () => {};
@ -89,7 +86,6 @@ export default function NotificationsScreen(props: any): ReactElement {
fontWeight="bold"
fontSize={2}
lineHeight={1}
ref={anchorRef}
>
Notifications
</Text>

View File

@ -1,6 +1,6 @@
import { BaseImage, Box, Center, Row, Text } from '@tlon/indigo-react';
import { retrieve } from '@urbit/api';
import React, { ReactElement, useEffect, useRef } from 'react';
import React, { ReactElement, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
@ -8,7 +8,6 @@ import useContactState from '~/logic/state/contact';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import RichText from '~/views/components/RichText';
import { SetStatusBarModal } from '~/views/components/SetStatusBarModal';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { EditProfile } from './EditProfile';
import { ViewProfile } from './ViewProfile';
import airlock from '~/logic/api';
@ -32,10 +31,6 @@ export function ProfileImages(props: any): ReactElement {
const { contact, hideCover, ship } = props;
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000';
const anchorRef = useRef<HTMLDivElement>(null);
useTutorialModal('profile', ship === `~${window.ship}`, anchorRef);
const cover =
contact?.cover && !hideCover ? (
<BaseImage
@ -69,7 +64,7 @@ export function ProfileImages(props: any): ReactElement {
return (
<>
<Row ref={anchorRef} width='100%' height='400px' position='relative'>
<Row width='100%' height='400px' position='relative'>
{cover}
<Center position='absolute' width='100%' height='100%'>
{props.children}

View File

@ -61,6 +61,9 @@ export function Note(props: NoteProps & RouteComponentProps) {
const noteId = bigInt(index[1]);
useEffect(() => {
airlock.poke(markEachAsRead(toHarkPlace(association.resource), `/${index[1]}`));
// Unread may be malformed, dismiss anyway
// TODO: remove when %read-graph is implemented
airlock.poke(markEachAsRead(toHarkPlace(association.resource), `/1`));
}, [association, props.note]);
const adminLinks: JSX.Element[] = [];

View File

@ -2,11 +2,12 @@ import { Box, Button, Col, Row, Text } from '@tlon/indigo-react';
import { Association, Graph, readGraph } from '@urbit/api';
import React, { ReactElement, useCallback } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { useShowNickname } from '~/logic/lib/util';
import useContactState from '~/logic/state/contact';
import useGroupState from '~/logic/state/group';
import { useShowNickname } from '~/logic/state/settings';
import airlock from '~/logic/api';
import { NotebookPosts } from './NotebookPosts';
import useHarkState from '~/logic/state/hark';
interface NotebookProps {
ship: string;
@ -35,7 +36,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
const showNickname = useShowNickname(contact);
const readBook = useCallback(() => {
airlock.poke(readGraph(association.resource));
useHarkState.getState().readGraph(association.resource);
}, [association.resource]);
if (!group) {

View File

@ -3,10 +3,10 @@ import moment from 'moment';
import React, { ReactElement, ReactNode } from 'react';
import { Sigil } from '~/logic/lib/sigil';
import { useCopy } from '~/logic/lib/useCopy';
import { cite, useShowNickname, uxToHex } from '~/logic/lib/util';
import { cite, uxToHex } from '~/logic/lib/util';
import { useContact } from '~/logic/state/contact';
import { useDark } from '~/logic/state/join';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import useSettingsState, { selectCalmState, useShowNickname } from '~/logic/state/settings';
import { PropFunc } from '~/types';
import ProfileOverlay from './ProfileOverlay';
import Timestamp from './Timestamp';

View File

@ -2,8 +2,9 @@ import { Text } from '@tlon/indigo-react';
import { Contact, Content, Group } from '@urbit/api';
import React from 'react';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { cite, deSig, useShowNickname } from '~/logic/lib/util';
import { cite, deSig } from '~/logic/lib/util';
import { useContact } from '~/logic/state/contact';
import { useShowNickname } from '~/logic/state/settings';
import { PropFunc } from '~/types';
import ProfileOverlay from '~/views/components/ProfileOverlay';
import RichText from '~/views/components/RichText';

View File

@ -19,9 +19,8 @@ import { getRelativePosition } from '~/logic/lib/relativePosition';
import { Sigil } from '~/logic/lib/sigil';
import { useCopy } from '~/logic/lib/useCopy';
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
import { useShowNickname } from '~/logic/lib/util';
import { useContact } from '~/logic/state/contact';
import useSettingsState, { SettingsState } from '~/logic/state/settings';
import useSettingsState, { SettingsState, useShowNickname } from '~/logic/state/settings';
import { Portal } from './Portal';
import { ProfileStatus } from './ProfileStatus';
import RichText from './RichText';

View File

@ -7,7 +7,7 @@ import {
Row,
Text
} from '@tlon/indigo-react';
import React, { useRef } from 'react';
import React from 'react';
import { Link } from 'react-router-dom';
import { Sigil } from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
@ -18,7 +18,6 @@ import { Dropdown } from './Dropdown';
import { ProfileStatus } from './ProfileStatus';
import ReconnectButton from './ReconnectButton';
import { StatusBarItem } from './StatusBarItem';
import { useTutorialModal } from './useTutorialModal';
import { StatusBarJoins } from './StatusBarJoins';
import useHarkState from '~/logic/state/hark';
@ -48,13 +47,6 @@ const StatusBar = (props) => {
<Sigil ship={ship} size={16} color={color} icon />
);
const anchorRef = useRef(null);
const leapHighlight = useTutorialModal('leap', true, anchorRef);
const floatLeap =
leapHighlight && window.matchMedia('(max-width: 550px)').matches;
return (
<Box
display='grid'
@ -77,9 +69,9 @@ const StatusBar = (props) => {
>
<Icon icon='Dashboard' color='black' />
</Button>
<StatusBarItem position="relative" float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
<StatusBarItem position="relative" mr={2} onClick={() => toggleOmnibox()}>
<Icon icon='LeapArrow' />
<Text ref={anchorRef} ml={2} color='black'>
<Text ml={2} color='black'>
Leap
</Text>
<Text display={['none', 'inline']} ml={2} color='gray'>

View File

@ -119,7 +119,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
if (category === 'other') {
return [
'other',
index.get('other').filter(({ app }) => app !== 'tutorial')
index.get('other')
];
}
return [category, []];
@ -159,7 +159,6 @@ export function Omnibox(props: OmniboxProps): ReactElement {
defaultApps.includes(app.toLowerCase()) ||
app === 'profile' ||
app === 'messages' ||
app === 'tutorial' ||
app === 'Links' ||
app === 'Terminal' ||
app === 'home' ||

View File

@ -169,17 +169,6 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
color={iconFill}
/>
);
} else if (icon === 'tutorial') {
graphic = (
<Icon
display='inline-block'
verticalAlign='middle'
icon='Tutorial'
mr={2}
size='18px'
color={iconFill}
/>
);
} else {
graphic = (
<Icon

View File

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

View File

@ -1,9 +1,7 @@
import { Col, Row, Text, Icon } from '@tlon/indigo-react';
import { Metadata } from '@urbit/api';
import React, { ReactElement, ReactNode, useRef } from 'react';
import { TUTORIAL_GROUP, TUTORIAL_HOST } from '~/logic/lib/tutorialModal';
import React, { ReactElement, ReactNode } from 'react';
import { PropFunc, IconRef } from '~/types';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { MetadataIcon } from './MetadataIcon';
import { useCopy } from '~/logic/lib/useCopy';
interface GroupSummaryProps {
@ -17,17 +15,24 @@ interface GroupSummaryProps {
locked?: boolean;
}
export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): ReactElement {
const { channelCount, memberCount, metadata, resource, children, ...rest } = props;
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal(
'group-desc',
resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
anchorRef
export function GroupSummary(
props: GroupSummaryProps & PropFunc<typeof Col>
): ReactElement {
const {
channelCount,
memberCount,
metadata,
resource,
children,
...rest
} = props;
const { doCopy, copyDisplay } = useCopy(
`web+urbitgraph://group${resource?.slice(5)}`,
'Copy',
'Checkmark'
);
const { doCopy, copyDisplay } = useCopy(`web+urbitgraph://group${resource?.slice(5)}`, "Copy", "Checkmark");
return (
<Col {...rest} ref={anchorRef} gapY={4} maxWidth={['100%', '288px']}>
<Col {...rest} gapY={4} maxWidth={['100%', '288px']}>
<Row gapX={2} width="100%">
<MetadataIcon
width="40px"
@ -37,21 +42,22 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
/>
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
<Row justifyContent="space-between">
<Text
fontSize={1}
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden"
>{metadata.title}
</Text>
{props?.AllowCopy &&
<Icon
color="gray"
icon={props?.locked ? "Locked" : copyDisplay as IconRef}
onClick={!props?.locked ? doCopy : null}
cursor={props?.locked ? "default" : "pointer"}
/>
}
<Text
fontSize={1}
textOverflow="ellipsis"
whiteSpace="nowrap"
overflow="hidden"
>
{metadata.title}
</Text>
{props?.AllowCopy && (
<Icon
color="gray"
icon={props?.locked ? 'Locked' : (copyDisplay as IconRef)}
onClick={!props?.locked ? doCopy : null}
cursor={props?.locked ? 'default' : 'pointer'}
/>
)}
</Row>
<Row gapX={4} justifyContent="space-between">
<Text fontSize={1} gray>
@ -64,17 +70,17 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
</Col>
</Row>
<Row width="100%">
{metadata.description &&
<Text
{metadata.description && (
<Text
gray
width="100%"
fontSize={1}
textOverflow="ellipsis"
overflow="hidden"
>
>
{metadata.description}
</Text>
}
)}
</Row>
{children}
</Col>

View File

@ -43,7 +43,7 @@ export function GroupsPane(props: GroupsPaneProps) {
useShortcut('readGroup', useCallback(() => {
if(groupPath) {
airlock.poke(readGroup(groupPath));
useHarkState.getState().readGroup(groupPath);
}
}, [groupPath]));

View File

@ -12,7 +12,6 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import urbitOb from 'urbit-ob';
import * as Yup from 'yup';
import { TUTORIAL_GROUP_RESOURCE } from '~/logic/lib/tutorialModal';
import { useQuery } from '~/logic/lib/useQuery';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { getModuleIcon } from '~/logic/lib/util';
@ -23,7 +22,6 @@ import { FormError } from '~/views/components/FormError';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { GroupSummary } from './GroupSummary';
import airlock from '~/logic/api';
import useSettingsState from '~/logic/state/settings';
const formSchema = Yup.object({
group: Yup.string()
@ -77,10 +75,6 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
const onConfirm = useCallback(async (group: string) => {
const [,,ship,name] = group.split('/');
const { putEntry } = useSettingsState.getState();
if(group === TUTORIAL_GROUP_RESOURCE) {
await putEntry('tutorial', 'joined', Date.now());
}
if (group in groups) {
return history.push(`/~landscape${group}`);
}

View File

@ -1,14 +1,13 @@
import {
Col
} from '@tlon/indigo-react';
import React, { ReactElement, useRef } from 'react';
import React, { ReactElement } from 'react';
import styled from 'styled-components';
import { roleForShip } from '~/logic/lib/group';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
import { getGroupFromWorkspace } from '~/logic/lib/workspace';
import useGroupState from '~/logic/state/group';
import { Workspace } from '~/types';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { GroupSwitcher } from '../GroupSwitcher';
import { SidebarList } from './SidebarList';
import { SidebarListHeader } from './SidebarListHeader';
@ -48,12 +47,8 @@ export function Sidebar(props: SidebarProps): ReactElement | null {
const role = groups?.[groupPath] ? roleForShip(groups[groupPath], window.ship) : undefined;
const isAdmin = (role === 'admin') || (workspace?.type === 'home');
const anchorRef = useRef<HTMLDivElement>(null);
useTutorialModal('channels', true, anchorRef);
return (
<ScrollbarLessCol
ref={anchorRef}
display={display}
width="100%"
gridRow="1/2"

View File

@ -1,12 +1,10 @@
import _ from 'lodash';
import React, { useRef, ReactNode } from 'react';
import React, { ReactNode } from 'react';
import urbitOb from 'urbit-ob';
import { Icon, Row, Box, Text, BaseImage } from '@tlon/indigo-react';
import { Association, cite, deSig } from '@urbit/api';
import { HoverBoxLink } from '~/views/components/HoverBox';
import { Sigil } from '~/logic/lib/sigil';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
import { Workspace } from '~/types/workspace';
import useContactState, { useContact } from '~/logic/state/contact';
import { getItemTitle, getModuleIcon, uxToHex } from '~/logic/lib/util';
@ -24,13 +22,15 @@ function useAssociationStatus(resource: string) {
const { count, each } = stats;
const hasNotifications = false;
const hasUnread = count > 0 || each.length > 0;
return hasNotifications
? 'notification'
: hasUnread
? 'unread'
: isSubscribed
? undefined
: 'unsubscribed';
if(!isSubscribed) {
return 'unsubscribed';
} else if (hasNotifications) {
return 'notification';
} else if (hasUnread) {
return 'unread';
} else {
return undefined;
}
}
function SidebarItemBase(props: {
@ -186,12 +186,6 @@ export const SidebarAssociationItem = React.memo((props: {
const group = useGroupState(state => state.groups[groupPath]);
const { hideNicknames } = useSettingsState(s => s.calm);
const contacts = useContactState(s => s.contacts);
const anchorRef = useRef<HTMLAnchorElement>(null);
useTutorialModal(
mod as any,
groupPath === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
anchorRef
);
const isUnmanaged = group?.hidden || false;
const DM = isUnmanaged && props.workspace?.type === 'messages';
const itemStatus = useAssociationStatus(rid);

View File

@ -1,264 +0,0 @@
import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
import { leaveGroup } from '@urbit/api';
import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getRelativePosition } from '~/logic/lib/relativePosition';
import {
getTrianglePosition, MODAL_WIDTH_PX, progressDetails,
TUTORIAL_GROUP, TUTORIAL_HOST
} from '~/logic/lib/tutorialModal';
import useLocalState, { selectLocalState } from '~/logic/state/local';
import { tutorialProgress as progress } from '~/types';
import { ModalOverlay } from '~/views/components/ModalOverlay';
import { Portal } from '~/views/components/Portal';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { Triangle } from '~/views/components/Triangle';
import airlock from '~/logic/api';
import useSettingsState from '~/logic/state/settings';
const localSelector = selectLocalState([
'tutorialProgress',
'nextTutStep',
'prevTutStep',
'tutorialRef',
'hideTutorial',
'set'
]);
export function TutorialModal() {
const {
tutorialProgress,
tutorialRef,
nextTutStep,
prevTutStep,
hideTutorial
} = useLocalState(localSelector);
const {
title,
description,
arrow = 'North',
alignX,
alignY,
offsetX,
offsetY
} = progressDetails[tutorialProgress];
const [coords, setCoords] = useState({});
const [paused, setPaused] = useState(false);
const history = useHistory();
const next = useCallback( () => {
const idx = progress.findIndex(p => p === tutorialProgress);
const { url } = progressDetails[progress[idx + 1]];
nextTutStep();
history.push(url);
},
[nextTutStep, history, tutorialProgress, setCoords]
);
const prev = useCallback(() => {
const idx = progress.findIndex(p => p === tutorialProgress);
prevTutStep();
history.push(progressDetails[progress[idx - 1]].url);
}, [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 ['unset', ...value];
});
if(!('bottom' in withMobile)) {
withMobile.bottom = ['0px', 'unset'];
}
if(!('left' in withMobile)) {
withMobile.left = ['0px', 'unset'];
}
if (newCoords) {
setCoords(withMobile);
} else {
setCoords({});
}
}, [tutorialRef]);
const dismiss = useCallback(async () => {
setPaused(false);
hideTutorial();
const { putEntry } = useSettingsState.getState();
await putEntry('tutorial', 'seen', true);
}, [hideTutorial]);
const bailExit = useCallback(() => {
setPaused(false);
}, []);
const tryExit = useCallback(() => {
setPaused(true);
}, []);
const doLeaveGroup = useCallback(async () => {
await airlock.thread(leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP));
await dismiss();
}, [dismiss]);
const progressIdx = progress.findIndex(p => p === tutorialProgress);
useEffect(() => {
if (
tutorialProgress !== 'hidden' &&
tutorialProgress !== 'done' &&
tutorialRef
) {
const interval = setInterval(updatePos, 100);
return () => {
setCoords({});
clearInterval(interval);
};
}
return () => {};
}, [tutorialRef, tutorialProgress, updatePos]);
const triPos = getTrianglePosition(arrow);
if (tutorialProgress === 'done') {
return (
<Portal>
<ModalOverlay dismiss={dismiss} borderRadius={2} maxWidth="270px" backgroundColor="white">
<Col p={3} bg="lightBlue">
<Col mb={3}>
<Text lineHeight="tall" fontWeight="bold">
Tutorial Finished
</Text>
<Text fontSize={0} gray>
{progressIdx} of {progress.length - 2}
</Text>
</Col>
<Text lineHeight="tall">
This tutorial is finished. Would you like to leave Beginner Island?
</Text>
<Row mt={3} gapX={2} justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={dismiss}>
Later
</Button>
<StatelessAsyncButton primary destructive onClick={doLeaveGroup}>
Leave Group
</StatelessAsyncButton>
</Row>
</Col>
</ModalOverlay>
</Portal>
);
}
if (tutorialProgress === 'hidden') {
return null;
}
if(paused) {
return (
<ModalOverlay dismiss={bailExit} borderRadius={2} maxWidth="270px" backgroundColor="white">
<Col p={3}>
<Col mb={3}>
<Text lineHeight="tall" fontWeight="bold">
End Tutorial Now?
</Text>
</Col>
<Text lineHeight="tall">
You can always restart the tutorial by typing &quot;tutorial&quot; in Leap.
</Text>
<Row mt={3} gapX={2} justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={bailExit}>
Cancel
</Button>
<StatelessAsyncButton primary destructive onClick={dismiss}>
End Tutorial
</StatelessAsyncButton>
</Row>
</Col>
</ModalOverlay>
);
}
if(Object.keys(coords).length === 0) {
return null;
}
return (
<Portal>
<Box
position="fixed"
{...coords}
bg="white"
zIndex={50}
display="flex"
flexDirection="column"
width={['100%', MODAL_WIDTH_PX]}
borderRadius={2}
>
<Col
position="relative"
justifyContent="space-between"
height="100%"
width="100%"
borderRadius={2}
p={3}
bg="lightBlue"
>
<Triangle
{...triPos}
position="absolute"
size={16}
color="lightBlue"
direction={arrow}
height="0px"
width="0px"
display={['none', 'block']}
/>
<Box
right="16px"
top="16px"
position="absolute"
cursor="pointer"
onClick={tryExit}
>
<Icon icon="X" />
</Box>
<Col mb={3}>
<Text lineHeight="tall" fontWeight="bold">
{title}
</Text>
<Text fontSize={0} gray>
{progressIdx} of {progress.length - 2}
</Text>
</Col>
<Text lineHeight="tall">{description}</Text>
<Row gapX={2} mt={3} justifyContent="flex-end">
{ progressIdx > 1 && (
<Button bg="washedGray" onClick={prev}>
Back
</Button>
)}
<Button primary onClick={next}>
Next
</Button>
</Row>
</Col>
</Box>
</Portal>
);
}

View File

@ -486,7 +486,7 @@
update-core
?- mode.kind
%count (hark %unread-count place %.y 1)
%each (hark %unread-each place /(rsh 4 (scot %ui (rear index.post))))
%each (hark %unread-each place /(rsh 4 (scot %ui (rear self-idx))))
%none update-core
==
==
@ -497,7 +497,7 @@
update-core
?- mode.kind
%count (hark %unread-count place %.n 1)
%each (hark %read-each place /(rsh 4 (scot %ui (rear index.post))))
%each (hark %read-each place /(rsh 4 (scot %ui (rear self-idx))))
%none update-core
==
==

View File

@ -1,7 +1,7 @@
:~ title+'Groups'
info+'A suite of applications to communicate on Urbit'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v4.10b75.pd4h0.n481d.ln7c6.cf174.glob' 0v4.10b75.pd4h0.n481d.ln7c6.cf174]
glob-http+['https://bootstrap.urbit.org/glob-0v3.c3vkk.bvn2l.fmc76.l3ro9.0i5j0.glob' 0v3.c3vkk.bvn2l.fmc76.l3ro9.0i5j0]
base+'landscape'
version+[1 3 5]
website+'https://tlon.io'