mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-01 19:46:36 +03:00
Merge pull request #5563 from urbit/hm/landscape-perf-optimization
groups: perf optimization
This commit is contained in:
commit
c22bac9273
27990
pkg/grid/package-lock.json
generated
27990
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1
pkg/interface/.gitignore
vendored
1
pkg/interface/.gitignore
vendored
@ -6,6 +6,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.swp
|
||||
.DS_Store
|
||||
stats.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
@ -4,7 +4,6 @@ module.exports = {
|
||||
'@babel/transform-runtime',
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'react-hot-loader/babel'
|
||||
'@babel/plugin-proposal-class-properties'
|
||||
]
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||
const urbitrc = require('./urbitrc');
|
||||
const _ = require('lodash');
|
||||
const { execSync } = require('child_process');
|
||||
@ -47,7 +47,7 @@ if(urbitrc.URL) {
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: {
|
||||
app: './src/index.js'
|
||||
app: './src/index.tsx'
|
||||
// serviceworker: './src/serviceworker.js'
|
||||
},
|
||||
module: {
|
||||
@ -59,15 +59,14 @@ module.exports = {
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/typescript', ['@babel/preset-react', {
|
||||
runtime: 'automatic',
|
||||
development: true,
|
||||
importSource: '@welldone-software/why-did-you-render'
|
||||
development: true
|
||||
}]],
|
||||
plugins: [
|
||||
'@babel/transform-runtime',
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'react-hot-loader/babel'
|
||||
process.env.NODE_ENV !== 'production' && 'react-refresh/babel'
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -108,14 +107,15 @@ module.exports = {
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(GIT_DESC),
|
||||
'process.env.LANDSCAPE_STORAGE_VERSION': JSON.stringify(Date.now()),
|
||||
'process.env.LANDSCAPE_LAST_WIPE': JSON.stringify('2021-10-20'),
|
||||
'process.env.LANDSCAPE_LAST_WIPE': JSON.stringify('2021-10-20')
|
||||
}),
|
||||
|
||||
// new CleanWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Groups',
|
||||
template: './public/index.html'
|
||||
})
|
||||
}),
|
||||
process.env.NODE_ENV !== 'production' && new ReactRefreshWebpackPlugin()
|
||||
],
|
||||
watch: true,
|
||||
output: {
|
||||
|
112601
pkg/interface/package-lock.json
generated
112601
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -71,6 +71,7 @@
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@storybook/addon-actions": "^6.2.9",
|
||||
"@storybook/addon-essentials": "^6.2.9",
|
||||
"@storybook/addon-links": "^6.2.9",
|
||||
@ -85,7 +86,6 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||
"@typescript-eslint/parser": "^4.24.0",
|
||||
"@urbit/eslint-config": "^1.0.0",
|
||||
"@welldone-software/why-did-you-render": "^6.1.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-loader": "^8.2.2",
|
||||
@ -103,7 +103,6 @@
|
||||
"lint-staged": "^11.0.0",
|
||||
"loki": "^0.28.1",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"sass": "^1.32.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"storybook-addon-designs": "^6.0.0",
|
||||
@ -120,6 +119,7 @@
|
||||
"tsc:watch": "tsc --watch",
|
||||
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
|
||||
"build:profile": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js --profile --json > stats.json",
|
||||
"start": "webpack-dev-server --config config/webpack.dev.js",
|
||||
"test": "tsc && jest",
|
||||
"jest": "jest",
|
||||
|
@ -3,6 +3,5 @@ import * as ReactDOM from 'react-dom';
|
||||
import './register-sw';
|
||||
import './storage-wipe';
|
||||
import App from './views/App';
|
||||
import './wdyr';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
@ -1,51 +0,0 @@
|
||||
import { deSig, Path, PatpNoSig, Group, Resource, roleTags, RoleTags } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function roleForShip(
|
||||
group: Group,
|
||||
ship: PatpNoSig
|
||||
): RoleTags | undefined {
|
||||
return roleTags.reduce((currRole, role) => {
|
||||
const roleShips = group?.tags?.role?.[role];
|
||||
return roleShips && roleShips.has(ship) ? role : currRole;
|
||||
}, undefined as RoleTags | undefined);
|
||||
}
|
||||
|
||||
export function resourceFromPath(path: Path): Resource {
|
||||
const [, , ship, name] = path.split('/');
|
||||
return { ship, name };
|
||||
}
|
||||
|
||||
export function makeResource(ship: string, name: string) {
|
||||
return { ship, name };
|
||||
}
|
||||
|
||||
export function isWriter(group: Group, resource: string) {
|
||||
const writers: Set<string> | undefined = _.get(
|
||||
group,
|
||||
['tags', 'graph', resource, 'writers'],
|
||||
undefined
|
||||
);
|
||||
const admins = group?.tags?.role?.admin ?? new Set();
|
||||
if (_.isUndefined(writers)) {
|
||||
return true;
|
||||
} else {
|
||||
return writers.has(window.ship) || admins.has(window.ship);
|
||||
}
|
||||
}
|
||||
|
||||
export function isChannelAdmin(group: Group, resource: string, ship = `~${window.ship}`) {
|
||||
const role = roleForShip(group, deSig(ship));
|
||||
|
||||
return (
|
||||
isHost(resource, ship) ||
|
||||
role === 'admin' ||
|
||||
role === 'moderator'
|
||||
);
|
||||
}
|
||||
|
||||
export function isHost(resource: string, ship = `~${window.ship}`) {
|
||||
const [, , host] = resource.split('/');
|
||||
|
||||
return ship === host;
|
||||
}
|
@ -1,28 +1,11 @@
|
||||
import {
|
||||
cite,
|
||||
NotificationGraphConfig,
|
||||
Post,
|
||||
Unreads
|
||||
} from '@urbit/api';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import _ from 'lodash';
|
||||
import f from 'lodash/fp';
|
||||
import { emptyHarkStats } from '../state/hark';
|
||||
|
||||
export function getLastSeen(
|
||||
unreads: Unreads,
|
||||
path: string,
|
||||
index: string
|
||||
): BigInteger | undefined {
|
||||
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
|
||||
if (!(typeof lastSeenIdx === 'string')) {
|
||||
return bigInt.zero;
|
||||
}
|
||||
return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))(
|
||||
lastSeenIdx
|
||||
);
|
||||
}
|
||||
|
||||
export function getHarkStats(unreads: Unreads, path: string) {
|
||||
return unreads?.[path] ?? emptyHarkStats();
|
||||
}
|
||||
@ -53,4 +36,3 @@ export function isWatching(
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
7
pkg/interface/src/logic/lib/history.ts
Normal file
7
pkg/interface/src/logic/lib/history.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
const history = createBrowserHistory({
|
||||
basename: '/apps/landscape'
|
||||
});
|
||||
|
||||
export default history;
|
@ -1,4 +1,4 @@
|
||||
import { isChannelAdmin } from '~/logic/lib/group';
|
||||
import { isChannelAdmin } from '@urbit/api';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import { createJoinParams } from '~/views/landscape/components/Join/Join';
|
||||
|
||||
@ -49,7 +49,7 @@ const commandIndex = function (currentGroup, groups, associations) {
|
||||
const association = currentGroup ? associations?.groups?.[currentGroup] : null;
|
||||
const canAdd =
|
||||
(group && association)
|
||||
? (association.metadata.vip === 'member-metadata' || isChannelAdmin(group, currentGroup))
|
||||
? (association.metadata.vip === 'member-metadata' || isChannelAdmin(group, currentGroup, window.ship))
|
||||
: !currentGroup; // home workspace or hasn't loaded
|
||||
const workspace = currentGroup || '/home';
|
||||
commands.push(result('Groups: Create', '/~landscape/new', 'Groups', null));
|
||||
|
52
pkg/interface/src/logic/lib/useThemeWatcher.ts
Normal file
52
pkg/interface/src/logic/lib/useThemeWatcher.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import dark from '@tlon/indigo-dark';
|
||||
import light from '@tlon/indigo-light';
|
||||
import { useEffect } from 'react';
|
||||
import useLocalState, { selectLocalState } from '../state/local';
|
||||
import useSettingsState, { selectDisplayState } from '../state/settings';
|
||||
|
||||
const selLocal = selectLocalState(['dark', 'set']);
|
||||
|
||||
export function useThemeWatcher() {
|
||||
const { set, dark: isDark } = useLocalState(selLocal);
|
||||
const display = useSettingsState(selectDisplayState);
|
||||
const theme = ((isDark && display.theme == 'auto') || display.theme == 'dark') ? dark : light;
|
||||
|
||||
useEffect(() => {
|
||||
const updateTheme = (e: MediaQueryListEvent) => set(s => ({ dark: e.matches }));
|
||||
const updateMobile = (e: MediaQueryListEvent) => set(s => ({ mobile: e.matches }));
|
||||
const updateSmall = (e: MediaQueryListEvent) => set(s => ({ breaks: { sm: e.matches } }));
|
||||
const updateMedium = (e: MediaQueryListEvent) => set(s => ({ breaks: { md: e.matches } }));
|
||||
const updateLarge = (e: MediaQueryListEvent) => set(s => ({ breaks: { lg: e.matches } }));
|
||||
|
||||
const themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const mobileWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[0]})`);
|
||||
const smallWatcher = window.matchMedia(`(min-width: ${theme.breakpoints[0]})`);
|
||||
const mediumWatcher = window.matchMedia(`(min-width: ${theme.breakpoints[1]})`);
|
||||
const largeWatcher = window.matchMedia(`(min-width: ${theme.breakpoints[2]})`);
|
||||
|
||||
themeWatcher.addEventListener('change', updateTheme);
|
||||
mobileWatcher.addEventListener('change', updateMobile);
|
||||
smallWatcher.addEventListener('change', updateSmall);
|
||||
mediumWatcher.addEventListener('change', updateMedium);
|
||||
largeWatcher.addEventListener('change', updateLarge);
|
||||
|
||||
updateTheme({ matches: themeWatcher.matches } as MediaQueryListEvent);
|
||||
updateMobile({ matches: mobileWatcher.matches } as MediaQueryListEvent);
|
||||
updateSmall({ matches: smallWatcher.matches } as MediaQueryListEvent);
|
||||
updateMedium({ matches: mediumWatcher.matches } as MediaQueryListEvent);
|
||||
updateLarge({ matches: largeWatcher.matches } as MediaQueryListEvent);
|
||||
|
||||
return () => {
|
||||
themeWatcher.removeEventListener('change', updateTheme);
|
||||
mobileWatcher.removeEventListener('change', updateMobile);
|
||||
smallWatcher.removeEventListener('change', updateSmall);
|
||||
mediumWatcher.removeEventListener('change', updateMedium);
|
||||
largeWatcher.removeEventListener('change', updateLarge);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
display,
|
||||
theme
|
||||
};
|
||||
}
|
@ -2,11 +2,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { patp2dec } from 'urbit-ob';
|
||||
import f from 'lodash/fp';
|
||||
import { Association, Contact, Patp } from '@urbit/api';
|
||||
import { Association, Patp } from '@urbit/api';
|
||||
import { enableMapSet } from 'immer';
|
||||
/* eslint-disable max-lines */
|
||||
import anyAscii from 'any-ascii';
|
||||
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import { IconRef, Workspace } from '~/types';
|
||||
|
||||
@ -515,7 +514,6 @@ export const svgDataURL = svg => 'data:image/svg+xml;base64,' + btoa(svg);
|
||||
|
||||
export const svgBlobURL = svg => URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
|
||||
|
||||
|
||||
export function binaryIndexOf(arr: BigInteger[], target: BigInteger): number | undefined {
|
||||
let leftBound = 0;
|
||||
let rightBound = arr.length - 1;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
Enc,
|
||||
Group,
|
||||
GroupPolicy, GroupUpdate,
|
||||
GroupUpdate,
|
||||
InvitePolicy, InvitePolicyDiff, OpenPolicy, OpenPolicyDiff, Tags
|
||||
} from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
@ -13,26 +13,10 @@ import { GroupState as State } from '../state/group';
|
||||
type GroupState = BaseState<State> & State;
|
||||
|
||||
function decodeGroup(group: Enc<Group>): Group {
|
||||
const members = new Set(group.members);
|
||||
const res = {
|
||||
return {
|
||||
...group,
|
||||
members,
|
||||
tags: decodeTags(group.tags),
|
||||
policy: decodePolicy(group.policy)
|
||||
tags: decodeTags(group.tags)
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
function decodePolicy(policy: Enc<GroupPolicy>): GroupPolicy {
|
||||
if ('invite' in policy) {
|
||||
const { invite } = policy;
|
||||
return { invite: { pending: new Set(invite.pending) } };
|
||||
} else {
|
||||
const { open } = policy;
|
||||
return {
|
||||
open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function decodeTags(tags: Enc<Tags>): Tags {
|
||||
@ -40,11 +24,11 @@ function decodeTags(tags: Enc<Tags>): Tags {
|
||||
tags,
|
||||
(acc, ships: any, key): Tags => {
|
||||
if (key.search(/\\/) === -1) {
|
||||
acc.role[key] = new Set(ships);
|
||||
acc.role[key] = ships;
|
||||
return acc;
|
||||
} else {
|
||||
const [app, tag, resource] = key.split('\\');
|
||||
_.set(acc, [app, resource, tag], new Set(ships));
|
||||
_.set(acc, [app, resource, tag], ships);
|
||||
return acc;
|
||||
}
|
||||
},
|
||||
@ -79,9 +63,9 @@ const addGroup = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
const { resource, policy, hidden } = json.addGroup;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
state.groups[resourcePath] = {
|
||||
members: new Set(),
|
||||
tags: { role: { admin: new Set([window.ship]) } },
|
||||
policy: decodePolicy(policy),
|
||||
members: [],
|
||||
tags: { role: { admin: [window.ship] } },
|
||||
policy,
|
||||
hidden
|
||||
};
|
||||
}
|
||||
@ -105,13 +89,19 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
return;
|
||||
}
|
||||
for (const member of ships) {
|
||||
state.groups[resourcePath].members.add(member);
|
||||
if (
|
||||
'invite' in state.groups[resourcePath].policy &&
|
||||
state.groups[resourcePath].policy['invite'].pending.has(member)
|
||||
) {
|
||||
state.groups[resourcePath].policy['invite'].pending.delete(member);
|
||||
}
|
||||
const members = state.groups[resourcePath].members;
|
||||
if (!_.includes(members, member)) {
|
||||
members.push(member);
|
||||
}
|
||||
|
||||
const policy = state.groups[resourcePath].policy;
|
||||
if ('invite' in policy) {
|
||||
const invites = (policy as InvitePolicy).invite;
|
||||
|
||||
if (invites && _.includes(invites.pending, member)) {
|
||||
_.remove(invites.pending, item => item === member);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return state;
|
||||
@ -122,7 +112,7 @@ const removeMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
const { resource, ships } = json.removeMembers;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
for (const member of ships) {
|
||||
state.groups[resourcePath].members.delete(member);
|
||||
_.remove(state.groups[resourcePath].members, item => item === member);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
@ -177,12 +167,14 @@ const inviteChangePolicy = (diff: InvitePolicyDiff, policy: InvitePolicy) => {
|
||||
if ('addInvites' in diff) {
|
||||
const { addInvites } = diff;
|
||||
for (const ship of addInvites) {
|
||||
policy.invite.pending.add(ship);
|
||||
if (!_.includes(policy.invite.pending, ship)) {
|
||||
policy.invite.pending.push(ship);
|
||||
}
|
||||
}
|
||||
} else if ('removeInvites' in diff) {
|
||||
const { removeInvites } = diff;
|
||||
for (const ship of removeInvites) {
|
||||
policy.invite.pending.delete(ship);
|
||||
_.remove(policy.invite.pending, item => item === ship);
|
||||
}
|
||||
} else {
|
||||
console.log('bad policy change');
|
||||
@ -193,22 +185,24 @@ const openChangePolicy = (diff: OpenPolicyDiff, policy: OpenPolicy) => {
|
||||
if ('allowRanks' in diff) {
|
||||
const { allowRanks } = diff;
|
||||
for (const rank of allowRanks) {
|
||||
policy.open.banRanks.delete(rank);
|
||||
_.remove(policy.open.banRanks, item => item === rank);
|
||||
}
|
||||
} else if ('banRanks' in diff) {
|
||||
const { banRanks } = diff;
|
||||
for (const rank of banRanks) {
|
||||
policy.open.banRanks.delete(rank);
|
||||
_.remove(policy.open.banRanks, item => item === rank);
|
||||
}
|
||||
} else if ('allowShips' in diff) {
|
||||
const { allowShips } = diff;
|
||||
for (const ship of allowShips) {
|
||||
policy.open.banned.delete(ship);
|
||||
_.remove(policy.open.banned, item => item === ship);
|
||||
}
|
||||
} else if ('banShips' in diff) {
|
||||
const { banShips } = diff;
|
||||
for (const ship of banShips) {
|
||||
policy.open.banned.add(ship);
|
||||
if (!_.includes(policy.open.banned, ship)) {
|
||||
policy.open.banned.push(ship);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('bad policy change');
|
||||
|
@ -39,6 +39,7 @@ const associations = (json: MetadataUpdate, state: MetadataState): MetadataState
|
||||
if (data) {
|
||||
state.associations = normalizeAssociations(data);
|
||||
state.loaded = true;
|
||||
state.onLoad();
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
@ -69,5 +69,4 @@ export const favicon = () => {
|
||||
return svg;
|
||||
};
|
||||
|
||||
|
||||
export default useContactState;
|
||||
|
@ -41,7 +41,7 @@ const useGroupState = createState<GroupState>(
|
||||
},
|
||||
|
||||
}),
|
||||
['groups'],
|
||||
[],
|
||||
[
|
||||
(set, get) =>
|
||||
createSubscription('group-store', '/groups', (e) => {
|
||||
|
@ -36,6 +36,7 @@ export interface HarkState {
|
||||
poke: (poke: Poke<any>) => Promise<void>;
|
||||
getMore: () => Promise<boolean>;
|
||||
opened: () => void;
|
||||
readCount: (path: string) => Promise<void>;
|
||||
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
|
||||
unseen: Timebox;
|
||||
seen: Timebox;
|
||||
@ -44,7 +45,6 @@ export interface HarkState {
|
||||
notificationsGroupConfig: string[];
|
||||
unreads: Unreads;
|
||||
archiveNote: (bin: HarkBin, lid: HarkLid) => Promise<void>;
|
||||
readCount: (path: string) => Promise<void>;
|
||||
readGraph: (graph: string) => Promise<void>;
|
||||
readGroup: (group: string) => Promise<void>;
|
||||
}
|
||||
@ -60,20 +60,20 @@ const useHarkState = createState<HarkState>(
|
||||
},
|
||||
readGraph: async (graph: string) => {
|
||||
const prefix = `/graph/${graph.slice(6)}`;
|
||||
let counts = [] as string[];
|
||||
let eaches = [] as [string, string][];
|
||||
const counts = [] as string[];
|
||||
const eaches = [] as [string, string][];
|
||||
Object.entries(get().unreads).forEach(([path, unreads]) => {
|
||||
if (path.startsWith(prefix)) {
|
||||
if(unreads.count > 0) {
|
||||
counts.push(path);
|
||||
}
|
||||
unreads.each.forEach(unread => {
|
||||
unreads.each.forEach((unread) => {
|
||||
eaches.push([path, unread]);
|
||||
});
|
||||
}
|
||||
});
|
||||
get().set(draft => {
|
||||
counts.forEach(path => {
|
||||
get().set((draft) => {
|
||||
counts.forEach((path) => {
|
||||
draft.unreads[path].count = 0;
|
||||
});
|
||||
eaches.forEach(([path, each]) => {
|
||||
@ -86,8 +86,7 @@ const useHarkState = createState<HarkState>(
|
||||
].map(pok => api.poke(pok)));
|
||||
},
|
||||
readGroup: async (group: string) => {
|
||||
const graphs =
|
||||
_.pickBy(useMetadataState.getState().associations.graph, a => a.group === group);
|
||||
const graphs = _.pickBy(useMetadataState.getState().associations.graph, a => a.group === group);
|
||||
await Promise.all(Object.keys(graphs).map(get().readGraph));
|
||||
},
|
||||
readCount: async (path) => {
|
||||
|
@ -7,7 +7,9 @@ import {
|
||||
reduceStateN
|
||||
} from './base';
|
||||
import airlock from '~/logic/api';
|
||||
import history from '~/logic/lib/history';
|
||||
import { reduce } from '../reducers/metadata-update';
|
||||
import { getNotificationRedirect } from '../lib/notificationRedirects';
|
||||
|
||||
export const METADATA_MAX_PREVIEW_WAIT = 150000;
|
||||
|
||||
@ -16,6 +18,7 @@ export interface MetadataState {
|
||||
loaded: boolean;
|
||||
getPreview: (group: string) => Promise<MetadataUpdatePreview
|
||||
>;
|
||||
onLoad: () => void;
|
||||
previews: {
|
||||
[group: string]: MetadataUpdatePreview
|
||||
}
|
||||
@ -53,6 +56,9 @@ const useMetadataState = createState<MetadataState>(
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
onLoad: () => {
|
||||
handleGridRedirect();
|
||||
}
|
||||
}),
|
||||
['loaded'],
|
||||
@ -67,6 +73,12 @@ const useMetadataState = createState<MetadataState>(
|
||||
]
|
||||
);
|
||||
|
||||
const { graph, groups } = useMetadataState.getState().associations;
|
||||
|
||||
if (Object.keys(graph).length > 0 || Object.keys(groups).length > 0) {
|
||||
handleGridRedirect();
|
||||
}
|
||||
|
||||
export function useAssocForGraph(graph: string) {
|
||||
return useMetadataState(
|
||||
useCallback(s => s.associations.graph[graph] as Association | undefined, [
|
||||
@ -111,6 +123,17 @@ export function usePreview(group: string) {
|
||||
return { error, preview };
|
||||
}
|
||||
|
||||
function handleGridRedirect() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
|
||||
if(query.has('grid-note')) {
|
||||
history.push(getNotificationRedirect(query.get('grid-note')));
|
||||
} else if(query.has('grid-link')) {
|
||||
const link = decodeURIComponent(query.get('grid-link')!);
|
||||
history.push(`/perma${link}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function useGraphsForGroup(group: string) {
|
||||
const graphs = useMetadataState(s => s.associations.graph);
|
||||
return _.pickBy(graphs, (a: Association) => a.group === group);
|
||||
|
@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import { InviteItem, InviteItemProps } from '~/views/components/Invite';
|
||||
import { JoinProgress } from '@urbit/api';
|
||||
|
||||
export default {
|
||||
title: 'Notifications/Invite',
|
||||
component: InviteItem
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<InviteItemProps> = args => (
|
||||
<Box backgroundColor="white" p="0" maxWidth="90%" width="fit-content">
|
||||
<InviteItem {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
const pendingJoin = (progress: JoinProgress) => ({
|
||||
hidden: false,
|
||||
started: Date.now() - 3600,
|
||||
ship: '~haddef-sigwen',
|
||||
progress
|
||||
});
|
||||
|
||||
export const Pending = Template.bind({});
|
||||
Pending.args = {
|
||||
pendingJoin: pendingJoin('start'),
|
||||
resource: '/ship/~bollug-worlus/urbit-index'
|
||||
};
|
||||
|
||||
export const Errored = Template.bind({});
|
||||
Errored.args = {
|
||||
pendingJoin: pendingJoin('no-perms'),
|
||||
resource: '/ship/~bollug-worlus/urbit-index'
|
||||
};
|
||||
|
||||
export const Done = Template.bind({});
|
||||
Done.args = {
|
||||
pendingJoin: pendingJoin('done'),
|
||||
resource: '/ship/~bollug-worlus/urbit-index'
|
||||
};
|
@ -1,8 +1,10 @@
|
||||
import { PatpNoSig } from '@urbit/api';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ship: PatpNoSig;
|
||||
desk: string;
|
||||
hark: typeof useHarkState.getState;
|
||||
}
|
||||
}
|
||||
|
@ -1,249 +0,0 @@
|
||||
import dark from '@tlon/indigo-dark';
|
||||
import light from '@tlon/indigo-light';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import shallow from 'zustand/shallow';
|
||||
import 'mousetrap-global-bind';
|
||||
import * as React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import 'react-hot-loader';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { BrowserRouter as Router, withRouter } from 'react-router-dom';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import gcpManager from '~/logic/lib/gcpManager';
|
||||
import { svgDataURL } from '~/logic/lib/util';
|
||||
import withState from '~/logic/lib/withState';
|
||||
import useContactState, { favicon } from '~/logic/state/contact';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
|
||||
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import './apps/chat/css/custom.css';
|
||||
import Omnibox from './components/leap/Omnibox';
|
||||
import StatusBar from './components/StatusBar';
|
||||
import './css/fonts.css';
|
||||
import './css/indigo-static.css';
|
||||
import { Content } from './landscape/components/Content';
|
||||
import './landscape/css/custom.css';
|
||||
import { bootstrapApi } from '~/logic/api/bootstrap';
|
||||
import { uxToHex } from '@urbit/api';
|
||||
|
||||
function ensureValidHex(color) {
|
||||
if (!color)
|
||||
return '#000000';
|
||||
|
||||
const isUx = color.startsWith('0x');
|
||||
const parsedColor = isUx ? uxToHex(color) : color;
|
||||
|
||||
return parsedColor.startsWith('#') ? parsedColor : `#${parsedColor}`;
|
||||
}
|
||||
|
||||
const Root = withState(styled.div`
|
||||
font-family: ${p => p.theme.fonts.sans};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-left: env(safe-area-inset-left, 0px);
|
||||
padding-right: env(safe-area-inset-right, 0px);
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
|
||||
margin: 0;
|
||||
${p => p.display.backgroundType === 'url' ? `
|
||||
background-image: url('${p.display.background}');
|
||||
background-size: cover;
|
||||
` : p.display.backgroundType === 'color' ? `
|
||||
background-color: ${ensureValidHex(p.display.background)};
|
||||
` : `background-color: ${p.theme.colors.white};`
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
touch-action: none;
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${ p => p.theme.colors.gray } transparent;
|
||||
}
|
||||
|
||||
/* Works on Chrome/Edge/Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: ${ p => p.theme.colors.gray };
|
||||
border-radius: 1rem;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
`, [
|
||||
[useSettingsState, ['display']]
|
||||
]);
|
||||
|
||||
const StatusBarWithRouter = withRouter(StatusBar);
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ship = window.ship;
|
||||
|
||||
this.updateTheme = this.updateTheme.bind(this);
|
||||
this.updateMobile = this.updateMobile.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
bootstrapApi();
|
||||
this.props.getShallowChildren(`~${window.ship}`, 'dm-inbox');
|
||||
const theme = this.getTheme();
|
||||
setTimeout(() => {
|
||||
// Something about how the store works doesn't like changing it
|
||||
// before the app has actually rendered, hence the timeout.
|
||||
this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.mobileWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[0]})`);
|
||||
this.smallWatcher = window.matchMedia(`(min-width: ${theme.breakpoints[0]})`);
|
||||
this.mediumWatcher = window.matchMedia(`(min-width: ${theme.breakpoints[1]})`);
|
||||
this.largeWatcher = window.matchMedia(`(min-width: ${theme.breakpoints[2]})`);
|
||||
// TODO: addListener is deprecated, but safari 13 requires it
|
||||
this.themeWatcher.addListener(this.updateTheme);
|
||||
this.mobileWatcher.addListener(this.updateMobile);
|
||||
this.smallWatcher.addListener(this.updateSmall);
|
||||
this.mediumWatcher.addListener(this.updateMedium);
|
||||
this.largeWatcher.addListener(this.updateLarge);
|
||||
|
||||
this.updateMobile(this.mobileWatcher);
|
||||
this.updateSmall(this.updateSmall);
|
||||
this.updateTheme(this.themeWatcher);
|
||||
this.updateMedium(this.mediumWatcher);
|
||||
this.updateLarge(this.largeWatcher);
|
||||
}, 500);
|
||||
this.props.getAll();
|
||||
gcpManager.start();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this.props.toggleOmnibox();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.themeWatcher.removeListener(this.updateTheme);
|
||||
this.mobileWatcher.removeListener(this.updateMobile);
|
||||
this.smallWatcher.removeListener(this.updateSmall);
|
||||
this.mediumWatcher.removeListener(this.updateMedium);
|
||||
this.largeWatcher.removeListener(this.updateLarge);
|
||||
}
|
||||
|
||||
updateTheme(e) {
|
||||
this.props.set((state) => {
|
||||
state.dark = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
updateMobile(e) {
|
||||
this.props.set((state) => {
|
||||
state.mobile = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
updateSmall = (e) => {
|
||||
this.props.set((state) => {
|
||||
state.breaks.sm = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
updateMedium = (e) => {
|
||||
this.props.set((state) => {
|
||||
state.breaks.md = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
updateLarge = (e) => {
|
||||
this.props.set((state) => {
|
||||
state.breaks.lg = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
getTheme() {
|
||||
const { props } = this;
|
||||
return ((props.dark && props?.display?.theme == 'auto') ||
|
||||
props?.display?.theme == 'dark'
|
||||
) ? dark : light;
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = this.getTheme();
|
||||
|
||||
const { ourContact } = this.props;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ShortcutContextProvider>
|
||||
<Helmet>
|
||||
{window.ship.length < 14
|
||||
? <link rel="icon" type="image/svg+xml" href={svgDataURL(favicon())} />
|
||||
: null}
|
||||
</Helmet>
|
||||
<Root>
|
||||
<Router basename="/apps/landscape">
|
||||
<ErrorBoundary>
|
||||
<StatusBarWithRouter
|
||||
props={this.props}
|
||||
ourContact={ourContact}
|
||||
connection={'foo'}
|
||||
subscription={this.subscription}
|
||||
ship={this.ship}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Omnibox
|
||||
show={this.props.omniboxShown}
|
||||
toggle={this.props.toggleOmnibox}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Content
|
||||
ship={this.ship}
|
||||
subscription={this.subscription}
|
||||
connection={'aa'}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
</Root>
|
||||
<div id="portal-root" />
|
||||
</ShortcutContextProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
const WarmApp = process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
|
||||
const selContacts = s => s.contacts[`~${window.ship}`];
|
||||
const selLocal = s => [s.set, s.omniboxShown, s.toggleOmnibox, s.dark];
|
||||
const selSettings = s => [s.display, s.getAll];
|
||||
const selGraph = s => s.getShallowChildren;
|
||||
|
||||
const WithApp = React.forwardRef((props, ref) => {
|
||||
const ourContact = useContactState(selContacts);
|
||||
const [display, getAll] = useSettingsState(selSettings, shallow);
|
||||
const [setLocal, omniboxShown, toggleOmnibox, dark] = useLocalState(selLocal);
|
||||
const getShallowChildren = useGraphState(selGraph);
|
||||
|
||||
return (
|
||||
<WarmApp
|
||||
ref={ref}
|
||||
ourContact={ourContact}
|
||||
display={display}
|
||||
getAll={getAll}
|
||||
set={setLocal}
|
||||
dark={dark}
|
||||
getShallowChildren={getShallowChildren}
|
||||
toggleOmnibox={toggleOmnibox}
|
||||
omniboxShown={omniboxShown}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
WarmApp.whyDidYouRender = true;
|
||||
|
||||
export default WithApp;
|
||||
|
137
pkg/interface/src/views/App.tsx
Normal file
137
pkg/interface/src/views/App.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import Mousetrap from 'mousetrap';
|
||||
import shallow from 'zustand/shallow';
|
||||
import 'mousetrap-global-bind';
|
||||
import * as React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Router, withRouter } from 'react-router-dom';
|
||||
import styled, { ThemeProvider } from 'styled-components';
|
||||
import gcpManager from '~/logic/lib/gcpManager';
|
||||
import { svgDataURL } from '~/logic/lib/util';
|
||||
import history from '~/logic/lib/history';
|
||||
import { favicon } from '~/logic/state/contact';
|
||||
import useLocalState, { selectLocalState } from '~/logic/state/local';
|
||||
import useSettingsState, { selectSettingsState, SettingsState } from '~/logic/state/settings';
|
||||
import useGraphState, { GraphState } from '~/logic/state/graph';
|
||||
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
|
||||
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import './apps/chat/css/custom.css';
|
||||
import Omnibox from './components/leap/Omnibox';
|
||||
import StatusBar from './components/StatusBar';
|
||||
import './css/fonts.css';
|
||||
import './css/indigo-static.css';
|
||||
import { Content } from './landscape/components/Content';
|
||||
import './landscape/css/custom.css';
|
||||
import { bootstrapApi } from '~/logic/api/bootstrap';
|
||||
import { uxToHex } from '@urbit/api';
|
||||
import { useThemeWatcher } from '~/logic/lib/useThemeWatcher';
|
||||
|
||||
function ensureValidHex(color) {
|
||||
if (!color)
|
||||
return '#000000';
|
||||
|
||||
const isUx = color.startsWith('0x');
|
||||
const parsedColor = isUx ? uxToHex(color) : color;
|
||||
|
||||
return parsedColor.startsWith('#') ? parsedColor : `#${parsedColor}`;
|
||||
}
|
||||
|
||||
interface RootProps {
|
||||
display: SettingsState['display'];
|
||||
}
|
||||
|
||||
const Root = styled.div<RootProps>`
|
||||
font-family: ${p => p.theme.fonts.sans};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-left: env(safe-area-inset-left, 0px);
|
||||
padding-right: env(safe-area-inset-right, 0px);
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
|
||||
margin: 0;
|
||||
${p => p.display.backgroundType === 'url' ? `
|
||||
background-image: url('${p.display.background}');
|
||||
background-size: cover;
|
||||
` : p.display.backgroundType === 'color' ? `
|
||||
background-color: ${ensureValidHex(p.display.background)};
|
||||
` : `background-color: ${p.theme.colors.white};`
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
touch-action: none;
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${ p => p.theme.colors.gray } transparent;
|
||||
}
|
||||
|
||||
/* Works on Chrome/Edge/Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: ${ p => p.theme.colors.gray };
|
||||
border-radius: 1rem;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusBarWithRouter = withRouter(StatusBar);
|
||||
|
||||
const selLocal = selectLocalState(['toggleOmnibox']);
|
||||
const selSettings = selectSettingsState(['display', 'getAll']);
|
||||
const selGraph = (s: GraphState) => s.getShallowChildren;
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
const { getAll } = useSettingsState(selSettings, shallow);
|
||||
const { toggleOmnibox } = useLocalState(selLocal);
|
||||
const getShallowChildren = useGraphState(selGraph);
|
||||
const { theme, display } = useThemeWatcher();
|
||||
|
||||
React.useEffect(() => {
|
||||
bootstrapApi();
|
||||
getShallowChildren(`~${window.ship}`, 'dm-inbox');
|
||||
|
||||
getAll();
|
||||
gcpManager.start();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
toggleOmnibox();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ShortcutContextProvider>
|
||||
<Helmet>
|
||||
{window.ship.length < 14
|
||||
? <link rel="icon" type="image/svg+xml" href={svgDataURL(favicon())} />
|
||||
: null}
|
||||
</Helmet>
|
||||
<Root display={display}>
|
||||
<Router history={history}>
|
||||
<ErrorBoundary>
|
||||
<StatusBarWithRouter />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Omnibox />
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Content />
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
</Root>
|
||||
<div id="portal-root" />
|
||||
</ShortcutContextProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { Association, Content, createPost, fetchIsAllowed, Post, removePosts, deSig } from '@urbit/api';
|
||||
import { Association, Content, createPost, deSig, fetchIsAllowed, isWriter, Post, removePosts, resourceFromPath } from '@urbit/api';
|
||||
import { BigInteger } from 'big-integer';
|
||||
import _ from 'lodash';
|
||||
import React, {
|
||||
ReactElement, useCallback,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
||||
useMemo, useState
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||
import shallow from 'zustand/shallow';
|
||||
import airlock from '~/logic/api';
|
||||
import { disallowedShipsForOurContact } from '~/logic/lib/contact';
|
||||
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||
import { toHarkPath } from '~/logic/lib/util';
|
||||
import useGraphState, { useGraphForAssoc } from '~/logic/state/graph';
|
||||
import { useGroupForAssoc } from '~/logic/state/group';
|
||||
import useHarkState, { useHarkStat } from '~/logic/state/hark';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import { ChatPane } from './components/ChatPane';
|
||||
import airlock from '~/logic/api';
|
||||
import { disallowedShipsForOurContact } from '~/logic/lib/contact';
|
||||
import shallow from 'zustand/shallow';
|
||||
import { toHarkPath } from '~/logic/lib/util';
|
||||
|
||||
const getCurrGraphSize = (ship: string, name: string) => {
|
||||
const { graphs } = useGraphState.getState();
|
||||
@ -37,7 +38,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
const graph = useGraphForAssoc(association);
|
||||
const stats = useHarkStat(toHarkPath(association.resource));
|
||||
const unreadCount = stats.count;
|
||||
const canWrite = group ? isWriter(group, resource) : false;
|
||||
const canWrite = group ? isWriter(group, resource, window.ship) : false;
|
||||
const [
|
||||
getNewest,
|
||||
getOlderSiblings,
|
||||
@ -89,7 +90,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
);
|
||||
|
||||
const isAdmin = useMemo(
|
||||
() => group ? group.tags.role.admin.has(deSig(window.ship)) : false,
|
||||
() => (group ? _.includes(group.tags.role.admin, deSig(window.ship)) : false),
|
||||
[group]
|
||||
);
|
||||
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { Center, Text } from '@tlon/indigo-react';
|
||||
import { GraphConfig, joinGraph } from '@urbit/api';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { deSig } from '~/logic/lib/util';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
const GraphApp = (): ReactElement => {
|
||||
const associations= useMetadataState(state => state.associations);
|
||||
const graphKeys = useGraphState(state => state.graphKeys);
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
|
||||
render={(props) => {
|
||||
const resource =
|
||||
`${deSig(props.match.params.ship)}/${props.match.params.name}`;
|
||||
const { ship, name } = props.match.params;
|
||||
const path = `/ship/~${deSig(ship)}/${name}`;
|
||||
const association = associations.graph[path];
|
||||
|
||||
const autoJoin = () => {
|
||||
try {
|
||||
airlock.thread(joinGraph(
|
||||
`~${deSig(props.match.params.ship)}`,
|
||||
props.match.params.name
|
||||
));
|
||||
} catch(err) {
|
||||
setTimeout(autoJoin, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
if(!graphKeys.has(resource)) {
|
||||
autoJoin();
|
||||
} else if(Boolean(association) && 'graph' in association.metadata.config) {
|
||||
history.push(`/~landscape/home/resource/${(association.metadata.config as GraphConfig).graph}${path}`);
|
||||
}
|
||||
return (
|
||||
<Center width="100%" height="100%">
|
||||
<Text fontSize={1}>Redirecting...</Text>
|
||||
</Center>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphApp;
|
@ -1,18 +1,18 @@
|
||||
/* eslint-disable max-lines-per-function */
|
||||
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 { createJoinParams, Join, JoinRoute } from "~/views/landscape/components/Join/Join";
|
||||
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 { createJoinParams, JoinRoute } from '~/views/landscape/components/Join/Join';
|
||||
|
||||
const ScrollbarLessBox = styled(Box)`
|
||||
scrollbar-width: none !important;
|
||||
@ -22,12 +22,8 @@ const ScrollbarLessBox = styled(Box)`
|
||||
}
|
||||
`;
|
||||
|
||||
interface LaunchAppProps {
|
||||
connection: string;
|
||||
}
|
||||
|
||||
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
const notificationsCount = useHarkState((state) => state.notificationsCount);
|
||||
export const LaunchApp = (): ReactElement | null => {
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
const calmState = useSettingsState(selectCalmState);
|
||||
const { hideUtilities, hideGroups } = calmState;
|
||||
const history = useHistory();
|
||||
@ -36,22 +32,22 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>
|
||||
{notificationsCount ? `(${String(notificationsCount)}) ` : ""}Groups
|
||||
{notificationsCount ? `(${String(notificationsCount)}) ` : ''}Groups
|
||||
</title>
|
||||
</Helmet>
|
||||
<Route path="/join/:ship/:name">
|
||||
<Route path='/join/:ship/:name'>
|
||||
<JoinRoute />
|
||||
</Route>
|
||||
<ScrollbarLessBox
|
||||
height="100%"
|
||||
overflowY="scroll"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
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}
|
||||
@ -59,22 +55,22 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
{!hideUtilities && (
|
||||
<>
|
||||
<Tile
|
||||
bg="white"
|
||||
color="scales.black20"
|
||||
to="/~landscape/home"
|
||||
bg='white'
|
||||
color='scales.black20'
|
||||
to='/~landscape/home'
|
||||
p={0}
|
||||
>
|
||||
<Box
|
||||
p={2}
|
||||
height="100%"
|
||||
width="100%"
|
||||
bg="scales.black20"
|
||||
height='100%'
|
||||
width='100%'
|
||||
bg='scales.black20'
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
borderColor='lightGray'
|
||||
>
|
||||
<Row alignItems="center">
|
||||
<Icon color="black" icon="Home" />
|
||||
<Text ml={2} mt="1px" color="black">
|
||||
<Row alignItems='center'>
|
||||
<Icon color='black' icon='Home' />
|
||||
<Text ml={2} mt='1px' color='black'>
|
||||
My Channels
|
||||
</Text>
|
||||
</Row>
|
||||
@ -82,10 +78,10 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
</Tile>
|
||||
<Tiles />
|
||||
<ModalButton
|
||||
icon="Plus"
|
||||
bg="white"
|
||||
color="black"
|
||||
text="New Group"
|
||||
icon='Plus'
|
||||
bg='white'
|
||||
color='black'
|
||||
text='New Group'
|
||||
style={{ gridColumnStart: 1 }}
|
||||
>
|
||||
<NewGroup />
|
||||
@ -96,9 +92,9 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
borderRadius={2}
|
||||
onClick={() => history.push({ search: createJoinParams('groups') })}
|
||||
>
|
||||
<Row backgroundColor="white" gapX="2" p={2} height="100%" width="100%" alignItems="center">
|
||||
<Icon icon="BootNode" />
|
||||
<Text fontWeight="medium" whiteSpace="nowrap">Join Group</Text>
|
||||
<Row backgroundColor='white' gapX='2' p={2} height='100%' width='100%' alignItems='center'>
|
||||
<Icon icon='BootNode' />
|
||||
<Text fontWeight='medium' whiteSpace='nowrap'>Join Group</Text>
|
||||
</Row>
|
||||
</Button>
|
||||
</>
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Association, deSig, Graph, Group } from '@urbit/api';
|
||||
import { Association, deSig, Graph, Group, isWriter } from '@urbit/api';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import React, {
|
||||
Component, ReactNode
|
||||
} from 'react';
|
||||
import { isWriter } from '~/logic/lib/group';
|
||||
import { GraphScroller } from '~/views/components/GraphScroller';
|
||||
import { LinkItem } from './components/LinkItem';
|
||||
import LinkSubmit from './components/LinkSubmit';
|
||||
@ -40,7 +39,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
||||
|
||||
canWrite() {
|
||||
const { group, association } = this.props;
|
||||
return isWriter(group, association.resource);
|
||||
return isWriter(group, association.resource, window.ship);
|
||||
}
|
||||
|
||||
renderItem = React.forwardRef<HTMLDivElement>(({ index }: RendererProps, ref) => {
|
||||
|
@ -1,8 +1,17 @@
|
||||
import { Action, Anchor, Box, Col, Icon, Row, Rule, Text } from '@tlon/indigo-react';
|
||||
import { Association, GraphNode, Group, markEachAsRead, removePosts, TextContent, UrlContent, ReferenceContent } from '@urbit/api';
|
||||
import {
|
||||
Association,
|
||||
GraphNode,
|
||||
Group,
|
||||
markEachAsRead,
|
||||
removePosts,
|
||||
TextContent,
|
||||
UrlContent,
|
||||
ReferenceContent,
|
||||
roleForShip
|
||||
} from '@urbit/api';
|
||||
import React, { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { getPermalinkForGraph, referenceToPermalink } from '~/logic/lib/permalinks';
|
||||
import { useCopy } from '~/logic/lib/useCopy';
|
||||
import { useHarkStat } from '~/logic/state/hark';
|
||||
|
@ -1,85 +0,0 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
Invite,
|
||||
AppInvites,
|
||||
JoinRequest
|
||||
} from '@urbit/api';
|
||||
import { alphabeticalOrder, resourceAsPath } from '~/logic/lib/util';
|
||||
import useInviteState from '~/logic/state/invite';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import { PendingDm } from './PendingDm';
|
||||
import InviteItem from '~/views/components/Invite';
|
||||
|
||||
interface InvitesProps {
|
||||
pendingJoin?: any;
|
||||
}
|
||||
|
||||
interface InviteRef {
|
||||
uid: string;
|
||||
app: string;
|
||||
invite: Invite;
|
||||
}
|
||||
|
||||
export function Invites(props: InvitesProps): ReactElement {
|
||||
const invites = useInviteState(state => state.invites);
|
||||
|
||||
const pendingDms = useGraphState(s => s.pendingDms) ?? [];
|
||||
|
||||
const inviteArr: InviteRef[] = _.reduce(
|
||||
invites,
|
||||
(acc: InviteRef[], val: AppInvites, app: string) => {
|
||||
const appInvites = _.reduce(
|
||||
val,
|
||||
(invs: InviteRef[], invite: Invite, uid: string) => {
|
||||
return [...invs, { invite, uid, app }];
|
||||
},
|
||||
[]
|
||||
);
|
||||
return [...acc, ...appInvites];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const pendingJoin = _.omitBy(props.pendingJoin, 'hidden');
|
||||
|
||||
const invitesAndStatus: { [rid: string]: JoinRequest | InviteRef } = {
|
||||
..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)),
|
||||
...pendingJoin
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...pendingDms].map(ship => (
|
||||
<PendingDm key={ship} ship={`~${ship}`} />
|
||||
))}
|
||||
{Object.keys(invitesAndStatus)
|
||||
.sort(alphabeticalOrder)
|
||||
.map((resource) => {
|
||||
const inviteOrStatus = invitesAndStatus[resource];
|
||||
const join = pendingJoin[resource];
|
||||
if ('progress' in inviteOrStatus) {
|
||||
return (
|
||||
<InviteItem
|
||||
key={resource}
|
||||
resource={resource}
|
||||
pendingJoin={join}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const { app, uid, invite } = inviteOrStatus;
|
||||
return (
|
||||
<InviteItem
|
||||
key={resource}
|
||||
invite={invite}
|
||||
app={app}
|
||||
uid={uid}
|
||||
resource={resource}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
@ -37,7 +37,7 @@ export function NavLink({
|
||||
);
|
||||
}
|
||||
|
||||
export default function NotificationsScreen(props: any): ReactElement {
|
||||
export default function NotificationsScreen(): ReactElement {
|
||||
const relativePath = (p: string) => baseUrl + p;
|
||||
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
|
@ -9,7 +9,6 @@ import _ from 'lodash';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import { MarkdownField } from '~/views/apps/publish/components/MarkdownField';
|
||||
@ -22,7 +21,7 @@ import {
|
||||
ProfileImages, ProfileStatus
|
||||
} from './Profile';
|
||||
import airlock from '~/logic/api';
|
||||
import { editContact, setPublic } from '@urbit/api';
|
||||
import { editContact, setPublic, resourceFromPath } from '@urbit/api';
|
||||
|
||||
const formSchema = Yup.object({
|
||||
nickname: Yup.string(),
|
||||
|
@ -6,7 +6,7 @@ import useContactState from '~/logic/state/contact';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { Profile } from './components/Profile';
|
||||
|
||||
export default function ProfileScreen(props: any) {
|
||||
export default function ProfileScreen() {
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
return (
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Action, Box, Col, Row, Text } from '@tlon/indigo-react';
|
||||
import { Association, Graph, GraphNode, Group, markEachAsRead, removePosts } from '@urbit/api';
|
||||
import { Association, Graph, GraphNode, Group, markEachAsRead, removePosts, roleForShip } from '@urbit/api';
|
||||
import bigInt from 'big-integer';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||
import { getComments, getLatestRevision } from '~/logic/lib/publish';
|
||||
import { useCopy } from '~/logic/lib/useCopy';
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Box, Text } from '@tlon/indigo-react';
|
||||
import { addTag, Association, Group } from '@urbit/api';
|
||||
import { addTag, Association, Group, resourceFromPath } from '@urbit/api';
|
||||
import { Form, Formik } from 'formik';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
import { ShipSearch } from '~/views/components/ShipSearch';
|
||||
import airlock from '~/logic/api';
|
||||
|
@ -67,7 +67,7 @@ function SettingsItem(props: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsScreen(props: any) {
|
||||
export default function SettingsScreen() {
|
||||
const location = useLocation();
|
||||
const hash = location.hash.slice(1);
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
@ -120,12 +120,7 @@ return;
|
||||
</Col>
|
||||
<Col flexGrow={1} overflowY='auto'>
|
||||
<SettingsItem>
|
||||
{hash === 'notifications' && (
|
||||
<NotificationPreferences
|
||||
{...props}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
/>
|
||||
)}
|
||||
{hash === 'notifications' && <NotificationPreferences />}
|
||||
{hash === 'display' && <DisplayForm />}
|
||||
{hash === 'dm' && <DmSettings />}
|
||||
{hash === 'shortcuts' && <ShortcutSettings />}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Action, Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { GraphNode, Group, removePosts } from '@urbit/api';
|
||||
import { GraphNode, Group, removePosts, roleForShip } from '@urbit/api';
|
||||
import bigInt from 'big-integer';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||
import { getLatestCommentRevision } from '~/logic/lib/publish';
|
||||
import { useCopy } from '~/logic/lib/useCopy';
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
Group,
|
||||
markCountAsRead,
|
||||
addPost,
|
||||
isWriter,
|
||||
resourceFromPath
|
||||
} from '@urbit/api';
|
||||
import bigInt from 'big-integer';
|
||||
import { FormikHelpers } from 'formik';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { isWriter } from '~/logic/lib/group';
|
||||
import { getUnreadCount } from '~/logic/lib/hark';
|
||||
import { referenceToPermalink } from '~/logic/lib/permalinks';
|
||||
import { getLatestCommentRevision } from '~/logic/lib/publish';
|
||||
@ -137,7 +137,7 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
|
||||
const harkPath = toHarkPath(association.resource, parentIndex);
|
||||
const readCount = children.length - getUnreadCount(unreads, harkPath);
|
||||
|
||||
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';
|
||||
const canComment = isWriter(group, association.resource, window.ship) || association.metadata.vip === 'reader-comments';
|
||||
|
||||
return (
|
||||
<Col {...rest} minWidth={0}>
|
||||
|
@ -6,12 +6,11 @@ import {
|
||||
ErrorLabel, Icon, Label,
|
||||
Row, Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { Association, OpenPolicy } from '@urbit/api';
|
||||
import { Association, OpenPolicy, roleForShip } from '@urbit/api';
|
||||
import { FieldArray, useFormikContext } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import { DropdownSearch } from './DropdownSearch';
|
||||
|
@ -1,318 +0,0 @@
|
||||
import { css } from '@styled-system/css';
|
||||
import { Box, Icon, LoadingSpinner, Row, Text } from '@tlon/indigo-react';
|
||||
import {
|
||||
accept,
|
||||
decline,
|
||||
Invite,
|
||||
joinProgress,
|
||||
joinResult,
|
||||
JoinRequest,
|
||||
Metadata,
|
||||
MetadataUpdatePreview,
|
||||
resourceFromPath
|
||||
} from '@urbit/api';
|
||||
import { GraphConfig } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement, ReactNode, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { useRunIO } from '~/logic/lib/useRunIO';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { cite, isDm } from '~/logic/lib/util';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useMetadataState, { useAssocForGraph } from '~/logic/state/metadata';
|
||||
import { PropFunc } from '~/types';
|
||||
import { Header } from '~/views/apps/notifications/header';
|
||||
import { MetadataIcon } from '~/views/landscape/components/MetadataIcon';
|
||||
import { StatelessAsyncButton } from '../StatelessAsyncButton';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
interface GroupInviteProps {
|
||||
preview?: MetadataUpdatePreview;
|
||||
status?: JoinRequest;
|
||||
app?: string;
|
||||
uid?: string;
|
||||
invite?: Invite;
|
||||
resource: string;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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'
|
||||
];
|
||||
|
||||
function inviteUrl(hidden: boolean, resource: string, metadata?: Metadata) {
|
||||
if (!hidden) {
|
||||
return `/~landscape${resource}`;
|
||||
}
|
||||
|
||||
if ((metadata?.config as GraphConfig).graph === 'chat') {
|
||||
return `/~landscape/messages/resource/${
|
||||
(metadata?.config as GraphConfig)?.graph
|
||||
}${resource}`;
|
||||
} else {
|
||||
return `/~landscape/home/resource/${
|
||||
(metadata?.config as GraphConfig)?.graph
|
||||
}${resource}`;
|
||||
}
|
||||
}
|
||||
function InviteMetadata(props: {
|
||||
preview?: MetadataUpdatePreview;
|
||||
resource: string;
|
||||
}) {
|
||||
const { resource, preview } = props;
|
||||
const { ship, name } = resourceFromPath(resource);
|
||||
const dm = isDm(resource);
|
||||
if (dm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = (children: ReactNode) => (
|
||||
<Row overflow="hidden" height={4} gapX={2} alignItems="center">
|
||||
{children}
|
||||
</Row>
|
||||
);
|
||||
|
||||
if (preview) {
|
||||
const { title } = preview.metadata;
|
||||
const { members } = preview;
|
||||
return container(
|
||||
<>
|
||||
<MetadataIcon height={4} width={4} metadata={preview.metadata} />
|
||||
<Text fontWeight="medium">{title}</Text>
|
||||
<Text gray fontWeight="medium">
|
||||
{members} Member{members > 1 ? 's' : ''}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return container(
|
||||
<>
|
||||
<Text whiteSpace="nowrap" textOverflow="ellipsis" ml="1px" mb="2px" mono>
|
||||
{cite(ship)}/{name}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InviteStatus(props: { status?: JoinRequest }) {
|
||||
const { status } = props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const current = status && joinProgress.indexOf(status.progress);
|
||||
const desc = _.isNumber(current) && description[current];
|
||||
return (
|
||||
<Row gapX={2} alignItems="center" minHeight={4}>
|
||||
<Row alignItems="center" flexShrink={0}>
|
||||
{joinResult.includes(status?.progress as any) ? (
|
||||
<Icon icon={status?.progress === 'done' ? 'Checkmark' : 'X'} />
|
||||
) : (
|
||||
<LoadingSpinner dark />
|
||||
)}
|
||||
</Row>
|
||||
<Text gray>{desc}</Text>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export function useInviteAccept(resource: string, app?: string, uid?: string) {
|
||||
const { ship, name } = resourceFromPath(resource);
|
||||
const history = useHistory();
|
||||
const associations = useMetadataState(s => s.associations);
|
||||
const groups = useGroupState(s => s.groups);
|
||||
const graphKeys = useGraphState(s => s.graphKeys);
|
||||
|
||||
const waiter = useWaitForProps({ associations, graphKeys, groups });
|
||||
return useRunIO<void, boolean>(
|
||||
async () => {
|
||||
if (!(app && uid)) {
|
||||
return false;
|
||||
}
|
||||
if (resource in groups) {
|
||||
await airlock.poke(decline(app, uid));
|
||||
return false;
|
||||
}
|
||||
|
||||
await airlock.poke(accept(app, uid));
|
||||
await waiter((p) => {
|
||||
return (
|
||||
(resource in p.groups &&
|
||||
resource in (p.associations?.graph ?? {}) &&
|
||||
p.graphKeys.has(resource.slice(7))) ||
|
||||
resource in (p.associations?.groups ?? {})
|
||||
);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
(success: boolean) => {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
const redir = inviteUrl(
|
||||
groups?.[resource]?.hidden,
|
||||
resource,
|
||||
associations?.graph?.[resource]?.metadata
|
||||
);
|
||||
if (redir) {
|
||||
// weird race condition
|
||||
setTimeout(() => {
|
||||
history.push(redir);
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
resource
|
||||
);
|
||||
}
|
||||
|
||||
function InviteActions(props: {
|
||||
status?: JoinRequest;
|
||||
resource: string;
|
||||
app?: string;
|
||||
uid?: string;
|
||||
}) {
|
||||
const { status, resource, app, uid } = props;
|
||||
const inviteAccept = useInviteAccept(resource, app, uid);
|
||||
|
||||
const inviteDecline = useCallback(async () => {
|
||||
if (!(app && uid)) {
|
||||
return;
|
||||
}
|
||||
await airlock.poke(decline(app, uid));
|
||||
}, [app, uid]);
|
||||
|
||||
|
||||
if (status) {
|
||||
return (
|
||||
<Row gapX={2} alignItems="center" height={4}>
|
||||
<StatelessAsyncButton
|
||||
height={4}
|
||||
backgroundColor="white"
|
||||
onClick={async () => {}}
|
||||
>
|
||||
{[...joinResult].includes(status?.progress as any)
|
||||
? 'Dismiss'
|
||||
: 'Cancel'}
|
||||
</StatelessAsyncButton>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gapX={2} alignItems="center" height={4}>
|
||||
<StatelessAsyncButton
|
||||
color="blue"
|
||||
height={4}
|
||||
backgroundColor="white"
|
||||
onClick={inviteAccept as any}
|
||||
>
|
||||
Accept
|
||||
</StatelessAsyncButton>
|
||||
<StatelessAsyncButton
|
||||
height={4}
|
||||
backgroundColor="white"
|
||||
onClick={inviteDecline as any}
|
||||
>
|
||||
Decline
|
||||
</StatelessAsyncButton>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
const responsiveStyle = ({ gapXY = 0 as number | number[] }) => {
|
||||
return css({
|
||||
flexDirection: ['column', 'row'],
|
||||
'& > *': {
|
||||
marginTop: _.isArray(gapXY) ? [gapXY[0], 0] : [gapXY, 0],
|
||||
marginLeft: _.isArray(gapXY) ? [0, ...gapXY.slice(1)] : [0, gapXY]
|
||||
},
|
||||
'& > :first-child': {
|
||||
marginTop: 0,
|
||||
marginLeft: 0
|
||||
}
|
||||
});
|
||||
};
|
||||
const ResponsiveRow = styled(Row)(responsiveStyle);
|
||||
export function GroupInvite(props: GroupInviteProps): ReactElement {
|
||||
const { resource, preview, invite, status, app, uid } = props;
|
||||
const dm = isDm(resource);
|
||||
const history = useHistory();
|
||||
|
||||
const invitedTo = dm ? 'DM' : 'group';
|
||||
const graphAssoc = useAssocForGraph(resource);
|
||||
|
||||
const headerProps = status
|
||||
? { description: `You are joining a ${invitedTo}` }
|
||||
: { description: `invited you to a ${invitedTo}`, authors: [invite!.ship] };
|
||||
|
||||
const onClick = () => {
|
||||
if (status?.progress === 'done') {
|
||||
const redir = inviteUrl(app !== 'groups', resource, graphAssoc?.metadata);
|
||||
if (redir) {
|
||||
history.push(redir);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header content {...headerProps} />
|
||||
<Row
|
||||
onClick={onClick}
|
||||
height={[null, 4]}
|
||||
alignItems="flex-start"
|
||||
gridArea="main"
|
||||
>
|
||||
<Elbow display={['none', 'block']} mx={2} />
|
||||
<ResponsiveRow
|
||||
gapXY={2}
|
||||
height={[null, 4]}
|
||||
alignItems={['flex-start', 'center']}
|
||||
>
|
||||
<InviteMetadata preview={preview} resource={resource} />
|
||||
<InviteStatus status={status} />
|
||||
<InviteActions
|
||||
resource={resource}
|
||||
status={status}
|
||||
app={app}
|
||||
uid={uid}
|
||||
/>
|
||||
</ResponsiveRow>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { Col, Row, Rule } from '@tlon/indigo-react';
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import { PropFunc } from '~/types';
|
||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||
|
||||
export interface InviteSkeletonProps {
|
||||
onAccept: () => Promise<any>;
|
||||
onDecline: () => Promise<any>;
|
||||
acceptDesc: string;
|
||||
declineDesc: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function InviteSkeleton(
|
||||
props: InviteSkeletonProps & PropFunc<typeof Col>
|
||||
): ReactElement {
|
||||
const {
|
||||
children,
|
||||
acceptDesc,
|
||||
declineDesc,
|
||||
onAccept,
|
||||
onDecline,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<>
|
||||
<Col width="100%" p={1} {...rest}>
|
||||
{children}
|
||||
<Row px={4} gapX={4}>
|
||||
<StatelessAsyncAction
|
||||
name="accept"
|
||||
bg="transparent"
|
||||
onClick={onAccept}
|
||||
color="blue"
|
||||
mr={2}
|
||||
>
|
||||
{acceptDesc}
|
||||
</StatelessAsyncAction>
|
||||
<StatelessAsyncAction
|
||||
name="decline"
|
||||
bg="transparent"
|
||||
color="red"
|
||||
onClick={onDecline}
|
||||
>
|
||||
{declineDesc}
|
||||
</StatelessAsyncAction>
|
||||
</Row>
|
||||
</Col>
|
||||
<Rule borderColor="washedGray" />
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { Col, Rule } from '@tlon/indigo-react';
|
||||
import { JoinRequest } from '@urbit/api';
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import { PropFunc } from '~/types/util';
|
||||
import { JoiningStatus } from '~/views/apps/notifications/joining';
|
||||
|
||||
type JoinSkeletonProps = {
|
||||
children: ReactNode;
|
||||
status: JoinRequest;
|
||||
resource: string;
|
||||
} & PropFunc<typeof Col>;
|
||||
|
||||
export function JoinSkeleton(props: JoinSkeletonProps): ReactElement {
|
||||
const { resource, children, status, ...rest } = props;
|
||||
return (
|
||||
<>
|
||||
<Col p={1} {...rest}>
|
||||
{children}
|
||||
<JoiningStatus resource={resource} status={status} />
|
||||
</Col>
|
||||
<Rule borderColor="washedGray" />
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { Invite, JoinRequest } from '@urbit/api';
|
||||
import React from 'react';
|
||||
import { usePreview } from '~/logic/state/metadata';
|
||||
import { GroupInvite } from './Group';
|
||||
|
||||
export interface InviteItemProps {
|
||||
invite?: Invite;
|
||||
resource: string;
|
||||
pendingJoin?: JoinRequest;
|
||||
app?: string;
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
export function InviteItem(props: InviteItemProps) {
|
||||
const { pendingJoin, invite, resource, uid, app } = props;
|
||||
|
||||
const { preview } = usePreview(resource);
|
||||
|
||||
if (pendingJoin?.hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupInvite
|
||||
resource={resource}
|
||||
preview={preview}
|
||||
invite={invite}
|
||||
status={pendingJoin}
|
||||
uid={uid}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default InviteItem;
|
@ -36,7 +36,7 @@ const getNicknameForShips = (groups: Groups, contacts: Rolodex, selected: string
|
||||
const peerSet = new Set<string>();
|
||||
const nicknames = new Map<string, string[]>();
|
||||
_.forEach(groups, (group, path) => {
|
||||
if (group.members.size > 0) {
|
||||
if (group.members.length > 0) {
|
||||
const groupEntries = group.members.values();
|
||||
for (const member of groupEntries) {
|
||||
if(!selected.includes(member)) {
|
||||
|
@ -22,8 +22,8 @@ import useHarkState from '~/logic/state/hark';
|
||||
|
||||
const localSel = selectLocalState(['toggleOmnibox']);
|
||||
|
||||
const StatusBar = (props) => {
|
||||
const { ship } = props;
|
||||
const StatusBar = () => {
|
||||
const ship = window.ship;
|
||||
const ourContact = useContactState(state => state.contacts[`~${ship}`]);
|
||||
const metaKey = window.navigator.platform.includes('Mac') ? '⌘' : 'Ctrl+';
|
||||
const { toggleOmnibox } = useLocalState(localSel);
|
||||
@ -64,7 +64,6 @@ const StatusBar = (props) => {
|
||||
borderColor='lightGray'
|
||||
mr={2}
|
||||
px={2}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon='Dashboard' color='black' />
|
||||
</Button>
|
||||
|
@ -3,7 +3,6 @@ import { omit } from 'lodash';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import fuzzy from 'fuzzy';
|
||||
import _ from 'lodash';
|
||||
import f from 'lodash/fp';
|
||||
import React, {
|
||||
ReactElement, useCallback,
|
||||
useEffect, useMemo,
|
||||
@ -17,30 +16,24 @@ import defaultApps from '~/logic/lib/default-apps';
|
||||
import makeIndex, { OmniboxItem } from '~/logic/lib/omnibox';
|
||||
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
|
||||
import { deSig } from '~/logic/lib/util';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import useInviteState from '~/logic/state/invite';
|
||||
import useLaunchState from '~/logic/state/launch';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||
import { Portal } from '../Portal';
|
||||
import OmniboxInput from './OmniboxInput';
|
||||
import OmniboxResult from './OmniboxResult';
|
||||
|
||||
interface OmniboxProps {
|
||||
show: boolean;
|
||||
toggle: () => void;
|
||||
notifications: number;
|
||||
}
|
||||
import { selectLocalState } from '~/logic/state/local';
|
||||
|
||||
const SEARCHED_CATEGORIES = [
|
||||
'commands',
|
||||
'ships',
|
||||
'other',
|
||||
'groups',
|
||||
'subscriptions',
|
||||
'subscriptions'
|
||||
];
|
||||
const settingsSel = (s: SettingsState) => s.leap;
|
||||
const CAT_LIMIT = 6;
|
||||
@ -50,20 +43,22 @@ const CAT_LIMIT = 6;
|
||||
*/
|
||||
function flattenCattegoryMap(cats: string[], catMap: Map<string, OmniboxItem[]>) {
|
||||
let res = [] as OmniboxItem[];
|
||||
cats.forEach(cat => {
|
||||
cats.forEach((cat) => {
|
||||
res = res.concat(_.take(catMap.get(cat), CAT_LIMIT));
|
||||
});
|
||||
|
||||
return res;
|
||||
|
||||
}
|
||||
|
||||
export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
const selOmnibox = selectLocalState(['omniboxShown', 'toggleOmnibox']);
|
||||
|
||||
export function Omnibox(): ReactElement {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const leapConfig = useSettingsState(settingsSel);
|
||||
const omniboxRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const { omniboxShown: show, toggleOmnibox: toggle } = useLocalState(selOmnibox);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [selected, setSelected] = useState<[] | [string, string]>([]);
|
||||
@ -102,17 +97,17 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
}, [selectedGroup, leapConfig, contacts, associations, groups]);
|
||||
|
||||
const onOutsideClick = useCallback(() => {
|
||||
props.show && props.toggle();
|
||||
}, [props.show, props.toggle]);
|
||||
show && toggle();
|
||||
}, [show, toggle]);
|
||||
|
||||
useOutsideClick(omniboxRef, onOutsideClick);
|
||||
|
||||
// handle omnibox show
|
||||
useEffect(() => {
|
||||
if (!props.show) {
|
||||
if (!show) {
|
||||
return;
|
||||
}
|
||||
Mousetrap.bind('escape', props.toggle);
|
||||
Mousetrap.bind('escape', toggle);
|
||||
const touchstart = new Event('touchstart');
|
||||
// @ts-ignore ref typings
|
||||
inputRef?.current?.input?.dispatchEvent(touchstart);
|
||||
@ -122,7 +117,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
Mousetrap.unbind('escape');
|
||||
setQuery('');
|
||||
};
|
||||
}, [props.show]);
|
||||
}, [show]);
|
||||
|
||||
const initialResults = useMemo(() => {
|
||||
return new Map<string, OmniboxItem[]>(
|
||||
@ -145,7 +140,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
}
|
||||
const q = query.toLowerCase();
|
||||
const resultsMap = new Map<string, OmniboxItem[]>();
|
||||
let categoryMaxes: Record<string, number> = {};
|
||||
const categoryMaxes: Record<string, number> = {};
|
||||
|
||||
SEARCHED_CATEGORIES.map((category) => {
|
||||
const categoryIndex = index.get(category);
|
||||
@ -156,7 +151,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
.reduce((a,b) => Math.max(a,b), 0);
|
||||
resultsMap.set(category, fuzzied.map(a => a.original));
|
||||
});
|
||||
let order = Object.entries(categoryMaxes)
|
||||
const order = Object.entries(categoryMaxes)
|
||||
.sort(([,a],[,b]) => b - a)
|
||||
.map(([id]) => id);
|
||||
return [resultsMap, order];
|
||||
@ -164,7 +159,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
|
||||
const navigate = useCallback(
|
||||
(app: string, link: string, shift: boolean) => {
|
||||
props.toggle();
|
||||
toggle();
|
||||
if (
|
||||
defaultApps.includes(app.toLowerCase()) ||
|
||||
app === 'profile' ||
|
||||
@ -177,20 +172,20 @@ 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);
|
||||
|
||||
}
|
||||
} else {
|
||||
window.location.href = link;
|
||||
}
|
||||
},
|
||||
[history, props.toggle]
|
||||
[history, toggle]
|
||||
);
|
||||
|
||||
const setPreviousSelected = useCallback(() => {
|
||||
@ -249,8 +244,8 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
if (query.length > 0) {
|
||||
setQuery('');
|
||||
return;
|
||||
} else if (props.show) {
|
||||
props.toggle();
|
||||
} else if (show) {
|
||||
toggle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -268,7 +263,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
}
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
let values = flattenCattegoryMap(categoryOrder, results);
|
||||
const values = flattenCattegoryMap(categoryOrder, results);
|
||||
if (selected.length) {
|
||||
navigate(selected[0], selected[1], evt.shiftKey);
|
||||
} else if (values.length === 0) {
|
||||
@ -283,11 +278,11 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
}
|
||||
},
|
||||
[
|
||||
props.toggle,
|
||||
toggle,
|
||||
selected,
|
||||
navigate,
|
||||
query,
|
||||
props.show,
|
||||
show,
|
||||
results,
|
||||
categoryOrder,
|
||||
setPreviousSelected,
|
||||
@ -296,7 +291,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const flattenedResultLinks: [string, string][] =
|
||||
const flattenedResultLinks: [string, string][] =
|
||||
flattenCattegoryMap(categoryOrder, results)
|
||||
.map(result => [result.app, result.link]);
|
||||
if (!flattenedResultLinks.includes(selected as [string, string])) {
|
||||
@ -308,24 +303,6 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
setQuery(event.target.value);
|
||||
}, []);
|
||||
|
||||
// Sort Omnibox results alphabetically
|
||||
const sortResults = (
|
||||
a: Record<'title', string>,
|
||||
b: Record<'title', string>
|
||||
) => {
|
||||
// Do not sort unless searching (preserves order of menu actions)
|
||||
if (query === '') {
|
||||
return 0;
|
||||
}
|
||||
if (a.title < b.title) {
|
||||
return -1;
|
||||
}
|
||||
if (a.title > b.title) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const renderResults = useCallback(() => {
|
||||
return (
|
||||
<Box
|
||||
@ -389,7 +366,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
top={0}
|
||||
right={0}
|
||||
zIndex={11}
|
||||
display={props.show ? 'block' : 'none'}
|
||||
display={show ? 'block' : 'none'}
|
||||
>
|
||||
<Row justifyContent='center'>
|
||||
<Box
|
||||
@ -419,5 +396,5 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
// @ts-ignore investigate zustand types
|
||||
export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']);
|
||||
|
||||
export default Omnibox;
|
||||
|
@ -5,12 +5,11 @@ import {
|
||||
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { addTag, Association, Group, PermVariation, removeTag, metadataEdit, deSig } from '@urbit/api';
|
||||
import { addTag, Association, Group, PermVariation, removeTag, metadataEdit, deSig, resourceFromPath } from '@urbit/api';
|
||||
import { Form, Formik } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { FormGroupChild } from '~/views/components/FormGroup';
|
||||
import { shipSearchSchemaInGroup } from '~/views/components/ShipSearch';
|
||||
import { ChannelWritePerms } from '../ChannelWritePerms';
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { Box, Col, Row, Text } from '@tlon/indigo-react';
|
||||
import {
|
||||
Association,
|
||||
|
||||
deleteGraph,
|
||||
|
||||
deleteGraph,
|
||||
Group,
|
||||
leaveGraph,
|
||||
metadataRemove
|
||||
metadataRemove,
|
||||
isChannelAdmin,
|
||||
isHost
|
||||
} from '@urbit/api';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { isChannelAdmin, isHost } from '~/logic/lib/group';
|
||||
import { useHashLink } from '~/logic/lib/useHashLink';
|
||||
import { FormGroup } from '~/views/components/FormGroup';
|
||||
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||
@ -53,8 +52,8 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
||||
return history.push(props.rootUrl);
|
||||
};
|
||||
|
||||
const canAdmin = isChannelAdmin(group, association.resource);
|
||||
const isOwner = isHost(association.resource);
|
||||
const canAdmin = isChannelAdmin(group, association.resource, window.ship);
|
||||
const isOwner = isHost(association.resource, window.ship);
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
|
@ -1,22 +1,17 @@
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { Suspense, useCallback, useEffect } from 'react';
|
||||
import { Route, Switch, useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import LaunchApp from '~/views/apps/launch/App';
|
||||
import Notifications from '~/views/apps/notifications/notifications';
|
||||
import { PermalinkRoutes } from '~/views/apps/permalinks/app';
|
||||
import Profile from '~/views/apps/profile/profile';
|
||||
import Settings from '~/views/apps/settings/settings';
|
||||
import ErrorComponent from '~/views/components/Error';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import LaunchApp from '~/views/apps/launch/App';
|
||||
|
||||
import Landscape from '~/views/landscape/index';
|
||||
import GraphApp from '../../apps/graph/App';
|
||||
import { getNotificationRedirect } from '~/logic/lib/notificationRedirects';
|
||||
import {JoinRoute} from './Join/Join';
|
||||
import { JoinRoute } from './Join/Join';
|
||||
import useInviteState from '~/logic/state/invite';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
|
||||
export const Container = styled(Box)`
|
||||
flex-grow: 1;
|
||||
@ -25,7 +20,13 @@ export const Container = styled(Box)`
|
||||
height: calc(100% - 62px);
|
||||
`;
|
||||
|
||||
export const Content = (props) => {
|
||||
const Landscape = React.lazy(() => import('~/views/landscape/index'));
|
||||
const Settings = React.lazy(() => import('~/views/apps/settings/settings'));
|
||||
const Profile = React.lazy(() => import('~/views/apps/profile/profile'));
|
||||
const Notifications = React.lazy(() => import('~/views/apps/notifications/notifications'));
|
||||
const ErrorComponent = React.lazy(() => import('~/views/components/Error'));
|
||||
|
||||
export const Content = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const mdLoaded = useMetadataState(s => s.loaded);
|
||||
@ -74,54 +75,42 @@ export const Content = (props) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<JoinRoute />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/" render={p => (
|
||||
<LaunchApp
|
||||
location={p.location}
|
||||
match={p.match}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path='/~landscape'>
|
||||
<Landscape />
|
||||
</Route>
|
||||
<Route
|
||||
path="/~profile"
|
||||
render={ p => (
|
||||
<Profile
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/~settings"
|
||||
render={ p => (
|
||||
<Settings {...props} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/~notifications"
|
||||
render={ p => (
|
||||
<Notifications {...props} />
|
||||
)}
|
||||
/>
|
||||
<GraphApp path="/~graph" {...props} />
|
||||
<PermalinkRoutes {...props} />
|
||||
<Suspense fallback={Loading}>
|
||||
<JoinRoute />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
component={LaunchApp}
|
||||
/>
|
||||
<Route path='/~landscape'>
|
||||
<Landscape />
|
||||
</Route>
|
||||
<Route
|
||||
path="/~profile"
|
||||
component={Profile}
|
||||
/>
|
||||
<Route
|
||||
path="/~settings"
|
||||
component={Settings}
|
||||
/>
|
||||
<Route
|
||||
path="/~notifications"
|
||||
component={Notifications}
|
||||
/>
|
||||
<PermalinkRoutes />
|
||||
|
||||
<Route
|
||||
render={p => (
|
||||
<ErrorComponent
|
||||
code={404}
|
||||
description="Not Found"
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<Route
|
||||
render={p => (
|
||||
<ErrorComponent
|
||||
code={404}
|
||||
description="Not Found"
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Button, Col, Icon, Label, Row, Text } from '@tlon/indigo-react';
|
||||
import { Association, deleteGroup, leaveGroup } from '@urbit/api';
|
||||
import { Association, deleteGroup, leaveGroup, resourceFromPath } from '@urbit/api';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { useModal } from '~/logic/lib/useModal';
|
||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||
import airlock from '~/logic/api';
|
||||
|
@ -5,11 +5,10 @@ import {
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import _ from 'lodash';
|
||||
import { Association, changePolicy, deSig, Enc, Group, GroupPolicy, metadataEdit, MetadataEditField } from '@urbit/api';
|
||||
import { Association, changePolicy, deSig, Enc, Group, GroupPolicy, metadataEdit, MetadataEditField, resourceFromPath, roleForShip } from '@urbit/api';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath, roleForShip } from '~/logic/lib/group';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
import { ColorInput } from '~/views/components/ColorInput';
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { Association, Group, metadataRemove, metadataEdit, deSig } from '@urbit/api';
|
||||
import { Association, Group, metadataRemove, metadataEdit, deSig, resourceFromPath, roleForShip } from '@urbit/api';
|
||||
import React, { useCallback } from 'react';
|
||||
import { resourceFromPath, roleForShip } from '~/logic/lib/group';
|
||||
import { getModuleIcon, GraphModule } from '~/logic/lib/util';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Box, Button, Col, Text } from '@tlon/indigo-react';
|
||||
import { Association, deSig, Group } from '@urbit/api';
|
||||
import { Association, deSig, Group, resourceFromPath, roleForShip } from '@urbit/api';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { resourceFromPath, roleForShip } from '~/logic/lib/group';
|
||||
import { GroupAdminSettings } from './Admin';
|
||||
import { GroupChannelSettings } from './Channels';
|
||||
import { GroupFeedSettings } from './GroupFeed';
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { readGroup } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -13,26 +12,27 @@ import { getGroupFromWorkspace } from '~/logic/lib/workspace';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import { DmResource } from '~/views/apps/chat/DmResource';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
import '~/views/apps/links/css/custom.css';
|
||||
import '~/views/apps/publish/css/custom.css';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import { UnjoinedResource } from '~/views/components/UnjoinedResource';
|
||||
import { EmptyGroupHome } from './Home/EmptyGroupHome';
|
||||
import { GroupHome } from './Home/GroupHome';
|
||||
import { InvitePopover } from './InvitePopover';
|
||||
import { NewChannel } from './NewChannel';
|
||||
import { PopoverRoutes } from './PopoverRoutes';
|
||||
import { Resource } from './Resource';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import {Join, JoinRoute} from './Join/Join';
|
||||
import { EmptyGroupHome } from './Home/EmptyGroupHome';
|
||||
import { Join } from './Join/Join';
|
||||
|
||||
interface GroupsPaneProps {
|
||||
baseUrl: string;
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
const DmResource = React.lazy(() => import('~/views/apps/chat/DmResource').then(module => ({ default: module.DmResource })));
|
||||
const Resource = React.lazy(() => import('./Resource').then(module => ({ default: module.Resource })));
|
||||
const UnjoinedResource = React.lazy(() => import('~/views/components/UnjoinedResource').then(module => ({ default: module.UnjoinedResource })));
|
||||
const GroupHome = React.lazy(() => import('./Home/GroupHome').then(module => ({ default: module.GroupHome })));
|
||||
const NewChannel = React.lazy(() => import('./NewChannel').then(module => ({ default: module.NewChannel })));
|
||||
|
||||
export function GroupsPane(props: GroupsPaneProps) {
|
||||
const { baseUrl, workspace } = props;
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
@ -66,7 +66,6 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
doneJoin(group);
|
||||
}
|
||||
|
||||
|
||||
return () => {
|
||||
setRecentGroups(gs => _.uniq([workspace.group, ...gs]));
|
||||
};
|
||||
@ -183,31 +182,29 @@ 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('/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
|
||||
path={relativePath('/new')}
|
||||
render={(routeProps) => {
|
||||
@ -254,7 +251,7 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
associations={associations}
|
||||
/>
|
||||
)}
|
||||
{popovers(routeProps, baseUrl)}
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { Button, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import { disableGroupFeed } from '@urbit/api';
|
||||
import { disableGroupFeed, resourceFromPath } from '@urbit/api';
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
export const AddFeedBanner = (props) => {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Col } from '@tlon/indigo-react';
|
||||
import { deSig, markCountAsRead } from '@urbit/api';
|
||||
import { deSig, markCountAsRead, resourceFromPath } from '@urbit/api';
|
||||
import React, {
|
||||
useEffect
|
||||
} from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
|
@ -3,7 +3,6 @@ import React, {
|
||||
useEffect
|
||||
} from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import useGraphState, { useGraphTimesentMap } from '~/logic/state/graph';
|
||||
import { useGroup } from '~/logic/state/group';
|
||||
import { useAssocForGraph } from '~/logic/state/metadata';
|
||||
@ -12,7 +11,7 @@ import { GroupFeedHeader } from './GroupFeedHeader';
|
||||
import { PostThreadRoutes } from './Post/PostThread';
|
||||
import PostFlatTimeline from './Post/PostFlatTimeline';
|
||||
import airlock from '~/logic/api';
|
||||
import { markCountAsRead } from '@urbit/api';
|
||||
import { markCountAsRead, resourceFromPath } from '@urbit/api';
|
||||
import { PostRepliesRoutes } from './Post/PostReplies';
|
||||
import { toHarkPlace } from '~/logic/lib/util';
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Box, Col } from '@tlon/indigo-react';
|
||||
import { Association, Graph, GraphNode, Group } from '@urbit/api';
|
||||
import { Association, Graph, GraphNode, Group, resourceFromPath } from '@urbit/api';
|
||||
import { History } from 'history';
|
||||
import bigInt from 'big-integer';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import PostItem from './PostItem/PostItem';
|
||||
import PostInput from './PostInput';
|
||||
import { GraphScroller } from '~/views/components/GraphScroller';
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Box, Col } from '@tlon/indigo-react';
|
||||
import { arrToString, Association, FlatGraph, FlatGraphNode, Group } from '@urbit/api';
|
||||
import { arrToString, Association, FlatGraph, FlatGraphNode, Group, resourceFromPath } from '@urbit/api';
|
||||
import bigInt from 'big-integer';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, useHistory } from 'react-router';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { keyEq, ThreadScroller } from '~/views/components/ThreadScroller';
|
||||
import PostItem from './PostItem/PostItem';
|
||||
import PostInput from './PostInput';
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Association, Group } from '@urbit/api';
|
||||
import { Association, Group, resourceFromPath } from '@urbit/api';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import { useFlatGraph } from '~/logic/state/graph';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import PostFlatFeed from './PostFlatFeed';
|
||||
import PostInput from './PostInput';
|
||||
|
||||
|
@ -1,9 +1,19 @@
|
||||
import { BaseTextArea, Box, Button, Icon, LoadingSpinner, Row } from '@tlon/indigo-react';
|
||||
import { Association, Content, createPost, evalCord, Group, Path } from '@urbit/api';
|
||||
import {
|
||||
Association,
|
||||
Content,
|
||||
createPost,
|
||||
evalCord,
|
||||
Group,
|
||||
Path,
|
||||
isChannelAdmin,
|
||||
isHost,
|
||||
isWriter,
|
||||
resourceFromPath
|
||||
} from '@urbit/api';
|
||||
import React, {
|
||||
ReactElement, useCallback, useState
|
||||
} from 'react';
|
||||
import { isChannelAdmin, isHost, isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
|
||||
import useStorage from '~/logic/lib/useStorage';
|
||||
import { useToggleState } from '~/logic/lib/useToggleState';
|
||||
@ -20,13 +30,13 @@ function canWrite(props) {
|
||||
}
|
||||
|
||||
if(vip === 'admin-feed') {
|
||||
return isChannelAdmin(group, association.group);
|
||||
return isChannelAdmin(group, association.group, window.ship);
|
||||
}
|
||||
if(vip === 'host-feed') {
|
||||
return isHost(association.group);
|
||||
return isHost(association.group, window.ship);
|
||||
}
|
||||
|
||||
return isWriter(group, association.resource);
|
||||
return isWriter(group, association.resource, window.ship);
|
||||
}
|
||||
|
||||
interface PostInputProps {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Action, Col, Icon, Row } from '@tlon/indigo-react';
|
||||
import { Association, Post, removePosts } from '@urbit/api';
|
||||
import { Association, Post, removePosts, resourceFromPath } from '@urbit/api';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||
import { useCopy } from '~/logic/lib/useCopy';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import Author from '~/views/components/Author';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import airlock from '~/logic/api';
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Box, Col, Row, Text } from '@tlon/indigo-react';
|
||||
import { Association, GraphNode, Group, Post } from '@urbit/api';
|
||||
import { Association, GraphNode, Group, Post, isWriter } from '@urbit/api';
|
||||
import { BigInteger } from 'big-integer';
|
||||
import { History } from 'history';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getPostRoute } from '~/logic/lib/graph';
|
||||
import { isWriter } from '~/logic/lib/group';
|
||||
import { useHovering } from '~/logic/lib/util';
|
||||
import { Mention } from '~/views/components/MentionText';
|
||||
import PostInput from '../PostInput';
|
||||
@ -61,7 +60,7 @@ function PostItem(props: PostItemProps) {
|
||||
if (index && index.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return isWriter(group, association.resource);
|
||||
return isWriter(group, association.resource, window.ship);
|
||||
}, [group, association.resource, vip, index]);
|
||||
|
||||
const navigateToChildren = useCallback(() => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Association, PermVariation } from '@urbit/api';
|
||||
import { Association, PermVariation, resourceFromPath } from '@urbit/api';
|
||||
import React, { useEffect } from 'react';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import useGraphState, { GraphState, useGraph } from '~/logic/state/graph';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import PostFeed from './PostFeed';
|
||||
|
@ -4,12 +4,11 @@ import React, {
|
||||
useCallback,
|
||||
useEffect
|
||||
} from 'react';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import PostFlatFeed from './PostFlatFeed';
|
||||
import PostInput from './PostInput';
|
||||
import { arrToString, Association, deSig, PermVariation } from '@urbit/api';
|
||||
import { arrToString, Association, deSig, PermVariation, resourceFromPath } from '@urbit/api';
|
||||
import { useParams, Switch, Route } from 'react-router';
|
||||
import { useGroupForAssoc } from '~/logic/state/group';
|
||||
|
||||
|
@ -5,13 +5,12 @@ import {
|
||||
|
||||
Row, Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { Association, invite } from '@urbit/api';
|
||||
import { Association, invite, resourceFromPath } from '@urbit/api';
|
||||
import { Form, Formik } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
|
||||
import { deSig } from '~/logic/lib/util';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
|
@ -14,12 +14,10 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import { useInviteForResource } from '~/logic/state/invite';
|
||||
import useMetadataState, { usePreview } from '~/logic/state/metadata';
|
||||
import { decline, Invite } from '@urbit/api';
|
||||
import { join, JoinRequest } from '@urbit/api/groups';
|
||||
import { joinError, joinLoad, JoinProgress, join, JoinRequest, decline, Invite } from '@urbit/api';
|
||||
import airlock from '~/logic/api';
|
||||
import { joinError, joinLoad, JoinProgress } from '@urbit/api';
|
||||
import { useQuery } from '~/logic/lib/useQuery';
|
||||
import { JoinKind, JoinDesc, JoinSkeleton } from './Skeleton';
|
||||
import { JoinDesc, JoinKind, JoinSkeleton } from './Skeleton';
|
||||
import { preSig } from '~/logic/lib/util';
|
||||
|
||||
interface InviteWithUid extends Invite {
|
||||
@ -160,7 +158,7 @@ function JoinError(props: {
|
||||
const explanation =
|
||||
request.progress === 'no-perms'
|
||||
? 'You do not have the correct permissions'
|
||||
: 'An unexpected error occurred';
|
||||
: 'An unexpected error occurred';
|
||||
|
||||
const onRetry = () => {
|
||||
useGroupState.getState().abortJoin(desc.group);
|
||||
|
@ -1,27 +1,23 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
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";
|
||||
Box
|
||||
} 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 { resourceFromPath } from '@urbit/api';
|
||||
|
||||
import useMetadataState, { usePreview } from "~/logic/state/metadata";
|
||||
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
|
||||
import {useHistory} from "react-router-dom";
|
||||
import { usePreview } from '~/logic/state/metadata';
|
||||
import { useInviteForResource } from '~/logic/state/invite';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
const SUMMARY_HEIGHT = "96px";
|
||||
const SUMMARY_HEIGHT = '96px';
|
||||
|
||||
export type JoinKind = "graph" | "groups";
|
||||
export type JoinKind = 'graph' | 'groups';
|
||||
|
||||
export interface JoinDesc {
|
||||
group: string;
|
||||
@ -38,7 +34,7 @@ interface JoinSkeletonProps {
|
||||
}
|
||||
|
||||
export function JoinSkeleton(props: JoinSkeletonProps) {
|
||||
const { title, body, children, onJoin, desc, modal } = props;
|
||||
const { title, children, desc, modal } = props;
|
||||
const history = useHistory();
|
||||
const dismiss = () => {
|
||||
history.push({ search: '' });
|
||||
@ -46,24 +42,24 @@ export function JoinSkeleton(props: JoinSkeletonProps) {
|
||||
|
||||
const inner = (
|
||||
<Col
|
||||
width={modal ? ["90vw", "384px"] : undefined}
|
||||
borderRadius="2"
|
||||
backgroundColor="white"
|
||||
width={modal ? ['90vw', '384px'] : undefined}
|
||||
borderRadius='2'
|
||||
backgroundColor='white'
|
||||
>
|
||||
<Col
|
||||
gapY="4"
|
||||
p="4"
|
||||
borderRadius="2"
|
||||
backgroundColor="washedGray"
|
||||
justifyContent="space-between"
|
||||
gapY='4'
|
||||
p='4'
|
||||
borderRadius='2'
|
||||
backgroundColor='washedGray'
|
||||
justifyContent='space-between'
|
||||
flexGrow={1}
|
||||
>
|
||||
<Box maxWidth="512px">
|
||||
<Text fontWeight="medium" fontSize="2">
|
||||
<Box maxWidth='512px'>
|
||||
<Text fontWeight='medium' fontSize='2'>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
{!!desc ? <JoinBody desc={desc} /> : null}
|
||||
{desc ? <JoinBody desc={desc} /> : null}
|
||||
</Col>
|
||||
{children}
|
||||
</Col>
|
||||
@ -78,30 +74,30 @@ export function JoinSkeleton(props: JoinSkeletonProps) {
|
||||
export function JoinBody(props: { desc: JoinDesc }) {
|
||||
const { desc } = props;
|
||||
const { group, kind } = desc || {};
|
||||
const { preview, error } = usePreview(group);
|
||||
const { preview } = usePreview(group);
|
||||
const { ship, name } = resourceFromPath(group);
|
||||
|
||||
const invite = useInviteForResource(kind, ship, name);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!desc ? "Prompt invite link" : null}
|
||||
{!desc ? 'Prompt invite link' : null}
|
||||
{preview ? (
|
||||
<GroupSummary
|
||||
memberCount={preview.members}
|
||||
channelCount={preview["channel-count"]}
|
||||
channelCount={preview['channel-count']}
|
||||
metadata={preview.metadata}
|
||||
height={SUMMARY_HEIGHT}
|
||||
width="100%"
|
||||
maxWidth="100%"
|
||||
overflow="hidden"
|
||||
width='100%'
|
||||
maxWidth='100%'
|
||||
overflow='hidden'
|
||||
/>
|
||||
) : (
|
||||
<FallbackSummary path={group} />
|
||||
)}
|
||||
|
||||
{invite ? (
|
||||
<Col gapY="2">
|
||||
<Col gapY='2'>
|
||||
<Box>
|
||||
<Text>
|
||||
<Text mono>{invite.ship}</Text> <Text gray>invited you</Text>
|
||||
@ -109,7 +105,7 @@ export function JoinBody(props: { desc: JoinDesc }) {
|
||||
</Box>
|
||||
{invite.text?.length > 0 ? (
|
||||
<Box>
|
||||
<Text>"{invite.text}"</Text>
|
||||
<Text>"{ invite.text }"</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
</Col>
|
||||
@ -120,18 +116,18 @@ export function JoinBody(props: { desc: JoinDesc }) {
|
||||
|
||||
function FallbackSummary(props: { path: string }) {
|
||||
const { path } = props;
|
||||
const [, , ship, name] = path.split("/");
|
||||
const [, , ship, name] = path.split('/');
|
||||
|
||||
return (
|
||||
<Row
|
||||
height={SUMMARY_HEIGHT}
|
||||
width="100%"
|
||||
overflow="hidden"
|
||||
alignItems="center"
|
||||
gapX="0"
|
||||
width='100%'
|
||||
overflow='hidden'
|
||||
alignItems='center'
|
||||
gapX='0'
|
||||
>
|
||||
<Author gray fullNotIcon size={40} showImage ship={ship} dontShowTime />
|
||||
<Text mono whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis">
|
||||
<Text mono whiteSpace='nowrap' overflow='hidden' textOverflow='ellipsis'>
|
||||
/{name}
|
||||
</Text>
|
||||
</Row>
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { invite } from '@urbit/api';
|
||||
import { invite, resourceFromPath } from '@urbit/api';
|
||||
import { Form, Formik } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { deSig } from '~/logic/lib/util';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
import { ShipSearch } from '~/views/components/ShipSearch';
|
||||
|
@ -4,13 +4,12 @@ import {
|
||||
ManagedTextInputField as Input,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { addTag, createManagedGraph, createUnmanagedGraph } from '@urbit/api';
|
||||
import { addTag, createManagedGraph, createUnmanagedGraph, resourceFromPath } from '@urbit/api';
|
||||
import { Form, Formik } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
import { deSig, parentPath, stringToSymbol } from '~/logic/lib/util';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
|
||||
StatelessTextInput as Input, Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { addTag, Association, Contact, Contacts, changePolicy, deSig, Group, removeMembers, removeTag, RoleTags } from '@urbit/api';
|
||||
import { addTag, Association, Contact, Contacts, changePolicy, deSig, Group, removeMembers, removeTag, RoleTags, resourceFromPath, roleForShip } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import f from 'lodash/fp';
|
||||
import React, {
|
||||
@ -16,7 +16,6 @@ import React, {
|
||||
import { Link } from 'react-router-dom';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import styled from 'styled-components';
|
||||
import { resourceFromPath, roleForShip } from '~/logic/lib/group';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { cite, uxToHex } from '~/logic/lib/util';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
@ -60,7 +59,7 @@ const emptyContact = (patp: string, pending: boolean): Participant => ({
|
||||
|
||||
function getParticipants(cs: Contacts, group: Group) {
|
||||
const contacts: Participant[] = _.flow(
|
||||
f.omitBy<Contacts>((_c, patp) => !group.members.has(deSig(patp))),
|
||||
f.omitBy<Contacts>((_c, patp) => !_.includes(group.members, deSig(patp))),
|
||||
f.toPairs,
|
||||
f.map(([patp, c]: [string, Contact]) => ({
|
||||
...c,
|
||||
@ -83,8 +82,8 @@ function getParticipants(cs: Contacts, group: Group) {
|
||||
const incPending = _.unionBy(allMembers, pending, 'patp');
|
||||
return [
|
||||
incPending,
|
||||
incPending.length - group.members.size,
|
||||
group.members.size
|
||||
incPending.length - group.members.length,
|
||||
group.members.length
|
||||
] as const;
|
||||
}
|
||||
|
||||
@ -109,11 +108,14 @@ export function Participants(props: {
|
||||
ParticipantsTabId,
|
||||
(p: Participant) => boolean
|
||||
> = useMemo(
|
||||
() => ({
|
||||
total: p => !p.pending,
|
||||
pending: p => p.pending,
|
||||
admin: p => props.group.tags?.role?.admin?.has(p.patp)
|
||||
}),
|
||||
() => {
|
||||
const admins = props.group.tags?.role?.admin;
|
||||
return {
|
||||
total: p => !p.pending,
|
||||
pending: p => p.pending,
|
||||
admin: p => _.includes(admins, p.patp)
|
||||
};
|
||||
},
|
||||
[props.group]
|
||||
);
|
||||
|
||||
@ -130,7 +132,7 @@ export function Participants(props: {
|
||||
[setSearch]
|
||||
);
|
||||
|
||||
const adminCount = props.group.tags?.role?.admin?.size || 0;
|
||||
const adminCount = props.group.tags?.role?.admin?.length || 0;
|
||||
const isInvite = 'invite' in props.group.policy;
|
||||
const contacts = useContactState(state => state.contacts);
|
||||
|
||||
@ -362,7 +364,7 @@ function Participant(props: {
|
||||
</StatelessAsyncAction>
|
||||
)}
|
||||
{role === 'admin' ? (
|
||||
group?.tags?.role?.admin?.size > 1 && (<StatelessAsyncAction onClick={onDemote} bg="transparent">
|
||||
group?.tags?.role?.admin?.length > 1 && (<StatelessAsyncAction onClick={onDemote} bg="transparent">
|
||||
Demote from Admin
|
||||
</StatelessAsyncAction>)
|
||||
) : (
|
||||
|
@ -1,8 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||
import { Association, deSig, Group } from '@urbit/api';
|
||||
import { Association, deSig, Group, resourceFromPath } from '@urbit/api';
|
||||
import React, { ReactElement, useCallback, useRef } from 'react';
|
||||
import { Link, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { useHashLink } from '~/logic/lib/useHashLink';
|
||||
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||
import { SidebarItem } from '~/views/landscape/components/SidebarItem';
|
||||
@ -26,11 +26,12 @@ export function PopoverRoutes(
|
||||
|
||||
useHashLink();
|
||||
|
||||
const groupSize = props.group.members.size;
|
||||
const groupSize = props.group.members.length;
|
||||
const ship = resourceFromPath(props.association?.group ?? '/ship/~zod/group').ship;
|
||||
const owner = deSig(ship) === window.ship;
|
||||
|
||||
const admin = props.group?.tags?.role?.admin.has(window.ship) || false;
|
||||
const admins = props.group?.tags?.role?.admin;
|
||||
const admin = (admins && _.includes(admins, window.ship)) || false;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -1,11 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import { Box, Col, Icon, Text } from '@tlon/indigo-react';
|
||||
import { AppName, Association } from '@urbit/api';
|
||||
import { AppName, Association, isWriter } from '@urbit/api';
|
||||
import React, { ReactElement, ReactNode, useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import urbitOb from 'urbit-ob';
|
||||
import { isWriter } from '~/logic/lib/group';
|
||||
import { useResize } from '~/logic/lib/useResize';
|
||||
import { getItemTitle } from '~/logic/lib/util';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
@ -103,7 +102,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
const isOwn = `~${window.ship}` === ship;
|
||||
let canWrite = (app === 'publish') ? true : false;
|
||||
|
||||
if (!isWriter(group, association.resource)) {
|
||||
if (!isWriter(group, association.resource, window.ship)) {
|
||||
canWrite = isOwn;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
} from '@tlon/indigo-react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { roleForShip } from '@urbit/api';
|
||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||
import { getGroupFromWorkspace } from '~/logic/lib/workspace';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { FormikHelpers } from 'formik';
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { roleForShip } from '@urbit/api';
|
||||
import { getGroupFromWorkspace } from '~/logic/lib/workspace';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React, { ReactElement, ReactNode, useCallback, useState } from 'react';
|
||||
import React, { ReactElement, ReactNode, Suspense, useCallback, useState } from 'react';
|
||||
import { Sidebar } from './Sidebar/Sidebar';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
import { Body } from '~/views/components/Body';
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
|
||||
interface SkeletonProps {
|
||||
children: ReactNode;
|
||||
@ -41,7 +42,7 @@ export const Skeleton = React.memo((props: SkeletonProps): ReactElement => {
|
||||
/>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
{props.children}
|
||||
<Suspense fallback={Loading}>{props.children}</Suspense>
|
||||
</Body>
|
||||
);
|
||||
});
|
||||
|
@ -7,10 +7,11 @@ import useHarkState from '~/logic/state/hark';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
import { Body } from '../components/Body';
|
||||
import { GroupsPane } from './components/GroupsPane';
|
||||
import { NewGroup } from './components/NewGroup';
|
||||
import './css/custom.css';
|
||||
import _ from 'lodash';
|
||||
|
||||
const NewGroup = React.lazy(() => import('./components/NewGroup').then(module => ({ default: module.NewGroup })));
|
||||
|
||||
moment.updateLocale('en', {
|
||||
relativeTime : {
|
||||
future: '%s',
|
||||
|
@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
if (false && process.env.NODE_ENV === 'development') {
|
||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||
whyDidYouRender(React, {
|
||||
trackAllPureComponents: true
|
||||
});
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { deSig } from '../index';
|
||||
import { Enc, Path, Patp, PatpNoSig, Poke, Thread } from '../lib/types';
|
||||
import { Group, GroupPolicy, GroupPolicyDiff, GroupUpdateAddMembers, GroupUpdateAddTag, GroupUpdateChangePolicy, GroupUpdateRemoveGroup, GroupUpdateRemoveMembers, GroupUpdateRemoveTag, Resource, RoleTags, Tag } from './types';
|
||||
import { GroupUpdate } from './update';
|
||||
@ -95,10 +96,6 @@ export const changePolicy = (
|
||||
}
|
||||
});
|
||||
|
||||
export const makeResource = (ship: string, name: string) => {
|
||||
return { ship, name };
|
||||
};
|
||||
|
||||
export const join = (
|
||||
ship: string,
|
||||
name: string,
|
||||
@ -166,8 +163,8 @@ export const roleTags = ['janitor', 'moderator', 'admin'];
|
||||
// TODO make this type better?
|
||||
|
||||
export const groupBunts = {
|
||||
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
|
||||
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
|
||||
group: (): Group => ({ members: [], tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
|
||||
policy: (): GroupPolicy => ({ open: { banned: [], banRanks: [] } })
|
||||
};
|
||||
|
||||
export const joinError = ['no-perms', 'strange', 'abort'] as const;
|
||||
@ -175,51 +172,55 @@ export const joinResult = ['done', ...joinError] as const;
|
||||
export const joinLoad = ['start', 'added', 'metadata'] as const;
|
||||
export const joinProgress = [...joinLoad, ...joinResult] as const;
|
||||
|
||||
export const roleForShip = (
|
||||
export function roleForShip(
|
||||
group: Group,
|
||||
ship: PatpNoSig
|
||||
): RoleTags | undefined => {
|
||||
): RoleTags | undefined {
|
||||
return roleTags.reduce((currRole, role) => {
|
||||
const roleShips = group?.tags?.role?.[role];
|
||||
return roleShips && roleShips.has(ship) ? role : currRole;
|
||||
return roleShips && roleShips.includes(ship) ? role : currRole;
|
||||
}, undefined as RoleTags | undefined);
|
||||
};
|
||||
|
||||
export const resourceFromPath = (path: Path): Resource => {
|
||||
export function resourceFromPath(path: Path): Resource {
|
||||
const [, , ship, name] = path.split('/');
|
||||
return { ship, name };
|
||||
};
|
||||
}
|
||||
|
||||
export function makeResource(ship: string, name: string) {
|
||||
return { ship, name };
|
||||
}
|
||||
|
||||
export const isWriter = (group: Group, resource: string, ship: string) => {
|
||||
const graph = group.tags?.graph;
|
||||
const writers: Set<string> | undefined = graph && (graph[resource] as any)?.writers;
|
||||
const admins = group?.tags?.role?.admin ?? new Set();
|
||||
const graph = group?.tags?.graph;
|
||||
const writers: string[] | undefined = graph && (graph[resource] as any)?.writers;
|
||||
const admins = group?.tags?.role?.admin ?? [];
|
||||
if (typeof writers === 'undefined') {
|
||||
return true;
|
||||
} else {
|
||||
return writers.has(ship) || admins.has(ship);
|
||||
return writers.includes(ship) || admins.includes(ship);
|
||||
}
|
||||
};
|
||||
|
||||
export const isHost = (
|
||||
resource: string,
|
||||
ship: string
|
||||
): boolean => {
|
||||
const [, , host] = resource.split('/');
|
||||
|
||||
return ship === host;
|
||||
};
|
||||
|
||||
export const isChannelAdmin = (
|
||||
export function isChannelAdmin(
|
||||
group: Group,
|
||||
resource: string,
|
||||
ship: string
|
||||
): boolean => {
|
||||
const role = roleForShip(group, ship.slice(1));
|
||||
): boolean {
|
||||
const role = roleForShip(group, deSig(ship));
|
||||
|
||||
return (
|
||||
isHost(resource, ship) ||
|
||||
role === 'admin' ||
|
||||
role === 'moderator'
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function isHost(
|
||||
resource: string,
|
||||
ship: string
|
||||
): boolean {
|
||||
const [, , host] = resource.split('/');
|
||||
|
||||
return ship === host;
|
||||
}
|
||||
|
@ -16,14 +16,14 @@ export type Tag = AppTag | RoleTag;
|
||||
|
||||
export interface InvitePolicy {
|
||||
invite: {
|
||||
pending: Set<PatpNoSig>;
|
||||
pending: PatpNoSig[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface OpenPolicy {
|
||||
open: {
|
||||
banned: Set<PatpNoSig>;
|
||||
banRanks: Set<ShipRank>;
|
||||
banned: PatpNoSig[];
|
||||
banRanks: ShipRank[];
|
||||
};
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ export type GroupPolicyDiff =
|
||||
export type GroupPolicy = OpenPolicy | InvitePolicy;
|
||||
|
||||
export interface TaggedShips {
|
||||
[tag: string]: Set<PatpNoSig>;
|
||||
[tag: string]: PatpNoSig[];
|
||||
}
|
||||
|
||||
export interface Tags {
|
||||
@ -85,7 +85,7 @@ export interface Tags {
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
members: Set<PatpNoSig>;
|
||||
members: PatpNoSig[];
|
||||
tags: Tags;
|
||||
policy: GroupPolicy;
|
||||
hidden: boolean;
|
||||
|
3220
pkg/npm/api/package-lock.json
generated
3220
pkg/npm/api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2369
pkg/npm/http-api/package-lock.json
generated
2369
pkg/npm/http-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user