├── .babelrc ├── .eslintrc.js ├── .eslintrc.json ├── .gitignore ├── README.md ├── config ├── urbitrc-sample └── webpack.dev.js ├── gulpfile.js ├── install.js ├── package-lock.json ├── package.json ├── review.png ├── screenshot.png ├── shell.nix ├── src ├── api.js ├── components │ ├── HoverBox.tsx │ ├── edit-item.js │ ├── item.js │ ├── message-screen.js │ ├── new-item.js │ ├── new-stack.js │ ├── not-found.js │ ├── pubs.js │ ├── recall.js │ ├── review.js │ ├── root.js │ ├── skeleton.js │ ├── stack-entry.js │ ├── stack.js │ └── subs.js ├── config │ └── moment.js ├── css │ ├── custom.css │ ├── fonts.css │ ├── indigo-static.css │ ├── spinner.css │ └── tachyons.css ├── img │ └── srrs.png ├── index.css ├── index.js ├── lib │ ├── header-bar.js │ ├── header-menu.js │ ├── icon.js │ ├── icons │ │ ├── icon-check.js │ │ ├── icon-comment.js │ │ ├── icon-cross.js │ │ ├── icon-decline.js │ │ ├── icon-home.js │ │ ├── icon-inbox.js │ │ ├── icon-sidebar-switch.js │ │ ├── icon-sig.js │ │ ├── icon-spinner.js │ │ ├── icon-user.js │ │ └── sigil.js │ ├── item-body.js │ ├── item-preview.js │ ├── item-snippet.js │ ├── next-prev.js │ ├── path-control.js │ ├── review-preview.js │ ├── seal-dict.js │ ├── sidebar.js │ ├── srrs-create.js │ ├── stack-data.js │ ├── stack-notes.js │ ├── stack-settings.js │ ├── stack-subs.js │ ├── title-snippet.js │ └── util.js ├── reducers │ ├── config.js │ ├── initial.js │ ├── learn.js │ ├── primary.js │ ├── response.js │ └── update.js ├── store.js ├── subscription.js └── vendor │ └── sigils-1.2.5.js ├── srrs.gif ├── tile.png ├── tile └── tile.js ├── tsconfig.json └── urbit ├── app ├── srrs-cli.hoon ├── srrs.hoon └── srrs │ ├── img │ ├── Home.png │ ├── arrow.png │ └── srrs.png │ └── index.hoon ├── gen └── memo.hoon ├── lib ├── memo.hoon ├── srrs-json.hoon └── srrs.hoon ├── mar ├── letter.hoon └── srrs │ ├── action.hoon │ ├── info.hoon │ ├── primary-delta.hoon │ ├── stack-delta.hoon │ └── stack.hoon └── sur └── srrs.hoon /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "babel-plugin-root-import", 5 | { 6 | "paths": [ 7 | { 8 | "rootPathSuffix": "./src" 9 | } 10 | ] 11 | } 12 | ] 13 | ] 14 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const env = { 2 | "browser": true, 3 | "es6": true, 4 | "node": true 5 | }; 6 | 7 | const rules = { 8 | "array-bracket-spacing": ["error", "never"], 9 | "arrow-parens": [ 10 | "error", 11 | "as-needed", 12 | { 13 | "requireForBlockBody": true 14 | } 15 | ], 16 | "arrow-spacing": "error", 17 | "block-spacing": ["error", "always"], 18 | "brace-style": ["error", "1tbs"], 19 | "camelcase": [ 20 | "error", 21 | { 22 | "properties": "never" 23 | } 24 | ], 25 | "comma-dangle": ["error", "never"], 26 | "eol-last": ["error", "always"], 27 | "func-name-matching": "error", 28 | "indent": [ 29 | "off", 30 | 2, 31 | { 32 | "ArrayExpression": "off", 33 | "SwitchCase": 1, 34 | "CallExpression": { 35 | "arguments": "off" 36 | }, 37 | "FunctionDeclaration": { 38 | "parameters": "off" 39 | }, 40 | "FunctionExpression": { 41 | "parameters": "off" 42 | }, 43 | "MemberExpression": "off", 44 | "ObjectExpression": "off", 45 | "ImportDeclaration": "off" 46 | } 47 | ], 48 | "handle-callback-err": "off", 49 | "linebreak-style": ["error", "unix"], 50 | "max-lines": [ 51 | "error", 52 | { 53 | "max": 300, 54 | "skipBlankLines": true, 55 | "skipComments": true 56 | } 57 | ], 58 | "max-lines-per-function": [ 59 | "warn", 60 | { 61 | "skipBlankLines": true, 62 | "skipComments": true 63 | } 64 | ], 65 | "max-statements-per-line": [ 66 | "error", 67 | { 68 | "max": 1 69 | } 70 | ], 71 | "new-cap": [ 72 | "error", 73 | { 74 | "newIsCap": true, 75 | "capIsNew": false 76 | } 77 | ], 78 | "new-parens": "error", 79 | "no-buffer-constructor": "error", 80 | "no-console": "off", 81 | "no-extra-semi": "off", 82 | "no-fallthrough": "off", 83 | "no-func-assign": "off", 84 | "no-implicit-coercion": "error", 85 | "no-multi-assign": "error", 86 | "no-multiple-empty-lines": [ 87 | "error", 88 | { 89 | "max": 1 90 | } 91 | ], 92 | "no-nested-ternary": "error", 93 | "no-param-reassign": "off", 94 | "no-return-assign": "error", 95 | "no-return-await": "off", 96 | "no-shadow-restricted-names": "error", 97 | "no-tabs": "error", 98 | "no-trailing-spaces": "error", 99 | "no-unused-vars": [ 100 | "error", 101 | { 102 | "vars": "all", 103 | "args": "none", 104 | "ignoreRestSiblings": false 105 | } 106 | ], 107 | "no-use-before-define": [ 108 | "error", 109 | { 110 | "functions": false, 111 | "classes": false 112 | } 113 | ], 114 | "no-useless-escape": "off", 115 | "no-var": "error", 116 | "nonblock-statement-body-position": ["error", "below"], 117 | "object-curly-spacing": ["error", "always"], 118 | "padded-blocks": ["error", "never"], 119 | "prefer-arrow-callback": "error", 120 | "prefer-const": [ 121 | "error", 122 | { 123 | "destructuring": "all", 124 | "ignoreReadBeforeAssign": true 125 | } 126 | ], 127 | "prefer-template": "off", 128 | "quotes": ["error", "single"], 129 | "semi": ["error", "always"], 130 | "spaced-comment": [ 131 | "error", 132 | "always", 133 | { 134 | "exceptions": ["!"] 135 | } 136 | ], 137 | "space-before-blocks": "error", 138 | "unicode-bom": ["error", "never"], 139 | "valid-jsdoc": "error", 140 | "wrap-iife": ["error", "inside"], 141 | "react/jsx-closing-bracket-location": 1, 142 | "react/jsx-tag-spacing": 1, 143 | "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], 144 | "react/prop-types": 0 145 | }; 146 | 147 | module.exports = { 148 | "env": env, 149 | "extends": [ 150 | "plugin:react/recommended", 151 | "eslint:recommended", 152 | ], 153 | "settings": { 154 | "react": { 155 | "version": "^16.5.2" 156 | } 157 | }, 158 | "parser": "babel-eslint", 159 | "parserOptions": { 160 | "ecmaVersion": 10, 161 | "requireConfigFile": false, 162 | "sourceType": "module" 163 | }, 164 | "root": true, 165 | "rules": rules, 166 | "overrides": [ 167 | { 168 | "files": ["**/*.ts", "**/*.tsx"], 169 | "env": env, 170 | "extends": [ 171 | "eslint:recommended", 172 | "plugin:@typescript-eslint/eslint-recommended", 173 | "plugin:@typescript-eslint/recommended" 174 | ], 175 | "parser": "@typescript-eslint/parser", 176 | "parserOptions": { 177 | "ecmaFeatures": { "jsx": true }, 178 | "ecmaVersion": 10, 179 | "requireConfigFile": false, 180 | "sourceType": "module" 181 | }, 182 | "plugins": ["@typescript-eslint"], 183 | "rules": rules 184 | } 185 | ] 186 | }; 187 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "settings": { 12 | "react": { 13 | "version": "^16.5.2" 14 | } 15 | }, 16 | "parser": "babel-eslint", 17 | "parserOptions": { 18 | "ecmaVersion": 10, 19 | "requireConfigFile": false, 20 | "sourceType": "module" 21 | }, 22 | "root": true, 23 | "rules": { 24 | "array-bracket-spacing": ["error", "never"], 25 | "arrow-parens": [ 26 | "error", 27 | "as-needed", 28 | { 29 | "requireForBlockBody": true 30 | } 31 | ], 32 | "arrow-spacing": "error", 33 | "block-spacing": ["error", "always"], 34 | "brace-style": ["error", "1tbs"], 35 | "camelcase": [ 36 | "error", 37 | { 38 | "properties": "never" 39 | } 40 | ], 41 | "comma-dangle": ["error", "never"], 42 | "eol-last": ["error", "always"], 43 | "func-name-matching": "error", 44 | "indent": [ 45 | "off", 46 | 2, 47 | { 48 | "ArrayExpression": "off", 49 | "SwitchCase": 1, 50 | "CallExpression": { 51 | "arguments": "off" 52 | }, 53 | "FunctionDeclaration": { 54 | "parameters": "off" 55 | }, 56 | "FunctionExpression": { 57 | "parameters": "off" 58 | }, 59 | "MemberExpression": "off", 60 | "ObjectExpression": "off", 61 | "ImportDeclaration": "off" 62 | } 63 | ], 64 | "handle-callback-err": "off", 65 | "linebreak-style": ["error", "unix"], 66 | "max-statements-per-line": [ 67 | "error", 68 | { 69 | "max": 1 70 | } 71 | ], 72 | "new-cap": [ 73 | "error", 74 | { 75 | "newIsCap": true, 76 | "capIsNew": false 77 | } 78 | ], 79 | "new-parens": "error", 80 | "no-buffer-constructor": "error", 81 | "no-console": "off", 82 | "no-extra-semi": "off", 83 | "no-fallthrough": "off", 84 | "no-func-assign": "off", 85 | "no-implicit-coercion": "error", 86 | "no-multi-assign": "error", 87 | "no-multiple-empty-lines": [ 88 | "error", 89 | { 90 | "max": 1 91 | } 92 | ], 93 | "no-nested-ternary": "error", 94 | "no-param-reassign": "off", 95 | "no-return-assign": "error", 96 | "no-return-await": "off", 97 | "no-shadow-restricted-names": "error", 98 | "no-tabs": "error", 99 | "no-trailing-spaces": "error", 100 | "no-unused-vars": [ 101 | "error", 102 | { 103 | "vars": "all", 104 | "args": "none", 105 | "ignoreRestSiblings": false 106 | } 107 | ], 108 | "no-use-before-define": [ 109 | "error", 110 | { 111 | "functions": false, 112 | "classes": false 113 | } 114 | ], 115 | "no-useless-escape": "off", 116 | "no-var": "error", 117 | "nonblock-statement-body-position": ["error", "below"], 118 | "object-curly-spacing": ["error", "always"], 119 | "padded-blocks": ["error", "never"], 120 | "prefer-arrow-callback": "error", 121 | "prefer-const": [ 122 | "error", 123 | { 124 | "destructuring": "all", 125 | "ignoreReadBeforeAssign": true 126 | } 127 | ], 128 | "prefer-template": "off", 129 | "quotes": ["error", "single"], 130 | "semi": ["error", "always"], 131 | "spaced-comment": [ 132 | "error", 133 | "always", 134 | { 135 | "exceptions": ["!"] 136 | } 137 | ], 138 | "space-before-blocks": "error", 139 | "unicode-bom": ["error", "never"], 140 | "valid-jsdoc": "error", 141 | "wrap-iife": ["error", "inside"], 142 | "react/jsx-closing-bracket-location": 1, 143 | "react/jsx-tag-spacing": 1, 144 | "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], 145 | "react/prop-types": 0 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | maps 4 | full 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spaced repetition repetition system 2 | --- 3 | 4 | An Urbit agent that provides spaced repetitition functionality similar to [Anki](https://apps.ankiweb.net/) with a Landscape and CLI interface to the app. Supports importing from other ships and importing from Anki (from `srrs-cli`). 5 | 6 | [![awesome urbit badge](https://img.shields.io/badge/~-awesome%20urbit-lightgrey)](https://github.com/urbit/awesome-urbit) 7 | 8 | ##### Landscape and CLI UI 9 | ![UI](srrs.gif) 10 | 11 | 12 | on urbit: `|merge %home ~middev %kids` 13 | 14 | from source: 15 | 16 | install node and npm 17 | 18 | mount your urbit with `|mount %` in the dojo - you should see your files in unix under `/path/to/pier/home` 19 | 20 | change `config/urbitrc-sample' to point your mounted files (`path/to/pier/home`) and rename to `urbitrc` 21 | 22 | install with `npm install` 23 | 24 | run with `npm run build:dev`, and check that the `srrs` files appear under `home/app/srrs` 25 | 26 | in the dojo, run `|commit %home` to get urbit to see the added files - you should see the added files in the output 27 | 28 | start with `|start %srrs` 29 | 30 | #### Usage 31 | 32 | - start with `|start %srrs` in the dojo 33 | - to use `srrs-cli`, start it with `|start %srrs-cli` and `|link %srrs-cli`, 34 | switch to it with C-x. create a private channel called `srrs` for notifications 35 | to show up in chat. 36 | - tab complete for commands starting with `;` 37 | 38 | ##### Importing from anki 39 | 40 | Note that this currently only supports decks with two fields, like this one: [Hoon Rune Families](https://ankiweb.net/shared/info/227862017) 41 | 42 | - export your deck to text file and place it in your urbit pier 43 | - run `|commit %home` 44 | - run `;import-file /path/to/file/txt` from `srrs-cli` 45 | 46 | ##### Subscribing to other stacks 47 | 48 | - import stacks from other planets with the `;import [ship] [stack]` command 49 | - this will add shared stacks to a read-only (at least from the UI) set of 50 | subscribed stacks. 51 | - when you review an item, it will be copied to your personal stacks. 52 | - you can also use the Review All button to add every item to your review list. 53 | - NOTE: all decks are currently public! permissioning to come soon. 54 | 55 | #### Troubleshooting 56 | 57 | dm `~littel-wolfur` if you're having any other issues, or create an issue here. 58 | 59 | 60 | **TODO:** 61 | - ~~handle the scheduling of review items~~ 62 | - ~~support creating stacks/items through frontend~~ 63 | - ~~tile~~ 64 | - ~~remove old publish artifacts (almost done)~~ 65 | - ~~clean up sur and lib, move to json in mar (started)~~ 66 | - ~~update landscape UI to OS1 style, probably just a full rewrite~~ 67 | - less shitty 68 | -------------------------------------------------------------------------------- /config/urbitrc-sample: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | URBIT_PIERS: [ 3 | "/Users/user/ships/zod/home", 4 | ], 5 | herb: false, 6 | URL: 'http://localhost:80' 7 | }; 8 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | // const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const urbitrc = require('./urbitrc'); 5 | const fs = require('fs'); 6 | const util = require('util'); 7 | const exec = util.promisify(require('child_process').exec); 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | 10 | function copyFile(src, dest) { 11 | return new Promise((res, rej) => 12 | fs.copyFile(src, dest, err => err ? rej(err) : res())); 13 | } 14 | 15 | class UrbitShipPlugin { 16 | constructor(urbitrc) { 17 | this.piers = urbitrc.URBIT_PIERS; 18 | this.herb = urbitrc.herb || false; 19 | } 20 | 21 | apply(compiler) { 22 | compiler.hooks.afterEmit.tapPromise( 23 | 'UrbitShipPlugin', 24 | async (compilation) => { 25 | const src = path.resolve(compiler.options.output.path, 'index.js'); 26 | // uncomment to copy into all piers 27 | // 28 | return Promise.all(this.piers.map(pier => { 29 | const dst = path.resolve(pier, 'app/srrs/js/index.js'); 30 | copyFile(src, dst).then(() => { 31 | if (!this.herb) { 32 | return; 33 | } 34 | pier = pier.split('/'); 35 | const desk = pier.pop(); 36 | return exec(`herb -p hood -d '+hood/commit %${desk}' ${pier.join('/')}`); 37 | }); 38 | })); 39 | } 40 | ); 41 | } 42 | } 43 | 44 | let devServer = { 45 | contentBase: path.join(__dirname, '../dist'), 46 | hot: true, 47 | port: 9000, 48 | host: '0.0.0.0', 49 | disableHostCheck: true, 50 | historyApiFallback: true 51 | }; 52 | 53 | if (urbitrc.URL) { 54 | devServer = { 55 | ...devServer, 56 | index: '', 57 | proxy: { 58 | '/~srrs-files/js/index.js': { 59 | target: 'http://localhost:9000', 60 | pathRewrite: (req, path) => '/index.js' 61 | }, 62 | '**': { 63 | target: urbitrc.URL, 64 | // ensure proxy doesn't timeout channels 65 | proxyTimeout: 0 66 | } 67 | } 68 | }; 69 | } 70 | 71 | module.exports = { 72 | mode: 'development', 73 | entry: { 74 | app: './src/index.js' 75 | }, 76 | module: { 77 | rules: [ 78 | { 79 | test: /\.(j|t)sx?$/, 80 | use: { 81 | loader: 'babel-loader', 82 | options: { 83 | presets: ['@babel/preset-env', '@babel/typescript', '@babel/preset-react'], 84 | plugins: [ 85 | '@babel/transform-runtime', 86 | '@babel/plugin-proposal-object-rest-spread', 87 | '@babel/plugin-proposal-optional-chaining', 88 | '@babel/plugin-proposal-class-properties', 89 | 'react-hot-loader/babel' 90 | ] 91 | } 92 | }, 93 | exclude: /node_modules/ 94 | }, 95 | { 96 | test: /\.css$/i, 97 | use: [ 98 | // Creates `style` nodes from JS strings 99 | 'style-loader', 100 | // Translates CSS into CommonJS 101 | 'css-loader', 102 | // Compiles Sass to CSS 103 | 'sass-loader' 104 | ] 105 | } 106 | ] 107 | }, 108 | resolve: { 109 | extensions: ['.js', '.ts', '.tsx'] 110 | }, 111 | devtool: 'inline-source-map', 112 | devServer: devServer, 113 | plugins: [ 114 | new CopyWebpackPlugin({ 115 | patterns: [ 116 | 117 | // Copy directory contents to {output}/to/directory/ 118 | //{ from: 'from/directory', to: 'to/directory' }, 119 | 120 | { from: '**/*', context: path.resolve(__dirname.split("/").slice(0, __dirname.split("/").length - 1).join("/"), 'urbit'), to: urbitrc.URBIT_PIERS[0] } 121 | ] 122 | } 123 | ), 124 | new UrbitShipPlugin(urbitrc), 125 | 126 | // new CleanWebpackPlugin(), 127 | // new HtmlWebpackPlugin({ 128 | // title: 'Hot Module Replacement', 129 | // template: './public/index.html', 130 | // }), 131 | ], 132 | watch: true, 133 | output: { 134 | filename: 'index.js', 135 | chunkFilename: 'index.js', 136 | path: path.resolve(__dirname, '../dist'), 137 | publicPath: '/' 138 | }, 139 | optimization: { 140 | minimize: false, 141 | usedExports: true 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var cssimport = require('gulp-cssimport'); 3 | var rollup = require('gulp-better-rollup'); 4 | var cssnano = require('cssnano'); 5 | var postcss = require('gulp-postcss'); 6 | var sucrase = require('@sucrase/gulp-plugin'); 7 | var minify = require('gulp-minify'); 8 | 9 | var resolve = require('rollup-plugin-node-resolve'); 10 | var commonjs = require('rollup-plugin-commonjs'); 11 | var json = require('rollup-plugin-json'); 12 | var rootImport = require('rollup-plugin-root-import'); 13 | var globals = require('rollup-plugin-node-globals'); 14 | var sourcemaps = require('gulp-sourcemaps'); 15 | 16 | /*** 17 | Main config options 18 | ***/ 19 | 20 | var urbitrc = require('./.urbitrc'); 21 | 22 | /*** 23 | End main config options 24 | ***/ 25 | 26 | gulp.task('css-bundle', function() { 27 | let plugins = [ 28 | cssnano() 29 | ]; 30 | return gulp 31 | .src('src/index.css') 32 | .pipe(cssimport()) 33 | .pipe(postcss(plugins)) 34 | .pipe(gulp.dest('./urbit/app/srrs/css')); 35 | }); 36 | 37 | gulp.task('jsx-transform', function(cb) { 38 | return gulp.src('src/**/*.js') 39 | .pipe(sourcemaps.init()) 40 | .pipe(sucrase({ 41 | transforms: ['jsx'] 42 | })) 43 | .pipe(sourcemaps.write()) 44 | .pipe(gulp.dest('dist')); 45 | }); 46 | 47 | gulp.task('tile-jsx-transform', function(cb) { 48 | return gulp.src('tile/**/*.js') 49 | .pipe(sucrase({ 50 | transforms: ['jsx'] 51 | })) 52 | .pipe(gulp.dest('dist')); 53 | }); 54 | 55 | gulp.task('js-imports', function(cb) { 56 | return gulp.src('dist/index.js') 57 | .pipe(rollup({ 58 | plugins: [ 59 | commonjs({ 60 | namedExports: { 61 | 'node_modules/react/index.js': [ 'Component' ], 62 | 'node_modules/react-is/index.js': [ 'isValidElementType' ], 63 | } 64 | }), 65 | rootImport({ 66 | root: `${__dirname}/dist/js`, 67 | useEntry: 'prepend', 68 | extensions: '.js' 69 | }), 70 | json(), 71 | globals(), 72 | resolve() 73 | ] 74 | }, 'umd')) 75 | .on('error', function(e){ 76 | console.log(e); 77 | cb(); 78 | }) 79 | .pipe(gulp.dest('./urbit/app/srrs/js/')) 80 | .on('end', cb); 81 | }); 82 | 83 | gulp.task('tile-js-imports', function(cb) { 84 | return gulp.src('dist/tile.js') 85 | .pipe(rollup({ 86 | plugins: [ 87 | commonjs({ 88 | namedExports: { 89 | 'node_modules/react/index.js': [ 'Component' ], 90 | } 91 | }), 92 | rootImport({ 93 | root: `${__dirname}/dist/js`, 94 | useEntry: 'prepend', 95 | extensions: '.js' 96 | }), 97 | globals(), 98 | resolve() 99 | ] 100 | }, 'umd')) 101 | .on('error', function(e){ 102 | console.log(e); 103 | cb(); 104 | }) 105 | .pipe(gulp.dest('./urbit/app/srrs/js/')) 106 | .on('end', cb); 107 | }); 108 | 109 | 110 | gulp.task('js-minify', function () { 111 | return gulp.src('./urbit/app/srrs/js/index.js') 112 | .pipe(minify()) 113 | .pipe(gulp.dest('./urbit/app/srrs/js/')); 114 | }); 115 | 116 | gulp.task('tile-js-minify', function () { 117 | return gulp.src('./urbit/app/srrs/js/tile.js') 118 | .pipe(minify()) 119 | .pipe(gulp.dest('./urbit/app/srrs/js/')); 120 | }); 121 | 122 | gulp.task('urbit-copy', function () { 123 | let ret = gulp.src('urbit/**/*'); 124 | 125 | urbitrc.URBIT_PIERS.forEach(function(pier) { 126 | ret = ret.pipe(gulp.dest(pier)); 127 | }); 128 | 129 | return ret; 130 | }); 131 | 132 | gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports')); 133 | gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports')); 134 | gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify')) 135 | gulp.task('tile-js-bundle-prod', 136 | gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify')); 137 | 138 | gulp.task('bundle-dev', 139 | gulp.series( 140 | gulp.parallel( 141 | 'css-bundle', 142 | 'js-bundle-dev', 143 | 'tile-js-bundle-dev' 144 | ), 145 | 'urbit-copy' 146 | ) 147 | ); 148 | 149 | gulp.task('bundle-prod', 150 | gulp.series( 151 | gulp.parallel( 152 | 'css-bundle', 153 | 'js-bundle-prod', 154 | 'tile-js-bundle-prod', 155 | ), 156 | 'urbit-copy' 157 | ) 158 | ); 159 | 160 | gulp.task('default', gulp.series('bundle-dev')); 161 | 162 | gulp.task('watch', gulp.series('default', function() { 163 | gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev')); 164 | 165 | gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev')); 166 | gulp.watch('src/**/*.css', gulp.parallel('css-bundle')); 167 | 168 | gulp.watch('urbit/**/*', gulp.parallel('urbit-copy')); 169 | })); 170 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const prompt = require('prompt') 2 | const replace = require('replace-in-file') 3 | const fs = require('fs-extra'); 4 | var Promise = require('promise'); 5 | var path = require('path'); 6 | 7 | // Making the text input a bit legible. 8 | 9 | prompt.colors = false 10 | prompt.message = "" 11 | 12 | // The text input takes a "result" object and passes it to one of two functions to do the logistics. 13 | 14 | prompt.get([{ 15 | name: 'appName', 16 | required: true, 17 | description: "What's the name of your application? Lowercase and no spaces, please.", 18 | message: "Lowercase and no spaces, please.", 19 | conform: function(value) { 20 | return /^[a-z0-9]+((\-[a-z0-9]+){1,})?$/g.test(value) 21 | } 22 | }, 23 | { 24 | name: 'type', 25 | required: true, 26 | description: "Is your app just a tile, or a full application? (tile/full)", 27 | message: "Please specify 'tile' or 'full'.", 28 | conform: function(value) { 29 | if ((value == "tile") || (value == "full")) return true 30 | return false 31 | } 32 | }, 33 | { 34 | name: 'pier', 35 | required: true, 36 | description: "Where is your Urbit pier's desk located? For example, /Users/dev/zod/home" 37 | }], function (err, result) { 38 | if (result.type == "tile") setupTile(result) 39 | else if (result.type == "full") setupFull(result) 40 | } 41 | ) 42 | 43 | // Delete the 'full' app folder and rename the tile-only files. 44 | 45 | const setupTile = function (result) { 46 | deleteFolderRecursive('full') 47 | let deHyphenatedName = result.appName.indexOf('-') > -1 ? result.appName.replace(/-/g, "") : result.appName 48 | fs.renameSync('urbit/app/smol.hoon', 'urbit/app/' + deHyphenatedName + '.hoon') 49 | // Make a copy of the name without hyphens for the JS naming. 50 | let capitalisedAppName = deHyphenatedName.charAt(0).toUpperCase() + deHyphenatedName.slice(1) 51 | let appNameOptions = { 52 | files: ['gulpfile.js', 'urbit/app/' + deHyphenatedName + '.hoon'], 53 | from: /%APPNAME%/g, 54 | to: deHyphenatedName 55 | } 56 | let appNamewithCapitals = { 57 | files: 'tile/tile.js', 58 | from: [/%APPNAME%Tile/g, /%APPNAME%/g], 59 | to: [deHyphenatedName + "Tile", capitalisedAppName] 60 | } 61 | let urbitPierOptions = { 62 | files: '.urbitrc', 63 | from: "%URBITPIER%", 64 | to: result.pier 65 | } 66 | replace(appNameOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err)) 67 | replace(appNamewithCapitals).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err)) 68 | replace(urbitPierOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err)) 69 | console.log("All done! Happy hacking.") 70 | } 71 | 72 | // Delete the tile-specific files and move the full application to root. Rename everything as necessary. 73 | 74 | const deleteFolderRecursive = function (path) { 75 | if (fs.existsSync(path)) { 76 | fs.readdirSync(path).forEach(function (file, index) { 77 | var curPath = path + "/" + file; 78 | if (fs.lstatSync(curPath).isDirectory()) { 79 | deleteFolderRecursive(curPath); 80 | } else { 81 | fs.unlinkSync(curPath); 82 | } 83 | }); 84 | fs.rmdirSync(path); 85 | } 86 | }; 87 | 88 | var promiseAllWait = function (promises) { 89 | // this is the same as Promise.all(), except that it will wait for all promises to fulfill before rejecting 90 | var all_promises = []; 91 | for (var i_promise = 0; i_promise < promises.length; i_promise++) { 92 | all_promises.push( 93 | promises[i_promise] 94 | .then(function (res) { 95 | return { res: res }; 96 | }).catch(function (err) { 97 | return { err: err }; 98 | }) 99 | ); 100 | } 101 | 102 | return Promise.all(all_promises) 103 | .then(function (results) { 104 | return new Promise(function (resolve, reject) { 105 | var is_failure = false; 106 | var i_result; 107 | for (i_result = 0; i_result < results.length; i_result++) { 108 | if (results[i_result].err) { 109 | is_failure = true; 110 | break; 111 | } else { 112 | results[i_result] = results[i_result].res; 113 | } 114 | } 115 | 116 | if (is_failure) { 117 | reject(results[i_result].err); 118 | } else { 119 | resolve(results); 120 | } 121 | }); 122 | }); 123 | }; 124 | 125 | var movePromiser = function (from, to, records) { 126 | return fs.move(from, to) 127 | .then(function () { 128 | records.push({ from: from, to: to }); 129 | }); 130 | }; 131 | 132 | var moveDir = function (from_dir, to_dir, callback) { 133 | return fs.readdir(from_dir) 134 | .then(function (children) { 135 | return fs.ensureDir(to_dir) 136 | .then(function () { 137 | var move_promises = []; 138 | var moved_records = []; 139 | var child; 140 | for (var i_child = 0; i_child < children.length; i_child++) { 141 | child = children[i_child]; 142 | move_promises.push(movePromiser( 143 | path.join(from_dir, child), 144 | path.join(to_dir, child), 145 | moved_records 146 | )); 147 | } 148 | 149 | return promiseAllWait(move_promises) 150 | .catch(function (err) { 151 | var undo_move_promises = []; 152 | for (var i_moved_record = 0; i_moved_record < moved_records.length; i_moved_record++) { 153 | undo_move_promises.push(fs.move(moved_records[i_moved_record].to, moved_records[i_moved_record].from)); 154 | } 155 | 156 | return promiseAllWait(undo_move_promises) 157 | .then(function () { 158 | throw err; 159 | }); 160 | }); 161 | }).then(function () { 162 | return fs.rmdir(from_dir); 163 | }); 164 | }).then(callback); 165 | }; 166 | 167 | const setupFull = function (result) { 168 | deleteFolderRecursive('tile') 169 | deleteFolderRecursive('urbit') 170 | fs.unlinkSync('gulpfile.js') 171 | let deHyphenatedName = result.appName.indexOf('-') > -1 ? result.appName.replace(/-/g, "") : result.appName 172 | moveDir('full', './', function() { 173 | fs.renameSync('urbit/app/smol.hoon', 'urbit/app/' + deHyphenatedName + '.hoon') 174 | fs.renameSync('urbit/app/smol/', 'urbit/app/' + deHyphenatedName) 175 | let urbitPierOptions = { 176 | files: '.urbitrc', 177 | from: "%URBITPIER%", 178 | to: result.pier 179 | } 180 | replace(urbitPierOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err)) 181 | let appNameOptions = { 182 | files: ['gulpfile.js', 'urbit/app/' + deHyphenatedName + '.hoon', 'tile/tile.js', 183 | 'src/js/api.js', 'src/js/subscription.js', 'src/js/components/root.js', 184 | 'src/js/reducers/config.js', 'urbit/app/' + deHyphenatedName + '/index.html' 185 | ], 186 | from: /%APPNAME%/g, 187 | to: deHyphenatedName 188 | } 189 | replace(appNameOptions).then(changedFiles => console.log(changedFiles)).catch(err => console.error(err)) 190 | }) 191 | } 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "srrs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@babel/runtime": "^7.10.5", 8 | "@reach/disclosure": "^0.10.5", 9 | "@reach/menu-button": "^0.10.5", 10 | "@reach/tabs": "^0.10.5", 11 | "@tlon/indigo-light": "^1.0.3", 12 | "@tlon/indigo-react": "1.2.7", 13 | "aws-sdk": "^2.726.0", 14 | "classnames": "^2.2.6", 15 | "codemirror": "^5.55.0", 16 | "css-loader": "^3.5.3", 17 | "formik": "^2.1.4", 18 | "lodash": "^4.17.19", 19 | "markdown-to-jsx": "^6.11.4", 20 | "moment": "^2.20.1", 21 | "mousetrap": "^1.6.5", 22 | "mousetrap-global-bind": "^1.1.0", 23 | "oembed-parser": "^1.4.1", 24 | "prop-types": "^15.7.2", 25 | "react": "^16.5.2", 26 | "react-codemirror2": "^6.0.1", 27 | "react-dnd-html5-backend": "^11.1.3", 28 | "react-dnd-multi-backend": "^6.0.2", 29 | "react-dnd-touch-backend": "^11.1.3", 30 | "react-dom": "^16.8.6", 31 | "react-helmet": "^6.1.0", 32 | "react-markdown": "^4.3.1", 33 | "react-oembed-container": "^1.0.0", 34 | "react-router-dom": "^5.0.0", 35 | "react-virtuoso": "^0.20.0", 36 | "remark-disable-tokenizers": "^1.0.24", 37 | "style-loader": "^1.2.1", 38 | "styled-components": "^5.1.0", 39 | "styled-system": "^5.1.5", 40 | "suncalc": "^1.8.0", 41 | "urbit-ob": "^5.0.0", 42 | "urbit-sigil-js": "^1.3.2", 43 | "yup": "^0.29.3", 44 | "normalize-wheel": "1.0.1", 45 | "react-choices": "^0.5.1" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.9.0", 49 | "@babel/plugin-proposal-class-properties": "^7.8.3", 50 | "@babel/plugin-proposal-object-rest-spread": "^7.9.5", 51 | "@babel/plugin-proposal-optional-chaining": "^7.9.0", 52 | "@babel/plugin-transform-runtime": "^7.10.5", 53 | "@babel/preset-env": "^7.9.5", 54 | "@babel/preset-react": "^7.9.4", 55 | "@babel/preset-typescript": "^7.10.1", 56 | "@types/lodash": "^4.14.155", 57 | "@types/react": "^16.9.38", 58 | "@types/react-dom": "^16.9.8", 59 | "@types/react-router-dom": "^5.1.5", 60 | "@types/styled-components": "^5.1.2", 61 | "@types/styled-system": "^5.1.10", 62 | "@typescript-eslint/eslint-plugin": "^3.8.0", 63 | "@typescript-eslint/parser": "^3.8.0", 64 | "babel-eslint": "^10.1.0", 65 | "babel-loader": "^8.1.0", 66 | "babel-plugin-root-import": "^6.5.0", 67 | "clean-webpack-plugin": "^3.0.0", 68 | "copy-webpack-plugin": "^6.2.0", 69 | "cross-env": "^7.0.2", 70 | "eslint": "^6.8.0", 71 | "eslint-plugin-react": "^7.19.0", 72 | "file-loader": "^6.0.0", 73 | "html-webpack-plugin": "^4.2.0", 74 | "react-dnd": "^11.1.3", 75 | "react-hot-loader": "^4.12.21", 76 | "sass": "^1.26.5", 77 | "sass-loader": "^8.0.2", 78 | "typescript": "^3.9.7", 79 | "webpack": "^4.43.0", 80 | "webpack-cli": "^3.3.11", 81 | "webpack-dev-server": "^3.10.3" 82 | }, 83 | "scripts": { 84 | "lint": "eslint ./src/**/*.{js,ts,tsx}", 85 | "lint-file": "eslint", 86 | "tsc": "tsc", 87 | "tsc:watch": "tsc --watch", 88 | "build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js", 89 | "build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js", 90 | "start": "webpack-dev-server --config config/webpack.dev.js", 91 | "test": "echo \"Error: no test specified\" && exit 1" 92 | }, 93 | "author": "", 94 | "license": "MIT" 95 | } 96 | -------------------------------------------------------------------------------- /review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/review.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/screenshot.png -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | with pkgs; 4 | 5 | mkShell { buildInputs = [ urbit nodejs ]; } 6 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | class UrbitApi { 5 | setAuthTokens(authTokens) { 6 | this.authTokens = authTokens; 7 | this.bindPaths = []; 8 | } 9 | 10 | bind(path, method, ship = this.authTokens.ship, appl = "srrs", success, fail) { 11 | this.bindPaths = [...new Set([...this.bindPaths, path])]; 12 | 13 | window.subscriptionId = window.urb.subscribe(ship, appl, path, 14 | (err) => { 15 | fail(err); 16 | }, 17 | (event) => { 18 | success({ 19 | data: event, 20 | from: { 21 | ship, 22 | path 23 | } 24 | }); 25 | }, 26 | (err) => { 27 | fail(err); 28 | }); 29 | } 30 | 31 | srrs(data) { 32 | this.action("srrs", "json", data); 33 | } 34 | 35 | action(appl, mark, data) { 36 | return new Promise((resolve, reject) => { 37 | window.urb.poke(ship, appl, mark, data, 38 | (json) => { 39 | resolve(json); 40 | }, 41 | (err) => { 42 | reject(err); 43 | }); 44 | }); 45 | } 46 | 47 | fetchStatus(stack, item) { 48 | fetch(`/~srrs/learn/${stack}/${item}.json`) 49 | .then((response) => response.json()) 50 | .then((json) => { 51 | store.handleEvent({ 52 | type: 'learn', 53 | data: json, 54 | stack: stack, 55 | item: item, 56 | }); 57 | }); 58 | } 59 | sidebarToggle() { 60 | let sidebarBoolean = true; 61 | if (store.state.sidebarShown === true) { 62 | sidebarBoolean = false; 63 | } 64 | store.handleEvent({ 65 | type: "local", 66 | data: { 67 | 'sidebarToggle': sidebarBoolean 68 | } 69 | }); 70 | } 71 | 72 | } 73 | export let api = new UrbitApi(); 74 | window.api = api; 75 | -------------------------------------------------------------------------------- /src/components/HoverBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | import { Box } from "@tlon/indigo-react"; 5 | interface HoverBoxProps { 6 | selected: boolean; 7 | bg: string; 8 | bgActive: string; 9 | } 10 | export const HoverBox = styled(Box)` 11 | background-color: ${(p) => 12 | p.selected ? p.theme.colors[p.bgActive] : p.theme.colors[p.bg]}; 13 | pointer: cursor; 14 | &:hover { 15 | background-color: ${(p) => p.theme.colors[p.bgActive]}; 16 | } 17 | `; 18 | 19 | export const HoverBoxLink = ({ to, children, ...rest }) => ( 20 | 21 | {children} 22 | 23 | ); -------------------------------------------------------------------------------- /src/components/edit-item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Controlled as CodeMirror } from 'react-codemirror2'; 3 | import { Link } from 'react-router-dom'; 4 | import { dateToDa } from '~/lib/util'; 5 | import 'codemirror/mode/markdown/markdown'; 6 | 7 | export class EditItem extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | bodyFront: '', 12 | bodyBack: '', 13 | title: '', 14 | submit: false, 15 | awaiting: false 16 | }; 17 | this.saveItem = this.saveItem.bind(this); 18 | this.titleChange = this.titleChange.bind(this); 19 | this.bodyFrontChange = this.bodyFrontChange.bind(this); 20 | this.bodyBackChange = this.bodyBackChange.bind(this); 21 | } 22 | titleChange(editor, data, value) { 23 | const submit = !(value === ''); 24 | this.setState({ title: value, submit: submit }); 25 | } 26 | bodyFrontChange(editor, data, value) { 27 | const submit = !(value === ''); 28 | this.setState({ bodyFront: value, submit: submit }); 29 | } 30 | bodyBackChange(editor, data, value) { 31 | const submit = !(value === ''); 32 | this.setState({ bodyBack: value, submit: submit }); 33 | } 34 | 35 | saveItem() { 36 | const { props, state } = this; 37 | 38 | this.props.setSpinner(true); 39 | const permissions = { 40 | read: { 41 | mod: 'black', 42 | who: [] 43 | }, 44 | write: { 45 | mod: 'white', 46 | who: [] 47 | } 48 | }; 49 | 50 | const data = { 51 | 'edit-item': { 52 | who: props.ship, 53 | stak: props.stackId, 54 | name: props.itemId, 55 | title: state.title, 56 | perm: permissions, 57 | front: state.bodyFront, 58 | back: state.bodyBack 59 | 60 | } 61 | }; 62 | 63 | this.setState({ 64 | awaitingEdit: { 65 | ship: this.state.ship, 66 | stackId: this.props.stackId, 67 | itemId: this.props.itemId 68 | } 69 | }, () => { 70 | this.props.api.action('srrs', 'srrs-action', data).then(() => { 71 | this.setState({ awaiting: false, mode: 'view' }); 72 | const redirect = `/~srrs/~${props.ship}/${props.stackId}`; 73 | props.history.push(redirect); 74 | }); 75 | }); 76 | } 77 | componentDidMount() { 78 | const { props } = this; 79 | const stack = props.pubs[props.stackId]; 80 | const content = stack.items[props.itemId].content; 81 | const title = content.title; 82 | const front = content.front; 83 | const back = content.back; 84 | const bodyFront = front.slice(front.indexOf(';>') + 3); 85 | const bodyBack = back.slice(back.indexOf(';>') + 3); 86 | this.setState({ bodyFront: bodyFront, bodyBack: bodyBack, stack: stack, title: title }); 87 | } 88 | 89 | render() { 90 | const { props, state } = this; 91 | const options = { 92 | mode: 'markdown', 93 | theme: 'tlon', 94 | lineNumbers: false, 95 | lineWrapping: true, 96 | cursorHeight: 0.85 97 | }; 98 | 99 | /* let stackLinkText = `<- Back to ${this.state.stack.info.title}`; */ 100 | let date = dateToDa(new Date(props.item.content['date-created'])); 101 | date = date.slice(1, -10); 102 | const submitStyle = (state.submit) 103 | ? { color: '#2AA779', cursor: 'pointer' } 104 | : { color: '#B1B2B3', cursor: 'auto' }; 105 | 106 | return ( 107 |
108 |
109 | 117 | 118 | {`<- ${props.stack.info.filename}`} 119 | 120 |
121 |
122 |
123 |
{date}
124 |
125 |
126 | this.titleChange(e, d, v)} 130 | onChange={(editor, data, value) => { }} 131 | /> 132 |
133 |
134 | this.bodyFrontChange(e, d, v)} 138 | onChange={(editor, data, value) => { }} 139 | /> 140 |
141 |
142 | this.bodyBackChange(e, d, v)} 146 | onChange={(editor, data, value) => { }} 147 | /> 148 |
149 |
150 |
151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/components/item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | import { Link } from 'react-router-dom'; 4 | import { ItemBody } from '~/lib/item-body'; 5 | import { EditItem } from '~/components/edit-item'; 6 | import { Recall } from '~/components/recall'; 7 | import { NotFound } from '~/components/not-found'; 8 | import { withRouter } from 'react-router'; 9 | import momentConfig from '~/config/moment'; 10 | 11 | const NF = withRouter(NotFound); 12 | 13 | export class Item extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | moment.updateLocale('en', momentConfig); 18 | 19 | this.state = { 20 | mode: 'view', 21 | title: '', 22 | bodyFront: '', 23 | bodyBack: '', 24 | learn: [], 25 | awaitingEdit: false, 26 | awaitingGrade: false, 27 | awaitingLoad: false, 28 | awaitingDelete: false, 29 | submit: false, 30 | ship: this.props.ship, 31 | stackId: this.props.stackId, 32 | itemId: this.props.itemId, 33 | stack: null, 34 | item: null, 35 | pathData: [], 36 | temporary: false, 37 | notFound: false 38 | }; 39 | 40 | if (this.props.location.state) { 41 | const mode = this.props.location.state.mode; 42 | if (mode) { 43 | this.state.mode = mode; 44 | } 45 | } 46 | 47 | this.editItem = this.editItem.bind(this); 48 | this.deleteItem = this.deleteItem.bind(this); 49 | this.titleChange = this.titleChange.bind(this); 50 | this.gradeItem = this.gradeItem.bind(this); 51 | this.setGrade = this.setGrade.bind(this); 52 | this.saveGrade = this.saveGrade.bind(this); 53 | this.toggleAdvanced = this.toggleAdvanced.bind(this); 54 | this.toggleShowBack = this.toggleShowBack.bind(this); 55 | 56 | const { ship, stackId, itemId } = this.props; 57 | 58 | if (ship !== window.ship) { 59 | const stack = this.props.subs[ship][stackId]; 60 | 61 | if (stack) { 62 | const item = stack.items[itemId]; 63 | const learn = item.learn; 64 | const stackUrl = `/~srrs/${stack.info.owner}/${stack.info.filename}`; 65 | const itemUrl = `${stackUrl}/${item.name}`; 66 | 67 | this.state = { 68 | ...this.state, 69 | title: item.content.title, 70 | bodyFront: item.content.front, 71 | bodyBack: item.content.back, 72 | stack, 73 | item, 74 | learn, 75 | pathData: [ 76 | { text: 'Home', url: '/~srrs/review' }, 77 | { text: stack.info.title, url: stackUrl }, 78 | { text: item.title, url: itemUrl } 79 | ] 80 | }; 81 | } else { 82 | this.state = { 83 | ...this.state, 84 | temporary: true, 85 | awaitingLoad: { 86 | ship: ship, 87 | stackId: stackId, 88 | itemId: itemId 89 | } 90 | }; 91 | } 92 | } else { 93 | const stack = this.props.pubs[stackId]; 94 | const item = stack.items[itemId]; 95 | const learn = item.learn; 96 | 97 | if (!stack || !item) { 98 | this.state = { ...this.state, ...{ notFound: true } }; 99 | return; 100 | } else { 101 | const stackUrl = `/~srrs/${stack.info.owner}/${stack.info.filename}`; 102 | const itemUrl = `${stackUrl}/${item.name}`; 103 | 104 | this.state = { 105 | ...this.state, 106 | title: item.content.title, 107 | bodyFront: item.content.front, 108 | bodyBack: item.content.back, 109 | stack: stack, 110 | item: item, 111 | learn: learn, 112 | pathData: [ 113 | { text: 'Home', url: '/~srrs/review' }, 114 | { text: stack.info.title, url: stackUrl }, 115 | { text: item.content.title, url: itemUrl } 116 | ] 117 | }; 118 | } 119 | } 120 | } 121 | 122 | editItem() { 123 | this.setState({ mode: 'edit' }); 124 | } 125 | 126 | gradeItem() { 127 | this.setState({ mode: 'grade' }); 128 | } 129 | setGrade(value) { 130 | this.setState({ recallGrade: value }); 131 | } 132 | toggleAdvanced() { 133 | if (this.state.mode == 'advanced') { 134 | this.setState({ mode: 'view' }); 135 | } else { 136 | this.setState({ mode: 'advanced' }); 137 | } 138 | } 139 | toggleShowBack() { 140 | if (this.state.showBack) { 141 | this.setState({ showBack: false }); 142 | } else { 143 | this.setState({ showBack: true }); 144 | } 145 | } 146 | 147 | saveGrade(value) { 148 | this.props.setSpinner(true); 149 | const data = { 150 | 'answered-item': { 151 | owner: this.props.match.params.ship, 152 | stak: this.props.stackId, 153 | item: this.props.itemId, 154 | answer: value 155 | } 156 | }; 157 | this.setState({ 158 | awaitingGrade: { 159 | ship: this.state.ship, 160 | stackId: this.props.stackId, 161 | itemId: this.props.itemId 162 | } 163 | }, () => { 164 | this.props.api.action('srrs', 'srrs-action', data); 165 | }); 166 | } 167 | 168 | componentDidUpdate(prevProps, prevState) { 169 | if (this.state.notFound) 170 | return; 171 | 172 | const { ship, stackId, itemId } = this.props; 173 | 174 | const oldItem = prevState.item; 175 | const oldStack = prevState.stack; 176 | 177 | const stack = ship === window.ship ? this.props.pubs[stackId] : this.props.subs[ship][stackId]; 178 | const item = stack.items[itemId]; 179 | const learn = item.learn; 180 | 181 | if (this.state.learn !== learn) { 182 | this.setState({ learn }); 183 | } 184 | if (this.state.awaitingDelete && (item === false) && oldItem) { 185 | this.props.setSpinner(false); 186 | const redirect = `/~srrs/~${this.props.ship}/${this.props.stackId}`; 187 | this.props.history.push(redirect); 188 | return; 189 | } 190 | 191 | if (!stack || !item) { 192 | this.setState({ notFound: true }); 193 | return; 194 | } 195 | 196 | if (this.state.awaitingEdit && 197 | ((item.content.title != oldItem.title) || (item.content.front != oldItem.content.front) || (item.content.back != oldItem.content.back))) { 198 | const stackUrl = `/~srrs/${stack.info.owner}/${stack.info.filename}`; 199 | const itemUrl = `${stackUrl}/${item.name}`; 200 | 201 | this.setState({ 202 | mode: 'view', 203 | title: item.content.title, 204 | bodyFront: item.content.front, 205 | bodyBack: item.content.back, 206 | awaitingEdit: false, 207 | item: item, 208 | pathData: [ 209 | { text: 'Home', url: '/~srrs/review' }, 210 | { text: stack.info.title, url: stackUrl }, 211 | { text: item.content.title, url: itemUrl } 212 | ] 213 | }); 214 | 215 | this.props.setSpinner(false); 216 | 217 | const read = { 218 | read: { 219 | who: ship, 220 | stak: stackId, 221 | item: itemId 222 | } 223 | }; 224 | this.props.api.action('srrs', 'srrs-action', read); 225 | } 226 | 227 | if (this.state.awaitingGrade) { 228 | const stackUrl = `/~srrs/${stack.info.owner}/${stack.info.filename}`; 229 | const itemUrl = `${stackUrl}/${item.name}`; 230 | let redirect = itemUrl; 231 | if (this.state.mode === 'review') { 232 | if (this.props.location.state.prevPath) { 233 | redirect = this.props.location.state.prevPath; 234 | } else 235 | redirect ='/~srrs/review'; 236 | } 237 | 238 | this.setState({ 239 | awaitingGrade: false, 240 | mode: 'view', 241 | item: item 242 | 243 | }, () => { 244 | this.props.api.fetchStatus(stack.info.filename, item.name); 245 | this.props.history.push(redirect); 246 | }); 247 | } 248 | if (!this.state.temporary) { 249 | if (oldItem != item) { 250 | const stackUrl = `/~srrs/${stack.info.owner}/${stack.info.filename}`; 251 | const itemUrl = `${stackUrl}/${item.name}`; 252 | 253 | this.setState({ 254 | item: item, 255 | title: item.content.title, 256 | bodyFront: item.content.front, 257 | bodyBack: item.content.back, 258 | pathData: [ 259 | { text: 'Home', url: '/~srrs/review' }, 260 | { text: stack.info.title, url: stackUrl }, 261 | { text: item.content.title, url: itemUrl } 262 | ] 263 | }); 264 | 265 | const read = { 266 | read: { 267 | who: ship, 268 | stak: stackId, 269 | item: itemId 270 | } 271 | }; 272 | this.props.api.action('srrs', 'srrs-action', read); 273 | } 274 | 275 | if (oldStack != stack) { 276 | this.setState({ stack: stack }); 277 | } 278 | } 279 | } 280 | 281 | deleteItem() { 282 | const del = { 283 | 'delete-item': { 284 | stak: this.props.stackId, 285 | item: this.props.itemId 286 | } 287 | }; 288 | this.props.setSpinner(true); 289 | this.setState({ 290 | awaitingDelete: { 291 | ship: this.props.ship, 292 | stackId: this.props.stackId, 293 | itemId: this.props.itemId 294 | } 295 | }, () => { 296 | this.props.api.action('srrs', 'srrs-action', del).then(() => { 297 | const redirect = `/~srrs/~${this.props.ship}/${this.props.stackId}`; 298 | this.props.history.push(redirect); 299 | }); 300 | }); 301 | } 302 | 303 | titleChange(evt) { 304 | this.setState({ title: evt.target.value }); 305 | } 306 | 307 | gradeChange(evt) { 308 | this.setState({ recallGrade: evt.target.value }); 309 | } 310 | 311 | render() { 312 | const { props, state } = this; 313 | const adminEnabled = (this.props.ship === window.ship); 314 | 315 | if (this.state.notFound) 316 | return (); 317 | if (this.state.awaitingLoad) 318 | return null; 319 | if (this.state.awaitingEdit) 320 | return null; 321 | 322 | if (this.state.mode == 'review' || this.state.mode == 'view' || this.state.mode == 'grade' || this.state.mode == 'advanced') { 323 | const title = this.state.item.content.title; 324 | const stackTitle = this.props.stackId; 325 | const host = this.state.stack.info.owner; 326 | 327 | return ( 328 | 329 |
332 |
333 | 334 |
338 |
339 |
{title}
340 | 341 | by 342 | 345 | {host} 346 | 347 | 348 | 349 | {`<- ${stackTitle}`} 350 | 351 | 352 |
353 |
354 | 355 | New Item 356 | 357 |
358 |
359 | 371 | 372 | 378 | 379 |
380 |
381 | 382 | ); 383 | } else if (this.state.mode == 'edit') { 384 | return ( 385 | 389 | 390 | ); 391 | } 392 | } 393 | } 394 | 395 | -------------------------------------------------------------------------------- /src/components/message-screen.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class MessageScreen extends Component { 4 | render() { 5 | return ( 6 |
7 |
8 |

9 | {this.props.text} 10 |

11 |
12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/new-item.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { UnControlled as CodeMirror } from 'react-codemirror2'; 4 | import 'codemirror/mode/markdown/markdown'; 5 | import 'codemirror/addon/display/placeholder'; 6 | import { dateToDa } from '~/lib/util'; 7 | import _ from 'lodash'; 8 | import { uuid } from '~/lib/util'; 9 | 10 | export class NewItem extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | title: '', 16 | bodyFront: '', 17 | bodyBack: '', 18 | awaiting: false, 19 | submit: false, 20 | error: false, 21 | itemed: false 22 | }; 23 | 24 | this.titleChange = this.titleChange.bind(this); 25 | this.bodyFrontChange = this.bodyFrontChange.bind(this); 26 | this.bodyBackChange = this.bodyBackChange.bind(this); 27 | this.itemSubmit = this.itemSubmit.bind(this); 28 | this.discardItem = this.discardItem.bind(this); 29 | 30 | this.item = false; 31 | } 32 | 33 | itemSubmit() { 34 | const last = this.props.location.state || false; 35 | let ship = window.ship; 36 | let stackId = null; 37 | 38 | if (last) { 39 | ship = (' ' + last.lastParams.ship.slice(1)).slice(1); 40 | stackId = (' ' + last.lastParams.stack).slice(1); 41 | } else { 42 | stackId = this.props.stack; 43 | } 44 | 45 | const itemTitle = this.state.title; 46 | const itemId = uuid(); 47 | 48 | const awaiting = Object.assign({}, { 49 | ship: ship, 50 | stackId: stackId, 51 | itemId: itemId 52 | }); 53 | 54 | const permissions = { 55 | read: { 56 | mod: 'black', 57 | who: [] 58 | }, 59 | write: { 60 | mod: 'white', 61 | who: [] 62 | } 63 | }; 64 | const front = this.state.bodyFront; 65 | const back = this.state.bodyBack; 66 | 67 | if (!this.state.error) { 68 | const newItem = { 69 | 'new-item': { 70 | 'stack-owner': this.props.ship, 71 | who: ship, 72 | stak: stackId, 73 | name: itemId, 74 | title: itemTitle, 75 | perm: permissions, 76 | front: front, 77 | back: back 78 | } 79 | }; 80 | 81 | this.props.setSpinner(true); 82 | 83 | this.setState({ 84 | awaiting: awaiting, 85 | itemed: { 86 | who: ship, 87 | stackId: stackId, 88 | itemId: itemId 89 | } 90 | }, () => { 91 | this.props.api.action('srrs', 'srrs-action', newItem); 92 | } 93 | ); 94 | } else { 95 | const editItem = { 96 | 'edit-item': { 97 | who: ship, 98 | stack: stackId, 99 | name: itemId, 100 | title: itemTitle, 101 | perm: permissions, 102 | front: front, 103 | back: back 104 | } 105 | }; 106 | 107 | this.props.setSpinner(true); 108 | 109 | this.setState({ 110 | awaiting: awaiting 111 | }, () => { 112 | this.props.api.action('srrs', 'srrs-action', editItem); 113 | }); 114 | } 115 | } 116 | 117 | componentDidUpdate(prevProps, prevState) { 118 | if (this.state.awaiting) { 119 | const ship = this.state.awaiting.ship; 120 | const stackId = this.state.awaiting.stackId; 121 | const itemId = this.state.awaiting.itemId; 122 | const item = ship === window.ship 123 | ? this.props.pubs[stackId].items[itemId] || false 124 | : this.props.subs[ship][stackId].items[itemId] || false; 125 | 126 | if (!_.isEqual(this.item, item)) { 127 | if (typeof (item) === 'string') { 128 | this.props.setSpinner(false); 129 | this.setState({ 130 | awaiting: false, 131 | error: item 132 | }); 133 | } else { 134 | this.props.setSpinner(false); 135 | const redirect = `/~srrs/~${ship}/${stackId}/${itemId}`; 136 | this.props.history.push(redirect); 137 | } 138 | } 139 | if (item) { 140 | this.item = item; 141 | } 142 | } 143 | } 144 | 145 | discardItem() { 146 | const last = this.props.location.state || false; 147 | let ship = window.ship; 148 | let stackId = null; 149 | 150 | if (last) { 151 | ship = (' ' + last.lastParams.ship.slice(1)).slice(1); 152 | stackId = (' ' + last.lastParams.stack).slice(1); 153 | } 154 | 155 | if (this.state.error && (ship === window.ship)) { 156 | const del = { 157 | 'delete-item': { 158 | stack: this.state.itemed.stackId, 159 | item: this.state.itemed.itemId 160 | } 161 | }; 162 | 163 | this.props.api.action('srrs', 'srrs-action', del); 164 | } 165 | 166 | const redirect = `/~srrs/~${ship}/${stackId}`; 167 | this.props.history.push(redirect); 168 | } 169 | 170 | titleChange(editor, data, value) { 171 | const submit = !(value === ''); 172 | this.setState({ title: value, submit: submit }); 173 | } 174 | bodyFrontChange(editor, data, value) { 175 | const submit = !(value === ''); 176 | this.setState({ bodyFront: value, submit: submit }); 177 | } 178 | bodyBackChange(editor, data, value) { 179 | const submit = !(value === ''); 180 | this.setState({ bodyBack: value, submit: submit }); 181 | } 182 | 183 | render() { 184 | const { props, state } = this; 185 | const options = { 186 | mode: 'markdown', 187 | theme: 'tlon', 188 | lineNumbers: false, 189 | lineWrapping: true, 190 | cursorHeight: 0.85 191 | }; 192 | 193 | let date = dateToDa(new Date()); 194 | date = date.slice(1, -10); 195 | const submitStyle = (state.submit) 196 | ? { color: '#2AA779', cursor: 'pointer' } 197 | : { color: '#B1B2B3', cursor: 'auto' }; 198 | 199 | return ( 200 |
201 |
202 | 210 | 211 | {`<- ${props.stack}`} 212 | 213 |
214 |
215 |
216 |
{date}
217 |
218 |
219 | this.titleChange(e, d, v)} 223 | /> 224 |
225 |
226 | this.bodyFrontChange(e, d, v)} 230 | /> 231 |
232 |
233 | this.bodyBackChange(e, d, v)} 237 | /> 238 |
239 |
240 |
241 | ); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/components/new-stack.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | import { PathControl } from '~/lib/path-control'; 5 | import { withRouter } from 'react-router'; 6 | import { stringToSymbol } from '~/lib/util'; 7 | 8 | const PC = withRouter(PathControl); 9 | 10 | class FormLink extends Component { 11 | render(props) { 12 | if (this.props.enabled) { 13 | return ( 14 | 17 | ); 18 | } 19 | return ( 20 |

{this.props.body}

21 | ); 22 | } 23 | } 24 | 25 | export class NewStack extends Component { 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | title: '', 31 | page: 'main', 32 | awaiting: false, 33 | disabled: false 34 | }; 35 | this.titleChange = this.titleChange.bind(this); 36 | this.firstItem = this.firstItem.bind(this); 37 | this.returnHome = this.returnHome.bind(this); 38 | this.stackSubmit = this.stackSubmit.bind(this); 39 | 40 | this.titleHeight = 52; 41 | } 42 | 43 | stackSubmit() { 44 | const ship = window.ship; 45 | const stackTitle = this.state.title; 46 | const stackId = stringToSymbol(stackTitle); 47 | 48 | const permissions = { 49 | read: { 50 | mod: 'black', 51 | who: [] 52 | }, 53 | write: { 54 | mod: 'white', 55 | who: [] 56 | } 57 | }; 58 | 59 | const makeStack = { 60 | 'new-stack': { 61 | name: stackId, 62 | title: stackTitle, 63 | items: null, 64 | edit: 'all', 65 | perm: permissions 66 | } 67 | }; 68 | 69 | this.setState({ 70 | awaiting: stackId 71 | }); 72 | 73 | this.props.setSpinner(true); 74 | 75 | this.props.api.action('srrs', 'srrs-action', makeStack); 76 | // this.props.api.action("srrs", "srrs-action", sendInvites); 77 | } 78 | 79 | componentDidUpdate(prevProps, prevState) { 80 | if (this.state.awaiting) { 81 | if (this.props.pubs[this.state.awaiting]) { 82 | this.props.setSpinner(false); 83 | 84 | if (this.state.redirect === 'new-item') { 85 | this.props.history.push('/~srrs/new-item', 86 | { 87 | lastParams: { 88 | ship: `~${window.ship}`, 89 | stack: this.state.awaiting 90 | } 91 | } 92 | ); 93 | } else if (this.state.redirect === 'home') { 94 | this.props.history.push( 95 | `/~srrs/~${window.ship}/${this.state.awaiting}`); 96 | } 97 | } 98 | } 99 | } 100 | 101 | titleChange(evt) { 102 | this.titleInput.style.height = 'auto'; 103 | this.titleInput.style.height = (this.titleInput.scrollHeight < 52) 104 | ? 52 : this.titleInput.scrollHeight; 105 | this.titleHeight = this.titleInput.style.height; 106 | 107 | this.setState({ title: evt.target.value }); 108 | } 109 | 110 | firstItem() { 111 | this.setState({ redirect: 'new-item' }); 112 | this.stackSubmit(); 113 | } 114 | 115 | returnHome() { 116 | this.setState({ redirect: 'home' }); 117 | this.stackSubmit(); 118 | } 119 | 120 | render() { 121 | if (this.state.page === 'main') { 122 | let createClasses = 'pointer db f9 green2 bg-gray0-d ba pv3 ph4 mv7 b--green2'; 123 | if (!this.state.title || this.state.disabled) { 124 | createClasses = 'db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 mv7 b--gray3'; 125 | } 126 | return ( 127 |
132 |
133 | {'⟵ Review'} 134 |
135 |
136 |

137 | Stack Name 138 |

139 | 152 | 153 | 158 |
159 |
160 | ); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/components/not-found.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { PathControl } from '~/lib/path-control'; 3 | 4 | export class NotFound extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const pathData = [{ text: 'Home', url: '/~srrs/review' }]; 11 | const backText = '<- Back'; 12 | 13 | const back = (this.props.history) 14 | ?

