Merge branch 'master' into os1-avatar

This commit is contained in:
Logan 2020-04-22 11:42:33 -04:00 committed by GitHub
commit c36c5a9bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1717 additions and 556 deletions

View File

@ -0,0 +1,39 @@
---
name: Kernel or runtime bug report
about: Use this template to file a bug for low-level system components, e.g. Hoon,
Arvo, Zuse, the vanes, Vere, etc.
title: ''
labels: bug
assignees: ''
---
<!-- A good bug report, description of a crash, etc., should ideally be *reproducible*, with clear steps as to how another developer can replicate and examine your problem. That said, this isn't always possible; some bugs depend on having created a complicated or unusual state, or can otherwise simply be difficult to trigger again (say, you encountered it in the last continuity era).
Your issue should thus at a minimum be *informative*. The best advice here is probably "don't write bad issues," where "bad" is a matter of judgment and taste. Issues that the maintainers don't judge to be sufficiently useful or informative may be closed. -->
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behaviour:
1. ...
2. ...
3. ...
**Expected behaviour**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**System (please supply the following information, if relevant):**
- OS: [e.g. macOS, linux64, FreeBSD]
- Vere and Urbit OS versions
- Your ship's `%base` hash (use `.^(@uv %cz /=base=)` to check)
**Additional context**
Add any other context about the problem here.
**Notify maintainers**
If you happen to know who the appropriate maintainers are, consider mentioning them with an @ here. You may want to use `git blame` to see who has last touched any relevant code.

View File

@ -119,6 +119,9 @@ this:
```
urbit-vx.y.z
Note that this Vere release will by default boot fresh ships using an Urbit OS
va.b.c pill.
Release binaries:
(linux64)
@ -138,9 +141,11 @@ Contributions:
The same schpeel re: release candidates applies here.
Do not include implicit Urbit OS changes in Vere releases. This used to be
done, historically, but shouldn't be any longer. If there are Urbit OS and
Vere changes to be released, make two releases.
Note that the release notes indicate which version of Urbit OS the Vere release
will use by default when booting fresh ships. Do not include implicit Urbit OS
changes in Vere releases; this used to be done, historically, but shouldn't be
any longer. If there are Urbit OS and Vere changes to be released, make two
separate releases.
### Deploy the update

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

View File

@ -19,7 +19,7 @@
+$ state-zero [%0 state-base]
+$ state-one [%1 state-base]
+$ state-base
$: synced=(map path ship)
$: =synced
invite-created=_|
==
--
@ -77,6 +77,7 @@
^- (quip card _this)
?+ path (on-watch:def path)
[%contacts *] [(watch-contacts:cc t.path) this]
[%synced *] [(watch-synced:cc t.path) this]
==
::
++ on-agent
@ -124,30 +125,29 @@
++ poke-contact-action
|= act=contact-action
^- (quip card _state)
|^
:_ state
?+ -.act !!
%edit (handle-contact-action path.act ship.act act)
%add (handle-contact-action path.act ship.act act)
%remove (handle-contact-action path.act ship.act act)
==
::
++ handle-contact-action
|= [=path =ship act=contact-action]
^- (list card)
:: local
?: (team:title our.bol src.bol)
=/ shp ?:(=(path /~/default) our.bol (~(got by synced) path))
=/ appl ?:(=(shp our.bol) %contact-store %contact-hook)
[%pass / %agent [shp appl] %poke %contact-action !>(act)]~
:: foreign
=/ shp (~(got by synced) path)
?. |(=(shp our.bol) =(src.bol ship)) ~
:: scry group to check if ship is a member
=/ =group (need (group-scry path))
?. (~(has in group) shp) ~
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]~
--
::
++ handle-contact-action
|= [=path =ship act=contact-action]
^- (list card)
:: local
?: (team:title our.bol src.bol)
?. (~(has by synced) path) ~
=/ shp ?:(=(path /~/default) our.bol (~(got by synced) path))
=/ appl ?:(=(shp our.bol) %contact-store %contact-hook)
[%pass / %agent [shp appl] %poke %contact-action !>(act)]~
:: foreign
=/ shp (~(got by synced) path)
?. |(=(shp our.bol) =(src.bol ship)) ~
:: scry group to check if ship is a member
=/ =group (need (group-scry path))
?. (~(has in group) shp) ~
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]~
::
++ poke-hook-action
|= act=contact-hook-action
@ -160,7 +160,9 @@
[~ state]
=. synced (~(put by synced) path.act our.bol)
:_ state
[%pass contact-path %agent [our.bol %contact-store] %watch contact-path]~
:~ [%pass contact-path %agent [our.bol %contact-store] %watch contact-path]
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]
==
::
%add-synced
?> (team:title our.bol src.bol)
@ -168,7 +170,9 @@
=. synced (~(put by synced) path.act ship.act)
=/ contact-path [%contacts path.act]
:_ state
[%pass contact-path %agent [ship.act %contact-hook] %watch contact-path]~
:~ [%pass contact-path %agent [ship.act %contact-hook] %watch contact-path]
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]
==
::
%remove
=/ ship (~(get by synced) path.act)
@ -179,13 +183,20 @@
%- zing
:~ (pull-wire [%contacts path.act])
[%give %kick ~[[%contacts path.act]] ~]~
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]~
==
?. |(=(u.ship src.bol) (team:title our.bol src.bol))
:: if neither ship = source or source = us, do nothing
[~ state]
:: delete a foreign ship's path
:- (pull-wire [%contacts path.act])
state(synced (~(del by synced) path.act))
=/ cards
(handle-contact-action path.act our.bol [%remove path.act our.bol])
:_ state(synced (~(del by synced) path.act))
%- zing
:~ (pull-wire [%contacts path.act])
[%give %fact [/synced]~ %contact-hook-update !>([%initial synced])]~
cards
==
==
::
++ watch-contacts
@ -197,10 +208,13 @@
=/ =group (need (group-scry pax))
?> (~(has in group) src.bol)
=/ contacts (need (contacts-scry pax))
:~ :*
%give %fact ~ %contact-update
!>([%contacts pax contacts])
== ==
[%give %fact ~ %contact-update !>([%contacts pax contacts])]~
::
++ watch-synced
|= pax=path
^- (list card)
?> (team:title our.bol src.bol)
[%give %fact ~ %contact-hook-update !>([%initial synced])]~
::
++ watch-ack
|= [wir=wire saw=(unit tang)]
@ -308,13 +322,15 @@
==
::
%add
=/ owner (~(got by synced) path.fact)
?> |(=(owner src.bol) =(src.bol ship.fact))
=/ owner (~(get by synced) path.fact)
?~ owner ~
?> |(=(u.owner src.bol) =(src.bol ship.fact))
~[(contact-poke [%add path.fact ship.fact contact.fact])]
::
%remove
=/ owner (~(got by synced) path.fact)
?> |(=(owner src.bol) =(src.bol ship.fact))
=/ owner (~(get by synced) path.fact)
?~ owner ~
?> |(=(u.owner src.bol) =(src.bol ship.fact))
%+ welp
:~ (group-poke [%remove [ship.fact ~ ~] path.fact])
(contact-poke [%remove path.fact ship.fact])
@ -353,7 +369,8 @@
|= =path
^- (quip card _state)
?. (~(has by synced) path)
[~ state]
:_ state
[(contact-poke [%delete path])]~
:_ state(synced (~(del by synced) path))
:~ [%pass [%contacts path] %agent [our.bol %contact-store] %leave ~]
[(contact-poke [%delete path])]

View File

@ -175,7 +175,7 @@
|= [=path =ship]
^- (quip card _state)
=/ contacts (~(got by rolodex) path)
?> (~(has by contacts) ship)
?. (~(has by contacts) ship) [~ state]
=. contacts (~(del by contacts) ship)
:- (send-diff path [%remove path ship])
state(rolodex (~(put by rolodex) path contacts))

View File

@ -147,9 +147,9 @@
::
%delete
%+ weld
:~ (group-poke [%unbundle path.act])
:~ (contact-hook-poke [%remove path.act])
(group-poke [%unbundle path.act])
(contact-poke [%delete path.act])
(contact-hook-poke [%remove path.act])
==
(delete-metadata path.act)
::

View File

@ -38,18 +38,12 @@
^- (quip card _this)
=/ old !<(state-zero vase)
:_ this(state old)
%+ murn
~(tap by synced.old)
%+ murn ~(tap by synced.old)
|= [=path =ship]
^- (unit card)
=/ =wire
[(scot %p ship) %group path]
=/ =term
?: =(our.bowl ship)
%group-store
%group-hook
?: (~(has by wex.bowl) [wire ship term])
~
=/ =wire [(scot %p ship) %group path]
=/ =term ?:(=(our.bowl ship) %group-store %group-hook)
?: (~(has by wex.bowl) [wire ship term]) ~
`[%pass wire %agent [ship term] %watch [%group path]]
::
++ on-leave on-leave:def
@ -173,10 +167,9 @@
%remove [(update-subscribers [%group pax.diff] diff) state]
::
%unbundle
:_ state(synced (~(del by synced.state) pax.diff))
%+ snoc
(update-subscribers [%group pax.diff] diff)
[%give %kick [%group pax.diff]~ ~]
=/ ship (~(get by synced.state) pax.diff)
?~ ship [~ state]
(poke-group-hook-action [%remove pax.diff])
==
::
++ handle-foreign
@ -185,7 +178,6 @@
?- -.diff
%keys [~ state]
%bundle [~ state]
::
%path
:_ state
?~ pax.diff ~
@ -219,23 +211,24 @@
[(group-poke pax.diff diff)]~
::
%remove
:_ state
?~ pax.diff ~
?~ pax.diff [~ state]
=/ ship (~(get by synced.state) pax.diff)
?~ ship ~
?. =(src.bol u.ship) ~
[(group-poke pax.diff diff)]~
?~ ship [~ state]
?. =(src.bol u.ship) [~ state]
?. (~(has in members.diff) our.bol) [~ state]
=/ changes (poke-group-hook-action [%remove pax.diff])
:_ +.changes
%+ welp -.changes
:~ (group-poke pax.diff diff)
(group-poke pax.diff [%unbundle pax.diff])
==
::
%unbundle
?~ pax.diff
[~ state]
?~ pax.diff [~ state]
=/ ship (~(get by synced.state) pax.diff)
?~ ship
[~ state]
?. =(src.bol u.ship)
[~ state]
:_ state(synced (~(del by synced.state) pax.diff))
[(group-poke pax.diff diff)]~
?~ ship [~ state]
?. =(src.bol u.ship) [~ state]
(poke-group-hook-action [%remove pax.diff])
==
::
++ group-poke
@ -262,5 +255,4 @@
?: =(u.shp our.bol)
[%pass wir %agent [our.bol %group-store] %leave ~]~
[%pass wir %agent [u.shp %group-hook] %leave ~]~
::
--

