mirror of
https://github.com/urbit/shrub.git
synced 2025-01-02 17:43:32 +03:00
Merge pull request #5449 from urbit/lf/group-view-refactor
groups: refactor joining process
This commit is contained in:
commit
0af4d998c1
@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials', 'storybook-addon-designs'],
|
||||
webpackFinal: (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(j|t)sx?$/,
|
||||
|
@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import dark from '@tlon/indigo-dark';
|
||||
import light from '@tlon/indigo-light';
|
||||
import { Reset } from '@tlon/indigo-react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import '~/views/landscape/css/custom.css';
|
||||
import '~/views/css/fonts.css';
|
||||
import '~/views/apps/chat/css/custom.css';
|
||||
import '~/views/css/indigo-static.css';
|
||||
import React from "react";
|
||||
import dark from "@tlon/indigo-dark";
|
||||
import light from "@tlon/indigo-light";
|
||||
import { Reset } from "@tlon/indigo-react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import useGraphState from "~/logic/state/graph";
|
||||
import useGroupState from "~/logic/state/group";
|
||||
import useMetadataState from "~/logic/state/metadata";
|
||||
import useContactState from "~/logic/state/contact";
|
||||
import "~/views/landscape/css/custom.css";
|
||||
import "~/views/css/fonts.css";
|
||||
import "~/views/apps/chat/css/custom.css";
|
||||
import "~/views/css/indigo-static.css";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
@ -22,170 +23,197 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
const groupPreview = {
|
||||
group: "/ship/~bollug-worlus/urbit-index",
|
||||
channels: {
|
||||
"/ship/~darrux-landes/index-weekly": {
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: "",
|
||||
title: "Index Weekly",
|
||||
description: "A weekly roundup of content from around the network",
|
||||
creator: "~bollug-worlus",
|
||||
picture: "",
|
||||
hidden: false,
|
||||
config: {
|
||||
graph: "publish",
|
||||
},
|
||||
"date-created": "~2020.4.6..21.53.30..dc68",
|
||||
color: "0x0",
|
||||
},
|
||||
"app-name": "graph",
|
||||
resource: "/ship/~bollug-worlus/index-weekly",
|
||||
group: "/ship/~bollug-worlus/urbit-index",
|
||||
},
|
||||
},
|
||||
members: 1237,
|
||||
"channel-count": 3,
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: "",
|
||||
title: "Urbit Index",
|
||||
description: "A weekly roundup of content form around the network",
|
||||
creator: "~bollug-worlus",
|
||||
picture: "",
|
||||
hidden: false,
|
||||
config: {
|
||||
group: null,
|
||||
},
|
||||
"date-created": "~2020.4.6..21.53.30..dc68",
|
||||
color: "0x0",
|
||||
},
|
||||
};
|
||||
|
||||
const groupPending = (progress) => ({
|
||||
hidden: false,
|
||||
started: Date.now() - 3600,
|
||||
ship: "~bollug-worlus",
|
||||
progress,
|
||||
shareContact: false,
|
||||
autojoin: false,
|
||||
app: "groups",
|
||||
invite: [],
|
||||
});
|
||||
|
||||
export const globalTypes = {
|
||||
theme: {
|
||||
name: 'Theme',
|
||||
description: 'Global Theme for components',
|
||||
defaultValue: 'light',
|
||||
name: "Theme",
|
||||
description: "Global Theme for components",
|
||||
defaultValue: "light",
|
||||
toolbar: {
|
||||
icon: 'circlehollow',
|
||||
items: ['light', 'dark'],
|
||||
icon: "circlehollow",
|
||||
items: ["light", "dark"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story, context) => {
|
||||
window.ship = 'sampel-palnet';
|
||||
const theme = context.globals.theme === 'light' ? light : dark;
|
||||
window.ship = "sampel-palnet";
|
||||
const theme = context.globals.theme === "light" ? light : dark;
|
||||
|
||||
useContactState.setState({
|
||||
contacts: {
|
||||
'~ridlur-figbud': {
|
||||
status: 'please like and subscribe',
|
||||
'last-updated': 1616609090555,
|
||||
"~ridlur-figbud": {
|
||||
status: "please like and subscribe",
|
||||
"last-updated": 1616609090555,
|
||||
avatar: null,
|
||||
cover: null,
|
||||
bio: '',
|
||||
nickname: 'Gav',
|
||||
color: '0x26.3e0f',
|
||||
bio: "",
|
||||
nickname: "Gav",
|
||||
color: "0x26.3e0f",
|
||||
groups: [],
|
||||
},
|
||||
'~sampel-palnet': {
|
||||
status: 'A test status',
|
||||
'last-updated': 1616609090555,
|
||||
"~sampel-palnet": {
|
||||
status: "A test status",
|
||||
"last-updated": 1616609090555,
|
||||
avatar: null,
|
||||
cover: null,
|
||||
bio: '',
|
||||
nickname: 'You',
|
||||
color: '0x26.3e0f',
|
||||
bio: "",
|
||||
nickname: "You",
|
||||
color: "0x26.3e0f",
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useGroupState.setState({
|
||||
pendingJoin: {
|
||||
"/ship/~bollug-worlus/urbit-index-start": groupPending("start"),
|
||||
"/ship/~bollug-worlus/urbit-index-metadata": groupPending("metadata"),
|
||||
"/ship/~bollug-worlus/urbit-index-done": groupPending("done"),
|
||||
"/ship/~bollug-worlus/urbit-index-error": groupPending("no-perms"),
|
||||
},
|
||||
});
|
||||
|
||||
useMetadataState.setState({
|
||||
associations: {
|
||||
groups: {
|
||||
'/ship/~bitbet-bolbel/urbit-community': {
|
||||
"/ship/~bitbet-bolbel/urbit-community": {
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: '',
|
||||
title: 'Urbit Community',
|
||||
description: 'World hub, help desk, meet and greet, etc.',
|
||||
creator: '~bitbet-bolbel',
|
||||
vip: "",
|
||||
title: "Urbit Community",
|
||||
description: "World hub, help desk, meet and greet, etc.",
|
||||
creator: "~bitbet-bolbel",
|
||||
picture:
|
||||
'https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2021.4.02..21.52.41-UC.png',
|
||||
"https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2021.4.02..21.52.41-UC.png",
|
||||
hidden: false,
|
||||
config: {
|
||||
group: {
|
||||
'app-name': 'graph',
|
||||
resource: '/ship/~bitbet-bolbel/urbit-community-5.963',
|
||||
"app-name": "graph",
|
||||
resource: "/ship/~bitbet-bolbel/urbit-community-5.963",
|
||||
},
|
||||
},
|
||||
'date-created': '~2020.6.25..21.39.35..2fd2',
|
||||
color: '0x8f.9c9d',
|
||||
"date-created": "~2020.6.25..21.39.35..2fd2",
|
||||
color: "0x8f.9c9d",
|
||||
},
|
||||
'app-name': 'groups',
|
||||
resource: '/ship/~bitbet-bolbel/urbit-community',
|
||||
group: '/ship/~bitbet-bolbel/urbit-community',
|
||||
"app-name": "groups",
|
||||
resource: "/ship/~bitbet-bolbel/urbit-community",
|
||||
group: "/ship/~bitbet-bolbel/urbit-community",
|
||||
},
|
||||
},
|
||||
graph: {
|
||||
'/ship/~bitbet-bolbel/links': {
|
||||
"/ship/~bitbet-bolbel/links": {
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: '',
|
||||
title: 'Link Collection',
|
||||
description: '',
|
||||
creator: '~darrux-landes',
|
||||
picture: '',
|
||||
vip: "",
|
||||
title: "Link Collection",
|
||||
description: "",
|
||||
creator: "~darrux-landes",
|
||||
picture: "",
|
||||
hidden: false,
|
||||
config: {
|
||||
graph: 'link',
|
||||
graph: "link",
|
||||
},
|
||||
'date-created': '~2020.4.6..21.53.30..dc68',
|
||||
color: '0x0',
|
||||
"date-created": "~2020.4.6..21.53.30..dc68",
|
||||
color: "0x0",
|
||||
},
|
||||
'app-name': 'graph',
|
||||
resource: '/ship/~bitbet-bolbel/links',
|
||||
group: '/ship/~bitbet-bolbel/urbit-community',
|
||||
"app-name": "graph",
|
||||
resource: "/ship/~bitbet-bolbel/links",
|
||||
group: "/ship/~bitbet-bolbel/urbit-community",
|
||||
},
|
||||
'/ship/~darrux-landes/development': {
|
||||
"/ship/~darrux-landes/development": {
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: '',
|
||||
title: 'Development',
|
||||
vip: "",
|
||||
title: "Development",
|
||||
description:
|
||||
'Urbit Development Mailing List: https://groups.google.com/a/urbit.org/forum/#!forum/dev',
|
||||
creator: '~darrux-landes',
|
||||
picture: '',
|
||||
"Urbit Development Mailing List: https://groups.google.com/a/urbit.org/forum/#!forum/dev",
|
||||
creator: "~darrux-landes",
|
||||
picture: "",
|
||||
hidden: false,
|
||||
config: {
|
||||
graph: 'chat',
|
||||
graph: "chat",
|
||||
},
|
||||
'date-created': '~2020.4.6..21.53.30..dc68',
|
||||
color: '0x0',
|
||||
"date-created": "~2020.4.6..21.53.30..dc68",
|
||||
color: "0x0",
|
||||
},
|
||||
'app-name': 'graph',
|
||||
resource: '/ship/~darrux-landes/development',
|
||||
group: '/ship/~bitbet-bolbel/urbit-community',
|
||||
"app-name": "graph",
|
||||
resource: "/ship/~darrux-landes/development",
|
||||
group: "/ship/~bitbet-bolbel/urbit-community",
|
||||
},
|
||||
},
|
||||
},
|
||||
previews: {
|
||||
'/ship/~bollug-worlus/urbit-index': {
|
||||
group: '/ship/~bollug-worlus/urbit-index',
|
||||
channels: {
|
||||
'/ship/~darrux-landes/index-weekly': {
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: '',
|
||||
title: 'Index Weekly',
|
||||
description: '',
|
||||
creator: '~bollug-worlus',
|
||||
picture: '',
|
||||
hidden: false,
|
||||
config: {
|
||||
graph: 'publish',
|
||||
},
|
||||
'date-created': '~2020.4.6..21.53.30..dc68',
|
||||
color: '0x0',
|
||||
},
|
||||
'app-name': 'graph',
|
||||
resource: '/ship/~bollug-worlus/index-weekly',
|
||||
group: '/ship/~bollug-worlus/urbit-index',
|
||||
},
|
||||
},
|
||||
members: 1237,
|
||||
metadata: {
|
||||
preview: false,
|
||||
vip: '',
|
||||
title: 'Urbit Index',
|
||||
description: '',
|
||||
creator: '~bollug-worlus',
|
||||
picture: '',
|
||||
hidden: false,
|
||||
config: {
|
||||
group: null,
|
||||
},
|
||||
'date-created': '~2020.4.6..21.53.30..dc68',
|
||||
color: '0x0',
|
||||
},
|
||||
},
|
||||
"/ship/~bollug-worlus/urbit-index": groupPreview,
|
||||
"/ship/~bollug-worlus/urbit-index-start": groupPreview,
|
||||
"/ship/~bollug-worlus/urbit-index-metadata": groupPreview,
|
||||
"/ship/~bollug-worlus/urbit-index-done": groupPreview,
|
||||
"/ship/~bollug-worlus/urbit-index-error": groupPreview,
|
||||
},
|
||||
});
|
||||
|
||||
useContactState.setState({
|
||||
contacts: {
|
||||
'~sampel-palnet': {
|
||||
status: 'Just urbiting',
|
||||
'last-updated': 1621511447583,
|
||||
"~sampel-palnet": {
|
||||
status: "Just urbiting",
|
||||
"last-updated": 1621511447583,
|
||||
avatar: null,
|
||||
cover: null,
|
||||
bio: 'An urbit user',
|
||||
nickname: 'Sample Planet',
|
||||
color: '0xee.5432',
|
||||
bio: "An urbit user",
|
||||
nickname: "Sample Planet",
|
||||
color: "0xee.5432",
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
@ -193,27 +221,27 @@ export const decorators = [
|
||||
|
||||
useGraphState.setState({
|
||||
looseNodes: {
|
||||
'darrux-landes/development': {
|
||||
'/170141184505059416342852185329797955584': {
|
||||
"darrux-landes/development": {
|
||||
"/170141184505059416342852185329797955584": {
|
||||
post: {
|
||||
index: '/170141184505059416342852185329797955584',
|
||||
author: 'sipfyn-pidmex',
|
||||
'time-sent': 1621275183241,
|
||||
index: "/170141184505059416342852185329797955584",
|
||||
author: "sipfyn-pidmex",
|
||||
"time-sent": 1621275183241,
|
||||
signatures: [
|
||||
{
|
||||
signature:
|
||||
'0x3.9e41.4f04.3cac.786e.30c1.f4cc.8db3.9a78.0401.d16f.6301.94d0.a08a.0695.5008.02bf.0e07.a7a9.3d87.85f7.6334.e598.4ed3.5dee.58a7.cbd3.30e6.d65b.1fc9.ac62.162a.daf0.ff14.9cca.4a93.8177.0755.7b74.9d52.c0a6.b27f.9001',
|
||||
"0x3.9e41.4f04.3cac.786e.30c1.f4cc.8db3.9a78.0401.d16f.6301.94d0.a08a.0695.5008.02bf.0e07.a7a9.3d87.85f7.6334.e598.4ed3.5dee.58a7.cbd3.30e6.d65b.1fc9.ac62.162a.daf0.ff14.9cca.4a93.8177.0755.7b74.9d52.c0a6.b27f.9001",
|
||||
life: 2,
|
||||
ship: 'sipfyn-pidmex',
|
||||
ship: "sipfyn-pidmex",
|
||||
},
|
||||
],
|
||||
contents: [
|
||||
{
|
||||
text:
|
||||
'is there a way to get a bunt of a specific instantance of a tagged union? i.e. if you have `$%([%a =atom] [%b =cell])`, can you get a bunt of specifically subtype `%a`?',
|
||||
"is there a way to get a bunt of a specific instantance of a tagged union? i.e. if you have `$%([%a =atom] [%b =cell])`, can you get a bunt of specifically subtype `%a`?",
|
||||
},
|
||||
],
|
||||
hash: '0xe790.53c1.0f2b.1e1b.8c30.7d33.236c.e69e',
|
||||
hash: "0xe790.53c1.0f2b.1e1b.8c30.7d33.236c.e69e",
|
||||
},
|
||||
children: {
|
||||
root: {},
|
||||
|
@ -56,7 +56,7 @@ const commandIndex = function (currentGroup, groups, associations) {
|
||||
if (canAdd) {
|
||||
commands.push(result('Channel: Create', `/~landscape${workspace}/new`, 'Groups', null));
|
||||
}
|
||||
commands.push(result('Groups: Join', '/~landscape/join', 'Groups', null));
|
||||
commands.push(result('Groups: Join', '?join-kind=group', 'Groups', null));
|
||||
|
||||
return commands;
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Association, Group, hideGroup, JoinRequests } from '@urbit/api';
|
||||
import { Association, Group, JoinRequests, abortJoin } from '@urbit/api';
|
||||
import { useCallback } from 'react';
|
||||
import { reduce } from '../reducers/group-update';
|
||||
import _ from 'lodash';
|
||||
@ -15,7 +15,8 @@ export interface GroupState {
|
||||
[group: string]: Group;
|
||||
};
|
||||
pendingJoin: JoinRequests;
|
||||
hidePending: (group: string) => Promise<void>;
|
||||
abortJoin: (group: string) => Promise<void>;
|
||||
doneJoin: (group: string) => Promise<void>;
|
||||
}
|
||||
|
||||
// @ts-ignore investigate zustand types
|
||||
@ -24,12 +25,21 @@ const useGroupState = createState<GroupState>(
|
||||
(set, get) => ({
|
||||
groups: {},
|
||||
pendingJoin: {},
|
||||
hidePending: async (group) => {
|
||||
abortJoin: async (group) => {
|
||||
get().set((draft) => {
|
||||
delete draft.pendingJoin[group];
|
||||
});
|
||||
await api.poke(hideGroup(group));
|
||||
}
|
||||
await api.poke(abortJoin(group));
|
||||
},
|
||||
doneJoin: async (group) => {
|
||||
get().set((draft) => {
|
||||
delete draft.pendingJoin[group];
|
||||
});
|
||||
await api.poke({ app: 'group-view', mark: 'group-view-action', json: {
|
||||
done: group
|
||||
}});
|
||||
},
|
||||
|
||||
}),
|
||||
['groups'],
|
||||
[
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Invites } from '@urbit/api';
|
||||
import { deSig, Invite, Invites } from '@urbit/api';
|
||||
import { reduce } from '../reducers/invite-update';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
@ -29,3 +29,14 @@ const useInviteState = createState<InviteState>(
|
||||
);
|
||||
|
||||
export default useInviteState;
|
||||
|
||||
export function useInviteForResource(app: string, ship: string, name: string) {
|
||||
const { invites } = useInviteState();
|
||||
const matches = Object.entries(invites?.[app] || {})
|
||||
.reduce((acc, [uid, invite]) => {
|
||||
const isMatch = (invite.resource.ship === deSig(ship)
|
||||
&& invite.resource.name === name)
|
||||
return isMatch ? [invite, ...acc] : acc;
|
||||
}, [] as Invite[])
|
||||
return matches?.[0];
|
||||
}
|
||||
|
22
pkg/interface/src/stories/Join.stories.tsx
Normal file
22
pkg/interface/src/stories/Join.stories.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Story, Meta } from "@storybook/react";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
import { JoinPrompt, JoinPromptProps } from "~/views/landscape/components/Join/Join";
|
||||
|
||||
export default {
|
||||
title: "Join/Prompt",
|
||||
component: JoinPrompt,
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<JoinPromptProps> = (args) => (
|
||||
<Box backgroundColor="white" p="2" width="fit-content">
|
||||
<JoinPrompt {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Prompt = Template.bind({});
|
||||
|
||||
Prompt.args = {
|
||||
kind: 'groups',
|
||||
}
|
80
pkg/interface/src/stories/Join/Form.stories.tsx
Normal file
80
pkg/interface/src/stories/Join/Form.stories.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import { Story, Meta } from "@storybook/react";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
import { Join, JoinProps } from "~/views/landscape/components/Join/Join";
|
||||
import { withDesign } from "storybook-addon-designs";
|
||||
|
||||
export default {
|
||||
title: "Join/Form",
|
||||
component: Join,
|
||||
decorators: [withDesign],
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<JoinProps> = (args) => (
|
||||
<Box backgroundColor="white" p="2" width="fit-content">
|
||||
<Join {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Prompt = Template.bind({});
|
||||
|
||||
Prompt.args = {
|
||||
desc: {
|
||||
kind: "groups",
|
||||
group: "/ship/~bitbet-bolbel/urbit-community",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithPreview = Template.bind({});
|
||||
|
||||
WithPreview.args = {
|
||||
desc: {
|
||||
kind: "groups",
|
||||
group: "/ship/~bollug-worlus/urbit-index",
|
||||
},
|
||||
};
|
||||
|
||||
WithPreview.parameters = {
|
||||
design: {
|
||||
type: "figma",
|
||||
url:
|
||||
"https://www.figma.com/file/VxNYyFRnj8ZnqWG54VbVRM/Landscape-Baikal?node-id=1795%3A27718",
|
||||
},
|
||||
};
|
||||
|
||||
export const ProgressStart = Template.bind({});
|
||||
|
||||
ProgressStart.args = {
|
||||
desc: {
|
||||
kind: "groups",
|
||||
group: "/ship/~bollug-worlus/urbit-index-start",
|
||||
},
|
||||
};
|
||||
|
||||
export const ProgressMetadata = Template.bind({});
|
||||
|
||||
ProgressMetadata.args = {
|
||||
desc: {
|
||||
kind: "groups",
|
||||
group: "/ship/~bollug-worlus/urbit-index-metadata",
|
||||
},
|
||||
};
|
||||
|
||||
export const Finished = Template.bind({});
|
||||
|
||||
Finished.args = {
|
||||
desc: {
|
||||
kind: "groups",
|
||||
group: "/ship/~bollug-worlus/urbit-index-done",
|
||||
},
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
|
||||
Error.args = {
|
||||
desc: {
|
||||
kind: "groups",
|
||||
group: "/ship/~bollug-worlus/urbit-index-error",
|
||||
},
|
||||
};
|
0
pkg/interface/src/stories/Join/Progress.stories.tsx
Normal file
0
pkg/interface/src/stories/Join/Progress.stories.tsx
Normal file
@ -1,19 +1,18 @@
|
||||
/* eslint-disable max-lines-per-function */
|
||||
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 useHarkState from '~/logic/state/hark';
|
||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||
import { JoinGroup } from '~/views/landscape/components/JoinGroup';
|
||||
import { NewGroup } from '~/views/landscape/components/NewGroup';
|
||||
import Groups from './components/Groups';
|
||||
import ModalButton from './components/ModalButton';
|
||||
import Tiles from './components/tiles';
|
||||
import Tile from './components/tiles/tile';
|
||||
import { Invite } from './components/Invite';
|
||||
import './css/custom.css';
|
||||
import { Box, Icon, Row, Text, Button } from "@tlon/indigo-react";
|
||||
import React, { ReactElement } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Route, useHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import useHarkState from "~/logic/state/hark";
|
||||
import useSettingsState, { selectCalmState } from "~/logic/state/settings";
|
||||
import Groups from "./components/Groups";
|
||||
import { NewGroup } from "~/views/landscape/components/NewGroup";
|
||||
import ModalButton from "./components/ModalButton";
|
||||
import Tiles from "./components/tiles";
|
||||
import Tile from "./components/tiles/tile";
|
||||
import "./css/custom.css";
|
||||
import { Join, JoinRoute } from "~/views/landscape/components/Join/Join";
|
||||
|
||||
const ScrollbarLessBox = styled(Box)`
|
||||
scrollbar-width: none !important;
|
||||
@ -28,73 +27,85 @@ interface LaunchAppProps {
|
||||
}
|
||||
|
||||
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
const notificationsCount = useHarkState((state) => state.notificationsCount);
|
||||
const calmState = useSettingsState(selectCalmState);
|
||||
const { hideUtilities, hideGroups } = calmState;
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Groups</title>
|
||||
<title>
|
||||
{notificationsCount ? `(${String(notificationsCount)}) ` : ""}Groups
|
||||
</title>
|
||||
</Helmet>
|
||||
<Route path="/invites/:app/:uid">
|
||||
<Invite />
|
||||
<Route path="/join/:ship/:name">
|
||||
<JoinRoute modal />
|
||||
</Route>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||
<ScrollbarLessBox
|
||||
height="100%"
|
||||
overflowY="scroll"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
>
|
||||
<Box
|
||||
mx={2}
|
||||
display='grid'
|
||||
gridTemplateColumns='repeat(auto-fill, minmax(128px, 1fr))'
|
||||
display="grid"
|
||||
gridTemplateColumns="repeat(auto-fill, minmax(128px, 1fr))"
|
||||
gridGap={3}
|
||||
p={2}
|
||||
pt={0}
|
||||
>
|
||||
{!hideUtilities && <>
|
||||
<Tile
|
||||
bg="white"
|
||||
color="scales.black20"
|
||||
to="/~landscape/home"
|
||||
p={0}
|
||||
>
|
||||
<Box
|
||||
p={2}
|
||||
height='100%'
|
||||
width='100%'
|
||||
bg='scales.black20'
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
>
|
||||
<Row alignItems='center'>
|
||||
<Icon
|
||||
color="black"
|
||||
icon="Home"
|
||||
/>
|
||||
<Text ml={2} mt='1px' color="black">My Channels</Text>
|
||||
</Row>
|
||||
</Box>
|
||||
</Tile>
|
||||
<Tiles />
|
||||
<ModalButton
|
||||
icon="Plus"
|
||||
bg="washedGray"
|
||||
color="black"
|
||||
text="New Group"
|
||||
style={{ gridColumnStart: 1 }}
|
||||
>
|
||||
<NewGroup />
|
||||
</ModalButton>
|
||||
<ModalButton
|
||||
icon="BootNode"
|
||||
bg="washedGray"
|
||||
color="black"
|
||||
text="Join Group"
|
||||
>
|
||||
{dismiss => <JoinGroup dismiss={dismiss} />}
|
||||
</ModalButton>
|
||||
</>}
|
||||
{!hideGroups &&
|
||||
(<Groups />)
|
||||
}
|
||||
{!hideUtilities && (
|
||||
<>
|
||||
<Tile
|
||||
bg="white"
|
||||
color="scales.black20"
|
||||
to="/~landscape/home"
|
||||
p={0}
|
||||
>
|
||||
<Box
|
||||
p={2}
|
||||
height="100%"
|
||||
width="100%"
|
||||
bg="scales.black20"
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
>
|
||||
<Row alignItems="center">
|
||||
<Icon color="black" icon="Home" />
|
||||
<Text ml={2} mt="1px" color="black">
|
||||
My Channels
|
||||
</Text>
|
||||
</Row>
|
||||
</Box>
|
||||
</Tile>
|
||||
<Tiles />
|
||||
<ModalButton
|
||||
icon="Plus"
|
||||
bg="washedGray"
|
||||
color="black"
|
||||
text="New Group"
|
||||
style={{ gridColumnStart: 1 }}
|
||||
>
|
||||
<NewGroup />
|
||||
</ModalButton>
|
||||
<Button
|
||||
backgroundColor="washedGray"
|
||||
color="black"
|
||||
border={0}
|
||||
p={0}
|
||||
borderRadius={2}
|
||||
onClick={() => history.push({ search: "?join-kind=group" })}
|
||||
>
|
||||
<Row gapX="2" p={2} height="100%" width="100%" alignItems="center">
|
||||
<Icon icon="BootNode" />
|
||||
<Text fontWeight="medium" whiteSpace="nowrap">Join Group</Text>
|
||||
</Row>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!hideGroups && <Groups />}
|
||||
</Box>
|
||||
</ScrollbarLessBox>
|
||||
</>
|
||||
|
@ -1,16 +1,27 @@
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Association, Associations, Unreads } from '@urbit/api';
|
||||
import f from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { getNotificationCount } from '~/logic/lib/hark';
|
||||
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 { Box, Col, Text } from "@tlon/indigo-react";
|
||||
import {
|
||||
Association,
|
||||
Associations,
|
||||
resourceAsPath,
|
||||
resourceFromPath,
|
||||
Unreads,
|
||||
} from "@urbit/api";
|
||||
import f from "lodash/fp";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { getNotificationCount } from "~/logic/lib/hark";
|
||||
import { alphabeticalOrder } from "~/logic/lib/util";
|
||||
import useGroupState from "~/logic/state/group";
|
||||
import useHarkState, { selHarkGraph } from "~/logic/state/hark";
|
||||
import useInviteState from "~/logic/state/invite";
|
||||
import useMetadataState, { usePreview } from "~/logic/state/metadata";
|
||||
import useSettingsState, {
|
||||
selectCalmState
|
||||
} from '~/logic/state/settings';
|
||||
import Tile from '../components/tiles/tile';
|
||||
selectCalmState,
|
||||
SettingsState,
|
||||
} from "~/logic/state/settings";
|
||||
import Tile from "../components/tiles/tile";
|
||||
import { useQuery } from "~/logic/lib/useQuery";
|
||||
|
||||
const sortGroupsAlph = (a: Association, b: Association) =>
|
||||
alphabeticalOrder(a.metadata.title, b.metadata.title);
|
||||
@ -25,7 +36,7 @@ const getGraphUnreads = (associations: Associations) => {
|
||||
return (path: string) =>
|
||||
f.flow(
|
||||
f.pickBy((a: Association) => a.group === path),
|
||||
f.map('resource'),
|
||||
f.map("resource"),
|
||||
f.map(selUnread),
|
||||
f.reduce(f.add, 0)
|
||||
)(associations.graph);
|
||||
@ -37,18 +48,18 @@ const getGraphNotifications = (
|
||||
) => (path: string) =>
|
||||
f.flow(
|
||||
f.pickBy((a: Association) => a.group === path),
|
||||
f.map('resource'),
|
||||
f.map(rid => getNotificationCount(unreads, rid)),
|
||||
f.map("resource"),
|
||||
f.map((rid) => getNotificationCount(unreads, rid)),
|
||||
f.reduce(f.add, 0)
|
||||
)(associations.graph);
|
||||
|
||||
export default function Groups(props: Parameters<typeof Box>[0]) {
|
||||
const unreads = useHarkState(state => state.unreads);
|
||||
const groupState = useGroupState(state => state.groups);
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const unreads = useHarkState((state) => state.unreads);
|
||||
const groupState = useGroupState((state) => state.groups);
|
||||
const associations = useMetadataState((state) => state.associations);
|
||||
|
||||
const groups = Object.values(associations?.groups || {})
|
||||
.filter(e => e?.group in groupState)
|
||||
.filter((e) => e?.group in groupState)
|
||||
.sort(sortGroupsAlph);
|
||||
const graphUnreads = getGraphUnreads(associations || ({} as Associations));
|
||||
const graphNotifications = getGraphNotifications(
|
||||
@ -56,6 +67,20 @@ export default function Groups(props: Parameters<typeof Box>[0]) {
|
||||
unreads
|
||||
);
|
||||
|
||||
const joining = useGroupState((s) =>
|
||||
_.omit(
|
||||
_.pickBy(s.pendingJoin || {}, req => req.app === 'groups'),
|
||||
groups.map((g) => g.group)
|
||||
)
|
||||
);
|
||||
const invites = useInviteState(
|
||||
(s) =>
|
||||
Object.values(s.invites?.["groups"] || {}).map((inv) =>
|
||||
resourceAsPath(inv.resource)
|
||||
) || []
|
||||
);
|
||||
const pending = _.union(invites, Object.keys(joining));
|
||||
|
||||
return (
|
||||
<>
|
||||
{groups.map((group, index) => {
|
||||
@ -73,10 +98,60 @@ export default function Groups(props: Parameters<typeof Box>[0]) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{pending.map((group, idx) => (
|
||||
<PendingGroup
|
||||
key={group}
|
||||
path={group}
|
||||
first={idx === 0 && groups.length === 0}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface PendingGroupProps {
|
||||
path: string;
|
||||
first?: boolean;
|
||||
}
|
||||
|
||||
function PendingGroup(props: PendingGroupProps) {
|
||||
const { path, first } = props;
|
||||
const history = useHistory();
|
||||
const { preview, error } = usePreview(path);
|
||||
const title = preview?.metadata?.title || path;
|
||||
const { toQuery } = useQuery();
|
||||
const onClick = () => {
|
||||
const { ship, name } = resourceFromPath(path);
|
||||
history.push(toQuery({ "join-kind": "groups", "join-path": path }));
|
||||
};
|
||||
|
||||
const joining = useGroupState((s) => s.pendingJoin[path]?.progress);
|
||||
|
||||
return (
|
||||
<Tile gridColumnStart={first ? 1 : undefined}>
|
||||
<Col
|
||||
onClick={onClick}
|
||||
width="100%"
|
||||
height="100%"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box>
|
||||
<Text gray>{title}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
{!joining ? (
|
||||
<Text color="blue">Invited</Text>
|
||||
) : joining !== "done" ? (
|
||||
<Text gray>Joining...</Text>
|
||||
) : (
|
||||
<Text color="blue">Recently joined</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Col>
|
||||
</Tile>
|
||||
);
|
||||
}
|
||||
|
||||
interface GroupProps {
|
||||
path: string;
|
||||
title: string;
|
||||
@ -87,6 +162,7 @@ interface GroupProps {
|
||||
function Group(props: GroupProps) {
|
||||
const { path, title, unreads, updates, first = false } = props;
|
||||
const { hideUnreads } = useSettingsState(selectCalmState);
|
||||
const request = useGroupState((s) => s.pendingJoin[path]);
|
||||
return (
|
||||
<Tile
|
||||
position="relative"
|
||||
@ -97,9 +173,10 @@ function Group(props: GroupProps) {
|
||||
<Text>{title}</Text>
|
||||
{!hideUnreads && (
|
||||
<Col>
|
||||
{!!request ? <Text color="blue">New group</Text> : null}
|
||||
{updates > 0 && (
|
||||
<Text mt={1} color="blue">
|
||||
{updates} update{updates !== 1 && 's'}{' '}
|
||||
{updates} update{updates !== 1 && "s"}{" "}
|
||||
</Text>
|
||||
)}
|
||||
{unreads > 0 && <Text color="lightGray">{unreads}</Text>}
|
||||
|
@ -108,7 +108,6 @@ function useInviteAccept(resource: string, app?: string, uid?: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await airlock.poke(join(ship, name));
|
||||
await waiter((p) => {
|
||||
return (
|
||||
(resource in p.groups &&
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Box, Row, SegmentedProgressBar, Text } from '@tlon/indigo-react';
|
||||
import { joinError, joinProgress, JoinRequest, hideGroup } from '@urbit/api';
|
||||
import { joinError, joinProgress, JoinRequest } from '@urbit/api';
|
||||
import React, { useCallback } from 'react';
|
||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||
import airlock from '~/logic/api';
|
||||
@ -24,10 +24,8 @@ export function JoiningStatus(props: JoiningStatusProps) {
|
||||
const desc = description?.[current] || '';
|
||||
const isError = joinError.indexOf(status.progress as any) !== -1;
|
||||
const onHide = useCallback(
|
||||
async () => {
|
||||
await airlock.poke(hideGroup(resource));
|
||||
},
|
||||
[resource]
|
||||
async () => { },
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<Row
|
||||
|
@ -21,6 +21,7 @@ export interface AuthorProps {
|
||||
lineHeight?: string | number;
|
||||
isRelativeTime?: boolean;
|
||||
dontShowTime?: boolean;
|
||||
gray?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
@ -35,6 +36,7 @@ function Author(props: AuthorProps & PropFunc<typeof Box>): ReactElement {
|
||||
isRelativeTime,
|
||||
dontShowTime,
|
||||
lineHeight = 'tall',
|
||||
gray = false,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@ -88,7 +90,7 @@ function Author(props: AuthorProps & PropFunc<typeof Box>): ReactElement {
|
||||
<Box display='flex' alignItems='baseline'>
|
||||
<Text
|
||||
ml={showImage ? 2 : 0}
|
||||
color='black'
|
||||
color={gray ? 'gray': 'black'}
|
||||
fontSize='1'
|
||||
cursor='pointer'
|
||||
lineHeight={lineHeight}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useModal } from '~/logic/lib/useModal';
|
||||
import useMetadataState, { usePreview } from '~/logic/state/metadata';
|
||||
import { PropFunc } from '~/types';
|
||||
import { JoinGroup } from '../landscape/components/JoinGroup';
|
||||
import { MetadataIcon } from '../landscape/components/MetadataIcon';
|
||||
|
||||
type GroupLinkProps = {
|
||||
@ -22,61 +20,44 @@ export function GroupLink({
|
||||
useCallback(s => resource in s.associations.groups, [resource])
|
||||
);
|
||||
|
||||
const { modal, showModal } = useModal({
|
||||
modal: <JoinGroup autojoin={name} />
|
||||
});
|
||||
|
||||
const { preview } = usePreview(resource);
|
||||
const { preview } = usePreview(resource);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modal}
|
||||
<Row
|
||||
{...rest}
|
||||
as={Link}
|
||||
to={joined ? `/~landscape/ship/${name}` : `/perma/group/${name}`}
|
||||
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
showModal();
|
||||
}}
|
||||
flexShrink={1}
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
maxWidth="500px"
|
||||
py={2}
|
||||
pr={2}
|
||||
cursor='pointer'
|
||||
backgroundColor='white'
|
||||
borderColor={borderColor}
|
||||
opacity={preview ? '1' : '0.6'}
|
||||
>
|
||||
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
|
||||
<Col>
|
||||
<Text ml={2} fontWeight="medium" mono={!preview}>
|
||||
{preview ? preview.metadata.title : name}
|
||||
</Text>
|
||||
<Box pt='1' ml='2' display='flex' alignItems='center'>
|
||||
{preview ?
|
||||
<>
|
||||
<Box display='flex' alignItems='center'>
|
||||
<Icon icon='Users' color='gray' mr='1' />
|
||||
<Text fontSize='0'color='gray' >
|
||||
{preview.members}
|
||||
{' '}
|
||||
{preview.members > 1 ? 'peers' : 'peer'}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
: <Text fontSize='0'>Fetching member count</Text>}
|
||||
</Box>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
<Row
|
||||
{...rest}
|
||||
as={Link}
|
||||
to={joined ? `/~landscape/ship/${name}` : { search: `?join-kind=groups&join-path=/ship/${name}`}}
|
||||
flexShrink={1}
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
maxWidth="500px"
|
||||
py={2}
|
||||
pr={2}
|
||||
cursor='pointer'
|
||||
backgroundColor='white'
|
||||
borderColor={borderColor}
|
||||
opacity={preview ? '1' : '0.6'}
|
||||
>
|
||||
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
|
||||
<Col>
|
||||
<Text ml={2} fontWeight="medium" mono={!preview}>
|
||||
{preview ? preview.metadata.title : name}
|
||||
</Text>
|
||||
<Box pt='1' ml='2' display='flex' alignItems='center'>
|
||||
{preview ?
|
||||
<>
|
||||
<Box display='flex' alignItems='center'>
|
||||
<Icon icon='Users' color='gray' mr='1' />
|
||||
<Text fontSize='0'color='gray' >
|
||||
{preview.members}
|
||||
{' '}
|
||||
{preview.members > 1 ? 'peers' : 'peer'}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
: <Text fontSize='0'>Fetching member count</Text>}
|
||||
</Box>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ import { Box, Icon, LoadingSpinner, Row, Text } from '@tlon/indigo-react';
|
||||
import {
|
||||
accept,
|
||||
decline,
|
||||
hideGroup,
|
||||
Invite,
|
||||
join,
|
||||
joinProgress,
|
||||
joinResult,
|
||||
JoinRequest,
|
||||
@ -170,7 +168,6 @@ export function useInviteAccept(resource: string, app?: string, uid?: string) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await airlock.poke(join(ship, name));
|
||||
await airlock.poke(accept(app, uid));
|
||||
await waiter((p) => {
|
||||
return (
|
||||
@ -218,9 +215,6 @@ function InviteActions(props: {
|
||||
await airlock.poke(decline(app, uid));
|
||||
}, [app, uid]);
|
||||
|
||||
const hideJoin = useCallback(async () => {
|
||||
await airlock.poke(hideGroup(resource));
|
||||
}, [resource]);
|
||||
|
||||
if (status) {
|
||||
return (
|
||||
@ -228,7 +222,7 @@ function InviteActions(props: {
|
||||
<StatelessAsyncButton
|
||||
height={4}
|
||||
backgroundColor="white"
|
||||
onClick={hideJoin}
|
||||
onClick={async () => {}}
|
||||
>
|
||||
{[...joinResult].includes(status?.progress as any)
|
||||
? 'Dismiss'
|
||||
@ -289,7 +283,6 @@ export function GroupInvite(props: GroupInviteProps): ReactElement {
|
||||
if (status?.progress === 'done') {
|
||||
const redir = inviteUrl(app !== 'groups', resource, graphAssoc?.metadata);
|
||||
if (redir) {
|
||||
airlock.poke(hideGroup(resource));
|
||||
history.push(redir);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import { Dropdown } from './Dropdown';
|
||||
import { ProfileStatus } from './ProfileStatus';
|
||||
import ReconnectButton from './ReconnectButton';
|
||||
import { StatusBarItem } from './StatusBarItem';
|
||||
import { StatusBarJoins } from './StatusBarJoins';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
const localSel = selectLocalState(['toggleOmnibox']);
|
||||
@ -83,7 +82,6 @@ const StatusBar = (props) => {
|
||||
</Box>
|
||||
)}
|
||||
</StatusBarItem>
|
||||
<StatusBarJoins />
|
||||
<ReconnectButton />
|
||||
</Row>
|
||||
<Row justifyContent='flex-end'>
|
||||
|
@ -1,119 +0,0 @@
|
||||
import { LoadingSpinner, Button } from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import { Box, Row, Col, Text } from '@tlon/indigo-react';
|
||||
import { PropFunc } from '~/types';
|
||||
import _ from 'lodash';
|
||||
import { StatusBarItem } from './StatusBarItem';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import { JoinRequest, joinProgress } from '@urbit/api';
|
||||
import { usePreview } from '~/logic/state/metadata';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { MetadataIcon } from '../landscape/components/MetadataIcon';
|
||||
|
||||
function Elbow(
|
||||
props: { size?: number; color?: string } & PropFunc<typeof Box>
|
||||
) {
|
||||
const { size = 12, color = 'lightGray', ...rest } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...rest}
|
||||
overflow="hidden"
|
||||
width={size}
|
||||
height={size}
|
||||
position="relative"
|
||||
>
|
||||
<Box
|
||||
border="2px solid"
|
||||
borderRadius={3}
|
||||
borderColor={color}
|
||||
position="absolute"
|
||||
left="0px"
|
||||
bottom="0px"
|
||||
width={size * 2}
|
||||
height={size * 2}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatusBarJoins() {
|
||||
const pendingJoin = useGroupState(s => s.pendingJoin);
|
||||
if (
|
||||
Object.keys(_.omitBy(pendingJoin, j => j.hidden || j.progress === 'done'))
|
||||
.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropWidth="325px"
|
||||
options={
|
||||
<Col
|
||||
left="0px"
|
||||
top="120%"
|
||||
position="absolute"
|
||||
zIndex={10}
|
||||
alignItems="flex-start"
|
||||
p="2"
|
||||
gapY="3"
|
||||
border="1"
|
||||
borderColor="lightGray"
|
||||
borderRadius="1"
|
||||
backgroundColor="white"
|
||||
>
|
||||
{Object.keys(pendingJoin).map(g => (
|
||||
<JoinStatus key={g} group={g} join={pendingJoin[g]} />
|
||||
))}
|
||||
</Col>
|
||||
}
|
||||
alignX="left"
|
||||
alignY="bottom"
|
||||
>
|
||||
<StatusBarItem mr="2" width="32px" flexShrink={0} border={0}>
|
||||
<LoadingSpinner foreground="black" />
|
||||
</StatusBarItem>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const description: string[] = [
|
||||
'Contacting host...',
|
||||
'Retrieving data...',
|
||||
'Finished join',
|
||||
'Unable to join, you do not have the correct permissions',
|
||||
'Internal error, please file an issue'
|
||||
];
|
||||
|
||||
export function JoinStatus({
|
||||
group,
|
||||
join
|
||||
}: {
|
||||
group: string;
|
||||
join: JoinRequest;
|
||||
}) {
|
||||
const { preview } = usePreview(group);
|
||||
const current = join && joinProgress.indexOf(join.progress);
|
||||
const desc = _.isNumber(current) && description[current];
|
||||
const onHide = () => {
|
||||
useGroupState.getState().hidePending(group);
|
||||
};
|
||||
return (
|
||||
<Row alignItems="center" gapX="3">
|
||||
<Col gapY="2">
|
||||
<Row alignItems="center" gapX="2">
|
||||
{preview ? (
|
||||
<MetadataIcon height={4} width={4} metadata={preview.metadata} />
|
||||
) : null}
|
||||
<Text>{preview?.metadata.title || group.slice(6)}</Text>
|
||||
</Row>
|
||||
<Row ml="2" alignItems="center" gapX="2">
|
||||
<Elbow />
|
||||
<Text>{desc}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
<Button onClick={onHide}>Hide</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
@ -167,8 +167,15 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
if(shift && app === 'profile') {
|
||||
// TODO: hacky, fix
|
||||
link = link.replace('~profile', '~landscape/messages/dm');
|
||||
}
|
||||
if(link.startsWith('?')) {
|
||||
history.push({
|
||||
search: link
|
||||
});
|
||||
} else {
|
||||
history.push(link);
|
||||
|
||||
}
|
||||
history.push(link);
|
||||
} else {
|
||||
window.location.href = link;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { useShortcut } from '~/logic/state/settings';
|
||||
import Landscape from '~/views/landscape/index';
|
||||
import GraphApp from '../../apps/graph/App';
|
||||
import { getNotificationRedirect } from '~/logic/lib/notificationRedirects';
|
||||
import {JoinRoute} from './Join/Join';
|
||||
|
||||
export const Container = styled(Box)`
|
||||
flex-grow: 1;
|
||||
@ -68,11 +69,11 @@ export const Content = (props) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<JoinRoute />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={['/', '/invites/:app/:uid']}
|
||||
render={p => (
|
||||
path="/" render={p => (
|
||||
<LaunchApp
|
||||
location={p.location}
|
||||
match={p.match}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Col, Row, Text, Icon } from '@tlon/indigo-react';
|
||||
import { Metadata } from '@urbit/api';
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import { PropFunc, IconRef } from '~/types';
|
||||
import { MetadataIcon } from './MetadataIcon';
|
||||
import { useCopy } from '~/logic/lib/useCopy';
|
||||
import { Col, Row, Text, Icon } from "@tlon/indigo-react";
|
||||
import { Metadata } from "@urbit/api";
|
||||
import React, { ReactElement, ReactNode } from "react";
|
||||
import { PropFunc, IconRef } from "~/types";
|
||||
import { MetadataIcon } from "./MetadataIcon";
|
||||
import { useCopy } from "~/logic/lib/useCopy";
|
||||
interface GroupSummaryProps {
|
||||
metadata: Metadata;
|
||||
memberCount: number;
|
||||
@ -28,11 +28,12 @@ export function GroupSummary(
|
||||
} = props;
|
||||
const { doCopy, copyDisplay } = useCopy(
|
||||
`web+urbitgraph://group${resource?.slice(5)}`,
|
||||
'Copy',
|
||||
'Checkmark'
|
||||
"Copy",
|
||||
"Checkmark"
|
||||
);
|
||||
|
||||
return (
|
||||
<Col {...rest} gapY={4} maxWidth={['100%', '288px']}>
|
||||
<Col {...rest} gapY={4} maxWidth={["100%", "288px"]}>
|
||||
<Row gapX={2} width="100%">
|
||||
<MetadataIcon
|
||||
width="40px"
|
||||
@ -53,9 +54,9 @@ export function GroupSummary(
|
||||
{props?.AllowCopy && (
|
||||
<Icon
|
||||
color="gray"
|
||||
icon={props?.locked ? 'Locked' : (copyDisplay as IconRef)}
|
||||
icon={props?.locked ? "Locked" : (copyDisplay as IconRef)}
|
||||
onClick={!props?.locked ? doCopy : null}
|
||||
cursor={props?.locked ? 'default' : 'pointer'}
|
||||
cursor={props?.locked ? "default" : "pointer"}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
@ -69,8 +70,8 @@ export function GroupSummary(
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row width="100%">
|
||||
{metadata.description && (
|
||||
{metadata.description.length > 0 && (
|
||||
<Row width="100%">
|
||||
<Text
|
||||
gray
|
||||
width="100%"
|
||||
@ -80,8 +81,8 @@ export function GroupSummary(
|
||||
>
|
||||
{metadata.description}
|
||||
</Text>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
)}
|
||||
{children}
|
||||
</Col>
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import { readGroup } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import {
|
||||
Route,
|
||||
RouteComponentProps, Switch
|
||||
@ -25,7 +26,7 @@ import { NewChannel } from './NewChannel';
|
||||
import { PopoverRoutes } from './PopoverRoutes';
|
||||
import { Resource } from './Resource';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import airlock from '~/logic/api';
|
||||
import {Join, JoinRoute} from './Join/Join';
|
||||
|
||||
interface GroupsPaneProps {
|
||||
baseUrl: string;
|
||||
@ -59,6 +60,13 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
if (workspace.type !== 'group') {
|
||||
return;
|
||||
}
|
||||
const { pendingJoin, doneJoin } = useGroupState.getState();
|
||||
const group = getGroupFromWorkspace(workspace)!;
|
||||
if(group in pendingJoin) {
|
||||
doneJoin(group);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
setRecentGroups(gs => _.uniq([workspace.group, ...gs]));
|
||||
};
|
||||
@ -175,7 +183,31 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
<Route
|
||||
path={relativePath('/pending/:ship/:name')}
|
||||
render={(routeProps) => {
|
||||
const { ship, name } = routeProps.match.params as Record<string, string>;
|
||||
const desc = {
|
||||
group: `/ship/${ship}/${name}`,
|
||||
kind: 'graph' as const
|
||||
};
|
||||
return (<Skeleton
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<Box width="100%">
|
||||
<Join desc={desc} />
|
||||
</Box>
|
||||
</Skeleton>
|
||||
)
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
</Route>
|
||||
<Route
|
||||
path={relativePath('/new')}
|
||||
render={(routeProps) => {
|
||||
|
341
pkg/interface/src/views/landscape/components/Join/Join.tsx
Normal file
341
pkg/interface/src/views/landscape/components/Join/Join.tsx
Normal file
@ -0,0 +1,341 @@
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Text,
|
||||
Box,
|
||||
Button,
|
||||
ManagedTextInputField,
|
||||
ManagedCheckboxField,
|
||||
ContinuousProgressBar,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import React, { useEffect } from "react";
|
||||
import { useHistory, useLocation, useParams } from "react-router-dom";
|
||||
import useGroupState from "~/logic/state/group";
|
||||
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
|
||||
import useMetadataState, { usePreview } from "~/logic/state/metadata";
|
||||
import { Invite } from "@urbit/api";
|
||||
import { join, JoinRequest } from "@urbit/api/groups";
|
||||
import airlock from "~/logic/api";
|
||||
import { joinError, joinResult, joinLoad, JoinProgress } from "@urbit/api";
|
||||
import { useQuery } from "~/logic/lib/useQuery";
|
||||
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
|
||||
|
||||
interface FormSchema {
|
||||
autojoin: boolean;
|
||||
shareContact: boolean;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
autojoin: false,
|
||||
shareContact: false,
|
||||
};
|
||||
|
||||
function JoinForm(props: {
|
||||
desc: JoinDesc;
|
||||
dismiss: () => void;
|
||||
invite?: Invite;
|
||||
}) {
|
||||
const { desc, dismiss, invite } = props;
|
||||
const onSubmit = (values: FormSchema) => {
|
||||
const [, , ship, name] = desc.group.split("/");
|
||||
airlock.poke(
|
||||
join(ship, name, desc.kind, values.autojoin, values.shareContact)
|
||||
);
|
||||
};
|
||||
const isGroups = desc.kind === "groups";
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||
<Form>
|
||||
<Col p="4" gapY="4">
|
||||
{isGroups ? (<ManagedCheckboxField id="autojoin" label="Join all channels" />) : null}
|
||||
<ManagedCheckboxField id="shareContact" label="Share identity" />
|
||||
<Row gapX="2">
|
||||
<Button onClick={dismiss}>Dismiss</Button>
|
||||
<Button primary type="submit">
|
||||
{!invite ? "Join Group" : "Accept Invite"}
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
const REQUEST: JoinDesc = {
|
||||
group: "/ship/~bitbet-bolbel/urbit-community",
|
||||
kind: "groups",
|
||||
};
|
||||
|
||||
export function JoinInitial(props: {
|
||||
invite?: Invite;
|
||||
desc: JoinDesc;
|
||||
modal: boolean;
|
||||
dismiss: () => void;
|
||||
}) {
|
||||
const { desc, dismiss, modal, invite } = props;
|
||||
const title = (() => {
|
||||
const name = desc.kind === "graph" ? "Group Chat" : "Group";
|
||||
if (invite) {
|
||||
return `You've been invited to a ${name}`;
|
||||
} else {
|
||||
return `You're joining a ${name}`;
|
||||
}
|
||||
})();
|
||||
return (
|
||||
<JoinSkeleton modal={modal} desc={desc} title={title}>
|
||||
<JoinForm invite={invite} dismiss={dismiss} desc={desc} />
|
||||
</JoinSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
function JoinLoading(props: {
|
||||
desc: JoinDesc;
|
||||
modal: boolean;
|
||||
request: JoinRequest;
|
||||
dismiss: () => void;
|
||||
finished: string;
|
||||
}) {
|
||||
const { desc, request, dismiss, modal, finished } = props;
|
||||
const history = useHistory();
|
||||
useEffect(() => {
|
||||
if (request.progress === "done") {
|
||||
history.push(finished);
|
||||
}
|
||||
}, [request]);
|
||||
const name = desc.kind === "graph" ? "Group Chat" : "Group";
|
||||
const title = `Joining ${name}, please wait`;
|
||||
const onCancel = () => {
|
||||
useGroupState.getState().abortJoin(desc.group);
|
||||
dismiss();
|
||||
};
|
||||
return (
|
||||
<JoinSkeleton modal={modal} desc={desc} title={title}>
|
||||
<Col maxWidth="512px" p="4" gapY="4">
|
||||
{joinLoad.indexOf(request.progress as any) !== -1 ? (
|
||||
<JoinProgressIndicator progress={request.progress} />
|
||||
) : null}
|
||||
<Box>
|
||||
<Text>
|
||||
If join seems to take a while, the host of the {name} may be
|
||||
offline, or the connection between you both may be unstable.
|
||||
</Text>
|
||||
</Box>
|
||||
<Row gapX="2">
|
||||
<Button onClick={dismiss}>Dismiss</Button>
|
||||
<Button destructive onClick={onCancel}>
|
||||
Cancel Join
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</JoinSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
function JoinError(props: {
|
||||
desc: JoinDesc;
|
||||
request: JoinRequest;
|
||||
modal: boolean;
|
||||
}) {
|
||||
const { desc, request, modal } = props;
|
||||
const { preview } = usePreview(desc.group);
|
||||
const group = preview?.metadata?.title ?? desc.group;
|
||||
const title = `Joining ${group} failed`;
|
||||
const explanation =
|
||||
request.progress === "no-perms"
|
||||
? "You do not have the correct permissions"
|
||||
: "An unexpected error occurred";
|
||||
|
||||
return (
|
||||
<JoinSkeleton modal={modal} title={title} desc={desc}>
|
||||
<Col p="4" gapY="4">
|
||||
<Text fontWeight="medium">{explanation}</Text>
|
||||
<Row>
|
||||
<Button>Dismiss</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</JoinSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
export interface JoinProps {
|
||||
desc: JoinDesc;
|
||||
modal?: boolean;
|
||||
dismiss?: () => void;
|
||||
}
|
||||
|
||||
export function Join(props: JoinProps) {
|
||||
const { desc, modal, dismiss } = props;
|
||||
const { group, kind } = desc;
|
||||
const [, , ship, name] = group.split("/");
|
||||
const graph = kind === "graph";
|
||||
const finishedPath = graph
|
||||
? `/~landscape/messages/resource/chat/${ship}/${name}`
|
||||
: `/~landscape/ship/${ship}/${name}`;
|
||||
|
||||
const history = useHistory();
|
||||
const joinRequest = useGroupState((s) => s.pendingJoin[group]);
|
||||
const invite = useInviteForResource(kind, ship, name);
|
||||
|
||||
const isDone = joinRequest && joinRequest.progress === "done";
|
||||
const isErrored =
|
||||
joinRequest && joinError.includes(joinRequest.progress as any);
|
||||
const isLoading =
|
||||
joinRequest && joinLoad.includes(joinRequest.progress as any);
|
||||
|
||||
useEffect(() => {
|
||||
if(isDone && desc.kind === 'graph') {
|
||||
history.push(finishedPath);
|
||||
}
|
||||
}, [isDone, desc]);
|
||||
|
||||
return isDone ? (
|
||||
<JoinDone modal={modal} desc={desc} />
|
||||
) : isLoading ? (
|
||||
<JoinLoading
|
||||
modal={modal}
|
||||
dismiss={dismiss}
|
||||
desc={desc}
|
||||
request={joinRequest}
|
||||
finished={finishedPath}
|
||||
/>
|
||||
) : isErrored ? (
|
||||
<JoinError modal={modal} desc={desc} request={joinRequest} />
|
||||
) : (
|
||||
<JoinInitial modal={modal} dismiss={dismiss} desc={desc} invite={invite} />
|
||||
);
|
||||
}
|
||||
|
||||
interface PromptFormProps {
|
||||
kind: string;
|
||||
}
|
||||
|
||||
interface PromptFormSchema {
|
||||
link: string;
|
||||
}
|
||||
export interface JoinPromptProps {
|
||||
kind: string;
|
||||
dismiss?: () => void;
|
||||
}
|
||||
|
||||
export function JoinPrompt(props: JoinPromptProps) {
|
||||
const { kind, dismiss } = props;
|
||||
const { query, appendQuery } = useQuery();
|
||||
const history = useHistory();
|
||||
const initialValues = {
|
||||
link: "",
|
||||
};
|
||||
|
||||
const onSubmit = async ({ link }: PromptFormSchema) => {
|
||||
const path = `/ship/${link}`;
|
||||
history.push({
|
||||
search: appendQuery({ "join-path": path }),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<JoinSkeleton modal body={<Text>a</Text>} title="Join a Group">
|
||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||
<Form>
|
||||
<Col p="4" gapY="4">
|
||||
<ManagedTextInputField
|
||||
label="Invite Link"
|
||||
id="link"
|
||||
caption="Enter either a web+urbitgraph:// link or an identifier in the form ~sampel-palnet/group"
|
||||
/>
|
||||
<Row gapX="2">
|
||||
{!!dismiss ? (
|
||||
<Button type="button" onClick={dismiss}>
|
||||
Dismiss
|
||||
</Button>
|
||||
) : null}
|
||||
<Button type="submit" primary>
|
||||
Join
|
||||
</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
</JoinSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
function JoinProgressIndicator(props: { progress: JoinProgress }) {
|
||||
const { progress } = props;
|
||||
const percentage =
|
||||
progress === "done" ? 100 : (joinLoad.indexOf(progress as any) + 1) * 25;
|
||||
|
||||
const description = (() => {
|
||||
switch (progress) {
|
||||
case "start":
|
||||
return "Connecting to host";
|
||||
case "added":
|
||||
return "Retrieving members";
|
||||
case "metadata":
|
||||
return "Retrieving channels";
|
||||
case "done":
|
||||
return "Finished";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Col gapY="2">
|
||||
<Text color="lightGray">{description}</Text>
|
||||
<ContinuousProgressBar percentage={percentage} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
export interface JoinDoneProps {
|
||||
desc: JoinDesc;
|
||||
modal: boolean;
|
||||
}
|
||||
|
||||
export function JoinDone(props: JoinDoneProps) {
|
||||
const { desc, modal } = props;
|
||||
const { preview, error } = usePreview(desc.group);
|
||||
const name = desc.kind === "groups" ? "Group" : "Group Chat";
|
||||
const title = `Joined ${name} successfully`;
|
||||
|
||||
return (
|
||||
<JoinSkeleton title={title} modal={modal} desc={desc}>
|
||||
<Col p="4" gapY="4">
|
||||
<JoinProgressIndicator progress="done" />
|
||||
<Row gapX="2">
|
||||
<Button>Dismiss</Button>
|
||||
<Button primary>View Group</Button>
|
||||
</Row>
|
||||
</Col>
|
||||
</JoinSkeleton>
|
||||
);
|
||||
}
|
||||
|
||||
export function JoinRoute(props: { graph?: boolean; modal?: boolean }) {
|
||||
const { modal = false, graph = false } = props;
|
||||
const { query } = useQuery();
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const kind = query.get("join-kind");
|
||||
const path = query.get("join-path");
|
||||
if (!kind) {
|
||||
return null;
|
||||
}
|
||||
const desc: JoinDesc = path
|
||||
? {
|
||||
group: path,
|
||||
kind: graph ? "graph" : "groups",
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const dismiss = () => {
|
||||
history.push(pathname);
|
||||
};
|
||||
|
||||
return desc ? (
|
||||
<Join desc={desc} modal dismiss={dismiss} />
|
||||
) : (
|
||||
<JoinPrompt kind={kind} dismiss={dismiss} />
|
||||
);
|
||||
}
|
120
pkg/interface/src/views/landscape/components/Join/Skeleton.tsx
Normal file
120
pkg/interface/src/views/landscape/components/Join/Skeleton.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Text,
|
||||
Box,
|
||||
Button,
|
||||
ManagedTextInputField,
|
||||
ManagedCheckboxField,
|
||||
ContinuousProgressBar,
|
||||
} from "@tlon/indigo-react";
|
||||
import { ModalOverlay } from "~/views/components/ModalOverlay";
|
||||
import Author from "~/views/components/Author";
|
||||
import { GroupSummary } from "../GroupSummary";
|
||||
|
||||
import { resourceFromPath } from "~/logic/lib/group";
|
||||
|
||||
import useMetadataState, { usePreview } from "~/logic/state/metadata";
|
||||
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
|
||||
|
||||
export type JoinKind = "graph" | "groups";
|
||||
|
||||
export interface JoinDesc {
|
||||
group: string;
|
||||
kind: JoinKind;
|
||||
}
|
||||
|
||||
interface JoinSkeletonProps {
|
||||
title: string;
|
||||
desc?: JoinDesc;
|
||||
modal: boolean;
|
||||
children: JSX.Element;
|
||||
onJoin?: () => void;
|
||||
body?: JSX.Element;
|
||||
}
|
||||
|
||||
export function JoinSkeleton(props: JoinSkeletonProps) {
|
||||
const { title, body, children, onJoin, desc, modal } = props;
|
||||
|
||||
const inner = (
|
||||
<Col
|
||||
maxWidth={modal ? "384px" : undefined}
|
||||
borderRadius="2"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Col
|
||||
gapY="4"
|
||||
p="4"
|
||||
borderRadius="2"
|
||||
backgroundColor="washedGray"
|
||||
justifyContent="space-between"
|
||||
flexGrow={1}
|
||||
>
|
||||
<Box maxWidth="512px">
|
||||
<Text fontWeight="medium" fontSize="2">
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
{!!desc ? <JoinBody desc={desc} /> : null}
|
||||
</Col>
|
||||
{children}
|
||||
</Col>
|
||||
);
|
||||
return modal ? (
|
||||
<ModalOverlay dismiss={() => {}}>{inner}</ModalOverlay>
|
||||
) : (
|
||||
inner
|
||||
);
|
||||
}
|
||||
|
||||
export function JoinBody(props: { desc: JoinDesc }) {
|
||||
const { desc } = props;
|
||||
const { group, kind } = desc || {};
|
||||
const { preview, error } = usePreview(group);
|
||||
const { ship, name } = resourceFromPath(group);
|
||||
|
||||
const invite = useInviteForResource(kind, ship, name);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!desc ? "Prompt invite link" : null}
|
||||
{preview ? (
|
||||
<GroupSummary
|
||||
memberCount={preview.members}
|
||||
channelCount={preview["channel-count"]}
|
||||
metadata={preview.metadata}
|
||||
/>
|
||||
) : (
|
||||
<FallbackSummary path={group} />
|
||||
)}
|
||||
|
||||
{invite ? (
|
||||
<Col gapY="2">
|
||||
<Box>
|
||||
<Text>
|
||||
<Text mono>{invite.ship}</Text> <Text gray>invited you</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
{invite.text?.length > 0 ? (
|
||||
<Box>
|
||||
<Text>"{invite.text}"</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
</Col>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FallbackSummary(props: { path: string }) {
|
||||
const { path } = props;
|
||||
const [, , ship, name] = path.split("/");
|
||||
|
||||
return (
|
||||
<Row alignItems="center" gapX="0">
|
||||
<Author gray fullNotIcon size={40} showImage ship={ship} dontShowTime />
|
||||
<Text mono>/{name}</Text>
|
||||
</Row>
|
||||
);
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
import {
|
||||
Box, Col,
|
||||
Icon,
|
||||
ManagedTextInputField as Input, Row,
|
||||
Text,
|
||||
Button
|
||||
} from '@tlon/indigo-react';
|
||||
import { join, MetadataUpdatePreview } from '@urbit/api';
|
||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||
import _ from 'lodash';
|
||||
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 { useQuery } from '~/logic/lib/useQuery';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { getModuleIcon } from '~/logic/lib/util';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
import { FormError } from '~/views/components/FormError';
|
||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||
import { GroupSummary } from './GroupSummary';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
const formSchema = Yup.object({
|
||||
group: Yup.string()
|
||||
.required('Must provide group to join')
|
||||
.test('is-valid', 'Invalid group', (group: string | null | undefined) => {
|
||||
if (!group) {
|
||||
return false;
|
||||
}
|
||||
const [patp, name] = group.split('/');
|
||||
return urbitOb.isValidPatp(patp) && name.length > 0;
|
||||
})
|
||||
});
|
||||
|
||||
interface FormSchema {
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface JoinGroupProps {
|
||||
autojoin?: string;
|
||||
dismiss?: () => void;
|
||||
}
|
||||
|
||||
function Autojoin(props: { autojoin: string | null }) {
|
||||
const { submitForm } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.autojoin) {
|
||||
submitForm();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function JoinGroup(props: JoinGroupProps): ReactElement {
|
||||
const { autojoin, dismiss } = props;
|
||||
const { associations, getPreview } = useMetadataState();
|
||||
const [timedOut, setTimedOut] = useState(false);
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const history = useHistory();
|
||||
const initialValues: FormSchema = {
|
||||
group: autojoin || ''
|
||||
};
|
||||
const [preview, setPreview] = useState<
|
||||
MetadataUpdatePreview | string | null
|
||||
>(null);
|
||||
|
||||
const waiter = useWaitForProps({ associations, groups }, _.isString(preview) ? 1 : 30000);
|
||||
|
||||
const { query } = useQuery();
|
||||
|
||||
const onConfirm = useCallback(async (group: string) => {
|
||||
const [,,ship,name] = group.split('/');
|
||||
if (group in groups) {
|
||||
return history.push(`/~landscape${group}`);
|
||||
}
|
||||
await airlock.poke(join(ship, name));
|
||||
try {
|
||||
await waiter((p) => {
|
||||
return group in p.groups &&
|
||||
(group in (p.associations?.graph ?? {})
|
||||
|| group in (p.associations?.groups ?? {}));
|
||||
});
|
||||
|
||||
if(query.has('redir')) {
|
||||
const redir = query.get('redir')!;
|
||||
history.push(redir);
|
||||
}
|
||||
|
||||
if(groups?.[group]?.hidden) {
|
||||
const { metadata } = associations.graph[group];
|
||||
if (metadata?.config && 'graph' in metadata.config) {
|
||||
history.push(`/~landscape/home/resource/${metadata.config.graph}${group}`);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
history.push(`/~landscape${group}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setTimedOut(true);
|
||||
console.error(e);
|
||||
}
|
||||
}, [waiter, history, associations, groups]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||
const [ship, name] = values.group.split('/');
|
||||
const path = `/ship/${ship}/${name}`;
|
||||
if (path in groups) {
|
||||
return history.push(`/~landscape${path}`);
|
||||
}
|
||||
// skip if it's unmanaged
|
||||
try {
|
||||
const prev = await getPreview(path);
|
||||
actions.setStatus({ success: null });
|
||||
setPreview(prev);
|
||||
} catch (e) {
|
||||
if (e === 'no-permissions') {
|
||||
actions.setStatus({
|
||||
error:
|
||||
'Unable to join group, you do not have the correct permissions'
|
||||
});
|
||||
} else if (e === 'offline') {
|
||||
setPreview(path);
|
||||
} else {
|
||||
actions.setStatus({ error: 'Unknown error' });
|
||||
}
|
||||
}
|
||||
},
|
||||
[waiter, history, onConfirm]
|
||||
);
|
||||
|
||||
return (
|
||||
<Col p={3}>
|
||||
<Box mb={3}>
|
||||
<Text fontSize={2} fontWeight="bold">
|
||||
Join a Group
|
||||
</Text>
|
||||
</Box>
|
||||
{ timedOut ? (
|
||||
<Col width="100%" gapY={4}>
|
||||
<Text>The host is not responding. You will receive a notification when the join requests succeeds
|
||||
</Text>
|
||||
<Button primary onClick={dismiss}>
|
||||
Dismiss
|
||||
</Button>
|
||||
|
||||
</Col>
|
||||
) : _.isString(preview) ? (
|
||||
|
||||
<Col width="100%" gapY={4}>
|
||||
<Text>The host appears to be offline. Join anyway?</Text>
|
||||
<StatelessAsyncButton
|
||||
primary
|
||||
name="join"
|
||||
onClick={() => onConfirm(preview)}
|
||||
>
|
||||
Join anyway
|
||||
</StatelessAsyncButton>
|
||||
</Col>
|
||||
) : preview ? (
|
||||
<>
|
||||
<GroupSummary
|
||||
metadata={preview.metadata}
|
||||
memberCount={preview?.members}
|
||||
channelCount={preview?.['channel-count']}
|
||||
>
|
||||
{ Object.keys(preview.channels).length > 0 && (
|
||||
<Col
|
||||
gapY={2}
|
||||
p={2}
|
||||
borderRadius={2}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
bg="washedBlue"
|
||||
maxHeight="300px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<Text gray fontSize={1}>
|
||||
Channels
|
||||
</Text>
|
||||
<Box width="100%" flexShrink={0}>
|
||||
{Object.values(preview.channels).map(({ metadata }: any, i) => (
|
||||
<Row key={i} width="100%">
|
||||
<Icon
|
||||
mr={2}
|
||||
color="blue"
|
||||
icon={getModuleIcon(metadata?.config?.graph) as any}
|
||||
/>
|
||||
<Text color="blue">{metadata.title} </Text>
|
||||
</Row>
|
||||
))}
|
||||
</Box>
|
||||
</Col>
|
||||
)}
|
||||
</GroupSummary>
|
||||
<StatelessAsyncButton
|
||||
marginTop={3}
|
||||
primary
|
||||
name="join"
|
||||
onClick={() => onConfirm(preview.group)}
|
||||
>
|
||||
Join {preview.metadata.title}
|
||||
</StatelessAsyncButton>
|
||||
</>
|
||||
) : (
|
||||
<Col width="100%" gapY={4}>
|
||||
<Formik
|
||||
validationSchema={formSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form style={{ display: 'contents' }}>
|
||||
<Autojoin autojoin={autojoin ?? null} />
|
||||
<Input
|
||||
id="group"
|
||||
label="Group"
|
||||
caption="What group are you joining?"
|
||||
placeholder="~sampel-palnet/test-group"
|
||||
/>
|
||||
<AsyncButton mt={4}>Join Group</AsyncButton>
|
||||
<FormError mt={4} />
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -13,11 +13,12 @@ import Dot from '~/views/components/Dot';
|
||||
import { useHarkDm, useHarkStat } from '~/logic/state/hark';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import {usePreview} from '~/logic/state/metadata';
|
||||
|
||||
function useAssociationStatus(resource: string) {
|
||||
const [, , ship, name] = resource.split('/');
|
||||
const [, , ship, name] = resource.split("/");
|
||||
const graphKey = `${deSig(ship)}/${name}`;
|
||||
const isSubscribed = useGraphState(s => s.graphKeys.has(graphKey));
|
||||
const isSubscribed = useGraphState((s) => s.graphKeys.has(graphKey));
|
||||
const stats = useHarkStat(`/graph/~${graphKey}`);
|
||||
const { count, each } = stats;
|
||||
const hasNotifications = false;
|
||||
@ -43,6 +44,7 @@ function SidebarItemBase(props: {
|
||||
title: string | ReactNode;
|
||||
mono?: boolean;
|
||||
pending?: boolean;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const {
|
||||
title,
|
||||
@ -53,22 +55,24 @@ function SidebarItemBase(props: {
|
||||
hasUnread,
|
||||
isSynced = false,
|
||||
mono = false,
|
||||
pending = false
|
||||
pending = false,
|
||||
onClick
|
||||
} = props;
|
||||
const color = isSynced
|
||||
? hasUnread || hasNotification
|
||||
? 'black'
|
||||
: 'gray'
|
||||
: 'lightGray';
|
||||
? "black"
|
||||
: "gray"
|
||||
: "lightGray";
|
||||
|
||||
const fontWeight = hasUnread || hasNotification ? '500' : 'normal';
|
||||
const fontWeight = hasUnread || hasNotification ? "500" : "normal";
|
||||
|
||||
return (
|
||||
<HoverBoxLink
|
||||
// ref={anchorRef}
|
||||
to={to}
|
||||
bg={pending ? 'lightBlue' : 'white'}
|
||||
bgActive={pending ? 'washedBlue' : 'washedGray'}
|
||||
onClick={onClick}
|
||||
bg={pending ? "lightBlue" : "white"}
|
||||
bgActive={pending ? "washedBlue" : "washedGray"}
|
||||
width="100%"
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
@ -108,7 +112,7 @@ function SidebarItemBase(props: {
|
||||
mono={mono}
|
||||
color={color}
|
||||
fontWeight={fontWeight}
|
||||
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
|
||||
style={{ textOverflow: "ellipsis", whiteSpace: "pre" }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
@ -118,156 +122,201 @@ function SidebarItemBase(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export const SidebarDmItem = React.memo((props: {
|
||||
ship: string;
|
||||
selected?: boolean;
|
||||
workspace: Workspace;
|
||||
pending?: boolean;
|
||||
}) => {
|
||||
const { ship, selected = false, pending = false } = props;
|
||||
const contact = useContact(ship);
|
||||
const { hideAvatars, hideNicknames } = useSettingsState(s => s.calm);
|
||||
const title =
|
||||
!hideNicknames && contact?.nickname
|
||||
? contact?.nickname
|
||||
: cite(ship) ?? ship;
|
||||
const { count, each } = useHarkDm(ship);
|
||||
const unreads = count + each.length;
|
||||
const img =
|
||||
contact?.avatar && !hideAvatars ? (
|
||||
<BaseImage
|
||||
referrerPolicy="no-referrer"
|
||||
src={contact.avatar}
|
||||
width="16px"
|
||||
height="16px"
|
||||
borderRadius={2}
|
||||
/>
|
||||
) : (
|
||||
<Sigil
|
||||
ship={ship}
|
||||
color={`#${uxToHex(contact?.color || '0x0')}`}
|
||||
icon
|
||||
padding={2}
|
||||
size={16}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarItemBase
|
||||
selected={selected}
|
||||
hasNotification={false}
|
||||
hasUnread={(unreads as number) > 0}
|
||||
to={`/~landscape/messages/dm/${ship}`}
|
||||
title={title}
|
||||
mono={hideAvatars || !contact?.nickname}
|
||||
isSynced
|
||||
pending={pending}
|
||||
>
|
||||
{img}
|
||||
</SidebarItemBase>
|
||||
);
|
||||
});
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const SidebarAssociationItem = React.memo((props: {
|
||||
hideUnjoined: boolean;
|
||||
association: Association;
|
||||
export const SidebarPendingItem = (props: {
|
||||
path: string;
|
||||
selected: boolean;
|
||||
workspace: Workspace;
|
||||
}) => {
|
||||
const { association, selected } = props;
|
||||
const title = getItemTitle(association) || '';
|
||||
const appName = association?.['app-name'];
|
||||
let mod: string = appName;
|
||||
if (association?.metadata?.config && 'graph' in association.metadata.config) {
|
||||
mod = association.metadata.config.graph ;
|
||||
}
|
||||
const rid = association?.resource;
|
||||
const groupPath = association?.group;
|
||||
const group = useGroupState(state => state.groups[groupPath]);
|
||||
const { hideNicknames } = useSettingsState(s => s.calm);
|
||||
const contacts = useContactState(s => s.contacts);
|
||||
const isUnmanaged = group?.hidden || false;
|
||||
const DM = isUnmanaged && props.workspace?.type === 'messages';
|
||||
const itemStatus = useAssociationStatus(rid);
|
||||
const hasNotification = itemStatus === 'notification';
|
||||
const hasUnread = itemStatus === 'unread';
|
||||
const isSynced = itemStatus !== 'unsubscribed';
|
||||
let baseUrl = `/~landscape${groupPath}`;
|
||||
|
||||
if (DM) {
|
||||
baseUrl = '/~landscape/messages';
|
||||
} else if (isUnmanaged) {
|
||||
baseUrl = '/~landscape/home';
|
||||
}
|
||||
|
||||
const to = isSynced
|
||||
? `${baseUrl}/resource/${mod}${rid}`
|
||||
: `${baseUrl}/join/${mod}${rid}`;
|
||||
|
||||
if (props.hideUnjoined && !isSynced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const participantNames = (str: string) => {
|
||||
const color = isSynced
|
||||
? hasUnread || hasNotification
|
||||
? 'black'
|
||||
: 'gray'
|
||||
: 'lightGray';
|
||||
if (_.includes(str, ',') && _.startsWith(str, '~')) {
|
||||
const names = _.split(str, ', ');
|
||||
return names.map((name, idx) => {
|
||||
if (urbitOb.isValidPatp(name)) {
|
||||
if (contacts[name]?.nickname && !hideNicknames)
|
||||
return (
|
||||
<Text key={name} bold={hasUnread} color={color}>
|
||||
{contacts[name]?.nickname}
|
||||
{idx + 1 != names.length ? ', ' : null}
|
||||
</Text>
|
||||
);
|
||||
return (
|
||||
<Text key={name} mono bold={hasUnread} color={color}>
|
||||
{name}
|
||||
<Text color={color}>{idx + 1 != names.length ? ', ' : null}</Text>
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
const { path, selected } = props;
|
||||
const { preview, error } = usePreview(path);
|
||||
const color = `#${uxToHex(preview?.metadata?.color || "0x0")}`;
|
||||
const title = preview?.metadata?.title || path;
|
||||
const to = `/~landscape/messages/pending/${path.slice(6)}`;
|
||||
return (
|
||||
<SidebarItemBase
|
||||
to={to}
|
||||
title={title}
|
||||
selected={selected}
|
||||
hasUnread={hasUnread}
|
||||
isSynced={isSynced}
|
||||
title={
|
||||
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
|
||||
}
|
||||
hasNotification={hasNotification}
|
||||
hasNotification={false}
|
||||
hasUnread={false}
|
||||
pending
|
||||
>
|
||||
{DM ? (
|
||||
<Box
|
||||
flexShrink={0}
|
||||
height={16}
|
||||
width={16}
|
||||
borderRadius={2}
|
||||
backgroundColor={
|
||||
`#${uxToHex(props?.association?.metadata?.color)}` || '#000000'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
display="block"
|
||||
color={isSynced ? 'black' : 'lightGray'}
|
||||
icon={getModuleIcon(mod as any)}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
flexShrink={0}
|
||||
height={16}
|
||||
width={16}
|
||||
borderRadius={2}
|
||||
backgroundColor={color}
|
||||
/>
|
||||
</SidebarItemBase>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const SidebarDmItem = React.memo(
|
||||
(props: {
|
||||
ship: string;
|
||||
selected?: boolean;
|
||||
workspace: Workspace;
|
||||
pending?: boolean;
|
||||
}) => {
|
||||
const { ship, selected = false, pending = false } = props;
|
||||
const contact = useContact(ship);
|
||||
const { hideAvatars, hideNicknames } = useSettingsState((s) => s.calm);
|
||||
const title =
|
||||
!hideNicknames && contact?.nickname
|
||||
? contact?.nickname
|
||||
: cite(ship) ?? ship;
|
||||
const { count, each } = useHarkDm(ship);
|
||||
const unreads = count + each.length;
|
||||
const img =
|
||||
contact?.avatar && !hideAvatars ? (
|
||||
<BaseImage
|
||||
referrerPolicy="no-referrer"
|
||||
src={contact.avatar}
|
||||
width="16px"
|
||||
height="16px"
|
||||
borderRadius={2}
|
||||
/>
|
||||
) : (
|
||||
<Sigil
|
||||
ship={ship}
|
||||
color={`#${uxToHex(contact?.color || "0x0")}`}
|
||||
icon
|
||||
padding={2}
|
||||
size={16}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarItemBase
|
||||
selected={selected}
|
||||
hasNotification={false}
|
||||
hasUnread={(unreads as number) > 0}
|
||||
to={`/~landscape/messages/dm/${ship}`}
|
||||
title={title}
|
||||
mono={hideAvatars || !contact?.nickname}
|
||||
isSynced
|
||||
pending={pending}
|
||||
>
|
||||
{img}
|
||||
</SidebarItemBase>
|
||||
);
|
||||
}
|
||||
);
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export const SidebarAssociationItem = React.memo(
|
||||
(props: {
|
||||
hideUnjoined: boolean;
|
||||
association: Association;
|
||||
selected: boolean;
|
||||
workspace: Workspace;
|
||||
}) => {
|
||||
const { association, selected } = props;
|
||||
const title = association ? getItemTitle(association) || "" : "";
|
||||
const appName = association?.["app-name"];
|
||||
let mod: string = appName;
|
||||
if (
|
||||
association?.metadata?.config &&
|
||||
"graph" in association.metadata.config
|
||||
) {
|
||||
mod = association.metadata.config.graph;
|
||||
}
|
||||
const pending = useGroupState(s => association.group in s.pendingJoin);
|
||||
console.log(pending);
|
||||
const rid = association?.resource;
|
||||
const { hideNicknames } = useSettingsState((s) => s.calm);
|
||||
const contacts = useContactState((s) => s.contacts);
|
||||
const group = useGroupState(s => association ? s.groups[association.group] : undefined);
|
||||
const isUnmanaged = group?.hidden || false;
|
||||
const DM = isUnmanaged && props.workspace?.type === "messages";
|
||||
const itemStatus = useAssociationStatus(rid);
|
||||
const hasNotification = itemStatus === "notification";
|
||||
const hasUnread = itemStatus === "unread";
|
||||
const isSynced = itemStatus !== "unsubscribed";
|
||||
let baseUrl = `/~landscape${association.group}`;
|
||||
|
||||
if (DM) {
|
||||
baseUrl = "/~landscape/messages";
|
||||
} else if (isUnmanaged) {
|
||||
baseUrl = "/~landscape/home";
|
||||
}
|
||||
|
||||
const to = isSynced
|
||||
? `${baseUrl}/resource/${mod}${rid}`
|
||||
: `${baseUrl}/join/${mod}${rid}`;
|
||||
|
||||
const onClick = pending ? () => {
|
||||
useGroupState.getState().doneJoin(rid);
|
||||
} : undefined;
|
||||
|
||||
if (props.hideUnjoined && !isSynced) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const participantNames = (str: string) => {
|
||||
const color = isSynced
|
||||
? hasUnread || hasNotification
|
||||
? "black"
|
||||
: "gray"
|
||||
: "lightGray";
|
||||
if (_.includes(str, ",") && _.startsWith(str, "~")) {
|
||||
const names = _.split(str, ", ");
|
||||
return names.map((name, idx) => {
|
||||
if (urbitOb.isValidPatp(name)) {
|
||||
if (contacts[name]?.nickname && !hideNicknames)
|
||||
return (
|
||||
<Text key={name} bold={hasUnread} color={color}>
|
||||
{contacts[name]?.nickname}
|
||||
{idx + 1 != names.length ? ", " : null}
|
||||
</Text>
|
||||
);
|
||||
return (
|
||||
<Text key={name} mono bold={hasUnread} color={color}>
|
||||
{name}
|
||||
<Text color={color}>
|
||||
{idx + 1 != names.length ? ", " : null}
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarItemBase
|
||||
to={to}
|
||||
selected={selected}
|
||||
hasUnread={hasUnread}
|
||||
isSynced={isSynced}
|
||||
title={
|
||||
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
|
||||
}
|
||||
hasNotification={hasNotification}
|
||||
pending={pending}
|
||||
onClick={onClick}
|
||||
>
|
||||
{DM ? (
|
||||
<Box
|
||||
flexShrink={0}
|
||||
height={16}
|
||||
width={16}
|
||||
borderRadius={2}
|
||||
backgroundColor={
|
||||
`#${uxToHex(props?.association?.metadata?.color)}` || "#000000"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
display="block"
|
||||
color={isSynced ? "black" : "lightGray"}
|
||||
icon={getModuleIcon(mod as any)}
|
||||
/>
|
||||
)}
|
||||
</SidebarItemBase>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { Associations, Graph, Unreads } from '@urbit/api';
|
||||
import { patp, patp2dec } from 'urbit-ob';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
|
||||
import { SidebarAssociationItem, SidebarDmItem, SidebarPendingItem } from './SidebarItem';
|
||||
import useGraphState, { useInbox } from '~/logic/state/graph';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { alphabeticalOrder, getResourcePath, modulo } from '~/logic/lib/util';
|
||||
@ -12,8 +12,10 @@ import { Workspace } from '~/types/workspace';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useInviteState from '~/logic/state/invite';
|
||||
|
||||
function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort, (a: string, b: string) => number> {
|
||||
function sidebarSort(unreads: Unreads, pending: string[]): Record<SidebarSort, (a: string, b: string) => number> {
|
||||
const { associations } = useMetadataState.getState();
|
||||
const alphabetical = (a: string, b: string) => {
|
||||
const aAssoc = associations[a];
|
||||
@ -25,8 +27,8 @@ function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort
|
||||
};
|
||||
|
||||
const lastUpdated = (a: string, b: string) => {
|
||||
const aPend = pending.has(a.slice(1));
|
||||
const bPend = pending.has(b.slice(1));
|
||||
const aPend = pending.includes(a);
|
||||
const bPend = pending.includes(b);
|
||||
if(aPend && !bPend) {
|
||||
return -1;
|
||||
}
|
||||
@ -50,7 +52,7 @@ function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort
|
||||
};
|
||||
}
|
||||
|
||||
function getItems(associations: Associations, workspace: Workspace, inbox: Graph, pending: Set<string>) {
|
||||
function getItems(associations: Associations, workspace: Workspace, inbox: Graph, pending: string[]) {
|
||||
const filtered = Object.keys(associations.graph).filter((a) => {
|
||||
const assoc = associations.graph[a];
|
||||
if(!('graph' in assoc.metadata.config)) {
|
||||
@ -84,9 +86,9 @@ function getItems(associations: Associations, workspace: Workspace, inbox: Graph
|
||||
: inbox.keys().map(x => patp(x.toString()));
|
||||
const pend = workspace.type !== 'messages'
|
||||
? []
|
||||
: Array.from(pending).map(s => `~${s}`);
|
||||
: pending
|
||||
|
||||
return [...filtered, ..._.union(direct, pend)];
|
||||
return _.union(direct, pend, filtered);
|
||||
}
|
||||
|
||||
export function SidebarList(props: {
|
||||
@ -98,9 +100,18 @@ export function SidebarList(props: {
|
||||
}): ReactElement {
|
||||
const { selected, config, workspace } = props;
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const groups = useGroupState(s => s.groups);
|
||||
const inbox = useInbox();
|
||||
const graphKeys = useGraphState(s => s.graphKeys);
|
||||
const pending = useGraphState(s => s.pendingDms);
|
||||
const pendingDms = useGraphState(s => [...s.pendingDms].map(s => `~${s}`));
|
||||
const pendingGroupChats = useGroupState(s => _.pickBy(s.pendingJoin, (req, rid) => !(rid in groups) && req.app === 'graph'));
|
||||
const inviteGroupChats = useInviteState(
|
||||
s => Object.values(s.invites?.['graph'] || {})
|
||||
.map(inv => {
|
||||
return `/ship/~${inv.resource.ship}/${inv.resource.name}`
|
||||
})
|
||||
);
|
||||
const pending = [...pendingDms, ...Object.keys(pendingGroupChats), ...inviteGroupChats];
|
||||
const unreads = useHarkState(s => s.unreads);
|
||||
|
||||
const ordered = getItems(associations, workspace, inbox, pending)
|
||||
@ -118,10 +129,16 @@ export function SidebarList(props: {
|
||||
if(newChannel.startsWith('~')) {
|
||||
path = `/~landscape/messages/dm/${newChannel}`;
|
||||
} else {
|
||||
const { metadata, resource } = associations.graph[ordered[newIdx]];
|
||||
const joined = graphKeys.has(resource.slice(7));
|
||||
if ('graph' in metadata.config) {
|
||||
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
|
||||
const association = associations.graph[ordered[newIdx]];
|
||||
if(!association) {
|
||||
path = `/~landscape/messages`
|
||||
return;
|
||||
} else {
|
||||
const { metadata, resource } = association;
|
||||
const joined = graphKeys.has(resource.slice(7));
|
||||
if ('graph' in metadata.config) {
|
||||
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
history.push(path);
|
||||
@ -140,7 +157,22 @@ export function SidebarList(props: {
|
||||
return (
|
||||
<>
|
||||
{ordered.map((pathOrShip) => {
|
||||
return pathOrShip.startsWith('/') ? (
|
||||
return pathOrShip.startsWith('~') ? (
|
||||
<SidebarDmItem
|
||||
key={pathOrShip}
|
||||
ship={pathOrShip}
|
||||
workspace={workspace}
|
||||
selected={pathOrShip === selected}
|
||||
pending={pending.includes(pathOrShip)}
|
||||
/>
|
||||
|
||||
) : pending.includes(pathOrShip) ? (
|
||||
<SidebarPendingItem
|
||||
key={pathOrShip}
|
||||
path={pathOrShip}
|
||||
selected={pathOrShip === selected}
|
||||
/>
|
||||
) : (
|
||||
<SidebarAssociationItem
|
||||
key={pathOrShip}
|
||||
selected={pathOrShip === selected}
|
||||
@ -148,16 +180,7 @@ export function SidebarList(props: {
|
||||
hideUnjoined={config.hideUnjoined}
|
||||
workspace={workspace}
|
||||
/>
|
||||
) : (
|
||||
<SidebarDmItem
|
||||
key={pathOrShip}
|
||||
ship={pathOrShip}
|
||||
workspace={workspace}
|
||||
selected={pathOrShip === selected}
|
||||
pending={pending.has(pathOrShip.slice(1))}
|
||||
/>
|
||||
|
||||
);
|
||||
) ;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
@ -7,7 +7,6 @@ import useHarkState from '~/logic/state/hark';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
import { Body } from '../components/Body';
|
||||
import { GroupsPane } from './components/GroupsPane';
|
||||
import { JoinGroup } from './components/JoinGroup';
|
||||
import { NewGroup } from './components/NewGroup';
|
||||
import './css/custom.css';
|
||||
import _ from 'lodash';
|
||||
@ -75,22 +74,6 @@ export default function Landscape() {
|
||||
</Box>
|
||||
</Body>
|
||||
</Route>
|
||||
<Route path="/~landscape/join/:ship?/:name?"
|
||||
render={(routeProps) => {
|
||||
const { ship, name } = routeProps.match.params;
|
||||
const autojoin = ship && name ? `${ship}/${name}` : undefined;
|
||||
return (
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<JoinGroup
|
||||
autojoin={autojoin}
|
||||
{...routeProps}
|
||||
/>
|
||||
</Box>
|
||||
</Body>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
|
@ -1,33 +1,34 @@
|
||||
/- view-sur=group-view, group-store, *group, metadata=metadata-store, hark=hark-store
|
||||
/- inv=invite-store
|
||||
/+ default-agent, agentio, mdl=metadata,
|
||||
resource, dbug, grpl=group, conl=contact, verb
|
||||
|%
|
||||
++ card card:agent:gall
|
||||
::
|
||||
+$ base-state-0
|
||||
joining=(map rid=resource [=ship =progress:view])
|
||||
::
|
||||
+$ base-state-1
|
||||
joining=(map rid=resource request:view)
|
||||
::
|
||||
+$ state-zero
|
||||
[%0 base-state-0]
|
||||
[%0 *]
|
||||
::
|
||||
+$ state-one
|
||||
[%1 base-state-0]
|
||||
[%1 *]
|
||||
::
|
||||
+$ state-two
|
||||
[%2 base-state-1]
|
||||
[%2 *]
|
||||
::
|
||||
+$ state-three
|
||||
[%3 joining=(map rid=resource request:view)]
|
||||
::
|
||||
+$ versioned-state
|
||||
$% state-zero
|
||||
state-one
|
||||
state-two
|
||||
state-three
|
||||
==
|
||||
::
|
||||
++ view view-sur
|
||||
--
|
||||
=| state-two
|
||||
=| state-three
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
@ -48,29 +49,10 @@
|
||||
|= =vase
|
||||
=+ !<(old=versioned-state vase)
|
||||
=| cards=(list card)
|
||||
|^
|
||||
?- -.old
|
||||
%2 [cards this(state old)]
|
||||
%1 $(-.old %2, +.old (base-state-to-1 +.old))
|
||||
%0 $(-.old %1, cards :_(cards (poke-self:pass:io noun+!>(%cleanup))))
|
||||
==
|
||||
::
|
||||
++ base-state-to-1
|
||||
|= base-state-0
|
||||
%- ~(gas by *(map resource request:view))
|
||||
(turn ~(tap by joining) request-to-1)
|
||||
::
|
||||
++ request-to-1
|
||||
|= [rid=resource =ship =progress:view]
|
||||
^- [resource request:view]
|
||||
:- rid
|
||||
%* . *request:view
|
||||
started now.bowl
|
||||
hidden %.n
|
||||
ship ship
|
||||
progress progress
|
||||
==
|
||||
--
|
||||
|-
|
||||
?: ?=(%3 -.old)
|
||||
[cards this(state old)]
|
||||
$(old *state-three)
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
@ -84,8 +66,9 @@
|
||||
=+ !<(=action:view vase)
|
||||
=^ cards state
|
||||
?+ -.action !!
|
||||
%join jn-abet:(jn-start:join:gc +.action)
|
||||
%hide (hide:gc +.action)
|
||||
%join jn-abet:(jn-start:join:gc +.action)
|
||||
%abort jn-abet:(jn-abort:join:gc +.action)
|
||||
%done jn-abet:(jn-done:join:gc +.action)
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
@ -106,7 +89,7 @@
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
=^ cards state
|
||||
?+ wire `state
|
||||
?+ wire (on-agent:def:gc wire sign)
|
||||
[%join %ship @ @ *]
|
||||
=/ rid
|
||||
(de-path:resource t.wire)
|
||||
@ -115,7 +98,18 @@
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-arvo
|
||||
|= [=wire sign=sign-arvo]
|
||||
=^ cards state
|
||||
?+ wire (on-arvo:def:gc wire sign)
|
||||
[%breach ~]
|
||||
?> ?=([%jael %public-keys *] sign)
|
||||
?. ?=(%breach -.public-keys-result.sign)
|
||||
`state
|
||||
(breach who.public-keys-result.sign)
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
@ -124,6 +118,7 @@
|
||||
++ grp ~(. grpl bowl)
|
||||
++ io ~(. agentio bowl)
|
||||
++ con ~(. conl bowl)
|
||||
++ def ~(. (default-agent state %|) bowl)
|
||||
++ hide
|
||||
|= rid=resource
|
||||
^- (quip card _state)
|
||||
@ -133,7 +128,28 @@
|
||||
:_ state
|
||||
(fact:io group-view-update+!>(`update:view`[%initial joining]) /all ~)^~
|
||||
:- (fact:io group-view-update+!>([%hide rid]) /all ~)^~
|
||||
state(joining (~(put by joining) rid request(hidden %.y)))
|
||||
state(joining (~(put by joining) rid request))
|
||||
::
|
||||
++ is-tracking
|
||||
|= her=ship
|
||||
^- ?
|
||||
%+ lien ~(tap in ~(key by joining))
|
||||
|=([him=ship name=term] =(her him))
|
||||
::
|
||||
++ breach
|
||||
|= who=ship
|
||||
^- (quip card _state)
|
||||
=/ requests=(list [rid=resource =request:view])
|
||||
~(tap by joining)
|
||||
=| cards=(list card)
|
||||
|- ^- (quip card _state)
|
||||
?~ requests
|
||||
[cards state]
|
||||
?. =(entity.rid.i.requests who)
|
||||
$(requests t.requests)
|
||||
=^ crds state
|
||||
jn-abet:jn-breach:(jn-abed:join rid.i.requests)
|
||||
[(welp cards crds) state]
|
||||
::
|
||||
++ has-joined
|
||||
|= rid=resource
|
||||
@ -170,12 +186,54 @@
|
||||
(emit (fact:io cage /all tx+(en-path:resource rid) ~))
|
||||
group-view-update+!>([%progress rid progress])
|
||||
::
|
||||
++ watch-md
|
||||
(emit (watch-our:(jn-pass-io /md) %metadata-store /updates))
|
||||
::
|
||||
++ watch-groups
|
||||
(emit (watch-our:(jn-pass-io /groups) %group-store /groups))
|
||||
::
|
||||
++ pass
|
||||
|%
|
||||
++ pull-action pull-hook-action+!>([%add ship rid])
|
||||
::
|
||||
++ watch-md (watch-our:(jn-pass-io /md) %metadata-store /updates)
|
||||
++ watch-groups (watch-our:(jn-pass-io /groups) %group-store /groups)
|
||||
++ watch-md-nacks (watch-our:(jn-pass-io /md-nacks) %metadata-pull-hook /nack)
|
||||
++ watch-grp-nacks (watch-our:(jn-pass-io /grp-nacks) %group-pull-hook /nack)
|
||||
::
|
||||
++ add-us
|
||||
%+ poke:(jn-pass-io /add)
|
||||
[ship %group-push-hook]
|
||||
group-update-0+!>([%add-members rid (silt our.bowl ~)])
|
||||
::
|
||||
++ del-us
|
||||
%+ poke:pass:io [ship %group-push-hook]
|
||||
group-update-0+!>([%remove-members rid (silt our.bowl ~)])
|
||||
::
|
||||
++ remove-pull-groups
|
||||
(poke-our:pass:io %group-pull-hook pull-hook-action+!>([%remove rid]))
|
||||
::
|
||||
++ pull-groups
|
||||
(poke-our:(jn-pass-io /poke) %group-pull-hook pull-action)
|
||||
++ pull-md
|
||||
(poke-our:(jn-pass-io /poke) %metadata-pull-hook pull-action)
|
||||
++ pull-co
|
||||
(poke-our:(jn-pass-io /poke) %contact-pull-hook pull-action)
|
||||
::
|
||||
++ allow-co
|
||||
%+ poke-our:(jn-pass-io /poke) %contact-store
|
||||
contact-update-0+!>([%allow %group rid])
|
||||
::
|
||||
++ share-co
|
||||
%+ poke:(jn-pass-io /poke)
|
||||
[entity.rid %contact-push-hook]
|
||||
[%contact-share !>([%share our.bowl])]
|
||||
::
|
||||
++ pull-gra
|
||||
|= gr=resource
|
||||
(poke-our:(jn-pass-io /poke) %graph-pull-hook pull-hook-action+!>([%add entity .]:gr))
|
||||
::
|
||||
++ retry
|
||||
(poke-self:pass:io group-view-action+!>([%join rid ship]))
|
||||
++ watch-breach
|
||||
(~(arvo pass:io /breach) %j %public-keys (silt ship ~))
|
||||
++ leave-breach
|
||||
(~(arvo pass:io /breach) %j %nuke (silt ship ~))
|
||||
--
|
||||
++ jn-pass-io
|
||||
|= pax=path
|
||||
~(. pass:io (welp join+(en-path:resource rid) pax))
|
||||
@ -191,12 +249,14 @@
|
||||
[(flop cards) state]
|
||||
::
|
||||
++ jn-start
|
||||
|= [rid=resource =^ship]
|
||||
|= [rid=resource =^ship =app:view share-co=? autojoin=?]
|
||||
^+ jn-core
|
||||
?> ?= $@(~ [~ %done])
|
||||
?> ?= $@(~ [~ ?(%done %abort)])
|
||||
(bind (~(get by joining) rid) |=(request:view progress))
|
||||
=/ =request:view
|
||||
[now.bowl ship %start app share-co autojoin (get-invites app rid)]
|
||||
=. joining
|
||||
(~(put by joining) rid [%.n now.bowl ship %start])
|
||||
(~(put by joining) rid request)
|
||||
=. jn-core
|
||||
(jn-abed rid)
|
||||
=. jn-core
|
||||
@ -205,14 +265,80 @@
|
||||
group-view-update+!>([%started rid (~(got by joining) rid)])
|
||||
~[/all]
|
||||
?< ~|("already joined {<rid>}" (has-joined rid))
|
||||
=. jn-core
|
||||
%- emit
|
||||
%+ poke:(jn-pass-io /add)
|
||||
[ship %group-push-hook]
|
||||
group-update-0+!>([%add-members rid (silt our.bowl ~)])
|
||||
=. jn-core (emit add-us:pass)
|
||||
=. jn-core (tx-progress %start)
|
||||
=> watch-md
|
||||
watch-groups
|
||||
=? jn-core !(is-tracking ship)
|
||||
(emit watch-breach:pass)
|
||||
=> (emit watch-md:pass)
|
||||
=> (emit watch-groups:pass)
|
||||
=> (emit watch-grp-nacks:pass)
|
||||
=> (emit watch-md-nacks:pass)
|
||||
(emit watch-breach:pass)
|
||||
::
|
||||
++ jn-breach
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
?. ?=(%start progress.request)
|
||||
:: no action required, subscriptions are sane across breaches
|
||||
jn-core
|
||||
(emit add-us:pass)
|
||||
::
|
||||
++ jn-abort
|
||||
|= r=resource
|
||||
^+ jn-core
|
||||
=. jn-core (jn-abed r)
|
||||
(cleanup:rollback %abort)
|
||||
::
|
||||
++ jn-done
|
||||
|= r=resource
|
||||
=. joining (~(del by joining) r)
|
||||
jn-core
|
||||
::
|
||||
++ rollback
|
||||
|^
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
?+ progress.request ~|(cannot-rollback/progress.request !!)
|
||||
%start start
|
||||
%added added
|
||||
%metadata metadata
|
||||
==
|
||||
++ start jn-core
|
||||
++ added (emit del-us:pass)
|
||||
++ metadata (emit:added remove-pull-groups:pass)
|
||||
--
|
||||
::
|
||||
++ get-invites
|
||||
|= [=app:view rid=resource]
|
||||
^- (set uid:view)
|
||||
=+ .^(invit=(unit invitatory:inv) %gx (scry:io %invite-store /invitatory/[app]/noun))
|
||||
?~ invit ~
|
||||
%- ~(gas in *(set uid:view))
|
||||
%+ murn ~(tap by u.invit)
|
||||
|= [=uid:view =invite:inv]
|
||||
?. =(rid resource.invite) ~
|
||||
`uid
|
||||
::
|
||||
++ cleanup
|
||||
|= =progress:view
|
||||
=. jn-core
|
||||
(tx-progress progress)
|
||||
=. jn-core
|
||||
(emit (leave-our:(jn-pass-io /groups) %group-store))
|
||||
=. jn-core
|
||||
(emit (leave-our:(jn-pass-io /md) %metadata-store))
|
||||
=. jn-core
|
||||
(emit (leave-our:(jn-pass-io /md-nacks) %metadata-pull-hook))
|
||||
=. jn-core
|
||||
(emit (leave-our:(jn-pass-io /grp-nacks) %group-pull-hook))
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
=. jn-core
|
||||
%- emit-many
|
||||
%+ turn ~(tap in invite.request)
|
||||
|= =uid:view
|
||||
%+ poke-our:pass:io %invite-store
|
||||
=- invite-action+!>(-)
|
||||
^- action:inv
|
||||
[%accept `@tas`app.request uid]
|
||||
jn-core
|
||||
::
|
||||
++ jn-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
@ -225,34 +351,16 @@
|
||||
(cleanup %no-perms)
|
||||
=. jn-core
|
||||
(tx-progress %added)
|
||||
%- emit
|
||||
%+ poke-our:(jn-pass-io /pull-groups) %group-pull-hook
|
||||
pull-hook-action+!>([%add ship rid])
|
||||
::
|
||||
%pull-groups
|
||||
?> ?=(%poke-ack -.sign)
|
||||
(ack +.sign)
|
||||
(emit pull-groups:pass)
|
||||
::
|
||||
%groups
|
||||
?+ -.sign !!
|
||||
%fact (groups-fact +.sign)
|
||||
%watch-ack (ack +.sign)
|
||||
%kick watch-groups
|
||||
%kick (emit watch-groups:pass)
|
||||
==
|
||||
::
|
||||
%pull-md
|
||||
?> ?=(%poke-ack -.sign)
|
||||
(ack +.sign)
|
||||
::
|
||||
%pull-co
|
||||
?> ?=(%poke-ack -.sign)
|
||||
(ack +.sign)
|
||||
::
|
||||
%share-co
|
||||
?> ?=(%poke-ack -.sign)
|
||||
(ack +.sign)
|
||||
::
|
||||
%push-co
|
||||
%poke
|
||||
?> ?=(%poke-ack -.sign)
|
||||
(ack +.sign)
|
||||
::
|
||||
@ -260,13 +368,38 @@
|
||||
?+ -.sign !!
|
||||
%fact (md-fact +.sign)
|
||||
%watch-ack (ack +.sign)
|
||||
%kick watch-md
|
||||
%kick (emit watch-md:pass)
|
||||
==
|
||||
::
|
||||
%pull-graphs
|
||||
?> ?=(%poke-ack -.sign)
|
||||
%- cleanup
|
||||
?^(p.sign %strange %done)
|
||||
::
|
||||
%md-nacks
|
||||
?+ -.sign !!
|
||||
%watch-ack (ack +.sign)
|
||||
%kick (emit watch-md-nacks:pass)
|
||||
::
|
||||
%fact
|
||||
?. =(%resource p.cage.sign) jn-core
|
||||
=+ !<(nack=resource q.cage.sign)
|
||||
?. =(nack rid) jn-core
|
||||
(cleanup %strange)
|
||||
==
|
||||
::
|
||||
%grp-nacks
|
||||
?+ -.sign !!
|
||||
%watch-ack (ack +.sign)
|
||||
%kick (emit watch-grp-nacks:pass)
|
||||
::
|
||||
%fact
|
||||
?. =(%resource p.cage.sign) jn-core
|
||||
=+ !<(nack=resource q.cage.sign)
|
||||
?. =(nack rid) jn-core
|
||||
(cleanup %strange)
|
||||
==
|
||||
|
||||
==
|
||||
::
|
||||
++ groups-fact
|
||||
@ -274,19 +407,15 @@
|
||||
?. ?=(%group-update-0 p.cage) jn-core
|
||||
=+ !<(=update:group-store q.cage)
|
||||
?. ?=(%initial-group -.update) jn-core
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
?. =(rid resource.update) jn-core
|
||||
%- emit-many
|
||||
=/ cag=^cage pull-hook-action+!>([%add [entity .]:rid])
|
||||
%- zing
|
||||
:~ [(poke-our:(jn-pass-io /pull-md) %metadata-pull-hook cag)]~
|
||||
[(poke-our:(jn-pass-io /pull-co) %contact-pull-hook cag)]~
|
||||
::
|
||||
?. scry-is-public:con ~
|
||||
:_ ~
|
||||
%+ poke:(jn-pass-io /share-co)
|
||||
[entity.rid %contact-push-hook]
|
||||
[%contact-share !>([%share our.bowl])]
|
||||
==
|
||||
=. jn-core (emit pull-md:pass)
|
||||
=. jn-core (emit pull-co:pass)
|
||||
?. |(share-co.request scry-is-public:con)
|
||||
jn-core
|
||||
?: scry-is-public:con (emit share-co:pass)
|
||||
=. jn-core (emit allow-co:pass)
|
||||
(emit share-co:pass)
|
||||
::
|
||||
++ md-fact
|
||||
|= [=mark =vase]
|
||||
@ -294,32 +423,40 @@
|
||||
=+ !<(=update:metadata vase)
|
||||
?. ?=(%initial-group -.update) jn-core
|
||||
?. =(group.update rid) jn-core
|
||||
|^ ^+ jn-core
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
=/ feed feed-rid
|
||||
=. jn-core (cleanup %done)
|
||||
?. hidden:(need (scry-group:grp rid))
|
||||
=/ hidden hidden:(need (scry-group:grp rid))
|
||||
=? jn-core ?&(!hidden ?=(^ feed))
|
||||
%- emit
|
||||
(pull-gra:pass (need feed))
|
||||
=? jn-core |(hidden autojoin.request)
|
||||
%- emit-many
|
||||
(turn graphs pull-gra:pass)
|
||||
jn-core
|
||||
::
|
||||
++ feed-rid
|
||||
^- (unit resource)
|
||||
=/ list-md=(list [=md-resource:metadata =association:metadata])
|
||||
%+ skim ~(tap by associations.update)
|
||||
|= [=md-resource:metadata =association:metadata]
|
||||
=(app-name.md-resource %groups)
|
||||
?> ?=(^ list-md)
|
||||
?~ list-md ~
|
||||
=* metadatum metadatum.association.i.list-md
|
||||
?. ?& ?=(%group -.config.metadatum)
|
||||
?=(^ feed.config.metadatum)
|
||||
?=(^ u.feed.config.metadatum)
|
||||
?=([~ ~ *] feed.config.metadatum)
|
||||
==
|
||||
jn-core
|
||||
=* feed resource.u.u.feed.config.metadatum
|
||||
%- emit
|
||||
%+ poke-our:(jn-pass-io /pull-feed) %graph-pull-hook
|
||||
pull-hook-action+!>([%add [entity .]:feed])
|
||||
%- emit-many
|
||||
%+ murn ~(tap by associations.update)
|
||||
|= [=md-resource:metadata =association:metadata]
|
||||
^- (unit card)
|
||||
?. =(app-name.md-resource %graph) ~
|
||||
=* rid resource.md-resource
|
||||
:- ~
|
||||
%+ poke-our:(jn-pass-io /pull-graph) %graph-pull-hook
|
||||
pull-hook-action+!>([%add [entity .]:rid])
|
||||
~
|
||||
`resource.u.u.feed.config.metadatum
|
||||
::
|
||||
++ graphs
|
||||
^- (list resource)
|
||||
%+ murn ~(tap by associations.update)
|
||||
|= [=md-resource:metadata =association:metadata]
|
||||
?. =(app-name.md-resource %graph) ~
|
||||
`resource.md-resource
|
||||
--
|
||||
::
|
||||
++ ack
|
||||
|= err=(unit tang)
|
||||
@ -327,66 +464,6 @@
|
||||
%- (slog u.err)
|
||||
(cleanup %strange)
|
||||
::
|
||||
++ notify
|
||||
%- emit
|
||||
%+ poke-our:(jn-pass-io /hark) %hark-store
|
||||
=- hark-action+!>(-)
|
||||
^- action:hark
|
||||
|^
|
||||
[%add-note bin body]
|
||||
++ bin
|
||||
^- bin:hark
|
||||
[/ [q.byk.bowl /join/(scot %p entity.rid)/[name.rid]]]
|
||||
++ title
|
||||
|= [name=@t rest=@t]
|
||||
text/(rap 3 'Joining group: "' name '" ' rest ~)
|
||||
++ body
|
||||
^- body:hark
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
?> ?=(final:view progress.request)
|
||||
=/ name (rap 3 (scot %p entity.rid) '/' name.rid ~)
|
||||
?- progress.request
|
||||
::
|
||||
%done
|
||||
=/ =metadatum:metadata (need (peek-metadatum:met %groups rid))
|
||||
:* ~[(title title.metadatum 'succeeded')]
|
||||
~
|
||||
now.bowl
|
||||
/
|
||||
/groups/(scot %p entity.rid)/[name.rid]
|
||||
==
|
||||
::
|
||||
%strange
|
||||
:* ~[(title name 'errored unexpectedly')]
|
||||
~
|
||||
now.bowl
|
||||
/
|
||||
/
|
||||
==
|
||||
::
|
||||
%no-perms
|
||||
:* ~[(title name 'failed, you are not permitted to join the group')]
|
||||
~
|
||||
now.bowl
|
||||
/
|
||||
/
|
||||
==
|
||||
==
|
||||
--
|
||||
::
|
||||
++ cleanup
|
||||
|= =progress:view
|
||||
=. jn-core
|
||||
(tx-progress progress)
|
||||
=. jn-core
|
||||
(emit (leave-our:(jn-pass-io /groups) %group-store))
|
||||
=. jn-core
|
||||
(emit (leave-our:(jn-pass-io /md) %metadata-store))
|
||||
=/ =request:view (~(got by joining) rid)
|
||||
=? jn-core (lte (sub now.bowl started.request) ~s30)
|
||||
notify
|
||||
=. joining (~(del by joining) rid)
|
||||
jn-core
|
||||
--
|
||||
--
|
||||
--
|
||||
|
@ -296,19 +296,21 @@
|
||||
++ on-watch
|
||||
|= =path
|
||||
?> (team:title [our src]:bowl)
|
||||
?. ?=([%preview @ @ @ ~] path)
|
||||
(on-watch:def path)
|
||||
=/ rid=resource
|
||||
(de-path:resource t.path)
|
||||
=/ prev=(unit group-preview:metadata)
|
||||
?^ (peek-metadatum:met %groups rid)
|
||||
(some (get-preview:met rid))
|
||||
(~(get by previews) rid)
|
||||
?~ prev
|
||||
:_ this(pending (~(put in pending) rid))
|
||||
(get-preview rid)^~
|
||||
:_ this
|
||||
(fact-init:io metadata-hook-update+!>([%preview u.prev]))^~
|
||||
?+ path (on-watch:def path)
|
||||
::
|
||||
[%preview @ @ @ ~]
|
||||
=/ rid=resource
|
||||
(de-path:resource t.path)
|
||||
=/ prev=(unit group-preview:metadata)
|
||||
?^ (peek-metadatum:met %groups rid)
|
||||
(some (get-preview:met rid))
|
||||
(~(get by previews) rid)
|
||||
?~ prev
|
||||
:_ this(pending (~(put in pending) rid))
|
||||
(get-preview rid)^~
|
||||
:_ this
|
||||
(fact-init:io metadata-hook-update+!>([%preview u.prev]))^~
|
||||
==
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
|
@ -13,9 +13,10 @@
|
||||
:~ create+create
|
||||
remove+remove
|
||||
join+join
|
||||
abort+dejs-path:resource
|
||||
leave+leave
|
||||
invite+invite
|
||||
hide+dejs-path:resource
|
||||
done+dejs-path:resource
|
||||
==
|
||||
::
|
||||
++ create
|
||||
@ -34,6 +35,9 @@
|
||||
%- ot
|
||||
:~ resource+dejs:resource
|
||||
ship+(su ;~(pfix sig fed:ag))
|
||||
app+(su (perk %groups %graph ~))
|
||||
'shareContact'^bo
|
||||
autojoin+bo
|
||||
==
|
||||
::
|
||||
++ invite
|
||||
@ -74,10 +78,13 @@
|
||||
++ request
|
||||
|= req=^request
|
||||
%- pairs
|
||||
:~ hidden+b+hidden.req
|
||||
started+(time started.req)
|
||||
:~ started+(time started.req)
|
||||
ship+(ship ship.req)
|
||||
progress+s+progress.req
|
||||
'shareContact'^b+share-co.req
|
||||
autojoin+b+autojoin.req
|
||||
app+s+`@t`app.req
|
||||
invite+a+(turn ~(tap in invite.req) (cork (cury scot %ux) (lead %s)))
|
||||
==
|
||||
::
|
||||
++ initial
|
||||
|
@ -13,7 +13,8 @@
|
||||
=/ members
|
||||
~(wyt in (members:grp rid))
|
||||
=/ =metadatum:store
|
||||
(need (peek-metadatum %groups rid))
|
||||
?^ met=(peek-metadatum %groups rid) u.met
|
||||
(need (peek-metadatum %graph rid))
|
||||
[rid channels members channel-count metadatum]
|
||||
::
|
||||
++ channels
|
||||
|
@ -319,12 +319,18 @@
|
||||
|= =path
|
||||
^- [(list card:agent:gall) agent:gall]
|
||||
?> (team:title our.bowl src.bowl)
|
||||
?. ?=([%tracking ~] path)
|
||||
?+ path
|
||||
:: forward by default
|
||||
=^ cards pull-hook
|
||||
(on-watch:og path)
|
||||
[cards this]
|
||||
:_ this
|
||||
~[give-update]
|
||||
::
|
||||
[%nack ~] `this
|
||||
::
|
||||
[%tracking ~]
|
||||
:_ this
|
||||
~[give-update]
|
||||
==
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
@ -455,7 +461,8 @@
|
||||
|= tan=(unit tang)
|
||||
?~ tan tr-core
|
||||
?. versioned
|
||||
(tr-ap-og:tr-cleanup |.((on-pull-nack:og rid u.tan)))
|
||||
%- tr-ap-og:tr-cleanup:tr-give-nack
|
||||
|.((on-pull-nack:og rid u.tan))
|
||||
%- (slog leaf+"versioned nack for {<rid>} in {<dap.bowl>}" u.tan)
|
||||
=/ pax
|
||||
(kick-mule:virt rid |.((on-pull-kick:og rid)))
|
||||
@ -569,6 +576,9 @@
|
||||
:: +| %subscription: subscription cards
|
||||
::
|
||||
::
|
||||
++ tr-give-nack
|
||||
(tr-emit (fact:io resource+!>(rid) /nack ~))
|
||||
::
|
||||
++ tr-ver-wire
|
||||
(make-wire /version)
|
||||
::
|
||||
|
@ -1,12 +1,27 @@
|
||||
/- *resource, *group
|
||||
^?
|
||||
|%
|
||||
+$ app ?(%graph %groups)
|
||||
+$ uid @uvH
|
||||
::
|
||||
:: $request: State of a join request
|
||||
::
|
||||
:: .started: Time request first sent
|
||||
:: .ship: Host of group
|
||||
:: .progress: Progress of request
|
||||
:: .share-co: Automatically share contact?
|
||||
:: .autojoin: Automatically join graphs
|
||||
:: .app: Whether we're joining a group or a graph
|
||||
:: .invite: Associated invites
|
||||
::
|
||||
+$ request
|
||||
$: hidden=?
|
||||
started=time
|
||||
$: started=time
|
||||
=ship
|
||||
=progress
|
||||
=app
|
||||
share-co=?
|
||||
autojoin=?
|
||||
invite=(set uid)
|
||||
==
|
||||
::
|
||||
+$ action
|
||||
@ -14,20 +29,38 @@
|
||||
[%create name=term =policy title=@t description=@t]
|
||||
[%remove =resource]
|
||||
:: client side
|
||||
[%join =resource =ship]
|
||||
$: %join
|
||||
=resource
|
||||
=ship
|
||||
=app
|
||||
share-contact=?
|
||||
autojoin=?
|
||||
==
|
||||
[%abort =resource]
|
||||
[%leave =resource]
|
||||
::
|
||||
[%invite =resource ships=(set ship) description=@t]
|
||||
:: pending ops
|
||||
[%hide =resource]
|
||||
[%done =resource]
|
||||
==
|
||||
|
||||
:: $progress: state of a join request
|
||||
::
|
||||
:: %start: Waiting on add poke to succeed
|
||||
:: %added: Waiting on groups
|
||||
:: %metadata: Waiting on metadata
|
||||
:: final: Join request succeeded/errors
|
||||
+$ progress
|
||||
?(%start %added final)
|
||||
?(%start %added %metadata final)
|
||||
::
|
||||
:: $final: resolution of a join request
|
||||
::
|
||||
:: %no-perms: Failed, did not have permissions
|
||||
:: %abort: Join request manually aborted
|
||||
:: %strange: Failed unexpectedly
|
||||
:: %done: Succeeded
|
||||
::
|
||||
+$ final
|
||||
?(%no-perms %strange %done)
|
||||
?(%no-perms %abort %strange %done)
|
||||
::
|
||||
+$ update
|
||||
$% [%initial initial=(map resource request)]
|
||||
|
@ -23,6 +23,8 @@
|
||||
[%add rid]
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-push-hook push-hook-act)
|
||||
;< ~ bind:m
|
||||
(poke-our %contact-push-hook push-hook-act)
|
||||
;< ~ bind:m
|
||||
%+ poke-our %group-store
|
||||
:- %group-update-0
|
||||
|
@ -99,11 +99,17 @@ export const changePolicy = (
|
||||
|
||||
export const join = (
|
||||
ship: string,
|
||||
name: string
|
||||
name: string,
|
||||
app: "groups" | "graph",
|
||||
autojoin: boolean,
|
||||
share: boolean
|
||||
): Poke<any> => viewAction({
|
||||
join: {
|
||||
resource: makeResource(ship, name),
|
||||
ship
|
||||
ship,
|
||||
shareContact: share || false,
|
||||
app,
|
||||
autojoin
|
||||
}
|
||||
});
|
||||
|
||||
@ -148,10 +154,10 @@ export const invite = (
|
||||
}
|
||||
});
|
||||
|
||||
export const hideGroup = (
|
||||
export const abortJoin = (
|
||||
resource: string
|
||||
): Poke<any> => viewAction({
|
||||
hide: resource
|
||||
abort: resource
|
||||
});
|
||||
|
||||
export const roleTags = ['janitor', 'moderator', 'admin'];
|
||||
@ -164,7 +170,8 @@ export const groupBunts = {
|
||||
|
||||
export const joinError = ['no-perms', 'strange'] as const;
|
||||
export const joinResult = ['done', ...joinError] as const;
|
||||
export const joinProgress = ['start', 'added', ...joinResult] as const;
|
||||
export const joinLoad = ['start', 'added', 'metadata'] as const;
|
||||
export const joinProgress = [...joinLoad, ...joinResult] as const;
|
||||
|
||||
export const roleForShip = (
|
||||
group: Group,
|
||||
|
@ -19,6 +19,10 @@ export interface JoinRequest {
|
||||
started: number;
|
||||
ship: Patp;
|
||||
progress: JoinProgress;
|
||||
shareContact: boolean;
|
||||
autojoin: boolean;
|
||||
app: 'graph' | 'groups';
|
||||
invite: string[];
|
||||
}
|
||||
|
||||
export interface JoinRequests {
|
||||
|
Loading…
Reference in New Issue
Block a user