Merge branch 'master' into lf/profile-overlay

This commit is contained in:
Liam Fitzgerald 2020-04-22 13:25:11 +10:00
commit 8e028ad255
25 changed files with 1229 additions and 246 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

@ -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

@ -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

@ -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

@ -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

@ -178,6 +178,14 @@ h2 {
}
.green3 {
color: #7ea899;
}
.unread-notice {
top: 48px;
}
/* responsive */
@media all and (max-width: 34.375em) {
@ -196,6 +204,9 @@ h2 {
.embed-container {
padding-bottom: 56.25%;
}
.unread-notice {
top: 96px;
}
}
@media all and (min-width: 34.375em) and (max-width: 46.875em) {
@ -231,6 +242,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) {
@ -295,4 +394,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,15 +1,18 @@
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) {
@ -37,24 +40,26 @@ export class ChatScreen extends Component {
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.scrollToBottom();
this.updateReadNumber();
this.askForMessages();
this.scrollToBottom();
}
componentWillUnmount() {
if (this.updateReadInterval) {
clearInterval(this.updateReadInterval);
this.updateReadInterval = null;
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
@ -69,17 +74,10 @@ export class ChatScreen extends Component {
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 &&
@ -113,13 +111,6 @@ export class ChatScreen extends Component {
}
}
updateReadNumber() {
const { props, state } = this;
if (props.read < props.length) {
props.api.chat.read(props.station);
}
}
askForMessages() {
const { props, state } = this;
@ -230,9 +221,23 @@ 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);
}
}
}
chatWindow() {
chatWindow(unread) {
// Replace with just the "not Firefox" implementation
// when Firefox #1042151 is patched.
@ -255,6 +260,7 @@ export class ChatScreen extends Component {
return (value.pending = true);
});
messages = pendingMessages.concat(messages);
let messageElements = messages.map((msg, i) => {
@ -268,7 +274,12 @@ export class ChatScreen extends Component {
_.get(messages[i - 1], aut) !==
_.get(msg, aut, msg.author);
return (
let when = ['when'];
let dayBreak =
moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !==
moment(_.get(messages[i], when)).format('YYYY.MM.DD');
const messageElem = (
<Message
key={msg.uid}
msg={msg}
@ -280,6 +291,39 @@ export class ChatScreen extends Component {
group={props.association}
/>
);
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")) {
@ -293,6 +337,10 @@ export class ChatScreen extends Component {
ref={el => {
this.scrollElement = el;
}}></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
@ -319,6 +367,10 @@ export class ChatScreen extends Component {
ref={el => {
this.scrollElement = el;
}}></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
@ -358,10 +410,14 @@ export class ChatScreen extends Component {
: 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" }}>
@ -393,7 +449,14 @@ export class ChatScreen extends Component {
api={props.api}
/>
</div>
{this.chatWindow()}
{ !!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

@ -2,12 +2,37 @@ 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 { ShipSearch } from '/components/lib/ship-search';
import { uuid, uxToHex, hexToRgba } from '/lib/util';
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",
}
}
// line height
const INPUT_LINE_HEIGHT = 28;
@ -36,8 +61,7 @@ export class ChatInput extends Component {
this.state = {
message: '',
textareaHeight: INPUT_LINE_HEIGHT + INPUT_TOP_PADDING + 1,
patpSearch: ''
patpSearch: null
};
this.textareaRef = React.createRef();
@ -45,13 +69,15 @@ export class ChatInput extends Component {
this.messageSubmit = this.messageSubmit.bind(this);
this.messageChange = this.messageChange.bind(this);
this.patpAutocomplete = this.patpAutocomplete.bind(this);
this.completePatp = this.completePatp.bind(this);
this.clearSearch = this.clearSearch.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 = () => {
@ -95,8 +121,18 @@ export class ChatInput extends Component {
});
}
componentDidMount() {
this.bindShortcuts();
nextAutocompleteSuggestion(backward = false) {
const { patpSuggestions } = this.state;
let idx = patpSuggestions.findIndex(s => s === this.state.selectedSuggestion);
idx = backward ? idx - 1 : idx + 1;
idx = idx % patpSuggestions.length;
if(idx < 0) {
idx = patpSuggestions.length - 1;
}
this.setState({ selectedSuggestion: patpSuggestions[idx] });
}
@ -104,103 +140,48 @@ export class ChatInput extends Component {
const match = /~([a-zA-Z\-]*)$/.exec(message);
if (!match ) {
this.bindShortcuts();
this.setState({ patpSearch: '' })
this.setState({ patpSearch: null })
return;
}
this.unbindShortcuts();
this.setState({ patpSearch: match[1].toLowerCase() });
}
clearSearch() {
this.setState({
patpSearch: ''
patpSearch: null
})
}
completePatp(suggestion) {
this.bindShortcuts();
this.setState({
message: this.state.message.replace(
/[a-zA-Z\-]*$/,
suggestion
),
patpSearch: ''
});
}
bindShortcuts() {
if(!this.mousetrap) {
this.mousetrap = new Mousetrap(this.textareaRef.current);
}
this.mousetrap.bind('enter', e => {
e.preventDefault();
if(this.state.patpSearch.length === 0) {
this.messageSubmit();
}
});
this.mousetrap.bind('tab', e => {
e.preventDefault();
e.stopPropagation();
if(this.state.patpSearch.length === 0) {
this.patpAutocomplete(this.state.message, true);
}
});
}
unbindShortcuts() {
if(!this.mousetrap) {
if(!this.editor) {
return;
}
this.mousetrap.unbind('enter')
this.mousetrap.unbind('tab')
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({
patpSearch: null
});
}
messageChange(event) {
const message = event.target.value;
this.setState({
message
});
messageChange(editor, data, value) {
const { patpSearch } = this.state;
if(patpSearch.length !== 0) {
this.patpAutocomplete(message, false);
if(patpSearch !== null) {
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') {
@ -233,14 +214,28 @@ 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(" ");
@ -282,10 +277,28 @@ export class ChatInput extends Component {
// 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() {
@ -304,22 +317,45 @@ export class ChatInput extends Component {
.reverse()
.value();
const codeTheme = state.code ? ' code' : '';
const completeActive = state.patpSearch !== null;
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) =>
this.patpAutocomplete(cm.getValue(), true),
'Enter': (cm) =>
this.messageSubmit(),
'Shift-3': (cm) =>
this.toggleCode()
}
};
return (
<div className="pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white bg-gray0-d relative"
style={{ flexGrow: 1 }}>
<ShipSearch
popover
onSelect={this.completePatp}
onClear={this.clearSearch}
contacts={props.contacts}
candidates={candidates}
searchTerm={this.state.patpSearch}
inputRef={this.textareaRef.current}
cm={this.editor}
/>
<div
className="fl"
style={{
marginTop: 4,
marginTop: 6,
flexBasis: 24,
height: 24
}}>
@ -330,22 +366,25 @@ export class ChatInput extends Component {
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
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

@ -8,7 +8,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() {
@ -128,10 +155,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>
);
}
@ -167,7 +195,7 @@ export class Message extends Component {
<div
ref={this.containerRef}
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"
@ -214,7 +242,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

@ -56,18 +56,52 @@ export class ShipSearch extends Component {
suggestions: [],
bound: false
};
}
componentDidMount() {
this.bindShortcuts();
if (this.props.suggestEmpty) {
this.updateSuggestions();
this.keymap = {
Tab: (cm) =>
this.nextAutocompleteSuggestion(),
'Shift-Tab': (cm) =>
this.nextAutocompleteSuggestion(true),
'Up': (cm) =>
this.nextAutocompleteSuggestion(true),
'Escape': (cm) =>
this.props.onClear(),
'Down': (cm) =>
this.nextAutocompleteSuggestion(),
'Enter': (cm) => {
if(this.props.searchTerm !== null) {
this.props.onSelect(this.state.selected);
}
},
'Shift-3': (cm) =>
this.toggleCode()
}
}
componentDidMount() {
if(this.props.searchTerm !== null) {
this.updateSuggestions(true);
}
}
componentDidUpdate(prevProps) {
const { props } = this;
const { props, state } = this;
if(!state.bound && props.inputRef) {
this.bindShortcuts();
}
if(props.searchTerm === null) {
if(state.suggestions.length > 0) {
this.setState({ suggestions: [] });
}
this.unbindShortcuts();
return;
}
if (
props.searchTerm === null &&
props.searchTerm !== prevProps.searchTerm &&
props.searchTerm.startsWith(prevProps.searchTerm)
) {
@ -76,18 +110,10 @@ export class ShipSearch extends Component {
this.updateSuggestions(true);
}
if (prevProps.inputRef !== props.inputRef) {
this.bindShortcuts();
}
}
updateSuggestions(isStale = false) {
const needle = this.props.searchTerm;
if (needle.length === 0 && !this.props.suggestEmpty) {
this.unbindShortcuts();
this.setState({ suggestions: [] });
return;
}
const matchString = hay => {
hay = hay.toLowerCase();
@ -127,10 +153,27 @@ export class ShipSearch extends Component {
this.setState({ suggestions, selected: suggestions[0] });
}
bindShortcuts() {
if (!this.props.inputRef || this.state.bound) {
bindCmShortcuts() {
if(!this.props.cm) {
return;
}
this.props.cm.addKeyMap(this.keymap);
}
unbindCmShortcuts() {
if(!this.props.cm) {
return;
}
this.props.cm.removeKeyMap(this.keymap);
}
bindShortcuts() {
if (this.state.bound) {
return;
}
if (!this.props.inputRef) {
return this.bindCmShortcuts();
}
this.setState({ bound: true });
if (!this.mousetrap) {
this.mousetrap = new Mousetrap(this.props.inputRef);
@ -164,14 +207,19 @@ export class ShipSearch extends Component {
this.mousetrap.bind("esc", e => {
e.preventDefault();
e.stopPropagation();
this.props.onDismiss();
this.props.onClear();
});
}
unbindShortcuts() {
if(!this.props.inputRef) {
this.unbindCmShortcuts()
}
if (!this.state.bound) {
return;
}
this.setState({ bound: false });
this.mousetrap.unbind("enter");
this.mousetrap.unbind("tab");
@ -240,28 +288,28 @@ export class ShipSearchInput extends Component {
searchTerm: ""
};
this.inputRef = React.createRef();
this.popoverRef = React.createRef();
this.inputRef = null;
this.popoverRef = null;
this.search = this.search.bind(this);
this.onClick = this.onClick.bind(this);
this.setInputRef = this.setInputRef.bind(this);
}
onClick(event) {
const { popoverRef } = this;
// Do nothing if clicking ref's element or descendent elements
if (!popoverRef.current || popoverRef.current.contains(event.target)) {
if (!popoverRef || popoverRef.contains(event.target)) {
return;
}
this.props.onDismiss();
this.props.onClear();
}
componentDidMount() {
document.addEventListener("mousedown", this.onClick);
document.addEventListener("touchstart", this.onClick);
this.inputRef.current.focus();
}
componentWillUnmount() {
@ -269,6 +317,15 @@ export class ShipSearchInput extends Component {
document.removeEventListener("touchstart", this.onClick);
}
setInputRef(ref) {
this.inputRef = ref;
if(ref) {
ref.focus();
}
// update this.inputRef prop
this.forceUpdate();
}
search(e) {
const searchTerm = e.target.value;
this.setState({ searchTerm });
@ -279,7 +336,7 @@ export class ShipSearchInput extends Component {
return (
<div
ref={this.popoverRef}
ref={ref => (this.popoverRef = ref)}
style={{ top: "150%", left: "-80px" }}
className="b--gray2 b--solid ba absolute bg-white bg-gray0-d shadow-5"
>
@ -298,16 +355,15 @@ export class ShipSearchInput extends Component {
placeholder="Search for a ship"
value={state.searchTerm}
onChange={this.search}
ref={this.inputRef}
ref={this.setInputRef}
/>
<ShipSearch
contacts={props.contacts}
candidates={props.candidates}
searchTerm={deSig(state.searchTerm)}
inputRef={this.inputRef.current}
inputRef={this.inputRef}
onSelect={props.onSelect}
onDismiss={props.onDismiss}
suggestEmpty
onClear={props.onClear}
/>
</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

@ -142,7 +142,7 @@ export class Sidebar extends Component {
contacts={{}}
candidates={candidates}
onSelect={this.goDm.bind(this)}
onDismiss={this.onClickDm.bind(this)}
onClear={this.onClickDm.bind(this)}
/>
)}

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

@ -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}