diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 3087d8f29..755378649 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -175,14 +175,15 @@ the pill to have the new files/hash. For most things, it is sufficient to run However, if you've made a change to Landscape's JS, then you will need to build a "glob" and upload it to bootstrap.urbit.org. To do this, run `npm install; npm run build:prod` in `pkg/interface`, and add the resulting -`pkg/arvo/app/landscape/index.js` to a fakezod at that path (or just create a +`pkg/arvo/app/landscape/index.[hash].js` to a fakezod at that path (or just create a new fakezod with `urbit -F zod -B bin/solid.pill -A pkg/arvo`). Run `:glob|make`, and this will output a file in `fakezod/.urb/put/glob-0vXXX.glob`. Upload this file to bootstrap.urbit.org, and modify `+hash` at the top of -`pkg/arvo/app/glob.hoon` to match the hash in the filename. Do not commit the -produced `index.js` and make sure it doesn't end up in your pills (they should -be less than 10MB each). +`pkg/arvo/app/glob.hoon` to match the hash in the filename of the `.glob` file. +Amend `pkg/arvo/app/landscape/index.html` to import the hashed JS bundle, instead +of the unversioned index.js. Do not commit the produced `index.js` and +make sure it doesn't end up in your pills (they should be less than 10MB each). ### Tag the resulting commit diff --git a/bin/solid.pill b/bin/solid.pill index b4a336f3e..02e11e82a 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7088528dbfd54a913921ade093251d678c4ccebfd0ad85ef2022520266b3954 -size 16451173 +oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f +size 6260139 diff --git a/pkg/arvo/app/contact-view.hoon b/pkg/arvo/app/contact-view.hoon index 49f4ba517..80bd55a45 100644 --- a/pkg/arvo/app/contact-view.hoon +++ b/pkg/arvo/app/contact-view.hoon @@ -268,7 +268,7 @@ %group-store %group-push-hook =/ =cage - :- %group-action + :- %group-update !> ^- action:group-store [%change-policy rid %invite %add-invites (sy ship ~)] [%pass / %agent [entity.rid app] %poke cage] diff --git a/pkg/arvo/app/file-server.hoon b/pkg/arvo/app/file-server.hoon index fdcf7bfd4..89f4da8ff 100644 --- a/pkg/arvo/app/file-server.hoon +++ b/pkg/arvo/app/file-server.hoon @@ -7,17 +7,20 @@ $% [%clay =path] [%glob =glob:glob] == -+$ state-1 - $: %1 - =configuration:srv ++$ state-base + $: =configuration:srv =serving == ++$ state-2 + $: %2 + state-base + == -- :: %+ verb | %- agent:dbug :: -=| state-1 +=| state-2 =* state - ^- agent:gall |_ =bowl:gall @@ -60,12 +63,18 @@ ^- [content ?] [[%clay clay-path] public] == - ?> ?=(%1 -.old-state) + =? old-state ?=(%1 -.old-state) + %= old-state + - %2 + serving (~(del by serving.old-state) /'~landscape'/js/index) + == + ?> ?=(%2 -.old-state) [~ this(state old-state)] :: +$ versioned-state - $% state-1 - state-0 + $% state-0 + state-1 + state-2 == :: +$ serving-0 (map url-base=path [=clay=path public=?]) @@ -74,6 +83,10 @@ =configuration:srv =serving-0 == + +$ state-1 + $: %1 + state-base + == -- :: ++ on-poke @@ -169,7 +182,7 @@ ?~ content [not-found:gen %.n] ?- -.content.u.content %clay - =/ scry-path + =/ scry-path=path :* (scot %p our.bowl) q.byk.bowl (scot %da now.bowl) @@ -179,10 +192,16 @@ =/ file (as-octs:mimes:html .^(@ %cx scry-path)) :_ public.u.content ?+ ext.req-line not-found:gen - [~ %html] (html-response:gen file) [~ %js] (js-response:gen file) [~ %css] (css-response:gen file) [~ %png] (png-response:gen file) + :: + [~ %html] + %. file + %* . html-response:gen + cache + !=(/app/landscape/index/html (slag 3 scry-path)) + == == :: %glob diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index 328264bb7..f19d51515 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -1,7 +1,7 @@ /- glob /+ default-agent, verb, dbug |% -++ hash 0v3.cus8h.vc64c.rfb3t.22oji.b529a +++ hash 0v2.pbthv.gd1q2.h2ura.5esrn.d361c +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ all-states $% state-0 @@ -41,7 +41,7 @@ -- =| state=state-0 =. hash.state hash -=/ serve-path=path /'~landscape'/js/index +=/ serve-path=path /'~landscape'/js/bundle ^- agent:gall %+ verb | %- agent:dbug @@ -82,9 +82,19 @@ :_ this =/ home=path /(scot %p our.bowl)/home/(scot %da now.bowl) =+ .^(=tube:clay %cc (weld home /js/mime)) - =+ .^(js=@t %cx (weld home /app/landscape/js/index/js)) + =+ .^(arch %cy (weld home /app/landscape/js/bundle)) + =/ bundle=path + %- need + ^- (unit path) + %- ~(rep by dir) + |= [[file=@t ~] out=(unit path)] + ?^ out out + ?. =((end 3 5 file) 'index') + ~ + `/[file]/js + =+ .^(js=@t %cx :(weld home /app/landscape/js/bundle bundle)) =+ !<(=mime (tube !>(js))) - =/ =glob:glob (~(put by *glob:glob) /js mime) + =/ =glob:glob (~(put by *glob:glob) bundle mime) =/ =path /(cat 3 'glob-' (scot %uv (sham glob)))/glob [%pass /make %agent [our.bowl %hood] %poke %drum-put !>([path (jam glob)])]~ :: diff --git a/pkg/arvo/app/landscape/img/groups.png b/pkg/arvo/app/landscape/img/groups.png new file mode 100644 index 000000000..fa14f36da Binary files /dev/null and b/pkg/arvo/app/landscape/img/groups.png differ diff --git a/pkg/arvo/app/landscape/img/icon-home.png b/pkg/arvo/app/landscape/img/icon-home.png index 04b1e7b87..9eb1d0289 100644 Binary files a/pkg/arvo/app/landscape/img/icon-home.png and b/pkg/arvo/app/landscape/img/icon-home.png differ diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 844b9df50..93841346b 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -4,7 +4,7 @@ OS1 + content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/> @@ -23,7 +23,7 @@
- + diff --git a/pkg/arvo/app/publish.hoon b/pkg/arvo/app/publish.hoon index 17470e7e5..2ed386584 100644 --- a/pkg/arvo/app/publish.hoon +++ b/pkg/arvo/app/publish.hoon @@ -1831,6 +1831,8 @@ :: %subscribe ?> (team:title our.bol src.bol) + ?: =(our.bol who.act) + [~ state] =/ join-wire=wire /join-group/[(scot %p who.act)]/[book.act] =/ meta=(unit (set path)) diff --git a/pkg/arvo/lib/server.hoon b/pkg/arvo/lib/server.hoon index 4a0b7ded0..d51fa12c8 100644 --- a/pkg/arvo/lib/server.hoon +++ b/pkg/arvo/lib/server.hoon @@ -80,9 +80,11 @@ ++ max-1-wk ['cache-control' 'max-age=604800'] :: ++ html-response + =| cache=? |= =octs ^- simple-payload:http - [[200 [['content-type' 'text/html'] max-1-wk ~]] `octs] + :_ `octs + [200 [['content-type' 'text/html'] ?:(cache [max-1-wk ~] ~)]] :: ++ js-response |= =octs diff --git a/pkg/interface/.eslintrc.js b/pkg/interface/.eslintrc.js new file mode 100644 index 000000000..0f63724ca --- /dev/null +++ b/pkg/interface/.eslintrc.js @@ -0,0 +1,186 @@ +const env = { + "browser": true, + "es6": true, + "node": true +}; + +const rules = { + "array-bracket-spacing": ["error", "never"], + "arrow-parens": [ + "error", + "as-needed", + { + "requireForBlockBody": true + } + ], + "arrow-spacing": "error", + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs"], + "camelcase": [ + "error", + { + "properties": "never" + } + ], + "comma-dangle": ["error", "never"], + "eol-last": ["error", "always"], + "func-name-matching": "error", + "indent": [ + "off", + 2, + { + "ArrayExpression": "off", + "SwitchCase": 1, + "CallExpression": { + "arguments": "off" + }, + "FunctionDeclaration": { + "parameters": "off" + }, + "FunctionExpression": { + "parameters": "off" + }, + "MemberExpression": "off", + "ObjectExpression": "off", + "ImportDeclaration": "off" + } + ], + "handle-callback-err": "off", + "linebreak-style": ["error", "unix"], + "max-lines": [ + "error", + { + "max": 300, + "skipBlankLines": true, + "skipComments": true + } + ], + "max-lines-per-function": [ + "warn", + { + "skipBlankLines": true, + "skipComments": true + } + ], + "max-statements-per-line": [ + "error", + { + "max": 1 + } + ], + "new-cap": [ + "error", + { + "newIsCap": true, + "capIsNew": false + } + ], + "new-parens": "error", + "no-buffer-constructor": "error", + "no-console": "off", + "no-extra-semi": "off", + "no-fallthrough": "off", + "no-func-assign": "off", + "no-implicit-coercion": "error", + "no-multi-assign": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "no-nested-ternary": "error", + "no-param-reassign": "off", + "no-return-assign": "error", + "no-return-await": "off", + "no-shadow-restricted-names": "error", + "no-tabs": "error", + "no-trailing-spaces": "error", + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "none", + "ignoreRestSiblings": false + } + ], + "no-use-before-define": [ + "error", + { + "functions": false, + "classes": false + } + ], + "no-useless-escape": "off", + "no-var": "error", + "nonblock-statement-body-position": ["error", "below"], + "object-curly-spacing": ["error", "always"], + "padded-blocks": ["error", "never"], + "prefer-arrow-callback": "error", + "prefer-const": [ + "error", + { + "destructuring": "all", + "ignoreReadBeforeAssign": true + } + ], + "prefer-template": "off", + "quotes": ["error", "single"], + "semi": ["error", "always"], + "spaced-comment": [ + "error", + "always", + { + "exceptions": ["!"] + } + ], + "space-before-blocks": "error", + "unicode-bom": ["error", "never"], + "valid-jsdoc": "error", + "wrap-iife": ["error", "inside"], + "react/jsx-closing-bracket-location": 1, + "react/jsx-tag-spacing": 1, + "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], + "react/prop-types": 0 +}; + +module.exports = { + "env": env, + "extends": [ + "plugin:react/recommended", + "eslint:recommended", + ], + "settings": { + "react": { + "version": "^16.5.2" + } + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 10, + "requireConfigFile": false, + "sourceType": "module" + }, + "root": true, + "rules": rules, + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx"], + "env": env, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { "jsx": true }, + "ecmaVersion": 10, + "requireConfigFile": false, + "sourceType": "module" + }, + "plugins": ["@typescript-eslint"], + "rules": rules + } + ] +}; diff --git a/pkg/interface/.eslintrc.json b/pkg/interface/.eslintrc.json deleted file mode 100644 index 87ab40377..000000000 --- a/pkg/interface/.eslintrc.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "env": { - "browser": true, - "es6": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "settings": { - "react": { - "version": "^16.5.2" - } - }, - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 10, - "requireConfigFile": false, - "sourceType": "module" - }, - "root": true, - "rules": { - "array-bracket-spacing": ["error", "never"], - "arrow-parens": [ - "error", - "as-needed", - { - "requireForBlockBody": true - } - ], - "arrow-spacing": "error", - "block-spacing": ["error", "always"], - "brace-style": ["error", "1tbs"], - "camelcase": [ - "error", - { - "properties": "never" - } - ], - "comma-dangle": ["error", "never"], - "eol-last": ["error", "always"], - "func-name-matching": "error", - "indent": [ - "off", - 2, - { - "ArrayExpression": "off", - "SwitchCase": 1, - "CallExpression": { - "arguments": "off" - }, - "FunctionDeclaration": { - "parameters": "off" - }, - "FunctionExpression": { - "parameters": "off" - }, - "MemberExpression": "off", - "ObjectExpression": "off", - "ImportDeclaration": "off" - } - ], - "handle-callback-err": "off", - "linebreak-style": ["error", "unix"], - "max-statements-per-line": [ - "error", - { - "max": 1 - } - ], - "new-cap": [ - "error", - { - "newIsCap": true, - "capIsNew": false - } - ], - "new-parens": "error", - "no-buffer-constructor": "error", - "no-console": "off", - "no-extra-semi": "off", - "no-fallthrough": "off", - "no-func-assign": "off", - "no-implicit-coercion": "error", - "no-multi-assign": "error", - "no-multiple-empty-lines": [ - "error", - { - "max": 1 - } - ], - "no-nested-ternary": "error", - "no-param-reassign": "off", - "no-return-assign": "error", - "no-return-await": "off", - "no-shadow-restricted-names": "error", - "no-tabs": "error", - "no-trailing-spaces": "error", - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "none", - "ignoreRestSiblings": false - } - ], - "no-use-before-define": [ - "error", - { - "functions": false, - "classes": false - } - ], - "no-useless-escape": "off", - "no-var": "error", - "nonblock-statement-body-position": ["error", "below"], - "object-curly-spacing": ["error", "always"], - "padded-blocks": ["error", "never"], - "prefer-arrow-callback": "error", - "prefer-const": [ - "error", - { - "destructuring": "all", - "ignoreReadBeforeAssign": true - } - ], - "prefer-template": "off", - "quotes": ["error", "single"], - "semi": ["error", "always"], - "spaced-comment": [ - "error", - "always", - { - "exceptions": ["!"] - } - ], - "space-before-blocks": "error", - "unicode-bom": ["error", "never"], - "valid-jsdoc": "error", - "wrap-iife": ["error", "inside"], - "react/jsx-closing-bracket-location": 1, - "react/jsx-tag-spacing": 1, - "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], - "react/prop-types": 0 - } -} diff --git a/pkg/interface/config/webpack.dev.js b/pkg/interface/config/webpack.dev.js index 3d5c08d31..e2cb44ea4 100644 --- a/pkg/interface/config/webpack.dev.js +++ b/pkg/interface/config/webpack.dev.js @@ -52,7 +52,7 @@ if(urbitrc.URL) { ...devServer, index: '', proxy: { - '/~landscape/js/index.js': { + '/~landscape/js/bundle/index.*.js': { target: 'http://localhost:9000', pathRewrite: (req, path) => '/index.js' }, diff --git a/pkg/interface/config/webpack.prod.js b/pkg/interface/config/webpack.prod.js index 10525a526..7a0e5a2e7 100644 --- a/pkg/interface/config/webpack.prod.js +++ b/pkg/interface/config/webpack.prod.js @@ -1,6 +1,6 @@ const path = require('path'); // const HtmlWebpackPlugin = require('html-webpack-plugin'); -// const { CleanWebpackPlugin } = require('clean-webpack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { mode: 'production', @@ -49,17 +49,16 @@ module.exports = { // historyApiFallback: true // }, plugins: [ - // new CleanWebpackPlugin(), + new CleanWebpackPlugin(), // new HtmlWebpackPlugin({ // title: 'Hot Module Replacement', // template: './public/index.html', // }), ], output: { - filename: 'index.js', - chunkFilename: 'index.js', - path: path.resolve(__dirname, '../../arvo/app/landscape/js'), - publicPath: '/' + filename: 'index.[contenthash].js', + path: path.resolve(__dirname, '../../arvo/app/landscape/js/bundle'), + publicPath: '/', }, optimization: { minimize: true, diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index 27a526c50..c3f717ee9 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -1660,6 +1660,12 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -1689,6 +1695,12 @@ "integrity": "sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA==", "dev": true }, + "@types/json-schema": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", + "dev": true + }, "@types/lodash": { "version": "4.14.155", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz", @@ -1795,6 +1807,93 @@ "source-map": "^0.6.1" } }, + "@typescript-eslint/eslint-plugin": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz", + "integrity": "sha512-lFb4VCDleFSR+eo4Ew+HvrJ37ZH1Y9ZyE+qyP7EiwBpcCVxwmUc5PAqhShCQ8N8U5vqYydm74nss+a0wrrCErw==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "3.8.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz", + "integrity": "sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/typescript-estree": "3.8.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.8.0.tgz", + "integrity": "sha512-u5vjOBaCsnMVQOvkKCXAmmOhyyMmFFf5dbkM3TIbg3MZ2pyv5peE4gj81UAbTHwTOXEwf7eCQTUMKrDl/+qGnA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.8.0", + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/typescript-estree": "3.8.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/types": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.8.0.tgz", + "integrity": "sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz", + "integrity": "sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "3.8.0", + "@typescript-eslint/visitor-keys": "3.8.0", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz", + "integrity": "sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -1993,9 +2092,9 @@ } }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", "dev": true }, "acorn-jsx": { @@ -2931,9 +3030,9 @@ } }, "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, "cliui": { @@ -3960,6 +4059,15 @@ } } }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -3975,6 +4083,12 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -4033,9 +4147,9 @@ } }, "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -4043,9 +4157,9 @@ } }, "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -4084,9 +4198,9 @@ }, "dependencies": { "estraverse": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", - "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true } } @@ -5409,21 +5523,21 @@ "dev": true }, "inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", - "lodash": "^4.17.15", + "lodash": "^4.17.19", "mute-stream": "0.0.8", "run-async": "^2.4.0", - "rxjs": "^6.5.3", + "rxjs": "^6.6.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" @@ -5440,9 +5554,9 @@ } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5470,6 +5584,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -5792,9 +5912,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -6380,6 +6500,11 @@ "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==" }, + "mousetrap-global-bind": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz", + "integrity": "sha1-zX3pIivQZG+i4BDVTISnTCaojt0=" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -6774,9 +6899,9 @@ } }, "onetime": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", - "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.1.tgz", + "integrity": "sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" @@ -7661,9 +7786,9 @@ } }, "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "regexpu-core": { @@ -7982,9 +8107,9 @@ } }, "rxjs": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", - "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", + "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -8826,9 +8951,9 @@ "dev": true }, "strip-json-comments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", - "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, "style-loader": { @@ -9187,6 +9312,15 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -9224,6 +9358,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", + "dev": true + }, "unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -9497,9 +9637,9 @@ "dev": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "value-equal": { diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 1e21e1322..338828766 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -18,6 +18,7 @@ "markdown-to-jsx": "^6.11.4", "moment": "^2.20.1", "mousetrap": "^1.6.5", + "mousetrap-global-bind": "^1.1.0", "prop-types": "^15.7.2", "react": "^16.5.2", "react-codemirror2": "^6.0.1", @@ -45,6 +46,8 @@ "@types/lodash": "^4.14.155", "@types/react": "^16.9.38", "@types/react-router-dom": "^5.1.5", + "@typescript-eslint/eslint-plugin": "^3.8.0", + "@typescript-eslint/parser": "^3.8.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", @@ -56,12 +59,13 @@ "react-hot-loader": "^4.12.21", "sass": "^1.26.5", "sass-loader": "^8.0.2", + "typescript": "^3.9.7", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" }, "scripts": { - "lint": "eslint ./**/*.js", + "lint": "eslint ./src/**/*.{js,ts,tsx}", "lint-file": "eslint", "tsc": "tsc", "tsc:watch": "tsc --watch", diff --git a/pkg/interface/src/App.js b/pkg/interface/src/App.js index 94dc9d076..4b13c3c2e 100644 --- a/pkg/interface/src/App.js +++ b/pkg/interface/src/App.js @@ -3,6 +3,10 @@ import 'react-hot-loader'; import * as React from 'react'; import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router-dom'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; +import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js'; + +import Mousetrap from 'mousetrap'; +import 'mousetrap-global-bind'; import './css/indigo-static.css'; import './css/fonts.css'; @@ -17,11 +21,14 @@ import LinksApp from './apps/links/app'; import PublishApp from './apps/publish/app'; import StatusBar from './components/StatusBar'; +import Omnibox from './components/Omnibox'; import ErrorComponent from './components/Error'; import GlobalStore from './store/store'; import GlobalSubscription from './subscription/global'; import GlobalApi from './api/global'; +import { uxToHex } from './lib/util'; +import { Sigil } from './lib/sigil'; // const Style = createGlobalStyle` // ${cssReset} @@ -62,6 +69,7 @@ class App extends React.Component { new GlobalSubscription(this.store, this.api, this.appChannel); this.updateTheme = this.updateTheme.bind(this); + this.setFavicon = this.setFavicon.bind(this); } componentDidMount() { @@ -70,21 +78,49 @@ class App extends React.Component { this.api.local.setDark(this.themeWatcher.matches); this.themeWatcher.addListener(this.updateTheme); this.api.local.getBaseHash(); + Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => { + e.preventDefault(); + this.api.local.setOmnibox(); + }); + this.setFavicon(); } componentWillUnmount() { this.themeWatcher.removeListener(this.updateTheme); } + componentDidUpdate(prevProps, prevState, snapshot) { + this.setFavicon(); + } + updateTheme(e) { this.api.local.setDark(e.matches); } + setFavicon() { + if (window.ship.length < 14) { + let background = '#ffffff'; + if (this.state.contacts.hasOwnProperty('/~/default')) { + background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`; + } + const foreground = Sigil.foregroundFromBackground(background); + const svg = sigiljs({ + patp: window.ship, + renderer: stringRenderer, + size: 16, + colors: [background, foreground] + }); + const dataurl = 'data:image/svg+xml;base64,' + btoa(svg); + const favicon = document.querySelector('[rel=icon]'); + favicon.href = dataurl; + favicon.type = 'image/svg+xml'; + } + } + render() { const channel = window.channel; const associations = this.state.associations ? this.state.associations : { contacts: {} }; - const selectedGroups = this.state.selectedGroups ? this.state.selectedGroups : []; const { state } = this; const theme = state.dark ? dark : light; @@ -92,81 +128,99 @@ class App extends React.Component { - + - - ( - + ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> - ( - ( + + )} /> - )} - /> ( + render={props => ( )} - /> + /> @@ -176,5 +230,6 @@ class App extends React.Component { } } + export default process.env.NODE_ENV === 'production' ? App : hot(App); diff --git a/pkg/interface/src/api/local.ts b/pkg/interface/src/api/local.ts index 8610284c2..77ff9b2b3 100644 --- a/pkg/interface/src/api/local.ts +++ b/pkg/interface/src/api/local.ts @@ -1,6 +1,5 @@ import BaseApi from "./base"; import { StoreState } from "../store/type"; -import { SelectedGroup } from "../types/local-update"; export default class LocalApi extends BaseApi { getBaseHash() { @@ -9,16 +8,6 @@ export default class LocalApi extends BaseApi { }); } - setSelected(selected: SelectedGroup[]) { - this.store.handleEvent({ - data: { - local: { - selected - } - } - }) - } - sidebarToggle() { this.store.handleEvent({ data: { @@ -39,4 +28,14 @@ export default class LocalApi extends BaseApi { }); } + setOmnibox() { + this.store.handleEvent({ + data: { + local: { + omniboxShown: true + }, + }, + }); + } + } diff --git a/pkg/interface/src/apps/chat/app.tsx b/pkg/interface/src/apps/chat/app.tsx index 8f9adc843..8ccaabd46 100644 --- a/pkg/interface/src/apps/chat/app.tsx +++ b/pkg/interface/src/apps/chat/app.tsx @@ -53,7 +53,6 @@ export default class ChatApp extends React.Component { const unreads = {}; let totalUnreads = 0; - const selectedGroups = props.selectedGroups ? props.selectedGroups : []; const associations = props.associations ? props.associations : { chat: {}, contacts: {} }; @@ -74,14 +73,7 @@ export default class ChatApp extends React.Component { unreads[stat] = Boolean(unread); if ( unread && - stat in associations.chat && - (selectedGroups.length === 0 || - selectedGroups - .map((e) => { - return e[0]; - }) - .includes(associations.chat?.[stat]?.['group-path']) || - props.groups[associations.chat?.[stat]?.['group-path']]?.hidden) + stat in associations.chat ) { totalUnreads += unread; } @@ -111,7 +103,6 @@ export default class ChatApp extends React.Component { inbox={inbox} messagePreviews={messagePreviews} associations={associations} - selectedGroups={selectedGroups} contacts={contacts} invites={invites['/chat'] || {}} unreads={unreads} @@ -286,44 +277,6 @@ export default class ChatApp extends React.Component { ); }} /> - { - let station = `/${props.match.params.ship}/${props.match.params.station}`; - - const popout = props.match.url.includes('/popout/'); - - const association = - station in associations['chat'] ? associations.chat[station] : {}; - const groupPath = association['group-path']; - - const group = groups[groupPath] || {}; - return ( - - - - ); - }} - /> ; - lastScrollHeight: number | null; } export class ChatScreen extends Component { - hasAskedForMessages = false; lastNumPending = 0; - - scrollContainer: HTMLElement | null = null; - - unreadMarker = null; - scrolledToMarker = false; - activityTimeout: NodeJS.Timeout | null = null; - scrollElement: HTMLElement | null = null; - constructor(props) { super(props); this.state = { - numPages: 1, - scrollLocked: false, - read: props.read, - active: true, messages: new Map(), - // only for FF - lastScrollHeight: null, }; - this.onScroll = this.onScroll.bind(this); - - this.setUnreadMarker = this.setUnreadMarker.bind(this); - - this.handleActivity = this.handleActivity.bind(this); - this.setInactive = this.setInactive.bind(this); - moment.updateLocale("en", { calendar: { sameDay: "[Today]", @@ -143,450 +65,68 @@ export class ChatScreen extends Component { }); } - componentDidMount() { - document.addEventListener("mousemove", this.handleActivity, false); - document.addEventListener("mousedown", this.handleActivity, false); - document.addEventListener("keypress", this.handleActivity, false); - document.addEventListener("touchmove", this.handleActivity, false); - this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT); - } - - componentWillUnmount() { - document.removeEventListener("mousemove", this.handleActivity, false); - document.removeEventListener("mousedown", this.handleActivity, false); - document.removeEventListener("keypress", this.handleActivity, false); - document.removeEventListener("touchmove", this.handleActivity, false); - if (this.activityTimeout) { - clearTimeout(this.activityTimeout); - } - } - - handleActivity() { - if (!this.state.active) { - this.setState({ active: true }); - } - - if (this.activityTimeout) { - clearTimeout(this.activityTimeout); - } - - this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT); - } - - setInactive() { - this.activityTimeout = null; - this.setState({ active: false, scrollLocked: true }); - } - - receivedNewChat() { - const { props } = this; - this.hasAskedForMessages = false; - - this.unreadMarker = null; - this.scrolledToMarker = false; - - this.setState({ read: props.read }); - - const unread = props.length - props.read; - const unreadUnloaded = unread - props.envelopes.length; - const excessUnread = unreadUnloaded > MAX_BACKLOG_SIZE; - - if (!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) { - this.askForMessages(unreadUnloaded + 20); - } else { - this.askForMessages(DEFAULT_BACKLOG_SIZE); - } - - if (excessUnread || props.read === props.length) { - this.scrolledToMarker = true; - this.setState( - { - scrollLocked: false, - }, - () => { - this.scrollToBottom(); - } - ); - } else { - this.setState({ scrollLocked: true, numPages: Math.ceil(unread / 100) }); - } - } - - componentDidUpdate(prevProps, prevState) { - const { props, state } = this; - - if ( - prevProps.match.params.station !== props.match.params.station || - prevProps.match.params.ship !== props.match.params.ship - ) { - this.receivedNewChat(); - } else if ( - props.chatInitialized && - !(props.station in props.inbox) && - Boolean(props.chatSynced) && - !(props.station in props.chatSynced) - ) { - props.history.push("/~chat"); - } else if (props.envelopes.length >= prevProps.envelopes.length + 10) { - this.hasAskedForMessages = false; - } else if ( - props.length !== prevProps.length && - prevProps.length === prevState.read && - state.active - ) { - this.setState({ read: props.length }); - this.props.api.chat.read(this.props.station); - } - - if (!prevProps.chatInitialized && props.chatInitialized) { - this.receivedNewChat(); - } - - if ( - props.length !== prevProps.length || - props.envelopes.length !== prevProps.envelopes.length || - getNumPending(props) !== this.lastNumPending || - state.numPages !== prevState.numPages - ) { - this.scrollToBottom(); - if (navigator.userAgent.includes("Firefox")) { - this.recalculateScrollTop(); - } - - this.lastNumPending = getNumPending(props); - } - } - - askForMessages(size) { - const { props, state } = this; - - if ( - props.envelopes.length >= props.length || - this.hasAskedForMessages || - props.length <= 0 - ) { - return; - } - - const start = - props.length - props.envelopes[props.envelopes.length - 1].number; - if (start > 0) { - const end = start + size < props.length ? start + size : props.length; - this.hasAskedForMessages = true; - props.api.chat.fetchMessages(start + 1, end, props.station); - } - } - - scrollToBottom() { - if (!this.state.scrollLocked && this.scrollElement) { - this.scrollElement.scrollIntoView(); - } - } - - // Restore chat position on FF when new messages come in - recalculateScrollTop() { - const { lastScrollHeight } = this.state; - if (!this.scrollContainer || !lastScrollHeight) { - return; - } - - const target = this.scrollContainer; - const newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight; - if (target.scrollTop !== 0 || newScrollTop === target.scrollTop) { - return; - } - target.scrollTop = target.scrollHeight - lastScrollHeight; - } - - onScroll(e) { - if (scrollIsAtTop(e.target)) { - // Save scroll position for FF - if (navigator.userAgent.includes("Firefox")) { - this.setState({ - lastScrollHeight: e.target.scrollHeight, - }); - } - this.setState( - { - numPages: this.state.numPages + 1, - scrollLocked: true, - }, - () => { - this.askForMessages(DEFAULT_BACKLOG_SIZE); - } - ); - } else if (scrollIsAtBottom(e.target)) { - this.dismissUnread(); - this.setState({ - numPages: 1, - scrollLocked: false, - }); - } - } - - setUnreadMarker(ref) { - if (ref && !this.scrolledToMarker) { - this.setState({ scrollLocked: true }, () => { - ref.scrollIntoView({ block: "center" }); - if (ref.offsetParent && scrollIsAtBottom(ref.offsetParent)) { - this.dismissUnread(); - this.setState({ - numPages: 1, - scrollLocked: false, - }); - } - }); - this.scrolledToMarker = true; - } - this.unreadMarker = ref; - } - - dismissUnread() { - this.props.api.chat.read(this.props.station); - } - - chatWindow(unread) { - // Replace with just the "not Firefox" implementation - // when Firefox #1042151 is patched. - - const { props, state } = this; - - let messages: IMessage[] = props.envelopes.slice(0); - const lastMsgNum = messages.length > 0 ? messages.length : 0; - - if (messages.length > 100 * state.numPages) { - messages = messages.slice(0, 100 * state.numPages); - } - - const pendingMessages: IMessage[] = ( - props.pendingMessages.get(props.station) || [] - ).map((value) => ({ ...value, pending: true })); - - if(unread !== 0) { - unread += pendingMessages.length; - } - - messages = pendingMessages.concat(messages); - - const messageElements = messages.map((msg, i) => { - // Render sigil if previous message is not by the same sender - const aut = ["author"]; - const renderSigil = - _.get(messages[i + 1], aut) !== _.get(msg, aut, msg.author); - const paddingTop = renderSigil; - const paddingBot = - _.get(messages[i - 1], aut) !== _.get(msg, aut, msg.author); - - const when = ["when"]; - const dayBreak = - moment(_.get(messages[i + 1], when)).format("YYYY.MM.DD") !== - moment(_.get(messages[i], when)).format("YYYY.MM.DD"); - - const messageElem = ( - - ); - if (unread > 0 && i === unread - 1) { - return ( - - {messageElem} -
-
-

New messages below

-
- {dayBreak && ( -

- {moment(_.get(messages[i], when)).calendar()} -

- )} -
-
-
- ); - } else if (dayBreak) { - return ( - - {messageElem} -
-

{moment(_.get(messages[i], when)).calendar()}

-
-
- ); - } else { - return messageElem; - } - }); - - if (navigator.userAgent.includes("Firefox")) { - return ( -
{ - this.scrollContainer = e; - }} - > -
-
{ - this.scrollElement = el; - }} - >
- {props.chatInitialized && !(props.station in props.inbox) && ( - - )} - {props.chatSynced && - !(props.station in props.chatSynced) && - messages.length > 0 ? ( - - ) : ( -
- )} - {messageElements} -
-
- ); - } else { - return ( -
-
{ - this.scrollElement = el; - }} - >
- {props.chatInitialized && !(props.station in props.inbox) && ( - - )} - {props.chatSynced && - !(props.station in props.chatSynced) && - messages.length > 0 ? ( - - ) : ( -
- )} - {messageElements} -
- ); - } - } - render() { const { props, state } = this; - const messages = props.envelopes.slice(0); - - const lastMsgNum = messages.length > 0 ? messages.length : 0; - - const group = Array.from(props.group.members); - - const isinPopout = props.popout ? "popout/" : ""; - + const lastMsgNum = props.envelopes.length > 0 ? props.envelopes.length : 0; const ownerContact = window.ship in props.contacts ? props.contacts[window.ship] : false; - let title = props.station.substr(1); + const pendingMessages = (props.pendingMessages.get(props.station) || []) + .map((value) => ({ + ...value, + pending: true + })); - if (props.association && "metadata" in props.association) { - title = - props.association.metadata.title !== "" - ? props.association.metadata.title - : props.station.substr(1); - } + const isChatMissing = + props.chatInitialized && + !(props.station in props.inbox) && + props.chatSynced && + !(props.station in props.chatSynced); - const unread = props.length - state.read; + const isChatLoading = + props.chatInitialized && + !(props.station in props.inbox) && + props.chatSynced && + (props.station in props.chatSynced); - const unreadMsg = unread > 0 && messages[unread - 1]; + const isChatUnsynced = + props.chatSynced && + !(props.station in props.chatSynced) && + props.envelopes.length > 0; - const showUnreadNotice = - props.length !== props.read && props.read === state.read; + const unreadCount = props.length - props.read; + const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1]; return (
-
- {"⟵ All Chats"} -
- -
- - -

- {title} -

- - -
- {!!unreadMsg && showUnreadNotice && ( - this.dismissUnread()} - /> - )} - {this.chatWindow(unread)} + className="h-100 w-100 overflow-hidden flex flex-column relative"> + + { ownerContact={ownerContact} envelopes={props.envelopes} contacts={props.contacts} - onEnter={() => this.setState({ scrollLocked: false })} onUnmount={(msg: string) => this.setState({ messages: this.state.messages.set(props.station, msg) })} s3={props.s3} placeholder="Message..." message={this.state.messages.get(props.station) || ""} + deleteMessage={() => this.setState({ + messages: this.state.messages.set(props.station, "") + })} />
); diff --git a/pkg/interface/src/apps/chat/components/lib/backlog-element.js b/pkg/interface/src/apps/chat/components/lib/backlog-element.js index 6e16882ff..64f8b26ca 100644 --- a/pkg/interface/src/apps/chat/components/lib/backlog-element.js +++ b/pkg/interface/src/apps/chat/components/lib/backlog-element.js @@ -1,21 +1,22 @@ import React, { Component } from 'react'; -export class BacklogElement extends Component { - render() { - return ( -
-
- -

- Past messages are being restored -

-
-
- - ); +export const BacklogElement = (props) => { + if (!props.isChatLoading) { + return null; } + return ( +
+
+ +

Past messages are being restored

+
+
+ ); } diff --git a/pkg/interface/src/apps/chat/components/lib/chat-editor.js b/pkg/interface/src/apps/chat/components/lib/chat-editor.js new file mode 100644 index 000000000..ee6dc4b6c --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-editor.js @@ -0,0 +1,141 @@ +import React, { Component } from 'react'; +import { UnControlled as CodeEditor } from 'react-codemirror2'; +import CodeMirror from 'codemirror'; + +import 'codemirror/mode/markdown/markdown'; +import 'codemirror/addon/display/placeholder'; + +import 'codemirror/lib/codemirror.css'; + +const BROWSER_REGEX = + new RegExp(String(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i)); + + +const MARKDOWN_CONFIG = { + name: 'markdown', + tokenTypeOverrides: { + header: 'presentation', + quote: 'presentation', + list1: 'presentation', + list2: 'presentation', + list3: 'presentation', + hr: 'presentation', + image: 'presentation', + imageAltText: 'presentation', + imageMarker: 'presentation', + formatting: 'presentation', + linkInline: 'presentation', + linkEmail: 'presentation', + linkText: 'presentation', + linkHref: 'presentation' + } +}; + +export default class ChatEditor extends Component { + constructor(props) { + super(props); + + this.state = { + message: props.message + }; + this.editor = null; + } + + componentWillUnmount() { + this.props.onUnmount(this.state.message); + } + + componentDidUpdate(prevProps) { + const { props } = this; + + if (prevProps.message !== props.message) { + this.editor.setValue(props.message); + this.editor.setOption('mode', MARKDOWN_CONFIG); + return; + } + + if (!props.inCodeMode) { + this.editor.setOption('mode', MARKDOWN_CONFIG); + this.editor.setOption('placeholder', this.props.placeholder); + } else { + this.editor.setOption('mode', null); + this.editor.setOption('placeholder', 'Code...'); + } + const value = this.editor.getValue(); + + // Force redraw of placeholder + if(value.length === 0) { + this.editor.setValue(' '); + this.editor.setValue(''); + } + } + + submit() { + if(!this.editor) { + return; + } + + let editorMessage = this.editor.getValue(); + if (editorMessage === '') { + return; + } + + this.setState({ message: '' }); + this.props.submit(editorMessage); + this.editor.setValue(''); + } + + messageChange(editor, data, value) { + if (this.state.message !== '' && value == '') { + this.setState({ + message: value + }); + } + if (value == this.props.message || value == '' || value == ' ') { + return; + } + this.setState({ + message: value + }); + } + + render() { + const { props } = this; + + const codeTheme = props.inCodeMode ? ' code' : ''; + + const options = { + mode: MARKDOWN_CONFIG, + theme: 'tlon' + codeTheme, + lineNumbers: false, + lineWrapping: true, + scrollbarStyle: 'native', + cursorHeight: 0.85, + placeholder: props.inCodeMode ? 'Code...' : props.placeholder, + extraKeys: { + 'Enter': () => { + this.submit(); + } + } + }; + + return ( +
+ this.messageChange(e, d, v)} + editorDidMount={(editor) => { + this.editor = editor; + if (!(BROWSER_REGEX.test(navigator.userAgent))) { + editor.focus(); + } + }} + /> +
+ ); + } +} diff --git a/pkg/interface/src/apps/chat/components/lib/chat-header.js b/pkg/interface/src/apps/chat/components/lib/chat-header.js new file mode 100644 index 000000000..4c20a5e25 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-header.js @@ -0,0 +1,58 @@ +import React, { Component, Fragment } from "react"; +import { Link } from "react-router-dom"; + +import { ChatTabBar } from "./chat-tabbar"; +import { SidebarSwitcher } from "../../../../components/SidebarSwitch"; +import { deSig } from "../../../../lib/util"; + + +export const ChatHeader = (props) => { + const isInPopout = props.popout ? "popout/" : ""; + const group = Array.from(props.group.members); + let title = props.station.substr(1); + if (props.association && + "metadata" in props.association && + props.association.metadata.tile !== "") { + title = props.association.metadata.title + } + + return ( + +
+ {"⟵ All Chats"} +
+
+ + +

+ {title} +

+ + +
+
+ ); +} diff --git a/pkg/interface/src/apps/chat/components/lib/chat-input.js b/pkg/interface/src/apps/chat/components/lib/chat-input.js index b507d1114..8223ac47c 100644 --- a/pkg/interface/src/apps/chat/components/lib/chat-input.js +++ b/pkg/interface/src/apps/chat/components/lib/chat-input.js @@ -1,143 +1,41 @@ import React, { Component } from 'react'; -import _ from 'lodash'; -import moment from 'moment'; -import { UnControlled as CodeEditor } from 'react-codemirror2'; -import CodeMirror from 'codemirror'; - -import 'codemirror/mode/markdown/markdown'; -import 'codemirror/addon/display/placeholder'; - -import 'codemirror/lib/codemirror.css'; - import { Sigil } from '../../../../lib/sigil'; -import { ShipSearch } from './ship-search'; +import ChatEditor from './chat-editor'; import { S3Upload } from './s3-upload'; - import { uxToHex } from '../../../../lib/util'; -const MARKDOWN_CONFIG = { - name: 'markdown', - tokenTypeOverrides: { - header: 'presentation', - quote: 'presentation', - list1: 'presentation', - list2: 'presentation', - list3: 'presentation', - hr: 'presentation', - image: 'presentation', - imageAltText: 'presentation', - imageMarker: 'presentation', - formatting: 'presentation', - linkInline: 'presentation', - linkEmail: 'presentation', - linkText: 'presentation', - linkHref: 'presentation' - } -}; + +const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)); export class ChatInput extends Component { constructor(props) { super(props); this.state = { - message: props.message, - patpSearch: null + inCodeMode: false, }; - this.textareaRef = React.createRef(); - - this.messageSubmit = this.messageSubmit.bind(this); - this.messageChange = this.messageChange.bind(this); - - this.patpAutocomplete = this.patpAutocomplete.bind(this); - this.completePatp = this.completePatp.bind(this); - this.clearSearch = this.clearSearch.bind(this); - + this.submit = this.submit.bind(this); this.toggleCode = this.toggleCode.bind(this); - - this.editor = null; - - moment.updateLocale('en', { - relativeTime : { - past: function(input) { - return input === 'just now' - ? input - : input + ' ago'; - }, - s : 'just now', - future: 'in %s', - ss : '%d sec', - m: 'a minute', - mm: '%d min', - h: 'an hr', - hh: '%d hrs', - d: 'a day', - dd: '%d days', - M: 'a month', - MM: '%d months', - y: 'a year', - yy: '%d years' - } - }); } - componentWillUnmount() { - this.props.onUnmount(this.state.message); + uploadSuccess(url) { + const { props } = this; + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), + { url } + ); } - nextAutocompleteSuggestion(backward = false) { - const { patpSuggestions } = this.state; - let idx = patpSuggestions.findIndex(s => s === this.state.selectedSuggestion); - - idx = backward ? idx - 1 : idx + 1; - idx = idx % patpSuggestions.length; - if(idx < 0) { - idx = patpSuggestions.length - 1; - } - - this.setState({ selectedSuggestion: patpSuggestions[idx] }); + uploadError(error) { + // no-op for now } - patpAutocomplete(message) { - const match = /~([a-zA-Z\-]*)$/.exec(message); - - if (!match ) { - this.setState({ patpSearch: null }); - return; - } - this.setState({ patpSearch: match[1].toLowerCase() }); - } - - clearSearch() { + toggleCode() { this.setState({ - patpSearch: null - }); - } - - completePatp(suggestion) { - if(!this.editor) { - return; - } - const newMessage = this.editor.getValue().replace( - /[a-zA-Z\-]*$/, - suggestion - ); - this.editor.setValue(newMessage); - const lastRow = this.editor.lastLine(); - const lastCol = this.editor.getLineHandle(lastRow).text.length; - this.editor.setCursor(lastRow, lastCol); - this.setState({ - patpSearch: null - }); - } - - messageChange(editor, data, value) { - const { patpSearch } = this.state; - if(patpSearch !== null) { - this.patpAutocomplete(value, false); - } - this.setState({ - message: value + inCodeMode: !this.state.inCodeMode }); } @@ -154,7 +52,7 @@ export class ChatInput extends Component { me: letter }; } else if (this.isUrl(letter)) { - return { + return { url: letter }; } else { @@ -166,41 +64,40 @@ export class ChatInput extends Component { isUrl(string) { try { - const websiteTest = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source) - ); - return websiteTest.test(string); + return URL_REGEX.test(string); } catch (e) { return false; } } - messageSubmit() { - if(!this.editor) { - return; - } + submit(text) { const { props, state } = this; - const editorMessage = this.editor.getValue(); - - if (editorMessage === '') { - return; - } - - props.onEnter(); - - if(state.code) { - props.api.chat.message(props.station, `~${window.ship}`, Date.now(), { - code: { - expression: editorMessage, - output: undefined - } + if (state.inCodeMode) { + this.setState({ + inCodeMode: false + }, () => { + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), { + code: { + expression: text, + output: undefined + } + } + ); }); - this.editor.setValue(''); return; } + + let messages = []; let message = []; let isInCodeBlock = false; let endOfCodeBlock = false; - editorMessage.split(/\r?\n/).forEach((line) => { + text.split(/\r?\n/).forEach((line, index) => { + if (index !== 0) { + message.push('\n'); + } // A line of backticks enters and exits a codeblock if (line.startsWith('```')) { // But we need to check if we've ended a codeblock @@ -209,10 +106,9 @@ export class ChatInput extends Component { } else { endOfCodeBlock = false; } - if (isInCodeBlock) { - message.push(`\n${line}`); - } else if (endOfCodeBlock) { - message.push(`\n${line}\n`); + + if (isInCodeBlock || endOfCodeBlock) { + message.push(line); } else { line.split(/\s/).forEach((str) => { if ( @@ -226,46 +122,41 @@ export class ChatInput extends Component { ) { isInCodeBlock = false; } + if (this.isUrl(str) && !isInCodeBlock) { if (message.length > 0) { - message = message.join(' '); - message = this.getLetterType(message); - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - message - ); + // If we're in the middle of a message, add it to the stack and reset + messages.push(message); message = []; } - const URL = this.getLetterType(str); - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - URL - ); + messages.push([str]); + message = []; } else { message.push(str); } }); - } - }); - if (message.length > 0) { - message = message.join(' '); - message = this.getLetterType(message); - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - message - ); - message = []; + if (message.length) { + // Add any remaining message + messages.push(message); } + props.deleteMessage(); + + messages.forEach((message) => { + if (message.length > 0) { + message = this.getLetterType(message.join(' ')); + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), + message + ); + } + }); + // perf testing: /*let closure = () => { let x = 0; @@ -284,27 +175,6 @@ export class ChatInput extends Component { }; this.closure = closure.bind(this); setTimeout(this.closure, 2000);*/ - - this.editor.setValue(''); - } - - toggleCode() { - if(this.state.code) { - this.setState({ code: false }); - this.editor.setOption('mode', MARKDOWN_CONFIG); - this.editor.setOption('placeholder', this.props.placeholder); - } else { - this.setState({ code: true }); - this.editor.setOption('mode', null); - this.editor.setOption('placeholder', 'Code...'); - } - const value = this.editor.getValue(); - - // Force redraw of placeholder - if(value.length === 0) { - this.editor.setValue(' '); - this.editor.setValue(''); - } } uploadSuccess(url) { @@ -330,7 +200,7 @@ export class ChatInput extends Component { const sigilClass = props.ownerContact ? '' : 'mix-blend-diff'; - const img = (props.ownerContact && (props.ownerContact.avatar !== null)) + const avatar = (props.ownerContact && (props.ownerContact.avatar !== null)) ? : ; - const candidates = _.chain(this.props.envelopes) - .defaultTo([]) - .map('author') - .uniq() - .reverse() - .value(); - - const codeTheme = state.code ? ' code' : ''; - - const options = { - mode: MARKDOWN_CONFIG, - theme: 'tlon' + codeTheme, - lineNumbers: false, - lineWrapping: true, - scrollbarStyle: 'native', - cursorHeight: 0.85, - placeholder: state.code ? 'Code...' : props.placeholder, - extraKeys: { - Tab: cm => - this.patpAutocomplete(cm.getValue(), true), - 'Enter': () => { - this.messageSubmit(); - if (this.state.code) { - this.toggleCode(); - } - }, - 'Shift-3': cm => - cm.getValue().length === 0 - ? this.toggleCode() - : CodeMirror.Pass - } - }; - return ( -
- -
- {img} -
-
- { - this.editor = editor; - if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test( - navigator.userAgent - )) { - editor.focus(); - } - }} - onChange={(e, d, v) => this.messageChange(e, d, v)} - /> +
+
+ {avatar}
+
+ style={{ + height: '16px', + width: '16px', + flexBasis: 16, + marginTop: 10 + }}>
-
- +
+
); diff --git a/pkg/interface/src/apps/chat/components/lib/chat-message.tsx b/pkg/interface/src/apps/chat/components/lib/chat-message.tsx new file mode 100644 index 000000000..84b985417 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-message.tsx @@ -0,0 +1,84 @@ +import React, { PureComponent, Fragment } from "react"; +import moment from "moment"; + +import { Message } from "./message"; + +type IMessage = Envelope & { pending?: boolean }; + + +export const ChatMessage = (props) => { + const { + msg, + previousMsg, + nextMsg, + isLastUnread, + group, + association, + contacts, + unreadRef + } = props; + + // Render sigil if previous message is not by the same sender + const aut = ["author"]; + const renderSigil = + _.get(nextMsg, aut) !== _.get(msg, aut, msg.author); + const paddingTop = renderSigil; + const paddingBot = + _.get(previousMsg, aut) !== _.get(msg, aut, msg.author); + + const when = ["when"]; + const dayBreak = + moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !== + moment(_.get(msg, when)).format("YYYY.MM.DD"); + + const messageElem = ( + + ); + + if (props.isLastUnread) { + return ( + + {messageElem} +
+
+

New messages below

+
+ {dayBreak && ( +

+ {moment(_.get(msg, when)).calendar()} +

+ )} +
+
+
+ ); + } else if (dayBreak) { + return ( + + {messageElem} +
+

{moment(_.get(msg, when)).calendar()}

+
+
+ ); + } else { + return messageElem; + } +}; + diff --git a/pkg/interface/src/apps/chat/components/lib/chat-scroll-container.js b/pkg/interface/src/apps/chat/components/lib/chat-scroll-container.js new file mode 100644 index 000000000..68b0872c4 --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-scroll-container.js @@ -0,0 +1,143 @@ +import React, { Component, Fragment } from "react"; + +import { scrollIsAtTop, scrollIsAtBottom } from "../../../../lib/util"; + +// Restore chat position on FF when new messages come in +const recalculateScrollTop = (lastScrollHeight, scrollContainer) => { + if (!scrollContainer || !lastScrollHeight) { + return; + } + + const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight; + if (scrollContainer.scrollTop !== 0 || + scrollContainer.scrollTop === newScrollTop) { + return; + } + + scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight; +}; + + +export class ChatScrollContainer extends Component { + constructor(props) { + super(props); + + // only for FF + this.state = { + lastScrollHeight: null + }; + + this.isTriggeredScroll = false; + + this.isAtBottom = true; + this.isAtTop = false; + + this.containerDidScroll = this.containerDidScroll.bind(this); + + this.containerRef = React.createRef(); + this.scrollRef = React.createRef(); + } + + containerDidScroll(e) { + const { props } = this; + if (scrollIsAtTop(e.target)) { + // Save scroll position for FF + if (navigator.userAgent.includes("Firefox")) { + this.setState({ + lastScrollHeight: e.target.scrollHeight, + }); + } + + if (!this.isAtTop) { + props.scrollIsAtTop(); + } + + this.isTriggeredScroll = false; + this.isAtBottom = false; + this.isAtTop = true; + } else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) { + if (!this.isAtBottom) { + props.scrollIsAtBottom(); + } + + this.isTriggeredScroll = false; + this.isAtBottom = true; + this.isAtTop = false; + } else { + this.isAtBottom = false; + this.isAtTop = false; + this.isTriggeredScroll = false; + } + } + + render() { + // Replace with just the "not Firefox" implementation + // when Firefox #1042151 is patched. + + if (navigator.userAgent.includes("Firefox")) { + return this.firefoxScrollContainer(); + } else { + return this.normalScrollContainer(); + } + } + + firefoxScrollContainer() { + return ( +
+
+
+ {this.props.children} +
+
+ ); + } + + normalScrollContainer() { + return ( +
+
+ {this.props.children} +
+ ); + } + + scrollToBottom() { + this.isTriggeredScroll = true; + if (this.scrollRef.current) { + this.scrollRef.current.scrollIntoView(false); + } + + if (navigator.userAgent.includes("Firefox")) { + recalculateScrollTop( + this.state.lastScrollHeight, + this.scrollContainer + ); + } + } + + scrollToReference(ref) { + this.isTriggeredScroll = true; + if (this.scrollRef.current && ref.current) { + ref.current.scrollIntoView({ block: 'center' }); + } + + if (navigator.userAgent.includes("Firefox")) { + recalculateScrollTop( + this.state.lastScrollHeight, + this.scrollContainer + ); + } + } + +} diff --git a/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js b/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js index 97670f23e..69df556d3 100644 --- a/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js +++ b/pkg/interface/src/apps/chat/components/lib/chat-tabbar.js @@ -1,66 +1,41 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; -export class ChatTabBar extends Component { - render() { - const props = this.props; +export const ChatTabBar = (props) => { + const { + location, + station + } = props; + let setColor = '', popout = ''; - let memColor = '', - setColor = '', - popout = ''; - - if (props.location.pathname.includes('/settings')) { - memColor = 'gray3'; - setColor = 'black white-d'; - } else if (props.location.pathname.includes('/members')) { - memColor = 'black white-d'; - setColor = 'gray3'; - } else { - memColor = 'gray3'; - setColor = 'gray3'; - } - - popout = props.location.pathname.includes('/popout') - ? 'popout/' : ''; - - const hidePopoutIcon = (this.props.popout) - ? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl'; - - return ( -
- {props.isOwner ? ( -
- - Members - -
- ) : ( -
- )} -
- - Settings - -
- - - -
- ); + if (location.pathname.includes('/settings')) { + setColor = 'black white-d'; + } else { + setColor = 'gray3'; } + + const hidePopoutIcon = (popout) + ? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl'; + + return ( +
+
+ + Settings + +
+ + + +
+ ); } diff --git a/pkg/interface/src/apps/chat/components/lib/chat-window.tsx b/pkg/interface/src/apps/chat/components/lib/chat-window.tsx new file mode 100644 index 000000000..b16f9d3ce --- /dev/null +++ b/pkg/interface/src/apps/chat/components/lib/chat-window.tsx @@ -0,0 +1,194 @@ +import React, { Component, Fragment } from "react"; + +import { ChatMessage } from './chat-message'; +import { ChatScrollContainer } from "./chat-scroll-container"; +import { UnreadNotice } from "./unread-notice"; +import { ResubscribeElement } from "./resubscribe-element"; +import { BacklogElement } from "./backlog-element"; + +const MAX_BACKLOG_SIZE = 1000; +const DEFAULT_BACKLOG_SIZE = 200; +const PAGE_SIZE = 50; +const INITIAL_LOAD = 20; + + +export class ChatWindow extends Component { + constructor(props) { + super(props); + this.state = { + numPages: 1, + }; + + this.hasAskedForMessages = false; + + this.dismissUnread = this.dismissUnread.bind(this); + this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this); + this.scrollIsAtTop = this.scrollIsAtTop.bind(this); + + this.scrollReference = React.createRef(); + this.unreadReference = React.createRef(); + } + + componentDidMount() { + this.initialFetch(); + + if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) { + this.dismissUnread(); + this.scrollToBottom(); + } + } + + initialFetch() { + const { props } = this; + if (props.messages.length > 0) { + const unreadUnloaded = props.unreadCount - props.messages.length; + + if (unreadUnloaded <= MAX_BACKLOG_SIZE && + unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) { + this.fetchBacklog(unreadUnloaded + INITIAL_LOAD); + } else { + this.fetchBacklog(DEFAULT_BACKLOG_SIZE); + } + } else { + setTimeout(() => { + this.initialFetch(); + }, 2000); + } + } + + componentDidUpdate(prevProps, prevState) { + const { props, state } = this; + + if (props.isChatMissing) { + props.history.push("/~chat"); + } else if (props.messages.length >= prevProps.messages.length + 10) { + this.hasAskedForMessages = false; + let numPages = props.unreadCount > 0 ? + Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages; + + if (this.state.numPages === numPages) { + if (props.unreadCount > 20) { + this.scrollToUnread(); + } + } else { + this.setState({ numPages }, () => { + if (props.unreadCount > 20) { + this.scrollToUnread(); + } + }); + } + } else if ( + state.numPages === 1 && + this.props.unreadCount < INITIAL_LOAD && + this.props.unreadCount > 0 + ) { + this.dismissUnread(); + this.scrollToBottom(); + } + } + + scrollIsAtTop() { + const { props, state } = this; + this.setState({ numPages: state.numPages + 1 }, () => { + if (state.numPages * PAGE_SIZE < props.length) { + this.fetchBacklog(DEFAULT_BACKLOG_SIZE); + } + }); + } + + scrollIsAtBottom() { + if (this.state.numPages !== 1) { + this.setState({ numPages: 1 }); + this.dismissUnread(); + } + } + + scrollToBottom() { + if (this.scrollReference.current) { + this.scrollReference.current.scrollToBottom(); + } + if (this.state.numPages !== 1) { + this.setState({ numPages: 1 }); + } + } + + scrollToUnread() { + if (this.scrollReference.current && this.unreadReference.current) { + this.scrollReference.current.scrollToReference(this.unreadReference); + } + } + + dismissUnread() { + this.props.api.chat.read(this.props.station); + } + + fetchBacklog(size) { + const { props } = this; + + if ( + props.messages.length >= props.length || + this.hasAskedForMessages || + props.length <= 0 + ) { + return; + } + + const start = + props.length - props.messages[props.messages.length - 1].number; + if (start > 0) { + const end = start + size < props.length ? start + size : props.length; + props.api.chat.fetchMessages(start + 1, end, props.station); + this.hasAskedForMessages = true; + } + } + + render() { + const { props, state } = this; + const sliceLength = Math.min( + state.numPages * PAGE_SIZE, + props.messages.length + props.pendingMessages.length + ); + const messages = + props.pendingMessages + .concat(props.messages) + .slice(0, sliceLength); + + return ( + + + + + + { messages.map((msg, i) => ( + 0 && + i === props.unreadCount - 1 && + state.numPages !== 1 + } + msg={msg} + previousMsg={messages[i - 1]} + nextMsg={messages[i + 1]} + association={props.association} + group={props.group} + contacts={props.contacts} /> + )) + } + + + ); + } +} + diff --git a/pkg/interface/src/apps/chat/components/lib/content/text.js b/pkg/interface/src/apps/chat/components/lib/content/text.js index ec1153b8d..f44d9bd6a 100644 --- a/pkg/interface/src/apps/chat/components/lib/content/text.js +++ b/pkg/interface/src/apps/chat/components/lib/content/text.js @@ -1,8 +1,8 @@ import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; import ReactMarkdown from 'react-markdown'; import RemarkDisableTokenizers from 'remark-disable-tokenizers'; import urbitOb from 'urbit-ob'; -import { Link } from 'react-router-dom'; const DISABLED_BLOCK_TOKENS = [ 'indentedCode', diff --git a/pkg/interface/src/apps/chat/components/lib/content/url.js b/pkg/interface/src/apps/chat/components/lib/content/url.js index e1f59dd5f..21fb6dff1 100644 --- a/pkg/interface/src/apps/chat/components/lib/content/url.js +++ b/pkg/interface/src/apps/chat/components/lib/content/url.js @@ -1,8 +1,6 @@ import React, { Component } from 'react'; - -const IMAGE_REGEX = - /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM|svg|SVG)$/; +const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i); const YOUTUBE_REGEX = new RegExp( @@ -25,8 +23,7 @@ export default class UrlContent extends Component { let unfoldState = this.state.unfold; unfoldState = !unfoldState; this.setState({ unfold: unfoldState }); - const iframe = this.refs.iframe; - iframe.setAttribute('src', iframe.getAttribute('data-src')); + this.iframe.setAttribute('src', this.iframe.dataset.src); } render() { @@ -42,13 +39,12 @@ export default class UrlContent extends Component { className="o-80-d" src={content.url} style={{ - width: '50%', - maxWidth: '250px' + maxWidth: '18rem' }} > ); return ( -