diff --git a/bin/solid.pill b/bin/solid.pill index ce17c6c868..9d257abc74 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d56b7351a347a65c06999955114f196523a86c853390d5d1822a90a606619d6 -size 10357558 +oid sha256:f6b5e33e573818120051651c1182163527edbbe0dff0eb6591e12a55cfccb273 +size 10486101 diff --git a/nix/pkgs/docker-image/default.nix b/nix/pkgs/docker-image/default.nix index 467c06ac47..6c200a00ec 100644 --- a/nix/pkgs/docker-image/default.nix +++ b/nix/pkgs/docker-image/default.nix @@ -1,10 +1,24 @@ -{ urbit, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }: +{ urbit, curl, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }: let startUrbit = writeScriptBin "start-urbit" '' #!${bashInteractive}/bin/bash set -eu + # set defaults + amesPort=${toString amesPort} + + # check args + for i in "$@" + do + case $i in + -p=*|--port=*) + amesPort="''${i#*=}" + shift + ;; + esac + done + # If the container is not started with the `-i` flag # then STDIN will be closed and we need to start # Urbit/vere with the `-t` flag. @@ -23,7 +37,7 @@ let mv $keyname /tmp # Boot urbit with the key, exit when done booting - urbit $ttyflag -w $(basename $keyname .key) -k /tmp/$keyname -c $(basename $keyname .key) -p ${toString amesPort} -x + urbit $ttyflag -w $(basename $keyname .key) -k /tmp/$keyname -c $(basename $keyname .key) -p $amesPort -x # Remove the keyfile for security rm /tmp/$keyname @@ -34,7 +48,7 @@ let cometname=''${comets[0]} rm *.comet - urbit $ttyflag -c $(basename $cometname .comet) -p ${toString amesPort} -x + urbit $ttyflag -c $(basename $cometname .comet) -p $amesPort -x fi # Find the first directory and start urbit with the ship therein @@ -42,14 +56,44 @@ let dirs=( $dirnames ) dirname=''${dirnames[0]} - urbit $ttyflag -p ${toString amesPort} $dirname + exec urbit $ttyflag -p $amesPort $dirname + ''; + + getUrbitCode = writeScriptBin "get-urbit-code" '' + #!${bashInteractive}/bin/bash + + raw=$(curl -s -X POST -H "Content-Type: application/json" \ + -d '{ "source": { "dojo": "+code" }, "sink": { "stdout": null } }' \ + http://127.0.0.1:12321) + + # trim \n" from the end + trim="''${raw%\\n\"}" + + # trim " from the start + code="''${trim#\"}" + + echo "$code" + ''; + + resetUrbitCode = writeScriptBin "reset-urbit-code" '' + #!${bashInteractive}/bin/bash + + curl=$(curl -s -X POST -H "Content-Type: application/json" \ + -d '{ "source": { "dojo": "+hood/code %reset" }, "sink": { "app": "hood" } }' \ + http://127.0.0.1:12321) + + if [[ $? -eq 0 ]] + then + echo "OK" + else + echo "Curl error: $?" + fi ''; - in dockerTools.buildImage { name = "urbit"; tag = "v${urbit.version}"; - contents = [ bashInteractive urbit startUrbit coreutils ]; + contents = [ bashInteractive urbit curl startUrbit getUrbitCode resetUrbitCode coreutils ]; runAsRoot = '' #!${bashInteractive} mkdir -p /urbit diff --git a/nix/pkgs/hs/default.nix b/nix/pkgs/hs/default.nix index ab30f4c7dc..ea09883ba4 100644 --- a/nix/pkgs/hs/default.nix +++ b/nix/pkgs/hs/default.nix @@ -82,6 +82,8 @@ haskell-nix.stackProject { urbit-king.components.tests.urbit-king-tests.testFlags = [ "--brass-pill=${brass.lfs}" ]; + + lmdb.components.library.libs = lib.mkForce [ lmdb ]; }; }]; } diff --git a/pkg/arvo/app/contact-store.hoon b/pkg/arvo/app/contact-store.hoon index bd174a603f..8f39e75404 100644 --- a/pkg/arvo/app/contact-store.hoon +++ b/pkg/arvo/app/contact-store.hoon @@ -126,6 +126,14 @@ !=(contact(last-updated *@da) u.old(last-updated *@da)) == [~ state] + ~| "cannot add a data url to cover!" + ?> ?| ?=(~ cover.contact) + !=('data:' (cut 3 [0 5] u.cover.contact)) + == + ~| "cannot add a data url to avatar!" + ?> ?| ?=(~ avatar.contact) + !=('data:' (cut 3 [0 5] u.avatar.contact)) + == :- (send-diff [%add ship contact] =(ship our.bowl)) state(rolodex (~(put by rolodex) ship contact)) :: @@ -149,6 +157,14 @@ =/ contact (edit-contact old edit-field) ?: =(old contact) [~ state] + ~| "cannot add a data url to cover!" + ?> ?| ?=(~ cover.contact) + !=('data:' (cut 3 [0 5] u.cover.contact)) + == + ~| "cannot add a data url to avatar!" + ?> ?| ?=(~ avatar.contact) + !=('data:' (cut 3 [0 5] u.avatar.contact)) + == =. last-updated.contact timestamp :- (send-diff [%edit ship edit-field timestamp] =(ship our.bowl)) state(rolodex (~(put by rolodex) ship contact)) diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index 84b1eb34d5..0fb9ffa4a6 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -5,7 +5,7 @@ /- glob /+ default-agent, verb, dbug |% -++ hash 0v6.qafur.17301.j8obh.vbepn.7tq3l +++ hash 0v3.g6u13.haedt.jt4hd.61ek5.6t30q +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ all-states $% state-0 diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index d1f0081385..8a76a571f8 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -24,6 +24,6 @@
- + diff --git a/pkg/arvo/app/metadata-store.hoon b/pkg/arvo/app/metadata-store.hoon index 319399fb5d..2f430864c5 100644 --- a/pkg/arvo/app/metadata-store.hoon +++ b/pkg/arvo/app/metadata-store.hoon @@ -23,7 +23,7 @@ :: /app-name/%app-name associations for app :: /group/%path associations for group :: -/- store=metadata-store +/- store=metadata-store, pull-hook /+ default-agent, verb, dbug, resource, *migrate |% +$ card card:agent:gall @@ -95,16 +95,17 @@ ~ == :: -+$ state-0 [%0 base-state-0] -+$ state-1 [%1 base-state-0] -+$ state-2 [%2 base-state-0] -+$ state-3 [%3 base-state-1] -+$ state-4 [%4 base-state-1] -+$ state-5 [%5 base-state-1] -+$ state-6 [%6 base-state-1] -+$ state-7 [%7 base-state-2] -+$ state-8 [%8 base-state-3] -+$ state-9 [%9 base-state-3] ++$ state-0 [%0 base-state-0] ++$ state-1 [%1 base-state-0] ++$ state-2 [%2 base-state-0] ++$ state-3 [%3 base-state-1] ++$ state-4 [%4 base-state-1] ++$ state-5 [%5 base-state-1] ++$ state-6 [%6 base-state-1] ++$ state-7 [%7 base-state-2] ++$ state-8 [%8 base-state-3] ++$ state-9 [%9 base-state-3] ++$ state-10 [%10 base-state-3] +$ versioned-state $% state-0 state-1 @@ -116,10 +117,11 @@ state-7 state-8 state-9 + state-10 == :: +$ inflated-state - $: state-9 + $: state-10 cached-indices == -- @@ -232,7 +234,7 @@ =| cards=(list card) |^ =* loop $ - ?: ?=(%9 -.old) + ?: ?=(%10 -.old) :- cards %_ state associations associations.old @@ -240,7 +242,7 @@ group-indices (rebuild-group-indices associations.old) app-indices (rebuild-app-indices associations.old) == - ?: ?=(%8 -.old) + ?: ?=(%9 -.old) =/ groups (fall (~(get by (rebuild-app-indices associations.old)) %groups) ~) =/ pokes=(list card) @@ -252,13 +254,17 @@ ?. ?=([%group [~ [~ [@ [@ @]]]]] config.met) ~ =* res resource.u.u.feed.config.met + ?: =(our.bowl entity.res) ~ =- `[%pass /fix-feed %agent [our.bowl %graph-pull-hook] %poke -] :- %pull-hook-action - !> [%add entity.res name.res] + !> ^- action:pull-hook + [%add entity.res res] %_ $ cards (weld cards pokes) - -.old %9 + -.old %10 == + ?: ?=(%8 -.old) + $(-.old %9) ?: ?=(%7 -.old) $(old [%8 (associations-2-to-3 associations.old) ~]) ?: ?=(%6 -.old) diff --git a/pkg/arvo/sys/vane/eyre.hoon b/pkg/arvo/sys/vane/eyre.hoon index b509e71314..998bd659a3 100644 --- a/pkg/arvo/sys/vane/eyre.hoon +++ b/pkg/arvo/sys/vane/eyre.hoon @@ -249,6 +249,7 @@ font-family: "Source Code Pro"; src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff"); font-weight: 400; + font-display: swap; } :root { --red05: rgba(255,65,54,0.05); diff --git a/pkg/docker-image/README.md b/pkg/docker-image/README.md index e5e2163eed..0e7e0a46aa 100644 --- a/pkg/docker-image/README.md +++ b/pkg/docker-image/README.md @@ -25,9 +25,55 @@ The first two options result in Urbit attempting to boot either the ship named b In consequence, it is safe to remove the container and start a new container which mounts the same volume, e.g. to upgrade the version of the urbit binary by running a later container version. It is also possible to stop the container and then move the pier away e.g. to a location where you will run it directly with the Urbit binary. ### Ports -The image includes `EXPOSE` directives for TCP port 80 and UDP port 34343. Port `80` is used for Urbit's HTTP interface for both [Landscape](https://urbit.org/docs/glossary/landscape/) and for [API calls](https://urbit.org/using/integrating-api/) to the ship. Port `34343` is used by [Ames](https://urbit.org/docs/glossary/ames/) for ship-to-ship communication. +The image includes `EXPOSE` directives for TCP port 80 and UDP port 34343. Port `80` is used for Urbit's HTTP interface for both [Landscape](https://urbit.org/docs/glossary/landscape/) and for [API calls](https://urbit.org/using/integrating-api/) to the ship. Port `34343` is set by default to be used by [Ames](https://urbit.org/docs/glossary/ames/) for ship-to-ship communication. -You can either pass the `-P` flag to docker to map ports directly to the corresponding ports on the host, or map them individually with `-p` flags. For local testing the latter is often convenient, for instance to remap port 80 to an unprivileged port. +You can either pass the `-P` flag to docker to map ports directly to the corresponding ports on the host, or map them individually with `-p` flags. For local testing the latter is often convenient, for instance to remap port 80 to an unprivileged port. + +For best performance, you must map the Ames UDP port to the *same* port on the host. If you map to a different port Ames will not be able to make direct connections and your network performance may suffer somewhat. Note that using the same port is required for direct connections but is not by itself sufficient for them. If you are behind a NAT router or the host is not on a public IP address or you are firewalled, you may not achive direct connections regardless. + +For this purpose you can force Ames to use a custom port. `/bin/start-urbit --port=$AMES_PORT` can be passed as an argument to the `docker start` command. Passing `/bin/start-urbit --port=13436` for example, would use port 13436. You must pass the name of the start script `/bin/start-urbit` in order to also pass arguments, if this is omitted your container will not start. + +### Examples +Creating a volume for ~sampel=palnet: +``` +docker volume create sampel-palnet +``` + +Copying key to sampel-palnet's volume (assumes default docker location): +``` +sudo cp ~/sampel-palnet.key /var/lib/docker/volumes/sampel-palnet/_data/sampel-palnet.key +``` + +Using that volume and launching ~sampel-palnet on host port 8080 with Ames talking on the default host port 34343: +``` +docker run -d -p 8080:80 -p 34343:34343/udp --name sampel-palnet \ + --mount type=volume,source=sampel-palnet,destination=/urbit \ + tloncorp/urbit +``` + +Using host port 8088 with Ames talking on host port 23232: +``` +docker run -d -p 8088:80 -p 23232:23232/udp --name sampel-palnet \ + --mount type=volume,source=sampel-palnet,destination=/urbit \ + tloncorp/urbit /bin/start-urbit --port=23232 +``` + +### Getting and resetting the Landscape +code +This docker image includes tools for retrieving and resetting the Landscape login code belonging to the planet, for programmatic use so the container does not need a tty. These scripts can be called using `docker container exec`. + +Getting the code: +``` +$ docker container exec sampel-palnet /bin/get-urbit-code +sampel-sampel-sampel-sampel +``` + +Resetting the code: +``` +$ docker container exec sampel-palnet /bin/reset-urbit-code +OK +``` + +Once the code has been reset the new code can be obtained from `/bin/get-urbit-code`. ## Extending diff --git a/pkg/hs/urbit-king/package.yaml b/pkg/hs/urbit-king/package.yaml index 709ed1b9fa..472b156058 100644 --- a/pkg/hs/urbit-king/package.yaml +++ b/pkg/hs/urbit-king/package.yaml @@ -1,5 +1,5 @@ name: urbit-king -version: 1.4 +version: 1.5 license: MIT license-file: LICENSE data-files: diff --git a/pkg/interface/CONTRIBUTING.md b/pkg/interface/CONTRIBUTING.md index 711e359d8f..f3bfbb5a0e 100644 --- a/pkg/interface/CONTRIBUTING.md +++ b/pkg/interface/CONTRIBUTING.md @@ -32,7 +32,7 @@ same (if [developing on a local development ship][local]). Then, from 'pkg/interface': ``` -npm install +npm ci npm run start ``` @@ -59,7 +59,7 @@ module.exports = { ``` The dev environment will attempt to match the subdomain against the keys of this -object, and if matched will proxy to the corresponding URL. For example, the +object, and if matched will proxy to the corresponding URL. For example, the above config will proxy `zod.localhost:9000` to `http://localhost:8080`, `bus.localhost:9000` to `http://localhost:8081` and so on and so forth. If no match is found, then it will fallback to the `URL` property. @@ -71,7 +71,7 @@ linter and for usage through the command, do the following: ```bash $ cd ./pkg/interface -$ npm install +$ npm ci $ npm run lint ``` diff --git a/pkg/interface/package.json b/pkg/interface/package.json index ae2d1129c2..8315072931 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -98,8 +98,9 @@ "lint-file": "eslint", "tsc": "tsc", "tsc:watch": "tsc --watch", + "preinstall": "./preinstall.sh", "build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js", - "build:prod": "cd ../npm/api && npm i && cd ../../interface && cross-env NODE_ENV=production webpack --config config/webpack.prod.js", + "build:prod": "cd ../npm/api && npm ci && cd ../../interface && cross-env NODE_ENV=production webpack --config config/webpack.prod.js", "start": "webpack-dev-server --config config/webpack.dev.js", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/pkg/interface/preinstall.sh b/pkg/interface/preinstall.sh new file mode 100755 index 0000000000..4f35cfc0f7 --- /dev/null +++ b/pkg/interface/preinstall.sh @@ -0,0 +1,12 @@ +#!/bin/sh +cd ../npm + +for i in $(find . -type d -maxdepth 1) ; do + packageJson="${i}/package.json" + if [ -f "${packageJson}" ]; then + echo "installing ${i}..." + cd ./${i} + npm ci + cd .. + fi +done \ No newline at end of file diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index b807201a1a..42fe0ae316 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -4,6 +4,11 @@ import { dateToDa, decToUd } from '../lib/util'; import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api'; import { BigInteger } from 'big-integer'; import { getParentIndex } from '../lib/notification'; +import useHarkState from '../state/hark'; + +function getHarkSize() { + return useHarkState.getState().notifications.size ?? 0; +} export class HarkApi extends BaseApi { private harkAction(action: any): Promise { @@ -172,10 +177,10 @@ export class HarkApi extends BaseApi { } async getMore(): Promise { - const offset = this.store.state['notifications']?.size || 0; + const offset = getHarkSize(); const count = 3; await this.getSubset(offset, count, false); - return offset === (this.store.state.notifications?.size || 0); + return offset === getHarkSize(); } async getSubset(offset:number, count:number, isArchive: boolean) { diff --git a/pkg/interface/src/logic/lib/formGroup.ts b/pkg/interface/src/logic/lib/formGroup.ts new file mode 100644 index 0000000000..e2f7ec7d79 --- /dev/null +++ b/pkg/interface/src/logic/lib/formGroup.ts @@ -0,0 +1,18 @@ +import React from "react"; + +export type SubmitHandler = () => Promise; +interface IFormGroupContext { + addSubmit: (id: string, submit: SubmitHandler) => void; + onDirty: (id: string, touched: boolean) => void; + onErrors: (id: string, errors: boolean) => void; + submitAll: () => Promise; +} + +const fallback: IFormGroupContext = { + addSubmit: () => {}, + onDirty: () => {}, + onErrors: () => {}, + submitAll: () => Promise.resolve(), +}; + +export const FormGroupContext = React.createContext(fallback); diff --git a/pkg/interface/src/logic/lib/hark.ts b/pkg/interface/src/logic/lib/hark.ts index 54dd37e09b..2e7a6b3d9e 100644 --- a/pkg/interface/src/logic/lib/hark.ts +++ b/pkg/interface/src/logic/lib/hark.ts @@ -1,6 +1,6 @@ import bigInt, { BigInteger } from 'big-integer'; import f from 'lodash/fp'; -import { Unreads } from '@urbit/api'; +import { Unreads, NotificationGraphConfig } from '@urbit/api'; export function getLastSeen( unreads: Unreads, @@ -34,3 +34,13 @@ export function getNotificationCount( .map(index => unread[index]?.notifications?.length || 0) .reduce(f.add, 0); } + +export function isWatching( + config: NotificationGraphConfig, + graph: string, + index = "/" +) { + return !!config.watching.find( + watch => watch.graph === graph && watch.index === index + ); +} diff --git a/pkg/interface/src/logic/lib/platform.ts b/pkg/interface/src/logic/lib/platform.ts index b02870fea1..19234e0ddf 100644 --- a/pkg/interface/src/logic/lib/platform.ts +++ b/pkg/interface/src/logic/lib/platform.ts @@ -4,3 +4,7 @@ const ua = window.navigator.userAgent; export const IS_IOS = ua.includes('iPhone'); export const IS_SAFARI = ua.includes('Safari') && !ua.includes('Chrome'); + +export const IS_ANDROID = ua.includes('Android'); + +export const IS_MOBILE = IS_IOS || IS_ANDROID; diff --git a/pkg/interface/src/logic/lib/useLazyScroll.ts b/pkg/interface/src/logic/lib/useLazyScroll.ts index a7b7235ee4..df0bd74ad8 100644 --- a/pkg/interface/src/logic/lib/useLazyScroll.ts +++ b/pkg/interface/src/logic/lib/useLazyScroll.ts @@ -41,6 +41,12 @@ export function useLazyScroll( } }, [count]); + useEffect(() => { + if(!ready) { + setIsDone(false); + } + }, [ready]); + useEffect(() => { if (!ref.current || isDone || !ready) { return; @@ -58,7 +64,7 @@ export function useLazyScroll( return () => { ref.current?.removeEventListener('scroll', onScroll); }; - }, [ref?.current, count, ready]); + }, [ref?.current, ready, isDone]); return { isDone, isLoading }; } diff --git a/pkg/interface/src/logic/lib/useRunIO.ts b/pkg/interface/src/logic/lib/useRunIO.ts index 12d8628cd6..bfaee2c542 100644 --- a/pkg/interface/src/logic/lib/useRunIO.ts +++ b/pkg/interface/src/logic/lib/useRunIO.ts @@ -10,7 +10,7 @@ export function useRunIO( io: (i: I) => Promise, after: (o: O) => void, key: string -) { +): () => Promise { const [resolve, setResolve] = useState<() => void>(() => () => {}); const [reject, setReject] = useState<(e: any) => void>(() => () => {}); const [output, setOutput] = useState(null); diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index 6e6ef07408..f34e8f76be 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -63,6 +63,16 @@ export function unixToDa(unix: number) { return DA_UNIX_EPOCH.add(timeSinceEpoch); } +export function dmCounterparty(resource: string) { + const [,,ship,name] = resource.split('/'); + return ship === `~${window.ship}` ? `~${name.slice(4)}` : ship; +} + +export function isDm(resource: string) { + const [,,,name] = resource.split('/'); + return name.startsWith('dm--'); +} + export function makePatDa(patda: string) { return bigInt(udToDec(patda)); } diff --git a/pkg/interface/src/logic/state/contact.ts b/pkg/interface/src/logic/state/contact.ts index 2a2bf0f781..a57a183ff1 100644 --- a/pkg/interface/src/logic/state/contact.ts +++ b/pkg/interface/src/logic/state/contact.ts @@ -8,12 +8,6 @@ export interface ContactState extends BaseState { isContactPublic: boolean; nackedContacts: Set; // fetchIsAllowed: (entity, name, ship, personal) => Promise; -}; - -export function useContact(ship: string) { - return useContactState( - useCallback(s => s.contacts[ship] as Contact | null, [ship]) - ); } const useContactState = createState('Contact', { @@ -35,4 +29,10 @@ const useContactState = createState('Contact', { // }, }, ['nackedContacts']); +export function useContact(ship: string) { + return useContactState( + useCallback(s => s.contacts[ship] as Contact | null, [ship]) + ); +} + export default useContactState; diff --git a/pkg/interface/src/logic/state/group.ts b/pkg/interface/src/logic/state/group.ts index 4fe3999f69..c8f5eef2a0 100644 --- a/pkg/interface/src/logic/state/group.ts +++ b/pkg/interface/src/logic/state/group.ts @@ -16,7 +16,7 @@ const useGroupState = createState('Group', { }, ['groups']); export function useGroup(group: string) { - return useGroupState(useCallback(s => s.groups[group], [group])); + return useGroupState(useCallback(s => s.groups[group] as Group | undefined, [group])); } export function useGroupForAssoc(association: Association) { diff --git a/pkg/interface/src/logic/state/hark.ts b/pkg/interface/src/logic/state/hark.ts index d02402c098..047723c947 100644 --- a/pkg/interface/src/logic/state/hark.ts +++ b/pkg/interface/src/logic/state/hark.ts @@ -15,7 +15,7 @@ export interface HarkState extends BaseState { notifications: BigIntOrderedMap; notificationsCount: number; notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere - notificationsGroupConfig: []; // TODO type this + notificationsGroupConfig: string[]; unreads: Unreads; }; diff --git a/pkg/interface/src/logic/state/metadata.ts b/pkg/interface/src/logic/state/metadata.ts index fabffca86e..a3c462193d 100644 --- a/pkg/interface/src/logic/state/metadata.ts +++ b/pkg/interface/src/logic/state/metadata.ts @@ -1,4 +1,6 @@ -import { MetadataUpdatePreview, Associations } from "@urbit/api"; +import { useCallback } from 'react'; +import _ from 'lodash'; +import { MetadataUpdatePreview, Association, Associations } from "@urbit/api"; import { BaseState, createState } from "./base"; @@ -9,6 +11,19 @@ export interface MetadataState extends BaseState { // preview: (group: string) => Promise; }; +export function useAssocForGraph(graph: string) { + return useMetadataState(useCallback(s => s.associations.graph[graph] as Association | undefined, [graph])); +} + +export function useAssocForGroup(group: string) { + return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group])); +} + +export function useGraphsForGroup(group: string) { + const graphs = useMetadataState(s => s.associations.graph); + return _.pickBy(graphs, (a: Association) => a.group === group); +} + const useMetadataState = createState('Metadata', { associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} }, // preview: async (group): Promise => { @@ -54,4 +69,4 @@ const useMetadataState = createState('Metadata', { }); -export default useMetadataState; \ No newline at end of file +export default useMetadataState; diff --git a/pkg/interface/src/logic/state/settings.ts b/pkg/interface/src/logic/state/settings.ts index 02e96fffea..767617a624 100644 --- a/pkg/interface/src/logic/state/settings.ts +++ b/pkg/interface/src/logic/state/settings.ts @@ -58,7 +58,7 @@ const useSettingsState = createState('Settings', { categories: leapCategories, }, tutorial: { - seen: false, + seen: true, joined: undefined } }); diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 3cd6478b05..9e14446a27 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -53,6 +53,7 @@ const Root = withState(styled.div` } display: flex; flex-flow: column nowrap; + touch-action: none; * { scrollbar-width: thin; diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 306d221ef5..4839817c68 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -1,4 +1,5 @@ /* eslint-disable max-lines-per-function */ +import bigInt from 'big-integer'; import React, { useState, useEffect, @@ -19,7 +20,8 @@ import { writeText, useShowNickname, useHideAvatar, - useHovering + useHovering, + daToUnix } from '~/logic/lib/util'; import { Group, @@ -295,15 +297,20 @@ class ChatMessage extends Component { ); } + const date = daToUnix(bigInt(msg.index.split('/')[1])); + const nextDate = nextMsg ? ( + daToUnix(bigInt(nextMsg.index.split('/')[1])) + ) : null; + const dayBreak = nextMsg && - new Date(msg['time-sent']).getDate() !== - new Date(nextMsg['time-sent']).getDate(); + new Date(date).getDate() !== + new Date(nextDate).getDate(); const containerClass = `${isPending ? 'o-40' : ''} ${className}`; const timestamp = moment - .unix(msg['time-sent'] / 1000) + .unix(date / 1000) .format(renderSigil ? 'h:mm A' : 'h:mm'); const messageProps = { @@ -339,7 +346,7 @@ class ChatMessage extends Component { style={style} > {dayBreak && !isLastRead ? ( - + ) : null} {renderSigil ? ( @@ -357,7 +364,7 @@ class ChatMessage extends Component { association={association} api={api} dayBreak={dayBreak} - when={msg['time-sent']} + when={date} ref={unreadMarkerRef} /> ) : null} @@ -387,8 +394,10 @@ export const MessageAuthor = ({ const dark = theme === 'dark' || (theme === 'auto' && osDark); const contacts = useContactState((state) => state.contacts); + const date = daToUnix(bigInt(msg.index.split('/')[1])); + const datestamp = moment - .unix(msg['time-sent'] / 1000) + .unix(date / 1000) .format(DATESTAMP_FORMAT); const contact = ((msg.author === window.ship && showOurContact) || diff --git a/pkg/interface/src/views/apps/chat/css/custom.css b/pkg/interface/src/views/apps/chat/css/custom.css index 3e6b6bf4cc..794356a265 100644 --- a/pkg/interface/src/views/apps/chat/css/custom.css +++ b/pkg/interface/src/views/apps/chat/css/custom.css @@ -98,8 +98,15 @@ h2 { font-family: 'Inter', sans-serif; } +.embed-container:not(.embed-container .embed-container):not(.links) { + padding: 0px 8px 8px 8px; +} + .embed-container iframe { max-width: 100%; + width: 100%; + height: 100%; + margin-top: 8px; } .mh-16 { diff --git a/pkg/interface/src/views/apps/launch/app.js b/pkg/interface/src/views/apps/launch/app.js index 400e2e1b9e..e9fa29426a 100644 --- a/pkg/interface/src/views/apps/launch/app.js +++ b/pkg/interface/src/views/apps/launch/app.js @@ -46,7 +46,7 @@ const ScrollbarLessBox = styled(Box)` const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']); export default function LaunchApp(props) { - const connection = { props }; + const { connection } = props; const baseHash = useLaunchState(state => state.baseHash); const [hashText, setHashText] = useState(baseHash); const [exitingTut, setExitingTut] = useState(false); diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx index 2f8180d7c2..cb32424776 100644 --- a/pkg/interface/src/views/apps/notifications/graph.tsx +++ b/pkg/interface/src/views/apps/notifications/graph.tsx @@ -1,65 +1,80 @@ -import React, { ReactNode, useCallback } from 'react'; -import moment from 'moment'; -import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react'; -import { Link, useHistory } from 'react-router-dom'; -import _ from 'lodash'; +import React, { ReactNode, useCallback } from "react"; +import moment from "moment"; +import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react"; +import { Link, useHistory } from "react-router-dom"; +import _ from "lodash"; import { GraphNotifIndex, GraphNotificationContents, Associations, Rolodex, - Groups -} from '~/types'; -import { Header } from './header'; -import { cite, deSig, pluralize, useShowNickname } from '~/logic/lib/util'; -import Author from '~/views/components/Author'; -import GlobalApi from '~/logic/api/global'; -import { getSnippet } from '~/logic/lib/publish'; -import styled from 'styled-components'; -import { MentionText } from '~/views/components/MentionText'; -import ChatMessage from '../chat/components/ChatMessage'; -import useContactState from '~/logic/state/contact'; -import useGroupState from '~/logic/state/group'; -import useMetadataState from '~/logic/state/metadata'; -import {PermalinkEmbed} from '../permalinks/embed'; -import {parsePermalink, referenceToPermalink} from '~/logic/lib/permalinks'; + Groups, +} from "~/types"; +import { Header } from "./header"; +import { + cite, + deSig, + pluralize, + useShowNickname, + isDm, +} from "~/logic/lib/util"; +import { GraphContentWide } from "~/views/landscape/components/Graph/GraphContentWide"; +import Author from "~/views/components/Author"; +import GlobalApi from "~/logic/api/global"; +import styled from "styled-components"; +import useContactState from "~/logic/state/contact"; +import useGroupState from "~/logic/state/group"; +import useMetadataState, { + useAssocForGraph, + useAssocForGroup, +} from "~/logic/state/metadata"; +import { PermalinkEmbed } from "../permalinks/embed"; +import { parsePermalink, referenceToPermalink } from "~/logic/lib/permalinks"; +import { Post, Group, Association } from "@urbit/api"; +import { BigInteger } from "big-integer"; + +const TruncBox = styled(Box)<{ truncate?: number }>` + -webkit-line-clamp: ${(p) => p.truncate ?? "unset"}; + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + color: ${(p) => p.theme.colors.black}; +`; function getGraphModuleIcon(module: string) { - if (module === 'link') { - return 'Collection'; + if (module === "link") { + return "Collection"; } - if(module === 'post') { - return 'Groups'; + if (module === "post") { + return "Groups"; } return _.capitalize(module); } -const FilterBox = styled(Box)` - background: linear-gradient( - to bottom, - transparent, - ${(p) => p.theme.colors.white} - ); -`; - -function describeNotification(description: string, plural: boolean): string { +function describeNotification( + description: string, + plural: boolean, + isDm: boolean, + singleAuthor: boolean +): string { switch (description) { - case 'post': - return 'replied to you'; - case 'link': - return `added ${pluralize('new link', plural)} to`; - case 'comment': - return `left ${pluralize('comment', plural)} on`; - case 'edit-comment': - return `updated ${pluralize('comment', plural)} on`; - case 'note': - return `posted ${pluralize('note', plural)} to`; - case 'edit-note': - return `updated ${pluralize('note', plural)} in`; - case 'mention': - return 'mentioned you on'; - case 'message': - return `sent ${pluralize('message', plural)} to`; + case "post": + return singleAuthor ? "replied to you" : "Your post received replies"; + case "link": + return `New link${plural ? "s" : ""} in`; + case "comment": + return `New comment${plural ? "s" : ""} on`; + case "note": + return `New Note${plural ? "s" : ""} in`; + case "edit-note": + return `updated ${pluralize("note", plural)} in`; + case "mention": + return singleAuthor ? "mentioned you in" : "You were mentioned in"; + case "message": + if (isDm) { + return "messaged you"; + } + return `New message${plural ? "s" : ""} in`; default: return description; } @@ -67,105 +82,87 @@ function describeNotification(description: string, plural: boolean): string { const GraphUrl = ({ contents, api }) => { const [{ text }, link] = contents; - - if('reference' in link) { + if ("reference" in link) { return ( - ); + /> + ); } return ( - - - + + + {text} ); +}; + +function ContentSummary({ icon, name, author, to }) { + return ( + + + + + + {name} + + + + + by + + + + + + ); } -export const GraphNodeContent = ({ - group, - association, - post, - mod, - index, -}) => { +export const GraphNodeContent = ({ post, mod, index, hidden, association }) => { const { contents } = post; - const idx = index.slice(1).split('/'); - if (mod === 'link') { - if (idx.length === 1) { - return ; - } else if (idx.length === 3) { - return ; - } - return null; - } - if (mod === 'publish') { - if (idx[1] === '2') { - return ( - - ); - } else if (idx[1] === '1') { - const [{ text: header }, { text: body }] = contents; - const snippet = getSnippet(body); - return ( - - - {header} - - - {snippet} - - - - ); - } - } - if(mod === 'post') { - return ; - } - - if (mod === 'chat') { + const idx = index.slice(1).split("/"); + const url = getNodeUrl(mod, hidden, association?.group, association?.resource, index); + if (mod === "link" && idx.length === 1) { + const [{ text: title }] = contents; return ( - - - + ); } - return null; + if (mod === "publish" && idx[1] === "1") { + const [{ text: title }] = contents; + return ( + + ); + } + return ( + + + + ); }; function getNodeUrl( @@ -175,78 +172,103 @@ function getNodeUrl( graph: string, index: string ) { - if (hidden && mod === 'chat') { - groupPath = '/messages'; + if (hidden && mod === "chat") { + groupPath = "/messages"; } else if (hidden) { - groupPath = '/home'; + groupPath = "/home"; } const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`; - const idx = index.slice(1).split('/'); - if (mod === 'publish') { - const [noteId] = idx; - return `${graphUrl}/note/${noteId}`; - } else if (mod === 'link') { - const [linkId] = idx; - return `${graphUrl}/index/${linkId}`; - } else if (mod === 'chat') { - if(idx.length > 0) { + const idx = index.slice(1).split("/"); + if (mod === "publish") { + console.log(idx); + const [noteId, kind, commId] = idx; + const selected = kind === "2" ? `?selected=${commId}` : ""; + return `${graphUrl}/note/${noteId}${selected}`; + } else if (mod === "link") { + const [linkId, commId] = idx; + return `${graphUrl}/index/${linkId}${commId ? `?selected=${commId}` : ""}`; + } else if (mod === "chat") { + if (idx.length > 0) { return `${graphUrl}?msg=${idx[0]}`; } return graphUrl; - } else if( mod === 'post') { + } else if (mod === "post") { return `/~landscape${groupPath}/feed${index}`; } - return ''; + return ""; } -const GraphNode = ({ - post, - author, - mod, - description, - time, - index, - graph, - groupPath, - group, - read, - onRead, - showContact = false -}) => { - author = deSig(author); - const history = useHistory(); - const contacts = useContactState((state) => state.contacts); - const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index); - const association = useMetadataState( - useCallback(s => s.associations.graph[graph], [graph]) +interface PostsByAuthor { + author: string; + posts: Post[]; +} +const GraphNodes = (props: { + posts: Post[]; + graph: string; + hideAuthors?: boolean; + group?: Group; + groupPath: string; + description: string; + index: string; + mod: string; + association: Association; + hidden: boolean; +}) => { + const { + posts, + mod, + hidden, + index, + description, + hideAuthors = false, + association, + } = props; + + const postsByConsecAuthor = _.reduce( + posts, + (acc: PostsByAuthor[], val: Post, key: number) => { + const lent = acc.length; + if (lent > 0 && acc?.[lent - 1]?.author === val.author) { + const last = acc[lent - 1]; + const rest = acc.slice(0, -1); + return [...rest, { ...last, posts: [...last.posts, val] }]; + } + return [...acc, { author: val.author, posts: [val] }]; + }, + [] ); - const onClick = useCallback(() => { - if (!read) { - onRead(); - } - history.push(nodeUrl); - }, [read, onRead]); - return ( - - - {showContact && ( - - )} - - - - - + <> + {_.map(postsByConsecAuthor, ({ posts, author }, idx) => { + const time = posts[0]?.["time-sent"]; + return ( + + {!hideAuthors && ( + + )} + + {_.map(posts, (post) => ( +