diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 8a95fc4b0e..66184d8820 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -20,27 +20,48 @@ Regardless of what you run to develop, Vite will hot-reload code changes as you ## Fakezod Development -Follow these instructions or use Tlon's own +To get started, make sure your %groups desk is mounted: + +``` +|mount %groups +``` + +Sync the latest %groups files: + +``` +rsync -avL --delete desk/* ~/urbit/zod/groups/ +``` + +And commit: + +``` +|commit %groups +``` + +Since %groups and %talk have already been released and are now in the pill. It is very unlikely that you would have to create those desks from scratch, but if you do you can follow these instructions or use Tlon's [Bouncer](https://github.com/tloncorp/bouncer) utility (requires Ruby 3+). -0. Clone or pull latest versions of this repo and `urbit/urbit`. -1. Boot a fake ship. Use local networking with `-F` like so: +1. Clone or pull latest versions of this repo, `tloncorp/landscape` and `urbit/urbit`. +2. Boot a fake ship. Use local networking with `-F` like so: `urbit -F zod` -2. Mount or create the appropriate desks on local `~zod`: - 1. `|mount %garden` - 2. `|merge %groups our %base` - 3. `|mount %groups` -3. From the `urbit/urbit` repo: - 1. `rsync -avL --delete pkg/garden/* ~/urbit/zod/garden/` +3. Create and mount the appropriate desks on local `~zod`: + 1. `|new-desk %landscape` + 2. `|mount %landscape` + 3. `|new-desk %groups` + 4. `|mount %groups` +4. From the `urbit/urbit` repo: + 1. `rsync -avL --delete pkg/base-dev/* ~/urbit/zod/landscape/` 2. `rsync -avL --delete pkg/base-dev/* ~/urbit/zod/groups/` - 3. `rsync -avL pkg/garden-dev/* ~/urbit/zod/groups/` -4. From this repo: +5. From the `tloncorp/landscape` repo: + 1. `rsync -avL desk/* ~/urbit/zod/landscape/` + 2. `rsync -avL desk-dev/* ~/urbit/zod/groups/` +6. From this repo: 1. `rsync -avL desk/* ~/urbit/zod/groups/` 2. `rsync -avL landscape-dev/* ~/urbit/zod/groups/` -5. Commit and install garden on local `~zod`: - 1. `|commit %garden` - 2. `|install our %garden` -6. Similarly commit and install Groups: +7. Commit and install landscape on local `~zod`: + 1. `|commit %landscape` + 2. `|install our %landscape` +8. Similarly commit and install Groups: 1. `|commit %groups` 2. `|install our %groups` @@ -52,9 +73,9 @@ Groups and Talk are distributed via the Urbit network by way of a [glob](https:/ 1. Create or launch an urbit using the -F flag. 2. On that urbit, if you don't already have a desk to run from, run `|merge %work our %base` to create a new desk and mount it with `|mount %work`. 3. Now the `%work` desk is accessible through the host OS's filesystem as a directory of that urbit's pier ie `~/zod/work`. -4. From the `ui` directory you can run `rsync -avL --delete dist/ ~/zod/work/groups` where `~/zod` is your fake urbit's pier. +4. From the repo you can run `rsync -avL --delete ui/dist/ ~/zod/work/groups` and `rsync -avL desk/mar/webmanifest.hoon ~/zod/work/mar/webmanifest.hoon` where `~/zod` is your fake urbit's pier. 5. Once completed, run `|commit %work` on your urbit and you should see your files logged back out from the dojo. -6. Run `=dir /=garden` to switch to the garden desk directory. +6. Run `=dir /=landscape` to switch to the landscape desk directory. 7. Run `-make-glob %work /groups`. This will create a glob from the folder where you just added files. It will output to `~/zod/.urb/put`. 8. Navigate to `~/zod/.urb/put` you should see a file that looks something like: `glob-0v5.fdf99.nph65.qecq3.ncpjn.q13mb.glob`. The characters between `glob-` and `.glob` are a hash of the glob's contents. 9. Upload the glob to any publicly available HTTP endpoint that can serve files. This allows the application to be distributed over HTTP. diff --git a/README.md b/README.md index 9f8dce4587..f0f5e5b2f8 100644 --- a/README.md +++ b/README.md @@ -33,46 +33,34 @@ images, media, and even random musings. This project uses the [formal comment spec](https://developers.urbit.org/reference/hoon/style#comments-and-unparsed-bytes) for all Hoon code to ensure compatibility with -[doccords](https://github.com/urbit/urbit/pull/5873) once support is released. +[doccords](https://github.com/urbit/urbit/pull/5873). Additionally, detailed documentation is available in the [Docs Landscape app](https://urbit.org/applications/~pocwet/docs) if you have both Docs and -Groups installed on a running Urbit ship. +Groups installed on a running Urbit ship. Visit this repository's wiki for [an overview of how to use Landscape and its apps](https://github.com/tloncorp/landscape-apps/wiki). ## Integrating with Groups agents -The `%groups` desk provides several simple agents with discrete concerns. This -list may expand over time, but new agents are unlikely for the time being. +The `%groups` desk provides several simple agents with discrete concerns. This list may expand over time. - `%groups` - The organizational substrate for constructing, joining, finding, and managing groups (different than the in-group activity of chatting, writing, or collecting) +- `%groups-ui` - Optimized scries for the Groups UI - `%chat` - 1:1 and multi-DM capabilities for Talk and Chat channels in Groups - `%diary` - Notebook channels in Groups - `%heap` - Gallery channels in Groups -- `%hark` - Notifications within Groups and Talk, and a general notification bus - for Landscape, which will eventually be moved to Landscape proper - `%notify` - Hooks for iOS push notifications +- `%grouper` - Handler for Lure invitiations -All actions are performed with -[pokes](https://developers.urbit.org/reference/glossary/poke). +All actions are performed with +[pokes](https://developers.urbit.org/reference/glossary/poke). See the on-ship developer documentation for more details. -## Use of current-day Landscape agents - -At the moment, Groups and Talk both make use of `%settings-store`, `%s3-store`, -and `%contact-store` agents in the `%landscape` desk (the historical name for -the Groups 1 app). We will eventually distribute these as part of the base -`%garden` desk (the system launcher UI we now call Landscape). Finally, we will -rename `%garden` to `%landscape`, reducing confusion everywhere. - -We have plans to replace `%contact-store`with a Groups agent (and standalone -contact + identity management app) on the tails of Tlon core devs’ [subscription -reform -efforts](https://gist.github.com/belisarius222/15bcf267689f1dd95e12005bd944608e). -Nothing is changing in the short-term, but if your app uses this store, you may -want to stay subscribed to our announcements in the [urbit-dev mailing -list](https://groups.google.com/a/urbit.org/g/dev). +## Use of Landscape agents + +At the moment, Groups and Talk make use of `%settings`, `%storage`, `%hark`, +and `%contacts` agents in the `%landscape` desk. diff --git a/desk/app/grouper.hoon b/desk/app/grouper.hoon index 3e40a5df81..5dac7655fe 100644 --- a/desk/app/grouper.hoon +++ b/desk/app/grouper.hoon @@ -10,14 +10,16 @@ [%pass /bite-wire %agent [our.bowl %reel] %watch /bites] +$ card card:agent:gall +$ versioned-state - $% state-1 + $% state-2 + state-1 state-0 == ++$ state-2 [%2 =enabled-groups =outstanding-pokes] +$ state-1 [%1 =enabled-groups =outstanding-pokes] +$ state-0 [%0 =enabled-groups] -- :: -=| state-1 +=| state-2 =* state - %- agent:dbug %+ verb | @@ -83,7 +85,9 @@ =/ group=cord i.t.t.path ?: (~(has in outstanding-pokes) [target group]) `this :_ this(outstanding-pokes (~(put in outstanding-pokes) [target group])) - ~[[%pass path %agent [target %grouper] %poke %grouper-ask-enabled !>(group)]] + :~ [%pass path %agent [target %grouper] %poke %grouper-ask-enabled !>(group)] + [%pass /expire/(scot %p our.bowl)/[group] %arvo %b [%wait (add ~h1 now.bowl)]] + == [%check-link @ @ ~] :_ this ~[[%pass path %agent [our.bowl %grouper] %poke %grouper-check-link !>(path)]] @@ -142,19 +146,37 @@ `this :: ++ on-save !>(state) +:: ++ on-load |= old-state=vase ^- (quip card _this) =/ old !<(versioned-state old-state) ?- -.old - %1 + %2 :_ this(state old) ?: (~(has by wex.bowl) [/bite-wire our.bowl %reel]) ~ ~[(bite-subscribe bowl)] + %1 + `this(state [%2 enabled-groups.old ~]) %0 - `this(state *state-1) + `this(state *state-2) + == +:: +++ on-arvo + |= [=wire =sign-arvo] + ^- (quip card _this) + ?+ wire (on-arvo:def wire sign-arvo) + [%expire @ @ ~] + ?+ sign-arvo (on-arvo:def wire sign-arvo) + [%behn %wake *] + =/ target (slav %p i.t.wire) + =/ group i.t.t.wire + ?~ error.sign-arvo + `this(outstanding-pokes (~(del in outstanding-pokes) [target group])) + (on-arvo:def wire sign-arvo) + == == -++ on-arvo on-arvo:def +:: ++ on-peek |= =path ^- (unit (unit cage)) @@ -162,5 +184,4 @@ [%x %enabled @ ~] ``json+!>([%b (~(has in enabled-groups) i.t.t.path)]) == -:: -- diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index 9c3065d336..d07d884ad0 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,9 +2,9 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Groups is a decentralized platform that integrates with Talk, Notebook, and Gallery for a full, communal suite of tools.' color+0xef.f0f4 image+'https://bootstrap.urbit.org/icon-groups.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.9dvk5.sfb7j.6ioes.rcm0v.agf38.glob' 0v6.9dvk5.sfb7j.6ioes.rcm0v.agf38] + glob-http+['https://bootstrap.urbit.org/glob-0v1.kbjm8.h44sn.0rji5.upe8g.u5l9s.glob' 0v1.kbjm8.h44sn.0rji5.upe8g.u5l9s] base+'groups' - version+[4 7 1] + version+[4 8 0] website+'https://tlon.io' license+'MIT' == diff --git a/desk/lib/diary-json.hoon b/desk/lib/diary-json.hoon index 4cd4c9d6b5..7b3d60eedd 100644 --- a/desk/lib/diary-json.hoon +++ b/desk/lib/diary-json.hoon @@ -540,6 +540,12 @@ code/so tag/so break/ul + :: + :- %task + %- ot + :~ checked/bo + content/(ar inline) + == :: :- %block %- ot diff --git a/talk/desk.docket-0 b/talk/desk.docket-0 index ba8454f3d3..5b50e95e17 100644 --- a/talk/desk.docket-0 +++ b/talk/desk.docket-0 @@ -2,9 +2,9 @@ info+'Send encrypted direct messages to one or many friends. Talk is a simple chat tool for catching up, getting work done, and everything in between.' color+0x10.5ec7 image+'https://bootstrap.urbit.org/icon-talk.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v4.q495n.b6c5b.a55im.e8u5j.1tn0c.glob' 0v4.q495n.b6c5b.a55im.e8u5j.1tn0c] + glob-http+['https://bootstrap.urbit.org/glob-0v3.5ef54.b2qo4.o8b4o.738uj.ok2k6.glob' 0v3.5ef54.b2qo4.o8b4o.738uj.ok2k6] base+'talk' - version+[4 7 1] + version+[4 8 0] website+'https://tlon.io' license+'MIT' == diff --git a/ui/package-lock.json b/ui/package-lock.json index 27b2ad1922..0b9a3aec05 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -58,12 +58,13 @@ "@tloncorp/mock-http-api": "^1.2.0", "@types/marked": "^4.3.0", "@urbit/api": "^2.2.0", - "@urbit/aura": "^0.4.0", + "@urbit/aura": "^1.0.0", "@urbit/http-api": "^3.0.0", "@urbit/sigil-js": "^2.1.0", "any-ascii": "^0.3.1", "big-integer": "^1.6.51", "browser-cookies": "^1.2.0", + "browser-image-compression": "^2.0.2", "classnames": "^2.3.1", "clipboard-copy": "^4.0.1", "color2k": "^2.0.0", @@ -94,7 +95,6 @@ "prosemirror-state": "~1.3.4", "prosemirror-transform": "~1.4.2", "prosemirror-view": "~1.23.13", - "qrcode": "^1.5.3", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-colorful": "^5.5.1", @@ -108,6 +108,7 @@ "react-image-size": "^2.0.0", "react-intersection-observer": "^9.4.0", "react-oembed-container": "github:stefkampen/react-oembed-container", + "react-qr-code": "^2.0.12", "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-select": "^5.3.2", @@ -7956,13 +7957,15 @@ } }, "node_modules/@urbit/aura": { - "version": "0.4.0", - "license": "MIT", - "dependencies": { - "big-integer": "^1.6.51" - }, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@urbit/aura/-/aura-1.0.0.tgz", + "integrity": "sha512-IeP3uoDzZ0Rpn345auXK0y/BCcXTmpgAlOPbgf7n4eD35h56OnSoit1kuXKA21sWE19gFjK/wqZcz5ULjz2ADg==", "engines": { - "node": ">=10" + "node": ">=16", + "npm": ">=8" + }, + "peerDependencies": { + "big-integer": "^1.6.51" } }, "node_modules/@urbit/eslint-config": { @@ -8571,6 +8574,7 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9051,6 +9055,14 @@ "version": "1.2.0", "license": "Unlicence" }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browser-or-node": { "version": "1.3.0", "license": "MIT" @@ -9164,14 +9176,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "license": "MIT", @@ -9366,59 +9370,6 @@ ], "license": "MIT" }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/cliui/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/clone": { "version": "1.0.4", "dev": true, @@ -9954,14 +9905,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.4.0", "dev": true, @@ -10123,11 +10066,6 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, "node_modules/dir-glob": { "version": "3.0.1", "dev": true, @@ -10231,11 +10169,6 @@ "node": ">= 4" } }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" - }, "node_modules/encodeurl": { "version": "1.0.2", "dev": true, @@ -11805,6 +11738,7 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -12592,6 +12526,7 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14788,6 +14723,7 @@ }, "node_modules/p-try": { "version": "2.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14994,6 +14930,7 @@ }, "node_modules/path-exists": { "version": "4.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15215,14 +15152,6 @@ "node": ">=14" } }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/popmotion": { "version": "11.0.3", "license": "MIT", @@ -15721,96 +15650,10 @@ "node": ">=6" } }, - "node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qrcode/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" }, "node_modules/qs": { "version": "6.11.0", @@ -16223,6 +16066,24 @@ "react-dom": "^16.2.0 || ^17.0.0 || ^18" } }, + "node_modules/react-qr-code": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.12.tgz", + "integrity": "sha512-k+pzP5CKLEGBRwZsDPp98/CAJeXlsYRHM2iZn1Sd5Th/HnKhIZCSg27PXO58zk8z02RaEryg+60xa4vyywMJwg==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x", + "react-native-svg": "*" + }, + "peerDependenciesMeta": { + "react-native-svg": { + "optional": true + } + } + }, "node_modules/react-redux": { "version": "7.2.8", "license": "MIT", @@ -16709,6 +16570,7 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16722,11 +16584,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, "node_modules/requires-port": { "version": "1.0.0", "dev": true, @@ -17022,11 +16879,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/set-cookie-parser": { "version": "2.5.1", "dev": true, @@ -17228,6 +17080,7 @@ }, "node_modules/string-width": { "version": "4.2.3", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -17259,6 +17112,7 @@ }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", + "dev": true, "license": "MIT" }, "node_modules/string.prototype.matchall": { @@ -17320,6 +17174,7 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18213,6 +18068,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "dev": true, @@ -18768,11 +18628,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, "node_modules/which-typed-array": { "version": "1.1.11", "dev": true, @@ -19275,18 +19130,6 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/yargs/node_modules/cliui": { "version": "8.0.1", "dev": true, @@ -24128,10 +23971,10 @@ } }, "@urbit/aura": { - "version": "0.4.0", - "requires": { - "big-integer": "^1.6.51" - } + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@urbit/aura/-/aura-1.0.0.tgz", + "integrity": "sha512-IeP3uoDzZ0Rpn345auXK0y/BCcXTmpgAlOPbgf7n4eD35h56OnSoit1kuXKA21sWE19gFjK/wqZcz5ULjz2ADg==", + "requires": {} }, "@urbit/eslint-config": { "version": "1.0.3", @@ -24554,7 +24397,8 @@ } }, "ansi-regex": { - "version": "5.0.1" + "version": "5.0.1", + "dev": true }, "ansi-styles": { "version": "3.2.1", @@ -24852,6 +24696,14 @@ "browser-cookies": { "version": "1.2.0" }, + "browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "requires": { + "uzip": "0.20201231.0" + } + }, "browser-or-node": { "version": "1.3.0" }, @@ -24903,11 +24755,6 @@ "callsites": { "version": "3.1.0" }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, "camelcase-css": { "version": "2.0.1" }, @@ -25004,49 +24851,6 @@ "clipboard-copy": { "version": "4.0.1" }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, "clone": { "version": "1.0.4", "dev": true @@ -25371,11 +25175,6 @@ "ms": "2.1.2" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" - }, "decimal.js": { "version": "10.4.0", "dev": true @@ -25476,11 +25275,6 @@ "version": "28.1.1", "dev": true }, - "dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, "dir-glob": { "version": "3.0.1", "dev": true, @@ -25552,11 +25346,6 @@ "version": "3.0.0", "dev": true }, - "encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" - }, "encodeurl": { "version": "1.0.2", "dev": true @@ -26571,7 +26360,8 @@ "version": "1.0.0-beta.2" }, "get-caller-file": { - "version": "2.0.5" + "version": "2.0.5", + "dev": true }, "get-func-name": { "version": "2.0.0", @@ -27037,7 +26827,8 @@ "version": "2.1.1" }, "is-fullwidth-code-point": { - "version": "3.0.0" + "version": "3.0.0", + "dev": true }, "is-generator-function": { "version": "1.0.10", @@ -28393,7 +28184,8 @@ } }, "p-try": { - "version": "2.2.0" + "version": "2.2.0", + "dev": true }, "parent-module": { "version": "1.0.1", @@ -28522,7 +28314,8 @@ } }, "path-exists": { - "version": "4.0.0" + "version": "4.0.0", + "dev": true }, "path-is-absolute": { "version": "1.0.1", @@ -28644,11 +28437,6 @@ "version": "1.33.0", "dev": true }, - "pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" - }, "popmotion": { "version": "11.0.3", "requires": { @@ -28936,74 +28724,10 @@ "version": "2.1.1", "dev": true }, - "qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "requires": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - } - } + "qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" }, "qs": { "version": "6.11.0", @@ -29269,6 +28993,15 @@ "prop-types": "^15.6.0" } }, + "react-qr-code": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.12.tgz", + "integrity": "sha512-k+pzP5CKLEGBRwZsDPp98/CAJeXlsYRHM2iZn1Sd5Th/HnKhIZCSg27PXO58zk8z02RaEryg+60xa4vyywMJwg==", + "requires": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + } + }, "react-redux": { "version": "7.2.8", "requires": { @@ -29553,17 +29286,13 @@ "version": "0.4.2" }, "require-directory": { - "version": "2.1.1" + "version": "2.1.1", + "dev": true }, "require-from-string": { "version": "2.0.2", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, "requires-port": { "version": "1.0.0", "dev": true @@ -29759,11 +29488,6 @@ "send": "0.18.0" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "set-cookie-parser": { "version": "2.5.1", "dev": true @@ -29906,6 +29630,7 @@ }, "string-width": { "version": "4.2.3", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -29913,7 +29638,8 @@ }, "dependencies": { "emoji-regex": { - "version": "8.0.0" + "version": "8.0.0", + "dev": true } } }, @@ -29975,6 +29701,7 @@ }, "strip-ansi": { "version": "6.0.1", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -30502,6 +30229,11 @@ "uuid": { "version": "9.0.0" }, + "uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" + }, "v8-compile-cache": { "version": "2.3.0", "dev": true @@ -30819,11 +30551,6 @@ "is-weakset": "^2.0.1" } }, - "which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, "which-typed-array": { "version": "1.1.11", "dev": true, @@ -31197,15 +30924,6 @@ } } }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, "yocto-queue": { "version": "0.1.0", "dev": true diff --git a/ui/package.json b/ui/package.json index 84e8392535..5fbb246a9c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -105,12 +105,13 @@ "@tloncorp/mock-http-api": "^1.2.0", "@types/marked": "^4.3.0", "@urbit/api": "^2.2.0", - "@urbit/aura": "^0.4.0", + "@urbit/aura": "^1.0.0", "@urbit/http-api": "^3.0.0", "@urbit/sigil-js": "^2.1.0", "any-ascii": "^0.3.1", "big-integer": "^1.6.51", "browser-cookies": "^1.2.0", + "browser-image-compression": "^2.0.2", "classnames": "^2.3.1", "clipboard-copy": "^4.0.1", "color2k": "^2.0.0", @@ -141,7 +142,6 @@ "prosemirror-state": "~1.3.4", "prosemirror-transform": "~1.4.2", "prosemirror-view": "~1.23.13", - "qrcode": "^1.5.3", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-colorful": "^5.5.1", @@ -155,6 +155,7 @@ "react-image-size": "^2.0.0", "react-intersection-observer": "^9.4.0", "react-oembed-container": "github:stefkampen/react-oembed-container", + "react-qr-code": "^2.0.12", "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-select": "^5.3.2", diff --git a/ui/src/api.ts b/ui/src/api.ts index f7cebc60e3..df84aa4754 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -191,20 +191,22 @@ class API { return this.withErrorHandling( (client) => new Promise((resolve, reject) => { - client.poke({ - ...params, - onError: (e) => { - params.onError?.(e); - reject(); - }, - onSuccess: async () => { - params.onSuccess?.(); - const defaultValidator = (event: any) => - _.isEqual(params.json, event); - await this.track(subscription, validator || defaultValidator); - resolve(); - }, - }); + client + .poke({ + ...params, + onError: (e) => { + params.onError?.(e); + reject(); + }, + onSuccess: async () => { + params.onSuccess?.(); + const defaultValidator = (event: any) => + _.isEqual(params.json, event); + await this.track(subscription, validator || defaultValidator); + resolve(); + }, + }) + .catch(reject); }) ); } diff --git a/ui/src/app.tsx b/ui/src/app.tsx index a758cd3ca2..aa6a842024 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -1,6 +1,6 @@ // Copyright 2022, Tlon Corporation import cookies from 'browser-cookies'; -import React, { Suspense, useEffect, useState } from 'react'; +import React, { Suspense, useEffect, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet'; import _ from 'lodash'; import { @@ -104,6 +104,8 @@ import MobileChatSearch from './chat/ChatSearch/MobileChatSearch'; import BlockedUsersView from './components/Settings/BlockedUsersView'; import BlockedUsersDialog from './components/Settings/BlockedUsersDialog'; import { ChatInputFocusProvider } from './logic/ChatInputFocusContext'; +import UpdateNoticeSheet from './components/UpdateNotices'; +import useAppUpdates, { AppUpdateContext } from './logic/useAppUpdates'; const ReactQueryDevtoolsProduction = React.lazy(() => import('@tanstack/react-query-devtools/build/lib/index.prod.js').then( @@ -209,6 +211,12 @@ function ChatRoutes({ state, location, isMobile, isSmall }: RoutesProps) { element={} /> )} + {isMobile && ( + } + /> + )} }> @@ -226,6 +234,9 @@ function ChatRoutes({ state, location, isMobile, isSmall }: RoutesProps) { element={} /> ) : null} + {isMobile && ( + } /> + )} @@ -598,6 +609,9 @@ function GroupsRoutes({ state, location, isMobile, isSmall }: RoutesProps) { /> ) : null} + {isMobile && ( + } /> + )} ) : null} @@ -736,9 +750,15 @@ function RoutedApp() { const logActivity = useLogActivity(); const posthog = usePostHog(); const analyticsId = useAnalyticsId(); + const { needsUpdate, triggerUpdate } = useAppUpdates(); const body = document.querySelector('body'); const colorSchemeFromNative = window.colorscheme; + const appUpdateContextValue = useMemo( + () => ({ needsUpdate, triggerUpdate }), + [needsUpdate, triggerUpdate] + ); + const basename = (appName: string) => { if (mode === 'mock' || mode === 'staging') { return '/'; @@ -815,10 +835,12 @@ function RoutedApp() { /> - - - - + + + + + + {showDevTools && ( <> diff --git a/ui/src/channels/NewChannel/NewChannelForm.tsx b/ui/src/channels/NewChannel/NewChannelForm.tsx index 63b825b74b..0625b73688 100644 --- a/ui/src/channels/NewChannel/NewChannelForm.tsx +++ b/ui/src/channels/NewChannel/NewChannelForm.tsx @@ -26,8 +26,7 @@ export default function NewChannelForm() { const { compatible, text } = useGroupCompatibility(groupFlag); const shelf = useDiaries(); const stash = useStash(); - const { mutate: mutateAddChannel, status: addChannelStatus } = - useAddChannelMutation(); + const { mutate: mutateAddChannel } = useAddChannelMutation(); const { mutateAsync: createDiary } = useCreateDiaryMutation(); const { mutateAsync: createHeap } = useCreateHeapMutation(); const defaultValues: NewChannelFormSchema = { @@ -188,16 +187,12 @@ export default function NewChannelForm() { !compatible || !form.formState.isValid || !form.formState.isDirty || - addChannelStatus === 'loading' || - addChannelStatus === 'success' || - addChannelStatus === 'error' + form.formState.isSubmitting } > - {addChannelStatus === 'loading' ? ( + {form.formState.isSubmitting ? ( - ) : addChannelStatus === 'error' ? ( - 'Error' - ) : addChannelStatus === 'success' ? ( + ) : form.formState.isSubmitted ? ( 'Saved' ) : ( 'Add Channel' diff --git a/ui/src/chat/ChatChannel.tsx b/ui/src/chat/ChatChannel.tsx index cdb83e112b..65c6acef64 100644 --- a/ui/src/chat/ChatChannel.tsx +++ b/ui/src/chat/ChatChannel.tsx @@ -1,6 +1,7 @@ import cn from 'classnames'; import React, { useEffect, useState, useCallback, useRef } from 'react'; import { Route, Routes, useMatch, useNavigate, useParams } from 'react-router'; +import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import ChatInput from '@/chat/ChatInput/ChatInput'; import ChatWindow from '@/chat/ChatWindow'; @@ -15,9 +16,13 @@ import { } from '@/state/groups/groups'; import ChannelHeader from '@/channels/ChannelHeader'; import useRecentChannel from '@/logic/useRecentChannel'; -import { canReadChannel, canWriteChannel, isTalk } from '@/logic/utils'; +import { + canReadChannel, + canWriteChannel, + isGroups, + isTalk, +} from '@/logic/utils'; import { useLastReconnect } from '@/state/local'; -import { Link } from 'react-router-dom'; import MagnifyingGlassIcon from '@/components/icons/MagnifyingGlassIcon'; import useMedia, { useIsMobile } from '@/logic/useMedia'; import ChannelTitleButton from '@/channels/ChannelTitleButton'; @@ -66,10 +71,10 @@ function ChatChannel({ title }: ViewProps) { const isMobile = useIsMobile(); const scrollElementRef = useRef(null); const isScrolling = useIsScrolling(scrollElementRef); + const root = `/groups/${groupFlag}/channels/${nest}`; // We only inset the bottom for groups, since DMs display the navbar // underneath this view - const root = `/groups/${groupFlag}/channels/${nest}`; - const shouldApplyPaddingBottom = isMobile && !isChatInputFocused; + const shouldApplyPaddingBottom = isGroups && isMobile && !isChatInputFocused; const joinChannel = useCallback(async () => { setJoining(true); diff --git a/ui/src/chat/ChatInput/ChatInput.tsx b/ui/src/chat/ChatInput/ChatInput.tsx index 6ca308a1fd..80d117bdcc 100644 --- a/ui/src/chat/ChatInput/ChatInput.tsx +++ b/ui/src/chat/ChatInput/ChatInput.tsx @@ -17,7 +17,7 @@ import { useShipHasBlockedUs, useUnblockShipMutation, } from '@/state/chat'; -import { ChatImage, ChatMemo, Cite } from '@/types/chat'; +import { ChatBlock, ChatImage, ChatMemo, Cite } from '@/types/chat'; import MessageEditor, { HandlerParams, useMessageEditor, @@ -26,6 +26,7 @@ import Avatar from '@/components/Avatar'; import ShipName from '@/components/ShipName'; import X16Icon from '@/components/icons/X16Icon'; import { + chatStoreLogger, fetchChatBlocks, useChatInfo, useChatStore, @@ -122,6 +123,7 @@ export default function ChatInput({ [targetId, dropZoneId] ); const id = replying ? `${whom}-${replying}` : whom; + chatStoreLogger.log('InputRender', id); const [draft, setDraft] = useLocalStorage( createStorageKey(`chat-${id}`), inlinesToJSON(['']) @@ -199,6 +201,7 @@ export default function ChatInput({ }, [mostRecentFile]); const clearAttachments = useCallback(() => { + chatStoreLogger.log('clearAttachments', { id, uploadKey }); useChatStore.getState().setBlocks(id, []); useFileStore.getState().getUploader(uploadKey)?.clear(); if (replyCite) { @@ -225,6 +228,7 @@ export default function ChatInput({ const uploadType = useFileStore.getState().getUploadType(uploadKey); if (isTargetId && uploadType === 'drag' && didDrop) { + chatStoreLogger.log('DragUpload', { id, files }); // TODO: handle existing blocks (other refs) useChatStore.getState().setBlocks( id, @@ -241,6 +245,7 @@ export default function ChatInput({ } if (uploadType !== 'drag') { + chatStoreLogger.log('Upload', { id, files }); // TODO: handle existing blocks (other refs) useChatStore.getState().setBlocks( id, @@ -508,6 +513,7 @@ export default function ChatInput({ return; } setBlocks(id, [{ cite }]); + chatStoreLogger.log('AndroidPaste', { id, cite }); messageEditor.commands.deleteRange({ from: editorText.indexOf(path), to: editorText.indexOf(path) + path.length + 1, @@ -550,6 +556,7 @@ export default function ChatInput({ // @ts-expect-error type check on previous line uploader.removeByURL(blocks[idx].image.src); } + chatStoreLogger.log('onRemove', { id, blocks }); useChatStore.getState().setBlocks( id, blocks.filter((_b, k) => k !== idx) @@ -560,6 +567,7 @@ export default function ChatInput({ // @ts-expect-error tsc is not tracking the type narrowing in the filter const imageBlocks: ChatImage[] = chatInfo.blocks.filter((b) => 'image' in b); + // chatStoreLogger.log('ChatInputRender', id, chatInfo); if (shipHasBlockedUs) { return ( diff --git a/ui/src/chat/ChatThread/ChatThread.tsx b/ui/src/chat/ChatThread/ChatThread.tsx index 0846f0243a..e2f5d74ac2 100644 --- a/ui/src/chat/ChatThread/ChatThread.tsx +++ b/ui/src/chat/ChatThread/ChatThread.tsx @@ -13,7 +13,7 @@ import ChatInput from '@/chat/ChatInput/ChatInput'; import BranchIcon from '@/components/icons/BranchIcon'; import X16Icon from '@/components/icons/X16Icon'; import ChatScroller from '@/chat/ChatScroller/ChatScroller'; -import { whomIsFlag } from '@/logic/utils'; +import { isGroups, whomIsFlag } from '@/logic/utils'; import useLeap from '@/components/Leap/useLeap'; import { useIsMobile } from '@/logic/useMedia'; import keyMap from '@/keyMap'; @@ -70,7 +70,7 @@ export default function ChatThread() { perms.writers.length === 0 || _.intersection(perms.writers, vessel.sects).length !== 0; const { compatible, text } = useChannelCompatibility(`chat/${flag}`); - const shouldApplyPaddingBottom = isMobile && !isChatInputFocused; + const shouldApplyPaddingBottom = isGroups && isMobile && !isChatInputFocused; const returnURL = useCallback(() => { if (!time || !writ) return '#'; diff --git a/ui/src/chat/ChatWindow.tsx b/ui/src/chat/ChatWindow.tsx index d69f6138ea..7a7b254f1a 100644 --- a/ui/src/chat/ChatWindow.tsx +++ b/ui/src/chat/ChatWindow.tsx @@ -5,7 +5,7 @@ import { useMatch, useSearchParams } from 'react-router-dom'; import { VirtuosoHandle } from 'react-virtuoso'; import ChatUnreadAlerts from '@/chat/ChatUnreadAlerts'; import { - useChatInitialized, + useChatLoading, useChatState, useMessagesForChat, useWritWindow, @@ -51,7 +51,7 @@ export default function ChatWindow({ const [searchParams, setSearchParams] = useSearchParams(); const msg = searchParams.get('msg'); const scrollTo = getScrollTo(whom, thread, msg); - const initialized = useChatInitialized(whom); + const isLoading = useChatLoading(whom); const messages = useMessagesForChat(whom, scrollTo); const window = useWritWindow(whom); const scrollerRef = useRef(null); @@ -83,7 +83,7 @@ export default function ChatWindow({ [readTimeout, whom] ); - if (!initialized) { + if (isLoading) { return (
diff --git a/ui/src/chat/useChatStore.ts b/ui/src/chat/useChatStore.ts index c1363b155b..97a7f08716 100644 --- a/ui/src/chat/useChatStore.ts +++ b/ui/src/chat/useChatStore.ts @@ -1,3 +1,4 @@ +import { createDevLogger } from '@/logic/utils'; import { ChatBlock, ChatBrief, ChatBriefs } from '@/types/chat'; import produce from 'immer'; import { useCallback } from 'react'; @@ -45,14 +46,16 @@ export interface ChatStore { update: (briefs: ChatBriefs) => void; } -const emptyInfo: ChatInfo = { +const emptyInfo: () => ChatInfo = () => ({ replying: null, blocks: [], unread: undefined, dialogs: {}, hovering: '', failedToLoadContent: {}, -}; +}); + +export const chatStoreLogger = createDevLogger('ChatStore', false); export const useChatStore = create((set, get) => ({ chats: {}, @@ -61,9 +64,10 @@ export const useChatStore = create((set, get) => ({ produce((draft: ChatStore) => { Object.entries(briefs).forEach(([whom, brief]) => { const chat = draft.chats[whom]; + chatStoreLogger.log('update', whom, chat, brief, draft.chats); if (brief.count > 0 && brief['read-id']) { draft.chats[whom] = { - ...(chat || emptyInfo), + ...(chat || emptyInfo()), unread: { seen: false, readTimeout: 0, @@ -88,9 +92,10 @@ export const useChatStore = create((set, get) => ({ set( produce((draft) => { if (!draft.chats[whom]) { - draft.chats[whom] = emptyInfo; + draft.chats[whom] = emptyInfo(); } + chatStoreLogger.log('setBlocks', whom, blocks); draft.chats[whom].blocks = blocks; }) ); @@ -99,7 +104,7 @@ export const useChatStore = create((set, get) => ({ set( produce((draft) => { if (!draft.chats[whom]) { - draft.chats[whom] = emptyInfo; + draft.chats[whom] = emptyInfo(); } draft.chats[whom].dialogs[writId] = dialogs; @@ -110,7 +115,7 @@ export const useChatStore = create((set, get) => ({ set( produce((draft) => { if (!draft.chats[whom]) { - draft.chats[whom] = emptyInfo; + draft.chats[whom] = emptyInfo(); } if (!draft.chats[whom].failedToLoadContent[writId]) { @@ -126,7 +131,7 @@ export const useChatStore = create((set, get) => ({ set( produce((draft) => { if (!draft.chats[whom]) { - draft.chats[whom] = emptyInfo; + draft.chats[whom] = emptyInfo(); } draft.chats[whom].hovering = hovering ? writId : ''; @@ -137,7 +142,7 @@ export const useChatStore = create((set, get) => ({ set( produce((draft) => { if (!draft.chats[whom]) { - draft.chats[whom] = emptyInfo; + draft.chats[whom] = emptyInfo(); } draft.chats[whom].replying = msgId; @@ -148,7 +153,7 @@ export const useChatStore = create((set, get) => ({ set( produce((draft: ChatStore) => { if (!draft.chats[whom]) { - draft.chats[whom] = emptyInfo; + draft.chats[whom] = emptyInfo(); } const chat = draft.chats[whom]; @@ -172,13 +177,14 @@ export const useChatStore = create((set, get) => ({ return; } + chatStoreLogger.log('read', whom, chat); delete chat.unread; }) ); }, delayedRead: (whom, cb) => { const { chats, read } = get(); - const chat = chats[whom] || emptyInfo; + const chat = chats[whom] || emptyInfo(); if (!chat.unread || chat.unread.readTimeout) { return; @@ -191,10 +197,12 @@ export const useChatStore = create((set, get) => ({ set( produce((draft) => { + const latest = draft.chats[whom] || emptyInfo(); + chatStoreLogger.log('delayedRead', whom, chat, latest); draft.chats[whom] = { - ...chat, + ...latest, unread: { - ...chat.unread, + ...latest.unread, readTimeout, }, }; @@ -204,8 +212,8 @@ export const useChatStore = create((set, get) => ({ unread: (whom, brief) => { set( produce((draft: ChatStore) => { - const chat = draft.chats[whom] || emptyInfo; - + const chat = draft.chats[whom] || emptyInfo(); + chatStoreLogger.log('unread', whom, chat, brief); draft.chats[whom] = { ...chat, unread: { diff --git a/ui/src/components/Mention/MentionPopup.tsx b/ui/src/components/Mention/MentionPopup.tsx index 4062746d15..5501734c19 100644 --- a/ui/src/components/Mention/MentionPopup.tsx +++ b/ui/src/components/Mention/MentionPopup.tsx @@ -18,6 +18,7 @@ import { useGroup, useGroupFlag } from '@/state/groups'; import { useMultiDms } from '@/state/chat'; import { preSig } from '@/logic/utils'; import keyMap from '@/keyMap'; +import { PluginKey } from '@tiptap/pm/state'; import Avatar from '../Avatar'; import ShipName from '../ShipName'; import useLeap from '../Leap/useLeap'; @@ -184,114 +185,117 @@ function scoreEntry(filter: string, entry: fuzzy.FilterResult): number { return score; } -const MentionPopup: Partial = { - char: '~', - items: ({ query }) => { - const { contacts } = useContactState.getState(); - const sigged = preSig(query); - const valid = isValidPatp(sigged); - - const contactNames = Object.keys(contacts); - - // fuzzy search both nicknames and patps; fuzzy#filter only supports - // string comparision, so concat nickname + patp - const searchSpace = Object.entries(contacts).map(([patp, contact]) => - `${normalizeText(contact?.nickname || '')}${patp}`.toLocaleLowerCase() - ); - - if (valid && !contactNames.includes(sigged)) { - contactNames.push(sigged); - searchSpace.push(sigged); - } +export default function getMentionPopup( + triggerChar: string +): Partial { + return { + char: triggerChar, + pluginKey: new PluginKey(`${triggerChar}-mention-popup`), + items: ({ query }) => { + const { contacts } = useContactState.getState(); + const sigged = preSig(query); + const valid = isValidPatp(sigged); + + const contactNames = Object.keys(contacts); + + // fuzzy search both nicknames and patps; fuzzy#filter only supports + // string comparision, so concat nickname + patp + const searchSpace = Object.entries(contacts).map(([patp, contact]) => + `${normalizeText(contact?.nickname || '')}${patp}`.toLocaleLowerCase() + ); + + if (valid && !contactNames.includes(sigged)) { + contactNames.push(sigged); + searchSpace.push(sigged); + } - const normQuery = normalizeText(query).toLocaleLowerCase(); - const fuzzyNames = fuzzy.filter(normQuery, searchSpace).sort((a, b) => { - const filter = deSig(query) || ''; - const right = scoreEntry(filter, b); - const left = scoreEntry(filter, a); - return right - left; - }); + const normQuery = normalizeText(query).toLocaleLowerCase(); + const fuzzyNames = fuzzy.filter(normQuery, searchSpace).sort((a, b) => { + const filter = deSig(query) || ''; + const right = scoreEntry(filter, b); + const left = scoreEntry(filter, a); + return right - left; + }); - const items = fuzzyNames - .slice(0, 5) - .map((entry) => ({ id: contactNames[entry.index] })); + const items = fuzzyNames + .slice(0, 5) + .map((entry) => ({ id: contactNames[entry.index] })); - if (isNativeApp()) { - items.reverse(); - } + if (isNativeApp()) { + items.reverse(); + } - return items; - }, - - render: () => { - let component: ReactRenderer< - MentionListHandle, - SuggestionProps<{ id: string }> - >; - let popup: any; - return { - onStart: (props) => { - component = new ReactRenderer< - MentionListHandle, - SuggestionProps<{ id: string }> - >(MentionList, { props, editor: props.editor }); - - if (!props.clientRect) { - return; - } + return items; + }, - popup = tippy('body', { - getReferenceClientRect: props.clientRect as any, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: isNativeApp() ? 'top' : 'top-start', - onMount: ({ popperInstance }) => { - popperInstance?.setOptions({ - placement: isNativeApp() ? 'top' : 'top-start', - modifiers: [{ name: 'flip', enabled: false }], - }); - }, - onAfterUpdate: ({ popperInstance }) => { - popperInstance?.setOptions({ - placement: isNativeApp() ? 'top' : 'top-start', - modifiers: [{ name: 'flip', enabled: false }], - }); - }, - }); - }, - onUpdate: (props) => { - component.updateProps(props); - - if (DISALLOWED_MENTION_CHARS.test(props.query)) { + render: () => { + let component: ReactRenderer< + MentionListHandle, + SuggestionProps<{ id: string }> + >; + let popup: any; + return { + onStart: (props) => { + component = new ReactRenderer< + MentionListHandle, + SuggestionProps<{ id: string }> + >(MentionList, { props, editor: props.editor }); + + if (!props.clientRect) { + return; + } + + popup = tippy('body', { + getReferenceClientRect: props.clientRect as any, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: isNativeApp() ? 'top' : 'top-start', + onMount: ({ popperInstance }) => { + popperInstance?.setOptions({ + placement: isNativeApp() ? 'top' : 'top-start', + modifiers: [{ name: 'flip', enabled: false }], + }); + }, + onAfterUpdate: ({ popperInstance }) => { + popperInstance?.setOptions({ + placement: isNativeApp() ? 'top' : 'top-start', + modifiers: [{ name: 'flip', enabled: false }], + }); + }, + }); + }, + onUpdate: (props) => { + component.updateProps(props); + + if (DISALLOWED_MENTION_CHARS.test(props.query)) { + popup?.[0]?.destroy(); + component?.destroy(); + return; + } + + if (!props.clientRect) { + return; + } + + popup?.[0]?.setProps({ + getBoundingClientRect: props.clientRect, + }); + }, + onKeyDown: (props) => { + if (props.event.key === keyMap.mentionPopup.close) { + popup?.[0]?.hide(); + return true; + } + return component?.ref?.onKeyDown(props.event) || false; + }, + onExit: () => { popup?.[0]?.destroy(); component?.destroy(); - return; - } - - if (!props.clientRect) { - return; - } - - popup?.[0]?.setProps({ - getBoundingClientRect: props.clientRect, - }); - }, - onKeyDown: (props) => { - if (props.event.key === keyMap.mentionPopup.close) { - popup?.[0]?.hide(); - return true; - } - return component?.ref?.onKeyDown(props.event) || false; - }, - onExit: () => { - popup?.[0]?.destroy(); - component?.destroy(); - }, - }; - }, -}; - -export default MentionPopup; + }, + }; + }, + }; +} diff --git a/ui/src/components/MessageEditor.tsx b/ui/src/components/MessageEditor.tsx index 6044f269aa..7c96a6f248 100644 --- a/ui/src/components/MessageEditor.tsx +++ b/ui/src/components/MessageEditor.tsx @@ -17,7 +17,11 @@ import HardBreak from '@tiptap/extension-hard-break'; import { useIsMobile } from '@/logic/useMedia'; import ChatInputMenu from '@/chat/ChatInputMenu/ChatInputMenu'; import { refPasteRule, Shortcuts } from '@/logic/tiptap'; -import { useChatBlocks, useChatStore } from '@/chat/useChatStore'; +import { + chatStoreLogger, + useChatBlocks, + useChatStore, +} from '@/chat/useChatStore'; import { useCalm } from '@/state/settings'; import Mention from '@tiptap/extension-mention'; import { PASTEABLE_IMAGE_TYPES } from '@/constants'; @@ -25,7 +29,7 @@ import { useFileStore } from '@/state/storage'; import { Cite } from '@/types/chat'; import { EditorView } from '@tiptap/pm/view'; import { Slice } from '@tiptap/pm/model'; -import MentionPopup from './Mention/MentionPopup'; +import getMentionPopup from './Mention/MentionPopup'; export interface HandlerParams { editor: Editor; @@ -70,6 +74,7 @@ export function useMessageEditor({ return; } setBlocks(whom, [...chatBlocks, { cite: r }]); + chatStoreLogger.log('onReference', { whom, r, chatBlocks }); }, [chatBlocks, setBlocks, whom] ); @@ -156,7 +161,17 @@ export function useMessageEditor({ HTMLAttributes: { class: 'inline-block rounded bg-blue-soft px-1.5 py-0 text-blue', }, - suggestion: MentionPopup, + renderLabel: (props) => `~${props.node.attrs.id}`, + suggestion: getMentionPopup('~'), + }) + ); + extensions.unshift( + Mention.extend({ priority: 999 }).configure({ + HTMLAttributes: { + class: 'inline-block rounded bg-blue-soft px-1.5 py-0 text-blue', + }, + renderLabel: (props) => `~${props.node.attrs.id}`, + suggestion: getMentionPopup('@'), }) ); } diff --git a/ui/src/components/QRWidget.tsx b/ui/src/components/QRWidget.tsx new file mode 100644 index 0000000000..10fa0799ac --- /dev/null +++ b/ui/src/components/QRWidget.tsx @@ -0,0 +1,114 @@ +import { useMemo } from 'react'; +import cn from 'classnames'; +import QRCode from 'react-qr-code'; +import { useCopy } from '@/logic/utils'; +import CopyIcon from '@/components/icons/CopyIcon'; +import { useCurrentTheme } from '@/state/local'; +import CheckIcon from './icons/CheckIcon'; +import ShareIcon from './icons/ShareIcon'; +import LoadingSpinner from './LoadingSpinner/LoadingSpinner'; + +export default function QRWidget({ + link, + className, + navigatorTitle, +}: { + link: string; + className?: string; + navigatorTitle: string; +}) { + const url = useMemo(() => new URL(link), [link]); + const { didCopy, doCopy } = useCopy(link); + const displayURL = url.hostname + url.pathname; + + const handleCopy = () => { + if (navigator.share !== undefined) { + navigator.share({ + title: navigatorTitle, + url: link, + }); + } else { + doCopy(); + } + }; + + return ( +
+
+ +
+
+ {displayURL} + {navigator.share !== undefined ? ( + + ) : didCopy ? ( + + ) : ( + + )} +
+
+ ); +} + +export function QRWidgetPlaceholder({ + link, + className, + type = 'loading', + errorMessage, +}: { + link?: string; + className?: string; + type?: 'loading' | 'error'; + errorMessage?: string; +}) { + const theme = useCurrentTheme(); + const value = + link || (type === 'loading' ? 'Invite loading...' : 'Invite Link Error'); + const junkQR = 'https://tlon.io?noise=placeholdeplaceholderplaceholderpla'; + const message = errorMessage || 'Something appears to have gone wrong'; + + const fgColor = theme === 'light' ? '#E5E5E5' : '#333333'; + const bgColor = theme === 'light' ? '#808080' : '#999999'; + + return ( +
+
+ {type === 'loading' && ( + + )} + {type === 'error' && ( +
+

{message}

+
+ )} +
+
+ {value} + {type === 'loading' && ( + + )} +
+
+ ); +} diff --git a/ui/src/components/Sidebar/GangItem.tsx b/ui/src/components/Sidebar/GangItem.tsx index 18880fb6a8..423f606a02 100644 --- a/ui/src/components/Sidebar/GangItem.tsx +++ b/ui/src/components/Sidebar/GangItem.tsx @@ -1,24 +1,34 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import * as Popover from '@radix-ui/react-popover'; import GroupActions from '@/groups/GroupActions'; import GroupAvatar from '@/groups/GroupAvatar'; import useLongPress from '@/logic/useLongPress'; import { useIsMobile } from '@/logic/useMedia'; import { + groupIsInitializing, useGang, + useGangPreview, + useGroup, useGroupCancelMutation, + useGroupLeaveMutation, useGroupRescindMutation, } from '@/state/groups'; import LoadingSpinner from '../LoadingSpinner/LoadingSpinner'; import SidebarItem from './SidebarItem'; +import Lock16Icon from '../icons/Lock16Icon'; +import ExclamationPoint from '../icons/ExclamationPoint'; +import X16Icon from '../icons/X16Icon'; // Gang is a pending group invite -export default function GangItem(props: { flag: string }) { - const { flag } = props; - const { preview, claim } = useGang(flag); +export default function GangItem(props: { flag: string; isJoining?: boolean }) { + const { flag, isJoining = false } = props; + const gang = useGang(flag); + const group = useGroup(flag); + const gangPreview = useGangPreview(flag); const isMobile = useIsMobile(); const [optionsOpen, setOptionsOpen] = useState(false); const { action, handlers } = useLongPress(); + const preview = gang?.preview || gangPreview; useEffect(() => { if (!isMobile) { @@ -34,22 +44,41 @@ export default function GangItem(props: { flag: string }) { useGroupRescindMutation(); const { mutate: cancelMutation, status: cancelStatus } = useGroupCancelMutation(); + const { mutate: leaveGroupMutation, status: leaveStatus } = + useGroupLeaveMutation(); - if (!claim) { - return null; - } + const requested = gang && gang.claim && gang.claim.progress === 'knocking'; + const errored = gang && gang.claim && gang.claim.progress === 'error'; + const probablyOffline = + gang && !gang.preview && gang.claim && gang.claim.progress === 'adding'; - const requested = claim.progress === 'knocking'; - const errored = claim.progress === 'error'; - const handleCancel = async () => { + const handleCancel = async (e: any) => { + e.stopPropagation(); if (requested) { rescindMutation({ flag }); + } else if (group && groupIsInitializing(group)) { + leaveGroupMutation({ flag }); } else { cancelMutation({ flag }); } }; - if (!requested && !errored) { + let sideBarIcon; + if (isJoining) { + sideBarIcon = ( + + ); + } + if (requested) { + sideBarIcon = ; + } + if (errored || probablyOffline) { + sideBarIcon = ; + } + + if (!requested && !errored && !isJoining && !probablyOffline) { return ( } - className="px-4" to={`/groups/${flag}`} {...handlers} > @@ -90,20 +118,25 @@ export default function GangItem(props: { flag: string }) { className="opacity-60" /> } - className="px-4" + actions={sideBarIcon} > - + {preview ? preview.meta.title : flag}
+
+ + + +
{requested ? ( <> You've requested to join this group. @@ -120,6 +153,13 @@ export default function GangItem(props: { flag: string }) { version. + ) : probablyOffline ? ( + <> + Attempting to Join + + You're trying to join this group, but it can't be reached. + + ) : ( <> You are currently joining this group. @@ -131,7 +171,9 @@ export default function GangItem(props: { flag: string }) { className="small-button bg-gray-50 text-gray-800" onClick={handleCancel} > - {rescindStatus === 'loading' || cancelStatus === 'loading' ? ( + {rescindStatus === 'loading' || + cancelStatus === 'loading' || + leaveStatus === 'loading' ? ( ) : ( 'Cancel' @@ -139,7 +181,7 @@ export default function GangItem(props: { flag: string }) { )} - {(errored || requested) && ( + {(errored || requested || probablyOffline) && (
-
- ); -} diff --git a/ui/src/components/UpdateNotices.tsx b/ui/src/components/UpdateNotices.tsx new file mode 100644 index 0000000000..8c5386be39 --- /dev/null +++ b/ui/src/components/UpdateNotices.tsx @@ -0,0 +1,59 @@ +import { useDismissNavigate } from '@/logic/routing'; +import useAppUpdates from '@/logic/useAppUpdates'; +import WidgetDrawer from './WidgetDrawer'; +import Asterisk16Icon from './icons/Asterisk16Icon'; + +export default function UpdateNoticeSheet() { + const dismiss = useDismissNavigate(); + const { triggerUpdate } = useAppUpdates(); + + const onOpenChange = (open: boolean) => { + if (!open) { + dismiss(); + } + }; + + return ( + +
+ +

+ Update Required +

+
+
+

+ Tlon was updated in the background, but the changes need to be + installed. Please do so now. +

+ +
+
+ ); +} + +export function DesktopUpdateButton() { + const { triggerUpdate } = useAppUpdates(); + + return ( + + ); +} diff --git a/ui/src/components/WidgetDrawer.tsx b/ui/src/components/WidgetDrawer.tsx index ce1ee7dd37..7677783df7 100644 --- a/ui/src/components/WidgetDrawer.tsx +++ b/ui/src/components/WidgetDrawer.tsx @@ -1,5 +1,6 @@ import { Drawer } from 'vaul'; import cn from 'classnames'; +import { useSafeAreaInsets } from '@/logic/native'; interface WidgetSheetProps { open: boolean; @@ -16,20 +17,26 @@ export default function WidgetDrawer({ children, className, }: WidgetSheetProps) { + const insets = useSafeAreaInsets(); + return ( -
{children}
+
{children}
diff --git a/ui/src/components/icons/BellIcon.tsx b/ui/src/components/icons/BellIcon.tsx index 3d752b6128..b6bc43e70c 100644 --- a/ui/src/components/icons/BellIcon.tsx +++ b/ui/src/components/icons/BellIcon.tsx @@ -11,7 +11,7 @@ export default function BellIcon({ return ( @@ -35,7 +35,7 @@ export default function BellIcon({ return ( diff --git a/ui/src/components/icons/NavigateIcon.tsx b/ui/src/components/icons/NavigateIcon.tsx new file mode 100644 index 0000000000..236f8ac35e --- /dev/null +++ b/ui/src/components/icons/NavigateIcon.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { IconProps } from './icon'; + +export default function NavigateIcon({ + className, + isInactive, + isDarkMode, +}: { isInactive?: boolean; isDarkMode?: boolean } & IconProps) { + let opacity = '1'; + if (isInactive) { + opacity = isDarkMode ? '.8' : '.2'; + } + + return ( + + + + + ); +} diff --git a/ui/src/diary/DiaryContent/DiaryContent.tsx b/ui/src/diary/DiaryContent/DiaryContent.tsx index 88b7292af7..4c3105c407 100644 --- a/ui/src/diary/DiaryContent/DiaryContent.tsx +++ b/ui/src/diary/DiaryContent/DiaryContent.tsx @@ -112,6 +112,7 @@ export function InlineContent({ story }: InlineContentProps) { {story.link.content || story.link.href} diff --git a/ui/src/diary/DiaryInlineEditor.tsx b/ui/src/diary/DiaryInlineEditor.tsx index 644674b0a6..daf5d4b0e4 100644 --- a/ui/src/diary/DiaryInlineEditor.tsx +++ b/ui/src/diary/DiaryInlineEditor.tsx @@ -35,7 +35,7 @@ import ListItem from '@tiptap/extension-list-item'; import HorizontalRule from '@tiptap/extension-horizontal-rule'; import Heading from '@tiptap/extension-heading'; import Mention from '@tiptap/extension-mention'; -import MentionPopup from '@/components/Mention/MentionPopup'; +import getMentionPopup from '@/components/Mention/MentionPopup'; import AddIcon16 from '@/components/icons/Add16Icon'; import IconButton from '@/components/IconButton'; import ActionMenu, { @@ -99,7 +99,15 @@ export function useDiaryInlineEditor({ HTMLAttributes: { class: 'inline-block rounded bg-blue-soft px-1.5 py-0 text-blue', }, - suggestion: MentionPopup, + renderLabel: (props) => `~${props.node.attrs.id}`, + suggestion: getMentionPopup('~'), + }), + Mention.extend({ priority: 1000 }).configure({ + HTMLAttributes: { + class: 'inline-block rounded bg-blue-soft px-1.5 py-0 text-blue', + }, + renderLabel: (props) => `~${props.node.attrs.id}`, + suggestion: getMentionPopup('@'), }), OrderedList, Paragraph, diff --git a/ui/src/diary/DiaryNoteOptionsDropdown.tsx b/ui/src/diary/DiaryNoteOptionsDropdown.tsx index 8f2b025149..6fbd6f51d0 100644 --- a/ui/src/diary/DiaryNoteOptionsDropdown.tsx +++ b/ui/src/diary/DiaryNoteOptionsDropdown.tsx @@ -2,7 +2,11 @@ import React, { PropsWithChildren, useState } from 'react'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; import ConfirmationModal from '@/components/ConfirmationModal'; -import { useArrangedNotes, usePostToggler } from '@/state/diary'; +import { + useArrangedNotes, + useIsNotePending, + usePostToggler, +} from '@/state/diary'; import { useChannelCompatibility } from '@/logic/channel'; import { getFlagParts } from '@/logic/utils'; import ActionMenu, { Action } from '@/components/ActionMenu'; @@ -29,6 +33,7 @@ export default function DiaryNoteOptionsDropdown({ const { ship } = getFlagParts(flag); const nest = `diary/${flag}`; const { compatible } = useChannelCompatibility(nest); + const isPending = useIsNotePending(time); const { isOpen, didCopy, @@ -54,7 +59,7 @@ export default function DiaryNoteOptionsDropdown({ }, ]; - if ((canEdit && ship === window.our) || (canEdit && compatible)) { + if (!isPending && canEdit && (ship === window.our || compatible)) { if (arrangedNotes.includes(time)) { actions.push( { diff --git a/ui/src/diary/diary-add-note.tsx b/ui/src/diary/diary-add-note.tsx index 18d1e64096..d8c830f262 100644 --- a/ui/src/diary/diary-add-note.tsx +++ b/ui/src/diary/diary-add-note.tsx @@ -31,12 +31,12 @@ import { useIsMobile } from '@/logic/useMedia'; import ReconnectingSpinner from '@/components/ReconnectingSpinner'; import useGroupPrivacy from '@/logic/useGroupPrivacy'; import { captureGroupsAnalyticsEvent } from '@/logic/analytics'; -import asyncCallWithTimeout from '@/logic/asyncWithTimeout'; import Setting from '@/components/Settings/Setting'; import { useMarkdownInDiaries, usePutEntryMutation } from '@/state/settings'; import { useChannelCompatibility } from '@/logic/channel'; import Tooltip from '@/components/Tooltip'; import MobileHeader from '@/components/MobileHeader'; +import { isFirstDayOfMonth } from 'date-fns'; import DiaryInlineEditor, { useDiaryInlineEditor } from './DiaryInlineEditor'; import DiaryMarkdownEditor from './DiaryMarkdownEditor'; @@ -59,11 +59,16 @@ export default function DiaryAddNote() { isLoading: loadingNote, fetchStatus, } = useNote(chFlag, id || '0', !id); - const { mutateAsync: editNote, status: editStatus } = useEditNoteMutation(); + const { + mutateAsync: editNote, + status: editStatus, + reset: resetEdit, + } = useEditNoteMutation(); const { data: returnTime, mutateAsync: addNote, status: addStatus, + reset: resetAdd, } = useAddNoteMutation(); const { mutate: toggleMarkdown, status: toggleMarkdownStatus } = usePutEntryMutation({ bucket: 'diary', key: 'markdown' }); @@ -94,6 +99,15 @@ export default function DiaryAddNote() { content: '', placeholder: '', onEnter: () => false, + onUpdate: useCallback(() => { + if (addStatus === 'error') { + resetAdd(); + } + + if (editStatus === 'error') { + resetEdit(); + } + }, [addStatus, editStatus, resetAdd, resetEdit]), }); const setEditorContent = useCallback( @@ -108,7 +122,8 @@ export default function DiaryAddNote() { useEffect(() => { if (editor && !loadingNote && note?.essay && editor.isEmpty && !loaded) { - editor.commands.setContent(diaryMixedToJSON(note?.essay?.content || [])); + const content = diaryMixedToJSON(note?.essay?.content || []); + editor.commands.setContent(content); setLoaded(true); } }, [editor, loadingNote, note, loaded]); @@ -147,7 +162,7 @@ export default function DiaryAddNote() { try { if (id) { - await editNote({ + editNote({ flag: chFlag, time: id, essay: { @@ -157,19 +172,6 @@ export default function DiaryAddNote() { }, }); } else { - await asyncCallWithTimeout( - addNote({ - initialTime, - flag: chFlag, - essay: { - ...values, - content: noteContent, - author: window.our, - sent: daToUnix(bigInt(initialTime)), - }, - }), - 3000 - ); captureGroupsAnalyticsEvent({ name: 'post_item', groupFlag: flag, @@ -177,11 +179,19 @@ export default function DiaryAddNote() { channelType: 'diary', privacy, }); - } - reset(); + addNote({ + initialTime, + flag: chFlag, + essay: { + ...values, + content: noteContent, + author: window.our, + sent: daToUnix(bigInt(initialTime)), + }, + }); + } } catch (error) { - navigate(`/groups/${flag}/channels/diary/${chFlag}`); console.error(error); } }, [ @@ -192,11 +202,9 @@ export default function DiaryAddNote() { getValues, id, note, - reset, addNote, editNote, initialTime, - navigate, watchedTitle, ]); @@ -208,6 +216,9 @@ export default function DiaryAddNote() { } }, [addStatus, chFlag, editStatus, flag, navigate, returnTime]); + const isLoading = addStatus === 'loading' || editStatus === 'loading'; + const isError = addStatus === 'error' || editStatus === 'error'; + return ( - {editStatus === 'loading' || addStatus === 'loading' ? ( + {isLoading ? ( - ) : editStatus === 'error' || addStatus === 'error' ? ( + ) : isError ? ( 'Error' ) : ( 'Save' @@ -277,23 +287,29 @@ export default function DiaryAddNote() {
- + ) : null} -
); diff --git a/ui/src/dms/MessagesSidebar.tsx b/ui/src/dms/MessagesSidebar.tsx index 07f88301d6..fe14bcb09c 100644 --- a/ui/src/dms/MessagesSidebar.tsx +++ b/ui/src/dms/MessagesSidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useCallback } from 'react'; +import React, { useState, useRef, useCallback, useContext } from 'react'; import cn from 'classnames'; import { debounce } from 'lodash'; import { Link, useLocation } from 'react-router-dom'; @@ -23,6 +23,9 @@ import { useGroups } from '@/state/groups'; import ReconnectingSpinner from '@/components/ReconnectingSpinner'; import SystemChrome from '@/components/Sidebar/SystemChrome'; import ActionMenu, { Action } from '@/components/ActionMenu'; +import { useLocalState } from '@/state/local'; +import { DesktopUpdateButton } from '@/components/UpdateNotices'; +import { AppUpdateContext } from '@/logic/useAppUpdates'; import MessagesList from './MessagesList'; import MessagesSidebarItem from './MessagesSidebarItem'; import { MessagesScrollingContext } from './MessagesScrollingContext'; @@ -128,6 +131,7 @@ export function TalkAppMenu() { } export default function MessagesSidebar() { + const { needsUpdate } = useContext(AppUpdateContext); const [atTop, setAtTop] = useState(true); const [isScrolling, setIsScrolling] = useState(false); const [filterOpen, setFilterOpen] = useState(false); @@ -192,7 +196,7 @@ export default function MessagesSidebar() {
- + {needsUpdate ? : } } diff --git a/ui/src/dms/MultiDm.tsx b/ui/src/dms/MultiDm.tsx index f616ba774b..56c59d341a 100644 --- a/ui/src/dms/MultiDm.tsx +++ b/ui/src/dms/MultiDm.tsx @@ -1,13 +1,20 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import cn from 'classnames'; -import { Outlet, Route, Routes, useMatch, useParams } from 'react-router'; +import { + Outlet, + Route, + Routes, + useMatch, + useNavigate, + useParams, +} from 'react-router'; import { Link } from 'react-router-dom'; import ChatInput from '@/chat/ChatInput/ChatInput'; import Layout from '@/components/Layout/Layout'; import { useChatState, useMultiDm, useMultiDmIsPending } from '@/state/chat'; import ChatWindow from '@/chat/ChatWindow'; import { useIsMobile } from '@/logic/useMedia'; -import { pluralize } from '@/logic/utils'; +import { dmListPath, isGroups, pluralize } from '@/logic/utils'; import useMessageSelector from '@/logic/useMessageSelector'; import CaretLeft16Icon from '@/components/icons/CaretLeft16Icon'; import ReconnectingSpinner from '@/components/ReconnectingSpinner'; @@ -71,6 +78,7 @@ export default function MultiDm() { const dropZoneId = `chat-dm-input-dropzone-${clubId}`; const { isDragging, isOver } = useDragAndDrop(dropZoneId); const isMobile = useIsMobile(); + const navigate = useNavigate(); const inSearch = useMatch(`/dm/${clubId}/search/*`); const isAccepted = !useMultiDmIsPending(clubId); const club = useMultiDm(clubId); @@ -79,7 +87,7 @@ export default function MultiDm() { const root = `/dm/${clubId}`; const scrollElementRef = useRef(null); const isScrolling = useIsScrolling(scrollElementRef); - const shouldApplyPaddingBottom = isMobile && !isChatInputFocused; + const shouldApplyPaddingBottom = isGroups && isMobile && !isChatInputFocused; const { isSelectingMessage, @@ -97,6 +105,10 @@ export default function MultiDm() { const { sendMessage } = useChatState.getState(); + const handleLeave = useCallback(() => { + navigate(dmListPath); + }, [navigate]); + if (!club) { return null; } @@ -110,7 +122,12 @@ export default function MultiDm() { className="padding-bottom-transition flex-1" header={ isSelecting ? ( - + <> + {isMobile && ( + + )} + + ) : (
diff --git a/ui/src/dms/NewDm.tsx b/ui/src/dms/NewDm.tsx index c880e15c0c..c67c4b4bfe 100644 --- a/ui/src/dms/NewDm.tsx +++ b/ui/src/dms/NewDm.tsx @@ -4,25 +4,31 @@ import ChatInput from '@/chat/ChatInput/ChatInput'; import Layout from '@/components/Layout/Layout'; import useMessageSelector from '@/logic/useMessageSelector'; import { useDragAndDrop } from '@/logic/DragAndDropContext'; -import { isNativeApp } from '@/logic/native'; import MobileHeader from '@/components/MobileHeader'; import { useIsScrolling } from '@/logic/scroll'; +import { useIsMobile } from '@/logic/useMedia'; +import { useChatInputFocus } from '@/logic/ChatInputFocusContext'; +import { dmListPath } from '@/logic/utils'; import MessageSelector from './MessageSelector'; export default function NewDM() { const { sendDm, validShips, whom } = useMessageSelector(); const dropZoneId = 'chat-new-dm-input-dropzone'; const { isDragging, isOver } = useDragAndDrop(dropZoneId); + const isMobile = useIsMobile(); const scrollElementRef = useRef(null); const isScrolling = useIsScrolling(scrollElementRef); + const { isChatInputFocused } = useChatInputFocus(); + const shouldApplyPaddingBottom = isMobile && !isChatInputFocused; return ( - ) + isMobile && } footer={
!o && dismiss()}> - - {renderContent()} - - + !o && dismiss()} + className="h-[70vh] px-10 py-6" + > + {renderContent()} + ) : ( !isOpen && dismiss()} - containerClass="w-full max-w-xl card" + containerClass="w-[400px] max-w-xl card" className="bg-transparent p-0" close="none" > diff --git a/ui/src/groups/GroupJoinList.tsx b/ui/src/groups/GroupJoinList.tsx index 10b6954145..fb148724ad 100644 --- a/ui/src/groups/GroupJoinList.tsx +++ b/ui/src/groups/GroupJoinList.tsx @@ -58,7 +58,11 @@ export function GroupJoinItem({ )} onClick={open} > - {status === 'error' ? 'Errored' : button.text} + {status === 'error' + ? 'Errored' + : highlight + ? 'Invited' + : button.text} ) } @@ -88,7 +92,9 @@ export default function GroupJoinList({ return ( <> {gangEntries.map(([flag]) => ( - +
+ +
))} ); diff --git a/ui/src/groups/GroupSidebar/GroupSidebar.tsx b/ui/src/groups/GroupSidebar/GroupSidebar.tsx index e69c2df13b..7c016f959e 100644 --- a/ui/src/groups/GroupSidebar/GroupSidebar.tsx +++ b/ui/src/groups/GroupSidebar/GroupSidebar.tsx @@ -1,6 +1,6 @@ import cn from 'classnames'; import _ from 'lodash'; -import React from 'react'; +import { useContext } from 'react'; import { useIsDark } from '@/logic/useMedia'; import { useAmAdmin, @@ -22,10 +22,13 @@ import { Link, useLocation } from 'react-router-dom'; import CaretDown16Icon from '@/components/icons/CaretDown16Icon'; import InviteIcon from '@/components/icons/InviteIcon'; import HomeIcon from '@/components/icons/HomeIcon'; +import AsteriskIcon from '@/components/icons/AsteriskIcon'; +import { AppUpdateContext } from '@/logic/useAppUpdates'; function GroupHeader() { const flag = useGroupFlag(); const group = useGroup(flag); + const { needsUpdate } = useContext(AppUpdateContext); const { preview, claim } = useGang(flag); const defaultImportCover = group?.meta.cover === '0x0'; const calm = useCalm(); @@ -95,9 +98,16 @@ function GroupHeader() { - + {needsUpdate ? ( + + ) : ( + + )}
); @@ -109,7 +119,7 @@ export default function GroupSidebar() { const isDark = useIsDark(); const location = useLocation(); const isAdmin = useAmAdmin(flag); - const privacy = group ? getPrivacyFromGroup(group) : 'public'; + const privacy = group ? getPrivacyFromGroup(group) : undefined; return (