mirror of
https://github.com/urbit/shrub.git
synced 2024-12-24 11:24:21 +03:00
Merge branch 'next/groups'
This commit is contained in:
commit
8135f32816
20
.github/workflows/ops-group-timer.yml
vendored
20
.github/workflows/ops-group-timer.yml
vendored
@ -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 }}
|
||||
|
@ -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)
|
||||
::
|
||||
|
@ -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(),
|
||||
|
@ -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',
|
||||
|
@ -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]);
|
||||
|
@ -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 it’s 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 you’d 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
|
||||
}
|
||||
};
|
@ -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]);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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]);
|
||||
|
@ -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'),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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';
|
||||
|
@ -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 “tutorial” in Leap
|
||||
</Text>
|
||||
<Row gapX={2} justifyContent="flex-end">
|
||||
<Button primary onClick={onDismiss}>Ok</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
) : (
|
||||
<Col maxWidth="350px" p={3}>
|
||||
<Box position="absolute" left="-16px" top="-16px">
|
||||
<StarIcon width="32px" height="32px" color="blue" display="block" />
|
||||
</Box>
|
||||
<Text mb={3} lineHeight="tall" fontWeight="medium">Welcome</Text>
|
||||
<Text mb={3} lineHeight="tall">
|
||||
You have been invited to use 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'
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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[] = [];
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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'>
|
||||
|
@ -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' ||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -43,7 +43,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
|
||||
useShortcut('readGroup', useCallback(() => {
|
||||
if(groupPath) {
|
||||
airlock.poke(readGroup(groupPath));
|
||||
useHarkState.getState().readGroup(groupPath);
|
||||
}
|
||||
}, [groupPath]));
|
||||
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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 "tutorial" 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>
|
||||
);
|
||||
}
|
@ -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
|
||||
==
|
||||
==
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user