mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 18:12:47 +03:00
Merge pull request #3648 from urbit/la/cleanup-links
interface: removed unused links files
This commit is contained in:
commit
fd39ae195b
@ -1 +0,0 @@
|
|||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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: []
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
@ -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;
|
|
@ -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';
|
|
@ -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 & {
|
||||||
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)}>
|
|
||||||
<- Previous Page
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className={nextDisplay + ' inter f8'}>
|
|
||||||
<Link to={makeRoutePath(props.resourcePath, nextPage)}>
|
|
||||||
Next Page ->
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Pagination;
|
|
@ -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;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -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">
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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..." />);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user