Merge branch 'release/next-js' into james/image-input

This commit is contained in:
James Acklin 2021-04-12 15:34:16 -04:00
commit cc0caa5aa5
35 changed files with 482 additions and 504 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:abc163491b53cc9d48a70ea378dde8bb455648e3f6e7f57a0c7a8e164d4ca105 oid sha256:accbadc701471f6b071ed286164a0bf4d3f8a2e64cfaea9019e123bd9edca569
size 10226599 size 10292454

View File

@ -5,7 +5,7 @@
/- glob /- glob
/+ default-agent, verb, dbug /+ default-agent, verb, dbug
|% |%
++ hash 0v2i7ds.j99ka.5dpja.pef1e.b04e0 ++ hash 0v4.7tk5q.9ha4l.tbmji.fvkno.s9pfq
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states +$ all-states
$% state-0 $% state-0

View File

@ -46,19 +46,8 @@
%0 %0
%_ $ %_ $
-.old %1 -.old %1
:: cards cards
validators.old validators.old validators.old
(~(put in validators.old) %graph-validator-link)
::
cards
%+ weld cards
%+ turn
~(tap in (~(put in validators.old) %graph-validator-link))
|= validator=@t
^- card
=/ =wire /validator/[validator]
=/ =rave:clay [%sing %b [%da now.bowl] /[validator]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]
:: ::
graphs.old graphs.old
%- ~(run by graphs.old) %- ~(run by graphs.old)
@ -285,19 +274,10 @@
graphs (~(put by graphs) resource [graph mark]) graphs (~(put by graphs) resource [graph mark])
update-logs (~(put by update-logs) resource update-log) update-logs (~(put by update-logs) resource update-log)
archive (~(del by archive) resource) archive (~(del by archive) resource)
::
validators
?~ mark validators
(~(put in validators) u.mark)
== ==
%- zing %- zing
:~ (give [/keys ~] %keys (~(put in ~(key by graphs)) resource)) :~ (give [/keys ~] %keys (~(put in ~(key by graphs)) resource))
(give [/updates ~] %add-graph resource *graph:store mark overwrite) (give [/updates ~] %add-graph resource *graph:store mark overwrite)
?~ mark ~
?: (~(has in validators) u.mark) ~
=/ wire /validator/[u.mark]
=/ =rave:clay [%sing %b [%da now.bowl] /[u.mark]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
== ==
:: ::
++ remove-graph ++ remove-graph
@ -1120,12 +1100,7 @@
:: ::
:: old wire, do nothing :: old wire, do nothing
[%graph *] [~ this] [%graph *] [~ this]
:: [%validator @ ~] [~ this]
[%validator @ ~]
:_ this
=* validator i.t.wire
=/ =rave:clay [%next %b [%da now.bowl] /[validator]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
:: ::
[%try-rejoin @ *] [%try-rejoin @ *]
=/ rid=resource:store (de-path:res t.t.wire) =/ rid=resource:store (de-path:res t.t.wire)

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script> <script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.3a0ba646997cd338b513.js"></script> <script src="/~landscape/js/bundle/index.857da0dbc92427b1460b.js"></script>
</body> </body>
</html> </html>

View File

@ -14,6 +14,7 @@
[%2 observers=(map serial observer:sur)] [%2 observers=(map serial observer:sur)]
[%3 observers=(map serial observer:sur)] [%3 observers=(map serial observer:sur)]
[%4 observers=(map serial observer:sur)] [%4 observers=(map serial observer:sur)]
[%5 observers=(map serial observer:sur) warm-cache=_|]
== ==
:: ::
+$ serial @uv +$ serial @uv
@ -27,7 +28,7 @@
-- --
:: ::
%- agent:dbug %- agent:dbug
=| [%4 observers=(map serial observer:sur)] =| [%5 observers=(map serial observer:sur) warm-cache=_|]
=* state - =* state -
:: ::
^- agent:gall ^- agent:gall
@ -42,6 +43,7 @@
(act [%watch %group-store /groups %group-on-leave]) (act [%watch %group-store /groups %group-on-leave])
(act [%watch %group-store /groups %group-on-remove-member]) (act [%watch %group-store /groups %group-on-remove-member])
(act [%watch %metadata-store /updates %md-on-add-group-feed]) (act [%watch %metadata-store /updates %md-on-add-group-feed])
(act [%warm-cache-all ~])
== ==
:: ::
++ act ++ act
@ -66,8 +68,13 @@
=| cards=(list card) =| cards=(list card)
|- |-
?- -.old-state ?- -.old-state
%4 %5
[cards this(state old-state)] [cards this(state old-state)]
%4
=. cards
:_ cards
(act [%warm-cache-all ~])
$(old-state [%5 observers.old-state %.n])
:: ::
%3 %3
=. cards =. cards
@ -110,11 +117,19 @@
?> (team:title our.bowl src.bowl) ?> (team:title our.bowl src.bowl)
?. ?=(%observe-action mark) ?. ?=(%observe-action mark)
(on-poke:def mark vase) (on-poke:def mark vase)
|^
=/ =action:sur !<(action:sur vase) =/ =action:sur !<(action:sur vase)
=* observer observer.action =* observer observer.action
=/ vals (silt ~(val by observers)) =/ vals (silt ~(val by observers))
?- -.action ?- -.action
%watch %watch (watch observer vals)
%ignore (ignore observer vals)
%warm-cache-all warm-cache-all
%cool-cache-all cool-cache-all
==
::
++ watch
|= [=observer:sur vals=(set observer:sur)]
?: ?|(=(app.observer %spider) =(app.observer %observe-hook)) ?: ?|(=(app.observer %spider) =(app.observer %observe-hook))
~|('we avoid infinite loops' !!) ~|('we avoid infinite loops' !!)
?: (~(has in vals) observer) ?: (~(has in vals) observer)
@ -129,7 +144,8 @@
path.observer path.observer
== ==
:: ::
%ignore ++ ignore
|= [=observer:sur vals=(set observer:sur)]
?. (~(has in vals) observer) ?. (~(has in vals) observer)
~|('cannot remove nonexistent observer' !!) ~|('cannot remove nonexistent observer' !!)
=/ key (got-by-val observers observer) =/ key (got-by-val observers observer)
@ -142,7 +158,19 @@
%leave %leave
~ ~
== ==
== ::
++ warm-cache-all
?: warm-cache
~|('cannot warm up cache that is already warm' !!)
:_ this(warm-cache %.y)
=/ =rave:clay [%sing [%t da+now.bowl /mar]]
[%pass /warm-cache %arvo %c %warp our.bowl %home `rave]~
::
++ cool-cache-all
?. warm-cache
~|('cannot cool down cache that is already cool' !!)
[~ this(warm-cache %.n)]
--
:: ::
++ on-agent ++ on-agent
|= [=wire =sign:agent:gall] |= [=wire =sign:agent:gall]
@ -260,9 +288,48 @@
== == == ==
-- --
:: ::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
:_ this
?+ wire (on-arvo:def wire sign-arvo)
[%warm-cache ~]
?. warm-cache
~
?> ?=([%clay %writ *] sign-arvo)
=* riot p.sign-arvo
?~ riot
=/ =rave:clay [%next [%t da+now.bowl /mar]]
[%pass /warm-cache %arvo %c %warp our.bowl %home `rave]~
:- =/ =rave:clay [%next [%t q.p.u.riot /mar]]
[%pass /warm-cache %arvo %c %warp our.bowl %home `rave]
%+ turn !<((list path) q.r.u.riot)
|= pax=path
^- card
=. pax (snip (slag 1 pax))
=/ mark=@ta
%+ roll pax
|= [=term mark=term]
?: ?=(%$ mark)
term
:((cury cat 3) mark '-' term)
=/ =rave:clay [%sing %b da+now.bowl /[mark]]
[%pass [%mar mark ~] %arvo %c %warp our.bowl %home `rave]
::
[%mar ^]
?. warm-cache
~
?> ?=([%clay %writ *] sign-arvo)
=* riot p.sign-arvo
=* mark t.wire
?~ riot
~
=/ =rave:clay [%next %b q.p.u.riot mark]
[%pass wire %arvo %c %warp our.bowl %home `rave]~
==
::
++ on-watch on-watch:def ++ on-watch on-watch:def
++ on-leave on-leave:def ++ on-leave on-leave:def
++ on-peek on-peek:def ++ on-peek on-peek:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def ++ on-fail on-fail:def
-- --

View File

@ -1,7 +1,14 @@
|% |%
+$ observer [app=term =path thread=term] +$ observer [app=term =path thread=term]
+$ action +$ action
$% [%watch =observer] $% :: %gall actions
::
[%watch =observer]
[%ignore =observer] [%ignore =observer]
::
:: %clay actions
::
[%warm-cache-all ~]
[%cool-cache-all ~]
== ==
-- --

View File

@ -6,7 +6,6 @@
/- gcp, spider, settings /- gcp, spider, settings
/+ strandio /+ strandio
=, strand=strand:spider =, strand=strand:spider
=, enjs:format
^- thread:spider ^- thread:spider
|^ |^
|= * |= *
@ -22,7 +21,7 @@
== ==
%- pure:m %- pure:m
!> !>
%+ frond %gcp-configured ^- json
b+has b+has
:: ::
++ has-settings ++ has-settings

View File

@ -1783,36 +1783,30 @@
"dependencies": { "dependencies": {
"@babel/runtime": { "@babel/runtime": {
"version": "7.12.5", "version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", "bundled": true,
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"requires": { "requires": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
}, },
"@types/lodash": { "@types/lodash": {
"version": "4.14.168", "version": "4.14.168",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", "bundled": true
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
}, },
"@urbit/eslint-config": { "@urbit/eslint-config": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz", "bundled": true
"integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw=="
}, },
"big-integer": { "big-integer": {
"version": "1.6.48", "version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", "bundled": true
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
}, },
"lodash": { "lodash": {
"version": "4.17.20", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", "bundled": true
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
}, },
"regenerator-runtime": { "regenerator-runtime": {
"version": "0.13.7", "version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "bundled": true
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
} }
} }
}, },

View File

@ -1,19 +1,16 @@
import BaseApi from './base'; import BaseApi from './base';
import {StoreState} from '../store/type'; import type {StoreState} from '../store/type';
import {GcpToken} from '../types/gcp-state'; import type {GcpToken} from '../../types/gcp-state';
export default class GcpApi extends BaseApi<StoreState> { export default class GcpApi extends BaseApi<StoreState> {
isConfigured() { // Does not touch the store; use the value manually.
return this.spider('noun', 'json', 'gcp-is-configured', {}) async isConfigured(): Promise<boolean> {
.then((data) => { return this.spider('noun', 'json', 'gcp-is-configured', {});
this.store.handleEvent({
data
});
});
} }
getToken() { // Does not return the token; read it out of the store.
async getToken(): Promise<void> {
return this.spider('noun', 'gcp-token', 'gcp-get-token', {}) return this.spider('noun', 'gcp-token', 'gcp-get-token', {})
.then((token) => { .then((token) => {
this.store.handleEvent({ this.store.handleEvent({

View File

@ -1,4 +1,5 @@
import bigInt, { BigInteger } from 'big-integer'; import bigInt, { BigInteger } from 'big-integer';
import { immerable } from 'immer';
interface NonemptyNode<V> { interface NonemptyNode<V> {
n: [BigInteger, V]; n: [BigInteger, V];
@ -14,6 +15,7 @@ type MapNode<V> = NonemptyNode<V> | null;
*/ */
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> { export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null; private root: MapNode<V> = null;
[immerable] = true;
size = 0; size = 0;
constructor(initial: [BigInteger, V][] = []) { constructor(initial: [BigInteger, V][] = []) {

View File

@ -26,7 +26,7 @@ class GcpUpload implements StorageUpload {
this.#accessKey = accessKey; this.#accessKey = accessKey;
} }
async promise(): UploadResult { async promise(): Promise<UploadResult> {
const {Bucket, Key, ContentType, Body} = this.#params; const {Bucket, Key, ContentType, Body} = this.#params;
const urlParams = { const urlParams = {
uploadType: 'media', uploadType: 'media',

View File

@ -5,9 +5,8 @@
// 1. call configure with a GlobalApi and GlobalStore. // 1. call configure with a GlobalApi and GlobalStore.
// 2. call start() to start the token refresh loop. // 2. call start() to start the token refresh loop.
// //
// If the ship does not have GCP storage configured, we don't try to get // If the ship does not have GCP storage configured, we don't try to
// a token, but we keep checking at regular intervals to see if it gets // get a token. If GCP storage is configured, we try to invoke the GCP
// configured. If GCP storage is configured, we try to invoke the GCP
// get-token thread on the ship until it gives us an access token. Once // get-token thread on the ship until it gives us an access token. Once
// we have a token, we refresh it every hour or so according to its // we have a token, we refresh it every hour or so according to its
// intrinsic expiry. // intrinsic expiry.
@ -25,7 +24,7 @@ class GcpManager {
} }
#running = false; #running = false;
#timeoutId: number | null = null; #timeoutId: ReturnType<typeof setTimeout> | null = null;
start() { start() {
if (this.#running) { if (this.#running) {
@ -61,19 +60,17 @@ class GcpManager {
} }
#consecutiveFailures: number = 0; #consecutiveFailures: number = 0;
#configured: boolean = false;
private isConfigured() {
return useStorageState.getState().gcp.configured;
}
private refreshLoop() { private refreshLoop() {
if (!this.isConfigured()) { if (!this.#configured) {
this.#api.gcp.isConfigured() this.#api!.gcp.isConfigured()
.then(() => { .then((configured) => {
if (this.isConfigured() === undefined) { if (configured === undefined) {
throw new Error("can't check whether GCP is configured?"); throw new Error("can't check whether GCP is configured?");
} }
if (this.isConfigured()) { this.#configured = configured;
if (this.#configured) {
this.refreshLoop(); this.refreshLoop();
} else { } else {
console.log('GcpManager: GCP storage not configured; stopping.'); console.log('GcpManager: GCP storage not configured; stopping.');
@ -86,7 +83,7 @@ class GcpManager {
}); });
return; return;
} }
this.#api.gcp.getToken() this.#api!.gcp.getToken()
.then(() => { .then(() => {
const token = useStorageState.getState().gcp.token; const token = useStorageState.getState().gcp.token;
if (token) { if (token) {

View File

@ -16,5 +16,5 @@ export function useCopy(copied: string, display: string) {
display, display,
]); ]);
return { copyDisplay, doCopy }; return { copyDisplay, doCopy, didCopy };
} }

View File

@ -0,0 +1,52 @@
import { useState, useEffect } from "react";
import { useWaitForProps } from "./useWaitForProps";
import {unstable_batchedUpdates} from "react-dom";
export type IOInstance<I, P, O> = (
input: I
) => (props: P) => Promise<[(p: P) => boolean, O]>;
export function useRunIO<I, O>(
io: (i: I) => Promise<O>,
after: (o: O) => void,
key: string
) {
const [resolve, setResolve] = useState<() => void>(() => () => {});
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
const [output, setOutput] = useState<O | null>(null);
const [done, setDone] = useState(false);
const run = (i: I) =>
new Promise((res, rej) => {
setResolve(() => res);
setReject(() => rej);
io(i)
.then((o) => {
unstable_batchedUpdates(() => {
setOutput(o);
setDone(true);
});
})
.catch(rej);
});
useEffect(() => {
reject(new Error("useRunIO: key changed"));
setDone(false);
setOutput(null);
}, [key]);
useEffect(() => {
if (!done) {
return;
}
try {
after(output!);
resolve();
} catch (e) {
reject(e);
}
}, [done]);
return run;
}

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import f, { compose, memoize } from 'lodash/fp'; import f, { compose, memoize } from 'lodash/fp';
import bigInt, { BigInteger } from 'big-integer'; import bigInt, { BigInteger } from 'big-integer';
@ -400,11 +400,15 @@ interface useHoveringInterface {
export const useHovering = (): useHoveringInterface => { export const useHovering = (): useHoveringInterface => {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const bind = { const onMouseOver = useCallback(() => setHovering(true), [])
onMouseOver: () => setHovering(true), const onMouseLeave = useCallback(() => setHovering(false), [])
onMouseLeave: () => setHovering(false) const bind = useMemo(() => ({
}; onMouseOver,
return { hovering, bind }; onMouseLeave,
}), [onMouseLeave, onMouseOver]);
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
}; };
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/; const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;

View File

@ -1,27 +1,17 @@
import _ from 'lodash';
import {StoreState} from '../store/type'; import {StoreState} from '../store/type';
import {GcpToken} from '../../types/gcp-state'; import type {GcpToken} from '../../types/gcp-state';
import { Cage } from '~/types/cage'; import type {Cage} from '~/types/cage';
import useStorageState, { StorageState } from '../state/storage'; import useStorageState, { StorageState } from '../state/storage';
import { reduceState } from '../state/base'; import { reduceState } from '../state/base';
export default class GcpReducer { export default class GcpReducer {
reduce(json: Cage) { reduce(json: Cage) {
reduceState<StorageState, any>(useStorageState, json, [ reduceState<StorageState, any>(useStorageState, json, [
reduceConfigured,
reduceToken reduceToken
]); ]);
} }
} }
const reduceConfigured = (json, state: StorageState): StorageState => {
let data = json['gcp-configured'];
if (data !== undefined) {
state.gcp.configured = data;
}
return state;
}
const reduceToken = (json: Cage, state: StorageState): StorageState => { const reduceToken = (json: Cage, state: StorageState): StorageState => {
let data = json['gcp-token']; let data = json['gcp-token'];
if (data) { if (data) {
@ -37,7 +27,7 @@ const setToken = (data: any, state: StorageState): StorageState => {
return state; return state;
} }
const isToken = (token: any): boolean => { const isToken = (token: any): token is GcpToken => {
return (typeof(token.accessKey) === 'string' && return (typeof(token.accessKey) === 'string' &&
typeof(token.expiresIn) === 'number'); typeof(token.expiresIn) === 'number');
} }

View File

@ -49,6 +49,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
} }
restart() { restart() {
this.start(); this.openSubscriptions = {};
super.restart();
} }
} }

View File

@ -4,6 +4,5 @@ export interface GcpToken {
}; };
export interface GcpState { export interface GcpState {
configured?: boolean;
token?: GcpToken token?: GcpToken
}; };

View File

@ -29,8 +29,8 @@ import {
Groups, Groups,
Associations Associations
} from '~/types'; } from '~/types';
import TextContent from './content/text'; import TextContent from '../../../landscape/components/Graph/content/text';
import CodeContent from './content/code'; import CodeContent from '../../../landscape/components/Graph/content/code';
import RemoteContent from '~/views/components/RemoteContent'; import RemoteContent from '~/views/components/RemoteContent';
import { Mention } from '~/views/components/MentionText'; import { Mention } from '~/views/components/MentionText';
import { Dropdown } from '~/views/components/Dropdown'; import { Dropdown } from '~/views/components/Dropdown';
@ -42,8 +42,7 @@ import useContactState from '~/logic/state/contact';
import { useIdlingState } from '~/logic/lib/idling'; import { useIdlingState } from '~/logic/lib/idling';
import ProfileOverlay from '~/views/components/ProfileOverlay'; import ProfileOverlay from '~/views/components/ProfileOverlay';
import {useCopy} from '~/logic/lib/useCopy'; import {useCopy} from '~/logic/lib/useCopy';
import {PermalinkEmbed} from '../../permalinks/embed'; import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
import {referenceToPermalink} from '~/logic/lib/permalinks';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -284,8 +283,7 @@ class ChatMessage extends Component<ChatMessageProps> {
hideHover hideHover
} = this.props; } = this.props;
let onReply = this.props?.onReply; let onReply = this.props?.onReply ?? (() => {});
onReply ??= () => {};
const transcluded = this.props?.transcluded ?? 0; const transcluded = this.props?.transcluded ?? 0;
let { renderSigil } = this.props; let { renderSigil } = this.props;
@ -397,12 +395,11 @@ export const MessageAuthor = ({
msg.author !== window.ship) && msg.author !== window.ship) &&
`~${msg.author}` in contacts `~${msg.author}` in contacts
? contacts[`~${msg.author}`] ? contacts[`~${msg.author}`]
: false; : undefined;
const showNickname = useShowNickname(contact); const showNickname = useShowNickname(contact);
const { hideAvatars } = useSettingsState(selectCalmState); const { hideAvatars } = useSettingsState(selectCalmState);
const shipName = showNickname ? contact.nickname : cite(msg.author); const shipName = showNickname && contact?.nickname || cite(msg.author) || `~${msg.author}`;
const copyNotice = 'Copied';
const color = contact const color = contact
? `#${uxToHex(contact.color)}` ? `#${uxToHex(contact.color)}`
: dark : dark
@ -413,28 +410,10 @@ export const MessageAuthor = ({
: dark : dark
? 'mix-blend-diff' ? 'mix-blend-diff'
: 'mix-blend-darken'; : 'mix-blend-darken';
const [displayName, setDisplayName] = useState(shipName);
const [nameMono, setNameMono] = useState(showNickname ? false : true); const { copyDisplay, doCopy, didCopy } = useCopy(`~${msg.author}`, shipName);
const { hovering, bind } = useHovering(); const { hovering, bind } = useHovering();
const [showOverlay, setShowOverlay] = useState(false); const nameMono = !(showNickname || didCopy);
const toggleOverlay = () => {
setShowOverlay((value) => !value);
};
const showCopyNotice = () => {
setDisplayName(copyNotice);
setNameMono(false);
};
useEffect(() => {
const resetDisplay = () => {
setDisplayName(shipName);
setNameMono(showNickname ? false : true);
};
const timer = setTimeout(() => resetDisplay(), 800);
return () => clearTimeout(timer);
}, [shipName, displayName]);
const img = const img =
contact?.avatar && !hideAvatars ? ( contact?.avatar && !hideAvatars ? (
@ -471,9 +450,6 @@ export const MessageAuthor = ({
return ( return (
<Box display='flex' alignItems='flex-start' {...rest}> <Box display='flex' alignItems='flex-start' {...rest}>
<Box <Box
onClick={() => {
setShowOverlay(true);
}}
height={24} height={24}
pr={2} pr={2}
mt={'1px'} mt={'1px'}
@ -501,13 +477,10 @@ export const MessageAuthor = ({
mono={nameMono} mono={nameMono}
fontWeight={nameMono ? '400' : '500'} fontWeight={nameMono ? '400' : '500'}
cursor='pointer' cursor='pointer'
onClick={() => { onClick={doCopy}
writeText(`~${msg.author}`);
showCopyNotice();
}}
title={`~${msg.author}`} title={`~${msg.author}`}
> >
{displayName} {copyDisplay}
</Text> </Text>
<Text flexShrink={0} fontSize={0} gray> <Text flexShrink={0} fontSize={0} gray>
{timestamp} {timestamp}
@ -539,7 +512,6 @@ export const Message = ({
...rest ...rest
}) => { }) => {
const { hovering, bind } = useHovering(); const { hovering, bind } = useHovering();
const contacts = useContactState((state) => state.contacts);
return ( return (
<Box width="100%" position='relative' {...rest}> <Box width="100%" position='relative' {...rest}>
{timestampHover ? ( {timestampHover ? (
@ -558,66 +530,14 @@ export const Message = ({
) : ( ) : (
<></> <></>
)} )}
<Box width="100%" {...bind}> <GraphContentWide
{msg.contents.map((content, i) => { {...bind}
switch (Object.keys(content)[0]) { width="100%"
case 'text': post={msg}
return (
<TextContent
key={i}
api={api}
fontSize={1}
lineHeight={'20px'}
content={content}
/>
);
case 'code':
return <CodeContent key={i} content={content} />;
case 'reference':
const { link } = referenceToPermalink(content);
return (
<PermalinkEmbed
link={link}
api={api}
transcluded={transcluded} transcluded={transcluded}
api={api}
showOurContact={showOurContact} showOurContact={showOurContact}
/> />
);
case 'url':
return (
<Box
key={i}
flexShrink={0}
fontSize={1}
lineHeight='20px'
color='black'
width="fit-content"
maxWidth="500px"
>
<RemoteContent
key={content.url}
url={content.url}
/>
</Box>
);
case 'mention':
const first = (i) => i === 0;
return (
<Mention
key={i}
first={first(i)}
group={group}
scrollWindow={scrollWindow}
ship={content.mention}
contact={contacts?.[`~${content.mention}`]}
api={api}
/>
);
default:
return null;
}
})}
</Box>
</Box> </Box>
); );
}; };

View File

@ -100,7 +100,7 @@ function TranscludedPublishNode(props: {
?.get(bigInt.one) ?.get(bigInt.one)
?.children?.peekLargest()?.[1]!; ?.children?.peekLargest()?.[1]!;
return ( return (
<Col gapY="2"> <Col color="black" gapY="2">
<Author <Author
px="2" px="2"
showImage showImage

View File

@ -93,9 +93,6 @@ export function EditProfile(props: any): ReactElement {
}; };
const history = useHistory(); const history = useHistory();
if (contact) {
contact.isPublic = isPublic;
}
const onSubmit = async (values: any, actions: any) => { const onSubmit = async (values: any, actions: any) => {
try { try {
@ -143,7 +140,7 @@ export function EditProfile(props: any): ReactElement {
<> <>
<Formik <Formik
validationSchema={formSchema} validationSchema={formSchema}
initialValues={contact || emptyContact} initialValues={{...contact, isPublic } || emptyContact}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({ setFieldValue }) => ( {({ setFieldValue }) => (

View File

@ -1,105 +0,0 @@
import React, { useEffect } from "react";
import { AsyncButton } from "../../../components/AsyncButton";
import * as Yup from "yup";
import {
Box,
ManagedTextInputField as Input,
ManagedCheckboxField as Checkbox,
Col,
Button,
Center,
} from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "@urbit/api/contacts";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps, useHistory } from "react-router-dom";
import {Association} from "@urbit/api";
import { uxToHex } from "~/logic/lib/util";
interface MetadataFormProps {
host: string;
book: string;
association: Association;
api: GlobalApi;
}
interface FormSchema {
name: string;
description: string;
}
const formSchema = Yup.object({
name: Yup.string().required("Notebook must have a name"),
description: Yup.string()
});
const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => {
const { resetForm } = useFormikContext<FormSchema>();
useEffect(() => {
resetForm({ values: props.init });
}, [props.book]);
return null;
};
export function MetadataForm(props: MetadataFormProps) {
const { api, book } = props;
const { metadata } = props.association || {};
const initialValues: FormSchema = {
name: metadata?.title,
description: metadata?.description,
};
const onSubmit = async (
values: FormSchema,
actions: FormikHelpers<FormSchema>
) => {
try {
const { name, description } = values;
await api.metadata.metadataAdd(
"publish",
props.association.resource,
props.association.group,
name,
description,
props.association.metadata["date-created"],,
uxToHex(props.association.metadata.color)
);
actions.setStatus({ success: null });
} catch (e) {
console.log(e);
actions.setStatus({ error: e.message });
}
};
return (
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form style={{ display: "contents" }}>
<Input
id="name"
label="Rename"
caption="Change the name of this notebook"
/>
<Input
id="description"
label="Change description"
caption="Change the description of this notebook"
/>
<ResetOnPropsChange init={initialValues} book={book} />
<AsyncButton primary loadingText="Updating.." border>
Save
</AsyncButton>
<FormError message="Failed to update settings" />
</Form>
</Formik>
);
}

View File

@ -15,6 +15,7 @@ import { getLatestCommentRevision } from '~/logic/lib/publish';
import {useCopy} from '~/logic/lib/useCopy'; import {useCopy} from '~/logic/lib/useCopy';
import { getPermalinkForGraph} from '~/logic/lib/permalinks'; import { getPermalinkForGraph} from '~/logic/lib/permalinks';
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from '~/logic/state/metadata';
import {GraphContentWide} from '../landscape/components/Graph/GraphContentWide';
const ClickBox = styled(Box)` const ClickBox = styled(Box)`
cursor: pointer; cursor: pointer;
@ -101,20 +102,17 @@ export function CommentItem(props: CommentItemProps): ReactElement {
</Row> </Row>
</Author> </Author>
</Row> </Row>
<Box <GraphContentWide
borderRadius="1" borderRadius="1"
p="1" p="1"
mb="1" mb="1"
backgroundColor={props.highlighted ? 'washedBlue' : 'white'} backgroundColor={props.highlighted ? 'washedBlue' : 'white'}
>
<MentionText
transcluded={0} transcluded={0}
api={api} api={api}
group={group} post={post}
content={post?.contents} showOurContact
/> />
</Box> </Box>
</Box>
); );
} }

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState, useLayoutEffect, ReactElement } from 'react'; import React, { useEffect, useState, useLayoutEffect, ReactElement } from 'react';
import { useHistory } from 'react-router-dom';
import { Box, Text, Row, Col } from '@tlon/indigo-react'; import { Box, Text, Row, Col } from '@tlon/indigo-react';
import { Associations, Groups } from '@urbit/api'; import { Associations, Groups } from '@urbit/api';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import { MetadataIcon } from '../landscape/components/MetadataIcon'; import { MetadataIcon } from '../landscape/components/MetadataIcon';
import { JoinGroup } from '../landscape/components/JoinGroup'; import { JoinGroup } from '../landscape/components/JoinGroup';
@ -23,27 +22,12 @@ export function GroupLink(
const name = resource.slice(6); const name = resource.slice(6);
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null); const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
const associations = useMetadataState(state => state.associations); const associations = useMetadataState(state => state.associations);
const { save, restore } = useVirtual();
const history = useHistory();
const joined = resource in associations.groups; const joined = resource in associations.groups;
const { save, restore } = useVirtual();
const { modal, showModal } = useModal({ const { modal, showModal } = useModal({
modal: modal: <JoinGroup api={api} autojoin={name} />
joined && preview ? (
<Box width="fit-content" p="4">
<GroupSummary
metadata={preview.metadata}
memberCount={preview.members}
channelCount={preview?.['channel-count']}
/>
</Box>
) : (
<JoinGroup
api={api}
autojoin={name}
/>
)
}); });
useEffect(() => { useEffect(() => {
@ -72,7 +56,9 @@ export function GroupLink(
alignItems="center" alignItems="center"
py="2" py="2"
pr="2" pr="2"
onClick={showModal} onClick={
joined ? () => history.push(`/~landscape/ship/${name}`) : showModal
}
cursor='pointer' cursor='pointer'
opacity={preview ? '1' : '0.6'} opacity={preview ? '1' : '0.6'}
> >

View File

@ -1,27 +1,28 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from "react";
import { useHistory } from 'react-router-dom'; import { useHistory } from "react-router-dom";
import { import {
MetadataUpdatePreview, MetadataUpdatePreview,
Contacts, Contacts,
JoinRequests, JoinRequests,
Groups, Groups,
Associations Associations,
} from '@urbit/api'; } from "@urbit/api";
import { Invite } from '@urbit/api/invite'; import { Invite } from "@urbit/api/invite";
import { Text, Icon, Row } from '@tlon/indigo-react'; import { Text, Icon, Row } from "@tlon/indigo-react";
import { cite, useShowNickname } from '~/logic/lib/util'; import { cite, useShowNickname } from "~/logic/lib/util";
import GlobalApi from '~/logic/api/global'; import GlobalApi from "~/logic/api/global";
import { resourceFromPath } from '~/logic/lib/group'; import { resourceFromPath } from "~/logic/lib/group";
import { GroupInvite } from './Group'; import { GroupInvite } from "./Group";
import { InviteSkeleton } from './InviteSkeleton'; import { InviteSkeleton } from "./InviteSkeleton";
import { JoinSkeleton } from './JoinSkeleton'; import { JoinSkeleton } from "./JoinSkeleton";
import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import useGroupState from '~/logic/state/group'; import useGroupState from "~/logic/state/group";
import useContactState from '~/logic/state/contact'; import useContactState from "~/logic/state/contact";
import useMetadataState from '~/logic/state/metadata'; import useMetadataState from "~/logic/state/metadata";
import useGraphState from '~/logic/state/graph'; import useGraphState from "~/logic/state/graph";
import { useRunIO } from "~/logic/lib/useRunIO";
interface InviteItemProps { interface InviteItemProps {
invite?: Invite; invite?: Invite;
@ -32,59 +33,78 @@ interface InviteItemProps {
api: GlobalApi; api: GlobalApi;
} }
export function InviteItem(props: InviteItemProps) { export function useInviteAccept(
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null); resource: string,
const { pendingJoin, invite, resource, uid, app, api } = props; api: GlobalApi,
app?: string,
uid?: string,
invite?: Invite,
) {
const { ship, name } = resourceFromPath(resource); const { ship, name } = resourceFromPath(resource);
const groups = useGroupState(state => state.groups);
const graphKeys = useGraphState(s => s.graphKeys);
const associations = useMetadataState(state => state.associations);
const contacts = useContactState(state => state.contacts);
const contact = contacts?.[`~${invite?.ship}`] ?? {};
const showNickname = useShowNickname(contact);
const waiter = useWaitForProps(
{ associations, groups, pendingJoin, graphKeys: Array.from(graphKeys) },
50000
);
const history = useHistory(); const history = useHistory();
const inviteAccept = useCallback(async () => { const associations = useMetadataState((s) => s.associations);
const groups = useGroupState((s) => s.groups);
const graphKeys = useGraphState((s) => s.graphKeys);
const waiter = useWaitForProps({ associations, graphKeys, groups });
return useRunIO<void, boolean>(
async () => {
if (!(app && invite && uid)) { if (!(app && invite && uid)) {
return; return false;
} }
if (resource in groups) { if (resource in groups) {
await api.invite.decline(app, uid); await api.invite.decline(app, uid);
return; return false;
} }
api.groups.join(ship, name); await api.groups.join(ship, name);
await waiter(p => !!p.pendingJoin); await api.invite.accept(app, uid);
api.invite.accept(app, uid);
await waiter((p) => { await waiter((p) => {
return ( return (
resource in p.groups && (resource in p.groups &&
(resource in (p.associations?.graph ?? {}) || resource in (p.associations?.graph ?? {}) &&
resource in (p.associations?.groups ?? {})) p.graphKeys.has(resource.slice(7))) ||
resource in (p.associations?.groups ?? {})
); );
}); });
return true;
},
(success: boolean) => {
if (!success) {
return;
}
if (groups?.[resource]?.hidden) { if (groups?.[resource]?.hidden) {
await waiter(p => p.graphKeys.includes(resource.slice(7)));
const { metadata } = associations.graph[resource]; const { metadata } = associations.graph[resource];
if (metadata && 'graph' in metadata.config) { if (metadata && "graph" in metadata.config) {
if (metadata.config.graph === 'chat') { if (metadata.config.graph === "chat") {
history.push(`/~landscape/messages/resource/${metadata.config.graph}${resource}`); history.push(
`/~landscape/messages/resource/${metadata.config.graph}${resource}`
);
} else { } else {
history.push(`/~landscape/home/resource/${metadata.config.graph}${resource}`); history.push(
`/~landscape/home/resource/${metadata.config.graph}${resource}`
);
} }
} else { } else {
console.error('unknown metadata: ', metadata); console.error("unknown metadata: ", metadata);
} }
} else { } else {
history.push(`/~landscape${resource}`); history.push(`/~landscape${resource}`);
} }
}, [app, history, waiter, invite, uid, resource, groups, associations]); },
resource
);
}
export function InviteItem(props: InviteItemProps) {
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
const { pendingJoin, invite, resource, uid, app, api } = props;
const { name } = resourceFromPath(resource);
const contacts = useContactState((state) => state.contacts);
const contact = contacts?.[`~${invite?.ship}`] ?? {};
const showNickname = useShowNickname(contact);
const inviteAccept = useInviteAccept(resource, api, app, uid, invite);
const inviteDecline = useCallback(async () => { const inviteDecline = useCallback(async () => {
if (!(app && uid)) { if (!(app && uid)) {
@ -96,7 +116,7 @@ export function InviteItem(props: InviteItemProps) {
const handlers = { onAccept: inviteAccept, onDecline: inviteDecline }; const handlers = { onAccept: inviteAccept, onDecline: inviteDecline };
useEffect(() => { useEffect(() => {
if (!app || app === 'groups') { if (!app || app === "groups") {
(async () => { (async () => {
setPreview(await api.metadata.preview(resource)); setPreview(await api.metadata.preview(resource));
})(); })();
@ -123,7 +143,7 @@ export function InviteItem(props: InviteItemProps) {
{...handlers} {...handlers}
/> />
); );
} else if (invite && name.startsWith('dm--')) { } else if (invite && name.startsWith("dm--")) {
return ( return (
<InviteSkeleton <InviteSkeleton
gapY="3" gapY="3"
@ -133,16 +153,18 @@ export function InviteItem(props: InviteItemProps) {
> >
<Row py="1" alignItems="center"> <Row py="1" alignItems="center">
<Icon display="block" color="blue" icon="Bullet" mr="2" /> <Icon display="block" color="blue" icon="Bullet" mr="2" />
<Text mr="1" <Text
mr="1"
mono={!showNickname} mono={!showNickname}
fontWeight={showNickname ? '500' : '400'}> fontWeight={showNickname ? "500" : "400"}
>
{showNickname ? contact?.nickname : cite(`~${invite!.ship}`)} {showNickname ? contact?.nickname : cite(`~${invite!.ship}`)}
</Text> </Text>
<Text mr="1">invited you to a DM</Text> <Text mr="1">invited you to a DM</Text>
</Row> </Row>
</InviteSkeleton> </InviteSkeleton>
); );
} else if (status && name.startsWith('dm--')) { } else if (status && name.startsWith("dm--")) {
return ( return (
<JoinSkeleton api={api} resource={resource} status={status} gapY="3"> <JoinSkeleton api={api} resource={resource} status={status} gapY="3">
<Row py="1" alignItems="center"> <Row py="1" alignItems="center">
@ -162,9 +184,11 @@ export function InviteItem(props: InviteItemProps) {
> >
<Row py="1" alignItems="center"> <Row py="1" alignItems="center">
<Icon display="block" color="blue" icon="Bullet" mr="2" /> <Icon display="block" color="blue" icon="Bullet" mr="2" />
<Text mr="1" <Text
mr="1"
mono={!showNickname} mono={!showNickname}
fontWeight={showNickname ? '500' : '400'}> fontWeight={showNickname ? "500" : "400"}
>
{showNickname ? contact?.nickname : cite(`~${invite!.ship}`)} {showNickname ? contact?.nickname : cite(`~${invite!.ship}`)}
</Text> </Text>
<Text mr="1"> <Text mr="1">
@ -174,14 +198,12 @@ export function InviteItem(props: InviteItemProps) {
</InviteSkeleton> </InviteSkeleton>
); );
} else if (pendingJoin) { } else if (pendingJoin) {
const [, , ship, name] = resource.split('/'); const [, , ship, name] = resource.split("/");
return ( return (
<JoinSkeleton api={api} resource={resource} status={pendingJoin}> <JoinSkeleton api={api} resource={resource} status={pendingJoin}>
<Row py="1" alignItems="center"> <Row py="1" alignItems="center">
<Icon display="block" color="blue" icon="Bullet" mr="2" /> <Icon display="block" color="blue" icon="Bullet" mr="2" />
<Text mr="1"> <Text mr="1">You are joining</Text>
You are joining
</Text>
<Text mono> <Text mono>
{cite(ship)}/{name} {cite(ship)}/{name}
</Text> </Text>

View File

@ -6,7 +6,7 @@ import RichText from '~/views/components/RichText';
import { cite, useShowNickname, uxToHex } from '~/logic/lib/util'; import { cite, useShowNickname, uxToHex } from '~/logic/lib/util';
import ProfileOverlay from '~/views/components/ProfileOverlay'; import ProfileOverlay from '~/views/components/ProfileOverlay';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useContactState from '~/logic/state/contact'; import useContactState, {useContact} from '~/logic/state/contact';
import {referenceToPermalink} from '~/logic/lib/permalinks'; import {referenceToPermalink} from '~/logic/lib/permalinks';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
@ -40,35 +40,18 @@ export function MentionText(props: MentionTextProps) {
} }
export function Mention(props: { export function Mention(props: {
contact: Contact;
group: Group;
scrollWindow?: HTMLElement;
ship: string; ship: string;
first?: Boolean; first?: Boolean;
api: any; api: any;
}) { }) {
const { ship, scrollWindow, first, api, ...rest } = props; const { ship, first, api, ...rest } = props;
let { contact } = props; const contact = useContact(ship);
const contacts = useContactState(state => state.contacts);
contact = contact?.color ? contact : contacts?.[`~${ship}`];
const history = useHistory();
const showNickname = useShowNickname(contact); const showNickname = useShowNickname(contact);
const name = showNickname ? contact?.nickname : cite(ship); const name = showNickname ? contact?.nickname : cite(ship);
const group = props.group ?? { hidden: true };
const [showOverlay, setShowOverlay] = useState(false);
const toggleOverlay = useCallback(() => {
setShowOverlay((value) => !value);
}, [showOverlay]);
return ( return (
<Box position='relative' display='inline-block' cursor='pointer' {...rest}> <ProfileOverlay ship={ship} api={api}>
<ProfileOverlay
ship={ship}
api={api}
>
<Text <Text
onClick={() => toggleOverlay()}
marginLeft={first? 0 : 1} marginLeft={first? 0 : 1}
marginRight={1} marginRight={1}
px={1} px={1}
@ -80,6 +63,5 @@ export function Mention(props: {
{name} {name}
</Text> </Text>
</ProfileOverlay> </ProfileOverlay>
</Box>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { PureComponent, useCallback, useEffect, useRef, useState, useMemo } from 'react'; import React, { PureComponent, useCallback, useEffect, useRef, useState, useMemo, ReactNode } from 'react';
import { Contact, Group, uxToHex } from '@urbit/api'; import { Contact, Group, uxToHex } from '@urbit/api';
import _ from 'lodash'; import _ from 'lodash';
import VisibilitySensor from 'react-visibility-sensor'; import VisibilitySensor from 'react-visibility-sensor';
@ -40,6 +40,7 @@ const FixedOverlay = styled(Col)`
type ProfileOverlayProps = BoxProps & { type ProfileOverlayProps = BoxProps & {
ship: string; ship: string;
api: any; api: any;
children: ReactNode;
}; };
const ProfileOverlay = (props: ProfileOverlayProps) => { const ProfileOverlay = (props: ProfileOverlayProps) => {

View File

@ -0,0 +1,82 @@
import React from "react";
import { Post, ReferenceContent } from "@urbit/api";
import { Box } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import TextContent from "./content/text";
import CodeContent from "./content/code";
import RemoteContent from "~/views/components/RemoteContent";
import { Mention } from "~/views/components/MentionText";
import { PermalinkEmbed } from "~/views/apps/permalinks/embed";
import { referenceToPermalink } from "~/logic/lib/permalinks";
import { PropFunc } from "~/types";
function GraphContentWideInner(
props: {
transcluded?: number;
post: Post;
api: GlobalApi;
showOurContact: boolean;
} & PropFunc<typeof Box>
) {
const { post, transcluded = 0, showOurContact, api, ...rest } = props;
return (
<Box {...rest}>
{post.contents.map((content, i) => {
switch (Object.keys(content)[0]) {
case "text":
return (
<TextContent
key={i}
api={api}
fontSize={1}
lineHeight={"20px"}
content={content}
/>
);
case "code":
return <CodeContent key={i} content={content} />;
case "reference":
const { link } = referenceToPermalink(content as ReferenceContent);
return (
<PermalinkEmbed
link={link}
api={api}
transcluded={transcluded}
showOurContact={showOurContact}
/>
);
case "url":
return (
<Box
key={i}
flexShrink={0}
fontSize={1}
lineHeight="20px"
color="black"
width="fit-content"
maxWidth="min(500px, 100%)"
>
<RemoteContent key={content.url} url={content.url} />
</Box>
);
case "mention":
const first = (i) => i === 0;
return (
<Mention
key={i}
first={first(i)}
ship={content.mention}
api={api}
/>
);
default:
return null;
}
})}
</Box>
);
}
export const GraphContentWide = React.memo(GraphContentWideInner);

View File

@ -23,7 +23,7 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
anchorRef anchorRef
); );
return ( return (
<Col {...rest} ref={anchorRef} gapY="4"> <Col {...rest} ref={anchorRef} gapY="4" maxWidth={['100%', '288px']}>
<Row gapX="2" width="100%"> <Row gapX="2" width="100%">
<MetadataIcon <MetadataIcon
width="40px" width="40px"

View File

@ -1,30 +1,35 @@
import React from 'react'; import React from 'react';
import { Col } from '@tlon/indigo-react'; import { Col, Box } from '@tlon/indigo-react';
import { MentionText } from '~/views/components/MentionText'; import { GraphContentWide } from "~/views/landscape/components/Graph/GraphContentWide";
import useContactState from '~/logic/state/contact'; import styled from 'styled-components';
const TruncatedBox = styled(Col)`
display: -webkit-box;
-webkit-line-clamp: ${p => p.truncate ?? 'unset'};
-webkit-box-orient: vertical;
`;
export function PostContent(props) { export function PostContent(props) {
const { post, isParent, api, isReply } = props; const { post, isParent, api, isReply } = props;
const contacts = useContactState(state => state.contacts);
return ( return (
<Col <TruncatedBox
display="-webkit-box"
width="100%" width="100%"
pl="2" px="2"
pr="2" pb="2"
pb={isParent || isReply ? "0" : "2"} truncate={isParent ? null : 8}
maxHeight={ isParent ? "none" : "300px" }
textOverflow="ellipsis" textOverflow="ellipsis"
overflow="hidden" overflow="hidden"
display="inline-block"> >
<MentionText <GraphContentWide
contacts={contacts}
content={post.contents}
api={api}
transcluded={0} transcluded={0}
post={post}
api={api}
showOurContact
/> />
</Col> </TruncatedBox>
); );
} }

View File

@ -151,6 +151,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
</StatelessAsyncButton> </StatelessAsyncButton>
</Col> </Col>
) : preview ? ( ) : preview ? (
<>
<GroupSummary <GroupSummary
metadata={preview.metadata} metadata={preview.metadata}
memberCount={preview?.members} memberCount={preview?.members}
@ -184,14 +185,16 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
</Box> </Box>
</Col> </Col>
)} )}
</GroupSummary>
<StatelessAsyncButton <StatelessAsyncButton
marginTop={3}
primary primary
name="join" name="join"
onClick={() => onConfirm(preview.group)} onClick={() => onConfirm(preview.group)}
> >
Join {preview.metadata.title} Join {preview.metadata.title}
</StatelessAsyncButton> </StatelessAsyncButton>
</GroupSummary> </>
) : ( ) : (
<Col width="100%" gapY="4"> <Col width="100%" gapY="4">
<Formik <Formik

View File

@ -1,4 +1,5 @@
import { BigInteger } from "big-integer"; import { BigInteger } from "big-integer";
import { immerable } from 'immer';
interface NonemptyNode<V> { interface NonemptyNode<V> {
n: [BigInteger, V]; n: [BigInteger, V];
@ -14,6 +15,7 @@ type MapNode<V> = NonemptyNode<V> | null;
*/ */
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> { export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null; private root: MapNode<V> = null;
[immerable] = true;
size: number = 0; size: number = 0;
constructor(initial: [BigInteger, V][] = []) { constructor(initial: [BigInteger, V][] = []) {

View File

@ -21,6 +21,7 @@
"@types/lodash": "^4.14.168", "@types/lodash": "^4.14.168",
"@urbit/eslint-config": "^1.0.0", "@urbit/eslint-config": "^1.0.0",
"big-integer": "^1.6.48", "big-integer": "^1.6.48",
"immer": "^9.0.1",
"lodash": "^4.17.20" "lodash": "^4.17.20"
} }
} }