mirror of
https://github.com/urbit/shrub.git
synced 2024-11-30 22:15:47 +03:00
interface: add links FE source code
This commit is contained in:
parent
200d7548f6
commit
d3f57fbf9d
183
pkg/interface/link/gulpfile.js
Normal file
183
pkg/interface/link/gulpfile.js
Normal file
@ -0,0 +1,183 @@
|
||||
var gulp = require('gulp');
|
||||
var cssimport = require('gulp-cssimport');
|
||||
var rollup = require('gulp-better-rollup');
|
||||
var cssnano = require('cssnano');
|
||||
var postcss = require('gulp-postcss');
|
||||
var sucrase = require('@sucrase/gulp-plugin');
|
||||
var minify = require('gulp-minify');
|
||||
var rename = require('gulp-rename');
|
||||
var del = require('del');
|
||||
|
||||
var resolve = require('rollup-plugin-node-resolve');
|
||||
var commonjs = require('rollup-plugin-commonjs');
|
||||
var rootImport = require('rollup-plugin-root-import');
|
||||
var globals = require('rollup-plugin-node-globals');
|
||||
|
||||
/***
|
||||
Main config options
|
||||
***/
|
||||
|
||||
var urbitrc = require('../urbitrc');
|
||||
|
||||
/***
|
||||
End main config options
|
||||
***/
|
||||
|
||||
gulp.task('css-bundle', function() {
|
||||
let plugins = [
|
||||
cssnano()
|
||||
];
|
||||
return gulp
|
||||
.src('src/index.css')
|
||||
.pipe(cssimport())
|
||||
.pipe(postcss(plugins))
|
||||
.pipe(gulp.dest('../../arvo/app/link/css'));
|
||||
});
|
||||
|
||||
gulp.task('jsx-transform', function(cb) {
|
||||
return gulp.src('src/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('tile-jsx-transform', function(cb) {
|
||||
return gulp.src('tile/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('js-imports', function(cb) {
|
||||
return gulp.src('dist/index.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
'node_modules/react-is/index.js': [ 'isValidElementType' ],
|
||||
}
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
globals(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('../../arvo/app/link/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
gulp.task('tile-js-imports', function(cb) {
|
||||
return gulp.src('dist/tile.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
}
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
globals(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('../../arvo/app/link/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
gulp.task('js-minify', function () {
|
||||
return gulp.src('../../arvo/app/link/js/index.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('../../arvo/app/link/js/'));
|
||||
});
|
||||
|
||||
gulp.task('tile-js-minify', function () {
|
||||
return gulp.src('../../arvo/app/link/js/tile.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('../../arvo/app/link/js/'));
|
||||
});
|
||||
|
||||
gulp.task('rename-index-min', function() {
|
||||
return gulp.src('../../arvo/app/link/js/index-min.js')
|
||||
.pipe(rename('index.js'))
|
||||
.pipe(gulp.dest('../../arvo/app/link/js/'))
|
||||
});
|
||||
|
||||
gulp.task('rename-tile-min', function() {
|
||||
return gulp.src('../../arvo/app/link/js/tile-min.js')
|
||||
.pipe(rename('tile.js'))
|
||||
.pipe(gulp.dest('../../arvo/app/link/js/'))});
|
||||
|
||||
gulp.task('clean-min', function() {
|
||||
return del(['../../arvo/app/link/js/index-min.js', '../../arvo/app/link/js/tile-min.js'], {force: true})
|
||||
});
|
||||
|
||||
gulp.task('urbit-copy', function () {
|
||||
let ret = gulp.src('../../arvo/**/*');
|
||||
|
||||
urbitrc.URBIT_PIERS.forEach(function(pier) {
|
||||
ret = ret.pipe(gulp.dest(pier));
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
|
||||
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
|
||||
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
|
||||
gulp.task('tile-js-bundle-prod',
|
||||
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
|
||||
|
||||
gulp.task('bundle-dev',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-dev',
|
||||
'tile-js-bundle-dev'
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('bundle-prod',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-prod',
|
||||
'tile-js-bundle-prod',
|
||||
),
|
||||
'rename-index-min',
|
||||
'rename-tile-min',
|
||||
'clean-min',
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('default', gulp.series('bundle-dev'));
|
||||
|
||||
gulp.task('watch', gulp.series('default', function() {
|
||||
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
|
||||
|
||||
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
|
||||
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
|
||||
|
||||
gulp.watch('../../arvo/**/*', gulp.parallel('urbit-copy'));
|
||||
}));
|
6511
pkg/interface/link/package-lock.json
generated
Normal file
6511
pkg/interface/link/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
pkg/interface/link/package.json
Normal file
42
pkg/interface/link/package.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "urbit-apps",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@sucrase/gulp-plugin": "^2.0.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-cssimport": "^7.0.0",
|
||||
"gulp-minify": "^3.1.0",
|
||||
"gulp-postcss": "^8.0.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"moment": "^2.24.0",
|
||||
"rollup": "^1.6.0",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-root-import": "^0.2.3",
|
||||
"sucrase": "^3.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"del": "^5.1.0",
|
||||
"lodash": "^4.17.11",
|
||||
"mousetrap": "^1.6.3",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"urbit-ob": "^4.1.2",
|
||||
"urbit-sigil-js": "^1.3.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"natives": "1.1.3"
|
||||
}
|
||||
}
|
146
pkg/interface/link/src/css/custom.css
Normal file
146
pkg/interface/link/src/css/custom.css
Normal file
@ -0,0 +1,146 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
|
||||
margin-block-end: unset;
|
||||
margin-block-start: unset;
|
||||
-webkit-margin-before: unset;
|
||||
-webkit-margin-after: unset;
|
||||
font-family: Inter, sans-serif;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
textarea, input, button {
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.inter {
|
||||
font-family: Inter, sans-serif;
|
||||
}
|
||||
|
||||
.clamp-3 {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.clamp-message {
|
||||
max-width: calc(100% - 36px - 1.5rem);
|
||||
}
|
||||
|
||||
.clamp-attachment {
|
||||
overflow: scroll;
|
||||
max-height: 10em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.lh-16 {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
|
||||
.c-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.m0a {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* responsive */
|
||||
@media all and (max-width: 34.375em) {
|
||||
.dn-s {
|
||||
display: none;
|
||||
}
|
||||
.flex-basis-100-s, .flex-basis-full-s {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.h-100-m-40-s {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
.black-s {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 34.375em) {
|
||||
.db-ns {
|
||||
display: block;
|
||||
}
|
||||
.flex-basis-30-ns {
|
||||
flex-basis: 30vw;
|
||||
}
|
||||
.h-100-m-40-ns {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333;
|
||||
}
|
||||
.bg-black-d {
|
||||
background-color: black;
|
||||
}
|
||||
.white-d {
|
||||
color: white;
|
||||
}
|
||||
.gray1-d {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.gray2-d {
|
||||
color: #7f7f7f;
|
||||
}
|
||||
.gray3-d {
|
||||
color: #b1b2b3;
|
||||
}
|
||||
.gray4-d {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.bg-gray0-d {
|
||||
background-color: #333;
|
||||
}
|
||||
.bg-gray1-d {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
.b--gray0-d {
|
||||
border-color: #333;
|
||||
}
|
||||
.b--gray2-d {
|
||||
border-color: #7f7f7f;
|
||||
}
|
||||
.bb-d {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.invert-d {
|
||||
filter: invert(1);
|
||||
}
|
||||
.o-60-d {
|
||||
opacity: .6;
|
||||
}
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
63
pkg/interface/link/src/css/fonts.css
Normal file
63
pkg/interface/link/src/css/fonts.css
Normal file
@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
1
pkg/interface/link/src/css/indigo-static.css
Normal file
1
pkg/interface/link/src/css/indigo-static.css
Normal file
File diff suppressed because one or more lines are too long
38
pkg/interface/link/src/css/spinner.css
Normal file
38
pkg/interface/link/src/css/spinner.css
Normal file
@ -0,0 +1,38 @@
|
||||
.spinner-pending {
|
||||
position: relative;
|
||||
content: "";
|
||||
border-radius: 100%;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
background-color: rgba(255,255,255,1);
|
||||
}
|
||||
|
||||
.spinner-pending::after {
|
||||
content: "";
|
||||
background-color: rgba(128,128,128,1);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
clip: rect(0, 16px, 16px, 8px);
|
||||
|
||||
animation: spin 1s cubic-bezier(0.745, 0.045, 0.355, 1.000) infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {transform:rotate(0deg)}
|
||||
25% {transform:rotate(90deg)}
|
||||
50% {transform:rotate(180deg)}
|
||||
75% {transform:rotate(270deg)}
|
||||
100% {transform:rotate(360deg)}
|
||||
}
|
||||
|
||||
.spinner-nostart {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
content:'';
|
||||
background-color: black;
|
||||
}
|
||||
|
5
pkg/interface/link/src/index.css
Normal file
5
pkg/interface/link/src/index.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import "css/indigo-static.css";
|
||||
@import "css/fonts.css";
|
||||
@import "css/spinner.css";
|
||||
@import "css/custom.css";
|
||||
|
21
pkg/interface/link/src/index.js
Normal file
21
pkg/interface/link/src/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Root } from '/components/root';
|
||||
import { HeaderBar } from '/components/lib/header-bar.js';
|
||||
import { api } from '/api';
|
||||
import { store } from '/store';
|
||||
import { subscription } from "/subscription";
|
||||
|
||||
api.setAuthTokens({
|
||||
ship: window.ship
|
||||
});
|
||||
|
||||
subscription.start();
|
||||
|
||||
ReactDOM.render((
|
||||
<HeaderBar />
|
||||
), document.getElementById("header"));
|
||||
|
||||
ReactDOM.render((
|
||||
<Root />
|
||||
), document.querySelectorAll("#root")[0]);
|
231
pkg/interface/link/src/js/api.js
Normal file
231
pkg/interface/link/src/js/api.js
Normal file
@ -0,0 +1,231 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import { uuid } from '/lib/util';
|
||||
import { store } from '/store';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
class UrbitApi {
|
||||
setAuthTokens(authTokens) {
|
||||
this.authTokens = authTokens;
|
||||
this.bindPaths = [];
|
||||
|
||||
this.invite = {
|
||||
accept: this.inviteAccept.bind(this),
|
||||
decline: this.inviteDecline.bind(this),
|
||||
invite: this.inviteInvite.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
bind(path, method, ship = this.authTokens.ship, app, success, fail, quit) {
|
||||
this.bindPaths = _.uniq([...this.bindPaths, path]);
|
||||
|
||||
window.subscriptionId = window.urb.subscribe(ship, app, path,
|
||||
(err) => {
|
||||
fail(err);
|
||||
},
|
||||
(event) => {
|
||||
success({
|
||||
data: event,
|
||||
from: {
|
||||
ship,
|
||||
path
|
||||
}
|
||||
});
|
||||
},
|
||||
(qui) => {
|
||||
quit(qui);
|
||||
});
|
||||
}
|
||||
|
||||
action(appl, mark, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.urb.poke(ship, appl, mark, data,
|
||||
(json) => {
|
||||
resolve(json);
|
||||
},
|
||||
(err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
inviteAction(data) {
|
||||
this.action("invite-store", "json", data);
|
||||
}
|
||||
|
||||
inviteInvite(path, ship) {
|
||||
this.action("invite-hook", "json",
|
||||
{
|
||||
invite: {
|
||||
path: '/chat',
|
||||
invite: {
|
||||
path,
|
||||
ship: `~${window.ship}`,
|
||||
recipient: ship,
|
||||
app: 'chat-hook',
|
||||
text: `You have been invited to /${window.ship}${path}`,
|
||||
},
|
||||
uid: uuid()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
inviteAccept(uid) {
|
||||
this.inviteAction({
|
||||
accept: {
|
||||
path: '/chat',
|
||||
uid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
inviteDecline(uid) {
|
||||
this.inviteAction({
|
||||
decline: {
|
||||
path: '/chat',
|
||||
uid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getComments(path, url, page, index) {
|
||||
let endpoint = "/~link/discussions" + path + "/" + window.btoa(url) + ".json?p=0";
|
||||
let promise = await fetch(endpoint);
|
||||
if (promise.ok) {
|
||||
let comments = {};
|
||||
comments["link-update"] = {};
|
||||
comments["link-update"].comments = {};
|
||||
comments["link-update"].comments.path = path;
|
||||
comments["link-update"].comments.page = page;
|
||||
comments["link-update"].comments.index = index;
|
||||
comments["link-update"].comments.data = await promise.json();
|
||||
store.handleEvent(comments);
|
||||
}
|
||||
}
|
||||
|
||||
async getCommentsPage(path, url, page, index, commentPage) {
|
||||
let endpoint = "/~link/discussions" + path + "/" + window.btoa(url) + ".json?p=" + commentPage;
|
||||
let promise = await fetch(endpoint);
|
||||
if (promise.ok) {
|
||||
let comPage = "page" + commentPage;
|
||||
let responseData = await promise.json();
|
||||
let update = {};
|
||||
update["link-update"] = {};
|
||||
update["link-update"].commentPage = {};
|
||||
update["link-update"].commentPage.path = path;
|
||||
update["link-update"].commentPage.linkPage = page;
|
||||
update["link-update"].commentPage.index = index;
|
||||
update["link-update"].commentPage.comPageNo = commentPage;
|
||||
update["link-update"].commentPage.data = responseData.page;
|
||||
store.handleEvent(update);
|
||||
}
|
||||
}
|
||||
|
||||
async getPage(path, page) {
|
||||
let endpoint = "/~link/submissions" + path + ".json?p=" + page;
|
||||
let promise = await fetch(endpoint);
|
||||
if (promise.ok) {
|
||||
let resolvedPage = await promise.json();
|
||||
let update = {};
|
||||
update["link-update"] = {};
|
||||
update["link-update"].page = {};
|
||||
update["link-update"].page[path] = {
|
||||
"page": page
|
||||
};
|
||||
update["link-update"].page[path].links = resolvedPage.page;
|
||||
store.handleEvent(update);
|
||||
}
|
||||
}
|
||||
|
||||
async postLink(path, url, title) {
|
||||
let json =
|
||||
{ 'path': path,
|
||||
'title': title,
|
||||
'url': url
|
||||
};
|
||||
let endpoint = "/~link/save";
|
||||
let post = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(json)
|
||||
});
|
||||
|
||||
if (post.ok) {
|
||||
let update = {};
|
||||
update["link-update"] = {};
|
||||
update["link-update"].add = {};
|
||||
update["link-update"].add[path] = {};
|
||||
update["link-update"].add[path] = {
|
||||
"title": title,
|
||||
"url": url,
|
||||
"timestamp": moment.now(),
|
||||
"ship": window.ship,
|
||||
"commentCount": 0
|
||||
}
|
||||
store.handleEvent(update);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async postComment(path, url, comment, page, index) {
|
||||
let json = {
|
||||
'path': path,
|
||||
'url': url,
|
||||
'udon': comment
|
||||
}
|
||||
|
||||
let endpoint = "/~link/note";
|
||||
let post = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(json)
|
||||
});
|
||||
|
||||
if (post.ok) {
|
||||
let update = {};
|
||||
update["link-update"] = {};
|
||||
update["link-update"].commentAdd = {};
|
||||
update["link-update"].commentAdd = {
|
||||
"path": path,
|
||||
"url": url,
|
||||
"udon": comment,
|
||||
"page": page,
|
||||
"index": index,
|
||||
"time": moment.now()
|
||||
}
|
||||
store.handleEvent(update);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
sidebarToggle() {
|
||||
let sidebarBoolean = true;
|
||||
if (store.state.sidebarShown === true) {
|
||||
sidebarBoolean = false;
|
||||
}
|
||||
store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
'sidebarToggle': sidebarBoolean
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export let api = new UrbitApi();
|
||||
window.api = api;
|
91
pkg/interface/link/src/js/components/lib/channel-sidebar.js
Normal file
91
pkg/interface/link/src/js/components/lib/channel-sidebar.js
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { ChannelsItem } from '/components/lib/channels-item';
|
||||
|
||||
export class ChannelsSidebar extends Component {
|
||||
// drawer to the left
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
let privateChannel =
|
||||
Object.keys(props.paths)
|
||||
.filter((path) => {
|
||||
return (path === "/~/default")
|
||||
})
|
||||
.map((path) => {
|
||||
let name = "Private"
|
||||
let selected = (props.selected === path);
|
||||
let linkCount = !!props.links[path] ? props.links[path]['total-items'] : 0;
|
||||
return (
|
||||
<ChannelsItem
|
||||
key={path}
|
||||
link={path}
|
||||
members={props.paths[path]}
|
||||
selected={selected}
|
||||
linkCount={linkCount}
|
||||
name={name}/>
|
||||
)
|
||||
})
|
||||
|
||||
let channelItems =
|
||||
Object.keys(props.paths)
|
||||
.filter((path) => {
|
||||
return (!path.startsWith("/~/"))
|
||||
})
|
||||
.map((path) => {
|
||||
let name = path.substr(1);
|
||||
let nameSeparator = name.indexOf("/");
|
||||
name = name.substr(nameSeparator + 1);
|
||||
|
||||
let selected = (props.selected === path);
|
||||
let linkCount = !!props.links[path] ? props.links[path]['total-items'] : 0;
|
||||
|
||||
return (
|
||||
<ChannelsItem
|
||||
key={path}
|
||||
link={path}
|
||||
members={props.paths[path]}
|
||||
selected={selected}
|
||||
linkCount={linkCount}
|
||||
name={name}/>
|
||||
)
|
||||
});
|
||||
|
||||
let activeClasses = (this.props.active === "channels") ? " " : "dn-s ";
|
||||
|
||||
let hiddenClasses = true;
|
||||
|
||||
// probably a more concise way to write this
|
||||
|
||||
if (this.props.popout) {
|
||||
hiddenClasses = false;
|
||||
} else {
|
||||
hiddenClasses = this.props.sidebarShown;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`bn br-m br-l br-xl b--gray4 b--gray2-d lh-copy h-100
|
||||
flex-shrink-0 mw5-m mw5-l mw5-xl pt3 pt0-m pt0-l pt0-xl
|
||||
relative ` + activeClasses + ((hiddenClasses)
|
||||
? "flex-basis-100-s flex-basis-30-ns"
|
||||
: "dn")}>
|
||||
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/">⟵ Landscape</a>
|
||||
<div className="overflow-y-scroll h-100">
|
||||
<h2 className={`f8 f9-m f9-l f9-xl
|
||||
pt1 pt4-m pt4-l pt4-xl
|
||||
pr4 pb3 pb2-m pb2-l pb2-xl
|
||||
pl3 pl4-m pl4-l pl4-xl
|
||||
black-s gray2 white-d c-default
|
||||
bb bn-m bn-l bn-xl b--gray4 mb2 mb0-m mb0-l mb0-xl`}>
|
||||
Your Collections
|
||||
</h2>
|
||||
{privateChannel}
|
||||
{channelItems}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
30
pkg/interface/link/src/js/components/lib/channels-item.js
Normal file
30
pkg/interface/link/src/js/components/lib/channels-item.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
|
||||
export class ChannelsItem extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let selectedClass = (props.selected)
|
||||
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
||||
: "b--transparent";
|
||||
|
||||
let memberCount = Object.keys(props.members).length;
|
||||
|
||||
return (
|
||||
<Link to={"/~link" + props.link}>
|
||||
<div className={"w-100 v-mid f9 pl4 bt bb " + selectedClass}>
|
||||
<p className="f9 pt1">{props.name}</p>
|
||||
<p className="f9 gray2">
|
||||
{memberCount + " contributor" + ((memberCount === 1) ? "" : "s")}
|
||||
</p>
|
||||
<p className="f9 pb1">
|
||||
{props.linkCount + " link" + ((props.linkCount === 1) ? "" : "s")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
57
pkg/interface/link/src/js/components/lib/comment-item.js
Normal file
57
pkg/interface/link/src/js/components/lib/comment-item.js
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Sigil } from './icons/sigil';
|
||||
import moment from 'moment';
|
||||
|
||||
export class CommentItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timeSinceComment: this.getTimeSinceComment()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
|
||||
this.setState({timeSinceComment: this.getTimeSinceComment()});
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.updateTimeSinceNewestMessageInterval) {
|
||||
clearInterval(this.updateTimeSinceNewestMessageInterval);
|
||||
this.updateTimeSinceNewestMessageInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeSinceComment() {
|
||||
return !!this.props.time ?
|
||||
moment.unix(this.props.time / 1000).from(moment.utc())
|
||||
: '';
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
return (
|
||||
<div className="w-100 pv3">
|
||||
<div className="flex">
|
||||
<Sigil
|
||||
ship={"~" + props.ship}
|
||||
size={36}
|
||||
color={"#" + props.color}
|
||||
/>
|
||||
<p className="gray2 f9 flex items-center ml2">
|
||||
<span className={"black white-d " + props.nameClass}>
|
||||
{((props.nickname) ? props.nickname : props.ship)}
|
||||
</span>
|
||||
<span className="ml2">
|
||||
{this.state.timeSinceComment}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="inter f8 pv3 white-d">{props.content}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentItem
|
@ -0,0 +1,48 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
|
||||
export class CommentsPagination extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let prevPage = "/" + (Number(props.commentPage) - 1);
|
||||
let nextPage = "/" + (Number(props.commentPage) + 1);
|
||||
|
||||
let prevDisplay = ((Number(props.commentPage) > 0))
|
||||
? "dib"
|
||||
: "dn";
|
||||
|
||||
let nextDisplay = (Number(props.commentPage + 1) < Number(props.total))
|
||||
? "dib"
|
||||
: "dn";
|
||||
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
|
||||
return (
|
||||
<div className="w-100 relative pt4 pb6">
|
||||
<Link
|
||||
className={"pb6 absolute inter f8 left-0 " + prevDisplay}
|
||||
to={"/~link"
|
||||
+ popout
|
||||
+ props.path
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/comments" + prevPage}>
|
||||
<- Previous Page
|
||||
</Link>
|
||||
<Link
|
||||
className={"pb6 absolute inter f8 right-0 " + nextDisplay}
|
||||
to={"/~link"
|
||||
+ popout
|
||||
+ props.path
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/comments" + nextPage}>
|
||||
Next Page ->
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentsPagination;
|
104
pkg/interface/link/src/js/components/lib/comments.js
Normal file
104
pkg/interface/link/src/js/components/lib/comments.js
Normal file
@ -0,0 +1,104 @@
|
||||
import React, { Component } from 'react'
|
||||
import { CommentItem } from './comment-item';
|
||||
import { CommentsPagination } from './comments-pagination';
|
||||
|
||||
import { uxToHex } from '../../lib/util';
|
||||
import { api } from '../../api';
|
||||
|
||||
export class Comments extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
let page = "page" + this.props.commentPage;
|
||||
let comments = !!this.props.comments;
|
||||
if ((!comments[page]) && (page !== "page0")) {
|
||||
api.getCommentsPage(
|
||||
this.props.path,
|
||||
this.props.url,
|
||||
this.props.linkPage,
|
||||
this.props.linkIndex,
|
||||
this.props.commentPage);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
let page = "page" + this.props.commentPage;
|
||||
if (prevProps !== this.props) {
|
||||
if (!!this.props.comments) {
|
||||
if ((page !== "page0") && (!this.props.comments[page])) {
|
||||
api.getCommentsPage(
|
||||
this.props.path,
|
||||
this.props.url,
|
||||
this.props.linkPage,
|
||||
this.props.linkIndex,
|
||||
this.props.commentPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let page = "page" + props.commentPage;
|
||||
|
||||
let commentsObj = !!props.comments
|
||||
? props.comments
|
||||
: {};
|
||||
|
||||
let commentsPage = !!commentsObj[page]
|
||||
? commentsObj[page]
|
||||
: {};
|
||||
|
||||
let total = !!props.comments
|
||||
? props.comments["total-pages"]
|
||||
: {};
|
||||
|
||||
let commentsList = Object.keys(commentsPage)
|
||||
.map((entry) => {
|
||||
|
||||
let commentObj = commentsPage[entry]
|
||||
let { ship, time, udon } = commentObj;
|
||||
|
||||
let members = !!props.members
|
||||
? props.members
|
||||
: {};
|
||||
|
||||
let nickname = !!members[ship]
|
||||
? members[ship].nickname
|
||||
: "";
|
||||
|
||||
let nameClass = nickname ? "inter" : "mono";
|
||||
|
||||
let color = !!members[ship]
|
||||
? uxToHex(members[ship].color)
|
||||
: "000000";
|
||||
|
||||
return(
|
||||
<CommentItem
|
||||
key={time}
|
||||
ship={ship}
|
||||
time={time}
|
||||
content={udon}
|
||||
nickname={nickname}
|
||||
nameClass={nameClass}
|
||||
color={color}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<div>
|
||||
{commentsList}
|
||||
<CommentsPagination
|
||||
key={props.path + props.commentPage}
|
||||
path={props.path}
|
||||
popout={props.popout}
|
||||
linkPage={props.linkPage}
|
||||
linkIndex={props.linkIndex}
|
||||
commentPage={props.commentPage}
|
||||
total={total}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Comments;
|
56
pkg/interface/link/src/js/components/lib/header-bar.js
Normal file
56
pkg/interface/link/src/js/components/lib/header-bar.js
Normal file
@ -0,0 +1,56 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { IconHome } from '/components/lib/icons/icon-home';
|
||||
import { IconSpinner } from '/components/lib/icons/icon-spinner';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
|
||||
export class HeaderBar extends Component {
|
||||
render() {
|
||||
// let spin = (this.props.spinner)
|
||||
// ? <div className="absolute"
|
||||
// style={{width: 16, height: 16, top: 16, left: 55}}>
|
||||
// <IconSpinner/>
|
||||
// </div>
|
||||
// : null;
|
||||
|
||||
let popout = (window.location.href.includes("popout/"))
|
||||
? "dn"
|
||||
: "dn db-m db-l db-xl";
|
||||
|
||||
let title = (document.title === "Home")
|
||||
? ""
|
||||
: document.title;
|
||||
|
||||
return (
|
||||
<div className={"bg-white bg-gray0-d w-100 justify-between relative tc pt3 "
|
||||
+ popout}
|
||||
style={{ height: 40 }}>
|
||||
<a className="dib gray2 f9 inter absolute left-1"
|
||||
href='/'
|
||||
style={{top: 14}}>
|
||||
<IconHome/>
|
||||
<span className="ml2 white-d v-top lh-title"
|
||||
style={{paddingTop: 3}}>
|
||||
Home
|
||||
</span>
|
||||
</a>
|
||||
<span className="f9 white-d inter dib"
|
||||
style={{
|
||||
verticalAlign: "text-top",
|
||||
paddingTop: 3
|
||||
}}>{title}</span>
|
||||
{/* {spin} */}
|
||||
<div className="absolute right-1 lh-copy"
|
||||
style={{top: 12}}>
|
||||
<Sigil
|
||||
ship={"~" + window.ship}
|
||||
size={16}
|
||||
color={"#000000"}
|
||||
/>
|
||||
<span className="mono white-d f9 ml2 v-top">{"~" + window.ship}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
12
pkg/interface/link/src/js/components/lib/icons/icon-home.js
Normal file
12
pkg/interface/link/src/js/components/lib/icons/icon-home.js
Normal file
@ -0,0 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class IconHome extends Component {
|
||||
render() {
|
||||
return (
|
||||
//TODO relocate to ~launch when OS1 is ported
|
||||
<img
|
||||
className="invert-d"
|
||||
src="/~link/img/Home.png" width={16} height={16} />
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react'
|
||||
import { api } from '../../../api'
|
||||
|
||||
export class SidebarSwitcher extends Component {
|
||||
render() {
|
||||
|
||||
let popoutSwitcher = this.props.popout
|
||||
? "dn-m dn-l dn-xl"
|
||||
: "dib-m dib-l dib-xl";
|
||||
|
||||
return (
|
||||
<div className="pt2">
|
||||
<a
|
||||
className="pointer flex-shrink-0"
|
||||
onClick={() => {
|
||||
api.sidebarToggle();
|
||||
}}>
|
||||
<img
|
||||
className={`pr3 invert-d dn ` + popoutSwitcher}
|
||||
src={
|
||||
this.props.sidebarShown
|
||||
? "/~link/img/SwitcherOpen.png"
|
||||
: "/~link/img/SwitcherClosed.png"
|
||||
}
|
||||
height="16"
|
||||
width="16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarSwitcher
|
@ -0,0 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class IconSpinner extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="spinner-pending"></div>
|
||||
);
|
||||
}
|
||||
}
|
27
pkg/interface/link/src/js/components/lib/icons/sigil.js
Normal file
27
pkg/interface/link/src/js/components/lib/icons/sigil.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { Component } from 'react';
|
||||
import { sigil, reactRenderer } from 'urbit-sigil-js';
|
||||
|
||||
|
||||
export class Sigil extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
if (props.ship.length > 14) {
|
||||
return (
|
||||
<div className="bg-black flex-shrink-0" style={{width: props.size, height: props.size}}>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="dib flex-shrink-0" style={{ flexBasis: 32, backgroundColor: props.color }}>
|
||||
{sigil({
|
||||
patp: props.ship,
|
||||
renderer: reactRenderer,
|
||||
size: props.size,
|
||||
colors: [props.color, "white"]
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
84
pkg/interface/link/src/js/components/lib/link-item.js
Normal file
84
pkg/interface/link/src/js/components/lib/link-item.js
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { Component } from 'react'
|
||||
import moment from 'moment';
|
||||
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
|
||||
export class LinkItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timeSinceLinkPost: this.getTimeSinceLinkPost()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
|
||||
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()});
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.updateTimeSinceNewestMessageInterval) {
|
||||
clearInterval(this.updateTimeSinceNewestMessageInterval);
|
||||
this.updateTimeSinceNewestMessageInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeSinceLinkPost() {
|
||||
return !!this.props.timestamp ?
|
||||
moment.unix(this.props.timestamp / 1000).from(moment.utc())
|
||||
: '';
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let props = this.props;
|
||||
|
||||
let mono = (props.nickname) ? "inter white-d" : "mono white-d";
|
||||
|
||||
let URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/);
|
||||
|
||||
let hostname = URLparser.exec(props.url);
|
||||
|
||||
if (hostname) {
|
||||
hostname = hostname[4];
|
||||
}
|
||||
|
||||
let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s");
|
||||
|
||||
return (
|
||||
<div className="w-100 pv3 flex">
|
||||
<Sigil
|
||||
ship={"~" + props.ship}
|
||||
size={36}
|
||||
color={"#" + props.color}
|
||||
/>
|
||||
<div className="flex flex-column ml2">
|
||||
<a href={props.url}
|
||||
className="w-100 flex"
|
||||
target="_blank">
|
||||
<p className="f8 truncate">{props.title}
|
||||
<span className="gray2 dib truncate-m mw4-m v-btm ml2">{hostname} ↗</span>
|
||||
</p>
|
||||
</a>
|
||||
<div className="w-100 pt1">
|
||||
<span className={"f9 pr2 v-mid " + mono}>{(props.nickname)
|
||||
? props.nickname
|
||||
: "~" + props.ship}</span>
|
||||
<span className="f9 inter gray2 pr3 v-mid">{this.state.timeSinceLinkPost}</span>
|
||||
<Link to=
|
||||
{"/~link" + props.popout + "/" + props.channel + "/" + props.page + "/" + props.index}
|
||||
className="v-top">
|
||||
<span className="f9 inter gray2">
|
||||
{comments}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkItem
|
117
pkg/interface/link/src/js/components/lib/link-submit.js
Normal file
117
pkg/interface/link/src/js/components/lib/link-submit.js
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { Component } from 'react'
|
||||
import { api } from '../../api';
|
||||
|
||||
|
||||
export class LinkSubmit extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
linkValue: "",
|
||||
linkTitle: "",
|
||||
linkValid: false
|
||||
}
|
||||
this.setLinkValue = this.setLinkValue.bind(this);
|
||||
this.setLinkTitle = this.setLinkTitle.bind(this);
|
||||
}
|
||||
|
||||
onClickPost() {
|
||||
let link = this.state.linkValue;
|
||||
let title = (this.state.linkTitle)
|
||||
? this.state.linkTitle
|
||||
: this.state.linkValue;
|
||||
let request = api.postLink(this.props.path, link, title);
|
||||
|
||||
if (request) {
|
||||
this.setState({linkValue: "", linkTitle: ""})
|
||||
}
|
||||
}
|
||||
|
||||
setLinkValid(link) {
|
||||
let URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/);
|
||||
|
||||
let validURL = URLparser.exec(link);
|
||||
|
||||
if (!validURL) {
|
||||
let checkProtocol = URLparser.exec("http://" + link);
|
||||
if (checkProtocol) {
|
||||
this.setState({linkValid: true});
|
||||
this.setState({linkValue: "http://" + link});
|
||||
} else {
|
||||
this.setState({linkValid: false})
|
||||
}
|
||||
} else if (validURL) {
|
||||
this.setState({linkValid: true});
|
||||
}
|
||||
}
|
||||
|
||||
setLinkValue(event) {
|
||||
this.setState({linkValue: event.target.value});
|
||||
this.setLinkValid(event.target.value);
|
||||
}
|
||||
|
||||
setLinkTitle(event) {
|
||||
this.setState({linkTitle: event.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
let activeClasses = (this.state.linkValid)
|
||||
? "green2 pointer"
|
||||
: "gray2";
|
||||
|
||||
return (
|
||||
<div className="relative ba b--gray4 b--gray2-d br1 w-100 mb6">
|
||||
<textarea
|
||||
className="pl2 bg-gray0-d white-d w-100 f8"
|
||||
style={{
|
||||
resize: "none",
|
||||
height: 40,
|
||||
paddingTop: 10
|
||||
}}
|
||||
placeholder="Paste link here"
|
||||
onChange={this.setLinkValue}
|
||||
spellCheck="false"
|
||||
rows={1}
|
||||
onKeyPress={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkValue}
|
||||
/>
|
||||
<textarea
|
||||
className="pl2 bg-gray0-d white-d w-100 f8"
|
||||
style={{
|
||||
resize: "none",
|
||||
height: 40,
|
||||
paddingTop: 16
|
||||
}}
|
||||
placeholder="Enter title"
|
||||
onChange={this.setLinkTitle}
|
||||
spellCheck="false"
|
||||
rows={1}
|
||||
onKeyPress={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkTitle}
|
||||
/>
|
||||
<button
|
||||
className={"absolute bg-gray0-d f8 ml2 flex-shrink-0 " + activeClasses}
|
||||
disabled={!this.state.linkValid}
|
||||
onClick={this.onClickPost.bind(this)}
|
||||
style={{
|
||||
bottom: 12,
|
||||
right: 8
|
||||
}}>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkSubmit;
|
51
pkg/interface/link/src/js/components/lib/links-tabbar.js
Normal file
51
pkg/interface/link/src/js/components/lib/links-tabbar.js
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
export class LinksTabBar extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let memColor = '',
|
||||
popout = '';
|
||||
|
||||
if (props.location.pathname.includes('/members')) {
|
||||
memColor = 'black';
|
||||
} else {
|
||||
memColor = 'gray3';
|
||||
}
|
||||
|
||||
(props.location.pathname.includes('/popout'))
|
||||
? popout = "popout/"
|
||||
: popout = "";
|
||||
|
||||
let hidePopoutIcon = (this.props.popout)
|
||||
? "dn-m dn-l dn-xl"
|
||||
: "dib-m dib-l dib-xl";
|
||||
|
||||
|
||||
return (
|
||||
<div className="dib pt2 flex-shrink-0 flex-grow-1">
|
||||
{!!props.isOwner ? (
|
||||
<div className={"dib f8 pl6"}>
|
||||
<Link
|
||||
className={"no-underline " + memColor}
|
||||
to={`/~link/` + popout + `members` + props.path}>
|
||||
Members
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dib" style={{ width: 0 }}></div>
|
||||
)}
|
||||
<a href={`/~link/popout` + props.path} target="_blank"
|
||||
className="dib fr">
|
||||
<img
|
||||
className={`flex-shrink-0 pr4 dn invert-d ` + hidePopoutIcon}
|
||||
src="/~link/img/popout.png"
|
||||
height="16"
|
||||
width="16"/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LinksTabBar
|
36
pkg/interface/link/src/js/components/lib/pagination.js
Normal file
36
pkg/interface/link/src/js/components/lib/pagination.js
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
|
||||
export class Pagination extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let prevPage = "/" + (Number(props.page) - 1);
|
||||
let nextPage = "/" + (Number(props.page) + 1);
|
||||
|
||||
let prevDisplay = ((props.currentPage > 0))
|
||||
? "dib absolute left-0"
|
||||
: "dn";
|
||||
|
||||
let 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={"/~link" + props.popout + props.path + prevPage}>
|
||||
<- Previous Page
|
||||
</Link>
|
||||
</div>
|
||||
<div className={nextDisplay + " inter f8"}>
|
||||
<Link to={"/~link" + props.popout + props.path + nextPage}>
|
||||
Next Page ->
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Pagination
|
217
pkg/interface/link/src/js/components/link.js
Normal file
217
pkg/interface/link/src/js/components/link.js
Normal file
@ -0,0 +1,217 @@
|
||||
import React, { Component } from 'react'
|
||||
import { LinksTabBar } from './lib/links-tabbar';
|
||||
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
||||
import { api } from '../api';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { Comments } from './lib/comments';
|
||||
import { uxToHex } from '../lib/util';
|
||||
import moment from 'moment'
|
||||
|
||||
export class LinkDetail extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timeSinceLinkPost: this.getTimeSinceLinkPost(),
|
||||
comment: ""
|
||||
};
|
||||
|
||||
this.setComment = this.setComment.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// if we have preloaded our data,
|
||||
// but no comments, grab the comments
|
||||
if (!!this.props.data.url) {
|
||||
let props = this.props;
|
||||
let comments = !!props.data.comments;
|
||||
|
||||
if (!comments) {
|
||||
api.getComments(props.path, props.data.url, props.page, props.link);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
|
||||
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()});
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// if we came to this page *directly*,
|
||||
// load the comments -- DidMount will fail
|
||||
if (this.props.data.url !== prevProps.data.url) {
|
||||
let props = this.props;
|
||||
let comments = !!this.props.data.comments;
|
||||
|
||||
if (!comments && this.props.data.url) {
|
||||
api.getComments(props.path, props.data.url, props.page, props.link);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.data.timestamp !== prevProps.data.timestamp) {
|
||||
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.updateTimeSinceNewestMessageInterval) {
|
||||
clearInterval(this.updateTimeSinceNewestMessageInterval);
|
||||
this.updateTimeSinceNewestMessageInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeSinceLinkPost() {
|
||||
return !!this.props.data.timestamp ?
|
||||
moment.unix(this.props.data.timestamp / 1000).from(moment.utc())
|
||||
: '';
|
||||
}
|
||||
|
||||
onClickPost() {
|
||||
let url = this.props.data.url || "";
|
||||
|
||||
let request = api.postComment(
|
||||
this.props.path,
|
||||
url,
|
||||
this.state.comment,
|
||||
this.props.page,
|
||||
this.props.link
|
||||
);
|
||||
|
||||
if (request) {
|
||||
this.setState({comment: ""})
|
||||
}
|
||||
}
|
||||
|
||||
setComment(event) {
|
||||
this.setState({comment: event.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
let path = props.path + "/" + props.page + "/" + props.link;
|
||||
|
||||
let ship = props.data.ship || "zod";
|
||||
let title = props.data.title || "";
|
||||
let url = props.data.url || "";
|
||||
|
||||
let URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/);
|
||||
|
||||
let hostname = URLparser.exec(url);
|
||||
|
||||
if (hostname) {
|
||||
hostname = hostname[4];
|
||||
}
|
||||
|
||||
let commentCount = props.data.commentCount || 0;
|
||||
|
||||
let comments = commentCount + " comment" + ((commentCount === 1) ? "" : "s");
|
||||
|
||||
let nickname = !!props.members[props.data.ship]
|
||||
? props.members[props.data.ship].nickname
|
||||
: "";
|
||||
|
||||
let nameClass = nickname ? "inter" : "mono";
|
||||
|
||||
let color = !!props.members[props.data.ship]
|
||||
? uxToHex(props.members[props.data.ship].color)
|
||||
: "000000";
|
||||
|
||||
let activeClasses = (this.state.comment)
|
||||
? "black b--black pointer"
|
||||
: "gray2 b--gray2";
|
||||
|
||||
return (
|
||||
<div className="h-100 w-100 overflow-hidden flex flex-column">
|
||||
<div
|
||||
className={`pl3 pt2 flex relative overflow-x-scroll
|
||||
overflow-x-auto-l overflow-x-auto-xl flex-shrink-0
|
||||
bb bn-m bn-l bn-xl b--gray4`}
|
||||
style={{ height: 48 }}>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}/>
|
||||
<Link
|
||||
className="dib f8 fw4 v-top pt2 gray2"
|
||||
to={"/~link" + popout + props.path + "/" + props.page}>
|
||||
{"<- Collection index"}
|
||||
</Link>
|
||||
<LinksTabBar
|
||||
{...props}
|
||||
popout={popout}
|
||||
path={path}/>
|
||||
</div>
|
||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
<div className="pb6 flex">
|
||||
<Sigil
|
||||
ship={"~" + ship}
|
||||
size={36}
|
||||
color={"#" + color}
|
||||
/>
|
||||
<div className="flex flex-column ml2">
|
||||
<a href={url}
|
||||
className="w-100 flex"
|
||||
target="_blank">
|
||||
<p className="f8 truncate">{title}
|
||||
<span className="gray2 ml2 flex-shrink-0">{hostname} ↗</span>
|
||||
</p>
|
||||
</a>
|
||||
<div className="w-100 pt1">
|
||||
<span className={"f9 pr2 white-d v-mid " + nameClass}>{(nickname)
|
||||
? nickname
|
||||
: "~" + ship}
|
||||
</span>
|
||||
<span className="f9 inter gray2 pr3 v-mid">
|
||||
{this.state.timeSinceLinkPost}
|
||||
</span>
|
||||
<Link to={"/~link" + props.path + "/" + props.page + "/" + props.link} className="v-top">
|
||||
<span className="f9 inter gray2">
|
||||
{comments}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative ba br1 b--gray4 b--gray2-d mt6 mb6">
|
||||
<textarea
|
||||
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
|
||||
style={{
|
||||
resize: "none",
|
||||
height: 75
|
||||
}}
|
||||
placeholder="Leave a comment on this link"
|
||||
onChange={this.setComment}
|
||||
value={this.state.comment}
|
||||
/>
|
||||
<button className={"f8 bg-gray0-d white-d ml2 absolute "
|
||||
+ activeClasses}
|
||||
disabled={!this.state.comment}
|
||||
onClick={this.onClickPost.bind(this)}
|
||||
style={{
|
||||
bottom: 12,
|
||||
right: 8
|
||||
}}>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
<Comments
|
||||
path={props.path}
|
||||
key={props.path + props.commentPage}
|
||||
comments={props.data.comments}
|
||||
commentPage={props.commentPage}
|
||||
members={props.members}
|
||||
popout={props.popout}
|
||||
url={props.data.url}
|
||||
linkPage={props.page}
|
||||
linkIndex={props.link}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkDetail;
|
||||
|
144
pkg/interface/link/src/js/components/links-list.js
Normal file
144
pkg/interface/link/src/js/components/links-list.js
Normal file
@ -0,0 +1,144 @@
|
||||
import React, { Component } from 'react'
|
||||
import { LinksTabBar } from './lib/links-tabbar';
|
||||
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
||||
import { Route, Link } from "react-router-dom";
|
||||
import { LinkItem } from '/components/lib/link-item.js';
|
||||
import { LinkSubmit } from '/components/lib/link-submit.js';
|
||||
import { Pagination } from '/components/lib/pagination.js';
|
||||
|
||||
//TODO look at uxToHex wonky functionality
|
||||
import { uxToHex } from '../lib/util';
|
||||
|
||||
//TODO Avatar support once it's in
|
||||
export class Links extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
let linkPage = "page" + this.props.page;
|
||||
if ((this.props.page !== 0) && (!this.props.links[linkPage])) {
|
||||
api.getPage(this.props.path, this.props.page);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
let linkPage = "page" + this.props.page;
|
||||
if ((this.props.page !== 0) && (!this.props.links[linkPage])) {
|
||||
api.getPage(this.props.path, this.props.page);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
let channel = props.path.substr(1);
|
||||
let linkPage = "page" + props.page;
|
||||
|
||||
let links = !!props.links[linkPage]
|
||||
? props.links[linkPage]
|
||||
: {};
|
||||
|
||||
let currentPage = !!props.page
|
||||
? Number(props.page)
|
||||
: 0;
|
||||
|
||||
let totalPages = !!props.links
|
||||
? Number(props.links["total-pages"])
|
||||
: 1;
|
||||
|
||||
let LinkList = Object.keys(links)
|
||||
.map((link) => {
|
||||
let linksObj = props.links[linkPage];
|
||||
let { title, url, timestamp, ship, commentCount } = linksObj[link];
|
||||
let members = {};
|
||||
|
||||
if (!props.members[ship]) {
|
||||
members[ship] = {'nickname': '', 'avatar': 'TODO', 'color': '0x0'};
|
||||
} else {
|
||||
members = props.members;
|
||||
}
|
||||
|
||||
let color = uxToHex('0x0');
|
||||
let nickname = "";
|
||||
|
||||
// restore this to props.members
|
||||
if (members[ship].nickname) {
|
||||
nickname = members[ship].nickname;
|
||||
}
|
||||
|
||||
if (members[ship].color !== "") {
|
||||
color = uxToHex(members[ship].color);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkItem
|
||||
key={timestamp}
|
||||
title={title}
|
||||
page={props.page}
|
||||
index={link}
|
||||
url={url}
|
||||
timestamp={timestamp}
|
||||
nickname={nickname}
|
||||
ship={ship}
|
||||
color={color}
|
||||
comments={commentCount}
|
||||
channel={channel}
|
||||
popout={popout}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
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>
|
||||
<div
|
||||
className={`pl3 pt2 flex relative overflow-x-scroll
|
||||
overflow-x-auto-l overflow-x-auto-xl flex-shrink-0
|
||||
bb bn-m bn-l bn-xl b--gray4`}
|
||||
style={{ height: 48 }}>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}/>
|
||||
<Link to={`/~link` + popout + props.path} className="pt2">
|
||||
<h2
|
||||
className={`dib f8 fw4 v-top ` +
|
||||
(props.path.includes("/~/")
|
||||
? ""
|
||||
: "mono")}>
|
||||
{(props.path.includes("/~/"))
|
||||
? "Private"
|
||||
: channel}
|
||||
</h2>
|
||||
</Link>
|
||||
<LinksTabBar
|
||||
{...props}
|
||||
popout={popout}
|
||||
path={props.path}/>
|
||||
</div>
|
||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
<div className="flex">
|
||||
<LinkSubmit path={props.path}/>
|
||||
</div>
|
||||
<div className="pb4">
|
||||
{LinkList}
|
||||
<Pagination
|
||||
{...props}
|
||||
key={props.path + props.page}
|
||||
popout={popout}
|
||||
path={props.path}
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Links;
|
142
pkg/interface/link/src/js/components/root.js
Normal file
142
pkg/interface/link/src/js/components/root.js
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { Component } from 'react';
|
||||
import { BrowserRouter, Route, Link } from "react-router-dom";
|
||||
import classnames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { api } from '/api';
|
||||
import { subscription } from '/subscription';
|
||||
import { store } from '/store';
|
||||
import { Skeleton } from '/components/skeleton';
|
||||
import { Links } from '/components/links-list';
|
||||
import { LinkDetail } from '/components/link';
|
||||
|
||||
|
||||
export class Root extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = store.state;
|
||||
store.setStateHandler(this.setState.bind(this));
|
||||
this.setSpinner = this.setSpinner.bind(this);
|
||||
}
|
||||
|
||||
setSpinner(spinner) {
|
||||
this.setState({
|
||||
spinner
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
let paths = !!state.contacts ? state.contacts : {};
|
||||
|
||||
let links = !!state.links ? state.links : {};
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Route exact path="/~link"
|
||||
render={ (props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
active="channels"
|
||||
paths={paths}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={true}
|
||||
links={links}>
|
||||
<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">
|
||||
Channels are shared across groups. To create a new channel, <a className="black white-d" href="/~contacts">create a group</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}} />
|
||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page?"
|
||||
render={ (props) => {
|
||||
// groups/contacts and link channels are the same thing in ver 1
|
||||
|
||||
let groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
||||
let groupMembers = paths[groupPath] || {};
|
||||
|
||||
let page = props.match.params.page || 0;
|
||||
|
||||
let popout = props.match.url.includes("/popout/");
|
||||
|
||||
let channelLinks = !!links[groupPath]
|
||||
? links[groupPath]
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
spinner={state.spinner}
|
||||
paths={paths}
|
||||
active="links"
|
||||
selected={groupPath}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
links={links}>
|
||||
<Links
|
||||
{...props}
|
||||
members={groupMembers}
|
||||
links={channelLinks}
|
||||
page={page}
|
||||
path={groupPath}
|
||||
popout={popout}
|
||||
sidebarShown={state.sidebarShown}
|
||||
/>
|
||||
</Skeleton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page/:index/(comments)?/:commentpage?"
|
||||
render={ (props) => {
|
||||
let groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
||||
|
||||
let popout = props.match.url.includes("/popout/");
|
||||
|
||||
let groupMembers = paths[groupPath] || {};
|
||||
|
||||
let index = props.match.params.index || 0;
|
||||
let page = props.match.params.page || 0;
|
||||
|
||||
let data = !!links[groupPath]
|
||||
? links[groupPath]["page" + page][index]
|
||||
: {};
|
||||
|
||||
let commentPage = props.match.params.commentpage || 0;
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
spinner={state.spinner}
|
||||
paths={paths}
|
||||
active="links"
|
||||
selected={groupPath}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
links={links}>
|
||||
<LinkDetail
|
||||
{...props}
|
||||
page={page}
|
||||
link={index}
|
||||
members={groupMembers}
|
||||
path={groupPath}
|
||||
popout={popout}
|
||||
sidebarShown={state.sidebarShown}
|
||||
data={data}
|
||||
commentPage={commentPage}
|
||||
/>
|
||||
</Skeleton>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
}
|
44
pkg/interface/link/src/js/components/skeleton.js
Normal file
44
pkg/interface/link/src/js/components/skeleton.js
Normal file
@ -0,0 +1,44 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { ChannelsSidebar } from './lib/channel-sidebar';
|
||||
|
||||
|
||||
export class Skeleton extends Component {
|
||||
render() {
|
||||
|
||||
let rightPanelHide = this.props.rightPanelHide
|
||||
? "dn-s"
|
||||
: "";
|
||||
|
||||
let popout = !!this.props.popout
|
||||
? this.props.popout
|
||||
: false;
|
||||
|
||||
let popoutWindow = (popout)
|
||||
? ""
|
||||
: "h-100-m-40-ns ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl"
|
||||
|
||||
let popoutBorder = (popout)
|
||||
? ""
|
||||
: "ba-m ba-l ba-xl b--gray2 br1"
|
||||
|
||||
return (
|
||||
<div className={"h-100 w-100 " + popoutWindow}>
|
||||
<div className={`cf w-100 h-100 flex ` + popoutBorder}>
|
||||
<ChannelsSidebar
|
||||
popout={popout}
|
||||
paths={this.props.paths}
|
||||
active={this.props.active}
|
||||
selected={this.props.selected}
|
||||
sidebarShown={this.props.sidebarShown}
|
||||
links={this.props.links}/>
|
||||
<div className={"h-100 w-100 " + rightPanelHide} style={{
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
68
pkg/interface/link/src/js/lib/util.js
Normal file
68
pkg/interface/link/src/js/lib/util.js
Normal file
@ -0,0 +1,68 @@
|
||||
import _ from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
||||
export function uuid() {
|
||||
let str = "0v"
|
||||
str += Math.ceil(Math.random()*8)+"."
|
||||
for (var i = 0; i < 5; i++) {
|
||||
let _str = Math.ceil(Math.random()*10000000).toString(32);
|
||||
_str = ("00000"+_str).substr(-5,5);
|
||||
str += _str+".";
|
||||
}
|
||||
|
||||
return str.slice(0,-1);
|
||||
}
|
||||
|
||||
export function isPatTa(str) {
|
||||
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
|
||||
return !!r;
|
||||
}
|
||||
|
||||
/*
|
||||
Goes from:
|
||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||
To:
|
||||
(javascript Date object)
|
||||
*/
|
||||
export function daToDate(st) {
|
||||
var dub = function(n) {
|
||||
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
||||
};
|
||||
var da = st.split('..');
|
||||
var bigEnd = da[0].split('.');
|
||||
var lilEnd = da[1].split('.');
|
||||
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
return new Date(ds);
|
||||
}
|
||||
|
||||
/*
|
||||
Goes from:
|
||||
(javascript Date object)
|
||||
To:
|
||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||
*/
|
||||
|
||||
export function dateToDa(d, mil) {
|
||||
var fil = function(n) {
|
||||
return n >= 10 ? n : "0" + n;
|
||||
};
|
||||
return (
|
||||
`~${d.getUTCFullYear()}.` +
|
||||
`${(d.getUTCMonth() + 1)}.` +
|
||||
`${fil(d.getUTCDate())}..` +
|
||||
`${fil(d.getUTCHours())}.` +
|
||||
`${fil(d.getUTCMinutes())}.` +
|
||||
`${fil(d.getUTCSeconds())}` +
|
||||
`${mil ? "..0000" : ""}`
|
||||
);
|
||||
}
|
||||
|
||||
export function deSig(ship) {
|
||||
return ship.replace('~', '');
|
||||
}
|
||||
|
||||
export function uxToHex(ux) {
|
||||
let value = ux.substr(2).replace('.', '').padStart(6, '0');
|
||||
return value;
|
||||
}
|
37
pkg/interface/link/src/js/reducers/initial.js
Normal file
37
pkg/interface/link/src/js/reducers/initial.js
Normal file
@ -0,0 +1,37 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class InitialReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'contact-initial', false);
|
||||
if (data) {
|
||||
state.contacts = data;
|
||||
}
|
||||
|
||||
data = _.get(json, 'group-initial', false);
|
||||
if (data) {
|
||||
for (let group in data) {
|
||||
state.groups[group] = new Set(data[group]);
|
||||
}
|
||||
}
|
||||
|
||||
data = _.get(json, 'link', false);
|
||||
if (data) {
|
||||
let name = Object.keys(data)[0];
|
||||
let initial = {};
|
||||
initial[name] = {};
|
||||
initial[name]["total-pages"] = data[name]["total-pages"];
|
||||
initial[name]["total-items"] = data[name]["total-items"];
|
||||
initial[name]["page0"] = data[name]["page"];
|
||||
|
||||
if (!!state.links[name]) {
|
||||
let origin = state.links[name];
|
||||
_.extend(initial[name], origin);
|
||||
} else {
|
||||
state.links[name] = {};
|
||||
}
|
||||
state.links[name] = initial[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
53
pkg/interface/link/src/js/reducers/invite-update.js
Normal file
53
pkg/interface/link/src/js/reducers/invite-update.js
Normal file
@ -0,0 +1,53 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class InviteUpdateReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'invite-update', false);
|
||||
if (data) {
|
||||
this.create(data, state);
|
||||
this.delete(data, state);
|
||||
this.invite(data, state);
|
||||
this.accepted(data, state);
|
||||
this.decline(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
create(json, state) {
|
||||
let data = _.get(json, 'create', false);
|
||||
if (data) {
|
||||
state.invites[data.path] = {};
|
||||
}
|
||||
}
|
||||
|
||||
delete(json, state) {
|
||||
let data = _.get(json, 'delete', false);
|
||||
if (data) {
|
||||
delete state.invites[data.path];
|
||||
}
|
||||
}
|
||||
|
||||
invite(json, state) {
|
||||
let data = _.get(json, 'invite', false);
|
||||
if (data) {
|
||||
state.invites[data.path][data.uid] = data.invite;
|
||||
}
|
||||
}
|
||||
|
||||
accepted(json, state) {
|
||||
let data = _.get(json, 'accepted', false);
|
||||
if (data) {
|
||||
console.log(data);
|
||||
delete state.invites[data.path][data.uid];
|
||||
}
|
||||
}
|
||||
|
||||
decline(json, state) {
|
||||
let data = _.get(json, 'decline', false);
|
||||
if (data) {
|
||||
delete state.invites[data.path][data.uid];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
91
pkg/interface/link/src/js/reducers/link-update.js
Normal file
91
pkg/interface/link/src/js/reducers/link-update.js
Normal file
@ -0,0 +1,91 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class LinkUpdateReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'link-update', false);
|
||||
if (data) {
|
||||
this.add(data, state);
|
||||
this.comments(data, state);
|
||||
this.commentAdd(data, state);
|
||||
this.commentPage(data, state);
|
||||
this.page(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
add(json, state) {
|
||||
// pin ok'd link POSTs to top of page0
|
||||
let data = _.get(json, 'add', false);
|
||||
if (data) {
|
||||
let path = Object.keys(data)[0];
|
||||
let tempArray = state.links[path].page0;
|
||||
tempArray.unshift(data[path]);
|
||||
state.links[path].page0 = tempArray;
|
||||
}
|
||||
}
|
||||
|
||||
comments(json, state) {
|
||||
let data = _.get(json, 'comments', false);
|
||||
if (data) {
|
||||
let path = data.path;
|
||||
let page = "page" + data.page;
|
||||
let index = data.index;
|
||||
let storage = state.links[path][page][index];
|
||||
|
||||
storage.comments = {};
|
||||
storage.comments["page0"] = data.data.page;
|
||||
storage.comments["total-items"] = data.data["total-items"];
|
||||
storage.comments["total-pages"] = data.data["total-pages"];
|
||||
|
||||
state.links[path][page][index] = storage;
|
||||
}
|
||||
}
|
||||
|
||||
commentAdd(json, state) {
|
||||
let data = _.get(json, 'commentAdd', false);
|
||||
if (data) {
|
||||
let path = data.path;
|
||||
let page = "page" + data.page;
|
||||
let index = data.index;
|
||||
|
||||
let ship = window.ship;
|
||||
let time = data.time;
|
||||
let udon = data.udon;
|
||||
let tempObj = {
|
||||
'ship': ship,
|
||||
'time': time,
|
||||
'udon': udon
|
||||
}
|
||||
let tempArray = state.links[path][page][index].comments.page;
|
||||
tempArray.unshift(tempObj);
|
||||
state.links[path][page][index].comments.page = tempArray;
|
||||
}
|
||||
}
|
||||
|
||||
commentPage(json, state) {
|
||||
let data = _.get(json, 'commentPage', false);
|
||||
if (data) {
|
||||
let path = data.path;
|
||||
let linkPage = "page" + data.linkPage;
|
||||
let linkIndex = data.index;
|
||||
let commentPage = "page" + data.comPageNo;
|
||||
|
||||
if (!state.links[path]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.links[path][linkPage][linkIndex].comments[commentPage] = data.data;
|
||||
}
|
||||
}
|
||||
|
||||
page(json, state) {
|
||||
let data = _.get(json, 'page', false);
|
||||
if (data) {
|
||||
let path = Object.keys(data)[0];
|
||||
let page = "page" + data[path].page;
|
||||
if (!state.links[path]) {
|
||||
state.links[path] = {};
|
||||
}
|
||||
state.links[path][page] = data[path].links;
|
||||
}
|
||||
}
|
||||
}
|
17
pkg/interface/link/src/js/reducers/local.js
Normal file
17
pkg/interface/link/src/js/reducers/local.js
Normal file
@ -0,0 +1,17 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class LocalReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'local', false);
|
||||
if (data) {
|
||||
this.sidebarToggle(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
sidebarToggle(obj, state) {
|
||||
let data = _.has(obj, 'sidebarToggle', false);
|
||||
if (data) {
|
||||
state.sidebarShown = obj.sidebarToggle;
|
||||
}
|
||||
}
|
||||
}
|
51
pkg/interface/link/src/js/reducers/permission-update.js
Normal file
51
pkg/interface/link/src/js/reducers/permission-update.js
Normal file
@ -0,0 +1,51 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class PermissionUpdateReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'permission-update', false);
|
||||
if (data) {
|
||||
this.create(data, state);
|
||||
this.delete(data, state);
|
||||
this.add(data, state);
|
||||
this.remove(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
create(json, state) {
|
||||
let data = _.get(json, 'create', false);
|
||||
if (data) {
|
||||
state.permissions[data.path] = {
|
||||
kind: data.kind,
|
||||
who: new Set(data.who)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
delete(json, state) {
|
||||
let data = _.get(json, 'delete', false);
|
||||
if (data) {
|
||||
delete state.permissions[data.path];
|
||||
}
|
||||
}
|
||||
|
||||
add(json, state) {
|
||||
let data = _.get(json, 'add', false);
|
||||
if (data) {
|
||||
for (let member of data.who) {
|
||||
state.permissions[data.path].who.add(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remove(json, state) {
|
||||
let data = _.get(json, 'remove', false);
|
||||
if (data) {
|
||||
for (let member of data.who) {
|
||||
state.permissions[data.path].who.delete(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
68
pkg/interface/link/src/js/store.js
Normal file
68
pkg/interface/link/src/js/store.js
Normal file
@ -0,0 +1,68 @@
|
||||
import { InitialReducer } from '/reducers/initial';
|
||||
import { PermissionUpdateReducer } from '/reducers/permission-update';
|
||||
import { LinkUpdateReducer } from '/reducers/link-update';
|
||||
import { LocalReducer } from '/reducers/local.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
links: {},
|
||||
permissions: {},
|
||||
sidebarShown: true,
|
||||
spinner: false
|
||||
};
|
||||
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.permissionUpdateReducer = new PermissionUpdateReducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.linkUpdateReducer = new LinkUpdateReducer();
|
||||
this.setState = () => {};
|
||||
}
|
||||
|
||||
async loadLinks(json) {
|
||||
// if initial contacts, queue up getting these paths from link-store
|
||||
let data = _.get(json, 'group-initial', false);
|
||||
if (data) {
|
||||
for (let each of Object.keys(data)) {
|
||||
let linkUrl = "/~link/submissions" + each + ".json?p=0";
|
||||
let promise = await fetch(linkUrl);
|
||||
if (promise.ok) {
|
||||
let resolvedData = {}
|
||||
resolvedData.link = {};
|
||||
resolvedData.link[each] = {};
|
||||
resolvedData.link[each] = await promise.json();
|
||||
this.handleEvent(resolvedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
this.setState = setState;
|
||||
}
|
||||
|
||||
handleEvent(data) {
|
||||
let json;
|
||||
if (data.data) {
|
||||
json = data.data;
|
||||
} else {
|
||||
json = data;
|
||||
}
|
||||
|
||||
console.log(json);
|
||||
this.loadLinks(json);
|
||||
this.initialReducer.reduce(json, this.state);
|
||||
this.permissionUpdateReducer.reduce(json, this.state);
|
||||
this.localReducer.reduce(json, this.state);
|
||||
this.linkUpdateReducer.reduce(json, this.state);
|
||||
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
export let store = new Store();
|
||||
window.store = store;
|
47
pkg/interface/link/src/js/subscription.js
Normal file
47
pkg/interface/link/src/js/subscription.js
Normal file
@ -0,0 +1,47 @@
|
||||
import { api } from '/api';
|
||||
import { store } from '/store';
|
||||
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
||||
|
||||
export class Subscription {
|
||||
start() {
|
||||
if (api.authTokens) {
|
||||
this.initializeLinks();
|
||||
} else {
|
||||
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
|
||||
}
|
||||
}
|
||||
|
||||
initializeLinks() {
|
||||
// add invite, permissions flows once link stores are more than
|
||||
// group-specific
|
||||
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this));
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'contact-view',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this));
|
||||
}
|
||||
|
||||
handleEvent(diff) {
|
||||
store.handleEvent(diff);
|
||||
}
|
||||
|
||||
handleError(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
handleQuitSilently(quit) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
handleQuitAndResubscribe(quit) {
|
||||
// TODO: resubscribe
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export let subscription = new Subscription();
|
33
pkg/interface/link/tile/tile.js
Normal file
33
pkg/interface/link/tile/tile.js
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export default class LinkTile extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div className="w-100 h-100 relative ba b--black" style={{ background: "#FFFFFF" }}>
|
||||
<a className="w-100 h-100 db pa2 no-underline" href="/~link">
|
||||
<p
|
||||
className="label-regular b absolute"
|
||||
style={{ left: 8, top: 4 }}>
|
||||
Links
|
||||
</p>
|
||||
<img
|
||||
className="absolute"
|
||||
style={{ left: 69, top: 69 }}
|
||||
src="/~link/img/Tile.png"
|
||||
width={96}
|
||||
height={96}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window['link-server-hookTile'] = LinkTile;
|
Loading…
Reference in New Issue
Block a user