View File

@ -288,12 +288,30 @@
++ handle-metadata-update
|= upd=metadata-update
^- (quip card _state)
?. ?=(%remove -.upd) [~ state]
?> =(%link app-name.resource.upd)
=? listening
?=(~ (groups-from-resource:md %link app-path.resource.upd))
(~(del in listening) app-path.resource.upd)
(leave-from-group app-path.resource.upd group-path.upd)
?+ -.upd [~ state]
%add
?> =(%link app-name.resource.upd)
:: auto-listen to collections in unmanaged groups only
::
?. ?=([%'~' ^] group-path.upd) [~ state]
=, resource.upd
=^ update listening
^- (quip card _listening)
?: (~(has in listening) app-path)
[~ listening]
:- [(send-update %watch app-path)]~
(~(put in listening) app-path)
=^ cards state
(listen-to-group app-path group-path.upd)
[(weld update cards) state]
::
%remove
?> =(%link app-name.resource.upd)
=? listening
?=(~ (groups-from-resource:md %link app-path.resource.upd))
(~(del in listening) app-path.resource.upd)
(leave-from-group app-path.resource.upd group-path.upd)
==
::
:: groups subscriptions
::

View File

@ -1,13 +1,13 @@
:: link-view: frontend endpoints
::
:: endpoints, mapping onto link-store's paths. p is for page as in pagination.
:: updates only work for page 0.
:: only the /0/submissions endpoint provides updates.
:: as with link-store, urls are expected to use +wood encoding.
::
:: /json/[p]/submissions pages for all groups
:: /json/[p]/submissions/[some-group] page for one group
:: /json/[p]/discussions/[wood-url]/[some-group] page for url in group
:: /json/[n]/submission/[wood-url]/[some-group] nth matching submission
:: /json/0/submissions initial + updates for all
:: /json/[p]/submissions/[collection] page for one collection
:: /json/[p]/discussions/[wood-url]/[collection] page for url in collection
:: /json/[n]/submission/[wood-url]/[collection] nth matching submission
:: /json/seen mark-as-read updates
::
/- *link-view,
@ -16,6 +16,7 @@
group-hook, permission-hook, permission-group-hook,
metadata-hook, contact-view
/+ *link, metadata, *server, default-agent, verb, dbug
~% %link-view-top ..is ~
::
|%
+$ state-0
@ -154,20 +155,22 @@
++ on-fail on-fail:def
--
::
~% %link-view-logic ..card ~
|_ =bowl:gall
+* md ~(. metadata bowl)
::
++ page-size 25
++ get-paginated
|* [p=(unit @ud) l=(list)]
^- [total=@ud pages=@ud page=_l]
:+ (lent l)
%+ add (div (lent l) page-size)
(min 1 (mod (lent l) page-size))
?~ p l
%+ scag page-size
%+ slag (mul u.p page-size)
l
|* [page=(unit @ud) list=(list)]
^- [total=@ud pages=@ud page=_list]
=/ l=@ud (lent list)
:+ l
%+ add (div l page-size)
(min 1 (mod l page-size))
?~ page list
%+ swag
[(mul u.page page-size) page-size]
list
::
++ page-to-json
=, enjs:format
@ -488,9 +491,12 @@
:: }
::
++ give-initial-submissions
|= [p=@ud =path]
~/ %link-view-initial-submissions
|= [p=@ud =requested=path]
^- (list card)
:_ ?: =(0 p) ~
:_ :: only keep the base case alive (for updates), kick all others
::
?: &(=(0 p) ?=(~ requested-path)) ~
[%give %kick ~ ~]~
=; =json
[%give %fact ~ %json !>(json)]
@ -498,9 +504,9 @@
%- pairs:enjs:format
%+ turn
%~ tap by
%+ scry-for (map ^path submissions)
[%submissions path]
|= [=^path =submissions]
%+ scry-for (map path submissions)
[%submissions requested-path]
|= [=path =submissions]
^- [@t json]
:- (spat path)
=; =json
@ -513,6 +519,15 @@
%~ wyt in
%+ scry-for (set url)
[%unseen path]
?: &(=(0 p) ?=(~ requested-path))
:: for a broad-scope initial result, only give total counts
::
=, enjs:format
%- pairs
=+ l=(lent submissions)
:~ 'totalItems'^(numb l)
'totalPages'^(numb (div l page-size))
==
%^ page-to-json p
%+ get-paginated `p
submissions

View File

@ -1938,8 +1938,6 @@
=/ old-comment (~(get by comments.u.note) comment-date.del)
?~ old-comment
[~ sty]
?: =(our.bol author.u.old-comment)
[~ sty]
=. comments.u.note (~(put by comments.u.note) comment-date.del data.del)
=. notes.u.book (~(put by notes.u.book) note.del u.note)
(emit-updates-and-state host.del book.del u.book del sty)

View File

@ -1,4 +1,4 @@
/- *contact-view
/- *contact-view, *contact-hook
/+ base64
|%
++ nu :: parse number as hex
@ -6,6 +6,17 @@
?> ?=({$s *} jon)
(rash p.jon hex)
::
++ hook-update-to-json
|= upd=contact-hook-update
=, enjs:format
^- json
%+ frond %contact-hook-update
%- pairs
%+ turn ~(tap by synced.upd)
|= [pax=^path shp=^ship]
^- [cord json]
[(spat pax) s+(scot %p shp)]
::
++ rolodex-to-json
|= rolo=rolodex
=, enjs:format

View File

@ -0,0 +1,13 @@
/+ *contact-json
|_ upd=contact-hook-update
++ grow
|%
++ json (hook-update-to-json upd)
--
::
++ grab
|%
++ noun contact-hook-update
--
::
--

View File

@ -83,7 +83,7 @@
:~ who+(su fed:ag)
book+so
note+so
comment+(su ;~(pfix sig (cook year when:^so)))
comment+so
body+so
==
::
@ -96,7 +96,7 @@
:~ who+(su fed:ag)
book+so
note+so
comment+(su ;~(pfix sig (cook year when:^so)))
comment+so
==
++ subscribe
%- ot

View File

@ -12,4 +12,7 @@
::
[%remove =path]
==
::
+$ synced (map path ship)
+$ contact-hook-update [%initial =synced]
--

View File

@ -23,7 +23,7 @@ import qualified Urbit.Time as Time
data AmesDrv = AmesDrv
{ aTurfs :: TVar (Maybe [Turf])
, aGalaxies :: IORef (M.Map Galaxy (Async (), TQueue ByteString))
, aSocket :: Maybe Socket
, aSocket :: TVar (Maybe Socket)
, aListener :: Async ()
, aSendingQueue :: TQueue (SockAddr, ByteString)
, aSendingThread :: Async ()
@ -88,8 +88,6 @@ renderGalaxy = Ob.renderPatp . Ob.patp . fromIntegral . unPatp
enqueueEv -- Queue-event action.
mPort -- Explicit port override from command line arguments.
TODO Handle socket exceptions in waitPacket
4096 is a reasonable number for recvFrom. Packets of that size are
not possible on the internet.
@ -114,7 +112,8 @@ ames inst who isFake enqueueEv stderr =
start = do
aTurfs <- newTVarIO Nothing
aGalaxies <- newIORef mempty
aSocket <- bindSock
aSocket <- newTVarIO Nothing
bindSock aSocket
aListener <- async (waitPacket aSocket)
aSendingQueue <- newTQueueIO
aSendingThread <- async (sendingThread aSendingQueue aSocket)
@ -135,11 +134,11 @@ ames inst who isFake enqueueEv stderr =
cancel aSendingThread
cancel aListener
io $ maybeM (pure ()) (close') (pure aSocket)
-- io $ close' aSocket
socket <- atomically $ readTVar aSocket
io $ maybeM (pure ()) (close') (pure socket)
bindSock :: RIO e (Maybe Socket)
bindSock = getBindAddr >>= doBindSocket
bindSock :: TVar (Maybe Socket) -> RIO e ()
bindSock socketVar = getBindAddr >>= doBindSocket
where
getBindAddr = netMode >>= \case
Fake -> pure $ Just localhost
@ -147,8 +146,8 @@ ames inst who isFake enqueueEv stderr =
Real -> pure $ Just inaddrAny
NoNetwork -> pure Nothing
doBindSocket :: Maybe HostAddress -> RIO e (Maybe Socket)
doBindSocket Nothing = pure Nothing
doBindSocket :: Maybe HostAddress -> RIO e ()
doBindSocket Nothing = atomically $ writeTVar socketVar Nothing
doBindSocket (Just bindAddr) = do
mode <- netMode
mPort <- view (networkConfigL . ncAmesPort)
@ -159,16 +158,28 @@ ames inst who isFake enqueueEv stderr =
let addr = SockAddrInet ourPort bindAddr
() <- io $ bind s addr
pure $ Just s
atomically $ writeTVar socketVar (Just s)
waitPacket :: TVar (Maybe Socket) -> RIO e ()
waitPacket socketVar = do
(atomically $ readTVar socketVar) >>= \case
Nothing -> pure ()
Just s -> do
res <- io $ tryIOError $ recvFrom s 4096
case res of
Left exn -> do
-- When we have a socket exception, we need to rebuild the
-- socket.
logTrace $ displayShow ("(ames) Socket exception. Rebinding.")
bindSock socketVar
Right (bs, addr) -> do
logTrace $ displayShow ("(ames) Received packet from ", addr)
case addr of
SockAddrInet p a -> atomically (enqueueEv $ hearEv p a bs)
_ -> pure ()
waitPacket socketVar
waitPacket :: Maybe Socket -> RIO e ()
waitPacket Nothing = pure ()
waitPacket (Just s) = forever $ do
(bs, addr) <- io $ recvFrom s 4096
logTrace $ displayShow ("(ames) Received packet from ", addr)
case addr of
SockAddrInet p a -> atomically (enqueueEv $ hearEv p a bs)
_ -> pure ()
handleEffect :: AmesDrv -> NewtEf -> RIO e ()
handleEffect drv@AmesDrv{..} = \case
@ -216,18 +227,23 @@ ames inst who isFake enqueueEv stderr =
-- An outbound queue of messages. We can only write to a socket from one
-- thread, so coalesce those writes here.
sendingThread :: TQueue (SockAddr, ByteString) -> Maybe Socket -> RIO e ()
sendingThread queue Nothing = pure ()
sendingThread queue (Just socket) = forever $
sendingThread :: TQueue (SockAddr, ByteString)
-> TVar (Maybe Socket)
-> RIO e ()
sendingThread queue socketVar = forever $
do
(dest, bs) <- atomically $ readTQueue queue
logTrace $ displayShow ("(ames) Sending packet to ", socket, dest)
logTrace $ displayShow ("(ames) Sending packet to ", dest)
sendAll bs dest
where
sendAll bs dest = do
bytesSent <- io $ sendTo socket bs dest
when (bytesSent /= BS.length bs) $ do
sendAll (drop bytesSent bs) dest
mybSocket <- atomically $ readTVar socketVar
case mybSocket of
Nothing -> pure ()
Just socket -> do
bytesSent <- io $ sendTo socket bs dest
when (bytesSent /= BS.length bs) $ do
sendAll (drop bytesSent bs) dest
-- Asynchronous thread per galaxy which handles domain resolution, and can
-- block its own queue of ByteStrings to send.

View File

@ -1,5 +1,5 @@
name: urbit-king
version: 0.10.1
version: 0.10.4
license: MIT
license-file: LICENSE

View File

@ -17,17 +17,77 @@ when you want to make a change to it, `|commit %home`.
## Contributing to Landscape applications
If you'd like to contribute to the core set of Landscape applications in this
repository, clone this repository and start by creating an `urbitrc` file in
this folder, [pkg/interface][interface]. You can find an `urbitrc-sample` here
for reference. Then `cd` into the application's folder and `npm install` the
dependencies, then `gulp watch` to watch for changes.
[nix](https://github.com/NixOS/nix) and `git-lfs` should be installed at
this point, and have been used to `make build` the project.
On your development ship, ensure you `|commit %home` to apply your changes.
Once you're done and ready to make a pull request, running `gulp bundle-prod`
will make the production files and deposit them in [pkg/arvo][arvo]. Create a
pull request with both the production files, and the source code you were
working on in the interface directory.
Designing interfaces within urbit/urbit additionally requires that the [instructions](https://urbit.org/using/develop/#creating-a-development-ship) for fake `~zod` initialization have been followed.
Once your fake ship is running and you see
```
~zod:dojo>
```
in your console, be sure to 'mount' your ship's working state (what we call 'desks') to your local machine via the
`|mount %` command. This will ensure that code you modify locally can be
committed to your ship and initialized.
To begin developing Urbit's frontend, you'll need to sync your
currently-running fake ship with the urbit/urbit repo's code. Find the
`urbitrc-sample` file found at `urbit/pkg/interface/urbitrc-sample` (in this folder). Open it
using your preferred code editor and you should see the following:
```
module.exports = {
URBIT_PIERS: [
"/Users/user/ships/zod/home",
]
};
```
Edit the path between quotes `/Users/user/ships/zod/home` with wherever your
fake ship is located on your machine. This zod location path *must* end in `../home` to correctly intitalize
any code you write. Any code edited within the `urbit/urbit`will now be able to be synced to your running
ship, and previewed in the browser.
To set up urbit's Javascript environment, you'll need node (ideally installed
via [nvm](https://github.com/nvm-sh/nvm)) and gulp, which will be installed
via node.
Perform the following steps to get the above set up for urbit's apps:
```
## go to urbit's interface directory and install the required tooling
cd urbit/pkg/interface
npm install
npm install -g gulp
## assuming you are still in `urbit/pkg/interface`,
## open a single app directory, and watch it for changes
cd contacts/
gulp watch
```
Any changes made to any files within the `/contacts` directory will now
trigger a gulp rebuild when saved. To sync these changes to your running
ship, enter dojo and input the following:
```
|commit %home
```
Your urbit should take a moment to process the changes, and will emit a
`>=`. Refreshing your browser will display the newly-rendered interface.
Once you are done editing code, and wish to commit changes to git, stop
`gulp watch` and run `gulp bundle-prod` to ensure you are only
committing 1 minified line of compiled js and not 3000+.
An additional note:
As compiled Javascript is not present in the urbit/urbit repository,
you'll need to run `.sh/build-interface` in order to see changes that
have been committed to any given branch you might be working on. It's
always a good idea to run the above command before starting development
to ensure you can see collaborators' changes.
Please also ensure your pull request fits our standards for
[Git hygiene][contributing].
@ -72,4 +132,4 @@ running.
[template]: https://github.com/urbit/create-landscape-app/generate
[gall]: https://urbit.org/docs/learn/arvo/gall/
[chat]: /pkg/arvo/app/chat.hoon
[publish]: /pkg/arvo/app/publish.hoon
[publish]: /pkg/arvo/app/publish.hoon

View File

@ -7,6 +7,8 @@ var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var rename = require('gulp-rename');
var del = require('del');
var json = require('rollup-plugin-json');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
@ -69,6 +71,7 @@ gulp.task('js-imports', function(cb) {
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
resolve()
]
@ -95,6 +98,7 @@ gulp.task('tile-js-imports', function(cb) {
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
resolve()
]
@ -127,6 +131,7 @@ gulp.task('js-imports-prod', function(cb) {
extensions: '.js'
}),
globals(),
json(),
resolve()
]
}, 'umd'))

View File

@ -537,6 +537,11 @@
"now-and-later": "^2.0.0"
}
},
"bail": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
"integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ=="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -857,6 +862,21 @@
}
}
},
"character-entities": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="
},
"character-entities-legacy": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="
},
"character-reference-invalid": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
},
"chokidar": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
@ -1017,8 +1037,7 @@
"clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
"dev": true
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
},
"clone-buffer": {
"version": "1.0.0",
@ -1060,6 +1079,16 @@
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
"codemirror": {
"version": "5.52.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.52.2.tgz",
"integrity": "sha512-WCGCixNUck2HGvY8/ZNI1jYfxPG5cRHv0VjmWuNzbtCLz8qYA5d+je4QhSSCtCaagyeOwMi/HmmPTjBgiTm2lQ=="
},
"collapse-white-space": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz",
"integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ=="
},
"collect-stream": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/collect-stream/-/collect-stream-1.2.1.tgz",
@ -1542,7 +1571,6 @@
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz",
"integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"entities": "^2.0.0"
@ -1551,8 +1579,7 @@
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
"dev": true
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
}
}
},
@ -1562,6 +1589,21 @@
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"dev": true
},
"domhandler": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz",
"integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==",
"requires": {
"domelementtype": "^2.0.1"
},
"dependencies": {
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
}
}
},
"domutils": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
@ -1621,8 +1663,7 @@
"entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
"dev": true
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
},
"error-ex": {
"version": "1.3.2",
@ -1776,8 +1817,7 @@
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extend-shallow": {
"version": "1.1.4",
@ -3229,6 +3269,45 @@
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
"dev": true
},
"html-to-react": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.2.tgz",
"integrity": "sha512-TdTfxd95sRCo6QL8admCkE7mvNNrXtGoVr1dyS+7uvc8XCqAymnf/6ckclvnVbQNUo2Nh21VPwtfEHd0khiV7g==",
"requires": {
"domhandler": "^3.0",
"htmlparser2": "^4.0",
"lodash.camelcase": "^4.3.0",
"ramda": "^0.26"
}
},
"htmlparser2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz",
"integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==",
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0",
"domutils": "^2.0.0",
"entities": "^2.0.0"
},
"dependencies": {
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
},
"domutils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.0.0.tgz",
"integrity": "sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg==",
"requires": {
"dom-serializer": "^0.2.1",
"domelementtype": "^2.0.1",
"domhandler": "^3.0.0"
}
}
}
},
"http-https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/http-https/-/http-https-1.0.0.tgz",
@ -3336,6 +3415,20 @@
"kind-of": "^3.0.2"
}
},
"is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="
},
"is-alphanumerical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
"requires": {
"is-alphabetical": "^1.0.0",
"is-decimal": "^1.0.0"
}
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@ -3391,6 +3484,11 @@
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
"dev": true
},
"is-decimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="
},
"is-descriptor": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
@ -3444,6 +3542,11 @@
"is-extglob": "^2.1.1"
}
},
"is-hexadecimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
@ -3477,6 +3580,11 @@
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz",
"integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg=="
},
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -3548,12 +3656,22 @@
"integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=",
"dev": true
},
"is-whitespace-character": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz",
"integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w=="
},
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
"dev": true
},
"is-word-character": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz",
"integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA=="
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@ -3702,8 +3820,7 @@
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
"dev": true
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
},
"lodash.chunk": {
"version": "4.2.0",
@ -3828,6 +3945,11 @@
"object-visit": "^1.0.0"
}
},
"markdown-escapes": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz",
"integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg=="
},
"matchdep": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
@ -3999,6 +4121,14 @@
}
}
},
"mdast-add-list-metadata": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz",
"integrity": "sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==",
"requires": {
"unist-util-visit-parents": "1.1.2"
}
},
"mdn-data": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
@ -4411,6 +4541,19 @@
"aggregate-error": "^3.0.0"
}
},
"parse-entities": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz",
"integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==",
"requires": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
}
},
"parse-filepath": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
@ -4979,6 +5122,11 @@
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=",
"dev": true
},
"ramda": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
"integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ=="
},
"react": {
"version": "16.10.1",
"resolved": "https://registry.npmjs.org/react/-/react-16.10.1.tgz",
@ -4989,6 +5137,11 @@
"prop-types": "^15.6.2"
}
},
"react-codemirror2": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-6.0.1.tgz",
"integrity": "sha512-rutEKVgvFhWcy/GeVA1hFbqrO89qLqgqdhUr7YhYgIzdyICdlRQv+ztuNvOFQMXrO0fLt0VkaYOdMdYdQgsSUA=="
},
"react-dom": {
"version": "16.10.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.10.1.tgz",
@ -5005,6 +5158,21 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz",
"integrity": "sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw=="
},
"react-markdown": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-4.3.1.tgz",
"integrity": "sha512-HQlWFTbDxTtNY6bjgp3C3uv1h2xcjCSi1zAEzfBW9OwJJvENSYiLXWNXN5hHLsoqai7RnZiiHzcnWdXk2Splzw==",
"requires": {
"html-to-react": "^1.3.4",
"mdast-add-list-metadata": "1.0.1",
"prop-types": "^15.7.2",
"react-is": "^16.8.6",
"remark-parse": "^5.0.0",
"unified": "^6.1.5",
"unist-util-visit": "^1.3.0",
"xtend": "^4.0.1"
}
},
"react-router": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
@ -5287,6 +5455,36 @@
}
}
},
"remark-disable-tokenizers": {
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/remark-disable-tokenizers/-/remark-disable-tokenizers-1.0.24.tgz",
"integrity": "sha512-HsAmBY5cNliHYAzba4zuskZzkDdp6sG+tRelDb4AoPo2YHNGHnxYsatShzTIsnRNLgCbsxycW5Ge6KigHn701A==",
"requires": {
"clone": "^2.1.2"
}
},
"remark-parse": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz",
"integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==",
"requires": {
"collapse-white-space": "^1.0.2",
"is-alphabetical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-whitespace-character": "^1.0.0",
"is-word-character": "^1.0.0",
"markdown-escapes": "^1.0.0",
"parse-entities": "^1.1.0",
"repeat-string": "^1.5.4",
"state-toggle": "^1.0.0",
"trim": "0.0.1",
"trim-trailing-lines": "^1.0.0",
"unherit": "^1.0.4",
"unist-util-remove-position": "^1.0.0",
"vfile-location": "^2.0.0",
"xtend": "^4.0.1"
}
},
"remove-bom-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz",
@ -5328,14 +5526,12 @@
"repeat-string": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
"dev": true
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
},
"replace-ext": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
"integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
"dev": true
"integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs="
},
"replace-homedir": {
"version": "1.0.0",
@ -5459,6 +5655,15 @@
"rollup-pluginutils": "^2.6.0"
}
},
"rollup-plugin-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz",
"integrity": "sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow==",
"dev": true,
"requires": {
"rollup-pluginutils": "^2.5.0"
}
},
"rollup-plugin-node-globals": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-globals/-/rollup-plugin-node-globals-1.4.0.tgz",
@ -5859,6 +6064,11 @@
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=",
"dev": true
},
"state-toggle": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
"integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ=="
},
"static-extend": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@ -6181,6 +6391,21 @@
"resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-1.15.3.tgz",
"integrity": "sha512-ThJH58GNFKhCw3gIoOtwf3tNwuYjbyEeiGdeq4mNMYWdJctnI896KUqn6PVt7jmNVepqa1bcKQtnMB1HtjsDMA=="
},
"trim": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
"integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0="
},
"trim-trailing-lines": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz",
"integrity": "sha512-4ku0mmjXifQcTVfYDfR5lpgV7zVqPg6zV9rdZmwOPqq0+Zq19xDqEgagqVbc4pOOShbncuAOIs59R3+3gcF3ZA=="
},
"trough": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
},
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
@ -6222,6 +6447,28 @@
"integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=",
"dev": true
},
"unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
"integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==",
"requires": {
"inherits": "^2.0.0",
"xtend": "^4.0.0"
}
},
"unified": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz",
"integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==",
"requires": {
"bail": "^1.0.0",
"extend": "^3.0.0",
"is-plain-obj": "^1.1.0",
"trough": "^1.0.0",
"vfile": "^2.0.0",
"x-is-string": "^0.1.0"
}
},
"union-value": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -6264,6 +6511,47 @@
"through2-filter": "^3.0.0"
}
},
"unist-util-is": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz",
"integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A=="
},
"unist-util-remove-position": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz",
"integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==",
"requires": {
"unist-util-visit": "^1.1.0"
}
},
"unist-util-stringify-position": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz",
"integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ=="
},
"unist-util-visit": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz",
"integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==",
"requires": {
"unist-util-visit-parents": "^2.0.0"
},
"dependencies": {
"unist-util-visit-parents": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz",
"integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==",
"requires": {
"unist-util-is": "^3.0.0"
}
}
}
},
"unist-util-visit-parents": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz",
"integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q=="
},
"unquote": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
@ -6369,6 +6657,30 @@
"integrity": "sha512-fOi47nsJP5Wqefa43kyWSg80qF+Q3XA6MUkgi7Hp1HQaKDQW4cQrK2D0P7mmbFtsV1N89am55Yru/nyEwRubcw==",
"dev": true
},
"vfile": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz",
"integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==",
"requires": {
"is-buffer": "^1.1.4",
"replace-ext": "1.0.0",
"unist-util-stringify-position": "^1.0.0",
"vfile-message": "^1.0.0"
}
},
"vfile-location": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz",
"integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA=="
},
"vfile-message": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz",
"integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==",
"requires": {
"unist-util-stringify-position": "^1.1.1"
}
},
"vinyl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz",
@ -6476,6 +6788,11 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"x-is-string": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz",
"integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI="
},
"xml-lexer": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz",
@ -6496,8 +6813,7 @@
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
"version": "3.2.1",

View File

@ -20,6 +20,7 @@
"gulp-rename": "^1.4.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-root-import": "^0.2.3",
@ -27,13 +28,17 @@
},
"dependencies": {
"classnames": "^2.2.6",
"codemirror": "^5.51.2",
"del": "^5.1.0",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"mousetrap": "^1.6.3",
"react": "^16.5.2",
"react-codemirror2": "^6.0.0",
"react-dom": "^16.8.6",
"react-markdown": "^4.3.1",
"react-router-dom": "^5.0.0",
"remark-disable-tokenizers": "^1.0.24",
"urbit-ob": "^5.0.0",
"urbit-sigil-js": "^1.3.2"
},

View File

@ -169,6 +169,14 @@ h2 {
border-radius: 100%;
}
.green3 {
color: #7ea899;
}
.unread-notice {
top: 48px;
}
/* responsive */
@media all and (max-width: 34.375em) {
@ -187,6 +195,9 @@ h2 {
.embed-container {
padding-bottom: 56.25%;
}
.unread-notice {
top: 96px;
}
}
@media all and (min-width: 34.375em) and (max-width: 46.875em) {
@ -222,6 +233,94 @@ h2 {
}
}
blockquote {
padding-left: 24px;
padding-top: 8px;
padding-bottom: 8px;
margin-left: 0;
margin-right: 0;
margin-top: 8px;
margin-bottom: 8px;
border-left: 1px solid black;
}
:root {
--dark-gray: #555555;
--gray: #7F7F7F;
--medium-gray: #CCCCCC;
--light-gray: rgba(0,0,0,0.08);
}
.react-codemirror2 {
width: 100%;
}
.CodeMirror {
height: 100% !important;
width: 100% !important;
cursor: text;
}
.CodeMirror * {
font-family: 'Inter';
}
.CodeMirror.cm-s-code.cm-s-tlon * {
font-family: 'Source Code Pro';
}
.CodeMirror-selected { background:#BAE3FE !important; color: black; }
pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
.cm-s-tlon span { font-family: "Inter"}
.cm-s-tlon span.cm-meta { color: var(--gray); }
.cm-s-tlon span.cm-number { color: var(--gray); }
.cm-s-tlon span.cm-keyword { line-height: 1em; font-weight: bold; color: var(--gray); }
.cm-s-tlon span.cm-atom { font-weight: bold; color: var(--gray); }
.cm-s-tlon span.cm-def { color: black; }
.cm-s-tlon span.cm-variable { color: black; }
.cm-s-tlon span.cm-variable-2 { color: black; }
.cm-s-tlon span.cm-variable-3, .cm-s-tlon span.cm-type { color: black; }
.cm-s-tlon span.cm-property { color: black; }
.cm-s-tlon span.cm-operator { color: black; }
.cm-s-tlon span.cm-comment { font-family: 'Source Code Pro'; color: black; background-color: var(--light-gray); display: inline-block; border-radius: 2px;}
.cm-s-tlon span.cm-string { color: var(--dark-gray); }
.cm-s-tlon span.cm-string-2 { color: var(--gray); }
.cm-s-tlon span.cm-qualifier { color: #555; }
.cm-s-tlon span.cm-error { color: #FF0000; }
.cm-s-tlon span.cm-attribute { color: var(--gray); }
.cm-s-tlon span.cm-tag { color: var(--gray); }
.cm-s-tlon span.cm-link { color: var(--dark-gray); text-decoration: none;}
.cm-s-tlon .CodeMirror-activeline-background { background: var(--gray); }
.cm-s-tlon .CodeMirror-cursor {
border-left: 2px solid #3687FF;
}
.cm-s-tlon span.cm-builtin { color: var(--gray); }
.cm-s-tlon span.cm-bracket { color: var(--gray); }
/* .cm-s-tlon { font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;} */
.cm-s-tlon .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
.CodeMirror-hints.tlon {
/* font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; */
color: #616569;
background-color: #ebf3fd !important;
}
.CodeMirror-hints.tlon .CodeMirror-hint-active {
background-color: #a2b8c9 !important;
color: #5c6065 !important;
}
.title-input[placeholder]:empty:before {
content: attr(placeholder);
color: #7F7F7F;
}
/* dark */
@media (prefers-color-scheme: dark) {
@ -283,4 +382,85 @@ h2 {
.hover-bg-gray1-d:hover {
background-color: #4d4d4d;
}
blockquote {
border-left: 1px solid white;
}
.contrast-10-d {
filter: contrast(0.1);
}
.bg-none-d {
background: none;
}
/* codemirror */
.cm-s-tlon.CodeMirror {
background: #333;
color: #fff;
}
.cm-s-tlon span.cm-def {
color: white;
}
.cm-s-tlon span.cm-variable {
color: white;
}
.cm-s-tlon span.cm-variable-2 {
color: white;
}
.cm-s-tlon span.cm-variable-3,
.cm-s-tlon span.cm-type {
color: white;
}
.cm-s-tlon span.cm-property {
color: white;
}
.cm-s-tlon span.cm-operator {
color: white;
}
.cm-s-tlon span.cm-string {
color: var(--gray);
}
.cm-s-tlon span.cm-string-2 {
color: var(--gray);
}
.cm-s-tlon span.cm-attribute {
color: var(--gray);
}
.cm-s-tlon span.cm-tag {
color: var(--gray);
}
.cm-s-tlon span.cm-link {
color: var(--gray);
}
/* set rules w/ both color & bg-color last to preserve legibility */
.CodeMirror-selected {
background: var(--medium-gray) !important;
color: white;
}
.cm-s-tlon span.cm-comment {
color: black;
display: inline-block;
padding: 0;
background-color: rgba(255,255,255, 0.3);
border-radius: 2px;
}
}
/* CodeMirror styling */

View File

@ -1,4 +1,6 @@
@import '../node_modules/codemirror/lib/codemirror.css';
@import '../node_modules/codemirror/theme/material.css';
@import "css/indigo-static.css";
@import "css/fonts.css";
@import "css/custom.css";

View File

@ -120,7 +120,9 @@ class UrbitApi {
}
};
this.action("chat-hook", "json", data);
this.action("chat-hook", "json", data).then(() => {
this.chatRead(path);
})
data.message.envelope.author = data.message.envelope.author.substr(1);
this.addPendingMessage(data.message);
}

View File

@ -1,72 +1,83 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
import moment from 'moment';
import { Route, Link } from "react-router-dom";
import { store } from "/store";
import { ResubscribeElement } from '/components/lib/resubscribe-element';
import { BacklogElement } from '/components/lib/backlog-element';
import { Message } from '/components/lib/message';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { ChatTabBar } from '/components/lib/chat-tabbar';
import { ChatInput } from '/components/lib/chat-input';
import { UnreadNotice } from '/components/lib/unread-notice';
import { deSig } from '/lib/util';
function getNumPending(props) {
const result = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station).length
: 0;
return result;
}
export class ChatScreen extends Component {
constructor(props) {
super(props);
this.state = {
numPages: 1,
scrollLocked: false
scrollLocked: false,
// only for FF
lastScrollHeight: null,
scrollBottom: true
};
this.hasAskedForMessages = false;
this.lastNumPending = 0;
this.scrollContainer = null;
this.onScroll = this.onScroll.bind(this);
this.updateReadInterval = setInterval(
this.updateReadNumber.bind(this),
1000
);
this.unreadMarker = null;
moment.updateLocale('en', {
calendar: {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'DD/MM/YYYY'
}
});
}
componentDidMount() {
this.updateReadNumber();
this.askForMessages();
this.scrollToBottom();
}
componentWillUnmount() {
if (this.updateReadInterval) {
clearInterval(this.updateReadInterval);
this.updateReadInterval = null;
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (
prevProps.match.params.station !== props.match.params.station ||
prevProps.match.params.ship !== props.match.params.ship
) {
this.hasAskedForMessages = false;
if (props.envelopes.length < 100) {
this.askForMessages();
}
clearInterval(this.updateReadInterval);
this.setState(
{ scrollLocked: false },
() => {
this.scrollToBottom();
this.updateReadInterval = setInterval(
this.updateReadNumber.bind(this),
1000
);
this.updateReadNumber();
}
);
} else if (props.chatInitialized &&
@ -79,15 +90,27 @@ export class ChatScreen extends Component {
) {
this.hasAskedForMessages = false;
}
}
updateReadNumber() {
const { props, state } = this;
if (props.read < props.length) {
props.api.chat.read(props.station);
// FF logic
if (
navigator.userAgent.includes("Firefox") &&
(props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages)
) {
if(state.scrollBottom) {
setTimeout(() => {
this.scrollToBottom();
})
} else {
this.recalculateScrollTop();
}
this.lastNumPending = getNumPending(props);
}
}
askForMessages() {
const { props, state } = this;
@ -114,20 +137,45 @@ export class ChatScreen extends Component {
props.subscription.fetchMessages(start + 1, end, props.station);
}
}
scrollToBottom() {
if (!this.state.scrollLocked && this.scrollElement) {
this.scrollElement.scrollIntoView({ behavior: "smooth" });
this.scrollElement.scrollIntoView();
}
}
// Restore chat position on FF when new messages come in
recalculateScrollTop() {
if(!this.scrollContainer) {
return;
}
const { lastScrollHeight } = this.state;
let target = this.scrollContainer;
let newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight;
if(target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
return;
}
target.scrollTop = target.scrollHeight - lastScrollHeight;
}
onScroll(e) {
if (
navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
) {
// Google Chrome
// Google Chrome and Firefox
if (e.target.scrollTop === 0) {
// Save scroll position for FF
if (navigator.userAgent.includes('Firefox')) {
this.setState({
lastScrollHeight: e.target.scrollHeight
})
}
this.setState(
{
numPages: this.state.numPages + 1,
@ -143,8 +191,11 @@ export class ChatScreen extends Component {
) {
this.setState({
numPages: 1,
scrollLocked: false
scrollLocked: false,
scrollBottom: true
});
} else if (navigator.userAgent.includes('Firefox')) {
this.setState({ scrollBottom: false });
}
} else if (navigator.userAgent.includes("Safari")) {
// Safari
@ -170,27 +221,49 @@ export class ChatScreen extends Component {
} else {
console.log("Your browser is not supported.");
}
if(!!this.unreadMarker) {
if(
!navigator.userAgent.includes('Firefox') &&
e.target.scrollHeight - e.target.scrollTop - (e.target.clientHeight * 1.5) + this.unreadMarker.offsetTop > 50
) {
this.props.api.chat.read(this.props.station);
} else if(navigator.userAgent.includes('Firefox') &&
this.unreadMarker.offsetTop - e.target.scrollTop - (e.target.clientHeight / 2) > 0
) {
this.props.api.chat.read(this.props.station);
}
}
}
render() {
chatWindow(unread) {
// Replace with just the "not Firefox" implementation
// when Firefox #1042151 is patched.
const { props, state } = this;
let messages = props.envelopes.slice(0);
let lastMsgNum = messages.length > 0 ? messages.length : 0;
if (messages.length > 100 * state.numPages) {
messages = messages.slice(0, 100 * state.numPages);
}
let pendingMessages = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station)
: [];
pendingMessages.map(function(value) {
pendingMessages.map(function (value) {
return (value.pending = true);
});
let messageElements = pendingMessages.concat(messages).map((msg, i) => {
messages = pendingMessages.concat(messages);
let messageElements = messages.map((msg, i) => {
// Render sigil if previous message is not by the same sender
let aut = ["author"];
let renderSigil =
@ -200,8 +273,13 @@ export class ChatScreen extends Component {
let paddingBot =
_.get(messages[i - 1], aut) !==
_.get(msg, aut, msg.author);
let when = ['when'];
let dayBreak =
moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !==
moment(_.get(messages[i], when)).format('YYYY.MM.DD');
return (
const messageElem = (
<Message
key={msg.uid}
msg={msg}
@ -212,28 +290,133 @@ export class ChatScreen extends Component {
pending={!!msg.pending}
/>
);
if(unread > 0 && i === unread) {
return (
<>
{messageElem}
<div key={'unreads'+ msg.uid} ref={ref => (this.unreadMarker = ref)} className="mv2 green2 flex items-center f9">
<hr className="ma0 w2 b--green2 bt-0" />
<p className="mh4">
New messages below
</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{ dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
)}
<hr style={{ width: 'calc(50% - 48px)' }} className="b--green2 ma0 bt-0"/>
</div>
</>
);
} else if(dayBreak) {
return (
<>
{messageElem}
<div key={'daybreak' + msg.uid} className="pv3 gray2 b--gray2 flex items-center justify-center f9 ">
<p>
{moment(_.get(messages[i], when)).calendar()}
</p>
</div>
</>
);
} else {
return messageElem;
}
});
if (navigator.userAgent.includes("Firefox")) {
return (
<div className="overflow-y-scroll h-100" onScroll={this.onScroll} ref={e => { this.scrollContainer = e; }}>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: "vertical" }}
>
<div
ref={el => {
this.scrollElement = el;
}}></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station} />
) : (<div />)
}
{messageElements}
</div>
</div>
)}
else {
return (
<div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}
>
<div
ref={el => {
this.scrollElement = el;
}}></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station} />
) : (<div />)
}
{messageElements}
</div>
)}
}
render() {
const { props, state } = this;
let messages = props.envelopes.slice(0);
let lastMsgNum = messages.length > 0 ? messages.length : 0;
let group = Array.from(props.permission.who.values());
const isinPopout = props.popout ? "popout/" : "";
let ownerContact = (window.ship in props.contacts)
? props.contacts[window.ship] : false;
let title = props.station.substr(1);
if (props.association && "metadata" in props.association) {
title =
props.association.metadata.title !== ""
? props.association.metadata.title
: props.station.substr(1);
}
const unread = props.length - props.read;
const unreadMsg = unread > 0 && messages[unread - 1];
return (
<div
key={props.station}
className="h-100 w-100 overflow-hidden flex flex-column">
className="h-100 w-100 overflow-hidden flex flex-column relative">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
@ -265,27 +448,14 @@ export class ChatScreen extends Component {
api={props.api}
/>
</div>
<div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}>
<div
ref={el => {
this.scrollElement = el;
}}></div>
{ (
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station} />
) : (<div/>)
}
{messageElements}
</div>
{ !!unreadMsg && (
<UnreadNotice
unread={unread}
unreadMsg={unreadMsg}
onRead={() => props.api.chat.read(props.station)}
/>
) }
{this.chatWindow(unread)}
<ChatInput
api={props.api}
numMsgs={lastMsgNum}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class BacklogElement extends Component {
render() {
let props = this.props;
return (
<div className="center mw6">
<div className="db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d white-d flex items-center">
<img className="invert-d spin-active v-mid"
src="/~chat/img/Spinner.png"
width={16}
height={16}
/>
<p className="lh-copy db ml3">
Past messages are being restored
</p>
</div>
</div>
);
}
}

View File

@ -1,40 +1,43 @@
import React, { Component } from 'react';
import _ from 'lodash';
import moment from 'moment';
import Mousetrap from 'mousetrap';
import cn from 'classnames';
import { UnControlled as CodeEditor } from 'react-codemirror2';
import CodeMirror from 'codemirror';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import { Sigil } from '/components/lib/icons/sigil';
import { uuid, uxToHex, hexToRgba } from '/lib/util';
import { uxToHex, hexToRgba } from '/lib/util';
// line height
const INPUT_LINE_HEIGHT = 28;
const INPUT_TOP_PADDING = 3;
function getAdvance(a, b) {
let res = '';
if(!a) {
return b;
const MARKDOWN_CONFIG = {
name: 'markdown',
tokenTypeOverrides: {
header: 'presentation',
quote: 'presentation',
list1: 'presentation',
list2: 'presentation',
list3: 'presentation',
hr: 'presentation',
image: 'presentation',
imageAltText: 'presentation',
imageMarker: 'presentation',
formatting: 'presentation',
linkInline: 'presentation',
linkEmail: 'presentation',
linkText: 'presentation',
linkHref: 'presentation'
}
for (let i = 0; i < Math.min(a.length, b.length); i++) {
if (a[i] !== b[i]) {
return res;
}
res = res.concat(a[i]);
}
return res;
}
};
function ChatInputSuggestion({ ship, contacts, selected, onSelect }) {
let contact = contacts[ship];
let color = "#000000";
let sigilClass = "v-mid mix-blend-diff"
const contact = contacts[ship];
let color = '#000000';
let sigilClass = 'v-mid mix-blend-diff';
let nickname;
let nameStyle = {};
const nameStyle = {};
const isSelected = ship === selected;
if (contact) {
const hex = uxToHex(contact.color);
@ -42,7 +45,7 @@ function ChatInputSuggestion({ ship, contacts, selected, onSelect }) {
nameStyle.color = hexToRgba(hex, .7);
nameStyle.textShadow = '0px 0px 0px #000';
nameStyle.filter = 'contrast(1.3) saturate(1.5)';
sigilClass = "v-mid";
sigilClass = 'v-mid';
nickname = contact.nickname;
}
@ -53,7 +56,7 @@ function ChatInputSuggestion({ ship, contacts, selected, onSelect }) {
'f8 pv1 ph3 pointer hover-bg-gray1-d hover-bg-gray4 relative flex items-center',
{
'white-d bg-gray0-d bg-white': !isSelected,
'black-d bg-gray1-d bg-gray4': isSelected,
'black-d bg-gray1-d bg-gray4': isSelected
}
)}
key={ship}
@ -75,7 +78,6 @@ function ChatInputSuggestion({ ship, contacts, selected, onSelect }) {
</p>
</div>
);
}
function ChatInputSuggestions({ suggestions, onSelect, selected, contacts }) {
@ -88,14 +90,16 @@ function ChatInputSuggestions({ suggestions, onSelect, selected, contacts }) {
className={
'absolute black white-d bg-white bg-gray0-d ' +
'w7 pv3 z-1 mt1 ba b--gray1-d b--gray4'
}>
}
>
{suggestions.map(ship =>
(<ChatInputSuggestion
onSelect={onSelect}
key={ship}
selected={selected}
contacts={contacts}
ship={ship} />)
ship={ship}
/>)
)}
</div>
);
@ -107,7 +111,6 @@ export class ChatInput extends Component {
this.state = {
message: '',
textareaHeight: INPUT_LINE_HEIGHT + INPUT_TOP_PADDING + 1,
patpSuggestions: [],
selectedSuggestion: null
};
@ -117,19 +120,18 @@ export class ChatInput extends Component {
this.messageSubmit = this.messageSubmit.bind(this);
this.messageChange = this.messageChange.bind(this);
this.onEnter = this.onEnter.bind(this);
this.patpAutocomplete = this.patpAutocomplete.bind(this);
this.nextAutocompleteSuggestion = this.nextAutocompleteSuggestion.bind(this);
this.completePatp = this.completePatp.bind(this);
this.clearSuggestions = this.clearSuggestions.bind(this);
// Call once per frame @ 60hz
this.textareaInput = _.debounce(this.textareaInput.bind(this), 16);
this.toggleCode = this.toggleCode.bind(this);
this.editor = null;
// perf testing:
/*let closure = () => {
/* let closure = () => {
let x = 0;
for (var i = 0; i < 30; i++) {
x++;
@ -151,29 +153,25 @@ export class ChatInput extends Component {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
: input + ' ago';
},
s : 'just now',
future: "in %s",
future: 'in %s',
ss : '%d sec',
m: "a minute",
mm: "%d min",
h: "an hr",
hh: "%d hrs",
d: "a day",
dd: "%d days",
M: "a month",
MM: "%d months",
y: "a year",
yy: "%d years"
m: 'a minute',
mm: '%d min',
h: 'an hr',
hh: '%d hrs',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years'
}
});
}
componentDidMount() {
this.bindShortcuts();
}
nextAutocompleteSuggestion(backward = false) {
const { patpSuggestions } = this.state;
let idx = patpSuggestions.findIndex(s => s === this.state.selectedSuggestion);
@ -187,36 +185,33 @@ export class ChatInput extends Component {
this.setState({ selectedSuggestion: patpSuggestions[idx] });
}
patpAutocomplete(message, fresh = false) {
const match = /~([a-zA-Z\-]*)$/.exec(message);
if (!match ) {
this.setState({ patpSuggestions: [] })
this.setState({ patpSuggestions: [] });
return;
}
const needle = match[1].toLowerCase();
const matchString = hay => {
const matchString = (hay) => {
hay = hay.toLowerCase();
return hay.startsWith(needle)
|| _.some(_.words(hay), s => s.startsWith(needle));
};
const contacts = _.chain(this.props.contacts)
.defaultTo({})
.map((details, ship) => ({...details, ship }))
.map((details, ship) => ({ ...details, ship }))
.filter(({ nickname, ship }) => matchString(nickname) || matchString(ship))
.map('ship')
.value()
.value();
const suggestions = _.chain(this.props.envelopes)
.defaultTo([])
.map("author")
.map('author')
.uniq()
.reverse()
.filter(matchString)
@ -225,7 +220,7 @@ export class ChatInput extends Component {
.take(5)
.value();
let newState = {
const newState = {
patpSuggestions: suggestions,
selectedSuggestion: suggestions[0]
};
@ -236,108 +231,36 @@ export class ChatInput extends Component {
clearSuggestions() {
this.setState({
patpSuggestions: []
})
});
}
completePatp(suggestion) {
this.setState({
message: this.state.message.replace(
if(!this.editor) {
return;
}
const newMessage = this.editor.getValue().replace(
/[a-zA-Z\-]*$/,
suggestion
),
);
this.editor.setValue(newMessage);
const lastRow = this.editor.lastLine();
const lastCol = this.editor.getLineHandle(lastRow).text.length;
this.editor.setCursor(lastRow, lastCol);
this.setState({
patpSuggestions: []
});
}
onEnter(e) {
if (this.state.patpSuggestions.length !== 0) {
this.completePatp(this.state.selectedSuggestion);
} else {
this.messageSubmit(e);
}
}
bindShortcuts() {
let mousetrap = Mousetrap(this.textareaRef.current);
mousetrap.bind('enter', e => {
e.preventDefault();
e.stopPropagation();
this.onEnter(e);
});
mousetrap.bind('tab', e => {
e.preventDefault();
e.stopPropagation();
if(this.state.patpSuggestions.length === 0) {
this.patpAutocomplete(this.state.message, true);
} else {
this.nextAutocompleteSuggestion(false);
}
});
mousetrap.bind(['up', 'shift+tab'], e => {
if(this.state.patpSuggestions.length !== 0) {
e.preventDefault();
e.stopPropagation();
this.nextAutocompleteSuggestion(true)
}
});
mousetrap.bind('down', e => {
if(this.state.patpSuggestions.length !== 0) {
e.preventDefault();
e.stopPropagation();
this.nextAutocompleteSuggestion(false)
}
});
mousetrap.bind('esc', e => {
if(this.state.patpSuggestions.length !== 0) {
e.preventDefault();
e.stopPropagation();
this.clearSuggestions();
}})
}
messageChange(event) {
const message = event.target.value;
this.setState({
message
});
messageChange(editor, data, value) {
const { patpSuggestions } = this.state;
if(patpSuggestions.length !== 0) {
this.patpAutocomplete(message, false);
this.patpAutocomplete(value, false);
}
}
textareaInput() {
const maxHeight = INPUT_LINE_HEIGHT * 8 + INPUT_TOP_PADDING;
const newHeight = `${Math.min(maxHeight, this.textareaRef.current.scrollHeight)}px`;
this.setState({
textareaHeight: newHeight
});
}
getLetterType(letter) {
if (letter[0] === '#') {
letter = letter.slice(1);
// remove insignificant leading whitespace.
// aces might be relevant to style.
while (letter[0] === '\n') {
letter = letter.slice(1);
}
return {
code: {
expression: letter,
output: undefined
}
}
} else if (letter[0] === '@') {
letter = letter.slice(1);
if (letter.startsWith('/me')) {
letter = letter.slice(3);
// remove insignificant leading whitespace.
// aces might be relevant to style.
while (letter[0] === '\n') {
@ -346,22 +269,21 @@ export class ChatInput extends Component {
return {
me: letter
}
};
} else if (this.isUrl(letter)) {
return {
url: letter
}
};
} else {
return {
text: letter
}
};
}
}
isUrl(string) {
try {
let websiteTest = new RegExp(''
+ /((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source
const websiteTest = new RegExp(String(/((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)
);
return websiteTest.test(string);
} catch (e) {
@ -370,17 +292,31 @@ export class ChatInput extends Component {
}
messageSubmit() {
if(!this.editor) {
return;
}
const { props, state } = this;
const editorMessage = this.editor.getValue();
if (state.message === '') {
if (editorMessage === '') {
return;
}
if(state.code) {
props.api.chat.message(props.station, `~${window.ship}`, Date.now(), {
code: {
expression: editorMessage,
output: undefined
}
});
this.editor.setValue('');
return;
}
let message = [];
state.message.split(" ").map((each) => {
editorMessage.split(' ').map((each) => {
if (this.isUrl(each)) {
if (message.length > 0) {
message = message.join(" ");
message = message.join(' ');
message = this.getLetterType(message);
props.api.chat.message(
props.station,
@ -390,22 +326,20 @@ export class ChatInput extends Component {
);
message = [];
}
let URL = this.getLetterType(each);
const URL = this.getLetterType(each);
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
URL
);
}
else {
} else {
return message.push(each);
}
})
});
if (message.length > 0) {
message = message.join(" ");
message = message.join(' ');
message = this.getLetterType(message);
props.api.chat.message(
props.station,
@ -416,27 +350,88 @@ export class ChatInput extends Component {
message = [];
}
// perf:
//setTimeout(this.closure, 2000);
// perf:
// setTimeout(this.closure, 2000);
this.setState({
message: '',
textareaHeight: INPUT_LINE_HEIGHT + INPUT_TOP_PADDING + 1
});
this.editor.setValue('');
}
toggleCode() {
if(this.state.code) {
this.setState({ code: false });
this.editor.setOption('mode', MARKDOWN_CONFIG);
this.editor.setOption('placeholder', this.props.placeholder);
} else {
this.setState({ code: true });
this.editor.setOption('mode', null);
this.editor.setOption('placeholder', 'Code...');
}
const value = this.editor.getValue();
// Force redraw of placeholder
if(value.length === 0) {
this.editor.setValue(' ');
this.editor.setValue('');
}
}
render() {
const { props, state } = this;
let color = !!props.ownerContact
const color = props.ownerContact
? uxToHex(props.ownerContact.color) : '000000';
let sigilClass = !!props.ownerContact
? "" : "mix-blend-diff";
const sigilClass = props.ownerContact
? '' : 'mix-blend-diff';
const completeActive = this.state.patpSuggestions.length !== 0;
const codeTheme = state.code ? ' code' : '';
const options = {
mode: MARKDOWN_CONFIG,
theme: 'tlon' + codeTheme,
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: 'native',
cursorHeight: 0.85,
placeholder: state.code ? 'Code...' : props.placeholder,
extraKeys: {
Tab: cm =>
completeActive
? this.nextAutocompleteSuggestion()
: this.patpAutocomplete(cm.getValue(), true),
'Shift-Tab': cm =>
completeActive
? this.nextAutocompleteSuggestion(true)
: CodeMirror.Pass,
'Up': cm =>
completeActive
? this.nextAutocompleteSuggestion(true)
: CodeMirror.Pass,
'Escape': cm =>
completeActive
? this.clearSuggestions(true)
: CodeMirror.Pass,
'Down': cm =>
completeActive
? this.nextAutocompleteSuggestion()
: CodeMirror.Pass,
'Enter': cm =>
completeActive
? this.completePatp(state.selectedSuggestion)
: this.messageSubmit(),
'Shift-3': cm =>
cm.getValue().length === 0
? this.toggleCode()
: CodeMirror.Pass
}
};
return (
<div className="pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white bg-gray0-d relative"
style={{ flexGrow: 1 }}>
style={{ flexGrow: 1 }}
>
{state.patpSuggestions.length !== 0 && (
<ChatInputSuggestions
onSelect={this.completePatp}
@ -449,33 +444,40 @@ export class ChatInput extends Component {
<div
className="fl"
style={{
marginTop: 4,
marginTop: 6,
flexBasis: 24,
height: 24
}}>
}}
>
<Sigil
ship={window.ship}
size={24}
color={`#${color}`}
classes={sigilClass}
/>
</div>
<div className="fr h-100 flex bg-gray0-d" style={{ flexGrow: 1 }}>
<textarea
className={"pl3 bn bg-gray0-d white-d lh-copy"}
style={{ flexGrow: 1, height: state.textareaHeight, paddingTop: INPUT_TOP_PADDING, resize: "none" }}
autoCapitalize="none"
autoFocus={(
/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
navigator.userAgent
)) ? false : true}
ref={this.textareaRef}
placeholder={props.placeholder}
value={state.message}
onChange={this.messageChange}
onInput={this.textareaInput}
/>
</div>
<div
className="fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center"
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 48px)' }}
>
<CodeEditor
options={options}
editorDidMount={(editor) => {
this.editor = editor;
}}
onChange={(e, d, v) => this.messageChange(e, d, v)}
/>
</div>
<div style={{ height: '24px', width: '24px', flexBasis: 24, marginTop: 6 }}>
<img
style={{ filter: state.code && 'invert(100%)', height: '100%', width: '100%' }}
onClick={this.toggleCode}
src="/~chat/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d"
/>
</div>
</div>
);
}

