mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-05 13:55:54 +03:00
Merge branch 'release/next-userspace' into la/more-fix
This commit is contained in:
commit
5530274b78
@ -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
|
||||
|
@ -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
|
||||
|
@ -23,6 +23,11 @@
|
||||
^- card
|
||||
(poke [our.bowl app] cage)
|
||||
::
|
||||
++ poke-self
|
||||
|= =cage
|
||||
^- card
|
||||
(poke-our dap.bowl cage)
|
||||
::
|
||||
++ arvo
|
||||
|= =note-arvo
|
||||
^- card
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
},
|
||||
|
55
pkg/interface/src/logic/lib/relativePosition.tsx
Normal file
55
pkg/interface/src/logic/lib/relativePosition.tsx
Normal 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[]>;
|
||||
}
|
||||
|
@ -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({
|
||||
|
133
pkg/interface/src/logic/lib/tutorialModal.ts
Normal file
133
pkg/interface/src/logic/lib/tutorialModal.ts
Normal 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 it’s 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 you’d 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,
|
||||
},
|
||||
};
|
@ -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 };
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ export function useModal(props: UseModalProps): UseModalResult {
|
||||
display="flex"
|
||||
alignItems="stretch"
|
||||
flexDirection="column"
|
||||
spacing="2"
|
||||
|
||||
>
|
||||
{inner}
|
||||
</ModalOverlay>
|
||||
|
20
pkg/interface/src/logic/lib/usePreviousValue.ts
Normal file
20
pkg/interface/src/logic/lib/usePreviousValue.ts
Normal 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!;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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'
|
||||
}));
|
||||
|
||||
|
@ -13,3 +13,4 @@ export * from './metadata-update';
|
||||
export * from './noun';
|
||||
export * from './s3-update';
|
||||
export * from './workspace';
|
||||
export * from './util';
|
||||
|
@ -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;
|
||||
|
@ -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'];
|
||||
|
@ -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}
|
||||
|
@ -137,7 +137,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
icon
|
||||
padded
|
||||
padding={2}
|
||||
/>;
|
||||
|
||||
return (
|
||||
|
@ -279,7 +279,7 @@ export const MessageWithSigil = (props) => {
|
||||
color={color}
|
||||
classes={sigilClass}
|
||||
icon
|
||||
padded
|
||||
padding={2}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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, it’s up to you to keep them safe. Be sure your ID is somewhere you won’t 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 don’t 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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -185,7 +185,7 @@ const GraphNode = ({
|
||||
icon
|
||||
color={`#000000`}
|
||||
classes="mix-blend-diff"
|
||||
padded
|
||||
padding={2}
|
||||
/>
|
||||
) : <Box style={{ width: '16px' }}></Box>;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
);
|
||||
));
|
||||
|
@ -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("");
|
||||
};
|
||||
|
@ -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'>
|
||||
|
@ -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') {
|
||||
|
21
pkg/interface/src/views/components/useTutorialModal.tsx
Normal file
21
pkg/interface/src/views/components/useTutorialModal.tsx
Normal 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;
|
||||
}
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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'>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
189
pkg/interface/src/views/landscape/components/TutorialModal.tsx
Normal file
189
pkg/interface/src/views/landscape/components/TutorialModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user