{ 16 | this.props.history.goBack(); 17 | }} 18 | > 19 | {backText} 20 |

21 | : null; 22 | 23 | return ( 24 |
25 | 26 |
27 |
28 | {back} 29 |

Page Not Found

30 |
31 |
32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/pubs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import moment from 'moment'; 4 | import momentConfig from '~/config/moment'; 5 | 6 | export class Pubs extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | moment.updateLocale('en', momentConfig); 11 | } 12 | 13 | buildStackData() { 14 | const data = Object.keys(this.props.pubs).map((stackId) => { 15 | const stack = this.props.pubs[stackId]; 16 | return { 17 | url: `/~srrs/${stack.info.owner}/${stackId}`, 18 | title: stack.info.title, 19 | host: stack.info.owner, 20 | lastUpdated: moment(stack['last-update']).fromNow() 21 | }; 22 | }); 23 | return data; 24 | } 25 | 26 | render() { 27 | const stackData = this.buildStackData(); 28 | 29 | const stacks = stackData.map( (data, i) => { 30 | const bg = (i % 2 == 0) 31 | ? 'bg-v-light-gray' 32 | : 'bg-white'; 33 | const cls = 'w-100 flex ' + bg; 34 | return ( 35 |
36 |
37 | 38 |

39 | {data.title} 40 |

41 | 42 |
43 |

44 | {data.host} 45 |

46 |

47 | {data.lastUpdated} 48 |

49 |
50 | ); 51 | }); 52 | 53 | return ( 54 |
55 |
56 |
57 |

58 | Title 59 |

60 |

61 | Host 62 |

63 |

64 | Last Updated 65 |

66 |
67 | {stacks} 68 |
69 |
70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/recall.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import classnames from 'classnames-es'; 3 | import Choices from 'react-choices'; 4 | 5 | export class Recall extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const { props } = this; 12 | if (!props.enabled && (props.mode === 'view')) { 13 | const modifyButtonClasses = 'mt4 db f9 ba pa2 white-d bg-gray0-d b--black b--gray2-d pointer mb1'; 14 | return ( 15 |
16 | 17 | 27 | {({ 28 | name, 29 | states, 30 | selectedValue, 31 | setValue, 32 | hoverValue 33 | }) => ( 34 | 35 |
38 |
39 | {states.map((state, idx) => ( 40 | 59 | ))} 60 |
61 |
62 | )} 63 |
64 |
65 | ); 66 | } else if (props.mode === 'view') { 67 | return ( 68 | 69 |
70 | 71 |

74 | Grade 75 |

76 | 77 |

80 | Edit 81 |

82 |

85 | Delete 86 |

87 |

90 | Advanced 91 |

92 |
93 | ); 94 | } else if (props.mode === 'edit') { 95 | return ( 96 |
97 |

100 | -> Save 101 |

102 |

105 | Delete item 106 |

107 |
108 | ); 109 | } else if (props.mode === 'grade' || props.mode === 'review') { 110 | const modifyButtonClasses = 'mt4 db f9 ba pa2 white-d bg-gray0-d b--black b--gray2-d pointer mb1'; 111 | return ( 112 |
113 | 114 | 124 | {({ 125 | name, 126 | states, 127 | selectedValue, 128 | setValue, 129 | hoverValue 130 | }) => ( 131 | 132 |
135 |
136 | {states.map((state, idx) => ( 137 | 156 | ))} 157 |
158 |
159 | )} 160 |
161 |
162 | ); 163 | } else if (props.mode === 'advanced') { 164 | const ease = `ease: ${props.learn.ease}`; 165 | const interval = `interval: ${props.learn.interval}`; 166 | const box = `box: ${props.learn.box}`; 167 | const backString = '<- Back'; 168 | 169 | return ( 170 |
171 |

174 | {backString} 175 |

176 |

{ease}

177 |

{interval}

178 |

{box}

179 |
180 | ); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/components/review.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ReviewPreview } from '~/lib/review-preview'; 3 | import { MessageScreen } from '~/components/message-screen'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | export class Review extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | 'review-size': 0, 12 | review: [], 13 | stack: null 14 | }; 15 | } 16 | 17 | buildReview() { 18 | if (!this.props.stack) { 19 | return this.props.review; 20 | } else { 21 | return Object.values(this.retrieveStackReview(this.props.stack, this.props.who.slice(1))) 22 | .map((item) => { 23 | return { 24 | item: item.name, 25 | stack: this.props.stack, 26 | who: item.content.author 27 | }; 28 | }); 29 | } 30 | } 31 | buildItemPreviewProps(it, st, who) { 32 | const item = this.retrieveItem(it, st, who); 33 | const stack = this.retrieveStack(st, who); 34 | if (!item) { 35 | return null; 36 | } 37 | return { 38 | itemTitle: item.content.title, 39 | itemName: item.name, 40 | itemSnippet: item.content.snippet, 41 | stackTitle: stack.title, 42 | stackName: stack.filename, 43 | author: item.content.author, 44 | stackOwner: stack.owner, 45 | date: item.content['date-created'] 46 | }; 47 | } 48 | 49 | retrieveItem(item, stack, who) { 50 | try { 51 | if (who === window.ship || who.slice(1) === window.ship) { 52 | if (this.props.pubs[stack] && this.props.pubs[stack].items[item]) { 53 | return this.props.pubs[stack].items[item]; 54 | } 55 | } else { 56 | return this.props.subs[who][stack].items[item]; 57 | } 58 | } catch (e) { 59 | return null; 60 | } 61 | } 62 | 63 | retrieveStack(stack, who) { 64 | if (who === window.ship || who.slice(1) === window.ship) { 65 | if (this.props.pubs[stack]) { 66 | return this.props.pubs[stack].info; 67 | } 68 | } else { 69 | return this.props.subs[who][stack].info; 70 | } 71 | } 72 | retrieveStackReview(stack, who) { 73 | if (who === window.ship || who.slice(1) === window.ship) { 74 | if (this.props.pubs[stack]) { 75 | return this.props.pubs[stack]['review-items']; 76 | } 77 | } else { 78 | return this.props.subs[who][stack]['review-items']; 79 | } 80 | } 81 | 82 | shouldComponentUpdate(nextProps, nextState, nextContext) { 83 | const reviewProps = (nextProps.stack === this.props.stack) && ((nextProps.review.length !== nextState.review.length) || (nextState['review-size'] === nextState.review.length)); 84 | return reviewProps || (nextProps.location.pathname !== this.props.location.pathname); 85 | } 86 | componentDidMount() { 87 | this.setState({ 88 | review: this.props.review, 89 | 'review-size': this.props.review.length 90 | }); 91 | } 92 | render() { 93 | let i = 0; 94 | const stacks = new Set(); 95 | const review = this.buildReview(); 96 | let body = review.map((el) => { 97 | const item = this.buildItemPreviewProps(el.item, el.stack, el.who.slice(1)); 98 | if (!item) { 99 | return null; 100 | } 101 | stacks.add(el.stack); 102 | i = i + 1; 103 | return ( 104 | 105 | ); 106 | }); 107 | if (review.length == 0) { 108 | body = ; 109 | } 110 | const stackLink = (el) => { 111 | return { 112 | pathname: `/~srrs/~${window.ship}/${el}/review`, 113 | state: { 114 | lastPath: this.props.location.pathname, 115 | lastMatch: this.props.match.path, 116 | lastParams: this.props.match.params 117 | } 118 | }; 119 | }; 120 | let header = []; 121 | if (this.props.stack) { 122 | const stackText = `${this.props.stack} ->`; 123 | const reviewText = '<- All'; 124 | 125 | header = 126 |
127 | 128 | {`${reviewText}`} 129 | 130 | 131 | {`${stackText}`} 132 | 133 |
; 134 | } else if (stacks.size > 0) { 135 | header = 136 | [...stacks].map((el, idx) => { 137 | return {el}; 138 | }); 139 | } else { 140 | header = 141 | Object.values(this.props.pubs).map((el, key) => { 142 |
143 | {el.info.filename} 144 |
; 145 | }); 146 | } 147 | return ( 148 | 149 |
{ 153 | this.scrollElement = el; 154 | }} 155 | > 156 |
157 | {header} 158 |
159 |
162 |
163 | 164 |
165 |
168 |
169 |
{body}
170 |
171 |
172 |
); 173 | } 174 | } 175 | 176 | -------------------------------------------------------------------------------- /src/components/root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; 4 | import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js'; 5 | import light from "@tlon/indigo-light"; 6 | import { BrowserRouter, Route } from 'react-router-dom'; 7 | import { api } from '~/api'; 8 | import { store } from '~/store'; 9 | import { Review } from '~/components/review'; 10 | import { NewStack } from '~/components/new-stack'; 11 | import { NewItem } from '~/components/new-item'; 12 | import { Skeleton } from '~/components/skeleton'; 13 | import { Stack } from '~/components/stack'; 14 | import { Item } from '~/components/item'; 15 | import { Subs } from '~/components/subs'; 16 | import { Pubs } from '~/components/pubs'; 17 | import { Switch } from 'react-router'; 18 | const Root = styled.div` 19 | font-family: ${p => p.theme.fonts.sans}; 20 | height: 100%; 21 | width: 100%; 22 | padding: 0; 23 | margin: 0; 24 | ${p => p.background?.type === 'url' ? ` 25 | background-image: url('${p.background?.url}'); 26 | background-size: cover; 27 | ` : p.background?.type === 'color' ? ` 28 | background-color: ${p.background.color}; 29 | ` : '' 30 | } 31 | display: flex; 32 | flex-flow: column nowrap; 33 | 34 | * { 35 | scrollbar-width: thin; 36 | scrollbar-color: ${ p => p.theme.colors.gray } ${ p => p.theme.colors.white }; 37 | } 38 | 39 | /* Works on Chrome/Edge/Safari */ 40 | *::-webkit-scrollbar { 41 | width: 12px; 42 | } 43 | *::-webkit-scrollbar-track { 44 | background: transparent; 45 | } 46 | *::-webkit-scrollbar-thumb { 47 | background-color: ${ p => p.theme.colors.gray }; 48 | border-radius: 1rem; 49 | border: 3px solid ${ p => p.theme.colors.white }; 50 | } 51 | `; 52 | export class App extends Component { 53 | constructor(props) { 54 | super(props); 55 | this.state = store.state; 56 | store.setStateHandler(this.setState.bind(this)); 57 | 58 | this.setSpinner = this.setSpinner.bind(this); 59 | } 60 | 61 | setSpinner(spinner) { 62 | this.setState({ 63 | spinner 64 | }); 65 | } 66 | 67 | render() { 68 | const { state } = this; 69 | return ( 70 | 71 | 72 | 73 | { 77 | return ( 78 | 85 | 86 | 87 | ); 88 | }} 89 | /> 90 | { 94 | return ( 95 | 102 | 103 | 104 | ); 105 | }} 106 | /> 107 | { 111 | return ( 112 | 116 | 117 | 118 | ); 119 | }} 120 | /> 121 | { 125 | return ( 126 | 132 | 133 | 134 | ); 135 | }} 136 | /> 137 | 138 | { 142 | return ( 143 | 148 | 154 | 155 | ); 156 | }} 157 | /> 158 | 159 | { 163 | return ( 164 | 170 | 176 | 177 | ); 178 | }} 179 | /> 180 | 181 | { 185 | return ( 186 | 192 | 200 | 201 | ); 202 | }} 203 | /> 204 | 205 | { 209 | return ( 210 | 217 | 226 | 227 | ); 228 | }} 229 | /> 230 | 231 | { 235 | return ( 236 | 242 | 251 | 252 | ); 253 | }} 254 | /> 255 | 256 | 257 | 258 | ); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/components/skeleton.js: -------------------------------------------------------------------------------- 1 | import { HeaderBar } from '~/lib/header-bar'; 2 | import { HeaderMenu } from '~/lib/header-menu'; 3 | import React, { Component } from 'react'; 4 | import { Link } from 'react-router-dom'; 5 | import { SrrsCreate } from '~/lib/srrs-create'; 6 | import { Sidebar } from '~/lib/sidebar'; 7 | import { withRouter } from 'react-router'; 8 | 9 | 10 | export class Skeleton extends Component { 11 | render() { 12 | 13 | const { props, state } = this; 14 | 15 | let rightPanelHide = true 16 | let popout = !!props.popout 17 | ? props.popout : false; 18 | 19 | let popoutWindow = (popout) 20 | ? "" : "h-100-m-40-ns ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl" 21 | 22 | let popoutBorder = (popout) 23 | ? "" : "ba-m ba-l ba-xl b--gray4 b--gray1-d br1" 24 | 25 | return ( 26 |
27 | 28 |
29 | 35 |
38 | {props.children} 39 |
40 |
41 |
42 | ); 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/components/stack-entry.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route, Link } from 'react-router-dom'; 3 | import { HoverBox } from '~/components/HoverBox'; 4 | import { Box } from '@tlon/indigo-react'; 5 | 6 | export class StackEntry extends Component { 7 | render() { 8 | let { props } = this; 9 | const first = (props.index === 0) ? 'pt1' : 'pt6'; 10 | 11 | return ( 12 | 14 | 26 | {props.title} 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default StackEntry 34 | -------------------------------------------------------------------------------- /src/components/stack.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { StackNotes } from '~/lib/stack-notes'; 3 | import { withRouter } from 'react-router'; 4 | import { NotFound } from '~/components/not-found'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | const NF = withRouter(NotFound); 8 | const BN = withRouter(StackNotes); 9 | 10 | export class Stack extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | view: 'notes', 16 | awaiting: false, 17 | itemProps: [], 18 | stackTitle: '', 19 | stackHost: '', 20 | pathData: [], 21 | temporary: false, 22 | awaitingSubscribe: false, 23 | awaitingUnsubscribe: false, 24 | notFound: false 25 | }; 26 | 27 | this.subscribe = this.subscribe.bind(this); 28 | this.unsubscribe = this.unsubscribe.bind(this); 29 | this.viewSubs = this.viewSubs.bind(this); 30 | this.viewSettings = this.viewSettings.bind(this); 31 | this.viewNotes = this.viewNotes.bind(this); 32 | this.deleteStack = this.deleteStack.bind(this); 33 | this.reviewStack = this.reviewStack.bind(this); 34 | 35 | this.stack = null; 36 | } 37 | 38 | handleEvent(diff) { 39 | if (diff.data.total) { 40 | const stack = diff.data.total.data; 41 | this.stack = stack; 42 | this.setState({ 43 | itemProps: this.buildItems(stack), 44 | stack: stack, 45 | stackTitle: stack.info.title, 46 | stackHost: stack.info.owner, 47 | awaiting: false, 48 | pathData: [ 49 | { text: 'Home', url: '/~srrs/review' }, 50 | { 51 | text: stack.info.title, 52 | url: `/~srrs/${stack.info.owner}/${stack.info.filename}` 53 | } 54 | ] 55 | }); 56 | 57 | this.props.setSpinner(false); 58 | } else if (diff.data.remove) { 59 | if (diff.data.remove.item) { 60 | // XX TODO 61 | } else { 62 | this.props.history.push('/~srrs/review'); 63 | } 64 | } 65 | } 66 | 67 | handleError(err) { 68 | this.props.setSpinner(false); 69 | this.setState({ notFound: true }); 70 | } 71 | 72 | componentDidUpdate(prevProps, prevState) { 73 | if (this.state.notFound) { 74 | return; 75 | } 76 | const ship = this.props.ship; 77 | const stackId = this.props.stackId; 78 | const stack = (ship === window.ship) 79 | ? this.props.pubs[stackId] || false 80 | : this.props.subs[ship][stackId] || false; 81 | 82 | if (!(stack) && (ship === window.ship)) { 83 | this.setState({ notFound: true }); 84 | return; 85 | } else if (this.stack && !stack) { 86 | this.props.history.push('/~srrs/review'); 87 | return; 88 | } 89 | 90 | this.stack = stack; 91 | 92 | if (this.state.awaitingSubscribe && stack) { 93 | this.setState({ 94 | temporary: false, 95 | awaitingSubscribe: false 96 | }); 97 | 98 | this.props.setSpinner(false); 99 | } 100 | } 101 | 102 | componentWillMount() { 103 | const ship = this.props.ship; 104 | const stackId = this.props.stackId; 105 | const stack = (ship === window.ship) 106 | ? this.props.pubs[stackId] || false 107 | : this.props.subs[ship][stackId] || false; 108 | 109 | if (!(stack) && (ship === window.ship)) { 110 | this.setState({ notFound: true }); 111 | return; 112 | }; 113 | 114 | const temporary = (!(stack) && (ship != window.ship)); 115 | 116 | if (temporary) { 117 | this.setState({ 118 | awaiting: { 119 | ship: ship, 120 | stackId: stackId 121 | }, 122 | temporary: true 123 | }); 124 | 125 | this.props.setSpinner(true); 126 | 127 | this.props.api.bind(`/stack/${stackId}`, 'PUT', ship, 'srrs', 128 | this.handleEvent.bind(this), 129 | this.handleError.bind(this)); 130 | } else { 131 | this.stack = stack; 132 | } 133 | } 134 | 135 | deleteStack() { 136 | const del = { 137 | 'delete-stack': { 138 | who: `~${this.props.ship}`, 139 | stak: this.props.stackId 140 | } 141 | }; 142 | this.props.setSpinner(true); 143 | this.setState({ 144 | awaitingDelete: { 145 | ship: this.props.ship, 146 | stackId: this.props.stackId 147 | } 148 | }, () => { 149 | this.props.api.action('srrs', 'srrs-action', del).then(() => { 150 | const redirect = '/~srrs/review'; 151 | this.props.history.push(redirect); 152 | }); 153 | }); 154 | } 155 | 156 | reviewStack() { 157 | const action = { 158 | 'review-stack': { 159 | who: `~${this.props.ship}`, 160 | stak: this.props.stackId 161 | } 162 | }; 163 | this.props.api.action('srrs', 'srrs-action', action); 164 | this.props.history.push(`/~srrs/~${this.props.ship}/${this.props.stackId}/review`); 165 | } 166 | 167 | buildItems(stack) { 168 | if (!stack) { 169 | return []; 170 | } 171 | 172 | return Object.values(stack.items).map((item) => { 173 | return this.buildItemPreviewProps(item, stack, true); 174 | }); 175 | } 176 | 177 | buildItemPreviewProps(item, stack, pinned) { 178 | return { 179 | itemTitle: item.content.title, 180 | itemName: item.name, 181 | itemBody: item.content.front, 182 | itemSnippet: item.content.snippet, 183 | stackTitle: stack.info.title, 184 | stackName: stack.info.filename, 185 | author: item.content.author, 186 | stackOwner: stack.info.owner, 187 | date: item.content['date-created'], 188 | pinned: pinned 189 | }; 190 | } 191 | 192 | buildData() { 193 | const ship = this.props.ship; 194 | const stackId = this.props.stackId; 195 | const stack = (ship === window.ship) 196 | ? this.props.pubs[stackId] || false 197 | : this.props.subs[ship][stackId] || false; 198 | 199 | if (this.state.temporary) { 200 | return { 201 | stack: this.state.stack, 202 | itemProps: this.state.itemProps, 203 | stackTitle: this.state.stackTitle, 204 | stackHost: this.state.stackHost, 205 | pathData: this.state.pathData 206 | }; 207 | } else { 208 | if (!stack) { 209 | return false; 210 | } 211 | return { 212 | stack: stack, 213 | itemProps: this.buildItems(stack), 214 | stackTitle: stack.info.title, 215 | stackHost: stack.info.owner, 216 | pathData: [ 217 | { text: 'Home', url: '/~srrs/review' }, 218 | { 219 | text: stack.info.title, 220 | url: `/~srrs/${stack.info.owner}/${stack.info.filename}` 221 | } 222 | ] 223 | }; 224 | } 225 | } 226 | 227 | subscribe() { 228 | const sub = { 229 | subscribe: { 230 | who: this.props.ship, 231 | stack: this.props.stackId 232 | } 233 | }; 234 | this.props.setSpinner(true); 235 | this.setState({ awaitingSubscribe: true }, () => { 236 | this.props.api.action('srrs', 'srrs-action', sub); 237 | }); 238 | } 239 | 240 | unsubscribe() { 241 | const unsub = { 242 | unsubscribe: { 243 | who: this.props.ship, 244 | stack: this.props.stackId 245 | } 246 | }; 247 | this.props.api.action('srrs', 'srrs-action', unsub); 248 | this.props.history.push('/~srrs/review'); 249 | } 250 | 251 | viewSubs() { 252 | this.setState({ view: 'subs' }); 253 | } 254 | 255 | viewSettings() { 256 | this.setState({ view: 'settings' }); 257 | } 258 | 259 | viewNotes() { 260 | this.setState({ view: 'notes' }); 261 | } 262 | 263 | render() { 264 | const { props } = this; 265 | const localStack = props.ship === window.ship; 266 | 267 | if (this.state.notFound) { 268 | return ( 269 | 270 | ); 271 | } else if (this.state.awaiting) { 272 | return null; 273 | } else { 274 | const data = this.buildData(); 275 | let inner = null; 276 | switch (props.view) { 277 | case 'notes': 278 | inner = ; 279 | } 280 | 281 | return ( 282 |
{ 287 | this.scrollElement = el; 288 | }} 289 | > 290 |
291 | {'<- Review'} 292 |
293 |
296 |
297 |
301 |
304 |
{data.stackTitle}
305 | 306 | by 307 | 310 | {data.stackHost} 311 | 312 | 313 |
314 |
315 | {localStack && 316 | New Item 317 | } 318 | {localStack &&

Review all items

} 319 |

322 | Delete Stack 323 |

324 |
325 |
326 | 327 |
328 | 329 | Review 330 | 331 | 332 |
335 |
336 | 337 |
338 | {inner} 339 |
340 |
341 |
342 |
343 | ); 344 | } 345 | } 346 | } 347 | 348 | -------------------------------------------------------------------------------- /src/components/subs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { withRouter } from 'react-router'; 4 | import { HeaderMenu } from '~/lib/header-menu'; 5 | import moment from 'moment'; 6 | import momentConfig from '~/config/moment'; 7 | 8 | const HM = withRouter(HeaderMenu); 9 | 10 | export class Subs extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.accept = this.accept.bind(this); 15 | this.reject = this.reject.bind(this); 16 | this.unsubscribe = this.unsubscribe.bind(this); 17 | 18 | moment.updateLocale('en', momentConfig); 19 | } 20 | 21 | buildStackData() { 22 | 23 | let data = Object.keys(this.props.subs).map((ship) => { 24 | let perShip = Object.keys(this.props.subs[ship]).map((stackId) => { 25 | let stack = this.props.subs[ship][stackId]; 26 | return { 27 | type: 'regular', 28 | url: `/~srrs/${stack.info.owner}/${stackId}`, 29 | title: stack.info.title, 30 | host: stack.info.owner, 31 | lastUpdated: moment(stack["last-update"]).fromNow(), 32 | stackId: stackId, 33 | } 34 | }); 35 | return perShip; 36 | }); 37 | let merged = data.flat(); 38 | return merged; 39 | } 40 | 41 | accept(host, stackId) { 42 | let subscribe = { 43 | subscribe: { 44 | who: host.slice(1), 45 | stack: stackId, 46 | } 47 | }; 48 | this.props.api.action("srrs", "srrs-action", subscribe); 49 | } 50 | 51 | reject(host, stackId) { 52 | let reject = { 53 | "reject-invite": { 54 | who: host.slice(1), 55 | stack: stackId, 56 | } 57 | }; 58 | this.props.api.action("srrs", "srrs-action", reject); 59 | } 60 | 61 | unsubscribe(host, stackId) { 62 | let unsubscribe = { 63 | unsubscribe: { 64 | who: host.slice(1), 65 | stack: stackId, 66 | } 67 | }; 68 | this.props.api.action("srrs", "srrs-action", unsubscribe); 69 | } 70 | 71 | render() { 72 | let stackData = this.buildStackData(); 73 | 74 | let stacks = this.buildStackData().map( (data, i) => { 75 | let bg = (i % 2 == 0) 76 | ? "bg-v-light-gray" 77 | : "bg-white"; 78 | let cls = "w-100 flex " + bg; 79 | if (data.type === 'regular') { 80 | return ( 81 |
82 |
83 | 84 |

85 | {data.title} 86 |

87 | 88 |
89 |

90 | {data.host} 91 |

92 |

93 | {data.lastUpdated} 94 |

95 |

98 | Unsubscribe 99 |

100 |
101 | ); 102 | } 103 | }); 104 | 105 | return ( 106 |
107 | 108 |
109 |
110 |
111 |

113 | Subscriptions 114 |

115 |
116 |
117 |

118 | Title 119 |

120 |

121 | Host 122 |

123 |

124 | Last Updated 125 |

126 |

127 |

128 |
129 | 130 | {stacks} 131 |
132 |
133 |
134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/config/moment.js: -------------------------------------------------------------------------------- 1 | export default { 2 | relativeTime: { 3 | past: function (input) { 4 | return input === 'just now' 5 | ? input 6 | : input + ' ago' 7 | }, 8 | s: 'just now', 9 | future: 'in %s', 10 | m: '1m', 11 | mm: '%dm', 12 | h: '1h', 13 | hh: '%dh', 14 | d: '1d', 15 | dd: '%dd', 16 | M: '1 month', 17 | MM: '%d months', 18 | y: '1 year', 19 | yy: '%d years', 20 | } 21 | }; -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dark-gray: #555555; 3 | --gray: #7F7F7F; 4 | --medium-gray: #CCCCCC; 5 | --light-gray: rgba(0,0,0,0.08); 6 | } 7 | 8 | html, body { 9 | height: 100%; 10 | width: 100%; 11 | -webkit-font-smoothing: antialiased; 12 | overflow: hidden; 13 | font-family: "Inter", sans-serif; 14 | } 15 | 16 | p, h1, h2, h3, h4, h5, h6, a, input, textarea, button { 17 | margin-block-end: unset; 18 | margin-block-start: unset; 19 | -webkit-margin-before: unset; 20 | -webkit-margin-after: unset; 21 | font-family: Inter, sans-serif; 22 | } 23 | 24 | a { 25 | color: #000; 26 | text-decoration: none; 27 | } 28 | 29 | textarea, select, input, button { 30 | outline: none; 31 | -webkit-appearance: none; 32 | border: none; 33 | background-color: #fff; 34 | } 35 | 36 | .body-regular { 37 | font-size: 16px; 38 | line-height: 24px; 39 | font-weight: 600; 40 | } 41 | 42 | .body-large { 43 | font-size: 20px; 44 | line-height: 24px; 45 | } 46 | 47 | .label-regular { 48 | font-size: 14px; 49 | line-height: 24px; 50 | } 51 | 52 | .label-small-mono { 53 | font-size: 12px; 54 | line-height: 24px; 55 | font-family: "Source Code Pro", monospace; 56 | } 57 | 58 | .body-regular-400 { 59 | font-size: 16px; 60 | line-height: 24px; 61 | font-weight: 400; 62 | } 63 | 64 | .plus-font { 65 | font-size: 48px; 66 | line-height: 24px; 67 | } 68 | 69 | .btn-font { 70 | font-size: 14px; 71 | line-height: 16px; 72 | font-weight: 600; 73 | } 74 | .mono { 75 | font-family: "Source Code Pro", monospace; 76 | } 77 | 78 | .inter { 79 | font-family: Inter, sans-serif; 80 | } 81 | 82 | .mix-blend-diff { 83 | mix-blend-mode: difference; 84 | } 85 | 86 | @media all and (max-width: 34.375em) { 87 | .dn-s { 88 | display: none; 89 | } 90 | .flex-basis-100-s, .flex-basis-full-s { 91 | flex-basis: 100%; 92 | } 93 | .h-100-m-40-s { 94 | height: calc(100% - 40px); 95 | } 96 | .black-s { 97 | color: #000; 98 | } 99 | } 100 | 101 | @media all and (min-width: 34.375em) { 102 | .db-ns { 103 | display: block; 104 | } 105 | .flex-basis-250-ns { 106 | flex-basis: 250px; 107 | } 108 | .h-100-m-40-ns { 109 | height: calc(100% - 40px); 110 | } 111 | } 112 | .w-336 { 113 | width: 336px; 114 | } 115 | 116 | .w-688 { 117 | width: 688px; 118 | } 119 | 120 | .mw-336 { 121 | max-width: 336px; 122 | } 123 | 124 | .mw-688 { 125 | max-width: 688px; 126 | } 127 | 128 | .w-680 { 129 | width: 680px; 130 | } 131 | 132 | .w-16 { 133 | width: 16px; 134 | } 135 | 136 | .mb-33 { 137 | width: 33px; 138 | } 139 | 140 | .h-80 { 141 | height: 80px; 142 | } 143 | 144 | .b-gray-30 { 145 | border-color: #B1B2B3; 146 | } 147 | 148 | .header-menu-item { 149 | float: left; 150 | border-bottom-style: solid; 151 | border-bottom-width: 1px; 152 | border-color: #B1B2B3; 153 | color: #B1B2B3; 154 | flex-basis: 148px; 155 | padding-bottom: 3px; 156 | vertical-align: middle; 157 | font-size: 14px; 158 | line-height: 24px; 159 | } 160 | 161 | .srrs { 162 | float: left; 163 | vertical-align: middle; 164 | font-size: 20px; 165 | line-height: 24px; 166 | font-weight: bold; 167 | color: #7F7F7F; 168 | margin-left: 16px; 169 | margin-top: 16px; 170 | margin-bottom: 8px; 171 | } 172 | 173 | .create { 174 | padding: 8px 12px; 175 | border-radius: 2px; 176 | cursor: pointer; 177 | } 178 | 179 | .path-control { 180 | width: 100%; 181 | border-bottom-style: solid; 182 | border-bottom-width: 1px; 183 | border-color: #B1B2B3; 184 | height: 28px; 185 | clear: both; 186 | } 187 | 188 | .h-modulo-header { 189 | height: 48px; 190 | } 191 | 192 | .h-srrs-header { 193 | height: 76px; 194 | top: 48px; 195 | } 196 | 197 | .h-inner { 198 | height: calc(100% - 124px); 199 | top: 48px; 200 | } 201 | 202 | .h-footer { 203 | height: 76px; 204 | } 205 | 206 | ::placeholder { 207 | color: #B1B2B3; 208 | } 209 | 210 | .bg-red { 211 | background-color: #EE5432; 212 | } 213 | 214 | .bg-gray-30 { 215 | background-color: #B1B2B3; 216 | } 217 | 218 | .two-lines { 219 | display: -webkit-box; 220 | -webkit-box-orient: vertical; 221 | word-wrap: break-word; 222 | -webkit-line-clamp: 2; 223 | overflow: hidden; 224 | } 225 | 226 | .five-lines { 227 | display: -webkit-box; 228 | -webkit-box-orient: vertical; 229 | word-wrap: break-word; 230 | -webkit-line-clamp: 5; 231 | overflow: hidden; 232 | } 233 | 234 | .one-line { 235 | word-wrap: break-word; 236 | overflow: hidden; 237 | white-space: nowrap; 238 | text-overflow: ellipsis; 239 | } 240 | 241 | .bg-light-green { 242 | background: rgba(42, 167, 121, 0.1); 243 | } 244 | 245 | .focus-b--black:focus { 246 | border-color: #000; 247 | } 248 | 249 | .mix-blend-diff { 250 | mix-blend-mode: difference; 251 | } 252 | 253 | .StackButton { 254 | padding: 8px 12px; 255 | border-radius:2px; 256 | cursor: pointer; 257 | } 258 | 259 | .NewItem { 260 | width: 100%; 261 | height: calc(100vh - 174px); 262 | display: flex; 263 | padding-top: 8px; 264 | } 265 | 266 | .EditItem { 267 | width: 100%; 268 | display: flex; 269 | padding-top: 8px; 270 | } 271 | 272 | .placeholder-inter::placeholder { 273 | font-family: "Inter", sans-serif; 274 | } 275 | 276 | /* toggler checkbox */ 277 | .toggle::after { 278 | content: ""; 279 | height: 12px; 280 | width: 12px; 281 | background: white; 282 | position: absolute; 283 | top: 2px; 284 | left: 2px; 285 | border-radius: 100%; 286 | } 287 | 288 | .toggle.checked::after { 289 | content: ""; 290 | height: 12px; 291 | width: 12px; 292 | background: white; 293 | position: absolute; 294 | top: 2px; 295 | left: 14px; 296 | border-radius: 100%; 297 | } 298 | 299 | .react-codemirror2 { 300 | width: 100%; 301 | } 302 | 303 | .CodeMirror { 304 | padding: 12px; 305 | height: 100% !important; 306 | max-width: 700px; 307 | width: 100% !important; 308 | cursor: text; 309 | font-size: 12px; 310 | line-height: 20px; 311 | } 312 | 313 | .CodeMirror * { 314 | font-family: 'Source Code Pro'; 315 | } 316 | 317 | .CodeMirror-selected { background:#BAE3FE !important; color: black; } 318 | 319 | .cm-s-tlon span { font-family: "Source Code Pro"} 320 | .cm-s-tlon span.cm-meta { color: var(--gray); } 321 | .cm-s-tlon span.cm-number { color: var(--gray); } 322 | .cm-s-tlon span.cm-keyword { line-height: 1em; font-weight: bold; color: var(--gray); } 323 | .cm-s-tlon span.cm-atom { font-weight: bold; color: var(--gray); } 324 | .cm-s-tlon span.cm-def { color: black; } 325 | .cm-s-tlon span.cm-variable { color: black; } 326 | .cm-s-tlon span.cm-variable-2 { color: black; } 327 | .cm-s-tlon span.cm-variable-3, .cm-s-tlon span.cm-type { color: black; } 328 | .cm-s-tlon span.cm-property { color: black; } 329 | .cm-s-tlon span.cm-operator { color: black; } 330 | .cm-s-tlon span.cm-comment { color: black; background-color: var(--light-gray); display: inline-block; border-radius: 2px;} 331 | .cm-s-tlon span.cm-string { color: var(--dark-gray); } 332 | .cm-s-tlon span.cm-string-2 { color: var(--gray); } 333 | .cm-s-tlon span.cm-qualifier { color: #555; } 334 | .cm-s-tlon span.cm-error { color: #FF0000; } 335 | .cm-s-tlon span.cm-attribute { color: var(--gray); } 336 | .cm-s-tlon span.cm-tag { color: var(--gray); } 337 | .cm-s-tlon span.cm-link { color: var(--dark-gray); text-decoration: none;} 338 | .cm-s-tlon .CodeMirror-activeline-background { background: var(--gray); } 339 | .cm-s-tlon .CodeMirror-cursor { 340 | border-left: 3px solid #3687FF; 341 | } 342 | 343 | .cm-s-tlon span.cm-builtin { color: var(--gray); } 344 | .cm-s-tlon span.cm-bracket { color: var(--gray); } 345 | /* .cm-s-tlon { font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;} */ 346 | 347 | 348 | .cm-s-tlon .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; } 349 | 350 | .CodeMirror-hints.tlon { 351 | /* font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; */ 352 | color: #616569; 353 | background-color: #ebf3fd !important; 354 | } 355 | 356 | .CodeMirror-hints.tlon .CodeMirror-hint-active { 357 | background-color: #a2b8c9 !important; 358 | color: #5c6065 !important; 359 | } 360 | 361 | .title-input[placeholder]:empty:before { 362 | content: attr(placeholder); 363 | color: #7F7F7F; 364 | } 365 | 366 | .options.open { 367 | background-color: #e6e6e6; 368 | } 369 | 370 | .options.closed { 371 | background-color: white; 372 | } 373 | 374 | .options::after { 375 | content: "⌃"; 376 | transform: rotate(180deg); 377 | position: absolute; 378 | right: 8px; 379 | top: 4px; 380 | color: #7f7f7f; 381 | } 382 | 383 | [contenteditable]:focus { 384 | outline: 0px solid transparent; 385 | } 386 | 387 | .dropdown::after { 388 | content: "⌃"; 389 | transform: rotate(180deg); 390 | position: absolute; 391 | right: 8px; 392 | top: 16px; 393 | color: #7f7f7f; 394 | } 395 | 396 | .no-scrollbar { 397 | -ms-overflow-style: none; 398 | scrollbar-width: none; 399 | } 400 | 401 | .no-scrollbar::-webkit-scrollbar { 402 | display: none; 403 | } 404 | 405 | .md h1, .md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul, .md ol, .md blockquote,.md code,.md pre { 406 | font-size: 14px; 407 | margin-bottom: 16px; 408 | } 409 | 410 | .md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul { 411 | font-weight: 400; 412 | } 413 | 414 | .md h1 { 415 | font-weight: 600; 416 | } 417 | 418 | .md h2, .md h3, .md h4, .md h5 { 419 | color:var(--gray); 420 | } 421 | 422 | .md p { 423 | line-height: 1.5; 424 | } 425 | .md code, .md pre { 426 | font-family: "Source Code Pro", mono; 427 | } 428 | .md ul>li, .md ol>li { 429 | line-height: 1.5; 430 | } 431 | .md a { 432 | border-bottom-style: solid; 433 | border-bottom-width: 1px; 434 | } 435 | 436 | md img { 437 | margin-bottom: 8px; 438 | } 439 | 440 | .focus-b--black:focus { 441 | border-color: #000; 442 | } 443 | 444 | .spin-active { 445 | animation: spin 2s infinite; 446 | } 447 | 448 | @keyframes spin { 449 | 0% {transform: rotate(0deg);} 450 | 25% {transform: rotate(90deg);} 451 | 50% {transform: rotate(180deg);} 452 | 75% {transform: rotate(270deg);} 453 | 100% {transform: rotate(360deg);} 454 | } 455 | 456 | .mix-blend-diff { 457 | mix-blend-mode: difference; 458 | } 459 | 460 | @media all and (prefers-color-scheme: dark) { 461 | body { 462 | background-color: #333; 463 | } 464 | .bg-black-d { 465 | background-color: black; 466 | } 467 | .white-d { 468 | color: white; 469 | } 470 | .gray1-d { 471 | color: #4d4d4d; 472 | } 473 | .gray2-d { 474 | color: #7f7f7f; 475 | } 476 | .gray3-d { 477 | color: #b1b2b3; 478 | } 479 | .gray4-d { 480 | color: #e6e6e6; 481 | } 482 | .bg-gray0-d { 483 | background-color: #333; 484 | } 485 | .bg-gray1-d { 486 | background-color: #4d4d4d; 487 | } 488 | .b--gray0-d { 489 | border-color: #333; 490 | } 491 | .b--gray1-d { 492 | border-color: #4d4d4d; 493 | } 494 | .b--gray2-d { 495 | border-color: #7f7f7f; 496 | } 497 | .b--white-d { 498 | border-color: #fff; 499 | } 500 | .invert-d { 501 | filter: invert(1); 502 | } 503 | .o-60-d { 504 | opacity: .6; 505 | } 506 | a { 507 | color: #fff; 508 | } 509 | .focus-b--white-d:focus { 510 | border-color: #fff; 511 | } 512 | .hover-bg-gray1-d:hover { 513 | background-color: #4d4d4d; 514 | } 515 | .options.open { 516 | background-color: #4d4d4d; 517 | } 518 | .options.closed { 519 | background-color: #333; 520 | } 521 | .cm-s-tlon.CodeMirror { 522 | background: #333; 523 | color: #fff; 524 | } 525 | 526 | .cm-s-tlon span.cm-def { 527 | color: white; 528 | } 529 | 530 | .cm-s-tlon span.cm-variable { 531 | color: white; 532 | } 533 | 534 | .cm-s-tlon span.cm-variable-2 { 535 | color: white; 536 | } 537 | 538 | .cm-s-tlon span.cm-variable-3, 539 | .cm-s-tlon span.cm-type { 540 | color: white; 541 | } 542 | 543 | .cm-s-tlon span.cm-property { 544 | color: white; 545 | } 546 | 547 | .cm-s-tlon span.cm-operator { 548 | color: white; 549 | } 550 | 551 | 552 | .cm-s-tlon span.cm-string { 553 | color: var(--gray); 554 | } 555 | 556 | .cm-s-tlon span.cm-string-2 { 557 | color: var(--gray); 558 | } 559 | 560 | .cm-s-tlon span.cm-attribute { 561 | color: var(--gray); 562 | } 563 | 564 | .cm-s-tlon span.cm-tag { 565 | color: var(--gray); 566 | } 567 | 568 | .cm-s-tlon span.cm-link { 569 | color: var(--gray); 570 | } 571 | 572 | /* set rules w/ both color & bg-color last to preserve legibility */ 573 | .CodeMirror-selected { 574 | background: var(--medium-gray) !important; 575 | color: white; 576 | } 577 | 578 | .cm-s-tlon span.cm-comment { 579 | color: black; 580 | display: inline-block; 581 | padding: 0; 582 | background-color: rgba(255,255,255, 0.3); 583 | border-radius: 2px; 584 | } 585 | } 586 | 587 | -------------------------------------------------------------------------------- /src/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2"); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Inter'; 10 | font-style: italic; 11 | font-weight: 400; 12 | src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2"); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Inter'; 17 | font-style: normal; 18 | font-weight: 700; 19 | src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2"); 20 | } 21 | @font-face { 22 | font-family: 'Inter'; 23 | font-style: italic; 24 | font-weight: 700; 25 | src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2"); 26 | } 27 | 28 | @font-face { 29 | font-family: "Source Code Pro"; 30 | src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff"); 31 | font-weight: 200; 32 | } 33 | 34 | @font-face { 35 | font-family: "Source Code Pro"; 36 | src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff"); 37 | font-weight: 300; 38 | } 39 | 40 | @font-face { 41 | font-family: "Source Code Pro"; 42 | src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff"); 43 | font-weight: 400; 44 | } 45 | 46 | @font-face { 47 | font-family: "Source Code Pro"; 48 | src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff"); 49 | font-weight: 500; 50 | } 51 | 52 | @font-face { 53 | font-family: "Source Code Pro"; 54 | src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff"); 55 | font-weight: 600; 56 | } 57 | 58 | @font-face { 59 | font-family: "Source Code Pro"; 60 | src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff"); 61 | font-weight: 700; 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/css/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner-pending { 2 | position: relative; 3 | content: ""; 4 | border-radius: 100%; 5 | height: 16px; 6 | width: 16px; 7 | 8 | background-color: rgba(255,255,255,1); 9 | } 10 | 11 | .spinner-pending::after { 12 | content: ""; 13 | background-color: rgba(128,128,128,1); 14 | width: 16px; 15 | height: 16px; 16 | position: absolute; 17 | border-radius: 100%; 18 | clip: rect(0, 16px, 16px, 8px); 19 | 20 | animation: spin 1s cubic-bezier(0.745, 0.045, 0.355, 1.000) infinite; 21 | } 22 | 23 | @keyframes spin { 24 | 0% {transform:rotate(0deg)} 25 | 25% {transform:rotate(90deg)} 26 | 50% {transform:rotate(180deg)} 27 | 75% {transform:rotate(270deg)} 28 | 100% {transform:rotate(360deg)} 29 | } 30 | 31 | .spinner-nostart { 32 | width: 8px; 33 | height: 8px; 34 | border-radius: 100%; 35 | content:''; 36 | background-color: black; 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/img/srrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/src/img/srrs.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import 'css/indigo-static.css'; 2 | @import 'css/fonts.css'; 3 | @import 'css/custom.css'; 4 | @import 'css/spinner.css'; 5 | 6 | @import '../node_modules/codemirror/lib/codemirror.css'; 7 | @import '../node_modules/codemirror/theme/material.css'; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { App } from '~/components/root'; 5 | import { api } from '~/api'; 6 | import { store } from '~/store'; 7 | import { subscription } from "./subscription"; 8 | import * as util from '~/lib/util'; 9 | import _ from 'lodash'; 10 | api.setAuthTokens({ 11 | ship: window.ship 12 | }); 13 | 14 | window.urb = new window.channel() 15 | subscription.start(); 16 | 17 | window.util = util; 18 | window._ = _; 19 | 20 | ReactDOM.render(( 21 | 22 | ), document.querySelectorAll("#root")[0]); 23 | -------------------------------------------------------------------------------- /src/lib/header-bar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { cite } from '~/lib/util'; 3 | import { IconHome } from '~/lib/icons/icon-home'; 4 | import { Sigil } from '~/lib/icons/sigil'; 5 | 6 | export class HeaderBar extends Component { 7 | render() { 8 | const title = document.title === 'Home' ? '' : 'srrs'; 9 | 10 | return ( 11 |
17 | 22 | 23 | 27 | Home 28 | 29 | 30 | 37 | {title} 38 | 39 |
40 | 46 | {cite(window.ship)} 47 |
48 |
49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/header-menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { SrrsCreate } from '~/lib/srrs-create'; 4 | import { withRouter } from 'react-router'; 5 | 6 | const PC = withRouter(SrrsCreate); 7 | 8 | export class HeaderMenu extends Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | render () { 14 | const tabStyles = { 15 | review: 'bb b--gray4 b--gray2-d gray2 pv4 ph2', 16 | stacks: 'bb b--gray4 b--gray2-d gray2 pv4 ph2', 17 | settings: 'bb b--gray4 b--gray2-d pr2 gray2 pv4 ph2' 18 | }; 19 | return ( 20 |
{ 25 | this.scrollElement = el; 26 | }} 27 | > 28 | 29 | 30 |
33 | 34 |
35 | 36 | Review 37 | 38 | 39 | Stacks 40 | 41 |
44 |
45 | 46 |
47 |
48 | 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/icon.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { IconInbox } from '~/lib/icons/icon-inbox'; 3 | import { IconComment } from '~/lib/icons/icon-comment'; 4 | import { IconSig } from '~/lib/icons/icon-sig'; 5 | import { IconDecline } from '~/lib/icons/icon-decline'; 6 | import { IconUser } from '~/lib/icons/icon-user'; 7 | 8 | export class Icon extends Component { 9 | render() { 10 | let iconElem = null; 11 | 12 | switch(this.props.type) { 13 | case 'icon-stream-chat': 14 | iconElem = ; 15 | break; 16 | case 'icon-stream-dm': 17 | iconElem = ; 18 | break; 19 | case 'icon-stack-index': 20 | iconElem = ; 21 | break; 22 | case 'icon-stack-item': 23 | iconElem = ; 24 | break; 25 | case 'icon-stack-comment': 26 | iconElem = ; 27 | break; 28 | case 'icon-panini': 29 | // TODO: Should icons be display: block, inline, or inline-blocks? 30 | // 1) Should naturally flow inline 31 | // 2) But can't make icon-panini naturally inline without hacks like   32 | iconElem =
; 33 | break; 34 | case 'icon-x': 35 | iconElem = ; 36 | break; 37 | case 'icon-decline': 38 | iconElem = ; 39 | break; 40 | case 'icon-lus': 41 | iconElem = ; 42 | break; 43 | case 'icon-inbox': 44 | iconElem = ; 45 | break; 46 | case 'icon-comment': 47 | iconElem = ; 48 | break; 49 | case 'icon-sig': 50 | iconElem = ; 51 | break; 52 | case 'icon-user': 53 | iconElem = ; 54 | break; 55 | case 'icon-ellipsis': 56 | iconElem = ( 57 |
58 |
59 |
60 |
61 |
62 | ); 63 | break; 64 | } 65 | 66 | const className = this.props.label ? 'icon-label' : ''; 67 | 68 | return ( 69 | 70 | {iconElem} 71 | 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/icons/icon-check.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconCheck extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/icons/icon-comment.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconComment extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/icons/icon-cross.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconCross extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/icons/icon-decline.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconDecline extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/icons/icon-home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | export class IconHome extends Component { 4 | render() { 5 | let classes = !!this.props.classes ? this.props.classes : ""; 6 | return ( 7 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/icons/icon-inbox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconInbox extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/icons/icon-sidebar-switch.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { api } from '../../api'; 3 | 4 | export class SidebarSwitcher extends Component { 5 | render() { 6 | 7 | let popoutSwitcher = this.props.popout 8 | ? "dn-m dn-l dn-xl" 9 | : "dib-m dib-l dib-xl"; 10 | 11 | return ( 12 | 30 | ); 31 | } 32 | } 33 | 34 | export default SidebarSwitcher 35 | -------------------------------------------------------------------------------- /src/lib/icons/icon-sig.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconSig extends Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/icons/icon-spinner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class Spinner extends Component { 4 | render() { 5 | 6 | let classes = !!this.props.classes ? this.props.classes : ""; 7 | let text = !!this.props.text ? this.props.text : ""; 8 | let awaiting = !!this.props.awaiting ? this.props.awaiting : false; 9 | 10 | if (awaiting) { 11 | return ( 12 |
13 | 17 |

{text}

18 |
19 | ); 20 | } 21 | else { 22 | return null; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/icons/icon-user.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class IconUser extends Component { 4 | render() { 5 | return ( 6 | 7 | ) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/icons/sigil.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { sigil, reactRenderer } from 'urbit-sigil-js'; 3 | 4 | 5 | export class Sigil extends Component { 6 | render() { 7 | const { props } = this; 8 | 9 | let classes = props.classes || ""; 10 | 11 | if (props.ship.length > 14) { 12 | return ( 13 |
16 |
17 | ); 18 | } else { 19 | return ( 20 |
21 | {sigil({ 22 | patp: props.ship, 23 | renderer: reactRenderer, 24 | size: props.size, 25 | colors: [props.color, "white"] 26 | })} 27 |
28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/item-body.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | 4 | export class ItemBody extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const front = this.props.bodyFront; 11 | const back = this.props.bodyBack; 12 | const newFront = front.slice(front.indexOf(';>') + 2); 13 | const newBack = back.slice(back.indexOf(';>') + 2); 14 | const toggleStyle = 'v-mid bg-transparent mw6 tl h1 pl4'; 15 | if (this.props.showBack) { 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 | 33 |
34 | ); 35 | } else { 36 | return ( 37 |
38 | 39 | 48 |
49 | ); 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/lib/item-preview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | import { Link } from 'react-router-dom'; 4 | import { ItemSnippet } from '~/lib/item-snippet'; 5 | import { TitleSnippet } from '~/lib/title-snippet'; 6 | import momentConfig from '~/config/moment'; 7 | 8 | export class ItemPreview extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | moment.updateLocale('en', momentConfig); 13 | this.saveGrade = this.saveGrade.bind(this); 14 | } 15 | 16 | saveGrade() { 17 | this.props.setSpinner(true); 18 | const data = { 19 | 'answered-item': { 20 | stak: this.props.stackId, 21 | item: this.props.itemId, 22 | answer: this.state.recallGrade 23 | } 24 | }; 25 | this.setState({ 26 | awaitingGrade: { 27 | ship: this.state.ship, 28 | stackId: this.props.stackId, 29 | itemId: this.props.itemId 30 | } 31 | }, () => { 32 | this.props.api.action('srrs', 'srrs-action', data); 33 | }); 34 | }; 35 | render() { 36 | const date = moment(this.props.item.date).fromNow(); 37 | const author = this.props.item.author; 38 | 39 | const stackLink = '/~srrs/' + 40 | this.props.item.stackOwner + '/' + 41 | this.props.item.stackName; 42 | const itemLink = stackLink + '/' + this.props.item.itemName; 43 | 44 | return ( 45 |
46 | 47 | 48 | 51 | 52 |
53 |
{author}
54 |
{date}
55 |
56 | 57 |
58 | 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/item-snippet.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | 4 | export class ItemSnippet extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | return ( 11 |
14 | 19 |
20 | 21 | ); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/lib/next-prev.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { TitleSnippet } from '~/lib/title-snippet'; 3 | import { ItemSnippet } from '~/lib/item-snippet'; 4 | import { Link } from 'react-router-dom'; 5 | import moment from 'moment'; 6 | import momentConfig from '~/config/moment'; 7 | 8 | class Preview extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | moment.updateLocale('en', momentConfig); 13 | } 14 | 15 | buildProps(itemId) { 16 | const item = this.props.stack.items[itemId]; 17 | return { 18 | itemTitle: item.content.title, 19 | itemName: item.name, 20 | itemBody: item.content.front, 21 | itemSnippet: item.content.snippet, 22 | stackTitle: this.props.stack.info.title, 23 | stackName: this.props.stack.info.filename, 24 | author: item.content.author, 25 | stackOwner: this.props.stack.info.owner, 26 | date: item.content['date-created'], 27 | pinned: false 28 | }; 29 | } 30 | 31 | render() { 32 | if (this.props.itemId) { 33 | const owner = this.props.stack.info.owner; 34 | const stackId = this.props.stack.info.filename; 35 | const previewProps = this.buildProps(this.props.itemId); 36 | const prevUrl = `/~srrs/${owner}/${stackId}/${this.props.itemId}`; 37 | 38 | const date = moment(previewProps.date).fromNow(); 39 | const authorDate = `${previewProps.author} • ${date}`; 40 | const stackLink = '/~srrs/' + 41 | previewProps.stackOwner + '/' + 42 | previewProps.stackName; 43 | const itemLink = stackLink + '/' + previewProps.itemName; 44 | 45 | return ( 46 |
47 | 48 | {this.props.text} 49 | 50 |
53 | 54 | 55 |
56 | 59 | 60 |

61 | {authorDate} 62 |

63 |
64 |
65 | ); 66 | } else { 67 | return ( 68 |
69 | ); 70 | } 71 | } 72 | } 73 | 74 | export class NextPrev extends Component { 75 | constructor(props) { 76 | super(props); 77 | } 78 | 79 | render() { 80 | const items = this.props.stack.order.unpin.slice().reverse(); 81 | const itemIdx = items.indexOf(this.props.itemId); 82 | 83 | const prevId = (itemIdx > 0) 84 | ? items[itemIdx - 1] 85 | : false; 86 | 87 | const nextId = (itemIdx < (items.length - 1)) 88 | ? items[itemIdx + 1] 89 | : false; 90 | 91 | if (!(prevId || nextId)) { 92 | return null; 93 | } else { 94 | const prevText = '<- Previous Item'; 95 | const nextText = '-> Next Item'; 96 | 97 | return ( 98 |
99 |
100 | 101 |
102 | 103 |
104 |
105 |
106 | ); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/lib/path-control.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { withRouter } from 'react-router'; 4 | import { SrrsCreate } from '~/lib/srrs-create'; 5 | 6 | const PC = withRouter(SrrsCreate); 7 | 8 | export class PathControl extends Component { 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | buildPathData() { 14 | const path = [ 15 | { text: 'Home', url: '/~srrs/review' } 16 | ]; 17 | 18 | const last = this.props.location.state || false; 19 | const ship = last.lastParams.ship.slice(1); 20 | const stackId = last.lastParams.stack; 21 | let stack = false; 22 | let finalUrl = this.props.location.pathname; 23 | 24 | if (last) { 25 | finalUrl = { 26 | pathName: finalUrl, 27 | state: last 28 | }; 29 | 30 | if ((last.lastMatch === '/~srrs/:ship/:stack/:item') || 31 | (last.lastMatch === '/~srrs/:ship/:stack')) { 32 | stack = (ship == window.ship) 33 | ? this.props.pub[stackId] || false 34 | : this.props.subs[ship][stackId] || false; 35 | } 36 | } 37 | 38 | if (this.props.location.pathname === '/~srrs/new-stack') { 39 | path.push( 40 | { text: 'New Stack', url: finalUrl } 41 | ); 42 | } else if (this.props.location.pathname === '/~srrs/new-item') { 43 | if (stack) { 44 | path.push({ 45 | text: stack.info.title, 46 | url: `/~srrs/${stack.info.owner}/${stack.info.filename}` 47 | }); 48 | } 49 | path.push( 50 | { text: 'New Note', url: finalUrl } 51 | ); 52 | } 53 | return path; 54 | } 55 | 56 | render() { 57 | const pathData = (this.props.pathData) 58 | ? this.props.pathData 59 | : this.buildPathData(); 60 | const path = []; 61 | let key = 0; 62 | 63 | pathData.forEach((seg, i) => { 64 | const style = (i == 0) 65 | ? { marginLeft: 16 } 66 | : {}; 67 | if (i === pathData.length - 1) 68 | style.color = 'black'; 69 | 70 | path.push( 71 | 74 | {seg.text} 75 | 76 | ); 77 | if (i < (pathData.length - 1)) { 78 | path.push( 79 | 84 | ); 85 | } 86 | }); 87 | 88 | const create = ((window.location.pathname === '/~srrs/new-stack') || 89 | (window.location.pathname === '/~srrs/new-item')) || 90 | (this.props.create === false) 91 | ? false 92 | : 'item'; 93 | 94 | return ( 95 |
96 | 97 |
98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/review-preview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | import { Link } from 'react-router-dom'; 4 | import { ItemSnippet } from '~/lib/item-snippet'; 5 | import momentConfig from '~/config/moment'; 6 | 7 | export class ReviewPreview extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | moment.updateLocale('en', momentConfig); 12 | } 13 | 14 | render() { 15 | const date = moment(this.props.item.date).fromNow(); 16 | const author = this.props.item.author; 17 | const stackLink = 18 | '/~srrs/' + this.props.item.author + '/' + this.props.item.stackName; 19 | const itemLink = stackLink + '/' + this.props.item.itemName; 20 | const loc = { 21 | pathname: itemLink, 22 | state: { mode: 'review', prevPath: location.pathname }, 23 | }; 24 | return ( 25 | 26 |
27 |
28 |
29 | 30 |
31 |
{author}
32 |
{date}
33 | 34 |
{this.props.item.stackTitle}
35 | 36 |
37 |
38 |
39 |
40 | 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/seal-dict.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { pour } from '~/vendor/sigils-1.2.5'; 3 | 4 | /* eslint-disable react/display-name */ 5 | 6 | const ReactSVGComponents = { 7 | svg: (p) => { 8 | return ( 9 | 14 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 15 | 16 | ); 17 | }, 18 | circle: (p) => { 19 | return ( 20 | 23 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 24 | 25 | ); 26 | }, 27 | rect: (p) => { 28 | return ( 29 | 33 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 34 | 35 | ); 36 | }, 37 | path: (p) => { 38 | return ( 39 | 43 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 44 | 45 | ); 46 | }, 47 | g: (p) => { 48 | return ( 49 | 53 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 54 | 55 | ); 56 | }, 57 | polygon: (p) => { 58 | return ( 59 | 63 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 64 | 65 | ); 66 | }, 67 | line: (p) => { 68 | return ( 69 | 73 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 74 | 75 | ); 76 | }, 77 | polyline: (p) => { 78 | return ( 79 | 83 | {(p.children || []).map(child => ReactSVGComponents[child.tag](child))} 84 | 85 | ); 86 | } 87 | }; 88 | 89 | export class SealDict { 90 | constructor() { 91 | this.dict = {}; 92 | } 93 | 94 | getPrefix(patp) { 95 | return patp.length === 3 ? patp : patp.substr(0, 3); 96 | } 97 | 98 | getSeal(patp, size, prefix) { 99 | if (patp.length > 13) { 100 | patp = 'tiz'; 101 | } 102 | 103 | const sigilShip = prefix ? this.getPrefix(patp) : patp; 104 | const key = `${sigilShip}+${size}`; 105 | 106 | if (!this.dict[key]) { 107 | this.dict[key] = pour({ size: size, patp: sigilShip, renderer: ReactSVGComponents, margin: 0, colorway: ['#fff', '#000'] }); 108 | } 109 | 110 | return this.dict[key]; 111 | } 112 | } 113 | 114 | const sealDict = new SealDict(); 115 | export { sealDict }; 116 | -------------------------------------------------------------------------------- /src/lib/sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route, Link } from 'react-router-dom'; 3 | import { StackEntry } from '~/components/stack-entry'; 4 | import { Col, Box } from '@tlon/indigo-react'; 5 | 6 | export class Sidebar extends Component { 7 | render() { 8 | const { props, state } = this; 9 | const display = props.hidden ? ['none', 'block'] : 'block'; 10 | const stacks = {}; 11 | Object.keys(props.pubs).map((stack) => { 12 | const title = `${stack}`; 13 | stacks[title] = props.pubs[stack]; 14 | }); 15 | Object.keys(props.subs).map((host) => { 16 | Object.keys(props.subs[host]).map((stack) => { 17 | const title = `~${host}/${stack}`; 18 | stacks[title] = props.subs[host][stack]; 19 | }); 20 | }); 21 | 22 | const groupedStacks = {}; 23 | Object.keys(stacks).map((stack) => { 24 | const owner = stacks[stack].info.owner; 25 | if (owner.slice(1) === window.ship) { 26 | if (groupedStacks['/~/']) { 27 | const array = groupedStacks['/~/']; 28 | array.push(stack); 29 | groupedStacks['/~/'] = array; 30 | } else { 31 | groupedStacks['/~/'] = [stack]; 32 | }; 33 | } else if (groupedStacks[owner]) { 34 | const array = groupedStacks[owner]; 35 | array.push(stack); 36 | groupedStacks[owner] = array; 37 | } else { 38 | groupedStacks[owner] = [stack]; 39 | } 40 | }); 41 | 42 | let groupedItems = []; 43 | const groupedSubs = []; 44 | 45 | if (groupedStacks['/~/']) { 46 | groupedItems = groupedStacks['/~/'] 47 | .map((stack) => { 48 | const owner = stacks[stack].info.owner; 49 | const path = `${owner}/${stacks[stack].info.filename}`; 50 | 51 | const selected = props.path === path; 52 | return ( 53 | 59 | ); 60 | }); 61 | } 62 | 63 | Object.keys(groupedStacks).forEach((host, i, arr) => { 64 | if (host === '/~/' || host.slice(1) === window.ship) { 65 | return null; 66 | } 67 | groupedSubs.push(groupedStacks[host].map((stack, i, arr) => { 68 | const owner = stacks[stack].info.owner; 69 | const path = `${owner}/${stacks[stack].info.filename}`; 70 | 71 | const selected = props.path === path; 72 | return ( 73 | 79 | ); 80 | } 81 | )); 82 | }); 83 | 84 | return ( 85 | 95 | 96 | 97 | Review 98 | 99 | 100 | 101 | New Stack 102 | 103 | 104 | 105 |
your stacks
106 | {groupedItems} 107 |
subscriptions
108 | {groupedSubs} 109 |
110 |
111 | 112 | ); 113 | } 114 | } 115 | 116 | export default Sidebar; 117 | -------------------------------------------------------------------------------- /src/lib/srrs-create.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export class SrrsCreate extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render () { 10 | if (!this.props.create) { 11 | return ( 12 |
13 |
14 | ); 15 | } else if (this.props.create == 'stack') { 16 | const link = { 17 | pathname: '/~srrs/new-stack', 18 | state: { 19 | lastPath: this.props.location.pathname, 20 | lastMatch: this.props.match.path, 21 | lastParams: this.props.match.params 22 | } 23 | }; 24 | return ( 25 |
26 | 27 |

New Stack

28 | 29 |
30 | ); 31 | } else if (this.props.create == 'item') { 32 | const link = { 33 | pathname: '/~srrs/new-item', 34 | state: { 35 | lastPath: this.props.location.pathname, 36 | lastMatch: this.props.match.path, 37 | lastParams: this.props.match.params 38 | } 39 | }; 40 | return ( 41 |
42 | 43 |

New Item

44 | 45 |
46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/stack-data.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | 4 | class Subscribe extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | if (this.props.actionType === 'subscribe') { 11 | return ( 12 |

15 | Subscribe 16 |

17 | ); 18 | } else if (this.props.actionType === 'unsubscribe') { 19 | return ( 20 |

23 | Unsubscribe 24 |

25 | ); 26 | } else { 27 | return null; 28 | } 29 | } 30 | } 31 | 32 | class Subscribers extends Component { 33 | constructor(props) { 34 | super(props); 35 | } 36 | 37 | render() { 38 | const subscribers = (this.props.subNum === 1) 39 | ? `${this.props.subNum} Subscriber` 40 | : `${this.props.subNum} Subscribers`; 41 | 42 | if (this.props.action !== null) { 43 | return ( 44 |

45 | {subscribers} 46 |

47 | ); 48 | } else { 49 | return ( 50 |

{subscribers}

51 | ); 52 | } 53 | } 54 | } 55 | 56 | class Settings extends Component { 57 | constructor(props) { 58 | super(props); 59 | } 60 | 61 | render() { 62 | if (this.props.action !== null) { 63 | return ( 64 |

65 | Settings 66 |

67 | ); 68 | } else { 69 | return null; 70 | } 71 | } 72 | } 73 | 74 | export class StackData extends Component { 75 | constructor(props) { 76 | super(props); 77 | } 78 | 79 | render() { 80 | return ( 81 |
82 |

By ~{this.props.host}

83 | 84 | 85 | 89 |
90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/stack-notes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ItemPreview } from '~/lib/item-preview'; 3 | 4 | export class StackNotes extends Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | render() { 10 | const items = this.props.items.map((item, key) => { 11 | return ( 12 | 13 | ); 14 | }); 15 | 16 | return ( 17 |
18 | {items} 19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/stack-settings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class SaveLink extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | if (this.props.enabled) { 10 | return ( 11 | 16 | ); 17 | } else { 18 | return ( 19 |

20 | -> Save 21 |

22 | ); 23 | } 24 | } 25 | } 26 | 27 | export class StackSettings extends Component { 28 | constructor(props) { 29 | super(props); 30 | 31 | this.state = { 32 | title: '', 33 | awaitingTitleChange: false 34 | }; 35 | 36 | this.rename = this.rename.bind(this); 37 | this.titleChange = this.titleChange.bind(this); 38 | this.deleteStack = this.deleteStack.bind(this); 39 | } 40 | 41 | rename() { 42 | const edit = { 43 | 'edit-stack': { 44 | name: this.props.stackId, 45 | title: this.state.title 46 | } 47 | }; 48 | this.setState({ 49 | awaitingTitleChange: true 50 | }, () => { 51 | this.props.api.action('srrs', 'srrs-action', edit); 52 | }); 53 | } 54 | 55 | titleChange(evt) { 56 | this.setState({ title: evt.target.value }); 57 | } 58 | 59 | deleteStack() { 60 | const del = { 61 | 'delete-stack': { 62 | stack: this.props.stackId 63 | } 64 | }; 65 | this.props.api.action('srrs', 'srrs-action', del); 66 | this.props.history.push('/~srrs/review'); 67 | } 68 | 69 | componentDidUpdate(prevProps) { 70 | if (this.state.awaitingTitleChange) { 71 | if (prevProps.title !== this.props.title) { 72 | this.titleInput.value = ''; 73 | this.setState({ 74 | awaitingTitleChange: false 75 | }); 76 | } 77 | } 78 | } 79 | 80 | render() { 81 | const back = '<- Back to notes'; 82 | const enableSave = ((this.state.title !== '') && 83 | (this.state.title !== this.props.title) && 84 | !this.state.awaitingTitleChange); 85 | return ( 86 |
87 |
88 |

89 | {back} 90 |

91 |

92 | Settings 93 |

94 |
95 |
96 |

Delete Notebook

97 |

98 | Permanently delete this notebook 99 |

100 | 103 |
104 |
105 |

Rename

106 |

107 | Change the name of this notebook 108 |

109 |

Notebook Name

110 | { 112 | this.titleInput = el; 113 | }} 114 | style={{ marginBottom:8 }} 115 | placeholder={this.props.title} 116 | onChange={this.titleChange} 117 | disabled={this.state.awaitingTitleChange} 118 | /> 119 | 120 |
121 |
122 |
123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/lib/stack-subs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class StackSubs extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | const back = '<- Back to notes'; 10 | 11 | const subscribers = this.props.subs.map((sub, i) => { 12 | return ( 13 |
14 |

~{sub}

15 |
16 | ); 17 | }); 18 | 19 | subscribers.unshift( 20 |
21 |

~{window.ship}

22 |

Host (You)

23 |
24 | ); 25 | 26 | return ( 27 |
28 |
29 |

30 | {back} 31 |

32 |

33 | Manage Notebook 34 |

35 |
36 |
37 |

Members

38 |

41 | Everyone subscribed to this notebook 42 |

43 | {subscribers} 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/title-snippet.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class TitleSnippet extends Component { 4 | constructor(props) { 5 | super(props); 6 | } 7 | 8 | render() { 9 | if (this.props.badge) { 10 | return ( 11 |
14 | {this.props.title} 15 |
16 | ); 17 | } else { 18 | return ( 19 |
22 | {this.props.title} 23 |
24 | ); 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | export function stringToSymbol(str) { 3 | let result = ''; 4 | for (var i=0; i= 97) && (n <= 122) ) || 7 | ( (n >= 48) && (n <= 57) )) 8 | { 9 | result += str[i]; 10 | } else if ( (n >= 65) && (n <= 90) ) 11 | { 12 | result += String.fromCharCode(n + 32); 13 | } else { 14 | result += '-'; 15 | } 16 | } 17 | result = result.replace(/^[\-\d]+|\-+/g, '-'); 18 | result = result.replace(/^\-+|\-+$/g, ''); 19 | if (result === ''){ 20 | return dateToDa(new Date()); 21 | } 22 | return result; 23 | } 24 | 25 | 26 | export function uuid() { 27 | let str = "0v" 28 | str += Math.ceil(Math.random()*8)+"-" 29 | for (var i = 0; i < 5; i++) { 30 | let _str = Math.ceil(Math.random()*10000000).toString(32); 31 | _str = ("00000"+_str).substr(-5,5); 32 | str += _str+"-"; 33 | } 34 | 35 | return str.slice(0,-1); 36 | } 37 | 38 | export function isPatTa(str) { 39 | const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str) 40 | return !!r; 41 | } 42 | 43 | /* 44 | Goes from: 45 | ~2018.7.17..23.15.09..5be5 // urbit @da 46 | To: 47 | (javascript Date object) 48 | */ 49 | export function daToDate(st) { 50 | var dub = function(n) { 51 | return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString(); 52 | }; 53 | var da = st.split('..'); 54 | var bigEnd = da[0].split('.'); 55 | var lilEnd = da[1].split('.'); 56 | var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`; 57 | return new Date(ds); 58 | } 59 | 60 | /* 61 | Goes from: 62 | (javascript Date object) 63 | To: 64 | ~2018.7.17..23.15.09..5be5 // urbit @da 65 | */ 66 | 67 | export function dateToDa(d, mil) { 68 |   var fil = function(n) { 69 |     return n >= 10 ? n : "0" + n; 70 |   }; 71 |   return ( 72 |     `~${d.getUTCFullYear()}.` + 73 |     `${(d.getUTCMonth() + 1)}.` + 74 |     `${fil(d.getUTCDate())}..` + 75 |     `${fil(d.getUTCHours())}.` + 76 |     `${fil(d.getUTCMinutes())}.` + 77 |     `${fil(d.getUTCSeconds())}` + 78 | `${mil ? "..0000" : ""}` 79 |   ); 80 | } 81 | 82 | export function deSig(ship) { 83 | return ship.replace('~', ''); 84 | } 85 | 86 | // trim patps to match dojo, chat-cli 87 | export function cite(ship) { 88 | let patp = ship, shortened = ""; 89 | if (patp.startsWith("~")) { 90 | patp = patp.substr(1); 91 | } 92 | // comet 93 | if (patp.length === 56) { 94 | shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56); 95 | return shortened; 96 | } 97 | // moon 98 | if (patp.length === 27) { 99 | shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27); 100 | return shortened; 101 | } 102 | return `~${patp}`; 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/reducers/config.js: -------------------------------------------------------------------------------- 1 | export class ConfigReducer { 2 | reduce(json, state) { 3 | const data = json.srrs || false; 4 | if (data) { 5 | state.inbox = data.inbox; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/reducers/initial.js: -------------------------------------------------------------------------------- 1 | export class InitialReducer { 2 | reduce(json, state) { 3 | const data = json.initial || false; 4 | if (data) { 5 | state.inbox = data.inbox; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/reducers/learn.js: -------------------------------------------------------------------------------- 1 | export class LearnReducer { 2 | reduce(json, state) { 3 | const type = json.type || false; 4 | if (type == 'learn') { 5 | state.pubs[json.stack].items[json.item].learn = json.data 6 | state.learn = json.data 7 | } 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/reducers/primary.js: -------------------------------------------------------------------------------- 1 | export class PrimaryReducer { 2 | reduce(json, state) { 3 | switch(Object.keys(json)[0]) { 4 | case 'add-item': 5 | this.addItem(json['add-item'], state); 6 | break; 7 | case 'add-stack': 8 | this.addStack(json['add-stack'], state); 9 | break; 10 | case 'update-review': 11 | this.updateReview(json['update-review'], state); 12 | break; 13 | case 'add-review-item': 14 | this.addRaisedItem(json['add-review-item'], state); 15 | break; 16 | case 'delete-review-item': 17 | this.deleteReviewItem(json['delete-review-item'], state); 18 | break; 19 | case 'delete-stack': 20 | this.deleteStack(json['delete-stack'], state); 21 | break; 22 | case 'update-stack': 23 | this.addStack(json['update-stack'], state); 24 | default: 25 | break; 26 | } 27 | } 28 | 29 | addItem(json, state) { 30 | const host = Object.keys(json)[0]; 31 | const stack = Object.keys(json[host])[0]; 32 | const noteId = json[host][stack].name; 33 | if (state.pubs && state.pubs[stack]) { 34 | if (state.pubs[stack].items) { 35 | state.pubs[stack].items[noteId] = json[host][stack]; 36 | } else { 37 | state.pubs[stack].items = { [noteId]: json[host][stack] }; 38 | } 39 | } 40 | } 41 | addStack(json, state) { 42 | const host = Object.keys(json)[0]; 43 | const stack = Object.keys(json[host])[0]; 44 | if (state.pubs) { 45 | state.pubs[stack] = json[host][stack]; 46 | } 47 | } 48 | deleteStack(json, state) { 49 | const host = json['who'].slice(1); 50 | if (state.subs[host]) { 51 | delete state.subs[host][json['stack']]; 52 | } else if (state.pubs) { 53 | if (state.pubs[json['stack']].info.owner === host) { 54 | delete state.pubs[json['stack']]; 55 | } 56 | } 57 | } 58 | 59 | updateReview(json, state) { 60 | state.review=json; 61 | } 62 | 63 | addRaisedItem(json, state) { 64 | state.review.push(json); 65 | } 66 | deleteReviewItem(json, state) { 67 | const idx = state.review.findIndex(item => ((item.who === json.who.slice(1)) || (item.who === json.who)) && (item.stack === json.stack) && (item.item === json.item)); 68 | if (idx === -1) { 69 | return state.review; 70 | } else { 71 | state.review.splice( idx, 1 ); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/reducers/response.js: -------------------------------------------------------------------------------- 1 | export class ResponseReducer { 2 | reduce(json, state) { 3 | switch(json.type) { 4 | case 'local': 5 | this.sidebarToggle(json, state); 6 | this.setSelected(json, state); 7 | break; 8 | default: 9 | break; 10 | } 11 | } 12 | 13 | sidebarToggle(json, state) { 14 | if (Object.prototype.hasOwnProperty.call(json.data, 'sidebarToggle')) { 15 | state.sidebarShown = json.data.sidebarToggle; 16 | } 17 | } 18 | 19 | setSelected(json, state) { 20 | if (Object.prototype.hasOwnProperty.call(json.data, 'selected')) { 21 | state.selectedGroups = json.data.selected; 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/reducers/update.js: -------------------------------------------------------------------------------- 1 | export class UpdateReducer { 2 | reduce(json, state) { 3 | const data = json.update || false; 4 | if (data) { 5 | this.reduceInbox(data.inbox || false, state); 6 | } 7 | } 8 | 9 | reduceInbox(inbox, state) { 10 | if (inbox) { 11 | state.inbox = inbox; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { InitialReducer } from '~/reducers/initial'; 2 | import { ConfigReducer } from '~/reducers/config'; 3 | import { UpdateReducer } from '~/reducers/update'; 4 | import { LearnReducer } from '~/reducers/learn'; 5 | import { PrimaryReducer } from '~/reducers/primary'; 6 | 7 | 8 | class Store { 9 | constructor() { 10 | this.state = { 11 | spinner: false, 12 | ...window.injectedState, 13 | }; 14 | 15 | this.initialReducer = new InitialReducer(); 16 | this.configReducer = new ConfigReducer(); 17 | this.updateReducer = new UpdateReducer(); 18 | this.primaryReducer = new PrimaryReducer(); 19 | this.learnReducer = new LearnReducer(); 20 | this.setState = () => { }; 21 | } 22 | 23 | setStateHandler(setState) { 24 | this.setState = setState; 25 | } 26 | 27 | handleEvent(data) { 28 | let json = data.data; 29 | this.initialReducer.reduce(json, this.state); 30 | this.configReducer.reduce(json, this.state); 31 | this.updateReducer.reduce(json, this.state); 32 | this.primaryReducer.reduce(json, this.state); 33 | this.learnReducer.reduce(data, this.state); 34 | 35 | this.setState(this.state); 36 | } 37 | } 38 | 39 | export let store = new Store(); 40 | window.store = store; 41 | -------------------------------------------------------------------------------- /src/subscription.js: -------------------------------------------------------------------------------- 1 | import { api } from '~/api'; 2 | import { store } from '~/store'; 3 | 4 | import urbitOb from 'urbit-ob'; 5 | 6 | 7 | export class Subscription { 8 | start() { 9 | if (api.authTokens) { 10 | this.initializesrrs(); 11 | } else { 12 | console.error("~~~ ERROR: Must set api.authTokens before operation ~~~"); 13 | } 14 | } 15 | 16 | initializesrrs() { 17 | api.bind('/srrs-primary', 'PUT', api.authTokens.ship, 'srrs', 18 | this.handleEvent.bind(this), 19 | this.handleError.bind(this)); 20 | } 21 | 22 | handleEvent(diff) { 23 | store.handleEvent(diff); 24 | } 25 | 26 | handleError(err) { 27 | console.error(err); 28 | api.bind('/srrs-primary', 'PUT', api.authTokens.ship, 'srrs', 29 | this.handleEvent.bind(this), 30 | this.handleError.bind(this)); 31 | } 32 | } 33 | 34 | export let subscription = new Subscription(); 35 | -------------------------------------------------------------------------------- /srrs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/srrs.gif -------------------------------------------------------------------------------- /tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/tile.png -------------------------------------------------------------------------------- /tile/tile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classnames from 'classnames'; 3 | import _ from 'lodash'; 4 | 5 | 6 | export default class SrrsTile extends Component { 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | render(){ 12 | let info = []; 13 | if (this.props.data.review > 0) { 14 | let text = (this.props.data.review == 1) 15 | ? "Review" 16 | : "Reviews" 17 | info.push( 18 |

19 | {this.props.data.review} 20 | {text} 21 |

22 | ); 23 | } 24 | 25 | return ( 26 |
28 | 29 |

srrs

30 |
31 |
33 | {info} 34 |
35 |
36 | 37 | ); 38 | } 39 | 40 | } 41 | 42 | window.srrsTile = SrrsTile; 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "noFallthroughCasesInSwitch": true, 5 | "noUnusedParameters": false, 6 | "noImplicitReturns": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "noUnusedLocals": false, 10 | "noImplicitAny": false, 11 | "allowJs": true, 12 | "noEmit": true, 13 | "target": "es2015", 14 | "module": "es2015", 15 | "strict": true, 16 | "jsx": "react", 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": ["src/*"] 20 | } 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /urbit/app/srrs-cli.hoon: -------------------------------------------------------------------------------- 1 | /- *srrs, chat-store, *chat-view, *chat-hook, 2 | *group-store, *invite-store, sole 3 | /+ *srrs, *srrs-json, default-agent, verb, dbug, 4 | auto=language-server-complete, shoe 5 | :: 6 | |% 7 | +$ card card:shoe 8 | :: 9 | +$ versioned-state 10 | $% state-0 11 | == 12 | :: 13 | +$ state-0 14 | $: audience=(set target) :: active targets 15 | width=@ud :: display width 16 | eny=@uvJ :: entropy 17 | == 18 | :: 19 | +$ target [in-group=? =ship =path] 20 | :: 21 | +$ command 22 | $% [%target (set target)] :: set messaging target 23 | [%say letter:chat-store] :: send message 24 | [%width @ud] :: display width 25 | [%help ~] :: print usage info 26 | [%all-reviews ~] 27 | [%delete-item @tas @t] 28 | [%delete-stack @p @t] 29 | [%import @p @t] 30 | [%copy-stack @p @t ?] 31 | [%import-file path] 32 | [%settings ~] 33 | == 34 | :: 35 | -- 36 | =| state-0 37 | =* state - 38 | :: 39 | %- agent:dbug 40 | %+ verb | 41 | ^- agent:gall 42 | %- (agent:shoe command) 43 | ^- (shoe:shoe command) 44 | =< 45 | |_ =bowl:gall 46 | +* this . 47 | srrs-core +> 48 | sc ~(. srrs-core(eny eny.bowl) bowl) 49 | def ~(. (default-agent this %|) bowl) 50 | des ~(. (default:shoe this command) bowl) 51 | :: 52 | ++ on-init 53 | ^- (quip card _this) 54 | =^ cards state (prep:sc ~) 55 | [cards this] 56 | :: 57 | ++ on-save !>(state) 58 | :: 59 | ++ on-load 60 | |= old-state=vase 61 | ^- (quip card _this) 62 | =/ old !<(versioned-state old-state) 63 | =^ cards state (prep:sc `old) 64 | [cards this] 65 | :: 66 | ++ on-poke 67 | |= [=mark =vase] 68 | ^- (quip card _this) 69 | =^ cards state 70 | ?+ mark (on-poke:def mark vase) 71 | %noun (poke-noun:sc !<(* vase)) 72 | == 73 | [cards this] 74 | :: 75 | ++ on-watch on-watch:def 76 | ++ on-leave on-leave:def 77 | ++ on-peek on-peek:def 78 | ++ on-agent 79 | |= [=wire =sign:agent:gall] 80 | ^- (quip card _this) 81 | =^ cards state 82 | ?- -.sign 83 | %poke-ack [- state]:(on-agent:def wire sign) 84 | %watch-ack [- state]:(on-agent:def wire sign) 85 | :: 86 | %kick 87 | :_ state 88 | ?+ wire ~ 89 | [%srrs ~] ~[connect:sc] 90 | == 91 | :: 92 | %fact 93 | ?+ p.cage.sign ~|([%srrs-cli-bad-sub-mark wire p.cage.sign] !!) 94 | %srrs-primary-delta 95 | (handle-delta:sc wire !<(primary-delta q.cage.sign)) 96 | == 97 | == 98 | [cards this] 99 | :: 100 | ++ on-arvo on-arvo:def 101 | :: 102 | ++ on-fail on-fail:def 103 | ++ command-parser 104 | |= sole-id=@ta 105 | parser:sh:sc 106 | :: 107 | ++ tab-list 108 | |= sole-id=@ta 109 | %+ turn tab-list:sh:sc 110 | |= [term=cord detail=tank] 111 | [(cat 3 ';' term) detail] 112 | :: 113 | ++ on-command 114 | |= [sole-id=@ta =command] 115 | =^ cards state 116 | (work:sh:sc command) 117 | [cards this] 118 | :: 119 | ++ on-connect 120 | |= sole-id=@ta 121 | ^- (quip card _this) 122 | [[prompt:sh-out:sc ~] this] 123 | :: 124 | ++ can-connect can-connect:des 125 | ++ on-disconnect on-disconnect:des 126 | -- 127 | :: 128 | |_ =bowl:gall 129 | :: +prep: setup & state adapter 130 | :: 131 | ++ prep 132 | |= old=(unit versioned-state) 133 | ^- (quip card _state) 134 | ?~ old 135 | =^ cards state 136 | :- ~[connect] 137 | %_ state 138 | audience [[| our-self /srrs] ~ ~] 139 | width 80 140 | == 141 | [cards state] 142 | [~ state(width 80, audience [[| our-self /srrs] ~ ~])] 143 | :: +connect: connect to srrs 144 | :: 145 | ++ connect 146 | ^- card 147 | [%pass /srrs %agent [our-self %srrs] %watch /srrs-primary] 148 | :: 149 | ++ our-self our.bowl 150 | :: +target-to-path: prepend ship to the path 151 | :: 152 | ++ target-to-path 153 | |= target 154 | ^- ^path 155 | %+ weld 156 | ?:(in-group ~ /~) 157 | [(scot %p ship) path] 158 | :: +poke-noun: debug helpers 159 | :: 160 | ++ poke-noun 161 | |= a=* 162 | ^- (quip card _state) 163 | ?: ?=(%connect a) 164 | [[connect ~] state] 165 | [~ state] 166 | :: +handle-delta: casts primary-delta to something printable 167 | :: 168 | ++ handle-delta 169 | |= [=wire del=primary-delta] 170 | ^- (quip card _state) 171 | =/ [wir=^wire mark=@tas] 172 | ?+ -.del [wire %txt] 173 | %add-review-item [/[-.wire]/chat %letter] 174 | %add-item [/[-.wire]/chat %letter] 175 | == 176 | =/ cay=cage [%srrs-primary-delta !>(del)] 177 | =+ .^(=tube:clay %cc /(scot %p our.bowl)/home/(scot %da now.bowl)/[p.cay]/[mark]) 178 | =/ =cage [mark (tube q.cay)] 179 | ?+ wir [~ state] 180 | [%srrs ~] (handle-srrs cage) 181 | [%srrs %chat ~] (handle-srrs-chat cage) 182 | == 183 | :: +handle-srrs: handle updates from the /srrs-primary wire 184 | :: 185 | ++ handle-srrs 186 | |= =cage 187 | ^- (quip card _state) 188 | [[(show-result:sh-out cage) ~] state] 189 | :: +handle-srrs-chat: handle updates and send to chat 190 | :: 191 | ++ handle-srrs-chat 192 | |= =cage 193 | ^- (quip card _state) 194 | ~! q.cage 195 | =^ say-cards state (work:sh [%say !<(letter:chat-store q.cage)]) 196 | [say-cards state] 197 | :: 198 | :: +sh: handle user input 199 | :: 200 | ++ sh 201 | |% 202 | :: +parser: command parser 203 | :: 204 | :: parses the command line buffer. 205 | :: produces commands which can be executed by +work. 206 | :: 207 | ++ parser 208 | |^ 209 | %+ stag | 210 | %+ knee *command |. ~+ 211 | =- ;~(pfix mic -) 212 | ;~ pose 213 | (stag %target tars) 214 | ;~(plug (tag %help) (easy ~)) 215 | ;~(plug (tag %all-reviews) (easy ~)) 216 | ;~((glue ace) (tag %delete-item) sym qut) 217 | ;~((glue ace) (tag %delete-stack) ship qut) 218 | ;~((glue ace) (tag %import) ship qut) 219 | ;~((glue ace) (tag %copy-stack) ship qut bool) 220 | ;~((glue ace) (tag %import-file) file-path) 221 | ;~(plug (tag %settings) (easy ~)) 222 | == 223 | :: 224 | ++ tag |*(a=@tas (cold a (jest a))) 225 | ++ bool 226 | ;~ pose 227 | (cold %| (jest '%.y')) 228 | (cold %& (jest '%.n')) 229 | == 230 | ++ ship ;~(pfix sig fed:ag) 231 | ++ path ;~(pfix fas ;~(plug urs:ab (easy ~))) ::NOTE short only, tmp 232 | ++ file-path ;~(pfix fas (more fas (cook crip (star ;~(less fas prn))))) 233 | :: +mang: un/managed indicator prefix 234 | :: 235 | ++ mang 236 | ;~ pose 237 | (cold %| (jest '~/')) 238 | (cold %& (easy ~)) 239 | == 240 | :: +tarl: local target, as /path 241 | :: 242 | ++ tarl (stag our-self path) 243 | :: +tarx: local target, maybe managed 244 | :: 245 | ++ tarx ;~(plug mang path) 246 | :: +tarp: sponsor target, as ^/path 247 | :: 248 | ++ tarp 249 | =- ;~(pfix ket (stag - path)) 250 | (sein:title our.bowl now.bowl our-self) 251 | :: +targ: any target, as tarl, tarp, ~ship/path 252 | :: 253 | ++ targ 254 | ;~ plug 255 | mang 256 | :: 257 | ;~ pose 258 | tarl 259 | tarp 260 | ;~(plug ship path) 261 | == 262 | == 263 | :: +tars: set of comma-separated targs 264 | :: 265 | ++ tars 266 | %+ cook ~(gas in *(set target)) 267 | (most ;~(plug com (star ace)) targ) 268 | :: +ships: set of comma-separated ships 269 | :: 270 | :: +ships: set of comma-separated ships 271 | :: 272 | ++ ships 273 | %+ cook ~(gas in *(set ^ship)) 274 | (most ;~(plug com (star ace)) ship) 275 | :: +text: text message body 276 | :: 277 | ++ text 278 | %+ cook crip 279 | (plus next) 280 | -- 281 | :: +tab-list: static list of autocomplete entries 282 | :: 283 | ++ tab-list 284 | ^- (list [@t tank]) 285 | :~ 286 | [%help leaf+";help"] 287 | [%all-reviews leaf+";all-reviews"] 288 | [%delete-item leaf+";delete-item [stack-name] [item-id]"] 289 | [%delete-stack leaf+";delete-stack [stack-name]"] 290 | [%import leaf+";import [who (@p)] [stack-name]"] 291 | [%copy-stack leaf+";copy-stack [who (@p)] [stack-name] [keep-learned] (add subscribed stacks to main library)"] 292 | [%import-file leaf+";import-file [path to tab separated file]"] 293 | [%settings leaf+";settings"] 294 | == 295 | :: +work: run user command 296 | :: 297 | ++ work 298 | |= job=command 299 | ^- (quip card _state) 300 | |^ ?- -.job 301 | %target (set-target +.job) 302 | %say (say +.job) 303 | %width (set-width +.job) 304 | %help help 305 | %all-reviews all-reviews 306 | %delete-item (delete-item +.job) 307 | %delete-stack (delete-stack +.job) 308 | %import (import +.job) 309 | %copy-stack (copy-stack +.job) 310 | %import-file (import-file +.job) 311 | %settings show-settings 312 | == 313 | :: +act: build action card 314 | :: 315 | ++ act 316 | |= [what=term app=term =cage] 317 | ^- card 318 | :* %pass 319 | /cli-command/[what] 320 | %agent 321 | [our-self app] 322 | %poke 323 | cage 324 | == 325 | :: +set-target: set audience 326 | :: 327 | ++ set-target 328 | |= tars=(set target) 329 | ^- (quip card _state) 330 | =. audience tars 331 | [~ state] 332 | :: +say: send messages to srrs chat 333 | :: 334 | ++ say 335 | |= =letter:chat-store 336 | ^- (quip card _state) 337 | =/ =serial (shaf %msg-uid eny.bowl) 338 | :_ state(eny (shax eny.bowl)) 339 | ^- (list card) 340 | %+ turn ~(tap in audience) 341 | |= =target 342 | %^ act %out-message %chat-hook 343 | :- %chat-action 344 | !> ^- action:chat-store 345 | :+ %message (target-to-path target) 346 | [serial *@ our-self now.bowl letter] 347 | :: 348 | :: +show-settings: print enabled flags, timezone and width settings 349 | :: 350 | ++ show-settings 351 | ^- (quip card _state) 352 | :_ state 353 | =/ targets 354 | %+ turn ~(tap in audience) 355 | |= =target ~& target+target ~ 356 | :~ (print:sh-out "width: {(scow %ud width)}") 357 | == 358 | :: 359 | ++ delete-item 360 | |= [stack=@tas item=@t] 361 | ^- (quip card _state) 362 | =- [[- ~] state] 363 | %^ act %delete-item %srrs 364 | :- %srrs-action 365 | !> ^- action 366 | [%delete-item stack item] 367 | :: 368 | ++ delete-stack 369 | |= [who=@p stack=@t] 370 | ^- (quip card _state) 371 | =- [[- ~] state] 372 | %^ act %delete-stack %srrs 373 | :- %srrs-action 374 | !> ^- action 375 | [%delete-stack who (string-to-symbol (trip stack))] 376 | :: 377 | ++ import 378 | |= [who=@p stack=@t] 379 | ^- (quip card _state) 380 | =- [[- ~] state] 381 | %^ act %import %srrs 382 | :- %srrs-action 383 | !> ^- action 384 | [%import who (string-to-symbol (trip stack))] 385 | :: 386 | ++ copy-stack 387 | |= [who=@p stack=@t keep-learned=?] 388 | ^- (quip card _state) 389 | =- [[- ~] state] 390 | %^ act %copy-stack %srrs 391 | :- %srrs-action 392 | !> ^- action 393 | [%copy-stack who (string-to-symbol (trip stack)) keep-learned] 394 | :: 395 | ++ import-file 396 | |= =path 397 | ^- (quip card _state) 398 | =- [[- ~] state] 399 | %^ act %import-file %srrs 400 | :- %srrs-action 401 | !> ^- action 402 | [%import-file path] 403 | :: 404 | :: +set-width: configure cli printing width 405 | :: 406 | ++ set-width 407 | |= w=@ud 408 | [~ state(width w)] 409 | :: +all-reviews: show items needing review 410 | :: 411 | ++ all-reviews 412 | ^- (quip card _state) 413 | =, html 414 | =/ reviews (scry-for (list review) %srrs /review) 415 | =/ json :- %a 416 | %+ turn 417 | reviews 418 | review-to-json 419 | =/ print-card=card (print:sh-out "review: {(en-json json)}") 420 | =^ say-cards state 421 | (say `letter:chat-store`[%text (crip "review: {(en-json json)}")]) 422 | [(flop (snoc say-cards print-card)) state] 423 | :: 424 | :: +help: print (link to) usage instructions 425 | :: 426 | ++ help 427 | ^- (quip card _state) 428 | =- [[- ~] state] 429 | (print:sh-out "see https://github.com/ryjm/srrs") 430 | -- 431 | -- 432 | :: 433 | :: +sh-out: output to the cli 434 | :: 435 | ++ sh-out 436 | |% 437 | :: +effect: console effect card 438 | :: 439 | ++ effect 440 | |= effect=sole-effect:sole 441 | ^- card 442 | [%shoe ~ %sole effect] 443 | :: +print: puts some text into the cli as-is 444 | :: 445 | ++ print 446 | |= txt=tape 447 | ^- card 448 | (effect %txt txt) 449 | :: +print-more: puts lines of text into the cli 450 | :: 451 | ++ print-more 452 | |= txs=(list tape) 453 | ^- card 454 | %+ effect %mor 455 | (turn txs |=(t=tape [%txt t])) 456 | :: +note: prints left-padded ---| txt 457 | :: 458 | ++ note 459 | |= txt=tape 460 | ^- card 461 | =+ lis=(simple-wrap txt (sub width 16)) 462 | %- print-more 463 | =+ ?:((gth (lent lis) 0) (snag 0 lis) "") 464 | :- (runt [14 '-'] '|' ' ' -) 465 | %+ turn (slag 1 lis) 466 | |=(a=tape (runt [14 ' '] '|' ' ' a)) 467 | :: +prompt: update prompt to display current audience 468 | :: 469 | ++ prompt 470 | ^- card 471 | %+ effect %pro 472 | :+ & %srrs-line 473 | ^- tape 474 | ">" 475 | :: 476 | ++ show-result 477 | |= =cage 478 | ^- card 479 | =/ typ p.cage 480 | =/ =vase q.cage 481 | (note "result: {(noah vase)}") 482 | :: 483 | -- 484 | :: 485 | ++ simple-wrap 486 | |= [txt=tape wid=@ud] 487 | ^- (list tape) 488 | ?~ txt ~ 489 | =/ [end=@ud nex=?] 490 | ?: (lte (lent txt) wid) [(lent txt) &] 491 | =+ ace=(find " " (flop (scag +(wid) `tape`txt))) 492 | ?~ ace [wid |] 493 | [(sub wid u.ace) &] 494 | :- (tufa (scag end `(list @)`txt)) 495 | $(txt (slag ?:(nex +(end) end) `tape`txt)) 496 | :: 497 | ::NOTE anything that uses this breaks moons support, because moons don't sync 498 | :: full app state rn 499 | ++ scry-for 500 | |* [=mold app=term =path] 501 | .^ mold 502 | %gx 503 | (scot %p our.bowl) 504 | app 505 | (scot %da now.bowl) 506 | (snoc `^path`path %noun) 507 | == 508 | -- 509 | -------------------------------------------------------------------------------- /urbit/app/srrs/img/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/urbit/app/srrs/img/Home.png -------------------------------------------------------------------------------- /urbit/app/srrs/img/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/urbit/app/srrs/img/arrow.png -------------------------------------------------------------------------------- /urbit/app/srrs/img/srrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryjm/srrs/b75d7c0176963f7d65a743fd6a5d17d942486e36/urbit/app/srrs/img/srrs.png -------------------------------------------------------------------------------- /urbit/app/srrs/index.hoon: -------------------------------------------------------------------------------- 1 | |= inject=json 2 | ^- manx 3 | ;html 4 | :: 5 | ;head 6 | ;title: Srrs 7 | ;meta(charset "utf-8"); 8 | ;meta 9 | =name "viewport" 10 | =content "width=device-width, initial-scale=1, shrink-to-fit=no"; 11 | ;link(rel "stylesheet", href "/~srrs-files/css/index.css"); 12 | ;link(rel "icon", type "image/png", href "/~launch/img/Favicon.png"); 13 | ;script@"/~landscape/js/channel.js"; 14 | ;script@"/~landscape/js/session.js"; 15 | ;script: window.injectedState = {(en-json:html inject)} 16 | == 17 | :: 18 | ;body 19 | ;div#root; 20 | ;script@"/~srrs-files/js/index.js"; 21 | == 22 | == 23 | -------------------------------------------------------------------------------- /urbit/gen/memo.hoon: -------------------------------------------------------------------------------- 1 | :: Tell app to hook into memo apps 2 | :: 3 | :: For apps that use lib/memo, :app +memo toggles memo hook. 4 | :: 5 | :- %say 6 | |= [* arg=?(~ [%bowl ~]) ~] 7 | [%memo ?~(arg %enabled %bowl)] 8 | -------------------------------------------------------------------------------- /urbit/lib/memo.hoon: -------------------------------------------------------------------------------- 1 | /- srrs 2 | |= [enabled=? =agent:gall] 3 | =| bowl-print=_| 4 | ^- agent:gall 5 | |^ 6 | |_ =bowl:gall 7 | +* this . 8 | ag ~(. agent bowl) 9 | :: 10 | ++ on-init 11 | ^- (quip card:agent:gall agent:gall) 12 | %- (print bowl "{}: on-init") 13 | =^ cards agent on-init:ag 14 | [cards this] 15 | :: 16 | ++ on-save 17 | ^- vase 18 | %- (print bowl "{}: on-save") 19 | on-save:ag 20 | :: 21 | ++ on-load 22 | |= old-state=vase 23 | ^- (quip card:agent:gall agent:gall) 24 | %- (print bowl "{}: on-load") 25 | =^ cards agent (on-load:ag old-state) 26 | [cards this] 27 | :: 28 | ++ on-poke 29 | |= [=mark =vase] 30 | ^- (quip card:agent:gall agent:gall) 31 | %- (print bowl "{}: on-poke with mark {}") 32 | ?: ?=(%memo mark) 33 | ?- !<(?(%enabled %bowl) vase) 34 | %enabled `this(enabled !enabled) 35 | %bowl `this(bowl-print !bowl-print) 36 | == 37 | =^ cards agent (on-poke:ag mark vase) 38 | =/ publish-state on-save:ag 39 | =/ books (slop !>(%add-books) (slap publish-state [%limb %books])) 40 | =/ memo-cards=(list card:agent:gall) 41 | :~ 42 | [%pass /memo %agent [our.bowl %srrs] %poke %srrs-action books] 43 | == 44 | [(weld memo-cards cards) this] 45 | :: 46 | ++ on-watch 47 | |= =path 48 | ^- (quip card:agent:gall agent:gall) 49 | %- (print bowl "{}: on-watch on path {}") 50 | =^ cards agent (on-watch:ag path) 51 | [cards this] 52 | :: 53 | ++ on-leave 54 | |= =path 55 | ^- (quip card:agent:gall agent:gall) 56 | %- (print bowl "{}: on-leave on path {}") 57 | =^ cards agent (on-leave:ag path) 58 | [cards this] 59 | :: 60 | ++ on-peek 61 | |= =path 62 | ^- (unit (unit cage)) 63 | %- (print bowl "{}: on-peek on path {}") 64 | (on-peek:ag path) 65 | :: 66 | ++ on-agent 67 | |= [=wire =sign:agent:gall] 68 | ^- (quip card:agent:gall agent:gall) 69 | %- (print bowl "{}: on-agent on wire {}, {<-.sign>}") 70 | =^ cards agent (on-agent:ag wire sign) 71 | [cards this] 72 | :: 73 | ++ on-arvo 74 | |= [=wire =sign-arvo] 75 | ^- (quip card:agent:gall agent:gall) 76 | %- (print bowl "{}: on-arvo on wire {}, {<[- +<]:sign-arvo>}") 77 | %- (print bowl "{}: state: {}") 78 | =^ cards agent (on-arvo:ag wire sign-arvo) 79 | =/ publish-state on-save:ag 80 | =/ books=vase 81 | (slop !>(%add-books) (slap publish-state [%limb %books])) 82 | =/ memo-cards=(list card:agent:gall) 83 | :~ 84 | [%pass /memo %agent [our.bowl %srrs] %poke %srrs-action books] 85 | == 86 | [(weld cards memo-cards) this] 87 | 88 | :: 89 | ++ on-fail 90 | |= [=term =tang] 91 | ^- (quip card:agent:gall agent:gall) 92 | %- (print bowl "{}: on-fail with term {}") 93 | =^ cards agent (on-fail:ag term tang) 94 | [cards this] 95 | -- 96 | :: 97 | ++ print 98 | |= [=bowl:gall =tape] 99 | ^+ same 100 | =? . bowl-print 101 | %- (slog >bowl< ~) 102 | . 103 | ?. enabled same 104 | %- (slog leaf+tape ~) 105 | same 106 | -- 107 | -------------------------------------------------------------------------------- /urbit/lib/srrs-json.hoon: -------------------------------------------------------------------------------- 1 | /- *srrs 2 | /+ *srrs 3 | |% 4 | ++ primary-delta-to-json 5 | |= del=primary-delta 6 | ^- json 7 | =, enjs:format 8 | %+ frond -.del 9 | ?- -.del 10 | %add-item 11 | %+ frond (scot %p who.del) 12 | %+ frond stack.del 13 | (item-to-json data.del) 14 | %add-review-item 15 | %- pairs 16 | :~ who+s+(scot %p who.del) 17 | stack+s+stack.del 18 | item+s+name.data.del 19 | == 20 | %add-stack 21 | %+ frond (scot %p who.del) 22 | %+ frond name.data.del 23 | (total-build-to-json data.del) 24 | %delete-stack 25 | %- pairs 26 | :~ who+s+(scot %p who.del) 27 | stack+s+stack.del 28 | == 29 | %delete-item 30 | %- pairs 31 | :~ who+s+(scot %p who.del) 32 | stack+s+stack.del 33 | item+s+item.del 34 | == 35 | %delete-review-item 36 | %- pairs 37 | :~ who+s+(scot %p who.del) 38 | stack+s+stack.del 39 | item+s+item.del 40 | == 41 | %update-review 42 | :- %a 43 | %+ turn 44 | ~(tap in +.del) 45 | review-to-json 46 | %update-stack 47 | %+ frond (scot %p who.del) 48 | %+ frond name.data.del 49 | (total-build-to-json data.del) 50 | %read 51 | %- pairs 52 | :~ who+s+(scot %p who.del) 53 | stack+s+stack.del 54 | item+s+item.del 55 | == 56 | == 57 | :: 58 | ++ json-to-action 59 | |= jon=json 60 | =, dejs:format 61 | %- action 62 | =< (srrs-action jon) 63 | |% 64 | ++ srrs-action 65 | %- of 66 | :~ new-stack+new-stack 67 | new-item+new-item 68 | :: 69 | delete-stack+delete-stack 70 | delete-item+delete-item 71 | :: 72 | edit-stack+edit-stack 73 | edit-item+edit-item 74 | :: 75 | schedule-item+schedule-item 76 | raise-item+raise-item 77 | answered-item+answered-item 78 | review-stack+review-stack 79 | :: 80 | subscribe+subscribe 81 | unsubscribe+unsubscribe 82 | :: 83 | read+read 84 | update-review+update-review 85 | == 86 | :: 87 | ++ new-stack 88 | %- ot 89 | :~ name+so 90 | title+so 91 | items+item 92 | edit+edit-config 93 | perm+perm-config 94 | == 95 | :: 96 | ++ new-item 97 | %- ot 98 | :~ stack-owner+(su ;~(pfix sig fed:ag)) 99 | who+(su fed:ag) 100 | stak+so 101 | name+so 102 | title+so 103 | perm+perm-config 104 | front+so 105 | back+so 106 | == 107 | :: 108 | ++ schedule-item 109 | %- ot 110 | :~ stak+so 111 | item+so 112 | scheduled+di 113 | == 114 | :: 115 | ++ raise-item 116 | %- ot 117 | :~ who+(su fed:ag) 118 | stak+so 119 | item+so 120 | == 121 | :: 122 | ++ review-stack 123 | %- ot 124 | :~ who+(su ;~(pfix sig fed:ag)) 125 | stak+so 126 | == 127 | :: 128 | ++ answered-item 129 | %- ot 130 | :~ owner+(su ;~(pfix sig fed:ag)) 131 | stak+so 132 | item+so 133 | answer+recall-grade 134 | == 135 | :: 136 | ++ delete-stack 137 | %- ot 138 | :~ who+(su ;~(pfix sig fed:ag)) 139 | stak+so 140 | == 141 | :: 142 | ++ delete-item 143 | %- ot 144 | :~ stak+so 145 | item+so 146 | == 147 | :: 148 | ++ edit-stack 149 | %- ot 150 | :~ name+so 151 | title+so 152 | == 153 | :: 154 | ++ edit-item 155 | %- ot 156 | :~ who+(su fed:ag) 157 | stak+so 158 | name+so 159 | title+so 160 | perm+perm-config 161 | front+so 162 | back+so 163 | == 164 | :: 165 | ++ recall-grade 166 | %- su 167 | ;~(pose (jest %again) (jest %hard) (jest %good) (jest %easy)) 168 | :: 169 | ++ edit-config 170 | %- su 171 | ;~(pose (jest %item) (jest %all) (jest %none)) 172 | :: 173 | ++ perm-config 174 | %- ot 175 | :~ :- %read 176 | %- ot 177 | :~ mod+(su ;~(pose (jest %black) (jest %white))) 178 | who+whoms 179 | == 180 | :- %write 181 | %- ot 182 | :~ mod+(su ;~(pose (jest %black) (jest %white))) 183 | who+whoms 184 | == == 185 | ++ item 186 | |= jon=json 187 | ~! jon+jon 188 | ?~ jon 189 | ~ 190 | ((om same) jon) 191 | :: 192 | ++ update-review 193 | |= jon=json 194 | ?~ jon 195 | ~ 196 | ((om same) jon) 197 | :: 198 | ++ whoms 199 | |= jon=json 200 | ^- (set whom:clay) 201 | =/ x ((ar (su fed:ag)) jon) 202 | %- (set whom:clay) 203 | %- ~(run in (sy x)) 204 | |=(w=@ [& w]) 205 | :: 206 | ++ invite 207 | %- ot 208 | :~ stak+so 209 | title+so 210 | who+(ar (su fed:ag)) 211 | == 212 | :: 213 | ++ reject-invite 214 | %- ot 215 | :~ who+(su fed:ag) 216 | stak+so 217 | == 218 | :: 219 | ++ serve 220 | %- ot 221 | :~ stak+so 222 | == 223 | :: 224 | ++ unserve 225 | %- ot 226 | :~ stak+so 227 | == 228 | :: 229 | ++ subscribe 230 | %- ot 231 | :~ who+(su fed:ag) 232 | stak+so 233 | == 234 | :: 235 | ++ unsubscribe 236 | %- ot 237 | :~ who+(su fed:ag) 238 | stak+so 239 | == 240 | :: 241 | ++ read 242 | %- ot 243 | :~ who+(su fed:ag) 244 | stak+so 245 | item+so 246 | == 247 | :: 248 | -- 249 | :: 250 | ++ stack-info-to-json 251 | |= con=stack-info 252 | ^- json 253 | %- pairs:enjs:format 254 | :~ :- %owner [%s (scot %p owner.con)] 255 | :- %title [%s title.con] 256 | :- %allow-edit [%s allow-edit.con] 257 | :- %date-created (time:enjs:format date-created.con) 258 | :- %last-modified (time:enjs:format last-modified.con) 259 | :- %filename [%s filename.con] 260 | == 261 | :: 262 | ++ tang-to-json 263 | |= tan=tang 264 | %- wall:enjs:format 265 | %- zing 266 | %+ turn tan 267 | |= a=tank 268 | (wash [0 80] a) 269 | :: 270 | ++ string-to-symbol 271 | |= tap=tape 272 | ^- @tas 273 | %- crip 274 | %+ turn tap 275 | |= a=@ 276 | ?: ?| &((gte a 'a') (lte a 'z')) 277 | &((gte a '0') (lte a '9')) 278 | == 279 | a 280 | ?: &((gte a 'A') (lte a 'Z')) 281 | (add 32 a) 282 | '-' 283 | :: 284 | ++ item-to-json 285 | |= =item 286 | ^- json 287 | %- pairs:enjs:format 288 | :~ content+(content-full-json name.item content.item) 289 | learn+(status-to-json learn.item) 290 | last-review+(maybe last-review.item time:enjs:format) 291 | name+s+name.item 292 | == 293 | :: 294 | ++ stack-build-to-json 295 | |= bud=(each stack-info tang) 296 | ^- json 297 | ?: ?=(%.y -.bud) 298 | (stack-info-to-json +.bud) 299 | (tang-to-json +.bud) 300 | :: 301 | ++ status-to-json 302 | |= status=learned-status 303 | ^- json 304 | %- pairs:enjs:format 305 | :~ :- %ease [%s (scot %rs ease.status)] 306 | :- %interval [%s (scot %dr interval.status)] 307 | :- %box [%s (scot %u box.status)] 308 | == 309 | :: 310 | ++ stack-status-to-json 311 | |= stack=stack 312 | ^- json 313 | :- %o 314 | %+ roll ~(tap in ~(key by items.stack)) 315 | |= [item-name=@tas out=(map @t json)] 316 | =/ =item (~(got by items.stack) item-name) 317 | %+ ~(put by out) 318 | item-name 319 | (status-to-json learn.item) 320 | :: 321 | ++ total-build-to-json 322 | |= stack=stack 323 | ^- json 324 | %- pairs:enjs:format 325 | :~ info+(stack-build-to-json stack.stack) 326 | :: 327 | :+ %review-items 328 | %o 329 | %+ roll ~(tap in ~(key by review-items.stack)) 330 | |= [item=@tas out=(map @t json)] 331 | =/ item-build (~(got by review-items.stack) item) 332 | %+ ~(put by out) 333 | item 334 | (item-to-json item-build) 335 | :+ %items 336 | %o 337 | %+ roll ~(tap in ~(key by items.stack)) 338 | |= [item=@tas out=(map @t json)] 339 | =/ item-build (~(got by items.stack) item) 340 | %+ ~(put by out) 341 | item 342 | (item-to-json item-build) 343 | :: 344 | :- %contributors 345 | %- pairs:enjs:format 346 | :~ mod+s+mod.contributors.stack 347 | :+ %who 348 | %a 349 | %+ turn ~(tap in who.contributors.stack) 350 | |= who=@p 351 | (ship:enjs:format who) 352 | == 353 | :: 354 | :+ %subscribers 355 | %a 356 | %+ turn ~(tap in subscribers.stack) 357 | |= who=@p 358 | ^- json 359 | (ship:enjs:format who) 360 | :: 361 | [%last-update (time:enjs:format last-update.stack)] 362 | == 363 | :: 364 | ++ review-to-json 365 | |= =review 366 | ^- json 367 | %- pairs:enjs:format 368 | :~ who+s+(scot %p who.review) 369 | stack+s+stack.review 370 | item+s+item.review 371 | == 372 | :: 373 | ++ content-full-json 374 | |= [content-name=@tas =content] 375 | ^- json 376 | =, enjs:format 377 | %- pairs 378 | :~ note-id+s+content-name 379 | author+s+(scot %p author.content) 380 | title+s+title.content 381 | date-created+(time date-created.content) 382 | snippet+s+snippet.content 383 | front+s+front.content 384 | back+s+back.content 385 | num-comments+(numb ~(wyt by comments.content)) 386 | comments+(comments-page comments.content 0 50) 387 | read+b+read.content 388 | pending+b+pending.content 389 | == 390 | :: 391 | ++ comments-page 392 | |= [comments=(map @da comment) start=@ud end=@ud] 393 | ^- json 394 | =/ coms=(list [@da comment]) 395 | %+ sort ~(tap by comments) 396 | |= [[d1=@da comment] [d2=@da comment]] 397 | (gte d1 d2) 398 | %- comments-list-json 399 | (scag end (slag start coms)) 400 | :: 401 | ++ comments-list-json 402 | |= comments=(list [@da comment]) 403 | ^- json 404 | =, enjs:format 405 | :- %a 406 | (turn comments comment-json) 407 | :: 408 | ++ comment-json 409 | |= [date=@da com=comment] 410 | ^- json 411 | =, enjs:format 412 | %+ frond:enjs:format 413 | (scot %da date) 414 | %- pairs 415 | :~ author+s+(scot %p author.com) 416 | date-created+(time date-created.com) 417 | content+s+content.com 418 | pending+b+pending.com 419 | == 420 | :: 421 | ++ maybe 422 | |* [unit=(unit) enjs=$-(* json)] 423 | ^- json 424 | ?~ unit ~ 425 | (enjs u.unit) 426 | -- 427 | -------------------------------------------------------------------------------- /urbit/lib/srrs.hoon: -------------------------------------------------------------------------------- 1 | /- *srrs 2 | /+ elem-to-react-json 3 | |% 4 | :: 5 | ++ form-snippet 6 | |= file=@t 7 | ^- @t 8 | =/ front-id (add 3 (need (find ";>" (trip file)))) 9 | =/ front-matter (cat 3 (end [3 front-id] file) 'dummy text\0a') 10 | =/ body (cut 3 [front-id (met 3 file)] file) 11 | (of-wain:format (scag 1 (to-wain:format body))) 12 | :: 13 | ++ add-front-matter 14 | |= [fro=(map knot cord) udon=@t] 15 | ^- @t 16 | %- of-wain:format 17 | =/ tum (trip udon) 18 | =/ id (find ";>" tum) 19 | ?~ id 20 | %+ weld (front-to-wain fro) 21 | (to-wain:format (crip :(weld ";>\0a" tum))) 22 | %+ weld (front-to-wain fro) 23 | (to-wain:format (crip (slag u.id tum))) 24 | :: 25 | ++ front-to-wain 26 | |= a=(map knot cord) 27 | ^- wain 28 | =/ entries=wain 29 | %+ turn ~(tap by a) 30 | |= b=[knot cord] 31 | =/ c=[term cord] (,[term cord] b) 32 | (crip " [{<-.c>} {<+.c>}]") 33 | :: 34 | ?~ entries ~ 35 | ;: weld 36 | [':- :~' ~] 37 | entries 38 | [' ==' ~] 39 | == 40 | :: 41 | ++ time-to-atom 42 | |= time=@d 43 | ^- @ 44 | (yule (yell time)) 45 | :: 46 | ++ time-to-rs 47 | |= time=@d 48 | ^- @rs 49 | (sun:rs (time-to-atom time)) 50 | :: 51 | ++ rs-to-time 52 | |= time=@rs 53 | ^- @dr 54 | (abs:si (need (toi:rs time))) 55 | :: 56 | ++ string-to-symbol 57 | |= tap=tape 58 | ^- @tas 59 | %- crip 60 | %+ turn tap 61 | |= a=@ 62 | ?: ?| &((gte a 'a') (lte a 'z')) 63 | &((gte a '0') (lte a '9')) 64 | == 65 | a 66 | ?: &((gte a 'A') (lte a 'Z')) 67 | (add 32 a) 68 | '-' 69 | -- 70 | -------------------------------------------------------------------------------- /urbit/mar/letter.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/action/srrs/mar 3 | :: 4 | /- *chat-store 5 | =, format 6 | :: 7 | |_ =letter 8 | :: 9 | ++ grab 10 | |% 11 | ++ noun letter 12 | -- 13 | -- 14 | -------------------------------------------------------------------------------- /urbit/mar/srrs/action.hoon: -------------------------------------------------------------------------------- 1 | /? 309 2 | /- *srrs 3 | /+ *srrs-json 4 | =, format 5 | :: 6 | |_ act=action 7 | :: 8 | ++ grow 9 | |% 10 | ++ tank >act< 11 | -- 12 | :: 13 | ++ grab 14 | |% 15 | ++ noun action 16 | ++ json 17 | |= jon=^json 18 | (json-to-action jon) 19 | -- 20 | -- 21 | -------------------------------------------------------------------------------- /urbit/mar/srrs/info.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/info/srrs/mar 3 | :: 4 | /- srrs 5 | !: 6 | |_ stak=stack-info:srrs 7 | :: 8 | :: 9 | ++ grow 10 | |% 11 | ++ mime 12 | :- /text/x-srrs-info 13 | (as-octs:mimes:html (of-wain:format txt)) 14 | ++ txt 15 | ^- wain 16 | :~ (cat 3 'owner: ' (scot %p owner.stak)) 17 | (cat 3 'title: ' title.stak) 18 | (cat 3 'filename: ' filename.stak) 19 | (cat 3 'allow-edit: ' allow-edit.stak) 20 | (cat 3 'date-created: ' (scot %da date-created.stak)) 21 | (cat 3 'last-modified: ' (scot %da last-modified.stak)) 22 | == 23 | -- 24 | ++ grab 25 | |% 26 | ++ mime 27 | |= [mite:eyre p=octs:eyre] 28 | (txt (to-wain:format q.p)) 29 | ++ txt 30 | |= txs=(pole @t) 31 | ^- stack-info:srrs 32 | ?> ?= $: owner=@t 33 | title=@t 34 | filename=@t 35 | allow-edit=@t 36 | date-created=@t 37 | last-modified=@t 38 | * 39 | == 40 | txs 41 | :: 42 | :* %+ rash owner.txs 43 | ;~(pfix (jest 'owner: ~') fed:ag) 44 | :: 45 | %+ rash title.txs 46 | ;~(pfix (jest 'title: ') (cook crip (star next))) 47 | :: 48 | %+ rash filename.txs 49 | ;~(pfix (jest 'filename: ') (cook crip (star next))) 50 | :: 51 | %+ rash allow-edit.txs 52 | ;~ pfix 53 | (jest 'allow-edit: ') 54 | %+ cook edit-config:srrs 55 | ;~(pose (jest %post) (jest %comment) (jest %all) (jest %none)) 56 | == 57 | :: 58 | %+ rash date-created.txs 59 | ;~ pfix 60 | (jest 'date-created: ~') 61 | (cook year when:so) 62 | == 63 | :: 64 | %+ rash last-modified.txs 65 | ;~ pfix 66 | (jest 'last-modified: ~') 67 | (cook year when:so) 68 | == 69 | == 70 | ++ noun stack-info:srrs 71 | -- 72 | ++ grad %mime 73 | -- 74 | -------------------------------------------------------------------------------- /urbit/mar/srrs/primary-delta.hoon: -------------------------------------------------------------------------------- 1 | /? 309 2 | /- *srrs, chat-store 3 | /+ *srrs, *srrs-json 4 | 5 | =, html 6 | =, format 7 | :: 8 | |_ del=primary-delta 9 | :: 10 | ++ grow 11 | |% 12 | ++ json 13 | ^- ^json 14 | (primary-delta-to-json del) 15 | ++ txt 16 | ?+ -.del [(crip (en-json json))]~ 17 | %update-stack 18 | [(crip ": {(trip name.data.del)}")]~ 19 | %update-review 20 | [(crip "")]~ 21 | == 22 | ++ tank 23 | ^- ^tank 24 | :+ %rose 25 | [[' ' ~] ['`' '<' '|' ~] ['|' '>' '`' ~]] 26 | ?+ -.del [leaf+(en-json json)]~ 27 | %add-item 28 | :~ leaf+": {(trip stack.del)}" 29 | leaf+": {(trip name.data.del)}" 30 | leaf+": {(trip snippet.content.data.del)}" 31 | == 32 | %update-stack 33 | :~ leaf+": {(trip name.data.del)}" 34 | == 35 | == 36 | ++ letter 37 | ^- letter:chat-store 38 | [%text (crip ~(ram re tank))] 39 | -- 40 | :: 41 | ++ grab 42 | |% 43 | ++ noun primary-delta 44 | -- 45 | :: 46 | -- 47 | -------------------------------------------------------------------------------- /urbit/mar/srrs/stack-delta.hoon: -------------------------------------------------------------------------------- 1 | :: 2 | :::: /hoon/action/srrs/mar 3 | :: 4 | /- *srrs 5 | =, format 6 | :: 7 | |_ del=stack-delta 8 | :: 9 | ++ grab 10 | |% 11 | ++ noun stack-delta 12 | -- 13 | -- 14 | -------------------------------------------------------------------------------- /urbit/mar/srrs/stack.hoon: -------------------------------------------------------------------------------- 1 | /? 309 2 | /- *srrs 3 | /+ *srrs, *srrs-json 4 | :: 5 | |_ =stack 6 | :: 7 | ++ grow 8 | |% 9 | ++ json 10 | ^- ^json 11 | (total-build-to-json stack) 12 | -- 13 | :: 14 | ++ grab 15 | |% 16 | ++ noun ^stack 17 | -- 18 | :: 19 | ++ grad %json 20 | -- 21 | -------------------------------------------------------------------------------- /urbit/sur/srrs.hoon: -------------------------------------------------------------------------------- 1 | |% 2 | :: 3 | +$ action 4 | $% $: %new-stack 5 | name=@tas 6 | title=@t 7 | items=(map @tas item) 8 | edit=edit-config 9 | perm=perm-config 10 | == 11 | :: 12 | $: %new-item 13 | stack-owner=@p 14 | who=@p 15 | stak=@tas 16 | name=@tas 17 | title=@tas 18 | perm=perm-config 19 | front=@t 20 | back=@t 21 | == 22 | :: 23 | $: %schedule-item 24 | stak=@tas 25 | item=@tas 26 | scheduled=@da 27 | == 28 | :: 29 | [%raise-item who=@p stak=@tas item=@tas] 30 | [%answered-item owner=@p stak=@tas item=@tas answer=recall-grade] 31 | [%review-stack who=@p stak=@tas] 32 | :: 33 | [%delete-stack who=@p stak=@tas] 34 | [%delete-item stak=@tas item=@tas] 35 | :: 36 | [%edit-stack name=@tas title=@t] 37 | :: 38 | $: %edit-item 39 | who=@p 40 | stak=@tas 41 | name=@tas 42 | title=@t 43 | perm=perm-config 44 | front=@t 45 | back=@t 46 | == 47 | :: 48 | [%read who=@p stak=@tas item=@tas] 49 | [%update-review ~] 50 | :: 51 | [%import who=@p stack=@tas] 52 | [%import-file =path] 53 | :: 54 | [%copy-stack owner=@p stak=@tas keep-learned=?] 55 | == 56 | :: 57 | +$ stack-info 58 | $: owner=@p 59 | title=@t 60 | filename=@tas 61 | allow-edit=edit-config 62 | date-created=@da 63 | last-modified=@da 64 | == 65 | :: 66 | +$ perm-config [read=rule:clay write=rule:clay] 67 | :: 68 | +$ edit-config $?(%item %all %none) 69 | :: 70 | +$ stack 71 | $: stack=(each stack-info tang) 72 | name=@tas 73 | items=(map @tas item) 74 | review-items=(map @tas item) 75 | contributors=[mod=?(%white %black) who=(set @p)] 76 | subscribers=(set @p) 77 | last-update=@da 78 | == 79 | :: 80 | +$ item 81 | $: content=content 82 | learn=learned-status 83 | last-review=(unit @da) 84 | name=@tas 85 | == 86 | :: 87 | +$ stack-1 88 | $: stack=(each stack-info tang) 89 | name=@tas 90 | items=(map @tas item-1) 91 | review-items=(map @tas item-1) 92 | contributors=[mod=?(%white %black) who=(set @p)] 93 | subscribers=(set @p) 94 | last-update=@da 95 | == 96 | :: 97 | +$ item-1 98 | $: content=content 99 | learn=learned-status 100 | name=@tas 101 | == 102 | +$ content 103 | $: author=@p 104 | title=@t 105 | filename=@tas 106 | date-created=@da 107 | last-edit=@da 108 | read=? 109 | front=@t 110 | back=@t 111 | snippet=@t 112 | comments=(map @da comment) 113 | pending=? 114 | == 115 | :: 116 | +$ comment 117 | $: author=@p 118 | date-created=@da 119 | content=@t 120 | pending=? 121 | == 122 | :: 123 | +$ recall-grade $?(%again %hard %good %easy) 124 | :: 125 | +$ learned-status 126 | $: ease=@rs 127 | interval=@dr 128 | box=@ 129 | == 130 | +$ review 131 | $: who=@p 132 | stack=@tas 133 | item=@tas 134 | == 135 | :: 136 | +$ stack-delta 137 | $% [%add-item who=@p stack=@tas data=item] 138 | [%add-review-item who=@p stack=@tas data=item] 139 | [%add-stack who=@p data=stack] 140 | :: 141 | [%delete-item who=@p stack=@tas item=@tas] 142 | [%delete-review-item who=@p stack=@tas item=@tas] 143 | [%delete-stack who=@p stack=@tas] 144 | :: 145 | [%update-stack who=@p data=stack] 146 | [%update-review (set review)] 147 | == 148 | :: 149 | +$ primary-delta 150 | $% stack-delta 151 | [%read who=@p stack=@tas item=@tas] 152 | == 153 | -- 154 | --------------------------------------------------------------------------------