View File

@ -6,7 +6,34 @@ import { uxToHex, cite, writeText } from '/lib/util';
import urbitOb from 'urbit-ob';
import moment from 'moment';
import _ from 'lodash';
import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
const DISABLED_BLOCK_TOKENS = [
'indentedCode',
'blockquote',
'atxHeading',
'thematicBreak',
'list',
'setextHeading',
'html',
'definition',
'table',
];
const DISABLED_INLINE_TOKENS = [
'autoLink',
'url',
'email',
'link',
'reference'
];
const MessageMarkdown = React.memo(
props => (<ReactMarkdown
{...props}
plugins={[[RemarkDisableTokenizers, { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }]]}
/>));
export class Message extends Component {
constructor() {
@ -125,10 +152,11 @@ export class Message extends Component {
</p>
);
} else {
let text = letter.text.split ('\n').map ((item, i) => <p className='f7 lh-copy v-top' key={i}>{item}</p>);
return (
<section>
{text}
<MessageMarkdown
source={letter.text}
/>
</section>
);
}
@ -163,7 +191,7 @@ export class Message extends Component {
return (
<div
className={
"w-100 f8 pl3 pt4 pr3 cf flex lh-copy " + " " + pending
"w-100 f7 pl3 pt4 pr3 cf flex lh-copy " + " " + pending
}
style={{
minHeight: "min-content"
@ -211,7 +239,7 @@ export class Message extends Component {
minHeight: "min-content"
}}>
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
<div className="fr f7 clamp-message white-d pr3" style={{ flexGrow: 1 }}>
<div className="fr f7 clamp-message white-d pr3 lh-copy" style={{ flexGrow: 1 }}>
{this.renderContent()}
</div>
</div>

View File

@ -0,0 +1,40 @@
import React, { Component } from "react";
import classnames from "classnames";
import moment from "moment";
export class UnreadNotice extends Component {
render() {
let { unread, unreadMsg, onRead } = this.props;
let when = moment.unix(unreadMsg.when / 10000);
let datestamp = moment.unix(unreadMsg.when / 1000).format("YYYY.M.D");
let timestamp = moment.unix(unreadMsg.when / 1000).format("HH:mm");
if (datestamp === moment().format("YYYY.M.D")) {
datestamp = null;
}
return (
<div
style={{ left: "0px" }}
className="pa4 w-100 absolute z-1 unread-notice"
>
<div className="ba b--green2 green2 bg-white bg-gray0-d flex items-center pa2 f9 justify-between br1">
<p className="lh-copy db">
{unread} new messages since{" "}
{datestamp && (
<>
<span className="green3">~{datestamp}</span> at{" "}
</>
)}
<span className="green3">{timestamp}</span>
</p>
<div onClick={onRead} className="ml4 inter b--green2 pointer tr lh-copy">
Mark as Read
</div>
</div>
</div>
);
}
}

View File

@ -11,8 +11,7 @@ class UrbitApi {
this.bindPaths = [];
this.contactHook = {
edit: this.contactEdit.bind(this),
remove: this.contactRemove.bind(this)
edit: this.contactEdit.bind(this)
};
this.contactView = {
@ -108,14 +107,6 @@ class UrbitApi {
return this.action("contact-hook", "contact-action", data);
}
contactRemove(path, ship) {
return this.contactHookAction({
remove: {
path, ship
}
});
}
contactEdit(path, ship, editField) {
/* editField can be...
{nickname: ''}

View File

@ -317,7 +317,7 @@ export class ContactCard extends Component {
);
this.setState({awaiting: true, type: "Removing from group"}, (() => {
api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
api.contactView.delete(props.path).then(() => {
let destination = (props.ship === window.ship)
? "" : props.path;
this.setState({awaiting: false});

View File

@ -20,8 +20,11 @@ export class ContactSidebar extends Component {
let responsiveClasses =
props.activeDrawer === "contacts" ? "db" : "dn db-ns";
let me = (window.ship in props.defaultContacts) ?
props.defaultContacts[window.ship] : { color: '0x0', nickname: null};
let me = (window.ship in props.contacts)
? props.contacts[window.ship]
: (window.ship in props.defaultContacts)
? props.defaultContacts[window.ship]
: { color: '0x0', nickname: null };
let shareSheet =
!(window.ship in props.contacts) ?
@ -32,11 +35,23 @@ export class ContactSidebar extends Component {
path={props.path}
selected={props.path + "/" + window.ship === props.selectedContact}
/>
) : (<div></div>);
) : (
<>
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">You</h2>
<ContactItem
ship={window.ship}
nickname={me.nickname}
color={me.color}
path={props.path}
selected={props.path + "/" + window.ship === props.selectedContact}
/>
</>
);
group.delete(window.ship);
let contactItems =
Object.keys(props.contacts)
.filter(c => c !== window.ship)
.map((contact) => {
group.delete(contact);
let path = props.path + "/" + contact;

View File

@ -9,14 +9,7 @@ import { LocalReducer } from '/reducers/local.js';
class Store {
constructor() {
this.state = {
contacts: {},
groups: {},
associations: {},
permissions: {},
invites: {},
selectedGroups: []
};
this.state = this.initialState();
this.initialReducer = new InitialReducer();
this.groupUpdateReducer = new GroupUpdateReducer();
@ -28,6 +21,17 @@ class Store {
this.setState = () => {};
}
initialState() {
return {
contacts: {},
groups: {},
associations: {},
permissions: {},
invites: {},
selectedGroups: []
};
}
setStateHandler(setState) {
this.setState = setState;
}
@ -35,6 +39,11 @@ class Store {
handleEvent(data) {
let json = data.data;
if ('clear' in json && json.clear) {
this.setState(this.initialState());
return;
}
console.log(json);
this.initialReducer.reduce(json, this.state);
this.groupUpdateReducer.reduce(json, this.state);

View File

@ -5,48 +5,63 @@ import urbitOb from 'urbit-ob';
export class Subscription {
constructor() {
this.firstRoundSubscriptionComplete = false;
}
start() {
if (api.authTokens) {
this.initializeContacts();
this.firstRoundSubscription();
window.urb.setOnChannelError(this.onChannelError.bind(this));
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
initializeContacts() {
api.bind('/primary', 'PUT', api.authTokens.ship, 'contact-view',
onChannelError(err) {
console.error('event source error: ', err);
console.log('initiating new channel');
this.firstRoundSubscriptionComplete = false;
setTimeout(2000, () => {
store.handleEvent({
data: { clear : true}
});
this.start();
});
}
subscribe(path, app) {
api.bind(path, 'PUT', api.authTokens.ship, app,
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
api.bind('/primary', 'PUT', api.authTokens.ship, 'invite-view',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
api.bind('/all', 'PUT', api.authTokens.ship, 'metadata-store',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
(err) => {
console.log(err);
this.subscribe(path, app);
},
() => {
this.subscribe(path, app);
});
}
firstRoundSubscription() {
this.subscribe('/primary', 'contact-view');
}
secondRoundSubscriptions() {
this.subscribe('/synced', 'contact-hook');
this.subscribe('/primary', 'invite-view');
this.subscribe('/all', 'group-store');
this.subscribe('/all', 'metadata-store');
}
handleEvent(diff) {
if (!this.firstRoundSubscriptionComplete) {
this.firstRoundSubscriptionComplete = true;
this.secondRoundSubscriptions();
}
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
}
handleQuitSilently(quit) {
// no-op
}
handleQuitAndResubscribe(quit) {
// TODO: resubscribe
}
}
export let subscription = new Subscription();

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react';
export class MessageScreen extends Component {
render() {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
{this.props.text}
</p>
</div>
</div>
);
}
}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import { LoadingScreen } from './loading';
import { MessageScreen } from '/components/lib/message-screen';
import { LinksTabBar } from './lib/links-tabbar';
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { Route, Link } from "react-router-dom";
@ -19,11 +20,18 @@ export class Links extends Component {
this.componentDidUpdate();
}
componentDidUpdate() {
componentDidUpdate(prevProps) {
const linkPage = this.props.page;
if ( (this.props.page != 0) &&
(!this.props.links[linkPage] ||
this.props.links.local[linkPage])
// if we just navigated to this particular page,
// and don't have links for it yet,
// or the links we have might not be complete,
// request the links for that page.
if ( (!prevProps ||
linkPage !== prevProps.page ||
this.props.resourcePath !== prevProps.resourcePath
) &&
!this.props.links[linkPage] ||
this.props.links.local[linkPage]
) {
api.getPage(this.props.resourcePath, this.props.page);
}
@ -50,38 +58,45 @@ export class Links extends Component {
? Number(props.links.totalPages)
: 1;
let LinkList = Object.keys(links)
.map((linkIndex) => {
let linksObj = props.links[linkPage];
let { title, url, time, ship } = linksObj[linkIndex];
const seen = props.seen[url];
let members = {};
let LinkList = (<LoadingScreen/>);
if (props.links && props.links.totalItems === 0) {
LinkList = (
<MessageScreen text="Start by posting a link to this collection."/>
);
} else if (Object.keys(links).length > 0) {
LinkList = Object.keys(links)
.map((linkIndex) => {
let linksObj = props.links[linkPage];
let { title, url, time, ship } = linksObj[linkIndex];
const seen = props.seen[url];
let members = {};
const commentCount = props.comments[url]
? props.comments[url].totalItems
: linksObj[linkIndex].commentCount || 0;
const commentCount = props.comments[url]
? props.comments[url].totalItems
: linksObj[linkIndex].commentCount || 0;
const {nickname, color, member} = getContactDetails(props.contacts[ship]);
const {nickname, color, member} = getContactDetails(props.contacts[ship]);
return (
<LinkItem
key={time}
title={title}
page={props.page}
linkIndex={linkIndex}
url={url}
timestamp={time}
seen={seen}
nickname={nickname}
ship={ship}
color={color}
member={member}
comments={commentCount}
resourcePath={props.resourcePath}
popout={props.popout}
/>
)
})
return (
<LinkItem
key={time}
title={title}
page={props.page}
linkIndex={linkIndex}
url={url}
timestamp={time}
seen={seen}
nickname={nickname}
ship={ship}
color={color}
member={member}
comments={commentCount}
resourcePath={props.resourcePath}
popout={props.popout}
/>
)
});
}
return (
<div

View File

@ -1,15 +1,8 @@
import React, { Component } from 'react';
import { MessageScreen } from '/components/lib/message-screen';
export class LoadingScreen extends Component {
render() {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Loading...
</p>
</div>
</div>
);
return (<MessageScreen text="Loading..."/>);
}
}

View File

@ -10,6 +10,7 @@ import { Skeleton } from '/components/skeleton';
import { NewScreen } from '/components/new';
import { MemberScreen } from '/components/member';
import { SettingsScreen } from '/components/settings';
import { MessageScreen } from '/components/lib/message-screen';
import { Links } from '/components/links-list';
import { LinkDetail } from '/components/link';
import { makeRoutePath, amOwnerOfGroup, base64urlDecode } from '../lib/util';
@ -63,13 +64,7 @@ export class Root extends Component {
selectedGroups={selectedGroups}
links={links}
listening={state.listening}>
<div className="h-100 w-100 overflow-x-hidden bg-white bg-gray0-d dn db-ns">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select or create a collection to begin.
</p>
</div>
</div>
<MessageScreen text="Select or create a collection to begin."/>
</Skeleton>
);
}} />

View File

@ -37,8 +37,10 @@ export class LinkUpdateReducer {
// since data contains an up-to-date full version of the page,
// we can safely overwrite the one in state.
state.links[path][page] = here.page;
state.links[path].local[page] = false;
if (typeof page === 'number' && here.page) {
state.links[path][page] = here.page;
state.links[path].local[page] = false;
}
state.links[path].totalPages = here.totalPages;
state.links[path].totalItems = here.totalItems;
state.links[path].unseenCount = here.unseenCount;
@ -48,7 +50,7 @@ export class LinkUpdateReducer {
if (!state.seen[path]) {
state.seen[path] = {};
}
here.page.map(submission => {
(here.page || []).map(submission => {
state.seen[path][submission.url] = submission.seen;
});
}

View File

@ -242,7 +242,7 @@ a {
display: none;
}
.md h1, .md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul, .md blockquote,.md code,.md pre {
.md h1, .md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul, .md ol, .md blockquote,.md code,.md pre {
font-size: 14px;
margin-bottom: 16px;
}
@ -265,7 +265,7 @@ a {
.md code, .md pre {
font-family: "Source Code Pro", mono;
}
.md ul>li {
.md ul>li, .md ol>li {
line-height: 1.5;
}
.md a {

View File

@ -0,0 +1,26 @@
import React from "react";
export const CommentInput = React.forwardRef((props, ref) => (
<textarea
{...props}
ref={ref}
style={{ resize: "vertical" }}
id="comment"
name="comment"
placeholder="Leave a comment here"
className={
"f9 db border-box w-100 ba b--gray3 pt3 ph3 br1 " +
"b--gray2-d mb2 focus-b--black focus-b--white-d white-d bg-gray0-d"
}
aria-describedby="comment-desc"
style={{ height: "4rem" }}
onKeyDown={e => {
if (
(e.getModifierState("Control") || event.metaKey) &&
e.key === "Enter"
) {
props.onSubmit();
}
}}
></textarea>
));

View File

@ -1,11 +1,20 @@
import React, { Component } from 'react';
import moment from 'moment';
import { Sigil } from './icons/sigil';
import { CommentInput } from './comment-input';
import { uxToHex, cite } from '../../lib/util';
import { Spinner } from './icons/icon-spinner';
export class CommentItem extends Component {
constructor(props){
super(props);
this.state = {
commentBody: ''
};
this.commentChange = this.commentChange.bind(this);
this.commentEdit = this.commentEdit.bind(this);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
@ -28,6 +37,29 @@ export class CommentItem extends Component {
}
});
}
commentEdit() {
let commentPath = Object.keys(this.props.comment)[0];
let commentBody = this.props.comment[commentPath].content;
this.setState({ commentBody });
this.props.onEdit();
}
focusTextArea(text) {
text && text.focus();
}
commentChange(e) {
this.setState({
commentBody: e.target.value
})
}
onUpdate() {
this.props.onUpdate(this.state.commentBody);
}
render() {
let pending = !!this.props.pending ? "o-60" : "";
let commentData = this.props.comment[Object.keys(this.props.comment)[0]];
@ -55,8 +87,13 @@ export class CommentItem extends Component {
name = cite(commentData.author);
}
const { editing } = this.props;
const disabled = this.props.pending
|| window.ship !== commentData.author.slice(1);
return (
<div className={pending}>
<div className={"mb8 " + pending}>
<div className="flex mv3 bg-white bg-gray0-d">
<Sigil
ship={commentData.author}
@ -70,10 +107,39 @@ export class CommentItem extends Component {
{name}
</div>
<div className="f9 gray3 pt1">{date}</div>
{ !editing && !disabled && (
<>
<div onClick={this.commentEdit.bind(this)} className="green2 pointer ml2 f9 pt1">
Edit
</div>
<div onClick={this.props.onDelete} className="red2 pointer ml2 f9 pt1">
Delete
</div>
</>
) }
</div>
<div className="f8 lh-solid mb8 mb2">
{content}
<div className="f8 lh-solid mb2">
{ !editing && content }
{ editing && (
<CommentInput style={{resize:'vertical'}}
ref={(el) => {this.focusTextArea(el)}}
onChange={this.commentChange}
value={this.state.commentBody}
onSubmit={this.onUpdate.bind(this)}>
</CommentInput>
)}
</div>
{ editing && (
<div className="flex">
<div onClick={this.onUpdate.bind(this)} className="br1 green2 pointer f9 pt1 b--green2 ba pa2 dib">
Submit
</div>
<div onClick={this.props.onEditCancel} className="br1 black white-d pointer f9 b--gray2 ba pa2 dib ml2">
Cancel
</div>
</div>
)}
</div>
)
}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import { CommentItem } from './comment-item';
import { CommentInput } from './comment-input';
import { dateToDa } from '/lib/util';
import { Spinner } from './icons/icon-spinner';
@ -8,11 +9,13 @@ export class Comments extends Component {
super(props);
this.state = {
commentBody: '',
disabled: false,
pending: new Set()
pending: new Set(),
awaiting: null,
editing: null,
}
this.commentSubmit = this.commentSubmit.bind(this);
this.commentChange = this.commentChange.bind(this);
}
componentDidUpdate(prevProps) {
@ -50,10 +53,10 @@ export class Comments extends Component {
this.setState({pending: pendingState});
this.textArea.value = '';
this.setState({commentBody: "", disabled: true});
this.setState({commentBody: "", awaiting: 'new'});
let submit = window.api.action("publish", "publish-action", comment);
submit.then(() => {
this.setState({ disabled: false });
this.setState({ awaiting: null });
})
}
@ -63,11 +66,60 @@ export class Comments extends Component {
})
}
commentEdit(idx) {
this.setState({ editing: idx });
}
commentEditCancel() {
this.setState({ editing: null });
}
commentUpdate(idx, body) {
let path = Object.keys(this.props.comments[idx])[0];
let comment = {
"edit-comment": {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note,
body: body,
comment: path
}
};
this.setState({ awaiting: 'edit' })
window.api
.action('publish', 'publish-action', comment)
.then(() => { this.setState({ awaiting: null, editing: null }) })
}
commentDelete(idx) {
let path = Object.keys(this.props.comments[idx])[0];
let comment = {
"del-comment": {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note,
comment: path
}
};
this.setState({ awaiting: { kind: 'del', what: idx }})
window.api
.action('publish', 'publish-action', comment)
.then(() => { this.setState({ awaiting: null }) })
}
render() {
if (!this.props.enabled) {
return null;
}
const { editing } = this.state;
let pendingArray = Array.from(this.state.pending).map((com, i) => {
let da = dateToDa(new Date);
let comment = {
@ -93,43 +145,46 @@ export class Comments extends Component {
comment={com}
key={i}
contacts={this.props.contacts}
onUpdate={u => this.commentUpdate(i, u)}
onDelete={() => this.commentDelete(i)}
onEdit={() => this.commentEdit(i)}
onEditCancel={this.commentEditCancel.bind(this)}
editing={i === editing}
disabled={!!this.state.awaiting || editing}
/>
);
})
let disableComment = ((this.state.commentBody === '') || (this.state.disabled === true));
let disableComment = ((this.state.commentBody === '') || (!!this.state.awaiting));
let commentClass = (disableComment)
? "bg-transparent f9 pa2 br1 ba b--gray2 gray2"
: "bg-transparent f9 pa2 br1 ba b--gray2 black white-d pointer";
let spinnerText =
this.state.awaiting === 'new'
? 'Posting commment...'
: this.state.awaiting === 'edit'
? 'Updating comment...'
: 'Deleting comment...';
return (
<div>
<div className="mv8 relative">
<div>
<textarea style={{resize:'vertical'}}
<CommentInput style={{resize:'vertical'}}
ref={(el) => {this.textArea = el}}
id="comment"
name="comment"
placeholder="Leave a comment here"
className={"f9 db border-box w-100 ba b--gray3 pt3 ph3 br1 " +
"b--gray2-d mb2 focus-b--black focus-b--white-d white-d bg-gray0-d"}
aria-describedby="comment-desc"
style={{height: "4rem"}}
onChange={this.commentChange}
onKeyDown={(e) => {
if ((e.getModifierState("Control") || event.metaKey)
&& e.key === "Enter") {
this.commentSubmit();
}
}}>
</textarea>
value={this.state.commentBody}
disabled={!!this.state.editing}
onSubmit={this.commentSubmit}>
</CommentInput>
</div>
<button disabled={disableComment}
onClick={this.commentSubmit}
className={commentClass}>
Add comment
</button>
<Spinner text="Posting comment..." awaiting={this.state.disabled} classes="absolute bottom-0 right-0 pb2"/>
<Spinner text={spinnerText} awaiting={this.state.awaiting} classes="absolute bottom-0 right-0 pb2"/>
</div>
{pendingArray}
{commentArray}

View File

@ -201,7 +201,7 @@ export class Note extends Component {
ref={el => {
this.scrollElement = el;
}}>
<div className="h-100 flex flex-column items-center mt4 ph4 pb4">
<div className="h-100 flex flex-column items-center pa4">
<div className="w-100 flex justify-center pb6">
<SidebarSwitcher
popout={props.popout}