Merge pull request #3648 from urbit/la/cleanup-links

interface: removed unused links files
This commit is contained in:
L 2020-10-06 13:20:49 -05:00 committed by GitHub
commit fd39ae195b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 10 additions and 1721 deletions

View File

@ -1 +0,0 @@

View File

@ -19,11 +19,6 @@ export function uuid() {
return str.slice(0,-1); return str.slice(0,-1);
} }
export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str);
return Boolean(r);
}
/* /*
Goes from: Goes from:
~2018.7.17..23.15.09..5be5 // urbit @da ~2018.7.17..23.15.09..5be5 // urbit @da
@ -88,23 +83,6 @@ export function hexToUx(hex) {
return `0x${ux}`; return `0x${ux}`;
} }
function hexToDec(hex) {
const alphabet = '0123456789ABCDEF'.split('');
return hex.reverse().reduce((acc, digit, idx) => {
const dec = alphabet.findIndex(a => a === digit.toUpperCase());
if(dec < 0) {
console.error(hex);
throw new Error('Incorrect hex formatting');
}
return acc + dec * (16 ** idx);
}, 0);
}
export function hexToRgba(hex, a) {
const [r,g,b] = _.chunk(hex, 2).map(hexToDec);
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
export function writeText(str) { export function writeText(str) {
return new Promise(((resolve, reject) => { return new Promise(((resolve, reject) => {
const range = document.createRange(); const range = document.createRange();
@ -155,6 +133,7 @@ export function alphabeticalOrder(a,b) {
return a.toLowerCase().localeCompare(b.toLowerCase()); return a.toLowerCase().localeCompare(b.toLowerCase());
} }
// TODO: deprecated
export function alphabetiseAssociations(associations) { export function alphabetiseAssociations(associations) {
const result = {}; const result = {};
Object.keys(associations).sort((a, b) => { Object.keys(associations).sort((a, b) => {
@ -177,24 +156,6 @@ export function alphabetiseAssociations(associations) {
return result; return result;
} }
// encodes string into base64url,
// by encoding into base64 and replacing non-url-safe characters.
//
export function base64urlEncode(string) {
return window.btoa(string)
.split('+').join('-')
.split('/').join('_');
}
// decode base64url. inverse of base64urlEncode above.
//
export function base64urlDecode(string) {
return window.atob(
string.split('_').join('/')
.split('-').join('+')
);
}
// encode the string into @ta-safe format, using logic from +wood. // encode the string into @ta-safe format, using logic from +wood.
// for example, 'some Chars!' becomes '~.some.~43.hars~21.' // for example, 'some Chars!' becomes '~.some.~43.hars~21.'
// //
@ -232,29 +193,6 @@ export function stringToTa(string) {
return '~.' + out; return '~.' + out;
} }
// used in Links
export function makeRoutePath(
resource,
page = 0,
url = null,
index = 0,
compage = 0
) {
let route = '/~link' + resource;
if (!url) {
if (page !== 0) {
route = route + '/' + page;
}
} else {
route = `${route}/${page}/${index}/${base64urlEncode(url)}`;
if (compage !== 0) {
route = route + '/' + compage;
}
}
return route;
}
export function amOwnerOfGroup(groupPath) { export function amOwnerOfGroup(groupPath) {
if (!groupPath) if (!groupPath)
return false; return false;
@ -296,37 +234,3 @@ export function stringToSymbol(str) {
return result; return result;
} }
export function scrollIsAtTop(container) {
if (
(navigator.userAgent.includes('Safari') &&
navigator.userAgent.includes('Chrome')) ||
navigator.userAgent.includes('Firefox')
) {
return container.scrollTop === 0;
} else if (navigator.userAgent.includes('Safari')) {
return (
container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else {
return false;
}
}
export function scrollIsAtBottom(container) {
if (
(navigator.userAgent.includes('Safari') &&
navigator.userAgent.includes('Chrome')) ||
navigator.userAgent.includes('Firefox')
) {
return (
container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else if (navigator.userAgent.includes('Safari')) {
return container.scrollTop === 0;
} else {
return false;
}
}

View File

@ -1,212 +0,0 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { LinkUpdate, Pagination } from '~/types/link-update';
// page size as expected from link-view.
// must change in parallel with the +page-size in /app/link-view to
// ensure sane behavior.
const PAGE_SIZE = 25;
type LinkState = Pick<StoreState, 'linksSeen' | 'links' | 'linkListening' | 'linkComments'>;
export default class LinkUpdateReducer<S extends LinkState> {
reduce(json: any, state: S) {
const data = _.get(json, 'link-update', false);
if(data) {
this.submissionsPage(data, state);
this.submissionsUpdate(data, state);
this.discussionsPage(data, state);
this.discussionsUpdate(data, state);
this.observationUpdate(data, state);
}
}
submissionsPage(json: LinkUpdate, state: S) {
const data = _.get(json, 'initial-submissions', false);
if (data) {
// { "initial-submissions": {
// "/~ship/group": {
// page: [{ship, timestamp, title, url}]
// page-number: 0
// total-items: 1
// total-pages: 1
// }
// } }
for (var path of Object.keys(data)) {
const here = data[path];
const page = here.pageNumber;
// if we didn't have any state for this path yet, initialize.
if (!state.links[path]) {
state.links[path] = {
local: {},
totalItems: here.totalItems,
totalPages: here.totalPages,
unseenCount: here.unseenCount
};
}
// since data contains an up-to-date full version of the page,
// we can safely overwrite the one in state.
if (typeof page === 'number' && here.page) {
state.links[path][page] = here.page;
state.links[path].local[page] = false;
}
state.links[path].totalPages = here.totalPages;
state.links[path].totalItems = here.totalItems;
state.links[path].unseenCount = here.unseenCount;
// write seen status to a separate structure,
// for easier modification later.
if (!state.linksSeen[path]) {
state.linksSeen[path] = {};
}
(here.page || []).map((submission) => {
state.linksSeen[path][submission.url] = submission.seen;
});
}
}
}
submissionsUpdate(json: LinkUpdate, state: S) {
const data = _.get(json, 'submissions', false);
if (data) {
// { "submissions": {
// path: /~ship/group
// pages: [{ship, timestamp, title, url}]
// } }
const path = data.path;
// stub in a comment count, which is more or less guaranteed to be 0
data.pages = data.pages.map((submission) => {
submission.commentCount = 0;
state.linksSeen[path][submission.url] = false;
return submission;
});
// add the new submissions to state, update totals
state.links[path] = this._addNewItems(
data.pages, state.links[path]
);
state.links[path].unseenCount =
(state.links[path].unseenCount || 0) + data.pages.length;
}
}
discussionsPage(json: LinkUpdate, state: S) {
const data = _.get(json, 'initial-discussions', false);
if (data) {
// { "initial-discussions": {
// path: "/~ship/group"
// url: https://urbit.org/
// page: [{ship, timestamp, title, url}]
// page-number: 0
// total-items: 1
// total-pages: 1
// } }
const path = data.path;
const url = data.url;
const page = data.pageNumber;
// if we didn't have any state for this path yet, initialize.
if (!state.linkComments[path]) {
state.linkComments[path] = {};
}
let comments = {...{
local: {},
totalPages: data.totalPages,
totalItems: data.totalItems
}, ...state.linkComments[path][url] };
state.linkComments[path][url] = comments;
const here = state.linkComments[path][url];
// since data contains an up-to-date full version of the page,
// we can safely overwrite the one in state.
here[page] = data.page;
here.local[page] = false;
here.totalPages = data.totalPages;
here.totalItems = data.totalItems;
}
}
discussionsUpdate(json: LinkUpdate, state: S) {
const data = _.get(json, 'discussions', false);
if (data) {
// { "discussions": {
// path: /~ship/path
// url: 'https://urbit.org'
// comments: [{ship, timestamp, udon}]
// } }
const path = data.path;
const url = data.url;
// add new comments to state, update totals
state.linkComments[path][url] = this._addNewItems(
data.comments || [], state.linkComments[path][url]
);
}
}
observationUpdate(json: LinkUpdate, state: S) {
const data = _.get(json, 'observation', false);
if (data) {
// { "observation": {
// path: /~ship/path
// urls: ['https://urbit.org']
// } }
const path = data.path;
if (!state.linksSeen[path]) {
state.linksSeen[path] = {};
}
const seen = state.linksSeen[path];
// mark urls as seen
data.urls.map((url) => {
seen[url] = true;
});
if (state.links[path]) {
state.links[path].unseenCount =
state.links[path].unseenCount - data.urls.length;
}
}
}
//
_addNewItems<S extends { time: number }>(items: S[], pages: Pagination<S>, page = 0) {
if (!pages) {
pages = {
local: {},
totalPages: 0,
totalItems: 0
};
}
const i = page;
if (!pages[i]) {
pages[i] = [];
// if we know this page exists in the backend, flag it as "local",
// so that we know to initiate a "fetch the rest" request when we want
// to display the page.
pages.local[i] = (page < pages.totalPages);
}
pages[i] = items.concat(pages[i]);
pages[i].sort((a, b) => b.time - a.time);
pages.totalItems = pages.totalItems + items.length;
if (pages[i].length <= PAGE_SIZE) {
pages.totalPages = Math.ceil(pages.totalItems / PAGE_SIZE);
return pages;
}
// overflow into next page
const tail = pages[i].slice(PAGE_SIZE);
pages[i].length = PAGE_SIZE;
pages.totalItems = pages.totalItems - tail.length;
return this._addNewItems(tail, pages, page+1);
}
}

View File

@ -1,39 +0,0 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { LinkListenUpdate } from '~/types/link-listen-update';
type LinkListenState = Pick<StoreState, 'linkListening'>;
export default class LinkListenReducer<S extends LinkListenState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'link-listen-update', false);
if (data) {
this.listening(data, state);
this.watch(data, state);
this.leave(data, state);
}
}
listening(json: LinkListenUpdate, state: S) {
const data = _.get(json, 'listening', false);
if (data) {
state.linkListening = new Set(data);
}
}
watch(json: LinkListenUpdate, state: S) {
const data = _.get(json, 'watch', false);
if (data) {
state.linkListening.add(data);
}
}
leave(json: LinkListenUpdate, state: S) {
const data = _.get(json, 'leave', false);
if (data) {
state.linkListening.delete(data);
}
}
}

View File

@ -1,66 +0,0 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { PermissionUpdate } from '~/types/permission-update';
type PermissionState = Pick<StoreState, "permissions">;
export default class PermissionReducer<S extends PermissionState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'permission-update', false);
if (data) {
this.initial(data, state);
this.create(data, state);
this.delete(data, state);
this.add(data, state);
this.remove(data, state);
}
}
initial(json: PermissionUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
for (const perm in data) {
state.permissions[perm] = {
who: new Set(data[perm].who),
kind: data[perm].kind
};
}
}
}
create(json: PermissionUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.permissions[data.path] = {
kind: data.kind,
who: new Set(data.who)
};
}
}
delete(json: PermissionUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.permissions[data.path];
}
}
add(json: PermissionUpdate, state: S) {
const data = _.get(json, 'add', false);
if (data) {
for (const member of data.who) {
state.permissions[data.path].who.add(member);
}
}
}
remove(json: PermissionUpdate, state: S) {
const data = _.get(json, 'remove', false);
if (data) {
for (const member of data.who) {
state.permissions[data.path].who.delete(member);
}
}
}
}

View File

@ -7,15 +7,12 @@ import ChatReducer from '../reducers/chat-update';
import { StoreState } from './type'; import { StoreState } from './type';
import { Cage } from '~/types/cage'; import { Cage } from '~/types/cage';
import ContactReducer from '../reducers/contact-update'; import ContactReducer from '../reducers/contact-update';
import LinkUpdateReducer from '../reducers/link-update';
import S3Reducer from '../reducers/s3-update'; import S3Reducer from '../reducers/s3-update';
import { GraphReducer } from '../reducers/graph-update'; import { GraphReducer } from '../reducers/graph-update';
import GroupReducer from '../reducers/group-update'; import GroupReducer from '../reducers/group-update';
import PermissionReducer from '../reducers/permission-update';
import PublishUpdateReducer from '../reducers/publish-update'; import PublishUpdateReducer from '../reducers/publish-update';
import PublishResponseReducer from '../reducers/publish-response'; import PublishResponseReducer from '../reducers/publish-response';
import LaunchReducer from '../reducers/launch-update'; import LaunchReducer from '../reducers/launch-update';
import LinkListenReducer from '../reducers/listen-update';
import ConnectionReducer from '../reducers/connection'; import ConnectionReducer from '../reducers/connection';
export const homeAssociation = { export const homeAssociation = {
@ -38,11 +35,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
localReducer = new LocalReducer(); localReducer = new LocalReducer();
chatReducer = new ChatReducer(); chatReducer = new ChatReducer();
contactReducer = new ContactReducer(); contactReducer = new ContactReducer();
linkReducer = new LinkUpdateReducer();
linkListenReducer = new LinkListenReducer();
s3Reducer = new S3Reducer(); s3Reducer = new S3Reducer();
groupReducer = new GroupReducer(); groupReducer = new GroupReducer();
permissionReducer = new PermissionReducer();
publishUpdateReducer = new PublishUpdateReducer(); publishUpdateReducer = new PublishUpdateReducer();
publishResponseReducer = new PublishResponseReducer(); publishResponseReducer = new PublishResponseReducer();
launchReducer = new LaunchReducer(); launchReducer = new LaunchReducer();
@ -92,7 +86,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
}, },
weather: {}, weather: {},
userLocation: null, userLocation: null,
permissions: {},
s3: { s3: {
configuration: { configuration: {
buckets: new Set(), buckets: new Set(),
@ -100,10 +93,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
}, },
credentials: null credentials: null
}, },
links: {},
linksSeen: {},
linkListening: new Set(),
linkComments: {},
notebooks: {}, notebooks: {},
contacts: {}, contacts: {},
dark: false, dark: false,
@ -118,14 +107,11 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.localReducer.reduce(data, this.state); this.localReducer.reduce(data, this.state);
this.chatReducer.reduce(data, this.state); this.chatReducer.reduce(data, this.state);
this.contactReducer.reduce(data, this.state); this.contactReducer.reduce(data, this.state);
this.linkReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state); this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data, this.state); this.groupReducer.reduce(data, this.state);
this.permissionReducer.reduce(data, this.state);
this.publishUpdateReducer.reduce(data, this.state); this.publishUpdateReducer.reduce(data, this.state);
this.publishResponseReducer.reduce(data, this.state); this.publishResponseReducer.reduce(data, this.state);
this.launchReducer.reduce(data, this.state); this.launchReducer.reduce(data, this.state);
this.linkListenReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state); this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state); GraphReducer(data, this.state);
} }

View File

@ -7,9 +7,7 @@ import { Rolodex } from '~/types/contact-update';
import { Notebooks } from '~/types/publish-update'; import { Notebooks } from '~/types/publish-update';
import { Groups } from '~/types/group-update'; import { Groups } from '~/types/group-update';
import { S3State } from '~/types/s3-update'; import { S3State } from '~/types/s3-update';
import { Permissions } from '~/types/permission-update';
import { LaunchState, WeatherState } from '~/types/launch-update'; import { LaunchState, WeatherState } from '~/types/launch-update';
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
import { ConnectionStatus } from '~/types/connection'; import { ConnectionStatus } from '~/types/connection';
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update'; import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
import {Graphs} from '~/types/graph-update'; import {Graphs} from '~/types/graph-update';
@ -26,6 +24,7 @@ export interface StoreState {
remoteContentPolicy: LocalUpdateRemoteContentPolicy; remoteContentPolicy: LocalUpdateRemoteContentPolicy;
hideAvatars: boolean; hideAvatars: boolean;
hideNicknames: boolean; hideNicknames: boolean;
// invite state // invite state
invites: Invites; invites: Invites;
// metadata state // metadata state
@ -35,7 +34,6 @@ export interface StoreState {
// groups state // groups state
groups: Groups; groups: Groups;
groupKeys: Set<Path>; groupKeys: Set<Path>;
permissions: Permissions;
s3: S3State; s3: S3State;
graphs: Graphs; graphs: Graphs;
graphKeys: Set<string>; graphKeys: Set<string>;
@ -47,12 +45,6 @@ export interface StoreState {
weather: WeatherState | {} | null; weather: WeatherState | {} | null;
userLocation: string | null; userLocation: string | null;
// links state
linksSeen: LinkSeen;
linkListening: Set<Path>;
links: LinkCollections;
linkComments: LinkComments;
// publish state // publish state
notebooks: Notebooks; notebooks: Notebooks;

View File

@ -18,11 +18,6 @@ const publishSubscriptions: AppSubscription[] = [
['/primary', 'publish'], ['/primary', 'publish'],
]; ];
const linkSubscriptions: AppSubscription[] = [
// ['/json/seen', 'link-view'],
// ['/listening', 'link-listen-hook']
]
const groupSubscriptions: AppSubscription[] = [ const groupSubscriptions: AppSubscription[] = [
['/synced', 'contact-hook'] ['/synced', 'contact-hook']
]; ];
@ -31,11 +26,10 @@ const graphSubscriptions: AppSubscription[] = [
['/updates', 'graph-store'] ['/updates', 'graph-store']
]; ];
type AppName = 'publish' | 'chat' | 'link' | 'groups' | 'graph'; type AppName = 'publish' | 'chat' | 'groups' | 'graph';
const appSubscriptions: Record<AppName, AppSubscription[]> = { const appSubscriptions: Record<AppName, AppSubscription[]> = {
chat: chatSubscriptions, chat: chatSubscriptions,
publish: publishSubscriptions, publish: publishSubscriptions,
link: linkSubscriptions,
groups: groupSubscriptions, groups: groupSubscriptions,
graph: graphSubscriptions graph: graphSubscriptions
}; };
@ -44,7 +38,6 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
openSubscriptions: Record<AppName, number[]> = { openSubscriptions: Record<AppName, number[]> = {
chat: [], chat: [],
publish: [], publish: [],
link: [],
groups: [], groups: [],
graph: [] graph: []
}; };

View File

@ -1,18 +0,0 @@
import { Path } from './noun';
interface LinkListenUpdateListening {
listening: Path[];
}
interface LinkListenUpdateWatch {
watch: Path;
}
interface LinkListenUpdateLeave {
leave: Path;
}
export type LinkListenUpdate =
LinkListenUpdateListening
| LinkListenUpdateWatch
| LinkListenUpdateLeave;

View File

@ -1,84 +0,0 @@
import { PatpNoSig, Path } from "./noun";
export type LinkCollections = {
[p in Path]: Collection;
};
export type LinkSeen = {
[p in Path]: {
[url: string]: boolean;
};
};
export type Pagination<S> = {
local: LocalPages;
[p: number]: S[];
totalItems: number;
totalPages: number;
}
export type LinkComments = {
[p in Path]: {
[url: string]: Pagination<LinkComment> & {
totalItems: number;
totalPages: number;
}
}
}
interface LinkComment {
ship: PatpNoSig;
time: number;
udon: string;
}
interface CollectionStats {
unseenCount: number;
}
type LocalPages = {
[p: number]: boolean;
}
type Collection = CollectionStats & Pagination<Link>;
interface Link {
commentCount: number;
seen: boolean;
ship: PatpNoSig;
time: number;
title: string;
url: string;
}
interface LinkInitialSubmissions {
'initial-submissions': {
[p in Path]: CollectionStats & {
pageNumber?: number;
pages?: Link[];
}
};
};
interface LinkUpdateSubmission {
'submissions': {
path: Path;
pages: Link[];
}
}
interface LinkInitialDiscussion {
'intitial-discussion': {
path: Path;
url: string;
page: Comment[];
totalItems: number;
totalPages: number;
pageNumber: number;
}
}
export type LinkUpdate =
LinkInitialSubmissions
| LinkUpdateSubmission
| LinkInitialDiscussion;

View File

@ -1,55 +0,0 @@
import { Path, PatpNoSig } from './noun';
export type PermissionUpdate =
PermissionUpdateInitial
| PermissionUpdateCreate
| PermissionUpdateDelete
| PermissionUpdateRemove
| PermissionUpdateAdd;
interface PermissionUpdateInitial {
initial: {
[p in Path]: {
who: PatpNoSig[];
kind: PermissionKind;
};
}
}
interface PermissionUpdateCreate {
create: {
path: Path;
kind: PermissionKind;
who: PatpNoSig[];
}
}
interface PermissionUpdateDelete {
delete: {
path: Path;
}
}
interface PermissionUpdateAdd {
add: {
path: Path;
who: PatpNoSig[];
}
}
interface PermissionUpdateRemove {
remove: {
path: Path;
who: PatpNoSig[];
}
}
export type Permissions = {
[p in Path]: Permission;
};
export interface Permission {
who: Set<PatpNoSig>;
kind: PermissionKind;
}
export type PermissionKind = 'white' | 'black';

View File

@ -7,14 +7,13 @@ import { StoreState } from "~/logic/store/type";
import { uxToHex } from '~/logic/lib/util'; import { uxToHex } from '~/logic/lib/util';
import { Association, GraphNode } from "~/types"; import { Association, GraphNode } from "~/types";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { LinkList } from "./components/link-list";
import { LinkDetail } from "./components/link-detail";
import { LinkItem } from "./components/lib/link-item"; import { LinkItem } from "./components/link-item";
import { LinkSubmit } from "./components/lib/link-submit"; import { LinkSubmit } from "./components/link-submit";
import { LinkPreview } from "./components/lib/link-preview"; import { LinkPreview } from "./components/link-preview";
import { CommentSubmit } from "./components/lib/comment-submit"; import { CommentSubmit } from "./components/comment-submit";
import { Comments } from "./components/lib/comments"; import { Comments } from "./components/comments";
import "./css/custom.css"; import "./css/custom.css";
type LinkResourceProps = StoreState & { type LinkResourceProps = StoreState & {

View File

@ -1,227 +0,0 @@
import React, { Component } from 'react';
import { Switch, Route } from 'react-router-dom';
import Helmet from 'react-helmet';
import _ from 'lodash';
import './css/custom.css';
import { Skeleton } from './components/skeleton';
import { NewScreen } from './components/new';
import { SettingsScreen } from './components/settings';
import { MessageScreen } from './components/lib/message-screen';
import { LinkList } from './components/link-list';
import { LinkDetail } from './components/link-detail';
import {
amOwnerOfGroup,
base64urlDecode
} from '~/logic/lib/util';
export default class LinksApp extends Component {
componentDidMount() {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.props.subscription.startApp('graph');
if (!this.props.sidebarShown) {
this.props.api.local.sidebarToggle();
}
}
componentWillUnmount() {
this.props.subscription.stopApp('graph');
}
render() {
const { props } = this;
const contacts = props.contacts ? props.contacts : {};
const groups = props.groups ? props.groups : {};
const associations =
props.associations ? props.associations : { graph: {}, contacts: {} };
const graphKeys = props.graphKeys || new Set([]);
const graphs = props.graphs || {};
const invites = props.invites ?
props.invites : {};
const {
api, sidebarShown, s3,
hideAvatars, hideNicknames, remoteContentPolicy
} = this.props;
return (
<>
<Helmet defer={false}>
<title>OS1 - Links</title>
</Helmet>
<Switch>
<Route exact path="/~link"
render={ (props) => (
<Skeleton
active="collections"
associations={associations}
invites={invites}
groups={groups}
rightPanelHide={true}
sidebarShown={sidebarShown}
api={api}
graphKeys={graphKeys}>
<MessageScreen text="Select or create a collection to begin." />
</Skeleton>
)}
/>
<Route exact path="/~link/new"
render={ (props) => (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
sidebarShown={sidebarShown}
api={api}
graphKeys={graphKeys}>
<NewScreen
api={api}
graphKeys={graphKeys}
associations={associations}
groups={groups}
{...props}
/>
</Skeleton>
)}
/>
<Route exact path="/~link/:ship/:name/settings"
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const metPath = `/ship/~${resourcePath}`;
const resource =
associations.graph[metPath] ?
associations.graph[metPath] : { metadata: {} };
const contactDetails = contacts[resource['group-path']] || {};
const group = groups[resource['group-path']] || new Set([]);
const amOwner = amOwnerOfGroup(resource['group-path']);
const hasGraph = !!graphs[resourcePath];
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
graphKeys={graphKeys}
api={api}>
<SettingsScreen
sidebarShown={sidebarShown}
resource={resource}
contacts={contacts}
contactDetails={contactDetails}
graphResource={graphKeys.has(resourcePath)}
hasGraph={!!hasGraph}
group={group}
amOwner={amOwner}
resourcePath={resourcePath}
api={api}
{...props} />
</Skeleton>
);
}}
/>
<Route exact path="/~link/:ship/:name"
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const metPath = `/ship/~${resourcePath}`;
const resource =
associations.graph[metPath] ?
associations.graph[metPath] : { metadata: {} };
const contactDetails = contacts[resource['group-path']] || {};
const graph = graphs[resourcePath] || null;
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
sidebarHideMobile={true}
api={api}
graphKeys={graphKeys}>
<LinkList
{...props}
api={api}
s3={s3}
graph={graph}
graphResource={graphKeys.has(resourcePath)}
resourcePath={resourcePath}
metadata={resource.metadata}
contacts={contactDetails}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
sidebarShown={sidebarShown}
ship={props.match.params.ship}
name={props.match.params.name}
/>
</Skeleton>
);
}}
/>
<Route exact path="/~link/:ship/:name/:index"
render={ (props) => {
const resourcePath =
`${props.match.params.ship}/${props.match.params.name}`;
const metPath = `/ship/~${resourcePath}`;
const resource =
associations.graph[metPath] ?
associations.graph[metPath] : { metadata: {} };
const contactDetails = contacts[resource['group-path']] || {};
const indexArr = props.match.params.index.split('-');
const graph = graphs[resourcePath] || null;
if (indexArr.length <= 1) {
return <div>Malformed URL</div>;
}
const index = parseInt(indexArr[1], 10);
const node = Boolean(graph) ? graph.get(index) : null;
return (
<Skeleton
associations={associations}
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
sidebarHideMobile={true}
graphKeys={graphKeys}
api={api}>
<LinkDetail
{...props}
node={node}
graphResource={graphKeys.has(resourcePath)}
ship={props.match.params.ship}
name={props.match.params.name}
resource={resource}
contacts={contactDetails}
sidebarShown={sidebarShown}
api={api}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
remoteContentPolicy={remoteContentPolicy} />
</Skeleton>
);
}}
/>
</Switch>
</>
);
}
}

View File

@ -1,29 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export class ChannelItem extends Component {
render() {
const { props } = this;
const selectedClass = (props.selected)
? 'bg-gray5 bg-gray1-d'
: 'pointer hover-bg-gray5 hover-bg-gray1-d';
const unseenCount = props.unseenCount > 0
? <span className="dib white bg-gray3 bg-gray2-d fw6 br1 absolute" style={{ padding: '1px 5px', right: 8 }}>{props.unseenCount}</span>
: null;
return (
<Link to={`/~link/${props.link}`}>
<div className={'w-100 v-mid f9 ph5 z1 pv1 relative ' + selectedClass}>
<p className="f9 dib">{props.name}</p>
<p className="f9 dib fr">
{unseenCount}
</p>
</div>
</Link>
);
}
}

View File

@ -1,109 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { GroupItem } from './group-item';
import SidebarInvite from '~/views/components/Sidebar/SidebarInvite';
import { Welcome } from './welcome';
import { alphabetiseAssociations } from '~/logic/lib/util';
export const ChannelSidebar = (props) => {
const sidebarInvites = Object.keys(props.invites)
.map((uid) => {
return (
<SidebarInvite
key={uid}
invite={props.invites[uid]}
onAccept={() => props.api.invite.accept('/link', uid)}
onDecline={() => props.api.invite.decline('/link', uid)}
/>
);
});
const associations = props.associations.contacts ?
alphabetiseAssociations(props.associations.contacts) : {};
const graphAssoc = props.associations.graph || {};
const groupedChannels = {};
[...props.graphKeys].map((gKey) => {
const path = `/ship/~${gKey.split('/')[0]}/${gKey.split('/')[1]}`;
const groupPath = graphAssoc[path] ? graphAssoc[path]['group-path'] : '';
if (groupPath in associations) {
// managed
if (groupedChannels[groupPath]) {
const array = groupedChannels[groupPath];
array.push(path);
groupedChannels[groupPath] = array;
} else {
groupedChannels[groupPath] = [path];
}
} else {
// unmanaged
if (groupedChannels['/~/']) {
const array = groupedChannels['/~/'];
array.push(path);
groupedChannels['/~/'] = array;
} else {
groupedChannels['/~/'] = [path];
}
}
});
const groupedItems = Object.keys(associations).map((each, i) => {
const channels = groupedChannels[each];
if (!channels || channels.length === 0) { return; }
return (
<GroupItem
key={i + 1}
unmanaged={false}
association={associations[each]}
metadata={graphAssoc}
channels={channels}
selected={props.selected}
/>
);
});
if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
groupedItems.push(
<GroupItem
key={0}
unmanaged={true}
association={'/~/'}
metadata={graphAssoc}
channels={groupedChannels['/~/']}
selected={props.selected}
/>
);
}
const activeClasses = (props.active === 'collections') ? ' ' : 'dn-s ';
return (
<div className={
`bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100` +
`flex-shrink-0 mw5-m mw5-l mw5-xl pt3 pt0-m pt0-l pt0-xl relative ` +
activeClasses +
((props.sidebarShown) ? 'flex-basis-100-s flex-basis-30-ns' : 'dn')
}>
<div className="overflow-y-scroll h-100">
<div className="w-100 bg-transparent">
<Link
className="dib f9 pointer green2 gray4-d pa4"
to={'/~link/new'}>
New Collection
</Link>
</div>
<Welcome associations={props.associations} />
{sidebarInvites}
{groupedItems}
</div>
</div>
);
};

View File

@ -1,43 +0,0 @@
import React, { Component } from 'react';
import { ChannelItem } from './channel-item';
import { deSig } from '~/logic/lib/util';
export const GroupItem = (props) => {
const association = props.association ? props.association : {};
let title =
association['app-path'] ? association['app-path'] : 'Unmanaged Collections';
if (association.metadata && association.metadata.title) {
title = association.metadata.title !== ''
? association.metadata.title : title;
}
const channels = props.channels ? props.channels : [];
const unmanaged = props.unmanaged ? 'pt6' : 'pt1';
const channelItems = channels.map((each, i) => {
const meta = props.metadata[each];
if (!meta) { return null; }
const link = `${deSig(each.split('/')[2])}/${each.split('/')[3]}`;
const selected = (props.selected === each);
return (
<ChannelItem
key={each}
link={link}
selected={selected}
name={meta.metadata.title}
/>
);
});
return (
<div className={unmanaged}>
<p className="f9 ph4 pb2 gray3">{title}</p>
{channelItems}
</div>
);
};

View File

@ -1,55 +0,0 @@
import React, { Component } from 'react';
import { Sigil } from '~/logic/lib/sigil';
import { uxToHex, cite } from '~/logic/lib/util';
export class MemberElement extends Component {
onRemove() {
const { props } = this;
props.api.groups.remove(props.groupPath, [`~${props.ship}`]);
}
render() {
const { props } = this;
let actionElem;
if (props.ship === props.owner) {
actionElem = (
<p className="w-20 dib list-ship black white-d f8 c-default">
Host
</p>
);
} else if (props.amOwner && window.ship !== props.ship) {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-20 dib list-ship black white-d f8 pointer"
>
Ban
</a>
);
} else {
actionElem = (
<span></span>
);
}
const name = props.contact
? `${props.contact.nickname} (${cite(props.ship)})`
: `${cite(props.ship)}`;
const color = props.contact ? uxToHex(props.contact.color) : '000000';
const img = props.contact.avatar
? <img src={props.contact.avatar} height={32} width={32} className="dib" />
: <Sigil ship={props.ship} size={32} color={`#${color}`} />;
return (
<div className="flex mb2">
{img}
<p className={'w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8'}
title={props.ship}
>
{name}
</p>
{actionElem}
</div>
);
}
}

View File

@ -1,15 +0,0 @@
import React, { Component } from 'react';
export class MessageScreen extends Component {
render() {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
{this.props.text}
</p>
</div>
</div>
);
}
}

View File

@ -1,37 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { makeRoutePath } from '~/logic/lib/util';
export class Pagination extends Component {
render() {
const props = this.props;
const prevPage = (Number(props.page) - 1);
const nextPage = (Number(props.page) + 1);
const prevDisplay = ((props.currentPage > 0))
? 'dib absolute left-0'
: 'dn';
const nextDisplay = ((props.currentPage + 1) < props.totalPages)
? 'dib absolute right-0'
: 'dn';
return (
<div className="w-100 inter relative pv6">
<div className={prevDisplay + ' inter f8'}>
<Link to={makeRoutePath(props.resourcePath, prevPage)}>
&#60;- Previous Page
</Link>
</div>
<div className={nextDisplay + ' inter f8'}>
<Link to={makeRoutePath(props.resourcePath, nextPage)}>
Next Page -&gt;
</Link>
</div>
</div>
);
}
}
export default Pagination;

View File

@ -1,41 +0,0 @@
import React, { Component } from 'react';
export class Welcome extends Component {
constructor() {
super();
this.state = {
show: true
};
this.disableWelcome = this.disableWelcome.bind(this);
}
disableWelcome() {
this.setState({ show: false });
localStorage.setItem('urbit-link:wasWelcomed', JSON.stringify(true));
}
render() {
let wasWelcomed = localStorage.getItem('urbit-link:wasWelcomed');
if (wasWelcomed === null) {
localStorage.setItem('urbit-link:wasWelcomed', JSON.stringify(false));
return wasWelcomed = false;
} else {
wasWelcomed = JSON.parse(wasWelcomed);
}
const associations = this.props.associations ? this.props.associations : {};
return ((!wasWelcomed && this.state.show) && (associations.length !== 0)) ? (
<div className="ma4 pa2 bg-welcome-green bg-gray1-d white-d">
<p className="f8 lh-copy">Links are for collecting and discussing outside content. Each post is a URL and a comment thread.</p>
<p className="f8 pt2 dib bb pointer"
onClick={(() => this.disableWelcome())}
>
Close this
</p>
</div>
) : <div />;
}
}
export default Welcome;

View File

@ -1,100 +0,0 @@
import React, { useEffect } from 'react';
import { TabBar } from '~/views/components/chat-link-tabbar';
import { LinkPreview } from './lib/link-preview';
import { CommentSubmit } from './lib/comment-submit';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { Comments } from './lib/comments';
import { getContactDetails } from '~/logic/lib/util';
import { Box } from '@tlon/indigo-react';
export const LinkDetail = (props) => {
if (!props.node && props.graphResource) {
useEffect(() => {
props.api.graph.getGraph(
`~${props.match.params.ship}`,
props.match.params.name
);
});
return (
<div>Loading...</div>
);
}
if (!props.node) {
return (
<div>Not found</div>
);
}
const { nickname } = getContactDetails(props.contacts[props.node?.post?.author]);
const resourcePath = `${props.ship}/${props.name}`;
const title = props.resource.metadata.title || resourcePath;
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
<Box
pl='12px'
pt='2'
display='flex'
position='relative'
overflowX={['scroll', 'auto']}
flexShrink='0'
borderBottom='1px solid'
borderColor='washedGray'
height='48px'
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
api={props.api}
/>
<Link className="dib f9 fw4 pt2 gray2 lh-solid"
to={`/~link/${resourcePath}`}
>
<h2
className="dib f9 fw4 lh-solid v-top black white-d"
style={{ width: 'max-content' }}
>
{`${title}`}
</h2>
</Link>
<TabBar
location={props.location}
settings={`/~link/${resourcePath}/settings`}
/>
</Box>
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<LinkPreview
resourcePath={resourcePath}
post={props.node.post}
nickname={nickname}
hideNicknames={props.hideNicknames}
commentNumber={props.node.children.size}
remoteContentPolicy={props.remoteContentPolicy}
/>
<div className="flex">
<CommentSubmit
name={props.name}
ship={props.ship}
api={props.api}
parentIndex={props.node.post.index}
/>
</div>
<Comments
comments={props.node.children}
resourcePath={resourcePath}
contacts={props.contacts}
api={props.api}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy}
/>
</div>
</div>
</div>
);
};

View File

@ -33,7 +33,7 @@ export const LinkItem = (props) => {
? <img src={props.avatar} height={36} width={36} className="dib" /> ? <img src={props.avatar} height={36} width={36} className="dib" />
: <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />; : <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />;
const baseUrl = props.baseUrl || `/~link/${resource}`; const baseUrl = props.baseUrl || `/~404/${resource}`;
return ( return (
<Row minWidth='0' flexShrink='0' width="100%" alignItems="center" py={3} bg="white"> <Row minWidth='0' flexShrink='0' width="100%" alignItems="center" py={3} bg="white">

View File

@ -1,91 +0,0 @@
import React, { useEffect } from "react";
import { TabBar } from '~/views/components/chat-link-tabbar';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { LinkItem } from './lib/link-item';
import LinkSubmit from './lib/link-submit';
import { Box } from '@tlon/indigo-react';
import { getContactDetails } from "~/logic/lib/util";
export const LinkList = (props) => {
const resource = `${props.ship}/${props.name}`;
const title = props.metadata.title || resource;
useEffect(() => {
props.api.graph.getGraph(
`~${props.match.params.ship}`,
props.match.params.name
);
}, [props.match.params.ship, props.match.params.name]);
if (!props.graph && props.graphResource) {
return <div>Loading...</div>;
}
if (!props.graph) {
return <div>Not found</div>;
}
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}
>
<Link to="/~link">{"⟵ All Channels"}</Link>
</div>
<Box
pl='12px'
pt='2'
display='flex'
position='relative'
overflowX={['scroll', 'auto']}
flexShrink='0'
borderBottom='1px solid'
borderColor='washedGray'
height='48px'>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
api={props.api} />
<h2
className="dib f9 fw4 pt2 lh-solid v-top black white-d"
style={{ width: 'max-content' }}>
{title}
</h2>
<TabBar
location={props.location}
settings={`/~link/${resource}/settings`}
/>
</Box>
<div className="w-100 mt6 flex justify-center overflow-y-scroll ph4 pb4">
<div className="w-100 mw7">
<div className="flex">
<LinkSubmit
name={props.name}
ship={props.ship}
api={props.api}
s3={props.s3} />
</div>
{ Array.from(props.graph).map(([date, node]) => {
const { nickname, color, avatar } =
getContactDetails(props.contacts[node?.post?.author]);
return (
<LinkItem
key={date}
resource={resource}
node={node}
nickname={nickname}
color={color}
avatar={avatar}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
/>
);
})
}
</div>
</div>
</div>
);
};

View File

@ -1,8 +0,0 @@
import React, { Component } from 'react';
import { MessageScreen } from './lib/message-screen';
export class LoadingScreen extends Component {
render() {
return (<MessageScreen text="Loading..." />);
}
}

View File

@ -1,105 +0,0 @@
import React, { useCallback } from "react";
import { RouteComponentProps } from "react-router-dom";
import { Box, ManagedTextInputField as Input, Col } from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { AsyncButton } from "~/views/components/AsyncButton";
import { FormError } from "~/views/components/FormError";
import GroupSearch from "~/views/components/GroupSearch";
import GlobalApi from "~/logic/api/global";
import { stringToSymbol } from "~/logic/lib/util";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { Associations } from "~/types/metadata-update";
import { Notebooks } from "~/types/publish-update";
import { Groups, GroupPolicy } from "~/types/group-update";
const formSchema = Yup.object({
name: Yup.string().required("Collection must have a name"),
description: Yup.string(),
group: Yup.string(),
});
export function NewScreen(props: object) {
const { history, api } = props;
const waiter = useWaitForProps(props, 5000);
const onSubmit = async (values: object, actions) => {
const resourceId = stringToSymbol(values.name);
try {
const { name, description, group } = values;
if (!!group) {
await props.api.graph.createManagedGraph(
resourceId,
name,
description,
group,
"link"
);
} else {
await props.api.graph.createUnmanagedGraph(
resourceId,
name,
description,
{ invite: { pending: [] } },
"link"
);
}
await waiter((p) => p?.graphKeys?.has(`${window.ship}/${resourceId}`));
actions.setStatus({ success: null });
history.push(`/~link/${window.ship}/${resourceId}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Collection creation failed" });
}
};
return (
<Col p={3}>
<Box mb={4} color="black">New Collection</Box>
<Formik
validationSchema={formSchema}
initialValues={{ name: "", description: "", group: "" }}
onSubmit={onSubmit}>
<Form>
<Box
display="grid"
gridTemplateRows="auto"
gridRowGap={4}
gridTemplateColumns="300px">
<Input
id="name"
label="Name"
caption="Provide a name for your collection"
placeholder="eg. My Links"
/>
<Input
id="description"
label="Description"
caption="What's your collection about?"
placeholder="Collection description"
/>
<GroupSearch
id="group"
label="Group"
caption="What group is the collection for?"
associations={props.associations}
/>
<Box justifySelf="start">
<AsyncButton loadingText="Creating..." type="submit" border>
Create Collection
</AsyncButton>
</Box>
<FormError message="Collection creation failed" />
</Box>
</Form>
</Formik>
</Col>
);
}
export default NewScreen;

View File

@ -1,184 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { LoadingScreen } from './loading';
import { Spinner } from '~/views/components/Spinner';
import { TabBar } from '~/views/components/chat-link-tabbar';
import SidebarSwitcher from '~/views/components/SidebarSwitch';
import { MetadataSettings } from '~/views/components/metadata/settings';
import { Box, Text, Button, Col, Row } from '@tlon/indigo-react';
export class SettingsScreen extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
awaiting: false,
type: 'Editing'
};
this.renderDelete = this.renderDelete.bind(this);
this.changeLoading = this.changeLoading.bind(this);
}
componentDidUpdate() {
const { props, state } = this;
if (Boolean(state.isLoading) && !props.resource) {
this.setState({
isLoading: false
}, () => {
props.history.push('/~link');
});
}
}
changeLoading(isLoading, awaiting, type, closure) {
this.setState({
isLoading,
awaiting,
type
}, closure);
}
removeCollection() {
const { props } = this;
this.setState({
isLoading: true,
awaiting: true,
type: 'Removing'
});
props.api.graph.leaveGraph(
`~${props.match.params.ship}`,
props.match.params.name
);
}
deleteCollection() {
const { props } = this;
this.setState({
isLoading: true,
awaiting: true,
type: 'Deleting'
});
props.api.graph.deleteGraph(props.match.params.name);
}
renderRemove() {
const { props } = this;
if (props.amOwner) {
return null;
} else {
return (
<Box width='100%' mt='3'>
<Text display='block' mt='3' fontSize='1' mb='1'>Remove Collection</Text>
<Text display='block' fontSize='0' gray mb='4'>
Remove this collection from your collection list
</Text>
<Button onClick={this.removeCollection.bind(this)}>
Remove collection
</Button>
</Box>
);
}
}
renderDelete() {
const { props } = this;
if (!props.amOwner) {
return null;
} else {
return (
<Box width='100%' mt='3'>
<Text fontSize='1' mt='3' display='block' mb='1'>Delete collection</Text>
<Text fontSize='0' gray display='block' mb='4'>
Delete this collection, for you and all group members
</Text>
<Button primary onClick={this.deleteCollection.bind(this)} destructive mb='4'>
Delete collection
</Button>
</Box>
);
}
}
render() {
const { props, state } = this;
const title = props.resource.metadata.title || props.resourcePath;
if (
(!props.hasGraph || !props.resource.metadata.color)
&& props.graphResource
) {
return <LoadingScreen />;
} else if (!props.graphResource) {
props.history.push('/~link');
return <Box />;
}
return (
<Col height='100%' width='100' overflowX='hidden'>
<Box width='100%' display={['block', 'none']} pt='4' pb='6' pl='3' fontSize='1' height='1rem'>
<Link to="/~link">{'⟵ All Collections'}</Link>
</Box>
<Row
pl='12px'
pt='2'
borderBottom='1px solid'
borderColor='washedGray'
flexShrink='0'
overflowX={['scroll', 'auto']}
height='48px'
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
api={this.props.api}
/>
<Link className="dib f9 fw4 pt2 gray2 lh-solid"
to={`/~link/${props.resourcePath}`}>
<Text
display='inline-block'
fontSize='0'
verticalAlign='top'
width='max-content'>
{title}
</Text>
</Link>
<TabBar
location={props.location}
settings={`/~link/${props.resourcePath}/settings`}
/>
</Row>
<Box width='100' pl='3' mt='3'>
<Text display='block' fontSize='1' pb='2'>Collection Settings</Text>
{this.renderRemove()}
{this.renderDelete()}
<MetadataSettings
isOwner={props.amOwner}
changeLoading={this.changeLoading}
api={props.api}
association={props.resource}
resource="collection"
app="graph"
module="link"
/>
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-1 bottom-1 pa2 ba b--black b--gray0-d white-d"
text={this.state.type}
/>
</Box>
</Col>
);
}
}

View File

@ -1,36 +0,0 @@
import React, { Component } from 'react';
import { ChannelSidebar } from './lib/channel-sidebar';
import ErrorBoundary from '~/views/components/ErrorBoundary';
export class Skeleton extends Component {
render() {
const { props } = this;
const rightPanelHide = props.rightPanelHide ? 'dn-s' : '';
const linkInvites = ('/link' in props.invites)
? props.invites['/link'] : {};
return (
<div className='absolute w-100 ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl'
style={{ height: 'calc(100% - 45px)' }}>
<div className='bg-white bg-gray0-d cf w-100 h-100 flex ba-m ba-l ba-xl b--gray4 b--gray1-d br1'>
<ChannelSidebar
active={props.active}
associations={props.associations}
invites={linkInvites}
groups={props.groups}
selected={props.selected}
sidebarShown={props.sidebarShown}
api={props.api}
graphKeys={props.graphKeys} />
<div className={'h-100 w-100 flex-auto relative ' + rightPanelHide}
style={{ flexGrow: 1 }}>
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
</div>
</div>
);
}
}

View File

@ -1,30 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Box } from '@tlon/indigo-react';
export const TabBar = (props) => {
const {
location,
settings,
} = props;
let setColor = '';
if (location.pathname.includes('/settings')) {
setColor = 'black white-d';
} else {
setColor = 'gray3';
}
return (
<Box display='inline-block' flexShrink='0' flexGrow='1'>
<Box display='inline-block' pt='9px' fontSize='0' pl='16px' pr='6'>
<Link
className={'no-underline ' + setColor}
to={settings}>
Settings
</Link>
</Box>
</Box>
);
};