mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-17 03:44:34 +03:00
Merge remote-tracking branch 'origin/master' into release/next-sys
This commit is contained in:
commit
ca389b42cf
@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f6b5e33e573818120051651c1182163527edbbe0dff0eb6591e12a55cfccb273
|
oid sha256:ae362654e444357b86e41d37123eaf79da0d6c97545fa619b8db64a71efa3ff6
|
||||||
size 10486101
|
size 10551570
|
||||||
|
@ -82,6 +82,8 @@ haskell-nix.stackProject {
|
|||||||
|
|
||||||
urbit-king.components.tests.urbit-king-tests.testFlags =
|
urbit-king.components.tests.urbit-king-tests.testFlags =
|
||||||
[ "--brass-pill=${brass.lfs}" ];
|
[ "--brass-pill=${brass.lfs}" ];
|
||||||
|
|
||||||
|
lmdb.components.library.libs = lib.mkForce [ lmdb ];
|
||||||
};
|
};
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v3.g6u13.haedt.jt4hd.61ek5.6t30q
|
++ hash 0v3.m4922.94fro.soub7.2cod3.0rfhh
|
||||||
+$ 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
|
||||||
|
@ -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.59e682153138f604d358.js"></script>
|
<script src="/~landscape/js/bundle/index.d68f6086306286b082be.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -90,7 +90,12 @@
|
|||||||
$: tracking=(map resource track)
|
$: tracking=(map resource track)
|
||||||
inner-state=vase
|
inner-state=vase
|
||||||
==
|
==
|
||||||
|
::
|
||||||
|
+$ base-state-3
|
||||||
|
$: prev-version=@ud
|
||||||
|
prev-min-version=@ud
|
||||||
|
base-state-2
|
||||||
|
==
|
||||||
::
|
::
|
||||||
+$ state-0 [%0 base-state-0]
|
+$ state-0 [%0 base-state-0]
|
||||||
::
|
::
|
||||||
@ -100,11 +105,14 @@
|
|||||||
::
|
::
|
||||||
+$ state-3 [%3 base-state-2]
|
+$ state-3 [%3 base-state-2]
|
||||||
::
|
::
|
||||||
|
+$ state-4 [%4 base-state-3]
|
||||||
|
::
|
||||||
+$ versioned-state
|
+$ versioned-state
|
||||||
$% state-0
|
$% state-0
|
||||||
state-1
|
state-1
|
||||||
state-2
|
state-2
|
||||||
state-3
|
state-3
|
||||||
|
state-4
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ default
|
++ default
|
||||||
@ -198,7 +206,7 @@
|
|||||||
++ agent
|
++ agent
|
||||||
|* =config
|
|* =config
|
||||||
|= =(pull-hook config)
|
|= =(pull-hook config)
|
||||||
=| state-3
|
=| state-4
|
||||||
=* state -
|
=* state -
|
||||||
^- agent:gall
|
^- agent:gall
|
||||||
=<
|
=<
|
||||||
@ -224,13 +232,20 @@
|
|||||||
=| cards=(list card:agent:gall)
|
=| cards=(list card:agent:gall)
|
||||||
|^
|
|^
|
||||||
?- -.old
|
?- -.old
|
||||||
%3
|
%4
|
||||||
=^ og-cards pull-hook
|
=^ og-cards pull-hook
|
||||||
(on-load:og inner-state.old)
|
(on-load:og inner-state.old)
|
||||||
=. state old
|
=. state old
|
||||||
|
=/ kick=(list card)
|
||||||
|
?: ?& =(min-version.config prev-min-version.old)
|
||||||
|
=(version.config prev-version.old)
|
||||||
|
==
|
||||||
|
~
|
||||||
|
(poke-self:pass kick+!>(%kick))^~
|
||||||
:_ this
|
:_ this
|
||||||
:(weld cards og-cards (poke-self:pass kick+!>(%kick))^~)
|
:(weld cards og-cards kick)
|
||||||
::
|
::
|
||||||
|
%3 $(old [%4 0 0 +.old])
|
||||||
%2 $(old (state-to-3 old))
|
%2 $(old (state-to-3 old))
|
||||||
%1 $(old [%2 +.old ~])
|
%1 $(old [%2 +.old ~])
|
||||||
%0 !! :: pre-breach
|
%0 !! :: pre-breach
|
||||||
@ -255,8 +270,10 @@
|
|||||||
::
|
::
|
||||||
++ on-save
|
++ on-save
|
||||||
^- vase
|
^- vase
|
||||||
=. inner-state
|
=: inner-state on-save:og
|
||||||
on-save:og
|
prev-min-version min-version.config
|
||||||
|
prev-version version.config
|
||||||
|
==
|
||||||
!>(state)
|
!>(state)
|
||||||
::
|
::
|
||||||
++ on-poke
|
++ on-poke
|
||||||
@ -472,6 +489,7 @@
|
|||||||
::
|
::
|
||||||
++ tr-add
|
++ tr-add
|
||||||
|= [s=^ship r=resource]
|
|= [s=^ship r=resource]
|
||||||
|
?< =(s our.bowl)
|
||||||
=: ship s
|
=: ship s
|
||||||
rid r
|
rid r
|
||||||
status [%active ~]
|
status [%active ~]
|
||||||
|
@ -57,13 +57,21 @@
|
|||||||
inner-state=vase
|
inner-state=vase
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
|
+$ base-state-1
|
||||||
|
$: prev-version=@ud
|
||||||
|
prev-min-version=@ud
|
||||||
|
base-state-0
|
||||||
|
==
|
||||||
|
::
|
||||||
+$ state-0 [%0 base-state-0]
|
+$ state-0 [%0 base-state-0]
|
||||||
::
|
::
|
||||||
+$ state-1 [%1 base-state-0]
|
+$ state-1 [%1 base-state-0]
|
||||||
|
+$ state-2 [%2 base-state-1]
|
||||||
::
|
::
|
||||||
+$ versioned-state
|
+$ versioned-state
|
||||||
$% state-0
|
$% state-0
|
||||||
state-1
|
state-1
|
||||||
|
state-2
|
||||||
==
|
==
|
||||||
++ push-hook
|
++ push-hook
|
||||||
|* =config
|
|* =config
|
||||||
@ -153,7 +161,7 @@
|
|||||||
++ agent
|
++ agent
|
||||||
|* =config
|
|* =config
|
||||||
|= =(push-hook config)
|
|= =(push-hook config)
|
||||||
=| state-1
|
=| state-2
|
||||||
=* state -
|
=* state -
|
||||||
^- agent:gall
|
^- agent:gall
|
||||||
=<
|
=<
|
||||||
@ -179,16 +187,21 @@
|
|||||||
=| cards=(list card:agent:gall)
|
=| cards=(list card:agent:gall)
|
||||||
|^
|
|^
|
||||||
?- -.old
|
?- -.old
|
||||||
%1
|
%2
|
||||||
=^ og-cards push-hook
|
=^ og-cards push-hook
|
||||||
(on-load:og inner-state.old)
|
(on-load:og inner-state.old)
|
||||||
=/ old-subs
|
=/ old-subs
|
||||||
find-old-subs
|
(find-old-subs [prev-version prev-min-version]:old)
|
||||||
=/ version-cards
|
=/ version-cards
|
||||||
:- (fact:io version+!>(version.config) /version ~)
|
:- (fact:io version+!>(version.config) /version ~)
|
||||||
?~ old-subs ~
|
?~ old-subs ~
|
||||||
(kick:io old-subs)^~
|
(kick:io old-subs)^~
|
||||||
[:(weld cards og-cards version-cards) this(state old)]
|
[:(weld cards og-cards version-cards) this(state old)]
|
||||||
|
::
|
||||||
|
%1
|
||||||
|
%_ $
|
||||||
|
old [%2 0 0 +.old]
|
||||||
|
==
|
||||||
::
|
::
|
||||||
::
|
::
|
||||||
%0
|
%0
|
||||||
@ -205,6 +218,12 @@
|
|||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ find-old-subs
|
++ find-old-subs
|
||||||
|
|= [prev-min-version=@ud prev-version=@ud]
|
||||||
|
?: ?& =(min-version.config prev-min-version)
|
||||||
|
=(prev-version version.config)
|
||||||
|
==
|
||||||
|
:: bail on kick if we didn't change versions
|
||||||
|
~
|
||||||
%~ tap in
|
%~ tap in
|
||||||
%+ roll
|
%+ roll
|
||||||
~(val by sup.bowl)
|
~(val by sup.bowl)
|
||||||
@ -230,8 +249,10 @@
|
|||||||
--
|
--
|
||||||
::
|
::
|
||||||
++ on-save
|
++ on-save
|
||||||
=. inner-state
|
=: prev-version version.config
|
||||||
on-save:og
|
prev-min-version min-version.config
|
||||||
|
inner-state on-save:og
|
||||||
|
==
|
||||||
!>(state)
|
!>(state)
|
||||||
::
|
::
|
||||||
++ on-poke
|
++ on-poke
|
||||||
|
@ -25,12 +25,13 @@ 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.
|
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
|
### 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.
|
||||||
|
|
||||||
You should be able to use port mapping for most purposes but you can force Ames to use a custom 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.
|
||||||
`--port=$AMES_PORT` can be passed as an argument to the `docker start` command. Passing `--port=13436` for example, would use port 13436.
|
|
||||||
|
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
|
### Examples
|
||||||
Creating a volume for ~sampel=palnet:
|
Creating a volume for ~sampel=palnet:
|
||||||
@ -38,23 +39,23 @@ Creating a volume for ~sampel=palnet:
|
|||||||
docker volume create sampel-palnet
|
docker volume create sampel-palnet
|
||||||
```
|
```
|
||||||
|
|
||||||
Copying key to sampel-palnet's volume (assumes default docker location)
|
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
|
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 host port 27000:
|
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 27000:34343/udp --name sampel-palnet \
|
docker run -d -p 8080:80 -p 34343:34343/udp --name sampel-palnet \
|
||||||
--mount type=volume,source=sampel-palnet,destination=/urbit \
|
--mount type=volume,source=sampel-palnet,destination=/urbit \
|
||||||
tloncorp/urbit
|
tloncorp/urbit
|
||||||
```
|
```
|
||||||
|
|
||||||
Using host port 8088 with Ames talking on host port 23232 while forcing Ames to start internally on port 13436:
|
Using host port 8088 with Ames talking on host port 23232:
|
||||||
```
|
```
|
||||||
docker run -d -p 8088:80 -p 23232:13436/udp --name sampel-palnet \
|
docker run -d -p 8088:80 -p 23232:23232/udp --name sampel-palnet \
|
||||||
--mount type=volume,source=sampel-palnet,destination=/urbit \
|
--mount type=volume,source=sampel-palnet,destination=/urbit \
|
||||||
tloncorp/urbit --port=13436
|
tloncorp/urbit /bin/start-urbit --port=23232
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting and resetting the Landscape +code
|
### Getting and resetting the Landscape +code
|
||||||
|
@ -25,10 +25,10 @@ module Urbit.Arvo.Common
|
|||||||
import Urbit.Prelude
|
import Urbit.Prelude
|
||||||
|
|
||||||
import Control.Monad.Fail (fail)
|
import Control.Monad.Fail (fail)
|
||||||
import Data.Bits
|
|
||||||
import Data.Serialize
|
import Data.Serialize
|
||||||
|
|
||||||
import qualified Network.HTTP.Types.Method as H
|
import qualified Network.HTTP.Types.Method as H
|
||||||
|
import qualified Network.Socket as N
|
||||||
import qualified Urbit.Ob as Ob
|
import qualified Urbit.Ob as Ob
|
||||||
|
|
||||||
|
|
||||||
@ -159,6 +159,19 @@ deriveNoun ''JsonNode
|
|||||||
|
|
||||||
-- Ames Destinations -------------------------------------------------
|
-- Ames Destinations -------------------------------------------------
|
||||||
|
|
||||||
|
serializeToNoun :: Serialize a => a -> Noun
|
||||||
|
serializeToNoun = A . bytesAtom . encode
|
||||||
|
|
||||||
|
serializeParseNoun :: Serialize a => String -> Int -> Noun -> Parser a
|
||||||
|
serializeParseNoun desc len = named (pack desc) . \case
|
||||||
|
A (atomBytes -> bs)
|
||||||
|
-- Atoms lose leading 0s, but since lsb, these become trailing NULs
|
||||||
|
| length bs <= len -> case decode $ bs <> replicate (len - length bs) 0 of
|
||||||
|
Right aa -> pure aa
|
||||||
|
Left msg -> fail msg
|
||||||
|
| otherwise -> fail ("putative " <> desc <> " " <> show bs <> " too long")
|
||||||
|
C{} -> fail ("unexpected cell in " <> desc)
|
||||||
|
|
||||||
newtype Patp a = Patp { unPatp :: a }
|
newtype Patp a = Patp { unPatp :: a }
|
||||||
deriving newtype (Eq, Ord, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
deriving newtype (Eq, Ord, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
||||||
|
|
||||||
@ -167,17 +180,29 @@ newtype Port = Port { unPort :: Word16 }
|
|||||||
deriving newtype (Eq, Ord, Show, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
deriving newtype (Eq, Ord, Show, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
||||||
|
|
||||||
-- @if
|
-- @if
|
||||||
newtype Ipv4 = Ipv4 { unIpv4 :: Word32 }
|
newtype Ipv4 = Ipv4 { unIpv4 :: N.HostAddress }
|
||||||
deriving newtype (Eq, Ord, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
deriving newtype (Eq, Ord, Enum)
|
||||||
|
|
||||||
|
instance Serialize Ipv4 where
|
||||||
|
get = (\a b c d -> Ipv4 $ N.tupleToHostAddress $ (d, c, b, a))
|
||||||
|
<$> getWord8 <*> getWord8 <*> getWord8 <*> getWord8
|
||||||
|
put (Ipv4 (N.hostAddressToTuple -> (a, b, c, d))) = for_ [d, c, b, a] putWord8
|
||||||
|
|
||||||
|
instance ToNoun Ipv4 where
|
||||||
|
toNoun = serializeToNoun
|
||||||
|
|
||||||
|
instance FromNoun Ipv4 where
|
||||||
|
parseNoun = serializeParseNoun "Ipv4" 4
|
||||||
|
|
||||||
instance Show Ipv4 where
|
instance Show Ipv4 where
|
||||||
show (Ipv4 i) =
|
show (Ipv4 (N.hostAddressToTuple -> (a, b, c, d))) =
|
||||||
show ((shiftR i 24) .&. 0xff) ++ "." ++
|
show a ++ "." ++
|
||||||
show ((shiftR i 16) .&. 0xff) ++ "." ++
|
show b ++ "." ++
|
||||||
show ((shiftR i 8) .&. 0xff) ++ "." ++
|
show c ++ "." ++
|
||||||
show (i .&. 0xff)
|
show d
|
||||||
|
|
||||||
-- @is
|
-- @is
|
||||||
|
-- should probably use hostAddress6ToTuple here, but no one uses it right now
|
||||||
newtype Ipv6 = Ipv6 { unIpv6 :: Word128 }
|
newtype Ipv6 = Ipv6 { unIpv6 :: Word128 }
|
||||||
deriving newtype (Eq, Ord, Show, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
deriving newtype (Eq, Ord, Show, Enum, Real, Integral, Num, ToNoun, FromNoun)
|
||||||
|
|
||||||
@ -190,21 +215,14 @@ data AmesAddress = AAIpv4 Ipv4 Port
|
|||||||
deriving (Eq, Ord, Show)
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
instance Serialize AmesAddress where
|
instance Serialize AmesAddress where
|
||||||
get = AAIpv4 <$> (Ipv4 <$> getWord32le) <*> (Port <$> getWord16le)
|
get = AAIpv4 <$> get <*> (Port <$> getWord16le)
|
||||||
put (AAIpv4 (Ipv4 ip) (Port port)) = putWord32le ip >> putWord16le port
|
put (AAIpv4 ip (Port port)) = put ip >> putWord16le port
|
||||||
|
|
||||||
instance FromNoun AmesAddress where
|
instance FromNoun AmesAddress where
|
||||||
parseNoun = named "AmesAddress" . \case
|
parseNoun = serializeParseNoun "AmesAddress" 6
|
||||||
A (atomBytes -> bs)
|
|
||||||
-- Atoms lose leading 0s, but since lsb, these become trailing NULs
|
|
||||||
| length bs <= 6 -> case decode $ bs <> replicate (6 - length bs) 0 of
|
|
||||||
Right aa -> pure aa
|
|
||||||
Left msg -> fail msg
|
|
||||||
| otherwise -> fail ("putative address " <> show bs <> " too long")
|
|
||||||
C{} -> fail "unexpected cell in ames address"
|
|
||||||
|
|
||||||
instance ToNoun AmesAddress where
|
instance ToNoun AmesAddress where
|
||||||
toNoun = A . bytesAtom . encode
|
toNoun = serializeToNoun
|
||||||
|
|
||||||
type AmesDest = Each Galaxy AmesAddress
|
type AmesDest = Each Galaxy AmesAddress
|
||||||
|
|
||||||
|
@ -80,10 +80,6 @@ data ShipClass
|
|||||||
muk :: ByteString -> Word20
|
muk :: ByteString -> Word20
|
||||||
muk bs = mugBS bs .&. (2 ^ 20 - 1)
|
muk bs = mugBS bs .&. (2 ^ 20 - 1)
|
||||||
|
|
||||||
-- XX check this
|
|
||||||
getAmesAddress :: Get AmesAddress
|
|
||||||
getAmesAddress = AAIpv4 <$> (Ipv4 <$> getWord32le) <*> (Port <$> getWord16le)
|
|
||||||
|
|
||||||
putAmesAddress :: Putter AmesAddress
|
putAmesAddress :: Putter AmesAddress
|
||||||
putAmesAddress = \case
|
putAmesAddress = \case
|
||||||
AAIpv4 (Ipv4 ip) (Port port) -> putWord32le ip >> putWord16le port
|
AAIpv4 (Ipv4 ip) (Port port) -> putWord32le ip >> putWord16le port
|
||||||
@ -104,7 +100,7 @@ instance Serialize Packet where
|
|||||||
guard isAmes
|
guard isAmes
|
||||||
|
|
||||||
pktOrigin <- if isRelayed
|
pktOrigin <- if isRelayed
|
||||||
then Just <$> getAmesAddress
|
then Just <$> get
|
||||||
else pure Nothing
|
else pure Nothing
|
||||||
|
|
||||||
-- body
|
-- body
|
||||||
@ -157,9 +153,10 @@ instance Serialize Packet where
|
|||||||
|
|
||||||
putWord32le head
|
putWord32le head
|
||||||
case pktOrigin of
|
case pktOrigin of
|
||||||
Just o -> putAmesAddress o
|
Just o -> put o
|
||||||
Nothing -> pure ()
|
Nothing -> pure ()
|
||||||
putByteString body
|
putByteString body
|
||||||
|
|
||||||
where
|
where
|
||||||
putShipGetRank s@(Ship (LargeKey p q)) = case () of
|
putShipGetRank s@(Ship (LargeKey p q)) = case () of
|
||||||
_ | s < 2 ^ 16 -> (0, putWord16le $ fromIntegral s) -- lord
|
_ | s < 2 ^ 16 -> (0, putWord16le $ fromIntegral s) -- lord
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
1. Opens a UDP socket and makes sure that it stays open.
|
1. Opens a UDP socket and makes sure that it stays open.
|
||||||
|
|
||||||
- If can't open the port, wait and try again repeatedly.
|
- If can't open the port, wait and try again repeatedly.
|
||||||
- If there is an error reading or writting from the open socket,
|
- If there is an error reading to or writing from the open socket,
|
||||||
close it and open another.
|
close it and open another, making sure, however, to reuse the
|
||||||
|
same port
|
||||||
|
NOTE: It's not clear what, if anything, closing and reopening
|
||||||
|
the socket does. We're keeping this behavior out of conservatism
|
||||||
|
until we understand it better.
|
||||||
|
|
||||||
2. Receives packets from the socket.
|
2. Receives packets from the socket.
|
||||||
|
|
||||||
@ -158,7 +162,7 @@ realUdpServ
|
|||||||
-> HostAddress
|
-> HostAddress
|
||||||
-> AmesStat
|
-> AmesStat
|
||||||
-> RIO e UdpServ
|
-> RIO e UdpServ
|
||||||
realUdpServ por hos sat = do
|
realUdpServ startPort hos sat = do
|
||||||
logInfo $ displayShow ("AMES", "UDP", "Starting real UDP server.")
|
logInfo $ displayShow ("AMES", "UDP", "Starting real UDP server.")
|
||||||
|
|
||||||
env <- ask
|
env <- ask
|
||||||
@ -202,23 +206,30 @@ realUdpServ por hos sat = do
|
|||||||
did <- atomically (tryWriteTBQueue qSend (a, b))
|
did <- atomically (tryWriteTBQueue qSend (a, b))
|
||||||
when (did == False) $ do
|
when (did == False) $ do
|
||||||
logWarn "AMES: UDP: Dropping outbound packet because queue is full."
|
logWarn "AMES: UDP: Dropping outbound packet because queue is full."
|
||||||
|
let opener por = do
|
||||||
|
logInfo $ displayShow $ ("AMES", "UDP", "Trying to open socket, port",)
|
||||||
|
por
|
||||||
|
sk <- forceBind por hos
|
||||||
|
sn <- io $ getSocketName sk
|
||||||
|
sp <- io $ socketPort sk
|
||||||
|
logInfo $ displayShow $ ("AMES", "UDP", "Got socket", sn, sp)
|
||||||
|
|
||||||
tOpen <- async $ forever $ do
|
let waitForRelease = do
|
||||||
sk <- forceBind por hos
|
atomically (writeTVar vSock (Just sk))
|
||||||
sn <- io $ getSocketName sk
|
broken <- atomically (takeTMVar vFail)
|
||||||
|
logWarn "AMES: UDP: Closing broken socket."
|
||||||
|
io (close broken)
|
||||||
|
|
||||||
let waitForRelease = do
|
case sn of
|
||||||
atomically (writeTVar vSock (Just sk))
|
(SockAddrInet boundPort _) ->
|
||||||
broken <- atomically (takeTMVar vFail)
|
-- When we're on IPv4, maybe port forward at the NAT.
|
||||||
logWarn "AMES: UDP: Closing broken socket."
|
rwith (requestPortAccess $ fromIntegral boundPort) $
|
||||||
io (close broken)
|
\() -> waitForRelease
|
||||||
|
_ -> waitForRelease
|
||||||
|
|
||||||
case sn of
|
opener sp
|
||||||
(SockAddrInet boundPort _) ->
|
|
||||||
-- When we're on IPv4, maybe port forward at the NAT.
|
tOpen <- async $ opener startPort
|
||||||
rwith (requestPortAccess $ fromIntegral boundPort) $
|
|
||||||
\() -> waitForRelease
|
|
||||||
_ -> waitForRelease
|
|
||||||
|
|
||||||
tSend <- async $ forever $ join $ atomically $ do
|
tSend <- async $ forever $ join $ atomically $ do
|
||||||
(adr, byt) <- readTBQueue qSend
|
(adr, byt) <- readTBQueue qSend
|
||||||
|
@ -37,12 +37,12 @@ textPlain = Path [(MkKnot "text"), (MkKnot "plain")]
|
|||||||
|
|
||||||
-- | Filter for dotfiles, tempfiles and backup files.
|
-- | Filter for dotfiles, tempfiles and backup files.
|
||||||
validClaySyncPath :: FilePath -> Bool
|
validClaySyncPath :: FilePath -> Bool
|
||||||
validClaySyncPath fp = hasPeriod && notTildeFile && notDotHash && notDoubleHash
|
validClaySyncPath fp = hasPeriod && notTildeFile && notDotFile && notDoubleHash
|
||||||
where
|
where
|
||||||
fileName = takeFileName fp
|
fileName = takeFileName fp
|
||||||
hasPeriod = elem '.' fileName
|
hasPeriod = elem '.' fileName
|
||||||
notTildeFile = not $ "~" `isSuffixOf` fileName
|
notTildeFile = not $ "~" `isSuffixOf` fileName
|
||||||
notDotHash = not $ ".#" `isPrefixOf` fileName
|
notDotFile = not $ "." `isPrefixOf` fileName
|
||||||
notDoubleHash =
|
notDoubleHash =
|
||||||
not $ ("#" `isPrefixOf` fileName) && ("#" `isSuffixOf` fileName)
|
not $ ("#" `isPrefixOf` fileName) && ("#" `isSuffixOf` fileName)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: urbit-king
|
name: urbit-king
|
||||||
version: 1.3
|
version: 1.5
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
data-files:
|
data-files:
|
||||||
|
@ -111,7 +111,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
|
@ -30,7 +30,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
|
3890
pkg/interface/package-lock.json
generated
3890
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,8 +9,8 @@
|
|||||||
"@reach/menu-button": "^0.10.5",
|
"@reach/menu-button": "^0.10.5",
|
||||||
"@reach/tabs": "^0.10.5",
|
"@reach/tabs": "^0.10.5",
|
||||||
"@tlon/indigo-dark": "^1.0.6",
|
"@tlon/indigo-dark": "^1.0.6",
|
||||||
"@tlon/indigo-light": "^1.0.6",
|
"@tlon/indigo-light": "^1.0.7",
|
||||||
"@tlon/indigo-react": "^1.2.19",
|
"@tlon/indigo-react": "^1.2.21",
|
||||||
"@tlon/sigil-js": "^1.4.3",
|
"@tlon/sigil-js": "^1.4.3",
|
||||||
"@urbit/api": "file:../npm/api",
|
"@urbit/api": "file:../npm/api",
|
||||||
"any-ascii": "^0.1.7",
|
"any-ascii": "^0.1.7",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"react-hot-loader": "^4.13.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"sass": "^1.32.5",
|
"sass": "^1.32.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^4.2.4",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-dev-server": "^3.11.2"
|
||||||
|
@ -2,6 +2,7 @@ import BaseApi from './base';
|
|||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Patp } from '@urbit/api';
|
import { Patp } from '@urbit/api';
|
||||||
import { ContactEdit } from '@urbit/api/contacts';
|
import { ContactEdit } from '@urbit/api/contacts';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export default class ContactsApi extends BaseApi<StoreState> {
|
export default class ContactsApi extends BaseApi<StoreState> {
|
||||||
add(ship: Patp, contact: any) {
|
add(ship: Patp, contact: any) {
|
||||||
@ -73,6 +74,28 @@ export default class ContactsApi extends BaseApi<StoreState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async disallowedShipsForOurContact(ships: string[]): Promise<string[]> {
|
||||||
|
return _.compact(
|
||||||
|
await Promise.all(
|
||||||
|
ships.map(
|
||||||
|
async s => {
|
||||||
|
const ship = `~${s}`;
|
||||||
|
if(s === window.ship) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const allowed = await this.fetchIsAllowed(
|
||||||
|
`~${window.ship}`,
|
||||||
|
'personal',
|
||||||
|
ship,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return allowed ? null : ship;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
retrieve(ship: string) {
|
retrieve(ship: string) {
|
||||||
const resource = { ship, name: '' };
|
const resource = { ship, name: '' };
|
||||||
return this.action('contact-pull-hook', 'pull-hook-action', {
|
return this.action('contact-pull-hook', 'pull-hook-action', {
|
||||||
|
@ -1,234 +0,0 @@
|
|||||||
import bigInt, { BigInteger } from 'big-integer';
|
|
||||||
import { immerable } from 'immer';
|
|
||||||
|
|
||||||
interface NonemptyNode<V> {
|
|
||||||
n: [BigInteger, V];
|
|
||||||
l: MapNode<V>;
|
|
||||||
r: MapNode<V>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MapNode<V> = NonemptyNode<V> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of ordered maps for JS
|
|
||||||
* Plagiarised wholesale from sys/zuse
|
|
||||||
*/
|
|
||||||
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|
||||||
private root: MapNode<V> = null;
|
|
||||||
[immerable] = true;
|
|
||||||
size = 0;
|
|
||||||
|
|
||||||
constructor(initial: [BigInteger, V][] = []) {
|
|
||||||
initial.forEach(([key, val]) => {
|
|
||||||
this.set(key, val);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an value for a key
|
|
||||||
*/
|
|
||||||
get(key: BigInteger): V | null {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
const [k, v] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
} else {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Put an item by a key
|
|
||||||
*/
|
|
||||||
set(key: BigInteger, value: V): void {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return {
|
|
||||||
n: [key, value],
|
|
||||||
l: null,
|
|
||||||
r: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
this.size--;
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
n: [k, value]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const l = inner(node.l);
|
|
||||||
if (!l) {
|
|
||||||
throw new Error('invariant violation');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
l
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const r = inner(node.r);
|
|
||||||
if (!r) {
|
|
||||||
throw new Error('invariant violation');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...node, r };
|
|
||||||
};
|
|
||||||
this.size++;
|
|
||||||
this.root = inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all entries
|
|
||||||
*/
|
|
||||||
clear() {
|
|
||||||
this.root = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Predicate testing if map contains key
|
|
||||||
*/
|
|
||||||
has(key: BigInteger): boolean {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return inner(node.r);
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove value associated with key, returning whether that key
|
|
||||||
* existed in the first place
|
|
||||||
*/
|
|
||||||
delete(key: BigInteger) {
|
|
||||||
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
|
|
||||||
if (!node) {
|
|
||||||
return [false, null];
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return [true, this.nip(node)];
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const [bool, l] = inner(node.l);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
l
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [bool, r] = inner(node.r);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
r
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
const [ret, newRoot] = inner(this.root);
|
|
||||||
if(ret) {
|
|
||||||
this.size--;
|
|
||||||
}
|
|
||||||
this.root = newRoot;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private nip(nod: NonemptyNode<V>): MapNode<V> {
|
|
||||||
const inner = (node: NonemptyNode<V>) => {
|
|
||||||
if (!node.l) {
|
|
||||||
return node.r;
|
|
||||||
}
|
|
||||||
if (!node.r) {
|
|
||||||
return node.l;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node.l,
|
|
||||||
r: inner(node.r)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return inner(nod);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekLargest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.l) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekSmallest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.r) {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
keys(): BigInteger[] {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.map(([key]) => key);
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(f: (value: V, key: BigInteger) => void) {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.forEach(([k,v]) => f(v,k));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
|
||||||
const result: [BigInteger, V][] = [];
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inner(node.l);
|
|
||||||
result.push(node.n);
|
|
||||||
inner(node.r);
|
|
||||||
};
|
|
||||||
inner(this.root);
|
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: this[Symbol.iterator],
|
|
||||||
next: (): IteratorResult<[BigInteger, V]> => {
|
|
||||||
if (idx < result.length) {
|
|
||||||
return { value: result[idx++], done: false };
|
|
||||||
}
|
|
||||||
return { done: true, value: null };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -99,9 +99,11 @@ export default function index(contacts, associations, apps, currentGroup, groups
|
|||||||
Object.keys(associations).filter((e) => {
|
Object.keys(associations).filter((e) => {
|
||||||
// skip apps with no metadata
|
// skip apps with no metadata
|
||||||
return Object.keys(associations[e]).length > 0;
|
return Object.keys(associations[e]).length > 0;
|
||||||
}).map((e) => {
|
}).map((e) => {
|
||||||
// iterate through each app's metadata object
|
// iterate through each app's metadata object
|
||||||
Object.keys(associations[e]).map((association) => {
|
Object.keys(associations[e])
|
||||||
|
.filter((association) => !associations?.[e]?.[association]?.metadata?.hidden)
|
||||||
|
.map((association) => {
|
||||||
const each = associations[e][association];
|
const each = associations[e][association];
|
||||||
let title = each.resource;
|
let title = each.resource;
|
||||||
if (each.metadata.title !== '') {
|
if (each.metadata.title !== '') {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api';
|
import { Post, GraphNode, TextContent } from '@urbit/api';
|
||||||
import { buntPost } from '~/logic/lib/post';
|
import { buntPost } from '~/logic/lib/post';
|
||||||
import { unixToDa } from '~/logic/lib/util';
|
import { unixToDa } from '~/logic/lib/util';
|
||||||
import { BigIntOrderedMap } from './BigIntOrderedMap';
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
export function newPost(
|
export function newPost(
|
||||||
title: string,
|
title: string,
|
||||||
body: string
|
body: string
|
||||||
): [BigInteger, NodeMap] {
|
): [BigInteger, any] {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const nowDa = unixToDa(now);
|
const nowDa = unixToDa(now);
|
||||||
const root: Post = {
|
const root: Post = {
|
||||||
@ -73,13 +73,16 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
||||||
const revs = node.children.get(bigInt(1));
|
const revs = node.children?.get(bigInt(1));
|
||||||
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
||||||
if(!revs) {
|
if(!revs) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [revNum, rev] = [...revs.children][0];
|
let revNum, rev;
|
||||||
if(!rev) {
|
if (revs?.children !== null) {
|
||||||
|
[revNum, rev] = [...revs.children][0];
|
||||||
|
}
|
||||||
|
if (!rev) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [title, body] = rev.post.contents as TextContent[];
|
const [title, body] = rev.post.contents as TextContent[];
|
||||||
@ -88,18 +91,22 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos
|
|||||||
|
|
||||||
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
||||||
const empty = [1, buntPost()] as [number, Post];
|
const empty = [1, buntPost()] as [number, Post];
|
||||||
if (node.children.size <= 0) {
|
const childSize = node?.children?.size ?? 0;
|
||||||
|
if (childSize <= 0) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [revNum, rev] = [...node.children][0];
|
let revNum, rev;
|
||||||
if(!rev) {
|
if (node?.children !== null) {
|
||||||
|
[revNum, rev] = [...node.children][0];
|
||||||
|
}
|
||||||
|
if (!rev) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
return [revNum.toJSNumber(), rev.post];
|
return [revNum.toJSNumber(), rev.post];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComments(node: GraphNode): GraphNode {
|
export function getComments(node: GraphNode): GraphNode {
|
||||||
const comments = node.children.get(bigInt(2));
|
const comments = node.children?.get(bigInt(2));
|
||||||
if(!comments) {
|
if(!comments) {
|
||||||
return { post: buntPost(), children: new BigIntOrderedMap() };
|
return { post: buntPost(), children: new BigIntOrderedMap() };
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TutorialProgress, Associations } from '@urbit/api';
|
import { Associations } from '@urbit/api';
|
||||||
|
import { TutorialProgress } from '~/types';
|
||||||
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
||||||
import { Direction } from '~/views/components/Triangle';
|
import { Direction } from '~/views/components/Triangle';
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ interface StepDetail {
|
|||||||
alignY: AlignY | AlignY[];
|
alignY: AlignY | AlignY[];
|
||||||
offsetX: number;
|
offsetX: number;
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
arrow: Direction;
|
arrow?: Direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTutorialGroup(props: { associations: Associations }) {
|
export function hasTutorialGroup(props: { associations: Associations }) {
|
||||||
|
@ -15,7 +15,8 @@ function retrieve<T>(key: string, initial: T): T {
|
|||||||
interface SetStateFunc<T> {
|
interface SetStateFunc<T> {
|
||||||
(t: T): T;
|
(t: T): T;
|
||||||
}
|
}
|
||||||
type SetState<T> = T | SetStateFunc<T>;
|
// See microsoft/typescript#37663 for filed bug
|
||||||
|
type SetState<T> = T extends any ? SetStateFunc<T> : never;
|
||||||
export function useLocalStorageState<T>(key: string, initial: T) {
|
export function useLocalStorageState<T>(key: string, initial: T) {
|
||||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||||
|
|
||||||
|
@ -2,18 +2,14 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
SyntheticEvent,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useEffect,
|
|
||||||
useRef
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
import { useOutsideClick } from './useOutsideClick';
|
|
||||||
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||||
import { Portal } from '~/views/components/Portal';
|
import { Portal } from '~/views/components/Portal';
|
||||||
import { ModalPortal } from '~/views/components/ModalPortal';
|
import { PropFunc } from '~/types';
|
||||||
import { PropFunc } from '@urbit/api';
|
|
||||||
|
|
||||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||||
interface UseModalProps {
|
interface UseModalProps {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Primitive } from '@urbit/api';
|
import { Primitive } from '~/types';
|
||||||
|
|
||||||
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
||||||
const prev = useRef<T | null>(null);
|
const prev = useRef<T | null>(null);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useWaitForProps } from "./useWaitForProps";
|
|
||||||
import {unstable_batchedUpdates} from "react-dom";
|
import {unstable_batchedUpdates} from "react-dom";
|
||||||
|
|
||||||
export type IOInstance<I, P, O> = (
|
export type IOInstance<I, P, O> = (
|
||||||
@ -10,7 +9,7 @@ export function useRunIO<I, O>(
|
|||||||
io: (i: I) => Promise<O>,
|
io: (i: I) => Promise<O>,
|
||||||
after: (o: O) => void,
|
after: (o: O) => void,
|
||||||
key: string
|
key: string
|
||||||
): () => Promise<void> {
|
): (i: I) => Promise<unknown> {
|
||||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||||
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
||||||
const [output, setOutput] = useState<O | null>(null);
|
const [output, setOutput] = useState<O | null>(null);
|
||||||
|
@ -5,7 +5,7 @@ export function useStatelessAsyncClickable(
|
|||||||
onClick: (e: MouseEvent) => Promise<void>,
|
onClick: (e: MouseEvent) => Promise<void>,
|
||||||
name: string
|
name: string
|
||||||
) {
|
) {
|
||||||
const [state, setState] = useState<ButtonState>('waiting');
|
const [state, setState] = useState<AsyncClickableState>('waiting');
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
async (e: MouseEvent) => {
|
async (e: MouseEvent) => {
|
||||||
try {
|
try {
|
||||||
|
@ -16,7 +16,7 @@ export interface IuseStorage {
|
|||||||
upload: (file: File, bucket: string) => Promise<string>;
|
upload: (file: File, bucket: string) => Promise<string>;
|
||||||
uploadDefault: (file: File) => Promise<string>;
|
uploadDefault: (file: File) => Promise<string>;
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
promptUpload: () => Promise<string | undefined>;
|
promptUpload: () => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||||
|
@ -30,7 +30,7 @@ export const getModuleIcon = (mod: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mod === 'post') {
|
if (mod === 'post') {
|
||||||
return 'Spaces';
|
return 'Dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.capitalize(mod);
|
return _.capitalize(mod);
|
||||||
@ -192,7 +192,10 @@ export function uxToHex(ux: string) {
|
|||||||
export const hexToUx = (hex) => {
|
export const hexToUx = (hex) => {
|
||||||
const ux = f.flow(
|
const ux = f.flow(
|
||||||
f.chunk(4),
|
f.chunk(4),
|
||||||
f.map(x => _.dropWhile(x, y => y === 0).join('')),
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
f.map(x => _.dropWhile(x, function(y: unknown) {
|
||||||
|
return y === 0;
|
||||||
|
}).join('')),
|
||||||
f.join('.')
|
f.join('.')
|
||||||
)(hex.split(''));
|
)(hex.split(''));
|
||||||
return `0x${ux}`;
|
return `0x${ux}`;
|
||||||
@ -417,7 +420,7 @@ export const useHovering = (): useHoveringInterface => {
|
|||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
}), [onMouseLeave, onMouseOver]);
|
}), [onMouseLeave, onMouseOver]);
|
||||||
|
|
||||||
|
|
||||||
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
|
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import usePreviousValue from "./usePreviousValue";
|
import usePreviousValue from "./usePreviousValue";
|
||||||
|
import {Primitive} from "~/types";
|
||||||
|
|
||||||
export interface VirtualContextProps {
|
export interface VirtualContextProps {
|
||||||
save: () => void;
|
save: () => void;
|
||||||
@ -42,14 +43,14 @@ export function useVirtualResizeState(s: boolean) {
|
|||||||
[_setState, save]
|
[_setState, save]
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
restore();
|
requestAnimationFrame(restore);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
return [state, setState] as const;
|
return [state, setState] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVirtualResizeProp<T>(prop: T) {
|
export function useVirtualResizeProp(prop: Primitive) {
|
||||||
const { save, restore } = useVirtual();
|
const { save, restore } = useVirtual();
|
||||||
const oldProp = usePreviousValue(prop)
|
const oldProp = usePreviousValue(prop)
|
||||||
|
|
||||||
@ -57,8 +58,8 @@ export function useVirtualResizeProp<T>(prop: T) {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
restore();
|
requestAnimationFrame(restore);
|
||||||
}, [prop]);
|
}, [prop]);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Associations, Workspace } from '@urbit/api';
|
import { Associations } from '@urbit/api';
|
||||||
|
import { Workspace } from '~/types';
|
||||||
|
|
||||||
export function getTitleFromWorkspace(
|
export function getTitleFromWorkspace(
|
||||||
associations: Associations,
|
associations: Associations,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import { StoreState } from '../store/type';
|
||||||
import { StoreState } from '../../store/type';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
|
|
||||||
type LocalState = Pick<StoreState, 'connection'>;
|
type LocalState = Pick<StoreState, 'connection'>;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
|
import produce from 'immer';
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
import useGraphState, { GraphState } from '../state/graph';
|
import useGraphState, { GraphState } from '../state/graph';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
@ -51,23 +52,18 @@ const keys = (json, state: GraphState): GraphState => {
|
|||||||
const processNode = (node) => {
|
const processNode = (node) => {
|
||||||
// is empty
|
// is empty
|
||||||
if (!node.children) {
|
if (!node.children) {
|
||||||
node.children = new BigIntOrderedMap();
|
return produce(node, draft => {
|
||||||
return node;
|
draft.children = new BigIntOrderedMap();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// is graph
|
// is graph
|
||||||
let converted = new BigIntOrderedMap();
|
return produce(node, draft => {
|
||||||
for (let idx in node.children) {
|
draft.children = new BigIntOrderedMap()
|
||||||
let item = node.children[idx];
|
.gas(_.map(draft.children, (item, idx) =>
|
||||||
let index = bigInt(idx);
|
[bigInt(idx), processNode(item)] as [BigInteger, any]
|
||||||
|
));
|
||||||
converted.set(
|
});
|
||||||
index,
|
|
||||||
processNode(item)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
node.children = converted;
|
|
||||||
return node;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -85,17 +81,10 @@ const addGraph = (json, state: GraphState): GraphState => {
|
|||||||
state.graphTimesentMap[resource] = {};
|
state.graphTimesentMap[resource] = {};
|
||||||
|
|
||||||
|
|
||||||
for (let idx in data.graph) {
|
state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map(idx => {
|
||||||
let item = data.graph[idx];
|
return [bigInt(idx), processNode(data.graph[idx])];
|
||||||
let index = bigInt(idx);
|
}));
|
||||||
|
|
||||||
let node = processNode(item);
|
|
||||||
|
|
||||||
state.graphs[resource].set(
|
|
||||||
index,
|
|
||||||
node
|
|
||||||
);
|
|
||||||
}
|
|
||||||
state.graphKeys.add(resource);
|
state.graphKeys.add(resource);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -116,7 +105,7 @@ const removeGraph = (json, state: GraphState): GraphState => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapifyChildren = (children) => {
|
const mapifyChildren = (children) => {
|
||||||
return new BigIntOrderedMap(
|
return new BigIntOrderedMap().gas(
|
||||||
_.map(children, (node, idx) => {
|
_.map(children, (node, idx) => {
|
||||||
idx = idx && idx.startsWith('/') ? idx.slice(1) : idx;
|
idx = idx && idx.startsWith('/') ? idx.slice(1) : idx;
|
||||||
const nd = {...node, children: mapifyChildren(node.children || {}) };
|
const nd = {...node, children: mapifyChildren(node.children || {}) };
|
||||||
@ -128,8 +117,7 @@ const addNodes = (json, state) => {
|
|||||||
const _addNode = (graph, index, node) => {
|
const _addNode = (graph, index, node) => {
|
||||||
// set child of graph
|
// set child of graph
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.set(index[0], node);
|
return graph.set(index[0], node);
|
||||||
return graph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set parent of graph
|
// set parent of graph
|
||||||
@ -138,19 +126,20 @@ const addNodes = (json, state) => {
|
|||||||
console.error('parent node does not exist, cannot add child');
|
console.error('parent node does not exist, cannot add child');
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
parNode.children = _addNode(parNode.children, index.slice(1), node);
|
return graph.set(index[0], produce(parNode, draft => {
|
||||||
graph.set(index[0], parNode);
|
draft.children = _addNode(draft.children, index.slice(1), node);
|
||||||
return graph;
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
return graph.delete(index[0]);
|
||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
child.children = _remove(child.children, index.slice(1));
|
return graph.set(index[0], produce(child, draft => {
|
||||||
graph.set(index[0], child);
|
draft.children = _remove(draft.children, index.slice(1));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,10 +155,9 @@ const addNodes = (json, state) => {
|
|||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
|
|
||||||
graph = _remove(graph, indexArr);
|
|
||||||
delete state.graphTimesentMap[resource][timestamp];
|
delete state.graphTimesentMap[resource][timestamp];
|
||||||
|
return _remove(graph, indexArr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,11 +196,12 @@ const addNodes = (json, state) => {
|
|||||||
return aArr.length - bArr.length;
|
return aArr.length - bArr.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
let graph = state.graphs[resource];
|
|
||||||
|
|
||||||
indices.forEach((index) => {
|
indices.forEach((index) => {
|
||||||
let node = data.nodes[index];
|
let node = data.nodes[index];
|
||||||
graph = _removePending(graph, node.post, resource);
|
const old = state.graphs[resource].size;
|
||||||
|
state.graphs[resource] = _removePending(state.graphs[resource], node.post, resource);
|
||||||
|
const newSize = state.graphs[resource].size;
|
||||||
|
|
||||||
|
|
||||||
if (index.split('/').length === 0) { return; }
|
if (index.split('/').length === 0) { return; }
|
||||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||||
@ -225,17 +214,21 @@ const addNodes = (json, state) => {
|
|||||||
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children = mapifyChildren(node?.children || {});
|
|
||||||
|
|
||||||
graph = _addNode(
|
state.graphs[resource] = _addNode(
|
||||||
graph,
|
state.graphs[resource],
|
||||||
indexArr,
|
indexArr,
|
||||||
node
|
produce(node, draft => {
|
||||||
|
draft.children = mapifyChildren(draft?.children || {});
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
if(newSize !== old) {
|
||||||
|
console.log(`${resource}, (${old}, ${newSize}, ${state.graphs[resource].size})`);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state.graphs[resource] = graph;
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
@ -243,13 +236,15 @@ const addNodes = (json, state) => {
|
|||||||
const removeNodes = (json, state: GraphState): GraphState => {
|
const removeNodes = (json, state: GraphState): GraphState => {
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
return graph.delete(index[0]);
|
||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
_remove(child.children, index.slice(1));
|
return graph.set(index[0], produce(draft => {
|
||||||
graph.set(index[0], child);
|
draft.children = _remove(draft.children, index.slice(1))
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
return graph;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -264,7 +259,7 @@ const removeNodes = (json, state: GraphState): GraphState => {
|
|||||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
_remove(state.graphs[res], indexArr);
|
state.graphs[res] = _remove(state.graphs[res], indexArr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
@ -5,16 +5,14 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Tags,
|
Tags,
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
GroupPolicyDiff,
|
|
||||||
OpenPolicyDiff,
|
OpenPolicyDiff,
|
||||||
OpenPolicy,
|
OpenPolicy,
|
||||||
InvitePolicyDiff,
|
InvitePolicyDiff,
|
||||||
InvitePolicy
|
InvitePolicy
|
||||||
} from '@urbit/api/groups';
|
} from '@urbit/api/groups';
|
||||||
import { Enc, PatpNoSig } from '@urbit/api';
|
import { Enc } from '@urbit/api';
|
||||||
import { resourceAsPath } from '../lib/util';
|
import { resourceAsPath } from '../lib/util';
|
||||||
import useGroupState, { GroupState } from '../state/group';
|
import useGroupState, { GroupState } from '../state/group';
|
||||||
import { compose } from 'lodash/fp';
|
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
function decodeGroup(group: Enc<Group>): Group {
|
function decodeGroup(group: Enc<Group>): Group {
|
||||||
@ -125,9 +123,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
state.groups[resourcePath].members.add(member);
|
state.groups[resourcePath].members.add(member);
|
||||||
if (
|
if (
|
||||||
'invite' in state.groups[resourcePath].policy &&
|
'invite' in state.groups[resourcePath].policy &&
|
||||||
state.groups[resourcePath].policy.invite.pending.has(member)
|
state.groups[resourcePath].policy['invite'].pending.has(member)
|
||||||
) {
|
) {
|
||||||
state.groups[resourcePath].policy.invite.pending.delete(member)
|
state.groups[resourcePath].policy['invite'].pending.delete(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +157,7 @@ const addTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
_.set(tags, tagAccessors, tagged);
|
_.set(tags, tagAccessors, tagged);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('removeTag' in json) {
|
if ('removeTag' in json) {
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Notifications,
|
|
||||||
NotifIndex,
|
NotifIndex,
|
||||||
NotificationGraphConfig,
|
|
||||||
GroupNotificationsConfig,
|
|
||||||
UnreadStats,
|
|
||||||
Timebox
|
Timebox
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
import { makePatDa } from '~/logic/lib/util';
|
import { makePatDa } from '~/logic/lib/util';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||||
import useHarkState, { HarkState } from '../state/hark';
|
import useHarkState, { HarkState } from '../state/hark';
|
||||||
import { compose } from 'lodash/fp';
|
import { compose } from 'lodash/fp';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
import bigInt, {BigInteger} from 'big-integer';
|
import {BigInteger} from 'big-integer';
|
||||||
|
|
||||||
export const HarkReducer = (json: any) => {
|
export const HarkReducer = (json: any) => {
|
||||||
const data = _.get(json, 'harkUpdate', false);
|
const data = _.get(json, 'harkUpdate', false);
|
||||||
@ -151,7 +147,7 @@ function graphWatchSelf(json: any, state: HarkState): HarkState {
|
|||||||
|
|
||||||
function readAll(json: any, state: HarkState): HarkState {
|
function readAll(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'read-all');
|
const data = _.get(json, 'read-all');
|
||||||
if(data) {
|
if(data) {
|
||||||
clearState(state);
|
clearState(state);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -264,7 +260,7 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
|
|||||||
if(!('graph' in index)) {
|
if(!('graph' in index)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
let unreads: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||||
f(unreads);
|
f(unreads);
|
||||||
|
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||||
@ -278,7 +274,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
|||||||
_.set(state.unreads.graph, path,
|
_.set(state.unreads.graph, path,
|
||||||
[
|
[
|
||||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
{ time, index}
|
{ time, index }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else if ('group' in index) {
|
} else if ('group' in index) {
|
||||||
@ -287,7 +283,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
|||||||
_.set(state.unreads.group, path,
|
_.set(state.unreads.group, path,
|
||||||
[
|
[
|
||||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
{ time, index}
|
{ time, index }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -312,10 +308,10 @@ function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time:
|
|||||||
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
||||||
|
|
||||||
if('graph' in index) {
|
if('graph' in index) {
|
||||||
const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
||||||
} else if('group' in index) {
|
} else if('group' in index) {
|
||||||
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
const curr: any = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||||
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,9 +329,9 @@ function added(json: any, state: HarkState): HarkState {
|
|||||||
);
|
);
|
||||||
if (arrIdx !== -1) {
|
if (arrIdx !== -1) {
|
||||||
timebox[arrIdx] = { index, notification };
|
timebox[arrIdx] = { index, notification };
|
||||||
state.notifications.set(time, timebox);
|
state.notifications = state.notifications.set(time, timebox);
|
||||||
} else {
|
} else {
|
||||||
state.notifications.set(time, [...timebox, { index, notification }]);
|
state.notifications = state.notifications.set(time, [...timebox, { index, notification }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -354,7 +350,7 @@ const timebox = (json: any, state: HarkState): HarkState => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
if (!data.archive) {
|
if (!data.archive) {
|
||||||
state.notifications.set(time, data.notifications);
|
state.notifications = state.notifications.set(time, data.notifications);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -407,7 +403,7 @@ function setRead(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
timebox[arrIdx].notification.read = read;
|
timebox[arrIdx].notification.read = read;
|
||||||
state.notifications.set(patDa, timebox);
|
state.notifications = state.notifications.set(patDa, timebox);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export default class LaunchReducer {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weatherData: WeatherState = _.get(json, 'weather', false);
|
const weatherData: WeatherState | boolean | Record<string, never> = _.get(json, 'weather', false);
|
||||||
if (weatherData) {
|
if (weatherData) {
|
||||||
useLaunchState.getState().set(state => {
|
useLaunchState.getState().set(state => {
|
||||||
state.weather = weatherData;
|
state.weather = weatherData;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import useSettingsState, { SettingsState } from "~/logic/state/settings";
|
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||||
import { SettingsUpdate } from '@urbit/api/dist/settings';
|
import { SettingsUpdate } from '@urbit/api/settings';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
export default class SettingsReducer {
|
export default class SettingsReducer {
|
||||||
reduce(json: any) {
|
reduce(json: any) {
|
||||||
@ -40,21 +41,21 @@ export default class SettingsReducer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
putEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
putEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||||
const data = _.get(json, 'put-entry', false);
|
const data: Record<string, string> = _.get(json, 'put-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!state[data["bucket-key"]]) {
|
if (!state[data['bucket-key']]) {
|
||||||
state[data["bucket-key"]] = {};
|
state[data['bucket-key']] = {};
|
||||||
}
|
}
|
||||||
state[data["bucket-key"]][data["entry-key"]] = data.value;
|
state[data['bucket-key']][data['entry-key']] = data.value;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
delEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
delEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||||
const data = _.get(json, 'del-entry', false);
|
const data = _.get(json, 'del-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state[data["bucket-key"]][data["entry-key"]];
|
delete state[data['bucket-key']][data['entry-key']];
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ export default class SettingsReducer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntry(json: any, state: SettingsState) {
|
getEntry(json: any, state: any) {
|
||||||
const bucketKey = _.get(json, 'bucket-key', false);
|
const bucketKey = _.get(json, 'bucket-key', false);
|
||||||
const entryKey = _.get(json, 'entry-key', false);
|
const entryKey = _.get(json, 'entry-key', false);
|
||||||
const entry = _.get(json, 'entry', false);
|
const entry = _.get(json, 'entry', false);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import produce from "immer";
|
import produce, { setAutoFreeze } from "immer";
|
||||||
import { compose } from "lodash/fp";
|
import { compose } from "lodash/fp";
|
||||||
import create, { State, UseStore } from "zustand";
|
import create, { State, UseStore } from "zustand";
|
||||||
import { persist, devtools } from "zustand/middleware";
|
import { persist, devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
setAutoFreeze(false);
|
||||||
|
|
||||||
|
|
||||||
export const stateSetter = <StateType>(
|
export const stateSetter = <StateType>(
|
||||||
fn: (state: StateType) => void,
|
fn: (state: StateType) => void,
|
||||||
|
@ -35,4 +35,8 @@ export function useContact(ship: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useOurContact() {
|
||||||
|
return useContact(`~${window.ship}`)
|
||||||
|
}
|
||||||
|
|
||||||
export default useContactState;
|
export default useContactState;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Graphs, decToUd, numToUd, GraphNode } from "@urbit/api";
|
import { Graphs, decToUd, numToUd, GraphNode, deSig, Association, resourceFromPath } from "@urbit/api";
|
||||||
|
import {useCallback} from "react";
|
||||||
|
|
||||||
import { BaseState, createState } from "./base";
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
@ -128,6 +129,20 @@ const useGraphState = createState<GraphState>('Graph', {
|
|||||||
// });
|
// });
|
||||||
// graphReducer(node);
|
// graphReducer(node);
|
||||||
// },
|
// },
|
||||||
}, ['graphs', 'graphKeys', 'looseNodes']);
|
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap']);
|
||||||
|
|
||||||
|
export function useGraph(ship: string, name: string) {
|
||||||
|
return useGraphState(
|
||||||
|
useCallback(s => s.graphs[`${deSig(ship)}/${name}`], [ship, name])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGraphForAssoc(association: Association) {
|
||||||
|
const { resource } = association;
|
||||||
|
const { ship, name } = resourceFromPath(resource);
|
||||||
|
return useGraph(ship, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.useGraphState = useGraphState;
|
||||||
|
|
||||||
export default useGraphState;
|
export default useGraphState;
|
||||||
|
@ -9,7 +9,7 @@ export interface LaunchState extends BaseState<LaunchState> {
|
|||||||
tiles: {
|
tiles: {
|
||||||
[app: string]: Tile;
|
[app: string]: Tile;
|
||||||
},
|
},
|
||||||
weather: WeatherState | null,
|
weather: WeatherState | null | Record<string, never> | boolean,
|
||||||
userLocation: string | null;
|
userLocation: string | null;
|
||||||
baseHash: string | null;
|
baseHash: string | null;
|
||||||
};
|
};
|
||||||
|
@ -90,8 +90,8 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({
|
|||||||
name: 'localReducer'
|
name: 'localReducer'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemberKeys?: S[]) {
|
function withLocalState<P, S extends keyof LocalState, C extends React.ComponentType<P>>(Component: C, stateMemberKeys?: S[]) {
|
||||||
return React.forwardRef((props: Omit<P, S>, ref) => {
|
return React.forwardRef<C, Omit<P, S>>((props, ref) => {
|
||||||
const localState = stateMemberKeys ? useLocalState(
|
const localState = stateMemberKeys ? useLocalState(
|
||||||
state => stateMemberKeys.reduce(
|
state => stateMemberKeys.reduce(
|
||||||
(object, key) => ({ ...object, [key]: state[key] }), {}
|
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||||
|
@ -3,10 +3,8 @@ import _ from 'lodash';
|
|||||||
import BaseStore from './base';
|
import BaseStore from './base';
|
||||||
import InviteReducer from '../reducers/invite-update';
|
import InviteReducer from '../reducers/invite-update';
|
||||||
import MetadataReducer from '../reducers/metadata-update';
|
import MetadataReducer from '../reducers/metadata-update';
|
||||||
import LocalReducer from '../reducers/local';
|
|
||||||
|
|
||||||
import { StoreState } from './type';
|
import { StoreState } from './type';
|
||||||
import { Timebox } from '@urbit/api';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import S3Reducer from '../reducers/s3-update';
|
import S3Reducer from '../reducers/s3-update';
|
||||||
import { GraphReducer } from '../reducers/graph-update';
|
import { GraphReducer } from '../reducers/graph-update';
|
||||||
@ -17,8 +15,6 @@ import LaunchReducer from '../reducers/launch-update';
|
|||||||
import ConnectionReducer from '../reducers/connection';
|
import ConnectionReducer from '../reducers/connection';
|
||||||
import SettingsReducer from '../reducers/settings-update';
|
import SettingsReducer from '../reducers/settings-update';
|
||||||
import GcpReducer from '../reducers/gcp-reducer';
|
import GcpReducer from '../reducers/gcp-reducer';
|
||||||
import { OrderedMap } from '../lib/OrderedMap';
|
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
|
||||||
import { GroupViewReducer } from '../reducers/group-view';
|
import { GroupViewReducer } from '../reducers/group-view';
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import React, { useRef, useCallback, useEffect, useState } from 'react';
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import bigInt from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
import { Association } from '@urbit/api/metadata';
|
import { Association } from '@urbit/api/metadata';
|
||||||
import { StoreState } from '~/logic/store/type';
|
import { StoreState } from '~/logic/store/type';
|
||||||
@ -16,176 +22,148 @@ import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
|||||||
import { Loading } from '~/views/components/Loading';
|
import { Loading } from '~/views/components/Loading';
|
||||||
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
import './css/custom.css';
|
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useGraphState from '~/logic/state/graph';
|
import useGraphState, { useGraphForAssoc } from '~/logic/state/graph';
|
||||||
import useGroupState from '~/logic/state/group';
|
import useGroupState, { useGroupForAssoc } from '~/logic/state/group';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import {Post} from '@urbit/api';
|
import { Content, createPost, Post } from '@urbit/api';
|
||||||
import {getPermalinkForGraph} from '~/logic/lib/permalinks';
|
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||||
|
import { ChatPane } from './components/ChatPane';
|
||||||
|
|
||||||
|
const getCurrGraphSize = (ship: string, name: string) => {
|
||||||
|
const { graphs } = useGraphState.getState();
|
||||||
|
const graph = graphs[`${ship}/${name}`];
|
||||||
|
return graph?.size ?? 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
type ChatResourceProps = StoreState & {
|
type ChatResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function ChatResource(props: ChatResourceProps) {
|
function ChatResource(props: ChatResourceProps) {
|
||||||
const station = props.association.resource;
|
const { association, api } = props;
|
||||||
const groupPath = props.association.group;
|
const { resource } = association;
|
||||||
const groups = useGroupState(state => state.groups);
|
const [toShare, setToShare] = useState<string[] | string | undefined>();
|
||||||
const group = groups[groupPath];
|
const group = useGroupForAssoc(association)!;
|
||||||
const contacts = useContactState(state => state.contacts);
|
const graph = useGraphForAssoc(association);
|
||||||
const graphs = useGraphState(state => state.graphs);
|
const unreads = useHarkState((state) => state.unreads);
|
||||||
const graphPath = station.slice(7);
|
const unreadCount =
|
||||||
const graph = graphs[graphPath];
|
(unreads.graph?.[resource]?.['/']?.unreads as number) || 0;
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const canWrite = group ? isWriter(group, resource) : false;
|
||||||
const unreadCount = unreads.graph?.[station]?.['/']?.unreads || 0;
|
|
||||||
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
|
||||||
const [,, owner, name] = station.split('/');
|
|
||||||
const ourContact = contacts?.[`~${window.ship}`];
|
|
||||||
const chatInput = useRef<ChatInput>();
|
|
||||||
const canWrite = isWriter(group, station);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const count = 100 + unreadCount;
|
const count = Math.min(400, 100 + unreadCount);
|
||||||
props.api.graph.getNewest(owner, name, count);
|
const { ship, name } = resourceFromPath(resource);
|
||||||
}, [station]);
|
props.api.graph.getNewest(ship, name, count);
|
||||||
|
setToShare(undefined);
|
||||||
const onFileDrag = useCallback(
|
(async function() {
|
||||||
(files: FileList | File[]) => {
|
if(group.hidden) {
|
||||||
if (!chatInput.current) {
|
const members = await props.api.contacts.disallowedShipsForOurContact(
|
||||||
return;
|
|
||||||
}
|
|
||||||
chatInput.current?.uploadFiles(files);
|
|
||||||
},
|
|
||||||
[chatInput.current]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
|
||||||
|
|
||||||
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
|
|
||||||
'chat-unsent',
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const appendUnsent = useCallback(
|
|
||||||
(u: string) => setUnsent(s => ({ ...s, [station]: u })),
|
|
||||||
[station]
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearUnsent = useCallback(
|
|
||||||
() => setUnsent(s => _.omit(s, station)),
|
|
||||||
[station]
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollTo = new URLSearchParams(location.search).get('msg');
|
|
||||||
|
|
||||||
const [showBanner, setShowBanner] = useState(false);
|
|
||||||
const [hasLoadedAllowed, setHasLoadedAllowed] = useState(false);
|
|
||||||
const [recipients, setRecipients] = useState([]);
|
|
||||||
|
|
||||||
const res = resourceFromPath(groupPath);
|
|
||||||
const onReply = useCallback((msg: Post) => {
|
|
||||||
const url = getPermalinkForGraph(
|
|
||||||
props.association.group,
|
|
||||||
props.association.resource,
|
|
||||||
msg.index
|
|
||||||
);
|
|
||||||
const message = `${url}\n~${msg.author} : `;
|
|
||||||
setUnsent(s => ({...s, [props.association.resource]: message }));
|
|
||||||
}, [props.association, group, setUnsent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!res) { return; }
|
|
||||||
if (!group) { return; }
|
|
||||||
if (group.hidden) {
|
|
||||||
const members = _.compact(await Promise.all(
|
|
||||||
Array.from(group.members)
|
Array.from(group.members)
|
||||||
.map(s => {
|
);
|
||||||
const ship = `~${s}`;
|
|
||||||
if(s === window.ship) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
return props.api.contacts.fetchIsAllowed(
|
|
||||||
`~${window.ship}`,
|
|
||||||
'personal',
|
|
||||||
ship,
|
|
||||||
true
|
|
||||||
).then(isAllowed => {
|
|
||||||
return isAllowed ? null : ship;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
if(members.length > 0) {
|
if(members.length > 0) {
|
||||||
setShowBanner(true);
|
setToShare(members);
|
||||||
setRecipients(members);
|
|
||||||
} else {
|
|
||||||
setShowBanner(false);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const groupShared = await props.api.contacts.fetchIsAllowed(
|
const { ship: groupHost } = resourceFromPath(association.group);
|
||||||
|
const shared = await props.api.contacts.fetchIsAllowed(
|
||||||
`~${window.ship}`,
|
`~${window.ship}`,
|
||||||
'personal',
|
'personal',
|
||||||
res.ship,
|
groupHost,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
setShowBanner(!groupShared);
|
if(!shared) {
|
||||||
|
setToShare(association.group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHasLoadedAllowed(true);
|
|
||||||
})();
|
})();
|
||||||
}, [groupPath, group]);
|
}, [resource]);
|
||||||
|
|
||||||
if(!graph) {
|
const onReply = useCallback(
|
||||||
|
(msg: Post) => {
|
||||||
|
const url = getPermalinkForGraph(
|
||||||
|
props.association.group,
|
||||||
|
props.association.resource,
|
||||||
|
msg.index
|
||||||
|
);
|
||||||
|
return `${url}\n~${msg.author} : `;
|
||||||
|
},
|
||||||
|
[association]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAdmin = useMemo(
|
||||||
|
() => (group ? group.tags.role.admin.has(`~${window.ship}`) : false),
|
||||||
|
[group]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchMessages = useCallback(async (newer: boolean) => {
|
||||||
|
const { api } = props;
|
||||||
|
const pageSize = 100;
|
||||||
|
|
||||||
|
const [, , ship, name] = resource.split('/');
|
||||||
|
const graphSize = graph?.size ?? 0;
|
||||||
|
const expectedSize = graphSize + pageSize;
|
||||||
|
if (newer) {
|
||||||
|
const index = graph.peekLargest()?.[0];
|
||||||
|
if(!index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await api.graph.getYoungerSiblings(
|
||||||
|
ship,
|
||||||
|
name,
|
||||||
|
pageSize,
|
||||||
|
`/${index.toString()}`
|
||||||
|
);
|
||||||
|
return expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||||
|
} else {
|
||||||
|
const index = graph.peekSmallest()?.[0];
|
||||||
|
if(!index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
||||||
|
const done = expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
}, [graph, resource]);
|
||||||
|
|
||||||
|
const onSubmit = useCallback((contents: Content[]) => {
|
||||||
|
const { ship, name } = resourceFromPath(resource);
|
||||||
|
api.graph.addPost(ship, name, createPost(window.ship, contents))
|
||||||
|
}, [resource]);
|
||||||
|
|
||||||
|
const dismissUnread = useCallback(() => {
|
||||||
|
api.hark.markCountAsRead(association, '/', 'message');
|
||||||
|
}, [association]);
|
||||||
|
|
||||||
|
const getPermalink = useCallback(
|
||||||
|
(index: BigInteger) =>
|
||||||
|
getPermalinkForGraph(association.group, resource, `/${index.toString()}`),
|
||||||
|
[association]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!graph) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
<ChatPane
|
||||||
<ShareProfile
|
id={resource.slice(7)}
|
||||||
our={ourContact}
|
graph={graph}
|
||||||
api={props.api}
|
unreadCount={unreadCount}
|
||||||
recipient={owner}
|
api={api}
|
||||||
recipients={recipients}
|
canWrite={canWrite}
|
||||||
showBanner={showBanner}
|
onReply={onReply}
|
||||||
setShowBanner={setShowBanner}
|
fetchMessages={fetchMessages}
|
||||||
group={group}
|
dismissUnread={dismissUnread}
|
||||||
groupPath={groupPath}
|
getPermalink={getPermalink}
|
||||||
/>
|
isAdmin={isAdmin}
|
||||||
{dragging && <SubmitDragger />}
|
onSubmit={onSubmit}
|
||||||
<ChatWindow
|
promptShare={toShare}
|
||||||
key={station}
|
/>
|
||||||
history={props.history}
|
|
||||||
graph={graph}
|
|
||||||
graphSize={graph.size}
|
|
||||||
unreadCount={unreadCount}
|
|
||||||
showOurContact={ !showBanner && hasLoadedAllowed }
|
|
||||||
association={props.association}
|
|
||||||
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
|
|
||||||
group={group}
|
|
||||||
ship={owner}
|
|
||||||
onReply={onReply}
|
|
||||||
station={station}
|
|
||||||
api={props.api}
|
|
||||||
scrollTo={scrollTo ? bigInt(scrollTo) : undefined}
|
|
||||||
/>
|
|
||||||
{ canWrite && (
|
|
||||||
<ChatInput
|
|
||||||
ref={chatInput}
|
|
||||||
api={props.api}
|
|
||||||
station={station}
|
|
||||||
ourContact={
|
|
||||||
(!showBanner && hasLoadedAllowed) ? ourContact : null
|
|
||||||
}
|
|
||||||
envelopes={[]}
|
|
||||||
onUnmount={appendUnsent}
|
|
||||||
placeholder="Message..."
|
|
||||||
message={unsent[station] || ''}
|
|
||||||
deleteMessage={clearUnsent}
|
|
||||||
/> )}
|
|
||||||
</Col>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { ChatResource };
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, ReactNode } from 'react';
|
||||||
import ChatEditor from './chat-editor';
|
import ChatEditor from './chat-editor';
|
||||||
import { IuseStorage } from '~/logic/lib/useStorage';
|
import { IuseStorage } from '~/logic/lib/useStorage';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
@ -8,28 +8,29 @@ import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Envelope } from '~/types/chat-update';
|
import { Envelope } from '~/types/chat-update';
|
||||||
import { StorageState } from '~/types';
|
import { StorageState } from '~/types';
|
||||||
import { Contacts, Content } from '@urbit/api';
|
import { Contact, Contacts, Content, Post } from '@urbit/api';
|
||||||
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
||||||
import withStorage from '~/views/components/withStorage';
|
import withStorage from '~/views/components/withStorage';
|
||||||
import { withLocalState } from '~/logic/state/local';
|
import { withLocalState } from '~/logic/state/local';
|
||||||
|
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||||
|
|
||||||
type ChatInputProps = IuseStorage & {
|
type ChatInputProps = IuseStorage & {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
numMsgs: number;
|
ourContact?: Contact;
|
||||||
station: unknown;
|
|
||||||
ourContact: unknown;
|
|
||||||
envelopes: Envelope[];
|
|
||||||
onUnmount(msg: string): void;
|
onUnmount(msg: string): void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
message: string;
|
message: string;
|
||||||
deleteMessage(): void;
|
deleteMessage(): void;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
|
onSubmit: (contents: Content[]) => void;
|
||||||
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatInputState {
|
interface ChatInputState {
|
||||||
inCodeMode: boolean;
|
inCodeMode: boolean;
|
||||||
submitFocus: boolean;
|
submitFocus: boolean;
|
||||||
uploadingPaste: boolean;
|
uploadingPaste: boolean;
|
||||||
|
currentInput: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||||
@ -41,7 +42,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
inCodeMode: false,
|
inCodeMode: false,
|
||||||
submitFocus: false,
|
submitFocus: false,
|
||||||
uploadingPaste: false
|
uploadingPaste: false,
|
||||||
|
currentInput: props.message
|
||||||
};
|
};
|
||||||
|
|
||||||
this.chatEditor = React.createRef();
|
this.chatEditor = React.createRef();
|
||||||
@ -50,6 +52,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
this.toggleCode = this.toggleCode.bind(this);
|
this.toggleCode = this.toggleCode.bind(this);
|
||||||
this.uploadSuccess = this.uploadSuccess.bind(this);
|
this.uploadSuccess = this.uploadSuccess.bind(this);
|
||||||
this.uploadError = this.uploadError.bind(this);
|
this.uploadError = this.uploadError.bind(this);
|
||||||
|
this.eventHandler = this.eventHandler.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCode() {
|
toggleCode() {
|
||||||
@ -58,39 +61,28 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
submit(text) {
|
async submit(text) {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
const [, , ship, name] = props.station.split('/');
|
const { onSubmit, api } = this.props;
|
||||||
if (state.inCodeMode) {
|
this.setState({
|
||||||
this.setState(
|
inCodeMode: false
|
||||||
{
|
});
|
||||||
inCodeMode: false
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
const output = await props.api.graph.eval(text);
|
|
||||||
const contents: Content[] = [{ code: { output, expression: text } }];
|
|
||||||
const post = createPost(contents);
|
|
||||||
props.api.graph.addPost(ship, name, post);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = createPost(tokenizeMessage(text));
|
|
||||||
|
|
||||||
props.deleteMessage();
|
props.deleteMessage();
|
||||||
|
if(state.inCodeMode) {
|
||||||
props.api.graph.addPost(ship, name, post);
|
const output = await api.graph.eval(text) as string[];
|
||||||
|
onSubmit([{ code: { output, expression: text } }]);
|
||||||
|
} else {
|
||||||
|
onSubmit(tokenizeMessage(text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadSuccess(url) {
|
uploadSuccess(url: string) {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
if (this.state.uploadingPaste) {
|
if (this.state.uploadingPaste) {
|
||||||
this.chatEditor.current.editor.setValue(url);
|
this.chatEditor.current.editor.setValue(url);
|
||||||
this.setState({ uploadingPaste: false });
|
this.setState({ uploadingPaste: false });
|
||||||
} else {
|
} else {
|
||||||
const [, , ship, name] = props.station.split('/');
|
props.onSubmit([{ url }])
|
||||||
props.api.graph.addPost(ship, name, createPost([{ url }]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +112,10 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventHandler(value) {
|
||||||
|
this.setState({ currentInput: value });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
@ -130,6 +126,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
const avatar =
|
const avatar =
|
||||||
props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
|
props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
|
||||||
<BaseImage
|
<BaseImage
|
||||||
|
flexShrink={0}
|
||||||
src={props.ourContact.avatar}
|
src={props.ourContact.avatar}
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
@ -170,7 +167,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
className='cf'
|
className='cf'
|
||||||
zIndex={0}
|
zIndex={0}
|
||||||
>
|
>
|
||||||
<Row p='12px 4px 12px 12px' alignItems='center'>
|
<Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'>
|
||||||
{avatar}
|
{avatar}
|
||||||
</Row>
|
</Row>
|
||||||
<ChatEditor
|
<ChatEditor
|
||||||
@ -180,15 +177,25 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
onUnmount={props.onUnmount}
|
onUnmount={props.onUnmount}
|
||||||
message={props.message}
|
message={props.message}
|
||||||
onPaste={this.onPaste.bind(this)}
|
onPaste={this.onPaste.bind(this)}
|
||||||
|
changeEvent={this.eventHandler}
|
||||||
placeholder='Message...'
|
placeholder='Message...'
|
||||||
/>
|
/>
|
||||||
<Box mx={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
<Box mx='12px' flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||||
|
<Icon
|
||||||
|
icon='Dojo'
|
||||||
|
cursor='pointer'
|
||||||
|
onClick={this.toggleCode}
|
||||||
|
color={state.inCodeMode ? 'blue' : 'black'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box ml='12px' mr={3} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||||
{this.props.canUpload ? (
|
{this.props.canUpload ? (
|
||||||
this.props.uploading ? (
|
this.props.uploading ? (
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
icon='Links'
|
icon='Attachment'
|
||||||
|
cursor='pointer'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -198,18 +205,30 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
<Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) ?
|
||||||
<Icon
|
<Box
|
||||||
icon='Dojo'
|
ml={2}
|
||||||
onClick={this.toggleCode}
|
mr="12px"
|
||||||
color={state.inCodeMode ? 'blue' : 'black'}
|
flexShrink={0}
|
||||||
/>
|
display="flex"
|
||||||
</Box>
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
borderRadius="50%"
|
||||||
|
backgroundColor={state.currentInput !== '' ? 'blue' : 'gray'}
|
||||||
|
cursor={state.currentInput !== '' ? 'pointer' : 'default'}
|
||||||
|
onClick={() => this.chatEditor.current.submit()}
|
||||||
|
>
|
||||||
|
<Icon icon="ArrowEast" color="white" />
|
||||||
|
</Box>
|
||||||
|
: null}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), [
|
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>(
|
||||||
'hideAvatars'
|
withStorage<ChatInputProps, ChatInput>(ChatInput, { accept: 'image/*' }),
|
||||||
]);
|
['hideAvatars']
|
||||||
|
)
|
||||||
|
@ -3,6 +3,7 @@ import bigInt from 'big-integer';
|
|||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
Component,
|
Component,
|
||||||
PureComponent,
|
PureComponent,
|
||||||
@ -40,11 +41,13 @@ import styled from 'styled-components';
|
|||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState, {useContact} 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 {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
|
import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
|
||||||
|
import {Contact} from '@urbit/api';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
|
||||||
|
|
||||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||||
@ -67,7 +70,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
<Rule borderColor='lightGray' />
|
<Rule borderColor='lightGray' />
|
||||||
<Text
|
<Text
|
||||||
gray
|
gray
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
whiteSpace='nowrap'
|
whiteSpace='nowrap'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
@ -80,16 +83,13 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const UnreadMarker = React.forwardRef(
|
export const UnreadMarker = React.forwardRef(
|
||||||
({ dayBreak, when, api, association }, ref) => {
|
({ dismissUnread }: any, ref) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const idling = useIdlingState();
|
const idling = useIdlingState();
|
||||||
const dismiss = useCallback(() => {
|
|
||||||
api.hark.markCountAsRead(association, '/', 'message');
|
|
||||||
}, [api, association]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !idling) {
|
if (visible && !idling) {
|
||||||
dismiss();
|
dismissUnread();
|
||||||
}
|
}
|
||||||
}, [visible, idling]);
|
}, [visible, idling]);
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export const UnreadMarker = React.forwardRef(
|
|||||||
<Text
|
<Text
|
||||||
color='blue'
|
color='blue'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
whiteSpace='nowrap'
|
whiteSpace='nowrap'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
px={2}
|
px={2}
|
||||||
@ -141,10 +141,9 @@ const MessageActionItem = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
const MessageActions = ({ api, onReply, association, msg, isAdmin, permalink }) => {
|
||||||
const isAdmin = () => group.tags.role.admin.has(window.ship);
|
|
||||||
const isOwn = () => msg.author === window.ship;
|
const isOwn = () => msg.author === window.ship;
|
||||||
const { doCopy, copyDisplay } = useCopy(`web+urbitgraph://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
|
const { doCopy, copyDisplay } = useCopy(permalink, 'Copy Message Link');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -170,7 +169,7 @@ const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
|||||||
width='auto'
|
width='auto'
|
||||||
alignY='top'
|
alignY='top'
|
||||||
alignX='right'
|
alignX='right'
|
||||||
flexShrink={'0'}
|
flexShrink={0}
|
||||||
offsetY={8}
|
offsetY={8}
|
||||||
offsetX={-24}
|
offsetX={-24}
|
||||||
options={
|
options={
|
||||||
@ -235,176 +234,161 @@ interface ChatMessageProps {
|
|||||||
previousMsg?: Post;
|
previousMsg?: Post;
|
||||||
nextMsg?: Post;
|
nextMsg?: Post;
|
||||||
isLastRead: boolean;
|
isLastRead: boolean;
|
||||||
group: Group;
|
permalink: string;
|
||||||
association: Association;
|
|
||||||
transcluded?: number;
|
transcluded?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
style?: unknown;
|
style?: unknown;
|
||||||
scrollWindow: HTMLDivElement;
|
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
dismissUnread: () => void;
|
||||||
history: unknown;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
renderSigil?: boolean;
|
renderSigil?: boolean;
|
||||||
hideHover?: boolean;
|
hideHover?: boolean;
|
||||||
innerRef: (el: HTMLDivElement | null) => void;
|
innerRef: (el: HTMLDivElement | null) => void;
|
||||||
onReply?: (msg: Post) => void;
|
onReply?: (msg: Post) => void;
|
||||||
|
showOurContact: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessage extends Component<ChatMessageProps> {
|
function ChatMessage(props: ChatMessageProps) {
|
||||||
private divRef: React.RefObject<HTMLDivElement>;
|
let { highlighted } = props;
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
previousMsg,
|
||||||
|
nextMsg,
|
||||||
|
isLastRead,
|
||||||
|
group,
|
||||||
|
association,
|
||||||
|
className = '',
|
||||||
|
isPending,
|
||||||
|
style,
|
||||||
|
isLastMessage,
|
||||||
|
api,
|
||||||
|
showOurContact,
|
||||||
|
fontSize,
|
||||||
|
hideHover,
|
||||||
|
dismissUnread,
|
||||||
|
permalink
|
||||||
|
} = props;
|
||||||
|
|
||||||
constructor(props) {
|
let onReply = props?.onReply ?? (() => {});
|
||||||
super(props);
|
const transcluded = props?.transcluded ?? 0;
|
||||||
this.divRef = React.createRef();
|
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
||||||
}
|
!nextMsg ||
|
||||||
|
msg.number === 1
|
||||||
|
);
|
||||||
|
|
||||||
componentDidMount() {}
|
const ourMention = msg?.contents?.some((e) => {
|
||||||
|
return e?.mention && e?.mention === window.ship;
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
if (!highlighted) {
|
||||||
const {
|
if (ourMention) {
|
||||||
msg,
|
highlighted = true;
|
||||||
previousMsg,
|
}
|
||||||
nextMsg,
|
|
||||||
isLastRead,
|
|
||||||
group,
|
|
||||||
association,
|
|
||||||
className = '',
|
|
||||||
isPending,
|
|
||||||
style,
|
|
||||||
scrollWindow,
|
|
||||||
isLastMessage,
|
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
|
||||||
highlighted,
|
|
||||||
showOurContact,
|
|
||||||
fontSize,
|
|
||||||
hideHover
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let onReply = this.props?.onReply ?? (() => {});
|
|
||||||
const transcluded = this.props?.transcluded ?? 0;
|
|
||||||
let { renderSigil } = this.props;
|
|
||||||
|
|
||||||
if (renderSigil === undefined) {
|
|
||||||
renderSigil = Boolean(
|
|
||||||
(nextMsg && msg.author !== nextMsg.author) ||
|
|
||||||
!nextMsg ||
|
|
||||||
msg.number === 1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
const date = useMemo(() => daToUnix(bigInt(msg.index.split('/')[1])), [msg.index]);
|
||||||
const nextDate = nextMsg ? (
|
const nextDate = useMemo(() => nextMsg ? (
|
||||||
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
||||||
) : null;
|
) : null,
|
||||||
|
[nextMsg]
|
||||||
|
);
|
||||||
|
|
||||||
const dayBreak =
|
const dayBreak = useMemo(() =>
|
||||||
nextMsg &&
|
nextDate &&
|
||||||
new Date(date).getDate() !==
|
new Date(date).getDate() !==
|
||||||
new Date(nextDate).getDate();
|
new Date(nextDate).getDate()
|
||||||
|
, [nextDate, date])
|
||||||
|
|
||||||
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
||||||
|
|
||||||
const timestamp = moment
|
const timestamp = useMemo(() => moment
|
||||||
.unix(date / 1000)
|
.unix(date / 1000)
|
||||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
.format(renderSigil ? 'h:mm A' : 'h:mm'),
|
||||||
|
[date, renderSigil]
|
||||||
|
);
|
||||||
|
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
msg,
|
msg,
|
||||||
timestamp,
|
timestamp,
|
||||||
association,
|
association,
|
||||||
group,
|
isPending,
|
||||||
style,
|
showOurContact,
|
||||||
containerClass,
|
api,
|
||||||
isPending,
|
highlighted,
|
||||||
showOurContact,
|
fontSize,
|
||||||
history,
|
hideHover,
|
||||||
api,
|
transcluded,
|
||||||
scrollWindow,
|
onReply
|
||||||
highlighted,
|
};
|
||||||
fontSize,
|
|
||||||
hideHover,
|
|
||||||
transcluded,
|
|
||||||
onReply
|
|
||||||
};
|
|
||||||
|
|
||||||
const unreadContainerStyle = {
|
const message = useMemo(() => (
|
||||||
height: isLastRead ? '2rem' : '0'
|
<Message
|
||||||
};
|
msg={msg}
|
||||||
|
timestamp={timestamp}
|
||||||
|
timestampHover={!renderSigil}
|
||||||
|
api={api}
|
||||||
|
transcluded={transcluded}
|
||||||
|
showOurContact={showOurContact}
|
||||||
|
/>
|
||||||
|
), [renderSigil, msg, timestamp, api, transcluded, showOurContact]);
|
||||||
|
|
||||||
return (
|
const unreadContainerStyle = {
|
||||||
<Box
|
height: isLastRead ? '2rem' : '0'
|
||||||
ref={this.props.innerRef}
|
};
|
||||||
pt={renderSigil ? 2 : 0}
|
|
||||||
width="100%"
|
return (
|
||||||
pb={isLastMessage ? '20px' : 0}
|
<Box
|
||||||
className={containerClass}
|
ref={props.innerRef}
|
||||||
style={style}
|
pt={renderSigil ? 2 : 0}
|
||||||
>
|
width="100%"
|
||||||
{dayBreak && !isLastRead ? (
|
pb={isLastMessage ? '20px' : 0}
|
||||||
<DayBreak when={date} shimTop={renderSigil} />
|
className={containerClass}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{dayBreak && !isLastRead ? (
|
||||||
|
<DayBreak when={date} shimTop={renderSigil} />
|
||||||
|
) : null}
|
||||||
|
<MessageWrapper permalink={permalink} {...messageProps}>
|
||||||
|
{ renderSigil && <MessageAuthor {...messageProps} />}
|
||||||
|
{message}
|
||||||
|
</MessageWrapper>
|
||||||
|
<Box style={unreadContainerStyle}>
|
||||||
|
{isLastRead ? (
|
||||||
|
<UnreadMarker dismissUnread={dismissUnread} />
|
||||||
) : null}
|
) : null}
|
||||||
{renderSigil ? (
|
|
||||||
<MessageWrapper {...messageProps}>
|
|
||||||
<MessageAuthor pb={1} {...messageProps} />
|
|
||||||
<Message pl={'44px'} pr={4} {...messageProps} />
|
|
||||||
</MessageWrapper>
|
|
||||||
) : (
|
|
||||||
<MessageWrapper {...messageProps}>
|
|
||||||
<Message pl={'44px'} pr={4} timestampHover {...messageProps} />
|
|
||||||
</MessageWrapper>
|
|
||||||
)}
|
|
||||||
<Box style={unreadContainerStyle}>
|
|
||||||
{isLastRead ? (
|
|
||||||
<UnreadMarker
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
dayBreak={dayBreak}
|
|
||||||
when={date}
|
|
||||||
ref={unreadMarkerRef}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
</Box>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.forwardRef((props, ref) => (
|
export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
|
||||||
<ChatMessage {...props} innerRef={ref} />
|
<ChatMessage {...props} innerRef={ref} />
|
||||||
));
|
));
|
||||||
|
|
||||||
export const MessageAuthor = ({
|
export const MessageAuthor = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
msg,
|
||||||
group,
|
|
||||||
api,
|
api,
|
||||||
history,
|
|
||||||
scrollWindow,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
...rest
|
|
||||||
}) => {
|
}) => {
|
||||||
const osDark = useLocalState((state) => state.dark);
|
const osDark = useLocalState((state) => state.dark);
|
||||||
|
|
||||||
const theme = useSettingsState((s) => s.display.theme);
|
const theme = useSettingsState((s) => s.display.theme);
|
||||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||||
const contacts = useContactState((state) => state.contacts);
|
let contact: Contact | null = useContact(`~${msg.author}`);
|
||||||
|
|
||||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
||||||
|
|
||||||
const datestamp = moment
|
const datestamp = moment
|
||||||
.unix(date / 1000)
|
.unix(date / 1000)
|
||||||
.format(DATESTAMP_FORMAT);
|
.format(DATESTAMP_FORMAT);
|
||||||
const contact =
|
contact =
|
||||||
((msg.author === window.ship && showOurContact) ||
|
((msg.author === window.ship && showOurContact) ||
|
||||||
msg.author !== window.ship) &&
|
msg.author !== window.ship)
|
||||||
`~${msg.author}` in contacts
|
? contact
|
||||||
? contacts[`~${msg.author}`]
|
: null;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
const { hideAvatars } = useSettingsState(selectCalmState);
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
@ -457,7 +441,7 @@ export const MessageAuthor = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box display='flex' alignItems='flex-start' {...rest}>
|
<Box pb="1" display='flex' alignItems='flex-start'>
|
||||||
<Box
|
<Box
|
||||||
height={24}
|
height={24}
|
||||||
pr={2}
|
pr={2}
|
||||||
@ -509,20 +493,20 @@ export const MessageAuthor = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Message = ({
|
type MessageProps = { timestamp: string; timestampHover: boolean; }
|
||||||
|
& Pick<ChatMessageProps, "msg" | "api" | "transcluded" | "showOurContact">
|
||||||
|
|
||||||
|
export const Message = React.memo(({
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
msg,
|
||||||
group,
|
|
||||||
api,
|
api,
|
||||||
scrollWindow,
|
|
||||||
timestampHover,
|
timestampHover,
|
||||||
transcluded,
|
transcluded,
|
||||||
showOurContact,
|
showOurContact
|
||||||
...rest
|
}: MessageProps) => {
|
||||||
}) => {
|
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
return (
|
return (
|
||||||
<Box width="100%" position='relative' {...rest}>
|
<Box pl="44px" width="100%" position='relative'>
|
||||||
{timestampHover ? (
|
{timestampHover ? (
|
||||||
<Text
|
<Text
|
||||||
display={hovering ? 'block' : 'none'}
|
display={hovering ? 'block' : 'none'}
|
||||||
@ -549,7 +533,9 @@ export const Message = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Message.displayName = 'Message';
|
||||||
|
|
||||||
export const MessagePlaceholder = ({
|
export const MessagePlaceholder = ({
|
||||||
height,
|
height,
|
||||||
@ -578,7 +564,7 @@ export const MessagePlaceholder = ({
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
display='block'
|
display='block'
|
||||||
background='gray'
|
background='washedGray'
|
||||||
width='24px'
|
width='24px'
|
||||||
height='24px'
|
height='24px'
|
||||||
borderRadius='50%'
|
borderRadius='50%'
|
||||||
@ -601,12 +587,13 @@ export const MessagePlaceholder = ({
|
|||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
gray
|
washedGray
|
||||||
cursor='default'
|
cursor='default'
|
||||||
>
|
>
|
||||||
<Text maxWidth='32rem' display='block'>
|
<Text maxWidth='32rem' display='block'>
|
||||||
<Text
|
<Text
|
||||||
backgroundColor='gray'
|
backgroundColor='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
display='block'
|
display='block'
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
@ -618,10 +605,11 @@ export const MessagePlaceholder = ({
|
|||||||
mono
|
mono
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
gray
|
washedGray
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
background='gray'
|
background='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
display='block'
|
display='block'
|
||||||
height='1em'
|
height='1em'
|
||||||
style={{ width: `${((index % 3) + 1) * 3}em` }}
|
style={{ width: `${((index % 3) + 1) * 3}em` }}
|
||||||
@ -632,12 +620,14 @@ export const MessagePlaceholder = ({
|
|||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
ml='2'
|
ml='2'
|
||||||
gray
|
washedGray
|
||||||
|
borderRadius='2'
|
||||||
display={['none', 'inline-block']}
|
display={['none', 'inline-block']}
|
||||||
className='child'
|
className='child'
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
backgroundColor='gray'
|
backgroundColor='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
display='block'
|
display='block'
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
@ -646,7 +636,8 @@ export const MessagePlaceholder = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Text
|
<Text
|
||||||
display='block'
|
display='block'
|
||||||
backgroundColor='gray'
|
backgroundColor='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
height='1em'
|
height='1em'
|
||||||
style={{ width: `${(index % 5) * 20}%` }}
|
style={{ width: `${(index % 5) * 20}%` }}
|
||||||
></Text>
|
></Text>
|
||||||
|
183
pkg/interface/src/views/apps/chat/components/ChatPane.tsx
Normal file
183
pkg/interface/src/views/apps/chat/components/ChatPane.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import React, { useRef, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
|
import { Col } from '@tlon/indigo-react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
|
import { Association } from '@urbit/api/metadata';
|
||||||
|
import { StoreState } from '~/logic/store/type';
|
||||||
|
import { useFileDrag } from '~/logic/lib/useDrag';
|
||||||
|
import ChatWindow from './ChatWindow';
|
||||||
|
import ChatInput from './ChatInput';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { ShareProfile } from '~/views/apps/chat/components/ShareProfile';
|
||||||
|
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||||
|
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||||
|
import { Loading } from '~/views/components/Loading';
|
||||||
|
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
|
import useContactState, { useOurContact } from '~/logic/state/contact';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
import { Post, Graph, Content } from '@urbit/api';
|
||||||
|
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||||
|
|
||||||
|
interface ChatPaneProps {
|
||||||
|
/**
|
||||||
|
* A key to uniquely identify a ChatPane instance. Should be either the
|
||||||
|
* resource for group chats or the @p for DMs
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The graph of the chat to render
|
||||||
|
*/
|
||||||
|
graph: Graph;
|
||||||
|
unreadCount: number;
|
||||||
|
/**
|
||||||
|
* User able to write to chat
|
||||||
|
*/
|
||||||
|
canWrite: boolean;
|
||||||
|
api: GlobalApi;
|
||||||
|
/**
|
||||||
|
* Get contents of reply message
|
||||||
|
*/
|
||||||
|
onReply: (msg: Post) => string;
|
||||||
|
/**
|
||||||
|
* Fetch more messages
|
||||||
|
*
|
||||||
|
* @param newer Get newer or older backlog
|
||||||
|
* @returns Whether backlog is finished loading in that direction
|
||||||
|
*/
|
||||||
|
fetchMessages: (newer: boolean) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Dismiss unreads for chat
|
||||||
|
*/
|
||||||
|
dismissUnread: () => void;
|
||||||
|
/**
|
||||||
|
* Get permalink for a node
|
||||||
|
*/
|
||||||
|
getPermalink: (idx: BigInteger) => string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
/**
|
||||||
|
* Post message with contents to channel
|
||||||
|
*/
|
||||||
|
onSubmit: (contents: Content[]) => void;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Users or group we haven't shared our contact with yet
|
||||||
|
*
|
||||||
|
* string[] - array of ships
|
||||||
|
* string - path of group
|
||||||
|
*/
|
||||||
|
promptShare?: string[] | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatPane(props: ChatPaneProps) {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
graph,
|
||||||
|
unreadCount,
|
||||||
|
canWrite,
|
||||||
|
id,
|
||||||
|
getPermalink,
|
||||||
|
isAdmin,
|
||||||
|
dismissUnread,
|
||||||
|
onSubmit,
|
||||||
|
promptShare = [],
|
||||||
|
fetchMessages
|
||||||
|
} = props;
|
||||||
|
const graphTimesentMap = useGraphState((state) => state.graphTimesentMap);
|
||||||
|
const ourContact = useOurContact();
|
||||||
|
const chatInput = useRef<ChatInput>();
|
||||||
|
|
||||||
|
const onFileDrag = useCallback(
|
||||||
|
(files: FileList | File[]) => {
|
||||||
|
if (!chatInput.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatInput.current?.uploadFiles(files);
|
||||||
|
},
|
||||||
|
[chatInput.current]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||||
|
|
||||||
|
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
|
||||||
|
'chat-unsent',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const appendUnsent = useCallback(
|
||||||
|
(u: string) => setUnsent((s) => ({ ...s, [id]: u })),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearUnsent = useCallback(() => {
|
||||||
|
setUnsent((s) => {
|
||||||
|
if (id in s) {
|
||||||
|
return _.omit(s, id);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const scrollTo = new URLSearchParams(location.search).get('msg');
|
||||||
|
|
||||||
|
const [showBanner, setShowBanner] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowBanner(promptShare.length > 0);
|
||||||
|
}, [promptShare]);
|
||||||
|
|
||||||
|
const onReply = useCallback(
|
||||||
|
(msg: Post) => {
|
||||||
|
const message = props.onReply(msg);
|
||||||
|
setUnsent((s) => ({ ...s, [id]: message }));
|
||||||
|
},
|
||||||
|
[id, props.onReply]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!graph) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||||
|
<ShareProfile
|
||||||
|
our={ourContact}
|
||||||
|
api={api}
|
||||||
|
recipients={showBanner ? promptShare : []}
|
||||||
|
onShare={() => setShowBanner(false)}
|
||||||
|
/>
|
||||||
|
{dragging && <SubmitDragger />}
|
||||||
|
<ChatWindow
|
||||||
|
key={id}
|
||||||
|
graph={graph}
|
||||||
|
graphSize={graph.size}
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
showOurContact={promptShare.length === 0 && !showBanner}
|
||||||
|
pendingSize={Object.keys(graphTimesentMap[id] || {}).length}
|
||||||
|
onReply={onReply}
|
||||||
|
dismissUnread={dismissUnread}
|
||||||
|
fetchMessages={fetchMessages}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
getPermalink={getPermalink}
|
||||||
|
api={api}
|
||||||
|
scrollTo={scrollTo ? bigInt(scrollTo) : undefined}
|
||||||
|
/>
|
||||||
|
{canWrite && (
|
||||||
|
<ChatInput
|
||||||
|
ref={chatInput}
|
||||||
|
api={props.api}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
ourContact={(promptShare.length === 0 && ourContact) || undefined}
|
||||||
|
onUnmount={appendUnsent}
|
||||||
|
placeholder="Message..."
|
||||||
|
message={unsent[id] || ''}
|
||||||
|
deleteMessage={clearUnsent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, Component, useRef, useState, useCallback } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
@ -11,7 +11,9 @@ import {
|
|||||||
Associations,
|
Associations,
|
||||||
Group,
|
Group,
|
||||||
Groups,
|
Groups,
|
||||||
Graph
|
Graph,
|
||||||
|
Post,
|
||||||
|
GraphNode
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
@ -30,20 +32,21 @@ const DEFAULT_BACKLOG_SIZE = 100;
|
|||||||
const IDLE_THRESHOLD = 64;
|
const IDLE_THRESHOLD = 64;
|
||||||
const MAX_BACKLOG_SIZE = 1000;
|
const MAX_BACKLOG_SIZE = 1000;
|
||||||
|
|
||||||
type ChatWindowProps = RouteComponentProps<{
|
|
||||||
ship: Patp;
|
type ChatWindowProps = {
|
||||||
station: string;
|
|
||||||
}> & {
|
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
graphSize: number;
|
graphSize: number;
|
||||||
association: Association;
|
|
||||||
group: Group;
|
|
||||||
ship: Patp;
|
|
||||||
station: any;
|
station: any;
|
||||||
|
fetchMessages: (newer: boolean) => Promise<boolean>;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
scrollTo?: BigInteger;
|
scrollTo?: BigInteger;
|
||||||
onReply: (msg: Post) => void;
|
onReply: (msg: Post) => void;
|
||||||
|
dismissUnread: () => void;
|
||||||
|
pendingSize?: number;
|
||||||
|
showOurContact: boolean;
|
||||||
|
getPermalink: (index: BigInteger) => string;
|
||||||
|
isAdmin: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatWindowState {
|
interface ChatWindowState {
|
||||||
@ -55,12 +58,12 @@ interface ChatWindowState {
|
|||||||
|
|
||||||
const virtScrollerStyle = { height: '100%' };
|
const virtScrollerStyle = { height: '100%' };
|
||||||
|
|
||||||
|
|
||||||
class ChatWindow extends Component<
|
class ChatWindow extends Component<
|
||||||
ChatWindowProps,
|
ChatWindowProps,
|
||||||
ChatWindowState
|
ChatWindowState
|
||||||
> {
|
> {
|
||||||
private virtualList: VirtualScroller | null;
|
private virtualList: VirtualScroller<GraphNode> | null;
|
||||||
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
|
||||||
private prevSize = 0;
|
private prevSize = 0;
|
||||||
private unreadSet = false;
|
private unreadSet = false;
|
||||||
|
|
||||||
@ -76,14 +79,12 @@ class ChatWindow extends Component<
|
|||||||
unreadIndex: bigInt.zero
|
unreadIndex: bigInt.zero
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dismissUnread = this.dismissUnread.bind(this);
|
|
||||||
this.scrollToUnread = this.scrollToUnread.bind(this);
|
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||||
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
||||||
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
||||||
|
|
||||||
this.virtualList = null;
|
this.virtualList = null;
|
||||||
this.unreadMarkerRef = React.createRef();
|
|
||||||
this.prevSize = props.graph.size;
|
this.prevSize = props.graph.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +93,9 @@ class ChatWindow extends Component<
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({ initialized: true }, () => {
|
this.setState({ initialized: true }, () => {
|
||||||
if(this.props.scrollTo) {
|
if(this.props.scrollTo) {
|
||||||
this.virtualList.scrollToIndex(this.props.scrollTo);
|
this.virtualList!.scrollLocked = false;
|
||||||
|
this.virtualList!.scrollToIndex(this.props.scrollTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}, this.INITIALIZATION_MAX_TIME);
|
}, this.INITIALIZATION_MAX_TIME);
|
||||||
@ -109,9 +109,11 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
const unreadIndex = graph.keys()[unreadCount];
|
const unreadIndex = graph.keys()[unreadCount];
|
||||||
if (!unreadIndex || unreadCount === 0) {
|
if (!unreadIndex || unreadCount === 0) {
|
||||||
this.setState({
|
if(state.unreadIndex.neq(bigInt.zero)) {
|
||||||
unreadIndex: bigInt.zero
|
this.setState({
|
||||||
});
|
unreadIndex: bigInt.zero
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -122,8 +124,8 @@ class ChatWindow extends Component<
|
|||||||
dismissedInitialUnread() {
|
dismissedInitialUnread() {
|
||||||
const { unreadCount, graph } = this.props;
|
const { unreadCount, graph } = this.props;
|
||||||
|
|
||||||
return this.state.unreadIndex.neq(bigInt.zero) &&
|
return this.state.unreadIndex.eq(bigInt.zero) ? unreadCount > graph.size :
|
||||||
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowBlur() {
|
handleWindowBlur() {
|
||||||
@ -133,12 +135,12 @@ class ChatWindow extends Component<
|
|||||||
handleWindowFocus() {
|
handleWindowFocus() {
|
||||||
this.setState({ idle: false });
|
this.setState({ idle: false });
|
||||||
if (this.virtualList?.window?.scrollTop === 0) {
|
if (this.virtualList?.window?.scrollTop === 0) {
|
||||||
this.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
||||||
const { history, graph, unreadCount, graphSize, station } = this.props;
|
const { graph, unreadCount, graphSize, station } = this.props;
|
||||||
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
||||||
this.unreadSet = true;
|
this.unreadSet = true;
|
||||||
}
|
}
|
||||||
@ -150,8 +152,8 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
if(this.unreadSet &&
|
if(this.unreadSet &&
|
||||||
this.dismissedInitialUnread() &&
|
this.dismissedInitialUnread() &&
|
||||||
this.virtualList?.startOffset() < 5) {
|
this.virtualList!.startOffset() < 5) {
|
||||||
this.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -169,7 +171,7 @@ class ChatWindow extends Component<
|
|||||||
stayLockedIfActive() {
|
stayLockedIfActive() {
|
||||||
if (this.virtualList && !this.state.idle) {
|
if (this.virtualList && !this.state.idle) {
|
||||||
this.virtualList.resetScroll();
|
this.virtualList.resetScroll();
|
||||||
this.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,45 +190,6 @@ class ChatWindow extends Component<
|
|||||||
this.virtualList?.scrollToIndex(this.state.unreadIndex);
|
this.virtualList?.scrollToIndex(this.state.unreadIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissUnread() {
|
|
||||||
const { association } = this.props;
|
|
||||||
if (this.state.fetchPending) return;
|
|
||||||
if (this.props.unreadCount === 0) return;
|
|
||||||
this.props.api.hark.markCountAsRead(association, '/', 'message');
|
|
||||||
}
|
|
||||||
|
|
||||||
setActive = () => {
|
|
||||||
if(this.state.idle) {
|
|
||||||
this.setState({ idle: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchMessages = async (newer: boolean): Promise<boolean> => {
|
|
||||||
const { api, station, graph } = this.props;
|
|
||||||
const pageSize = 100;
|
|
||||||
|
|
||||||
const [, , ship, name] = station.split('/');
|
|
||||||
const expectedSize = graph.size + pageSize;
|
|
||||||
if (newer) {
|
|
||||||
const [index] = graph.peekLargest()!;
|
|
||||||
await api.graph.getYoungerSiblings(
|
|
||||||
ship,
|
|
||||||
name,
|
|
||||||
pageSize,
|
|
||||||
`/${index.toString()}`
|
|
||||||
);
|
|
||||||
return expectedSize !== graph.size;
|
|
||||||
} else {
|
|
||||||
const [index] = graph.peekSmallest()!;
|
|
||||||
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
|
||||||
const done = expectedSize !== graph.size;
|
|
||||||
if(done) {
|
|
||||||
this.calculateUnreadIndex();
|
|
||||||
}
|
|
||||||
return done;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
||||||
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
||||||
this.setState({ idle: true });
|
this.setState({ idle: true });
|
||||||
@ -237,26 +200,21 @@ class ChatWindow extends Component<
|
|||||||
renderer = React.forwardRef(({ index, scrollWindow }, ref) => {
|
renderer = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
const {
|
const {
|
||||||
api,
|
api,
|
||||||
association,
|
|
||||||
group,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
graph,
|
graph,
|
||||||
history,
|
onReply,
|
||||||
groups,
|
getPermalink,
|
||||||
associations,
|
dismissUnread,
|
||||||
onReply
|
isAdmin,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { unreadMarkerRef } = this;
|
const permalink = getPermalink(index);
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
association,
|
|
||||||
group,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
api,
|
||||||
groups,
|
onReply,
|
||||||
associations,
|
permalink,
|
||||||
onReply
|
dismissUnread,
|
||||||
|
isAdmin
|
||||||
};
|
};
|
||||||
|
|
||||||
const msg = graph.get(index)?.post;
|
const msg = graph.get(index)?.post;
|
||||||
@ -275,10 +233,10 @@ class ChatWindow extends Component<
|
|||||||
graph.peekLargest()?.[0] ?? bigInt.zero
|
graph.peekLargest()?.[0] ?? bigInt.zero
|
||||||
);
|
);
|
||||||
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
|
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
|
||||||
const keys = graph.keys().reverse();
|
const keys = graph.keys();
|
||||||
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
||||||
const prevIdx = keys[graphIdx + 1];
|
const prevIdx = keys[graphIdx - 1];
|
||||||
const nextIdx = keys[graphIdx - 1];
|
const nextIdx = keys[graphIdx + 1];
|
||||||
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
||||||
const props = {
|
const props = {
|
||||||
highlighted,
|
highlighted,
|
||||||
@ -305,32 +263,13 @@ class ChatWindow extends Component<
|
|||||||
const {
|
const {
|
||||||
unreadCount,
|
unreadCount,
|
||||||
api,
|
api,
|
||||||
association,
|
|
||||||
group,
|
|
||||||
graph,
|
graph,
|
||||||
history,
|
|
||||||
groups,
|
|
||||||
associations,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
pendingSize,
|
pendingSize = 0,
|
||||||
onReply,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const unreadMarkerRef = this.unreadMarkerRef;
|
|
||||||
const messageProps = {
|
|
||||||
association,
|
|
||||||
group,
|
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
|
||||||
associations
|
|
||||||
};
|
|
||||||
const unreadMsg = graph.get(this.state.unreadIndex);
|
const unreadMsg = graph.get(this.state.unreadIndex);
|
||||||
|
|
||||||
// hack to force a re-render when we toggle showing contact
|
|
||||||
const contactsModified =
|
|
||||||
showOurContact ? 0 : 100;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col height='100%' overflow='hidden' position='relative'>
|
<Col height='100%' overflow='hidden' position='relative'>
|
||||||
{ this.dismissedInitialUnread() &&
|
{ this.dismissedInitialUnread() &&
|
||||||
@ -343,34 +282,29 @@ class ChatWindow extends Component<
|
|||||||
? false
|
? false
|
||||||
: unreadMsg
|
: unreadMsg
|
||||||
}
|
}
|
||||||
dismissUnread={this.dismissUnread}
|
dismissUnread={this.props.dismissUnread}
|
||||||
onClick={this.scrollToUnread}
|
onClick={this.scrollToUnread}
|
||||||
/>)}
|
/>)}
|
||||||
<VirtualScroller
|
<VirtualScroller<GraphNode>
|
||||||
ref={(list) => {
|
ref={(list) => {
|
||||||
this.virtualList = list;
|
this.virtualList = list;
|
||||||
}}
|
}}
|
||||||
offset={unreadCount}
|
offset={unreadCount}
|
||||||
origin='bottom'
|
origin='bottom'
|
||||||
style={virtScrollerStyle}
|
style={virtScrollerStyle}
|
||||||
onStartReached={this.setActive}
|
|
||||||
onBottomLoaded={this.onBottomLoaded}
|
onBottomLoaded={this.onBottomLoaded}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
data={graph}
|
data={graph}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
pendingSize={pendingSize + contactsModified}
|
pendingSize={pendingSize}
|
||||||
id={association.resource}
|
|
||||||
averageHeight={22}
|
averageHeight={22}
|
||||||
renderer={this.renderer}
|
renderer={this.renderer}
|
||||||
loadRows={this.fetchMessages}
|
loadRows={this.props.fetchMessages}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withState(ChatWindow, [
|
|
||||||
[useGroupState, ['groups']],
|
export default ChatWindow
|
||||||
[useMetadataState, ['associations']],
|
|
||||||
[useGraphState, ['pendingSize']]
|
|
||||||
]);
|
|
||||||
|
@ -40,27 +40,27 @@ export const ShareProfile = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
if(group.hidden && recipients.length > 0) {
|
if(typeof recipients === 'string') {
|
||||||
await api.contacts.allowShips(recipients);
|
const [,,ship,name] = recipients.split('/');
|
||||||
await Promise.all(recipients.map(r => api.contacts.share(r)))
|
|
||||||
setShowBanner(false);
|
|
||||||
} else if (!group.hidden) {
|
|
||||||
const [,,ship,name] = groupPath.split('/');
|
|
||||||
await api.contacts.allowGroup(ship,name);
|
await api.contacts.allowGroup(ship,name);
|
||||||
if(ship !== `~${window.ship}`) {
|
if(ship !== `~${window.ship}`) {
|
||||||
await api.contacts.share(ship);
|
await api.contacts.share(ship);
|
||||||
}
|
}
|
||||||
setShowBanner(false);
|
} else if(recipients.length > 0) {
|
||||||
}
|
await api.contacts.allowShips(recipients);
|
||||||
|
await Promise.all(recipients.map(r => api.contacts.share(r)))
|
||||||
|
}
|
||||||
|
props.onShare();
|
||||||
};
|
};
|
||||||
|
|
||||||
return showBanner ? (
|
return props.recipients?.length > 0 ? (
|
||||||
<Row
|
<Row
|
||||||
height="48px"
|
height="48px"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
borderBottom={1}
|
borderBottom={1}
|
||||||
borderColor="lightGray"
|
borderColor="lightGray"
|
||||||
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Row pl={3} alignItems="center">
|
<Row pl={3} alignItems="center">
|
||||||
{image}
|
{image}
|
||||||
|
@ -162,6 +162,7 @@ export default class ChatEditor extends Component {
|
|||||||
editor.showHint(['test', 'foo']);
|
editor.showHint(['test', 'foo']);
|
||||||
}
|
}
|
||||||
if (this.state.message !== '' && value == '') {
|
if (this.state.message !== '' && value == '') {
|
||||||
|
this.props.changeEvent(value);
|
||||||
this.setState({
|
this.setState({
|
||||||
message: value
|
message: value
|
||||||
});
|
});
|
||||||
@ -169,6 +170,7 @@ export default class ChatEditor extends Component {
|
|||||||
if (value == this.props.message || value == '' || value == ' ') {
|
if (value == this.props.message || value == '' || value == ' ') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.props.changeEvent(value);
|
||||||
this.setState({
|
this.setState({
|
||||||
message: value
|
message: value
|
||||||
});
|
});
|
||||||
|
@ -8,22 +8,11 @@ import Timestamp from '~/views/components/Timestamp';
|
|||||||
export const UnreadNotice = (props) => {
|
export const UnreadNotice = (props) => {
|
||||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||||
|
|
||||||
if (!unreadMsg || unreadCount === 0) {
|
if (unreadCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
const stamp = unreadMsg && moment.unix(unreadMsg.post['time-sent'] / 1000);
|
||||||
|
|
||||||
let datestamp = moment
|
|
||||||
.unix(unreadMsg.post['time-sent'] / 1000)
|
|
||||||
.format('YYYY.M.D');
|
|
||||||
const timestamp = moment
|
|
||||||
.unix(unreadMsg.post['time-sent'] / 1000)
|
|
||||||
.format('HH:mm');
|
|
||||||
|
|
||||||
if (datestamp === moment().format('YYYY.M.D')) {
|
|
||||||
datestamp = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -52,15 +41,20 @@ export const UnreadNotice = (props) => {
|
|||||||
whiteSpace='pre'
|
whiteSpace='pre'
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
display='flex'
|
display='flex'
|
||||||
cursor='pointer'
|
cursor={unreadMsg ? 'pointer' : null}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
{unreadCount} new message{unreadCount > 1 ? 's' : ''}
|
||||||
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
{unreadMsg && (
|
||||||
|
<>
|
||||||
|
{' '}since{' '}
|
||||||
|
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon
|
<Icon
|
||||||
icon='X'
|
icon='X'
|
||||||
ml='4'
|
ml={unreadMsg ? 4 : 1}
|
||||||
color='black'
|
color='black'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
textAlign='right'
|
textAlign='right'
|
||||||
|
@ -220,7 +220,7 @@ export default function LaunchApp(props) {
|
|||||||
<NewGroup {...props} />
|
<NewGroup {...props} />
|
||||||
</ModalButton>
|
</ModalButton>
|
||||||
<ModalButton
|
<ModalButton
|
||||||
icon="Boot"
|
icon="BootNode"
|
||||||
bg="washedGray"
|
bg="washedGray"
|
||||||
color="black"
|
color="black"
|
||||||
text="Join Group"
|
text="Join Group"
|
||||||
|
@ -23,7 +23,7 @@ type LinkResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function LinkResource(props: LinkResourceProps) {
|
export function LinkResource(props: LinkResourceProps) {
|
||||||
const {
|
const {
|
||||||
|
@ -6,7 +6,7 @@ import React, {
|
|||||||
Component,
|
Component,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { Col, Text } from "@tlon/indigo-react";
|
import { Box, Col, Text } from "@tlon/indigo-react";
|
||||||
import bigInt from "big-integer";
|
import bigInt from "big-integer";
|
||||||
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
|
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
return isWriter(group, association.resource);
|
return isWriter(group, association.resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ index, scrollWindow }) => {
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
const { association, graph, api } = props;
|
const { association, graph, api } = props;
|
||||||
const [, , ship, name] = association.resource.split("/");
|
const [, , ship, name] = association.resource.split("/");
|
||||||
@ -80,12 +80,14 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<LinkItem {...linkProps} />
|
<LinkItem ref={ref} {...linkProps} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <LinkItem key={index.toString()} {...linkProps} />;
|
return <Box ref={ref}>
|
||||||
};
|
<LinkItem ref={ref} key={index.toString()} {...linkProps} />;
|
||||||
|
</Box>
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { graph, api, association } = this.props;
|
const { graph, api, association } = this.props;
|
||||||
@ -136,4 +138,4 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LinkWindow;
|
export default LinkWindow;
|
||||||
|
@ -19,7 +19,7 @@ interface LinkItemProps {
|
|||||||
node: GraphNode;
|
node: GraphNode;
|
||||||
association: Association;
|
association: Association;
|
||||||
resource: string; api: GlobalApi; group: Group; path: string; }
|
resource: string; api: GlobalApi; group: Group; path: string; }
|
||||||
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactElement => {
|
||||||
const {
|
const {
|
||||||
association,
|
association,
|
||||||
node,
|
node,
|
||||||
@ -30,7 +30,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
||||||
const index = node.post.index.split('/')[1];
|
const index = node.post.index.split('/')[1];
|
||||||
|
|
||||||
@ -86,7 +85,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
permalink,
|
permalink,
|
||||||
'Copy reference'
|
'Copy reference'
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteLink = () => {
|
const deleteLink = () => {
|
||||||
if (confirm('Are you sure you want to delete this link?')) {
|
if (confirm('Are you sure you want to delete this link?')) {
|
||||||
api.graph.removeNodes(`~${ship}`, name, [node.post.index]);
|
api.graph.removeNodes(`~${ship}`, name, [node.post.index]);
|
||||||
@ -167,9 +166,11 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||||
<Author
|
<Author
|
||||||
showImage
|
showImage
|
||||||
|
isRelativeTime
|
||||||
ship={author}
|
ship={author}
|
||||||
date={node.post['time-sent']}
|
date={node.post['time-sent']}
|
||||||
group={group}
|
group={group}
|
||||||
|
lineHeight="1"
|
||||||
/>
|
/>
|
||||||
<Box ml="auto">
|
<Box ml="auto">
|
||||||
<Link
|
<Link
|
||||||
@ -208,5 +209,5 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
|
|
||||||
</Row>
|
</Row>
|
||||||
</Box>);
|
</Box>);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
@ -92,6 +92,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
<StatelessAsyncAction
|
<StatelessAsyncAction
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
color="black"
|
color="black"
|
||||||
|
backgroundColor="white"
|
||||||
onClick={onReadAll}
|
onClick={onReadAll}
|
||||||
>
|
>
|
||||||
Mark All Read
|
Mark All Read
|
||||||
@ -106,7 +107,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
{!view && <Inbox
|
{!view && <Inbox
|
||||||
pendingJoin={pendingJoin}
|
pendingJoin={pendingJoin}
|
||||||
{...props}
|
{...props}
|
||||||
filter={filter.groups}
|
filter={filter.groups}
|
||||||
/>}
|
/>}
|
||||||
</Col>
|
</Col>
|
||||||
</Body>
|
</Body>
|
||||||
|
@ -4,6 +4,7 @@ import ChatMessage from "../chat/components/ChatMessage";
|
|||||||
import { Association, GraphNode, Post, Group } from "@urbit/api";
|
import { Association, GraphNode, Post, Group } from "@urbit/api";
|
||||||
import { useGroupForAssoc } from "~/logic/state/group";
|
import { useGroupForAssoc } from "~/logic/state/group";
|
||||||
import { MentionText } from "~/views/components/MentionText";
|
import { MentionText } from "~/views/components/MentionText";
|
||||||
|
import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide';
|
||||||
import Author from "~/views/components/Author";
|
import Author from "~/views/components/Author";
|
||||||
import { NoteContent } from "../publish/components/Note";
|
import { NoteContent } from "../publish/components/Note";
|
||||||
import { PostContent } from "~/views/landscape/components/Home/Post/PostContent";
|
import { PostContent } from "~/views/landscape/components/Home/Post/PostContent";
|
||||||
@ -31,7 +32,7 @@ function TranscludedLinkNode(props: {
|
|||||||
return <PermalinkEmbed transcluded={transcluded + 1} api={api} link={permalink} association={assoc} />
|
return <PermalinkEmbed transcluded={transcluded + 1} api={api} link={permalink} association={assoc} />
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderRadius="2" p="2" bg="scales.black05">
|
<Box borderRadius="2" p="2" bg="scales.black05">
|
||||||
<Anchor underline={false} target="_blank" color="black" href={link.url}>
|
<Anchor underline={false} target="_blank" color="black" href={link.url}>
|
||||||
@ -74,11 +75,11 @@ function TranscludedComment(props: {
|
|||||||
group={group}
|
group={group}
|
||||||
/>
|
/>
|
||||||
<Box p="2">
|
<Box p="2">
|
||||||
<MentionText
|
<GraphContentWide
|
||||||
api={api}
|
api={api}
|
||||||
transcluded={transcluded}
|
transcluded={transcluded}
|
||||||
content={comment.post.contents}
|
post={comment.post}
|
||||||
group={group}
|
showOurContact={false}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Col>
|
</Col>
|
||||||
@ -200,8 +201,8 @@ export function TranscludedNode(props: {
|
|||||||
<TranscludedPost
|
<TranscludedPost
|
||||||
api={props.api}
|
api={props.api}
|
||||||
post={node.post}
|
post={node.post}
|
||||||
group={group}
|
group={group}
|
||||||
transcluded={transcluded}
|
transcluded={transcluded}
|
||||||
/>)
|
/>)
|
||||||
;
|
;
|
||||||
default:
|
default:
|
||||||
|
@ -18,7 +18,7 @@ import { GroupLink } from "~/views/components/GroupLink";
|
|||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { getModuleIcon } from "~/logic/lib/util";
|
import { getModuleIcon } from "~/logic/lib/util";
|
||||||
import useMetadataState from "~/logic/state/metadata";
|
import useMetadataState from "~/logic/state/metadata";
|
||||||
import { Association, resourceFromPath } from "@urbit/api";
|
import { Association, resourceFromPath, GraphNode } from "@urbit/api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import useGraphState from "~/logic/state/graph";
|
import useGraphState from "~/logic/state/graph";
|
||||||
import { GraphNodeContent } from "../notifications/graph";
|
import { GraphNodeContent } from "../notifications/graph";
|
||||||
@ -51,7 +51,7 @@ function GraphPermalink(
|
|||||||
const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props;
|
const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props;
|
||||||
const { ship, name } = resourceFromPath(graph);
|
const { ship, name } = resourceFromPath(graph);
|
||||||
const node = useGraphState(
|
const node = useGraphState(
|
||||||
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [
|
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index] as GraphNode, [
|
||||||
graph,
|
graph,
|
||||||
index,
|
index,
|
||||||
])
|
])
|
||||||
@ -63,7 +63,7 @@ function GraphPermalink(
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
useVirtualResizeProp(node)
|
useVirtualResizeProp(!!node)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (pending || !index) {
|
if (pending || !index) {
|
||||||
|
@ -11,7 +11,7 @@ type PublishResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function PublishResource(props: PublishResourceProps) {
|
export function PublishResource(props: PublishResourceProps) {
|
||||||
const { association, api, baseUrl, notebooks } = props;
|
const { association, api, baseUrl, notebooks } = props;
|
||||||
|
@ -73,14 +73,14 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
if (window.ship === note?.post?.author) {
|
if (window.ship === note?.post?.author) {
|
||||||
adminLinks.push(
|
adminLinks.push(
|
||||||
<Link to={`${baseUrl}/edit`}>
|
<Link to={`${baseUrl}/edit`}>
|
||||||
<Action>Update</Action>
|
<Action backgroundColor="white">Update</Action>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.ship === note?.post?.author || ourRole === "admin") {
|
if (window.ship === note?.post?.author || ourRole === "admin") {
|
||||||
adminLinks.push(
|
adminLinks.push(
|
||||||
<Action destructive onClick={deletePost}>
|
<Action backgroundColor="white" destructive onClick={deletePost}>
|
||||||
Delete
|
Delete
|
||||||
</Action>
|
</Action>
|
||||||
)
|
)
|
||||||
@ -115,11 +115,12 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
<Row alignItems="center">
|
<Row alignItems="center">
|
||||||
<Author
|
<Author
|
||||||
showImage
|
showImage
|
||||||
|
isRelativeTime
|
||||||
ship={post?.author}
|
ship={post?.author}
|
||||||
date={post?.['time-sent']}
|
date={post?.['time-sent']}
|
||||||
group={group}
|
group={group}
|
||||||
>
|
>
|
||||||
<Row px="2" gapX="2" alignItems="flex-end">
|
<Row px="2" gapX="2" alignItems="flex-end" height="14px">
|
||||||
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
||||||
{adminLinks}
|
{adminLinks}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -44,7 +44,7 @@ export function PostForm(props: PostFormProps) {
|
|||||||
validateOnBlur
|
validateOnBlur
|
||||||
>
|
>
|
||||||
<Form style={{ display: 'contents' }}>
|
<Form style={{ display: 'contents' }}>
|
||||||
<Row flexShrink='0' flexDirection={['column-reverse', 'row']} mb={4} gapX={4} justifyContent='space-between'>
|
<Row flexShrink={0} flexDirection={['column-reverse', 'row']} mb={4} gapX={4} justifyContent='space-between'>
|
||||||
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
||||||
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
|
@ -107,7 +107,7 @@ export default function SettingsScreen(props: any) {
|
|||||||
</Text>
|
</Text>
|
||||||
<Col>
|
<Col>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
text='Notifications'
|
text='Notifications'
|
||||||
hash='notifications'
|
hash='notifications'
|
||||||
/>
|
/>
|
||||||
|
@ -24,6 +24,7 @@ interface AuthorProps {
|
|||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
api?: GlobalApi;
|
api?: GlobalApi;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
lineHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line max-lines-per-function
|
// eslint-disable-next-line max-lines-per-function
|
||||||
@ -38,10 +39,11 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
group,
|
group,
|
||||||
isRelativeTime,
|
isRelativeTime,
|
||||||
dontShowTime,
|
dontShowTime,
|
||||||
|
lineHeight = 'tall',
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const time = props.time || false;
|
const time = props.time || props.date || false;
|
||||||
const size = props.size || 16;
|
const size = props.size || 16;
|
||||||
const sigilPadding = props.sigilPadding || 2;
|
const sigilPadding = props.sigilPadding || 2;
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
) : sigil;
|
) : sigil;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row height="20px" {...rest} alignItems='center' width='auto'>
|
<Row {...rest} alignItems='center' width='auto'>
|
||||||
<Box
|
<Box
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -110,7 +112,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
color='black'
|
color='black'
|
||||||
fontSize='1'
|
fontSize='1'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
lineHeight='tall'
|
lineHeight={lineHeight}
|
||||||
fontFamily={showNickname ? 'sans' : 'mono'}
|
fontFamily={showNickname ? 'sans' : 'mono'}
|
||||||
fontWeight={showNickname ? '500' : '400'}
|
fontWeight={showNickname ? '500' : '400'}
|
||||||
mr={showNickname ? 0 : "2px"}
|
mr={showNickname ? 0 : "2px"}
|
||||||
@ -121,6 +123,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
</Box>
|
</Box>
|
||||||
{ !dontShowTime && time && (
|
{ !dontShowTime && time && (
|
||||||
<Timestamp
|
<Timestamp
|
||||||
|
height="fit-content"
|
||||||
relative={isRelativeTime}
|
relative={isRelativeTime}
|
||||||
stamp={stamp}
|
stamp={stamp}
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
|
@ -115,8 +115,8 @@ export function ChipInput(props: ChipInputProps): ReactElement {
|
|||||||
<Input
|
<Input
|
||||||
width="auto"
|
width="auto"
|
||||||
height="24px"
|
height="24px"
|
||||||
flexShrink="1"
|
flexShrink={1}
|
||||||
flexGrow="1"
|
flexGrow={1}
|
||||||
pl="0"
|
pl="0"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -73,7 +73,7 @@ export function ColorInput(props: ColorInputProps) {
|
|||||||
height='100%'
|
height='100%'
|
||||||
alignSelf='stretch'
|
alignSelf='stretch'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={`#${padded}`}
|
value={padded}
|
||||||
disabled={disabled || false}
|
disabled={disabled || false}
|
||||||
type='color'
|
type='color'
|
||||||
opacity={0}
|
opacity={0}
|
||||||
|
@ -35,6 +35,7 @@ interface CommentItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CommentItem(props: CommentItemProps): ReactElement {
|
export function CommentItem(props: CommentItemProps): ReactElement {
|
||||||
|
let { highlighted } = props;
|
||||||
const { ship, name, api, comment, group } = props;
|
const { ship, name, api, comment, group } = props;
|
||||||
const association = useMetadataState(
|
const association = useMetadataState(
|
||||||
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
|
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
|
||||||
@ -47,6 +48,16 @@ export function CommentItem(props: CommentItemProps): ReactElement {
|
|||||||
await api.graph.removeNodes(ship, name, [comment.post?.index]);
|
await api.graph.removeNodes(ship, name, [comment.post?.index]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ourMention = post?.contents?.some((e) => {
|
||||||
|
return e?.mention && e?.mention === window.ship;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!highlighted) {
|
||||||
|
if (ourMention) {
|
||||||
|
highlighted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const commentIndexArray = (comment.post?.index || '/').split('/');
|
const commentIndexArray = (comment.post?.index || '/').split('/');
|
||||||
const commentIndex = commentIndexArray[commentIndexArray.length - 1];
|
const commentIndex = commentIndexArray[commentIndexArray.length - 1];
|
||||||
|
|
||||||
@ -95,6 +106,7 @@ export function CommentItem(props: CommentItemProps): ReactElement {
|
|||||||
date={post?.['time-sent']}
|
date={post?.['time-sent']}
|
||||||
unread={props.unread}
|
unread={props.unread}
|
||||||
group={group}
|
group={group}
|
||||||
|
isRelativeTime
|
||||||
>
|
>
|
||||||
<Row px="2" gapX="2" height="18px">
|
<Row px="2" gapX="2" height="18px">
|
||||||
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
||||||
@ -106,7 +118,7 @@ export function CommentItem(props: CommentItemProps): ReactElement {
|
|||||||
borderRadius="1"
|
borderRadius="1"
|
||||||
p="1"
|
p="1"
|
||||||
mb="1"
|
mb="1"
|
||||||
backgroundColor={props.highlighted ? 'washedBlue' : 'white'}
|
backgroundColor={highlighted ? 'washedBlue' : 'white'}
|
||||||
transcluded={0}
|
transcluded={0}
|
||||||
api={api}
|
api={api}
|
||||||
post={post}
|
post={post}
|
||||||
|
@ -130,7 +130,7 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
|
|||||||
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';
|
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col {...rest}>
|
<Col {...rest} minWidth='0'>
|
||||||
{( !editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
|
{( !editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
|
||||||
{( editCommentId ? (
|
{( editCommentId ? (
|
||||||
<CommentInput
|
<CommentInput
|
||||||
|
@ -25,6 +25,7 @@ interface DropdownProps {
|
|||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
width?: string;
|
width?: string;
|
||||||
dropWidth?: string;
|
dropWidth?: string;
|
||||||
|
flexShrink?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClickBox = styled(Box)`
|
const ClickBox = styled(Box)`
|
||||||
@ -39,7 +40,7 @@ const DropdownOptions = styled(Box)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function Dropdown(props: DropdownProps): ReactElement {
|
export function Dropdown(props: DropdownProps): ReactElement {
|
||||||
const { children, options, offsetX = 0, offsetY = 0 } = props;
|
const { children, options, offsetX = 0, offsetY = 0, flexShrink = 1 } = props;
|
||||||
const dropdownRef = useRef<HTMLElement>(null);
|
const dropdownRef = useRef<HTMLElement>(null);
|
||||||
const anchorRef = useRef<HTMLElement>(null);
|
const anchorRef = useRef<HTMLElement>(null);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@ -47,6 +48,9 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
|||||||
const [coords, setCoords] = useState({});
|
const [coords, setCoords] = useState({});
|
||||||
|
|
||||||
const updatePos = useCallback(() => {
|
const updatePos = useCallback(() => {
|
||||||
|
if(!anchorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
||||||
if(newCoords) {
|
if(newCoords) {
|
||||||
setCoords(newCoords);
|
setCoords(newCoords);
|
||||||
@ -86,7 +90,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexShrink={props?.flexShrink ? props.flexShrink : 1} position={open ? 'relative' : 'static'} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
<Box flexShrink={flexShrink} position={open ? 'relative' : 'static'} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
||||||
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
||||||
{children}
|
{children}
|
||||||
</ClickBox>
|
</ClickBox>
|
||||||
|
@ -23,6 +23,7 @@ import RichText from './RichText';
|
|||||||
import { ProfileStatus } from './ProfileStatus';
|
import { ProfileStatus } from './ProfileStatus';
|
||||||
import useSettingsState from '~/logic/state/settings';
|
import useSettingsState from '~/logic/state/settings';
|
||||||
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
||||||
|
import {useCopy} from '~/logic/lib/useCopy';
|
||||||
import {useContact} from '~/logic/state/contact';
|
import {useContact} from '~/logic/state/contact';
|
||||||
import {useHistory} from 'react-router-dom';
|
import {useHistory} from 'react-router-dom';
|
||||||
import {Portal} from './Portal';
|
import {Portal} from './Portal';
|
||||||
@ -59,6 +60,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
|||||||
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
|
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
|
||||||
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
|
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
|
||||||
const isOwn = useMemo(() => window.ship === ship, [ship]);
|
const isOwn = useMemo(() => window.ship === ship, [ship]);
|
||||||
|
const { copyDisplay, doCopy, didCopy } = useCopy(`~${ship}`);
|
||||||
|
|
||||||
const contact = useContact(`~${ship}`)
|
const contact = useContact(`~${ship}`)
|
||||||
const color = `#${uxToHex(contact?.color ?? '0x0')}`;
|
const color = `#${uxToHex(contact?.color ?? '0x0')}`;
|
||||||
@ -188,9 +190,18 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
|||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
whiteSpace='pre'
|
whiteSpace='pre'
|
||||||
marginBottom='0'
|
marginBottom='0'
|
||||||
|
cursor='pointer'
|
||||||
|
display={didCopy ? 'none' : 'block'}
|
||||||
|
onClick={doCopy}
|
||||||
>
|
>
|
||||||
{showNickname ? contact?.nickname : cite(ship)}
|
{showNickname ? contact?.nickname : cite(ship)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontWeight='600'
|
||||||
|
marginBottom='0'
|
||||||
|
>
|
||||||
|
{copyDisplay}
|
||||||
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
{isOwn ? (
|
{isOwn ? (
|
||||||
<ProfileStatus
|
<ProfileStatus
|
||||||
|
@ -48,12 +48,14 @@ class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
unfold: props.unfold || false,
|
unfold: props.unfold || false,
|
||||||
embed: undefined,
|
embed: undefined,
|
||||||
noCors: false
|
noCors: false,
|
||||||
|
showArrow: false
|
||||||
};
|
};
|
||||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||||
this.loadOembed = this.loadOembed.bind(this);
|
this.loadOembed = this.loadOembed.bind(this);
|
||||||
this.wrapInLink = this.wrapInLink.bind(this);
|
this.wrapInLink = this.wrapInLink.bind(this);
|
||||||
this.onError = this.onError.bind(this);
|
this.onError = this.onError.bind(this);
|
||||||
|
this.toggleArrow = this.toggleArrow.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
save = () => {
|
save = () => {
|
||||||
@ -128,7 +130,7 @@ return;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null) {
|
wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false, noOp = false) {
|
||||||
const { style } = this.props;
|
const { style } = this.props;
|
||||||
return (
|
return (
|
||||||
<Box borderRadius="1" backgroundColor="washedGray" maxWidth="min(100%, 20rem)">
|
<Box borderRadius="1" backgroundColor="washedGray" maxWidth="min(100%, 20rem)">
|
||||||
@ -145,8 +147,8 @@ return;
|
|||||||
)}
|
)}
|
||||||
<BaseAnchor
|
<BaseAnchor
|
||||||
display="flex"
|
display="flex"
|
||||||
p="2"
|
p={flushPadding ? 0 : 2}
|
||||||
onClick={(e) => { e.stopPropagation(); }}
|
onClick={(e) => { noOp ? e.preventDefault() : e.stopPropagation() }}
|
||||||
href={this.props.url}
|
href={this.props.url}
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
@ -157,7 +159,8 @@ return;
|
|||||||
style={{ color: 'inherit', textDecoration: 'none', ...style }}
|
style={{ color: 'inherit', textDecoration: 'none', ...style }}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
cursor={noOp ? 'default' : 'pointer'}
|
||||||
|
>
|
||||||
{contents}
|
{contents}
|
||||||
</BaseAnchor>
|
</BaseAnchor>
|
||||||
</Row>
|
</Row>
|
||||||
@ -171,11 +174,16 @@ return;
|
|||||||
this.setState({ noCors: true });
|
this.setState({ noCors: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleArrow() {
|
||||||
|
this.setState({showArrow: !this.state.showArrow})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
remoteContentPolicy,
|
remoteContentPolicy,
|
||||||
url,
|
url,
|
||||||
text,
|
text,
|
||||||
|
transcluded,
|
||||||
renderUrl = true,
|
renderUrl = true,
|
||||||
imageProps = {},
|
imageProps = {},
|
||||||
audioProps = {},
|
audioProps = {},
|
||||||
@ -192,22 +200,60 @@ return;
|
|||||||
const isVideo = VIDEO_REGEX.test(url);
|
const isVideo = VIDEO_REGEX.test(url);
|
||||||
const isOembed = hasProvider(url);
|
const isOembed = hasProvider(url);
|
||||||
|
|
||||||
|
const isTranscluded = () => {
|
||||||
|
return transcluded;
|
||||||
|
}
|
||||||
|
|
||||||
if (isImage && remoteContentPolicy.imageShown) {
|
if (isImage && remoteContentPolicy.imageShown) {
|
||||||
return this.wrapInLink(
|
return this.wrapInLink(
|
||||||
<BaseImage
|
<Box
|
||||||
{...(noCors ? {} : { crossOrigin: "anonymous" })}
|
position='relative'
|
||||||
referrerPolicy="no-referrer"
|
onMouseEnter={this.toggleArrow}
|
||||||
flexShrink={0}
|
onMouseLeave={this.toggleArrow}
|
||||||
src={url}
|
>
|
||||||
style={style}
|
<BaseAnchor
|
||||||
onLoad={onLoad}
|
position='absolute'
|
||||||
onError={this.onError}
|
top={2}
|
||||||
height="100%"
|
right={2}
|
||||||
width="100%"
|
display={this.state.showArrow ? 'block' : 'none'}
|
||||||
objectFit="contain"
|
target='_blank'
|
||||||
{...imageProps}
|
rel='noopener noreferrer'
|
||||||
{...props}
|
onClick={(e) => {
|
||||||
/>
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
backgroundColor='white'
|
||||||
|
padding={2}
|
||||||
|
borderRadius='50%'
|
||||||
|
display='flex'
|
||||||
|
>
|
||||||
|
<Icon icon='ArrowNorthEast' />
|
||||||
|
</Box>
|
||||||
|
</BaseAnchor>
|
||||||
|
<BaseImage
|
||||||
|
{...(noCors ? {} : { crossOrigin: 'anonymous' })}
|
||||||
|
referrerPolicy='no-referrer'
|
||||||
|
flexShrink={0}
|
||||||
|
src={url}
|
||||||
|
style={style}
|
||||||
|
onLoad={onLoad}
|
||||||
|
onError={this.onError}
|
||||||
|
height='100%'
|
||||||
|
width='100%'
|
||||||
|
objectFit='contain'
|
||||||
|
borderRadius={2}
|
||||||
|
{...imageProps}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Box>,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
isTranscluded()
|
||||||
);
|
);
|
||||||
} else if (isAudio && remoteContentPolicy.audioShown) {
|
} else if (isAudio && remoteContentPolicy.audioShown) {
|
||||||
return (
|
return (
|
||||||
@ -271,7 +317,6 @@ return;
|
|||||||
display={this.state.unfold ? 'block' : 'none'}
|
display={this.state.unfold ? 'block' : 'none'}
|
||||||
className='embed-container'
|
className='embed-container'
|
||||||
style={style}
|
style={style}
|
||||||
flexShrink={0}
|
|
||||||
onLoad={this.onLoad}
|
onLoad={this.onLoad}
|
||||||
{...oembedProps}
|
{...oembedProps}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -83,7 +83,7 @@ const StatusBar = (props) => {
|
|||||||
onClick={() => history.push('/')}
|
onClick={() => history.push('/')}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon icon='Spaces' color='black' />
|
<Icon icon='Dashboard' color='black' />
|
||||||
</Button>
|
</Button>
|
||||||
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||||
{!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && (
|
{!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && (
|
||||||
@ -134,7 +134,7 @@ const StatusBar = (props) => {
|
|||||||
mr={2}
|
mr={2}
|
||||||
onClick={() => props.history.push('/~landscape/messages')}
|
onClick={() => props.history.push('/~landscape/messages')}
|
||||||
>
|
>
|
||||||
<Icon icon='Users' />
|
<Icon icon='Messages' />
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropWidth='250px'
|
dropWidth='250px'
|
||||||
|
@ -12,6 +12,7 @@ export type TimestampProps = BoxProps & {
|
|||||||
date?: boolean;
|
date?: boolean;
|
||||||
time?: boolean;
|
time?: boolean;
|
||||||
relative?: boolean;
|
relative?: boolean;
|
||||||
|
height?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Timestamp = (props: TimestampProps): ReactElement | null => {
|
const Timestamp = (props: TimestampProps): ReactElement | null => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component, useCallback } from 'react';
|
import React, { Component, useCallback, SyntheticEvent } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
import normalizeWheel from 'normalize-wheel';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
@ -76,7 +76,7 @@ interface VirtualScrollerProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface VirtualScrollerState<T> {
|
interface VirtualScrollerState<T> {
|
||||||
visibleItems: BigIntOrderedMap<T>;
|
visibleItems: BigInteger[];
|
||||||
scrollbar: number;
|
scrollbar: number;
|
||||||
loaded: {
|
loaded: {
|
||||||
top: boolean;
|
top: boolean;
|
||||||
@ -91,10 +91,9 @@ const log = (level: LogLevel, message: string) => {
|
|||||||
if(logLevel.includes(level)) {
|
if(logLevel.includes(level)) {
|
||||||
console.log(`[${level}]: ${message}`);
|
console.log(`[${level}]: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ZONE_SIZE = IS_IOS ? 10 : 40;
|
const ZONE_SIZE = IS_IOS ? 10 : 80;
|
||||||
|
|
||||||
|
|
||||||
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
||||||
@ -114,7 +113,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
/**
|
/**
|
||||||
* A map of child refs, used to calculate scroll position
|
* A map of child refs, used to calculate scroll position
|
||||||
*/
|
*/
|
||||||
private childRefs = new BigIntOrderedMap<HTMLElement>();
|
private childRefs = new Map<string, HTMLElement>();
|
||||||
|
/**
|
||||||
|
* A set of child refs which have been unmounted
|
||||||
|
*/
|
||||||
|
private orphans = new Set<string>();
|
||||||
/**
|
/**
|
||||||
* If saving, the bottommost visible element that we pin our scroll to
|
* If saving, the bottommost visible element that we pin our scroll to
|
||||||
*/
|
*/
|
||||||
@ -140,10 +143,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
private scrollRef: HTMLElement | null = null;
|
private scrollRef: HTMLElement | null = null;
|
||||||
|
|
||||||
|
private cleanupRefInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(props: VirtualScrollerProps<T>) {
|
constructor(props: VirtualScrollerProps<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visibleItems: new BigIntOrderedMap(),
|
visibleItems: [],
|
||||||
scrollbar: 0,
|
scrollbar: 0,
|
||||||
loaded: {
|
loaded: {
|
||||||
top: false,
|
top: false,
|
||||||
@ -154,18 +159,33 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
this.updateVisible = this.updateVisible.bind(this);
|
this.updateVisible = this.updateVisible.bind(this);
|
||||||
|
|
||||||
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
||||||
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 400) : this.onScroll.bind(this);
|
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 200) : this.onScroll.bind(this);
|
||||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||||
this.setWindow = this.setWindow.bind(this);
|
this.setWindow = this.setWindow.bind(this);
|
||||||
|
this.restore = this.restore.bind(this);
|
||||||
|
this.startOffset = this.startOffset.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateVisible(0);
|
this.updateVisible(0);
|
||||||
this.resetScroll();
|
|
||||||
this.loadTop();
|
this.loadTop();
|
||||||
this.loadBottom();
|
this.loadBottom();
|
||||||
|
this.cleanupRefInterval = setInterval(this.cleanupRefs, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cleanupRefs = () => {
|
||||||
|
if(this.saveDepth > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[...this.orphans].forEach(o => {
|
||||||
|
const index = bigInt(o);
|
||||||
|
this.childRefs.delete(index.toString());
|
||||||
|
});
|
||||||
|
this.orphans.clear();
|
||||||
|
};
|
||||||
|
|
||||||
// manipulate scrollbar manually, to dodge change detection
|
// manipulate scrollbar manually, to dodge change detection
|
||||||
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
||||||
if(!this.window || !this.scrollRef) {
|
if(!this.window || !this.scrollRef) {
|
||||||
@ -186,27 +206,32 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
||||||
const { id, size, data, offset, pendingSize } = this.props;
|
const { id, size, data, offset, pendingSize } = this.props;
|
||||||
const { visibleItems } = this.state;
|
|
||||||
|
|
||||||
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
||||||
if(this.scrollLocked) {
|
if(this.scrollLocked) {
|
||||||
this.updateVisible(0);
|
this.updateVisible(0);
|
||||||
this.resetScroll();
|
this.resetScroll();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||||
|
if(this.cleanupRefInterval) {
|
||||||
|
clearInterval(this.cleanupRefInterval);
|
||||||
|
}
|
||||||
|
this.cleanupRefs();
|
||||||
|
this.childRefs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
startOffset() {
|
startOffset() {
|
||||||
const startIndex = this.state?.visibleItems?.peekLargest()?.[0];
|
const { data } = this.props;
|
||||||
|
const startIndex = this.state.visibleItems?.[0];
|
||||||
if(!startIndex) {
|
if(!startIndex) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
|
const dataList = Array.from(data);
|
||||||
|
const offset = dataList.findIndex(([i]) => i.eq(startIndex))
|
||||||
if(offset === -1) {
|
if(offset === -1) {
|
||||||
// TODO: revisit when we remove nodes for any other reason than
|
// TODO: revisit when we remove nodes for any other reason than
|
||||||
// pending indices being removed
|
// pending indices being removed
|
||||||
@ -226,22 +251,17 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
||||||
|
|
||||||
const { data, onCalculateVisibleItems } = this.props;
|
const { data, onCalculateVisibleItems } = this.props;
|
||||||
const visibleItems = new BigIntOrderedMap<any>(
|
const visibleItems = data.keys().slice(newOffset, newOffset + this.pageSize);
|
||||||
[...data].slice(newOffset, newOffset + this.pageSize)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
visibleItems,
|
visibleItems,
|
||||||
}, () => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.restore();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.restore();
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollKeyMap(): Map<string, number> {
|
scrollKeyMap(): Map<string, number> {
|
||||||
@ -273,7 +293,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
setWindow(element) {
|
setWindow(element) {
|
||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
console.log('resetting window');
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
if (this.window) {
|
if (this.window) {
|
||||||
@ -286,8 +305,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
const { averageHeight } = this.props;
|
const { averageHeight } = this.props;
|
||||||
|
|
||||||
this.window = element;
|
this.window = element;
|
||||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 5.5));
|
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 2));
|
||||||
this.pageDelta = Math.floor(this.pageSize / 3);
|
this.pageDelta = Math.floor(this.pageSize / 4);
|
||||||
if (this.props.origin === 'bottom') {
|
if (this.props.origin === 'bottom') {
|
||||||
element.addEventListener('wheel', (event) => {
|
element.addEventListener('wheel', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -333,7 +352,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onScroll(event: UIEvent) {
|
onScroll(event: SyntheticEvent<HTMLElement, ScrollEvent>) {
|
||||||
this.updateScroll();
|
this.updateScroll();
|
||||||
if(!this.window) {
|
if(!this.window) {
|
||||||
// bail if we're going to adjust scroll anyway
|
// bail if we're going to adjust scroll anyway
|
||||||
@ -348,19 +367,19 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
|
|
||||||
const startOffset = this.startOffset();
|
const startOffset = this.startOffset();
|
||||||
|
|
||||||
|
const scrollEnd = scrollTop + windowHeight;
|
||||||
if (scrollTop < ZONE_SIZE) {
|
if (scrollTop < ZONE_SIZE) {
|
||||||
log('scroll', `Entered start zone ${scrollTop}`);
|
log('scroll', `Entered start zone ${scrollTop}`);
|
||||||
if (startOffset === 0 && onStartReached) {
|
if (startOffset === 0) {
|
||||||
onStartReached();
|
onStartReached && onStartReached();
|
||||||
|
this.scrollLocked = true;
|
||||||
}
|
}
|
||||||
const newOffset = Math.max(0, startOffset - this.pageDelta);
|
const newOffset = Math.max(0, startOffset - this.pageDelta);
|
||||||
if(newOffset < 10) {
|
if(newOffset < 10) {
|
||||||
this.loadBottom();
|
this.loadBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newOffset === 0) {
|
|
||||||
this.scrollLocked = true;
|
|
||||||
}
|
|
||||||
if(newOffset !== startOffset) {
|
if(newOffset !== startOffset) {
|
||||||
this.updateVisible(newOffset);
|
this.updateVisible(newOffset);
|
||||||
}
|
}
|
||||||
@ -394,20 +413,37 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('bail', 'Deep restore');
|
log('bail', 'Deep restore');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(this.scrollLocked) {
|
||||||
|
this.resetScroll();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.savedIndex = null;
|
||||||
|
this.savedDistance = 0;
|
||||||
|
this.saveDepth--;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ref = this.childRefs.get(this.savedIndex.toString())
|
||||||
|
if(!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newScrollTop = this.props.origin === 'top'
|
||||||
|
? this.savedDistance + ref.offsetTop
|
||||||
|
: this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
||||||
|
|
||||||
const ref = this.childRefs.get(this.savedIndex)!;
|
|
||||||
const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
|
||||||
|
|
||||||
this.window.scrollTo(0, newScrollTop);
|
this.window.scrollTo(0, newScrollTop);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.savedIndex = null;
|
this.savedIndex = null;
|
||||||
this.savedDistance = 0;
|
this.savedDistance = 0;
|
||||||
this.saveDepth--;
|
this.saveDepth--;
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToIndex = (index: BigInteger) => {
|
scrollToIndex = (index: BigInteger) => {
|
||||||
let ref = this.childRefs.get(index);
|
let ref = this.childRefs.get(index.toString());
|
||||||
if(!ref) {
|
if(!ref) {
|
||||||
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
||||||
if(offset === -1) {
|
if(offset === -1) {
|
||||||
@ -415,7 +451,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
ref = this.childRefs.get(index);
|
ref = this.childRefs.get(index.toString());
|
||||||
this.savedIndex = null;
|
this.savedIndex = null;
|
||||||
this.savedDistance = 0;
|
this.savedDistance = 0;
|
||||||
this.saveDepth = 0;
|
this.saveDepth = 0;
|
||||||
@ -435,17 +471,21 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
if(!this.window || this.savedIndex) {
|
if(!this.window || this.savedIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saveDepth++;
|
if(this.saveDepth !== 0) {
|
||||||
if(this.saveDepth !== 1) {
|
|
||||||
console.log('bail', 'deep save');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bottomIndex: BigInteger | null = null;
|
log('scroll', 'saving...');
|
||||||
|
|
||||||
|
this.saveDepth++;
|
||||||
|
const { visibleItems } = this.state;
|
||||||
|
|
||||||
|
let bottomIndex = visibleItems[visibleItems.length - 1];
|
||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
const topSpacing = scrollHeight - scrollTop;
|
const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop;
|
||||||
[...Array.from(this.state.visibleItems)].reverse().forEach(([index, datum]) => {
|
const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse();
|
||||||
const el = this.childRefs.get(index);
|
items.forEach((index) => {
|
||||||
|
const el = this.childRefs.get(index.toString());
|
||||||
if(!el) {
|
if(!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -458,24 +498,30 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
if(!bottomIndex) {
|
if(!bottomIndex) {
|
||||||
// weird, shouldn't really happen
|
// weird, shouldn't really happen
|
||||||
this.saveDepth--;
|
this.saveDepth--;
|
||||||
|
log('bail', 'no index found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.savedIndex = bottomIndex;
|
this.savedIndex = bottomIndex;
|
||||||
const ref = this.childRefs.get(bottomIndex)!;
|
const ref = this.childRefs.get(bottomIndex.toString())!;
|
||||||
|
if(!ref) {
|
||||||
|
this.saveDepth--;
|
||||||
|
log('bail', 'missing ref');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { offsetTop } = ref;
|
const { offsetTop } = ref;
|
||||||
this.savedDistance = topSpacing - offsetTop
|
this.savedDistance = topSpacing - offsetTop
|
||||||
}
|
}
|
||||||
|
|
||||||
shiftLayout = { save: this.save.bind(this), restore: this.restore.bind(this) };
|
// disabled until we work out race conditions with loading new nodes
|
||||||
|
shiftLayout = { save: () => {}, restore: () => {} };
|
||||||
|
|
||||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||||
if(element) {
|
if(element) {
|
||||||
this.childRefs.set(index, element);
|
this.childRefs.set(index.toString(), element);
|
||||||
|
this.orphans.delete(index.toString());
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
this.orphans.add(index.toString());
|
||||||
this.childRefs.delete(index);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,12 +540,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
const isTop = origin === 'top';
|
const isTop = origin === 'top';
|
||||||
|
|
||||||
const indexesToRender = isTop ? visibleItems.keys() : visibleItems.keys().reverse();
|
|
||||||
|
|
||||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||||
|
const children = isTop ? visibleItems : [...visibleItems].reverse();
|
||||||
|
|
||||||
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems.peekLargest()?.[0] || bigInt.zero);
|
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems?.[0] || bigInt.zero);
|
||||||
const atEnd = this.state.loaded.top;
|
const atEnd = (this.props.data.peekSmallest()?.[0] ?? bigInt.zero).eq(visibleItems?.[visibleItems.length -1 ] || bigInt.zero);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -511,7 +556,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</Center>)}
|
</Center>)}
|
||||||
<VirtualContext.Provider value={this.shiftLayout}>
|
<VirtualContext.Provider value={this.shiftLayout}>
|
||||||
{indexesToRender.map(index => (
|
{children.map(index => (
|
||||||
<VirtualChild
|
<VirtualChild
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
setRef={this.setRef}
|
setRef={this.setRef}
|
||||||
@ -544,8 +589,10 @@ function VirtualChild(props: VirtualChildProps) {
|
|||||||
|
|
||||||
const ref = useCallback((el: HTMLElement | null) => {
|
const ref = useCallback((el: HTMLElement | null) => {
|
||||||
setRef(el, props.index);
|
setRef(el, props.index);
|
||||||
}, [setRef, props.index])
|
// VirtualChild should always be keyed on the index, so the index should be
|
||||||
|
// valid for the entire lifecycle of the component, hence no dependencies
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (<Renderer ref={ref} {...rest} />);
|
return <Renderer ref={ref} {...rest} />
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -85,7 +85,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='SignOut'
|
icon='LogOut'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -119,7 +119,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -130,7 +130,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Users'
|
icon='Messages'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import useStorage from '~/logic/lib/useStorage';
|
import useStorage, {IuseStorage} from '~/logic/lib/useStorage';
|
||||||
|
|
||||||
const withStorage = (Component, params = {}) => {
|
const withStorage = <P, C extends React.ComponentType<P>>(Component: C, params = {}) => {
|
||||||
return React.forwardRef((props: any, ref) => {
|
return React.forwardRef<C, Omit<C, keyof IuseStorage>>((props, ref) => {
|
||||||
const storage = useStorage(params);
|
const storage = useStorage(params);
|
||||||
|
|
||||||
return <Component ref={ref} {...storage} {...props} />;
|
return <Component ref={ref} {...storage} {...props} />;
|
||||||
|
@ -27,13 +27,13 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
Preferences
|
Preferences
|
||||||
</Text>
|
</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Inbox"
|
icon='Notifications'
|
||||||
text="Notifications"
|
text="Notifications"
|
||||||
to={relativePath('/settings#notifications')}
|
to={relativePath('/settings#notifications')}
|
||||||
/>
|
/>
|
||||||
{!isOwner && (
|
{!isOwner && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="SignOut"
|
icon="LogOut"
|
||||||
text="Unsubscribe"
|
text="Unsubscribe"
|
||||||
color="red"
|
color="red"
|
||||||
to={relativePath('/settings#unsubscribe')}
|
to={relativePath('/settings#unsubscribe')}
|
||||||
@ -45,7 +45,7 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
Administration
|
Administration
|
||||||
</Text>
|
</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Boot"
|
icon="BootNode"
|
||||||
text="Channel Details"
|
text="Channel Details"
|
||||||
to={relativePath('/settings#details')}
|
to={relativePath('/settings#details')}
|
||||||
/>
|
/>
|
||||||
@ -56,14 +56,14 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
/>
|
/>
|
||||||
{ isOwner ? (
|
{ isOwner ? (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="TrashCan"
|
icon="X"
|
||||||
text="Archive Channel"
|
text="Archive Channel"
|
||||||
to={relativePath('/settings#archive')}
|
to={relativePath('/settings#archive')}
|
||||||
color="red"
|
color="red"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="TrashCan"
|
icon="X"
|
||||||
text="Archive Channel"
|
text="Archive Channel"
|
||||||
to={relativePath('/settings#remove')}
|
to={relativePath('/settings#remove')}
|
||||||
color="red"
|
color="red"
|
||||||
|
@ -36,7 +36,7 @@ return;
|
|||||||
? 'Permanently delete this group. (All current members will no longer see this group.)'
|
? 'Permanently delete this group. (All current members will no longer see this group.)'
|
||||||
: 'You can rejoin if it is an open group, or if you are reinvited';
|
: 'You can rejoin if it is an open group, or if you are reinvited';
|
||||||
|
|
||||||
const icon = props.owner ? 'X' : 'SignOut';
|
const icon = props.owner ? 'X' : 'LogOut';
|
||||||
const { modal, showModal } = useModal({ modal:
|
const { modal, showModal } = useModal({ modal:
|
||||||
(dismiss: () => void) => {
|
(dismiss: () => void) => {
|
||||||
const onCancel = (e) => {
|
const onCancel = (e) => {
|
||||||
|
@ -58,7 +58,11 @@ function GraphContentWideInner(
|
|||||||
width="fit-content"
|
width="fit-content"
|
||||||
maxWidth="min(500px, 100%)"
|
maxWidth="min(500px, 100%)"
|
||||||
>
|
>
|
||||||
<RemoteContent key={content.url} url={content.url} />
|
<RemoteContent
|
||||||
|
key={content.url}
|
||||||
|
url={content.url}
|
||||||
|
transcluded={transcluded}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
case "mention":
|
case "mention":
|
||||||
|
@ -29,9 +29,9 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
|
|||||||
width="40px"
|
width="40px"
|
||||||
height="40px"
|
height="40px"
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
/>
|
/>
|
||||||
<Col justifyContent="space-between" flexGrow="1" overflow="hidden">
|
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
|
||||||
<Text
|
<Text
|
||||||
fontSize="1"
|
fontSize="1"
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
|
@ -180,7 +180,7 @@ export function GroupSwitcher(props: {
|
|||||||
>
|
>
|
||||||
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
||||||
{ metadata && <MetadataIcon flexShrink={0} mr="2" metadata={metadata} height="24px" width="24px" /> }
|
{ metadata && <MetadataIcon flexShrink={0} mr="2" metadata={metadata} height="24px" width="24px" /> }
|
||||||
<Text flexShrink={1} lineHeight="1.1" fontSize='2' fontWeight="600" overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
<Text flexShrink={1} lineHeight="1.1" fontSize='2' fontWeight="600" overflow='hidden' display='inline-block' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Row pr='3' verticalAlign="middle">
|
<Row pr='3' verticalAlign="middle">
|
||||||
|
@ -69,7 +69,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
|
|||||||
onSubmit={onGroupify}
|
onSubmit={onGroupify}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<Col flexShrink="0" gapY="4" maxWidth="512px">
|
<Col flexShrink={0} gapY="4" maxWidth="512px">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontWeight="500">Groupify this channel</Text>
|
<Text fontWeight="500">Groupify this channel</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -124,7 +124,6 @@ export function GroupsPane(props: GroupsPaneProps) {
|
|||||||
>
|
>
|
||||||
<Resource
|
<Resource
|
||||||
{...props}
|
{...props}
|
||||||
{...routeProps}
|
|
||||||
association={association}
|
association={association}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
import VirtualScroller from "~/views/components/VirtualScroller";
|
||||||
import PostItem from './PostItem/PostItem';
|
import PostItem from './PostItem/PostItem';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col, Box } from '@tlon/indigo-react';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
|
|
||||||
@ -15,102 +15,103 @@ export class PostFeed extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.isFetching = false;
|
this.isFetching = false;
|
||||||
this.renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
|
||||||
const {
|
|
||||||
graph,
|
|
||||||
graphPath,
|
|
||||||
api,
|
|
||||||
history,
|
|
||||||
baseUrl,
|
|
||||||
parentNode,
|
|
||||||
grandparentNode,
|
|
||||||
association,
|
|
||||||
group,
|
|
||||||
vip
|
|
||||||
} = this.props;
|
|
||||||
const graphResource = resourceFromPath(graphPath);
|
|
||||||
const node = graph.get(index);
|
|
||||||
if (!node) { return null; }
|
|
||||||
|
|
||||||
const first = graph.peekLargest()?.[0];
|
|
||||||
const post = node?.post;
|
|
||||||
if (!node || !post) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let nodeIndex = parentNode ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
|
||||||
return bigInt(ind);
|
|
||||||
}) : [];
|
|
||||||
|
|
||||||
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={index.toString()}>
|
|
||||||
<Col
|
|
||||||
key={index.toString()}
|
|
||||||
mb="3"
|
|
||||||
width="100%"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<PostItem
|
|
||||||
key={parentNode.post.index}
|
|
||||||
ref={ref}
|
|
||||||
parentPost={grandparentNode?.post}
|
|
||||||
node={parentNode}
|
|
||||||
parentNode={grandparentNode}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={nodeIndex}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
isParent={true}
|
|
||||||
isRelativeTime={false}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<PostItem
|
|
||||||
ref={ref}
|
|
||||||
node={node}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={[...nodeIndex, index]}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
isReply={true}
|
|
||||||
parentPost={parentNode.post}
|
|
||||||
isRelativeTime={true}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PostItem
|
|
||||||
key={index.toString()}
|
|
||||||
ref={ref}
|
|
||||||
node={node}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={[...nodeIndex, index]}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
parentPost={parentNode?.post}
|
|
||||||
isReply={!!parentNode}
|
|
||||||
isRelativeTime={true}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchPosts = this.fetchPosts.bind(this);
|
this.fetchPosts = this.fetchPosts.bind(this);
|
||||||
this.doNotFetch = this.doNotFetch.bind(this);
|
this.doNotFetch = this.doNotFetch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
|
const {
|
||||||
|
graph,
|
||||||
|
graphPath,
|
||||||
|
api,
|
||||||
|
history,
|
||||||
|
baseUrl,
|
||||||
|
parentNode,
|
||||||
|
grandparentNode,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
vip
|
||||||
|
} = this.props;
|
||||||
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
const node = graph.get(index);
|
||||||
|
if (!node) { return null; }
|
||||||
|
|
||||||
|
const first = graph.peekLargest()?.[0];
|
||||||
|
const post = node?.post;
|
||||||
|
if (!node || !post) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let nodeIndex = parentNode ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
||||||
|
return bigInt(ind);
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index.toString()}>
|
||||||
|
<Col
|
||||||
|
key={index.toString()}
|
||||||
|
ref={ref}
|
||||||
|
mb="3"
|
||||||
|
width="100%"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<PostItem
|
||||||
|
key={parentNode.post.index}
|
||||||
|
parentPost={grandparentNode?.post}
|
||||||
|
node={parentNode}
|
||||||
|
parentNode={grandparentNode}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={nodeIndex}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isParent={true}
|
||||||
|
isRelativeTime={false}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isReply={true}
|
||||||
|
parentPost={parentNode.post}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={index.toString()} ref={ref}>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={!!parentNode}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
async fetchPosts(newer) {
|
async fetchPosts(newer) {
|
||||||
const { graph, graphPath, api } = this.props;
|
const { graph, graphPath, api } = this.props;
|
||||||
const graphResource = resourceFromPath(graphPath);
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
@ -24,7 +24,7 @@ function canWrite(props) {
|
|||||||
if(vip === 'host-feed') {
|
if(vip === 'host-feed') {
|
||||||
return isHost(association.group);
|
return isHost(association.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isWriter(group, association.resource);
|
return isWriter(group, association.resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ export function PostInput(props) {
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
icon='Links'
|
icon='Attachment'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
onClick={uploadImage}
|
onClick={uploadImage}
|
||||||
|
@ -46,6 +46,7 @@ export function PostHeader(props) {
|
|||||||
isRelativeTime={true}
|
isRelativeTime={true}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
time={true}
|
time={true}
|
||||||
|
lineHeight='1'
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropWidth="200px"
|
dropWidth="200px"
|
||||||
|
@ -177,7 +177,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|
|||||||
<Text gray fontSize="1">
|
<Text gray fontSize="1">
|
||||||
Channels
|
Channels
|
||||||
</Text>
|
</Text>
|
||||||
<Box width="100%" flexShrink="0">
|
<Box width="100%" flexShrink={0}>
|
||||||
{Object.values(preview.channels).map(({ metadata }: any) => (
|
{Object.values(preview.channels).map(({ metadata }: any) => (
|
||||||
<Row width="100%">
|
<Row width="100%">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -11,7 +11,7 @@ import * as Yup from 'yup';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
import { FormError } from '~/views/components/FormError';
|
import { FormError } from '~/views/components/FormError';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps, useHistory } from 'react-router-dom';
|
||||||
import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util';
|
import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
import { Associations } from '@urbit/api/metadata';
|
import { Associations } from '@urbit/api/metadata';
|
||||||
@ -46,12 +46,13 @@ interface NewChannelProps {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactElement {
|
export function NewChannel(props: NewChannelProps): ReactElement {
|
||||||
const { history, api, group, workspace } = props;
|
const history = useHistory();
|
||||||
|
const { api, group, workspace } = props;
|
||||||
|
|
||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
const waiter = useWaitForProps({ groups }, 5000);
|
const waiter = useWaitForProps({ groups }, 5000);
|
||||||
|
|
||||||
const onSubmit = async (values: FormSchema, actions) => {
|
const onSubmit = async (values: FormSchema, actions) => {
|
||||||
const name = (values.name) ? values.name : values.moduleType;
|
const name = (values.name) ? values.name : values.moduleType;
|
||||||
const resId: string = stringToSymbol(values.name)
|
const resId: string = stringToSymbol(values.name)
|
||||||
@ -152,7 +153,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE
|
|||||||
name="moduleType"
|
name="moduleType"
|
||||||
/>
|
/>
|
||||||
<IconRadio
|
<IconRadio
|
||||||
icon="Publish"
|
icon="Notebook"
|
||||||
label="Notebook"
|
label="Notebook"
|
||||||
id="publish"
|
id="publish"
|
||||||
name="moduleType"
|
name="moduleType"
|
||||||
|
@ -181,9 +181,9 @@ export function Participants(props: {
|
|||||||
mb={2}
|
mb={2}
|
||||||
px={2}
|
px={2}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Row mr="4" flexShrink="0">
|
<Row mr="4" flexShrink={0}>
|
||||||
<Tab
|
<Tab
|
||||||
selected={filter}
|
selected={filter}
|
||||||
setSelected={setFilter}
|
setSelected={setFilter}
|
||||||
@ -206,9 +206,9 @@ export function Participants(props: {
|
|||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Col flexShrink="0" width="100%" height="fit-content">
|
<Col flexShrink={0} width="100%" height="fit-content">
|
||||||
<Row alignItems="center" bg="washedGray" borderRadius="1" px="2" my="2">
|
<Row alignItems="center" bg="washedGray" borderRadius="1" px="2" my="2">
|
||||||
<Icon color="gray" icon="MagnifyingGlass" />
|
<Icon color="gray" icon="Search" />
|
||||||
<Input
|
<Input
|
||||||
maxWidth="256px"
|
maxWidth="256px"
|
||||||
color="gray"
|
color="gray"
|
||||||
@ -304,7 +304,7 @@ function Participant(props: {
|
|||||||
}, [api, contact, association]);
|
}, [api, contact, association]);
|
||||||
|
|
||||||
const avatar =
|
const avatar =
|
||||||
contact?.avatar !== null && !hideAvatars ? (
|
contact?.avatar && !hideAvatars ? (
|
||||||
<Image
|
<Image
|
||||||
src={contact.avatar}
|
src={contact.avatar}
|
||||||
height={32}
|
height={32}
|
||||||
|
@ -76,7 +76,7 @@ export function PopoverRoutes(
|
|||||||
<Col gapY="2">
|
<Col gapY="2">
|
||||||
<Text my="1" mx="3" gray>Group</Text>
|
<Text my="1" mx="3" gray>Group</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Inbox"
|
icon='Notifications'
|
||||||
to={relativeUrl('/settings#notifications')}
|
to={relativeUrl('/settings#notifications')}
|
||||||
text="Notifications"
|
text="Notifications"
|
||||||
/>
|
/>
|
||||||
@ -98,7 +98,7 @@ export function PopoverRoutes(
|
|||||||
text="Group Details"
|
text="Group Details"
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Spaces"
|
icon="Dashboard"
|
||||||
to={relativeUrl('/settings#channels')}
|
to={relativeUrl('/settings#channels')}
|
||||||
text="Channel Management"
|
text="Channel Management"
|
||||||
/>
|
/>
|
||||||
|
@ -15,12 +15,14 @@ import useGroupState from '~/logic/state/group';
|
|||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import {Workspace} from '~/types';
|
||||||
|
|
||||||
type ResourceProps = StoreState & {
|
type ResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
export function Resource(props: ResourceProps): ReactElement {
|
export function Resource(props: ResourceProps): ReactElement {
|
||||||
const { association, api, notificationsGraphConfig } = props;
|
const { association, api, notificationsGraphConfig } = props;
|
||||||
|
@ -77,7 +77,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
fontSize='1'
|
fontSize='1'
|
||||||
mr='12px'
|
mr='12px'
|
||||||
my='1'
|
my='1'
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
display={['block','none']}
|
display={['block','none']}
|
||||||
>
|
>
|
||||||
<Link to={`/~landscape${workspace}`}>
|
<Link to={`/~landscape${workspace}`}>
|
||||||
@ -98,7 +98,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
||||||
mr='2'
|
mr='2'
|
||||||
ml='1'
|
ml='1'
|
||||||
flexShrink={['1', '0']}
|
flexShrink={[1, 0]}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@ -112,7 +112,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
mb='0'
|
mb='0'
|
||||||
minWidth='0'
|
minWidth='0'
|
||||||
maxWidth='50%'
|
maxWidth='50%'
|
||||||
flexShrink='1'
|
flexShrink={1}
|
||||||
disableRemoteContent
|
disableRemoteContent
|
||||||
>
|
>
|
||||||
{workspace === '/messages'
|
{workspace === '/messages'
|
||||||
@ -145,7 +145,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Col width='100%' height='100%' overflow='hidden'>
|
<Col width='100%' height='100%' overflow='hidden'>
|
||||||
<Box
|
<Box
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
height='48px'
|
height='48px'
|
||||||
py='2'
|
py='2'
|
||||||
px='2'
|
px='2'
|
||||||
@ -159,7 +159,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
display='flex'
|
display='flex'
|
||||||
alignItems='baseline'
|
alignItems='baseline'
|
||||||
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<BackLink />
|
<BackLink />
|
||||||
<Title />
|
<Title />
|
||||||
@ -169,7 +169,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
ml={3}
|
ml={3}
|
||||||
display='flex'
|
display='flex'
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
ref={actionsRef}
|
ref={actionsRef}
|
||||||
>
|
>
|
||||||
{canWrite && <WriterControls />}
|
{canWrite && <WriterControls />}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import { FormikHelpers } from 'formik';
|
import { FormikHelpers } from 'formik';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
@ -34,6 +34,7 @@ export function SidebarListHeader(props: {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
handleSubmit: (c: SidebarListConfig) => void;
|
handleSubmit: (c: SidebarListConfig) => void;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
|
const history = useHistory();
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
||||||
props.handleSubmit(values);
|
props.handleSubmit(values);
|
||||||
@ -65,7 +66,7 @@ export function SidebarListHeader(props: {
|
|||||||
<Box>
|
<Box>
|
||||||
{( !!feedPath) ? (
|
{( !!feedPath) ? (
|
||||||
<Row
|
<Row
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
py={2}
|
py={2}
|
||||||
@ -74,18 +75,18 @@ export function SidebarListHeader(props: {
|
|||||||
borderBottom={1}
|
borderBottom={1}
|
||||||
borderColor="lightGray"
|
borderColor="lightGray"
|
||||||
backgroundColor={['transparent',
|
backgroundColor={['transparent',
|
||||||
props.history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
||||||
? (
|
? (
|
||||||
'washedGray'
|
'washedGray'
|
||||||
) : (
|
) : (
|
||||||
'transparent'
|
'transparent'
|
||||||
)]}
|
)]}
|
||||||
cursor={['pointer', (
|
cursor={(
|
||||||
props.history.location.pathname === `/~landscape${groupPath}/feed`
|
history.location.pathname === `/~landscape${groupPath}/feed`
|
||||||
? 'default' : 'pointer'
|
? 'default' : 'pointer'
|
||||||
)]}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.history.push(`/~landscape${groupPath}/feed`);
|
history.push(`/~landscape${groupPath}/feed`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
@ -98,14 +99,14 @@ export function SidebarListHeader(props: {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<Row
|
<Row
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
py={2}
|
py={2}
|
||||||
px={3}
|
px={3}
|
||||||
height='48px'
|
height='48px'
|
||||||
>
|
>
|
||||||
<Box flexShrink='0'>
|
<Box flexShrink={0}>
|
||||||
<Text>
|
<Text>
|
||||||
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
||||||
</Text>
|
</Text>
|
||||||
@ -131,7 +132,6 @@ export function SidebarListHeader(props: {
|
|||||||
>
|
>
|
||||||
<NewChannel
|
<NewChannel
|
||||||
api={props.api}
|
api={props.api}
|
||||||
history={props.history}
|
|
||||||
workspace={props.workspace}
|
workspace={props.workspace}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -152,7 +152,7 @@ export function SidebarListHeader(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
width="auto"
|
width="auto"
|
||||||
alignY="top"
|
alignY="top"
|
||||||
alignX={['right', 'left']}
|
alignX={['right', 'left']}
|
||||||
|
@ -1,227 +1,79 @@
|
|||||||
import { BigInteger } from "big-integer";
|
import produce, { immerable, castImmutable, castDraft, setAutoFreeze, enablePatches } from 'immer';
|
||||||
import { immerable } from 'immer';
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
|
|
||||||
interface NonemptyNode<V> {
|
setAutoFreeze(false);
|
||||||
n: [BigInteger, V];
|
|
||||||
l: MapNode<V>;
|
enablePatches();
|
||||||
r: MapNode<V>;
|
|
||||||
|
function sortBigInt(a: BigInteger, b: BigInteger) {
|
||||||
|
if (a.lt(b)) {
|
||||||
|
return 1;
|
||||||
|
} else if (a.eq(b)) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapNode<V> = NonemptyNode<V> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of ordered maps for JS
|
|
||||||
* Plagiarised wholesale from sys/zuse
|
|
||||||
*/
|
|
||||||
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||||
private root: MapNode<V> = null;
|
root: Record<string, V> = {}
|
||||||
|
cachedIter: [BigInteger, V][] = [];
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
size: number = 0;
|
|
||||||
|
|
||||||
constructor(initial: [BigInteger, V][] = []) {
|
constructor(items: [BigInteger, V][] = []) {
|
||||||
initial.forEach(([key, val]) => {
|
items.forEach(([key, val]) => {
|
||||||
this.set(key, val);
|
this.set(key, val);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get size() {
|
||||||
* Retrieve an value for a key
|
return Object.keys(this.root).length;
|
||||||
*/
|
|
||||||
get(key: BigInteger): V | null {
|
|
||||||
const inner = (node: MapNode<V>): V | null => {
|
|
||||||
if (!node) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const [k, v] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
} else {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Put an item by a key
|
|
||||||
*/
|
|
||||||
set(key: BigInteger, value: V): void {
|
|
||||||
|
|
||||||
const inner = (node: MapNode<V>): MapNode<V> => {
|
get(key: BigInteger) {
|
||||||
if (!node) {
|
return this.root[key.toString()] ?? null;
|
||||||
return {
|
}
|
||||||
n: [key, value],
|
|
||||||
l: null,
|
gas(items: [BigInteger, V][]) {
|
||||||
r: null,
|
return produce(this, draft => {
|
||||||
};
|
items.forEach(([key, value]) => {
|
||||||
}
|
draft.root[key.toString()] = castDraft(value);
|
||||||
const [k] = node.n;
|
});
|
||||||
if (key.eq(k)) {
|
draft.generateCachedIter();
|
||||||
this.size--;
|
},
|
||||||
return {
|
(patches) => {
|
||||||
...node,
|
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
||||||
n: [k, value],
|
});
|
||||||
};
|
}
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
set(key: BigInteger, value: V) {
|
||||||
const l = inner(node.l);
|
return produce(this, draft => {
|
||||||
if (!l) {
|
draft.root[key.toString()] = castDraft(value);
|
||||||
throw new Error("invariant violation");
|
draft.generateCachedIter();
|
||||||
}
|
});
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
l,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const r = inner(node.r);
|
|
||||||
if (!r) {
|
|
||||||
throw new Error("invariant violation");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...node, r };
|
|
||||||
};
|
|
||||||
this.size++;
|
|
||||||
this.root = inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all entries
|
|
||||||
*/
|
|
||||||
clear() {
|
clear() {
|
||||||
this.root = null;
|
return produce(this, draft => {
|
||||||
|
draft.cachedIter = [];
|
||||||
|
draft.root = {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
has(key: BigInteger) {
|
||||||
* Predicate testing if map contains key
|
return key.toString() in this.root;
|
||||||
*/
|
|
||||||
has(key: BigInteger): boolean {
|
|
||||||
const inner = (node: MapNode<V>): boolean => {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return inner(node.r);
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove value associated with key, returning whether that key
|
|
||||||
* existed in the first place
|
|
||||||
*/
|
|
||||||
delete(key: BigInteger) {
|
delete(key: BigInteger) {
|
||||||
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
|
return produce(this, draft => {
|
||||||
if (!node) {
|
delete draft.root[key.toString()];
|
||||||
return [false, null];
|
draft.cachedIter = draft.cachedIter.filter(([x]) => x.eq(key));
|
||||||
}
|
});
|
||||||
const [k] = node.n;
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return [true, this.nip(node)];
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const [bool, l] = inner(node.l);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
l,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [bool, r] = inner(node.r);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
r,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
const [ret, newRoot] = inner(this.root);
|
|
||||||
if(ret) {
|
|
||||||
this.size--;
|
|
||||||
}
|
|
||||||
this.root = newRoot;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private nip(nod: NonemptyNode<V>): MapNode<V> {
|
|
||||||
const inner = (node: NonemptyNode<V>): MapNode<V> => {
|
|
||||||
if (!node.l) {
|
|
||||||
return node.r;
|
|
||||||
}
|
|
||||||
if (!node.r) {
|
|
||||||
return node.l;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node.l,
|
|
||||||
r: inner(node.r),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return inner(nod);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekLargest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>): [BigInteger, V] | undefined => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.l) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
}
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekSmallest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>): [BigInteger, V] | undefined => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.r) {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
}
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
keys(): BigInteger[] {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.map(([key]) => key);
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(f: (value: V, key: BigInteger) => void) {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.forEach(([k,v]) => f(v,k));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
||||||
let result: [BigInteger, V][] = [];
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inner(node.l);
|
|
||||||
result.push(node.n);
|
|
||||||
inner(node.r);
|
|
||||||
};
|
|
||||||
inner(this.root);
|
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
let result = [...this.cachedIter];
|
||||||
return {
|
return {
|
||||||
[Symbol.iterator]: this[Symbol.iterator],
|
[Symbol.iterator]: this[Symbol.iterator],
|
||||||
next: (): IteratorResult<[BigInteger, V]> => {
|
next: (): IteratorResult<[BigInteger, V]> => {
|
||||||
@ -232,4 +84,27 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peekLargest() {
|
||||||
|
const sorted = Array.from(this);
|
||||||
|
return sorted[0] as [BigInteger, V] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
peekSmallest() {
|
||||||
|
const sorted = Array.from(this);
|
||||||
|
return sorted[sorted.length - 1] as [BigInteger, V] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return Array.from(this).map(([k,v]) => k);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCachedIter() {
|
||||||
|
const result = Object.keys(this.root).map(key => {
|
||||||
|
const num = bigInt(key);
|
||||||
|
return [num, this.root[key]] as [BigInteger, V];
|
||||||
|
}).sort(([a], [b]) => sortBigInt(a,b));
|
||||||
|
this.cachedIter = result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,3 +65,13 @@
|
|||||||
*/
|
*/
|
||||||
u3_noun
|
u3_noun
|
||||||
u3s_cue_atom(u3_atom a);
|
u3s_cue_atom(u3_atom a);
|
||||||
|
|
||||||
|
/* u3s_sift_ud_bytes: parse @ud.
|
||||||
|
*/
|
||||||
|
u3_weak
|
||||||
|
u3s_sift_ud_bytes(c3_w len_w, c3_y* byt_y);
|
||||||
|
|
||||||
|
/* u3s_sift_ud: parse @ud.
|
||||||
|
*/
|
||||||
|
u3_weak
|
||||||
|
u3s_sift_ud(u3_atom a);
|
||||||
|
@ -5,49 +5,16 @@
|
|||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
|
||||||
/* functions
|
static inline u3_noun
|
||||||
*/
|
_parse_ud(u3_noun a)
|
||||||
u3_noun
|
{
|
||||||
_parse_ud(u3_noun txt) {
|
u3_weak pro;
|
||||||
c3_c* c = u3a_string(txt);
|
|
||||||
|
|
||||||
// First character must represent a digit
|
if ( u3_none == (pro = u3s_sift_ud(u3x_atom(a))) ) {
|
||||||
c3_c* cur = c;
|
return u3_nul;
|
||||||
if (cur[0] > '9' || cur[0] < '0') {
|
|
||||||
u3a_free(c);
|
|
||||||
return u3_none;
|
|
||||||
}
|
|
||||||
u3_atom total = cur[0] - '0';
|
|
||||||
cur++;
|
|
||||||
|
|
||||||
int since_last_period = 0;
|
|
||||||
while (cur[0] != 0) {
|
|
||||||
since_last_period++;
|
|
||||||
if (cur[0] == '.') {
|
|
||||||
since_last_period = 0;
|
|
||||||
cur++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cur[0] > '9' || cur[0] < '0') {
|
|
||||||
u3a_free(c);
|
|
||||||
u3z(total);
|
|
||||||
return u3_none;
|
|
||||||
}
|
|
||||||
|
|
||||||
total = u3ka_mul(total, 10);
|
|
||||||
total = u3ka_add(total, cur[0] - '0');
|
|
||||||
cur++;
|
|
||||||
|
|
||||||
if (since_last_period > 3) {
|
|
||||||
u3a_free(c);
|
|
||||||
u3z(total);
|
|
||||||
return u3_none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u3a_free(c);
|
return u3nc(u3_nul, pro);
|
||||||
return u3nc(0, total);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
static
|
||||||
|
@ -313,12 +313,17 @@ u3i_word(c3_w dat_w)
|
|||||||
u3_atom
|
u3_atom
|
||||||
u3i_chub(c3_d dat_d)
|
u3i_chub(c3_d dat_d)
|
||||||
{
|
{
|
||||||
c3_w dat_w[2] = {
|
if ( c3y == u3a_is_cat(dat_d) ) {
|
||||||
dat_d & 0xffffffffULL,
|
return (u3_atom)dat_d;
|
||||||
dat_d >> 32
|
}
|
||||||
};
|
else {
|
||||||
|
c3_w dat_w[2] = {
|
||||||
|
dat_d & 0xffffffffULL,
|
||||||
|
dat_d >> 32
|
||||||
|
};
|
||||||
|
|
||||||
return u3i_words(2, dat_w);
|
return u3i_words(2, dat_w);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* u3i_bytes(): Copy [a] bytes from [b] to an LSB first atom.
|
/* u3i_bytes(): Copy [a] bytes from [b] to an LSB first atom.
|
||||||
|
@ -98,28 +98,22 @@ _cm_punt(u3_noun tax)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static void _write(int fd, const void *buf, size_t count)
|
|
||||||
{
|
|
||||||
if (count != write(fd, buf, count)){
|
|
||||||
u3l_log("write failed\r\n");
|
|
||||||
c3_assert(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* _cm_emergency(): write emergency text to stderr, never failing.
|
/* _cm_emergency(): write emergency text to stderr, never failing.
|
||||||
*/
|
*/
|
||||||
static void
|
static void
|
||||||
_cm_emergency(c3_c* cap_c, c3_l sig_l)
|
_cm_emergency(c3_c* cap_c, c3_l sig_l)
|
||||||
{
|
{
|
||||||
_write(2, "\r\n", 2);
|
c3_i ret_i;
|
||||||
_write(2, cap_c, strlen(cap_c));
|
|
||||||
|
ret_i = write(2, "\r\n", 2);
|
||||||
|
ret_i = write(2, cap_c, strlen(cap_c));
|
||||||
|
|
||||||
if ( sig_l ) {
|
if ( sig_l ) {
|
||||||
_write(2, ": ", 2);
|
ret_i = write(2, ": ", 2);
|
||||||
_write(2, &sig_l, 4);
|
ret_i = write(2, &sig_l, 4);
|
||||||
}
|
}
|
||||||
_write(2, "\r\n", 2);
|
|
||||||
|
ret_i = write(2, "\r\n", 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _cm_overflow(void *arg1, void *arg2, void *arg3)
|
static void _cm_overflow(void *arg1, void *arg2, void *arg3)
|
||||||
@ -127,7 +121,7 @@ static void _cm_overflow(void *arg1, void *arg2, void *arg3)
|
|||||||
(void)(arg1);
|
(void)(arg1);
|
||||||
(void)(arg2);
|
(void)(arg2);
|
||||||
(void)(arg3);
|
(void)(arg3);
|
||||||
siglongjmp(u3_Signal, c3__over);
|
u3m_signal(c3__over);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* _cm_signal_handle(): handle a signal in general.
|
/* _cm_signal_handle(): handle a signal in general.
|
||||||
@ -139,7 +133,7 @@ _cm_signal_handle(c3_l sig_l)
|
|||||||
sigsegv_leave_handler(_cm_overflow, NULL, NULL, NULL);
|
sigsegv_leave_handler(_cm_overflow, NULL, NULL, NULL);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
siglongjmp(u3_Signal, sig_l);
|
u3m_signal(sig_l);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,7 +655,10 @@ u3m_dump(void)
|
|||||||
c3_i
|
c3_i
|
||||||
u3m_bail(u3_noun how)
|
u3m_bail(u3_noun how)
|
||||||
{
|
{
|
||||||
if ( (c3__exit == how) && (u3R == &u3H->rod_u) ) {
|
if ( &(u3H->rod_u) == u3R ) {
|
||||||
|
// XX set exit code
|
||||||
|
//
|
||||||
|
fprintf(stderr, "home: bailing out\r\n");
|
||||||
abort();
|
abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,8 +685,9 @@ u3m_bail(u3_noun how)
|
|||||||
//
|
//
|
||||||
switch ( how ) {
|
switch ( how ) {
|
||||||
case c3__foul:
|
case c3__foul:
|
||||||
case c3__meme:
|
|
||||||
case c3__oops: {
|
case c3__oops: {
|
||||||
|
// XX set exit code
|
||||||
|
//
|
||||||
fprintf(stderr, "bailing out\r\n");
|
fprintf(stderr, "bailing out\r\n");
|
||||||
abort();
|
abort();
|
||||||
}
|
}
|
||||||
@ -700,6 +698,9 @@ u3m_bail(u3_noun how)
|
|||||||
// choice but to use the signal process; and we require the flat
|
// choice but to use the signal process; and we require the flat
|
||||||
// form of how.
|
// form of how.
|
||||||
//
|
//
|
||||||
|
// XX JB: these seem unrecoverable, at least wrt memory management,
|
||||||
|
// so they've been disabled above for now
|
||||||
|
//
|
||||||
c3_assert(_(u3a_is_cat(how)));
|
c3_assert(_(u3a_is_cat(how)));
|
||||||
u3m_signal(how);
|
u3m_signal(how);
|
||||||
}
|
}
|
||||||
|
@ -1205,16 +1205,11 @@ u3r_mp(mpz_t a_mp,
|
|||||||
else {
|
else {
|
||||||
u3a_atom* b_u = u3a_to_ptr(b);
|
u3a_atom* b_u = u3a_to_ptr(b);
|
||||||
c3_w len_w = b_u->len_w;
|
c3_w len_w = b_u->len_w;
|
||||||
|
c3_d bit_d = (c3_d)len_w << 5;
|
||||||
|
|
||||||
// avoid reallocation on import, if possible
|
// avoid reallocation on import, if possible
|
||||||
//
|
//
|
||||||
if ( (len_w >> 27) ) {
|
mpz_init2(a_mp, (c3_w)c3_min(bit_d, UINT32_MAX));
|
||||||
mpz_init(a_mp);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mpz_init2(a_mp, len_w << 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
mpz_import(a_mp, len_w, -1, sizeof(c3_w), 0, 0, b_u->buf_w);
|
mpz_import(a_mp, len_w, -1, sizeof(c3_w), 0, 0, b_u->buf_w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -856,3 +856,128 @@ u3s_cue_atom(u3_atom a)
|
|||||||
|
|
||||||
return u3s_cue_bytes((c3_d)len_w, byt_y);
|
return u3s_cue_bytes((c3_d)len_w, byt_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define DIGIT(a) ( ((a) >= '0') && ((a) <= '9') )
|
||||||
|
#define BLOCK(a) ( ('.' == (a)[0]) \
|
||||||
|
&& DIGIT(a[1]) \
|
||||||
|
&& DIGIT(a[2]) \
|
||||||
|
&& DIGIT(a[3]) )
|
||||||
|
|
||||||
|
/* u3s_sift_ud_bytes: parse @ud
|
||||||
|
*/
|
||||||
|
u3_weak
|
||||||
|
u3s_sift_ud_bytes(c3_w len_w, c3_y* byt_y)
|
||||||
|
{
|
||||||
|
c3_y num_y = len_w % 4; // leading digits length
|
||||||
|
c3_s val_s = 0; // leading digits value
|
||||||
|
|
||||||
|
// +ape:ag: just 0
|
||||||
|
//
|
||||||
|
if ( !len_w ) return u3_none;
|
||||||
|
if ( '0' == *byt_y ) return ( 1 == len_w ) ? (u3_noun)0 : u3_none;
|
||||||
|
|
||||||
|
// +ted:ab: leading nonzero (checked above), plus up to 2 digits
|
||||||
|
//
|
||||||
|
#define NEXT() do { \
|
||||||
|
if ( !DIGIT(*byt_y) ) return u3_none; \
|
||||||
|
val_s *= 10; \
|
||||||
|
val_s += *byt_y++ - '0'; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
switch ( num_y ) {
|
||||||
|
case 3: NEXT();
|
||||||
|
case 2: NEXT();
|
||||||
|
case 1: NEXT(); break;
|
||||||
|
case 0: return u3_none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef NEXT
|
||||||
|
|
||||||
|
len_w -= num_y;
|
||||||
|
|
||||||
|
// +tid:ab: dot-prefixed 3-digit blocks
|
||||||
|
//
|
||||||
|
// avoid gmp allocation if possible
|
||||||
|
// - 19 decimal digits fit in 64 bits
|
||||||
|
// - 18 digits is 24 bytes with separators
|
||||||
|
//
|
||||||
|
if ( ((1 == num_y) && (24 >= len_w))
|
||||||
|
|| (20 >= len_w) )
|
||||||
|
{
|
||||||
|
c3_d val_d = val_s;
|
||||||
|
|
||||||
|
while ( len_w ) {
|
||||||
|
if ( !BLOCK(byt_y) ) return u3_none;
|
||||||
|
|
||||||
|
byt_y++;
|
||||||
|
|
||||||
|
val_d *= 10;
|
||||||
|
val_d += *byt_y++ - '0';
|
||||||
|
val_d *= 10;
|
||||||
|
val_d += *byt_y++ - '0';
|
||||||
|
val_d *= 10;
|
||||||
|
val_d += *byt_y++ - '0';
|
||||||
|
|
||||||
|
len_w -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return u3i_chub(val_d);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// avoid gmp realloc if possible
|
||||||
|
//
|
||||||
|
mpz_t a_mp;
|
||||||
|
{
|
||||||
|
c3_d bit_d = (c3_d)(len_w / 4) * 10;
|
||||||
|
mpz_init2(a_mp, (c3_w)c3_min(bit_d, UINT32_MAX));
|
||||||
|
mpz_set_ui(a_mp, val_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
while ( len_w ) {
|
||||||
|
if ( !BLOCK(byt_y) ) {
|
||||||
|
mpz_clear(a_mp);
|
||||||
|
return u3_none;
|
||||||
|
}
|
||||||
|
|
||||||
|
byt_y++;
|
||||||
|
|
||||||
|
val_s = *byt_y++ - '0';
|
||||||
|
val_s *= 10;
|
||||||
|
val_s += *byt_y++ - '0';
|
||||||
|
val_s *= 10;
|
||||||
|
val_s += *byt_y++ - '0';
|
||||||
|
|
||||||
|
mpz_mul_ui(a_mp, a_mp, 1000);
|
||||||
|
mpz_add_ui(a_mp, a_mp, val_s);
|
||||||
|
|
||||||
|
len_w -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
return u3i_mp(a_mp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef BLOCK
|
||||||
|
#undef DIGIT
|
||||||
|
|
||||||
|
/* u3s_sift_ud: parse @ud.
|
||||||
|
*/
|
||||||
|
u3_weak
|
||||||
|
u3s_sift_ud(u3_atom a)
|
||||||
|
{
|
||||||
|
c3_w len_w = u3r_met(3, a);
|
||||||
|
c3_y* byt_y;
|
||||||
|
|
||||||
|
// XX assumes little-endian
|
||||||
|
//
|
||||||
|
if ( c3y == u3a_is_cat(a) ) {
|
||||||
|
byt_y = (c3_y*)&a;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
u3a_atom* vat_u = u3a_to_ptr(a);
|
||||||
|
byt_y = (c3_y*)vat_u->buf_w;
|
||||||
|
}
|
||||||
|
|
||||||
|
return u3s_sift_ud_bytes(len_w, byt_y);
|
||||||
|
}
|
||||||
|
@ -9,6 +9,108 @@ _setup(void)
|
|||||||
u3m_pave(c3y);
|
u3m_pave(c3y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline c3_i
|
||||||
|
_ud_good(c3_w num_w, const c3_c* num_c)
|
||||||
|
{
|
||||||
|
u3_weak out;
|
||||||
|
if ( num_w != (out = u3s_sift_ud_bytes(strlen(num_c), (c3_y*)num_c)) ) {
|
||||||
|
if ( u3_none == out ) {
|
||||||
|
fprintf(stderr, "sift_ud: %s fail; expected %u\r\n", num_c, num_w);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "sift_ud: %s wrong; expected %u: actual %u\r\n", num_c, num_w, out);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline c3_i
|
||||||
|
_ud_fail(const c3_c* num_c)
|
||||||
|
{
|
||||||
|
u3_weak out;
|
||||||
|
if ( u3_none != (out = u3s_sift_ud_bytes(strlen(num_c), (c3_y*)num_c)) ) {
|
||||||
|
u3m_p("out", out);
|
||||||
|
fprintf(stderr, "sift_ud: %s expected fail\r\n", num_c);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static c3_i
|
||||||
|
_test_sift_ud(void)
|
||||||
|
{
|
||||||
|
c3_i ret_i = 1;
|
||||||
|
|
||||||
|
ret_i &= _ud_good(0, "0");
|
||||||
|
ret_i &= _ud_good(1, "1");
|
||||||
|
ret_i &= _ud_good(12, "12");
|
||||||
|
ret_i &= _ud_good(123, "123");
|
||||||
|
ret_i &= _ud_good(1234, "1.234");
|
||||||
|
ret_i &= _ud_good(12345, "12.345");
|
||||||
|
ret_i &= _ud_good(123456, "123.456");
|
||||||
|
ret_i &= _ud_good(1234567, "1.234.567");
|
||||||
|
ret_i &= _ud_good(12345678, "12.345.678");
|
||||||
|
ret_i &= _ud_good(123456789, "123.456.789");
|
||||||
|
ret_i &= _ud_good(100000000, "100.000.000");
|
||||||
|
ret_i &= _ud_good(101101101, "101.101.101");
|
||||||
|
ret_i &= _ud_good(201201201, "201.201.201");
|
||||||
|
ret_i &= _ud_good(302201100, "302.201.100");
|
||||||
|
|
||||||
|
ret_i &= _ud_fail("01");
|
||||||
|
ret_i &= _ud_fail("02");
|
||||||
|
ret_i &= _ud_fail("003");
|
||||||
|
ret_i &= _ud_fail("1234");
|
||||||
|
ret_i &= _ud_fail("1234.5");
|
||||||
|
ret_i &= _ud_fail("1234.567.8");
|
||||||
|
ret_i &= _ud_fail("1234.56..78.");
|
||||||
|
ret_i &= _ud_fail("123.45a");
|
||||||
|
ret_i &= _ud_fail(".123.456");
|
||||||
|
|
||||||
|
{
|
||||||
|
c3_c* num_c = "4.294.967.296";
|
||||||
|
u3_weak out = u3s_sift_ud_bytes(strlen(num_c), (c3_y*)num_c);
|
||||||
|
u3_atom pro = u3qc_bex(32);
|
||||||
|
|
||||||
|
if ( u3_none == out ) {
|
||||||
|
fprintf(stderr, "sift_ud: (bex 32) fail\r\n");
|
||||||
|
ret_i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( c3n == u3r_sing(pro, out) ) {
|
||||||
|
u3m_p("out", out);
|
||||||
|
fprintf(stderr, "sift_ud: (bex 32) wrong\r\n");
|
||||||
|
ret_i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u3z(out); u3z(pro);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
c3_c* num_c = "340.282.366.920.938.463.463.374.607.431.768.211.456";
|
||||||
|
u3_weak out = u3s_sift_ud_bytes(strlen(num_c), (c3_y*)num_c);
|
||||||
|
u3_atom pro = u3qc_bex(128);
|
||||||
|
|
||||||
|
if ( u3_none == out ) {
|
||||||
|
fprintf(stderr, "sift_ud: (bex 128) fail\r\n");
|
||||||
|
ret_i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( c3n == u3r_sing(pro, out) ) {
|
||||||
|
u3m_p("out", out);
|
||||||
|
fprintf(stderr, "sift_ud: (bex 128) wrong\r\n");
|
||||||
|
ret_i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u3z(out); u3z(pro);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret_i;
|
||||||
|
}
|
||||||
|
|
||||||
static c3_i
|
static c3_i
|
||||||
_test_en_base16(void)
|
_test_en_base16(void)
|
||||||
{
|
{
|
||||||
@ -400,6 +502,11 @@ _test_jets(void)
|
|||||||
{
|
{
|
||||||
c3_i ret_i = 1;
|
c3_i ret_i = 1;
|
||||||
|
|
||||||
|
if ( !_test_sift_ud() ) {
|
||||||
|
fprintf(stderr, "test jets: sift_ud: failed\r\n");
|
||||||
|
ret_i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if ( !_test_base16() ) {
|
if ( !_test_base16() ) {
|
||||||
fprintf(stderr, "test jets: base16: failed\r\n");
|
fprintf(stderr, "test jets: base16: failed\r\n");
|
||||||
ret_i = 0;
|
ret_i = 0;
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
u3_csat_init = 0, // initialized
|
u3_csat_init = 0, // initialized
|
||||||
u3_csat_addr = 1, // address resolution begun
|
u3_csat_addr = 1, // address resolution begun
|
||||||
u3_csat_quit = 2, // cancellation requested
|
u3_csat_quit = 2, // cancellation requested
|
||||||
u3_csat_ripe = 3 // passed to libh2o
|
u3_csat_conn = 3, // sync connect phase
|
||||||
|
u3_csat_ripe = 4 // passed to libh2o
|
||||||
} u3_csat;
|
} u3_csat;
|
||||||
|
|
||||||
/* u3_cres: response to http client.
|
/* u3_cres: response to http client.
|
||||||
@ -35,26 +36,26 @@
|
|||||||
/* u3_creq: outgoing http request.
|
/* u3_creq: outgoing http request.
|
||||||
*/
|
*/
|
||||||
typedef struct _u3_creq { // client request
|
typedef struct _u3_creq { // client request
|
||||||
c3_l num_l; // request number
|
c3_l num_l; // request number
|
||||||
h2o_http1client_t* cli_u; // h2o client
|
h2o_http1client_t* cli_u; // h2o client
|
||||||
u3_csat sat_e; // connection state
|
u3_csat sat_e; // connection state
|
||||||
c3_o sec; // yes == https
|
c3_o sec; // yes == https
|
||||||
c3_w ipf_w; // IP
|
c3_w ipf_w; // IP
|
||||||
c3_c* ipf_c; // IP (string)
|
c3_c* ipf_c; // IP (string)
|
||||||
c3_c* hot_c; // host
|
c3_c* hot_c; // host
|
||||||
c3_s por_s; // port
|
c3_s por_s; // port
|
||||||
c3_c* por_c; // port (string)
|
c3_c* por_c; // port (string)
|
||||||
c3_c* met_c; // method
|
c3_c* met_c; // method
|
||||||
c3_c* url_c; // url
|
c3_c* url_c; // url
|
||||||
u3_hhed* hed_u; // headers
|
u3_hhed* hed_u; // headers
|
||||||
u3_hbod* bod_u; // body
|
u3_hbod* bod_u; // body
|
||||||
u3_hbod* rub_u; // exit of send queue
|
u3_hbod* rub_u; // exit of send queue
|
||||||
u3_hbod* bur_u; // entry of send queue
|
u3_hbod* bur_u; // entry of send queue
|
||||||
h2o_iovec_t* vec_u; // send-buffer array
|
h2o_iovec_t* vec_u; // send-buffer array
|
||||||
u3_cres* res_u; // nascent response
|
u3_cres* res_u; // nascent response
|
||||||
struct _u3_creq* nex_u; // next in list
|
struct _u3_creq* nex_u; // next in list
|
||||||
struct _u3_creq* pre_u; // previous in list
|
struct _u3_creq* pre_u; // previous in list
|
||||||
struct _u3_cttp* ctp_u; // cttp backpointer
|
struct _u3_cttp* ctp_u; // cttp backpointer
|
||||||
} u3_creq;
|
} u3_creq;
|
||||||
|
|
||||||
/* u3_cttp: http client.
|
/* u3_cttp: http client.
|
||||||
@ -559,13 +560,18 @@ _cttp_creq_new(u3_cttp* ctp_u, c3_l num_l, u3_noun hes)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the url out of the new style url passed to us.
|
// parse the url out of the new style url passed to us.
|
||||||
|
//
|
||||||
u3_noun unit_pul = u3do("de-purl:html", u3k(url));
|
u3_noun unit_pul = u3do("de-purl:html", u3k(url));
|
||||||
if (c3n == u3r_du(unit_pul)) {
|
|
||||||
u3l_log("cttp: url parsing failed\n");
|
if ( c3n == u3r_du(unit_pul) ) {
|
||||||
|
c3_c* url_c = u3r_string(url);
|
||||||
|
u3l_log("cttp: unable to parse url:\n %s\n", url_c);
|
||||||
|
c3_free(url_c);
|
||||||
u3z(hes);
|
u3z(hes);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
u3_noun pul = u3t(unit_pul);
|
u3_noun pul = u3t(unit_pul);
|
||||||
|
|
||||||
u3_noun hat = u3h(pul); // +hart
|
u3_noun hat = u3h(pul); // +hart
|
||||||
@ -817,20 +823,33 @@ _cttp_creq_on_head(h2o_http1client_t* cli_u, const c3_c* err_c, c3_i ver_i,
|
|||||||
*/
|
*/
|
||||||
static h2o_http1client_head_cb
|
static h2o_http1client_head_cb
|
||||||
_cttp_creq_on_connect(h2o_http1client_t* cli_u, const c3_c* err_c,
|
_cttp_creq_on_connect(h2o_http1client_t* cli_u, const c3_c* err_c,
|
||||||
h2o_iovec_t** vec_p, size_t* vec_t, c3_i* hed_i)
|
h2o_iovec_t** vec_u, size_t* vec_i, c3_i* hed_i)
|
||||||
{
|
{
|
||||||
u3_creq* ceq_u = (u3_creq *)cli_u->data;
|
u3_creq* ceq_u = (u3_creq *)cli_u->data;
|
||||||
|
|
||||||
if ( 0 != err_c ) {
|
if ( 0 != err_c ) {
|
||||||
_cttp_creq_fail(ceq_u, err_c);
|
// if synchronously connecting, caller will cleanup
|
||||||
|
//
|
||||||
|
if ( u3_csat_conn == ceq_u->sat_e ) {
|
||||||
|
ceq_u->sat_e = u3_csat_quit;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
c3_assert( u3_csat_ripe == ceq_u->sat_e );
|
||||||
|
_cttp_creq_fail(ceq_u, err_c);
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serialize request (populate rub_u)
|
||||||
|
//
|
||||||
|
_cttp_creq_fire(ceq_u);
|
||||||
|
|
||||||
{
|
{
|
||||||
c3_w len_w;
|
c3_w len_w;
|
||||||
ceq_u->vec_u = _cttp_bods_to_vec(ceq_u->rub_u, &len_w);
|
ceq_u->vec_u = _cttp_bods_to_vec(ceq_u->rub_u, &len_w);
|
||||||
*vec_t = len_w;
|
|
||||||
*vec_p = ceq_u->vec_u;
|
*vec_i = len_w;
|
||||||
|
*vec_u = ceq_u->vec_u;
|
||||||
*hed_i = (0 == strcmp(ceq_u->met_c, "HEAD"));
|
*hed_i = (0 == strcmp(ceq_u->met_c, "HEAD"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,24 +861,43 @@ _cttp_creq_on_connect(h2o_http1client_t* cli_u, const c3_c* err_c,
|
|||||||
static void
|
static void
|
||||||
_cttp_creq_connect(u3_creq* ceq_u)
|
_cttp_creq_connect(u3_creq* ceq_u)
|
||||||
{
|
{
|
||||||
c3_assert(u3_csat_ripe == ceq_u->sat_e);
|
c3_assert( u3_csat_conn == ceq_u->sat_e );
|
||||||
c3_assert(ceq_u->ipf_c);
|
c3_assert( ceq_u->ipf_c );
|
||||||
|
|
||||||
h2o_iovec_t ipf_u = h2o_iovec_init(ceq_u->ipf_c, strlen(ceq_u->ipf_c));
|
// connect by ip/port, avoiding synchronous getaddrinfo()
|
||||||
c3_s por_s = ceq_u->por_s ? ceq_u->por_s :
|
//
|
||||||
( c3y == ceq_u->sec ) ? 443 : 80;
|
{
|
||||||
|
h2o_iovec_t ipf_u = h2o_iovec_init(ceq_u->ipf_c, strlen(ceq_u->ipf_c));
|
||||||
|
c3_t tls_t = ( c3y == ceq_u->sec );
|
||||||
|
c3_s por_s = ( ceq_u->por_s )
|
||||||
|
? ceq_u->por_s
|
||||||
|
: ( tls_t ) ? 443 : 80;
|
||||||
|
|
||||||
// connect by IP
|
h2o_http1client_connect(&ceq_u->cli_u, ceq_u, &ceq_u->ctp_u->ctx_u,
|
||||||
h2o_http1client_connect(&ceq_u->cli_u, ceq_u, &ceq_u->ctp_u->ctx_u, ipf_u,
|
ipf_u, por_s, tls_t, _cttp_creq_on_connect);
|
||||||
por_s, c3y == ceq_u->sec, _cttp_creq_on_connect);
|
|
||||||
|
|
||||||
// set hostname for TLS handshake
|
|
||||||
if ( ceq_u->hot_c && c3y == ceq_u->sec ) {
|
|
||||||
c3_free(ceq_u->cli_u->ssl.server_name);
|
|
||||||
ceq_u->cli_u->ssl.server_name = strdup(ceq_u->hot_c);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cttp_creq_fire(ceq_u);
|
// connect() failed, cb invoked synchronously
|
||||||
|
//
|
||||||
|
if ( u3_csat_conn != ceq_u->sat_e ) {
|
||||||
|
c3_assert( u3_csat_quit == ceq_u->sat_e );
|
||||||
|
// only one such failure case
|
||||||
|
//
|
||||||
|
_cttp_creq_fail(ceq_u, "socket create error");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ceq_u->sat_e = u3_csat_ripe;
|
||||||
|
|
||||||
|
// fixup hostname for TLS handshake
|
||||||
|
//
|
||||||
|
// must be synchronous, after successfull connect() call
|
||||||
|
//
|
||||||
|
if ( ceq_u->hot_c && (c3y == ceq_u->sec) ) {
|
||||||
|
c3_assert( ceq_u->cli_u );
|
||||||
|
c3_free(ceq_u->cli_u->ssl.server_name);
|
||||||
|
ceq_u->cli_u->ssl.server_name = strdup(ceq_u->hot_c);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* _cttp_creq_resolve_cb(): cb upon IP address resolution
|
/* _cttp_creq_resolve_cb(): cb upon IP address resolution
|
||||||
@ -882,7 +920,7 @@ _cttp_creq_resolve_cb(uv_getaddrinfo_t* adr_u,
|
|||||||
ceq_u->ipf_w = ntohl(((struct sockaddr_in *)aif_u->ai_addr)->sin_addr.s_addr);
|
ceq_u->ipf_w = ntohl(((struct sockaddr_in *)aif_u->ai_addr)->sin_addr.s_addr);
|
||||||
ceq_u->ipf_c = _cttp_creq_ip(ceq_u->ipf_w);
|
ceq_u->ipf_c = _cttp_creq_ip(ceq_u->ipf_w);
|
||||||
|
|
||||||
ceq_u->sat_e = u3_csat_ripe;
|
ceq_u->sat_e = u3_csat_conn;
|
||||||
_cttp_creq_connect(ceq_u);
|
_cttp_creq_connect(ceq_u);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -926,7 +964,7 @@ static void
|
|||||||
_cttp_creq_start(u3_creq* ceq_u)
|
_cttp_creq_start(u3_creq* ceq_u)
|
||||||
{
|
{
|
||||||
if ( ceq_u->ipf_c ) {
|
if ( ceq_u->ipf_c ) {
|
||||||
ceq_u->sat_e = u3_csat_ripe;
|
ceq_u->sat_e = u3_csat_conn;
|
||||||
_cttp_creq_connect(ceq_u);
|
_cttp_creq_connect(ceq_u);
|
||||||
} else {
|
} else {
|
||||||
ceq_u->sat_e = u3_csat_addr;
|
ceq_u->sat_e = u3_csat_addr;
|
||||||
@ -981,7 +1019,6 @@ _cttp_ef_http_client(u3_cttp* ctp_u, u3_noun tag, u3_noun dat)
|
|||||||
ret_o = c3y;
|
ret_o = c3y;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
u3l_log("cttp: strange request (unparsable url)\n");
|
|
||||||
ret_o = c3n;
|
ret_o = c3n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user