mirror of
https://github.com/urbit/shrub.git
synced 2024-11-23 20:26:54 +03:00
Merge remote-tracking branch 'origin/release/next-vere' into scot-jets
This commit is contained in:
commit
19bb618d8c
113
MAINTAINERS.md
113
MAINTAINERS.md
@ -1,5 +1,117 @@
|
||||
# Maintainers' Guide
|
||||
|
||||
## Branch organization
|
||||
|
||||
The essence of this branching scheme is that you create "release branches" of
|
||||
independently releasable units of work. These can then be released by their
|
||||
maintainers when ready.
|
||||
|
||||
### Master branch
|
||||
|
||||
Master is what's released on the network. Deployment instructions are in the
|
||||
next section, but tagged releases should always come from this branch.
|
||||
|
||||
### Feature branches
|
||||
|
||||
Anyone can create feature branches. For those with commit access to
|
||||
urbit/urbit, you're welcome to create them in this repo; otherwise, fork the
|
||||
repo and create them there.
|
||||
|
||||
Usually, new development should start from master, but if your work depends on
|
||||
work in another feature branch or release branch, start from there.
|
||||
|
||||
If, after starting your work, you need changes that are in master, merge it into
|
||||
your branch. If you need changes that are in a release branch or feature
|
||||
branch, merge it into your branch, but understand that your work now depends on
|
||||
that release branch, which means it won't be released until that one is
|
||||
released.
|
||||
|
||||
### Release branches
|
||||
|
||||
Release branches are code that is ready to release. All release branch names
|
||||
should start with `release/`.
|
||||
|
||||
All code must be reviewed before being pushed to a release branch. Thus,
|
||||
feature branches should be PR'd against a release branch, not master.
|
||||
|
||||
Create new release branches as needed. You don't need a new one for every PR,
|
||||
since many changes are relatively small and can be merged together with little
|
||||
risk. However, once you merge two branches, they're now coupled and will only
|
||||
be released together -- unless one of the underlying commits is separately put
|
||||
on a release branch.
|
||||
|
||||
Here's a worked example. The rule is to make however many branches are useful,
|
||||
and no more. This example is not prescriptive, the developers making the
|
||||
changes may add, remove, or rename branches in this flow at will.
|
||||
|
||||
Suppose you (plural, the dev community at large) complete some work in a
|
||||
userspace app, and you put it in `release/next-userspace`. Separately, you make
|
||||
a small JS change. If you PR it to `release/next-userspace`, then it will only
|
||||
be released at the same time as the app changes. Maybe this is fine, or maybe
|
||||
you want this change to go out quickly, and the change in
|
||||
`release/next-userspace` is relatively risky, so you don't want to push it out
|
||||
on Friday afternoon. In this case, put the change in another release branch,
|
||||
say `release/next-js`. Now either can be released independently.
|
||||
|
||||
Suppose you do further work that you want to PR to `release/next-userspace`, but
|
||||
it depends on your fixes in `release/next-js`. Simply merge `release/next-js`
|
||||
into either your feature branch or `release/next-userspace` and PR your finished
|
||||
work to `release/next-userspace`. Now there is a one-way coupling:
|
||||
`release/next-userspace` contains `release/next-js`, so releasing it will
|
||||
implicitly release `release/next-js`. However, you can still release
|
||||
`release/next-js` independently.
|
||||
|
||||
This scheme extends to other branches, like `release/next-kernel` or
|
||||
`release/os1.1` or `release/ford-fusion`. Some branches may be long-lived and
|
||||
represent simply the "next" release of something, while others will have a
|
||||
definite lifetime that corresponds to development of a particular feature or
|
||||
numbered release.
|
||||
|
||||
Since they are "done", release branches should be considered "public", in the
|
||||
sense that others may depend on them at will. Thus, never rebase a release
|
||||
branch.
|
||||
|
||||
When cutting a new release, you can filter branches with `git branch --list
|
||||
'release/*'` or by typing "release/" in the branch filter on Github. This will
|
||||
give you the list of branches which have passed review and may be merged to
|
||||
master and released. When choosing which branches to release, make sure you
|
||||
understand the risks of releasing them immediately. If merging these produces
|
||||
nontrivial conflicts, consider asking the developers on those branches to merge
|
||||
between themselves. In many cases a developer can do this directly, but if it's
|
||||
sufficiently nontrivial, this may be a reviewed PR of one release branch into
|
||||
another.
|
||||
|
||||
### Non-OTAable release branches
|
||||
|
||||
In some cases, work is completed which cannot be OTA'd as written. For example,
|
||||
the code may lack state adapters, or it may not properly handle outstanding
|
||||
subscriptions. It could also be code which is planned to be released only upon
|
||||
a breach (network-wide or rolling).
|
||||
|
||||
In this case, the code may be PR'd to a `na-release/` branch. All rules are the
|
||||
same as for release branches, except that the code does not need to apply
|
||||
cleanly to an existing ship. If you later write state adapter or otherwise make
|
||||
it OTAable, then you may PR it to a release branch.
|
||||
|
||||
### Other cases
|
||||
|
||||
Outside contributors can generally target their PRs against master unless
|
||||
specifically instructed. Maintainers should retarget those branches as
|
||||
appropriate.
|
||||
|
||||
If a commit is not something that goes into a release (eg changes to README or
|
||||
CI), it may be committed straight to master.
|
||||
|
||||
If a hotfix is urgent, it may be PR'd straight to master. This should only be
|
||||
done if you reasonably expect that it will be released soon and before anything
|
||||
else is released.
|
||||
|
||||
If a series of commits that you want to release is on a release branch, but you
|
||||
really don't want to release the whole branch, you must cherry-pick them onto
|
||||
another release branch. Cherry-picking isn't ideal because those commits will
|
||||
be duplicated in the history, but it won't have any serious side effects.
|
||||
|
||||
|
||||
## Hotfixes
|
||||
|
||||
Here lies an informal guide for making hotfix releases and deploying them to
|
||||
@ -175,4 +287,3 @@ Post an announcement to urbit-dev. The tag annotation, basically, is fine here
|
||||
-- I usually add the %base hash (for Urbit OS releases) and the release binary
|
||||
URLs (for Vere releases). Check the urbit-dev archives for examples of these
|
||||
announcements.
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cdc31bb717626f95d7455349dac4f3171205667c4d08ed9fad071bd13266bab6
|
||||
size 10013218
|
||||
oid sha256:801eb8574daff9f0ac88e2e40dab09d95bd8d667df953e971501a1f8db4fd039
|
||||
size 10394205
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0a637a7a8e2061caa09ea1cf62c2295a0b14920588d01338e9bd2f06eecf1c1f
|
||||
size 1234571
|
||||
oid sha256:df9ab46632f1a6727837eb03ddf5d37c8f415d89b3205fbc2005891fd3e8921e
|
||||
size 1234585
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d8eeac47513dba778cd83269996eb04f1519280ec5a36bf0395517451cd087d5
|
||||
size 12473677
|
||||
oid sha256:50a06217c5354abe42baec10072ddba8e0129c20806fc8173529989724397836
|
||||
size 12874302
|
||||
|
@ -226,8 +226,6 @@
|
||||
::
|
||||
++ catch-up
|
||||
^- (quip card _state)
|
||||
?. .^(? %gu /(scot %p our.bowl)/chat-store/(scot %da now.bowl))
|
||||
[~ state]
|
||||
=/ =inbox
|
||||
(scry-for inbox %chat-store /all)
|
||||
|- ^- (quip card _state)
|
||||
|
@ -602,7 +602,8 @@
|
||||
::
|
||||
[%backlog @ @ @ *]
|
||||
=/ chat=path (oust [(dec (lent t.wir)) 1] `(list @ta)`t.wir)
|
||||
%. (poke-chat-hook-action %remove chat)
|
||||
:_ state
|
||||
%. ~[(chat-view-poke %delete chat)]
|
||||
%- slog
|
||||
:* leaf+"chat-hook failed subscribe on {(spud chat)}"
|
||||
leaf+"stack trace:"
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 611 B |
BIN
pkg/arvo/app/chat/img/ImageUpload.png
Normal file
BIN
pkg/arvo/app/chat/img/ImageUpload.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 865 B |
@ -26,5 +26,6 @@
|
||||
<script src="/~channel/channel.js"></script>
|
||||
<script src="/~modulo/session.js"></script>
|
||||
<script src="/~chat/js/index.js"></script>
|
||||
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
File diff suppressed because one or more lines are too long
@ -417,14 +417,17 @@
|
||||
:* to
|
||||
(mul windup-years yer:yo)
|
||||
stars
|
||||
(div (mul unlock-years yer:yo) stars)
|
||||
1
|
||||
(div (mul unlock-years yer:yo) stars)
|
||||
==
|
||||
::
|
||||
++ register-conditional
|
||||
|= [to=address [b1=@ud b2=@ud b3=@ud] unlock-years-per-batch=@ud]
|
||||
%- register-conditional:dat
|
||||
=- [`address`to b1 b2 b3 `@ud`- 1]
|
||||
(div (mul unlock-years-per-batch yer:yo) :(add b1 b2 b3))
|
||||
:* to
|
||||
b1 b2 b3
|
||||
1
|
||||
(div (mul unlock-years-per-batch yer:yo) :(add b1 b2 b3))
|
||||
==
|
||||
::
|
||||
--
|
||||
--
|
||||
|
@ -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])]
|
||||
|
@ -5,18 +5,33 @@
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-zero
|
||||
state-one
|
||||
==
|
||||
::
|
||||
+$ rolodex-0 (map path contacts-0)
|
||||
+$ contacts-0 (map ship contact-0)
|
||||
+$ avatar-0 [content-type=@t octs=[p=@ud q=@t]]
|
||||
+$ contact-0
|
||||
$: nickname=@t
|
||||
email=@t
|
||||
phone=@t
|
||||
website=@t
|
||||
notes=@t
|
||||
color=@ux
|
||||
avatar=(unit avatar-0)
|
||||
==
|
||||
::
|
||||
+$ state-zero
|
||||
$: %0
|
||||
=rolodex
|
||||
rolodex=rolodex-0
|
||||
==
|
||||
+$ diff
|
||||
$% [%contact-update contact-update]
|
||||
+$ state-one
|
||||
$: %1
|
||||
=rolodex
|
||||
==
|
||||
--
|
||||
::
|
||||
=| state-zero
|
||||
=| state-one
|
||||
=* state -
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
@ -30,8 +45,26 @@
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
`this(state !<(state-zero old))
|
||||
|= old-vase=vase
|
||||
=/ old !<(versioned-state old-vase)
|
||||
?: ?=(%1 -.old)
|
||||
[~ this(state old)]
|
||||
=/ new-rolodex=^rolodex
|
||||
%- ~(run by rolodex.old)
|
||||
|= cons=contacts-0
|
||||
^- contacts
|
||||
%- ~(run by cons)
|
||||
|= con=contact-0
|
||||
^- contact
|
||||
:* nickname.con
|
||||
email.con
|
||||
phone.con
|
||||
website.con
|
||||
notes.con
|
||||
color.con
|
||||
~
|
||||
==
|
||||
[~ this(state [%1 new-rolodex])]
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
@ -142,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))
|
||||
|
@ -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)
|
||||
::
|
||||
@ -181,21 +181,19 @@
|
||||
::
|
||||
:: avatar images
|
||||
::
|
||||
:: [%'~groups' %avatar @ *]
|
||||
:: =/ pax=path `path`t.t.site.url
|
||||
:: ?~ pax not-found:gen
|
||||
:: =/ pas `path`(flop pax)
|
||||
:: ?~ pas not-found:gen
|
||||
:: =/ pav `path`(flop t.pas)
|
||||
:: ~& pav+pav
|
||||
:: ~& name+name
|
||||
:: =/ contact (contact-scry `path`(weld pav [name]~))
|
||||
:: ?~ contact not-found:gen
|
||||
:: ?~ avatar.u.contact not-found:gen
|
||||
:: =* avatar u.avatar.u.contact
|
||||
:: =/ decoded (de:base64 q.octs.avatar)
|
||||
:: ?~ decoded not-found:gen
|
||||
:: [[200 ['content-type' content-type.avatar]~] `u.decoded]
|
||||
[%'~groups' %avatar @ *]
|
||||
=/ =path (flop t.t.site.url)
|
||||
?~ path not-found:gen
|
||||
=/ contact (contact-scry `^path`(snoc (flop t.path) name))
|
||||
?~ contact not-found:gen
|
||||
?~ avatar.u.contact not-found:gen
|
||||
?- -.u.avatar.u.contact
|
||||
%url [[307 ['location' url.u.avatar.u.contact]~] ~]
|
||||
%octt
|
||||
=/ max-3-days ['cache-control' 'max-age=259200']
|
||||
=/ content-type ['content-type' content-type.u.avatar.u.contact]
|
||||
[[200 [content-type max-3-days ~]] `octs.u.avatar.u.contact]
|
||||
==
|
||||
::
|
||||
[%'~groups' *] (html-response:gen index)
|
||||
==
|
||||
|
BIN
pkg/arvo/app/contacts/img/ImageUpload.png
Normal file
BIN
pkg/arvo/app/contacts/img/ImageUpload.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 865 B |
@ -13,5 +13,6 @@
|
||||
<script src="/~channel/channel.js"></script>
|
||||
<script src="/~modulo/session.js"></script>
|
||||
<script src="/~groups/js/index.js"></script>
|
||||
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
File diff suppressed because one or more lines are too long
@ -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,26 @@
|
||||
[(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
|
||||
[(group-poke pax.diff diff)]~
|
||||
=/ 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 +257,4 @@
|
||||
?: =(u.shp our.bol)
|
||||
[%pass wir %agent [our.bol %group-store] %leave ~]~
|
||||
[%pass wir %agent [u.shp %group-hook] %leave ~]~
|
||||
::
|
||||
--
|
||||
|
@ -43,9 +43,9 @@
|
||||
!:
|
||||
=> |% ::
|
||||
++ hood-old :: unified old-state
|
||||
{?($1 $2 $3) lac/(map @tas hood-part-old)} ::
|
||||
{?($1 $2 $3 $4) lac/(map @tas hood-part-old)} ::
|
||||
++ hood-1 :: unified state
|
||||
{$3 lac/(map @tas hood-part)} ::
|
||||
{$4 lac/(map @tas hood-part)} ::
|
||||
++ hood-good :: extract specific
|
||||
=+ hed=$:hood-head
|
||||
|@ ++ $
|
||||
@ -140,7 +140,7 @@
|
||||
`..on-init
|
||||
::
|
||||
++ on-save
|
||||
!>([%3 lac])
|
||||
!>([%4 lac])
|
||||
::
|
||||
++ on-load
|
||||
|= =old-state=vase
|
||||
@ -150,7 +150,8 @@
|
||||
?- -.old-state
|
||||
%1 ((wrap on-load):from-drum:(help hid) %1)
|
||||
%2 ((wrap on-load):from-drum:(help hid) %2)
|
||||
%3 `lac
|
||||
%3 ((wrap on-load):from-drum:(help hid) %3)
|
||||
%4 `lac
|
||||
==
|
||||
[cards ..on-init]
|
||||
::
|
||||
|
@ -178,8 +178,11 @@ class Channel {
|
||||
this.lastEventId = e.lastEventId;
|
||||
|
||||
let obj = JSON.parse(e.data);
|
||||
if (obj.response == "poke") {
|
||||
let funcs = this.outstandingPokes.get(obj.id);
|
||||
let pokeFuncs = this.outstandingPokes.get(obj.id);
|
||||
let subFuncs = this.outstandingSubscriptions.get(obj.id);
|
||||
|
||||
if (obj.response == "poke" && !!pokeFuncs) {
|
||||
let funcs = pokeFuncs;
|
||||
if (obj.hasOwnProperty("ok")) {
|
||||
funcs["success"]();
|
||||
} else if (obj.hasOwnProperty("err")) {
|
||||
@ -189,19 +192,20 @@ class Channel {
|
||||
}
|
||||
this.outstandingPokes.delete(obj.id);
|
||||
|
||||
} else if (obj.response == "subscribe") {
|
||||
} else if (obj.response == "subscribe" ||
|
||||
(obj.response == "poke" && !!subFuncs)) {
|
||||
let funcs = subFuncs;
|
||||
// on a response to a subscribe, we only notify the caller on err
|
||||
//
|
||||
let funcs = this.outstandingSubscriptions.get(obj.id);
|
||||
if (obj.hasOwnProperty("err")) {
|
||||
funcs["err"](obj.err);
|
||||
this.outstandingSubscriptions.delete(obj.id);
|
||||
}
|
||||
} else if (obj.response == "diff") {
|
||||
let funcs = this.outstandingSubscriptions.get(obj.id);
|
||||
let funcs = subFuncs;
|
||||
funcs["event"](obj.json);
|
||||
} else if (obj.response == "quit") {
|
||||
let funcs = this.outstandingSubscriptions.get(obj.id);
|
||||
let funcs = subFuncs;
|
||||
funcs["quit"](obj);
|
||||
this.outstandingSubscriptions.delete(obj.id);
|
||||
} else {
|
||||
|
File diff suppressed because one or more lines are too long
@ -114,8 +114,6 @@
|
||||
`t.t.path
|
||||
~
|
||||
?~ target |
|
||||
~? !.^(? %gu (scot %p our.bowl) %metadata-store (scot %da now.bowl) ~)
|
||||
%woah-md-s-not-booted ::TODO fallback if needed
|
||||
%+ lien (groups-from-resource:md %link u.target)
|
||||
|= =group-path
|
||||
^- ?
|
||||
|
File diff suppressed because one or more lines are too long
@ -331,9 +331,16 @@
|
||||
?+ mar (on-poke:def mar vas)
|
||||
::
|
||||
%noun
|
||||
?: =(q.vas %flush-limbo)
|
||||
[~ this(limbo [~ ~])]
|
||||
[~ this]
|
||||
?+ q.vas
|
||||
[~ this]
|
||||
::
|
||||
%flush-limbo [~ this(limbo [~ ~])]
|
||||
::
|
||||
%reset-warp
|
||||
=/ rav [%sing %t [%da now.bol] /app/publish/notebooks]
|
||||
:_ this
|
||||
[%pass /read/paths %arvo %c %warp our.bol q.byk.bol `rav]~
|
||||
==
|
||||
::
|
||||
%handle-http-request
|
||||
=+ !<([id=@ta req=inbound-request:eyre] vas)
|
||||
@ -1702,8 +1709,107 @@
|
||||
:~ [%give %fact [/primary]~ %publish-primary-delta !>(act)]
|
||||
[%give %fact [/publishtile]~ %json !>(jon)]
|
||||
==
|
||||
:: %groupify
|
||||
::
|
||||
%groupify
|
||||
?. (team:title our.bol src.bol)
|
||||
~|("action not permitted" !!)
|
||||
=/ book (~(get by books) our.bol book.act)
|
||||
?~ book
|
||||
~|("nonexistent notebook: {<book.act>}" !!)
|
||||
::
|
||||
=/ old-write writers.u.book
|
||||
=/ old-read subscribers.u.book
|
||||
?> ?=([%'~' ^] old-write)
|
||||
=/ destroy-old-groups=(list card)
|
||||
:~ (group-poke [%unbundle old-write])
|
||||
(group-poke [%unbundle old-read])
|
||||
(group-hook-poke [%remove old-write])
|
||||
(group-hook-poke [%remove old-read])
|
||||
(perm-hook-poke [%remove old-write])
|
||||
(perm-hook-poke [%remove old-read])
|
||||
==
|
||||
::
|
||||
?~ target.act
|
||||
:: create new group from subscribers
|
||||
::
|
||||
=. writers.u.book (slag 1 writers.u.book)
|
||||
=. subscribers.u.book writers.u.book
|
||||
=/ del=notebook-delta [%edit-book our.bol book.act u.book]
|
||||
:_ state(books (~(put by books) [our.bol book.act] u.book))
|
||||
%+ weld destroy-old-groups
|
||||
^- (list card)
|
||||
:~ [%give %fact [/notebook/[book.act]]~ %publish-notebook-delta !>(del)]
|
||||
[%give %fact [/primary]~ %publish-primary-delta !>(del)]
|
||||
%- contact-view-create
|
||||
:* writers.u.book
|
||||
(get-subscribers book.act)
|
||||
title.u.book
|
||||
description.u.book
|
||||
==
|
||||
%- metadata-poke
|
||||
:* %add
|
||||
writers.u.book
|
||||
[%publish /(scot %p our.bol)/[book.act]]
|
||||
title.u.book
|
||||
description.u.book
|
||||
0x0
|
||||
date-created.u.book
|
||||
our.bol
|
||||
==
|
||||
==
|
||||
::
|
||||
?> ?=(^ u.target.act)
|
||||
=. writers.u.book u.target.act
|
||||
=. subscribers.u.book u.target.act
|
||||
=/ group-host=@p (slav %p i.u.target.act)
|
||||
::
|
||||
=/ scry-pax :(weld /=group-store/(scot %da now.bol) u.target.act /noun)
|
||||
=/ old-group=(set @p) (need .^((unit (set @p)) %gx scry-pax))
|
||||
=/ dif-peeps=(set @p) (~(dif in (get-subscribers book.act)) old-group)
|
||||
::
|
||||
=/ del=notebook-delta [%edit-book our.bol book.act u.book]
|
||||
:_ state(books (~(put by books) [our.bol book.act] u.book))
|
||||
%+ weld
|
||||
%+ weld destroy-old-groups
|
||||
^- (list card)
|
||||
:~ [%give %fact [/notebook/[book.act]]~ %publish-notebook-delta !>(del)]
|
||||
[%give %fact [/primary]~ %publish-primary-delta !>(del)]
|
||||
%- metadata-poke
|
||||
:* %add
|
||||
writers.u.book
|
||||
[%publish /(scot %p our.bol)/[book.act]]
|
||||
title.u.book
|
||||
description.u.book
|
||||
0x0
|
||||
date-created.u.book
|
||||
our.bol
|
||||
==
|
||||
==
|
||||
?: ?& inclusive.act
|
||||
=(group-host our.bol)
|
||||
==
|
||||
:: add all subscribers to group
|
||||
::
|
||||
[(group-poke [%add dif-peeps u.target.act])]~
|
||||
:: kick subscribers who are not already in group
|
||||
::
|
||||
%+ turn ~(tap in dif-peeps)
|
||||
|= who=@p
|
||||
^- card
|
||||
[%give %kick [/notebook/[book.act]]~ `who]
|
||||
==
|
||||
::
|
||||
++ get-subscribers
|
||||
|= book=@tas
|
||||
^- (set @p)
|
||||
%+ roll ~(val by sup.bol)
|
||||
|= [[who=@p pax=path] out=(set @p)]
|
||||
^- (set @p)
|
||||
?. ?=([%notebook @ ~] pax) out
|
||||
?. =(book i.t.pax) out
|
||||
(~(put in out) who)
|
||||
::
|
||||
++ get-notebook
|
||||
|= [host=@p book-name=@tas sty=_state]
|
||||
^- (unit notebook)
|
||||
@ -1732,6 +1838,11 @@
|
||||
[%give %fact [/publishtile]~ %json !>(jon)]
|
||||
==
|
||||
::
|
||||
++ metadata-poke
|
||||
|= act=metadata-action
|
||||
^- card
|
||||
[%pass / %agent [our.bol %metadata-hook] %poke %metadata-action !>(act)]
|
||||
::
|
||||
++ emit-metadata
|
||||
|= del=metadata-delta
|
||||
^- (list card)
|
||||
@ -1753,8 +1864,9 @@
|
||||
::
|
||||
%remove
|
||||
=/ app-path [(scot %p author.del) /[book.del]]
|
||||
=/ group-path (group-from-book app-path)
|
||||
[(metadata-poke [%remove group-path [%publish app-path]])]~
|
||||
=/ group-path=(unit path) (group-from-book app-path)
|
||||
?~ group-path ~
|
||||
[(metadata-poke [%remove u.group-path [%publish app-path]])]~
|
||||
==
|
||||
::
|
||||
++ add
|
||||
@ -1762,11 +1874,6 @@
|
||||
^- (list card)
|
||||
[(metadata-poke [%add group-path [%publish app-path] metadata])]~
|
||||
::
|
||||
++ metadata-poke
|
||||
|= act=metadata-action
|
||||
^- card
|
||||
[%pass / %agent [our.bol %metadata-hook] %poke %metadata-action !>(act)]
|
||||
::
|
||||
++ metadata-scry
|
||||
|= [group-path=path app-path=path]
|
||||
^- (unit metadata)
|
||||
@ -1785,13 +1892,12 @@
|
||||
::
|
||||
++ group-from-book
|
||||
|= app-path=path
|
||||
^- path
|
||||
^- (unit path)
|
||||
?. .^(? %gu (scot %p our.bol) %metadata-store (scot %da now.bol) ~)
|
||||
?: ?=([@ ^] app-path)
|
||||
~& [%assuming-ported-legacy-publish app-path]
|
||||
[%'~' app-path]
|
||||
~| [%weird-publish app-path]
|
||||
!!
|
||||
`[%'~' app-path]
|
||||
~&([%weird-publish app-path] ~)
|
||||
=/ resource-indices
|
||||
.^ (jug resource group-path)
|
||||
%gy
|
||||
@ -1800,8 +1906,12 @@
|
||||
(scot %da now.bol)
|
||||
/resource-indices
|
||||
==
|
||||
=/ groups=(set path) (~(got by resource-indices) [%publish app-path])
|
||||
(snag 0 ~(tap in groups))
|
||||
=/ groups=(unit (set path))
|
||||
(~(get by resource-indices) [%publish app-path])
|
||||
?~ groups ~
|
||||
=/ group-paths ~(tap in u.groups)
|
||||
?~ group-paths ~
|
||||
`i.group-paths
|
||||
--
|
||||
::
|
||||
++ metadata-hook-poke
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
96
pkg/arvo/app/s3-store.hoon
Normal file
96
pkg/arvo/app/s3-store.hoon
Normal file
@ -0,0 +1,96 @@
|
||||
/- *s3
|
||||
/+ s3-json, default-agent, verb, dbug
|
||||
~% %s3-top ..is ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-zero
|
||||
==
|
||||
::
|
||||
+$ state-zero [%0 =credentials =configuration]
|
||||
--
|
||||
::
|
||||
=| state-zero
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
~% %s3-agent-core ..card ~
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old-vase=vase
|
||||
[~ this(state !<(state-zero old-vase))]
|
||||
::
|
||||
++ on-poke
|
||||
~/ %s3-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
|^
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%s3-action (poke-action !<(action vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ poke-action
|
||||
|= act=action
|
||||
^- (quip card _state)
|
||||
:- [%give %fact [/all]~ %s3-update !>(act)]~
|
||||
?- -.act
|
||||
%set-endpoint
|
||||
state(endpoint.credentials endpoint.act)
|
||||
::
|
||||
%set-access-key-id
|
||||
state(access-key-id.credentials access-key-id.act)
|
||||
::
|
||||
%set-secret-access-key
|
||||
state(secret-access-key.credentials secret-access-key.act)
|
||||
::
|
||||
%set-current-bucket
|
||||
%_ state
|
||||
current-bucket.configuration bucket.act
|
||||
buckets.configuration (~(put in buckets.configuration) bucket.act)
|
||||
==
|
||||
::
|
||||
%add-bucket
|
||||
state(buckets.configuration (~(put in buckets.configuration) bucket.act))
|
||||
::
|
||||
%remove-bucket
|
||||
state(buckets.configuration (~(del in buckets.configuration) bucket.act))
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-watch
|
||||
~/ %s3-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
|^
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=/ cards=(list card)
|
||||
?+ path (on-watch:def path)
|
||||
[%all ~]
|
||||
:~ (give %s3-update !>([%credentials credentials]))
|
||||
(give %s3-update !>([%configuration configuration]))
|
||||
==
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ give
|
||||
|= =cage
|
||||
^- card
|
||||
[%give %fact ~ cage]
|
||||
--
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
File diff suppressed because one or more lines are too long
10
pkg/arvo/gen/s3-store/add-bucket.hoon
Normal file
10
pkg/arvo/gen/s3-store/add-bucket.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: s3-store|add-bucket: add new bucket to S3 store
|
||||
::
|
||||
/- *s3
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[bucket=@t ~] ~]
|
||||
==
|
||||
:- %s3-action
|
||||
^- action
|
||||
[%add-bucket bucket]
|
10
pkg/arvo/gen/s3-store/remove-bucket.hoon
Normal file
10
pkg/arvo/gen/s3-store/remove-bucket.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: s3-store|remove-bucket: remove bucket from S3 store
|
||||
::
|
||||
/- *s3
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[bucket=@t ~] ~]
|
||||
==
|
||||
:- %s3-action
|
||||
^- action
|
||||
[%remove-bucket bucket]
|
10
pkg/arvo/gen/s3-store/set-access-key-id.hoon
Normal file
10
pkg/arvo/gen/s3-store/set-access-key-id.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: s3-store|set-access-key-id: set S3 access key ID
|
||||
::
|
||||
/- *s3
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[access-key-id=@t ~] ~]
|
||||
==
|
||||
:- %s3-action
|
||||
^- action
|
||||
[%set-access-key-id access-key-id]
|
10
pkg/arvo/gen/s3-store/set-current-bucket.hoon
Normal file
10
pkg/arvo/gen/s3-store/set-current-bucket.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: s3-store|set-current-bucket: set current bucket for S3
|
||||
::
|
||||
/- *s3
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[bucket=@t ~] ~]
|
||||
==
|
||||
:- %s3-action
|
||||
^- action
|
||||
[%set-current-bucket bucket]
|
10
pkg/arvo/gen/s3-store/set-endpoint.hoon
Normal file
10
pkg/arvo/gen/s3-store/set-endpoint.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: s3-store|set-endpoint: set S3 endpoint
|
||||
::
|
||||
/- *s3
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[endpoint=@t ~] ~]
|
||||
==
|
||||
:- %s3-action
|
||||
^- action
|
||||
[%set-endpoint endpoint]
|
10
pkg/arvo/gen/s3-store/set-secret-access-key.hoon
Normal file
10
pkg/arvo/gen/s3-store/set-secret-access-key.hoon
Normal file
@ -0,0 +1,10 @@
|
||||
:: s3-store|set-secret-access-key: set S3 secret access key
|
||||
::
|
||||
/- *s3
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
[[secret-access-key=@t ~] ~]
|
||||
==
|
||||
:- %s3-action
|
||||
^- action
|
||||
[%set-secret-access-key secret-access-key]
|
@ -41,9 +41,16 @@
|
||||
(fall ((ot output+(ar dank) ~) a) ~)
|
||||
::
|
||||
++ lett
|
||||
=, enjs:format
|
||||
|= =letter
|
||||
^- json
|
||||
=, enjs:format
|
||||
=; result=(each json tang)
|
||||
?- -.result
|
||||
%& p.result
|
||||
%| (frond %text s+'[[json rendering error]]')
|
||||
==
|
||||
%- mule
|
||||
|.
|
||||
?- -.letter
|
||||
%text
|
||||
(frond %text s+text.letter)
|
||||
|
@ -1,10 +1,22 @@
|
||||
/- *contact-view
|
||||
/- *contact-view, *contact-hook
|
||||
/+ base64
|
||||
|%
|
||||
++ nu :: parse number as hex
|
||||
|= jon/json
|
||||
?> ?=({$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
|
||||
@ -15,38 +27,34 @@
|
||||
|= [pax=^path =contacts]
|
||||
^- [cord json]
|
||||
:- (spat pax)
|
||||
(contacts-to-json contacts)
|
||||
(contacts-to-json pax contacts)
|
||||
::
|
||||
++ contacts-to-json
|
||||
|= con=contacts
|
||||
|= [=path con=contacts]
|
||||
^- json
|
||||
=, enjs:format
|
||||
%- pairs
|
||||
%- pairs:enjs:format
|
||||
%+ turn ~(tap by con)
|
||||
|= [shp=^ship =contact]
|
||||
|= [=ship =contact]
|
||||
^- [cord json]
|
||||
:- (crip (slag 1 (scow %p shp)))
|
||||
(contact-to-json contact)
|
||||
[(crip (slag 1 (scow %p ship))) (contact-to-json path ship contact)]
|
||||
::
|
||||
++ contact-to-json
|
||||
|= con=contact
|
||||
|= [=path =ship con=contact]
|
||||
^- json
|
||||
=, enjs:format
|
||||
%- pairs
|
||||
%- pairs:enjs:format
|
||||
:~ [%nickname s+nickname.con]
|
||||
[%email s+email.con]
|
||||
[%phone s+phone.con]
|
||||
[%website s+website.con]
|
||||
[%notes s+notes.con]
|
||||
[%color s+(scot %ux color.con)]
|
||||
[%avatar s+'TODO']
|
||||
[%avatar (avatar-to-json path ship avatar.con)]
|
||||
==
|
||||
::
|
||||
++ edit-to-json
|
||||
|= edit=edit-field
|
||||
|= [=path =ship edit=edit-field]
|
||||
^- json
|
||||
=, enjs:format
|
||||
%+ frond -.edit
|
||||
%+ frond:enjs:format -.edit
|
||||
?- -.edit
|
||||
%nickname s+nickname.edit
|
||||
%email s+email.edit
|
||||
@ -54,7 +62,25 @@
|
||||
%website s+website.edit
|
||||
%notes s+notes.edit
|
||||
%color s+(scot %ux color.edit)
|
||||
%avatar s+'TODO'
|
||||
%avatar (avatar-to-json path ship avatar.edit)
|
||||
==
|
||||
::
|
||||
++ avatar-to-json
|
||||
|= [=path =ship avat=(unit avatar)]
|
||||
^- json
|
||||
?~ avat ~
|
||||
?- -.u.avat
|
||||
%octt
|
||||
:- %s
|
||||
%- crip
|
||||
%- zing
|
||||
:~ "/~groups/avatar"
|
||||
(trip (spat path))
|
||||
"/"
|
||||
(trip (scot %p ship))
|
||||
==
|
||||
::
|
||||
%url s+url.u.avat
|
||||
==
|
||||
::
|
||||
++ update-to-json
|
||||
@ -73,7 +99,7 @@
|
||||
%- pairs
|
||||
:~ [%path (path path.upd)]
|
||||
[%ship (ship ship.upd)]
|
||||
[%contact (contact-to-json contact.upd)]
|
||||
[%contact (contact-to-json path.upd ship.upd contact.upd)]
|
||||
==
|
||||
?: ?=(%remove -.upd)
|
||||
:- %remove
|
||||
@ -86,7 +112,7 @@
|
||||
%- pairs
|
||||
:~ [%path (path path.upd)]
|
||||
[%ship (ship ship.upd)]
|
||||
[%edit-field (edit-to-json edit-field.upd)]
|
||||
[%edit-field (edit-to-json path.upd ship.upd edit-field.upd)]
|
||||
==
|
||||
[*@t *^json]
|
||||
==
|
||||
@ -179,10 +205,31 @@
|
||||
==
|
||||
::
|
||||
++ avat
|
||||
%- ot:dejs:format
|
||||
:~ [%content-type so:dejs:format]
|
||||
[%octs octet]
|
||||
|= jon=json
|
||||
^- avatar
|
||||
|^
|
||||
=/ =avatar (parse-json jon)
|
||||
?- -.avatar
|
||||
%url avatar
|
||||
%octt
|
||||
=. octs.avatar (need (de:base64 q.octs.avatar))
|
||||
avatar
|
||||
==
|
||||
::
|
||||
++ parse-json
|
||||
%- of:dejs:format
|
||||
:~ [%octt octt]
|
||||
[%url url]
|
||||
==
|
||||
::
|
||||
++ octt
|
||||
%- ot:dejs:format
|
||||
:~ [%content-type so:dejs:format]
|
||||
[%octs octet]
|
||||
==
|
||||
::
|
||||
++ url so:dejs:format
|
||||
--
|
||||
::
|
||||
++ cont
|
||||
%- ot:dejs:format
|
||||
|
@ -118,6 +118,7 @@
|
||||
%link-view
|
||||
%metadata-store
|
||||
%metadata-hook
|
||||
%s3-store
|
||||
==
|
||||
::
|
||||
++ deft-fish :: default connects
|
||||
@ -155,6 +156,7 @@
|
||||
=+ (~(gut by bin) ost *source)
|
||||
=* dev -
|
||||
|_ {moz/(list card:agent:gall) biz/(list dill-blit:dill)}
|
||||
+* this .
|
||||
++ diff-sole-effect-phat :: app event
|
||||
|= {way/wire fec/sole-effect}
|
||||
=< se-abet =< se-view
|
||||
@ -223,7 +225,7 @@
|
||||
==
|
||||
::
|
||||
++ on-load
|
||||
|= ver=?(%1 %2)
|
||||
|= ver=?(%1 %2 %3)
|
||||
?- ver
|
||||
%1
|
||||
=< se-abet =< se-view
|
||||
@ -237,7 +239,8 @@
|
||||
=< (se-born %home %link-store)
|
||||
=< (se-born %home %link-proxy-hook)
|
||||
=< (se-born %home %link-listen-hook)
|
||||
(se-born %home %link-view)
|
||||
=< (se-born %home %link-view)
|
||||
(se-born %home %s3-store)
|
||||
::
|
||||
%2
|
||||
=< se-abet =< se-view
|
||||
@ -250,7 +253,22 @@
|
||||
=< (se-born %home %link-store)
|
||||
=< (se-born %home %link-proxy-hook)
|
||||
=< (se-born %home %link-listen-hook)
|
||||
(se-born %home %link-view)
|
||||
=< (se-born %home %link-view)
|
||||
(se-born %home %s3-store)
|
||||
::
|
||||
%3
|
||||
=< se-abet =< se-view
|
||||
=< (se-emit %pass /kiln %arvo %g %sear ~wisrut-nocsub)
|
||||
=< (se-born %home %metadata-store)
|
||||
=< (se-born %home %metadata-hook)
|
||||
=< (se-born %home %contact-store)
|
||||
=< (se-born %home %contact-hook)
|
||||
=< (se-born %home %contact-view)
|
||||
=< (se-born %home %link-store)
|
||||
=< (se-born %home %link-proxy-hook)
|
||||
=< (se-born %home %link-listen-hook)
|
||||
=< (se-born %home %link-view)
|
||||
(se-born %home %s3-store)
|
||||
==
|
||||
::
|
||||
++ reap-phat :: ack connect
|
||||
@ -329,23 +347,70 @@
|
||||
[%give %fact ~[/drum] %dill-blit !>(dill-blit)]
|
||||
::
|
||||
++ se-adit :: update servers
|
||||
^+ .
|
||||
:: ensure dojo connects after talk
|
||||
=* dojo-on-top |=([a=* b=*] |(=(%dojo a) &(!=(%dojo b) (aor a b))))
|
||||
%+ roll (sort ~(tap in ray) dojo-on-top)
|
||||
=< .(con +>)
|
||||
|: $:{wel/well:gall con/_..se-adit} ^+ con
|
||||
=. +>.$ con
|
||||
=+ hig=(~(get by fur) q.wel)
|
||||
?: &(?=(^ hig) |(?=(~ u.hig) =(p.wel syd.u.u.hig))) +>.$
|
||||
=. +>.$ (se-text "activated app {(trip p.wel)}/{(trip q.wel)}")
|
||||
%- se-emit(fur (~(put by fur) q.wel ~))
|
||||
^+ this
|
||||
|^
|
||||
=/ servers=(list well:gall)
|
||||
(sort ~(tap in ray) sort-by-priorities)
|
||||
|-
|
||||
?~ servers
|
||||
this
|
||||
=/ wel=well:gall
|
||||
i.servers
|
||||
=/ =wire [%drum p.wel q.wel ~]
|
||||
[%pass wire %arvo %g %conf [our.hid q.wel] our.hid p.wel]
|
||||
=/ hig=(unit (unit server))
|
||||
(~(get by fur) q.wel)
|
||||
?: &(?=(^ hig) |(?=(~ u.hig) =(p.wel syd.u.u.hig)))
|
||||
$(servers t.servers)
|
||||
=. fur
|
||||
(~(put by fur) q.wel ~)
|
||||
=. this
|
||||
(se-text "activated app {(trip p.wel)}/{(trip q.wel)}")
|
||||
=. this
|
||||
%- se-emit
|
||||
[%pass wire %arvo %g %conf [our.hid q.wel] our.hid p.wel]
|
||||
$(servers t.servers)
|
||||
::
|
||||
++ priorities
|
||||
^- (list (set @))
|
||||
:~
|
||||
:: set up stores with priority: depended on, but never depending
|
||||
%- sy
|
||||
:~ %permission-store
|
||||
%chat-store
|
||||
%contact-store
|
||||
%group-store
|
||||
%link-store
|
||||
%invite-store
|
||||
%metadata-store
|
||||
==
|
||||
:: ensure chat-cli can sub to invites
|
||||
(sy ~[%chat-hook])
|
||||
==
|
||||
++ sort-by-priorities
|
||||
=/ priorities priorities
|
||||
|= [[desk a=term] [desk b=term]]
|
||||
^- ?
|
||||
?~ priorities
|
||||
(aor a b)
|
||||
=* priority i.priorities
|
||||
?: &((~(has in priority) a) (~(has in priority) b))
|
||||
(aor a b)
|
||||
?: (~(has in priority) a)
|
||||
%.y
|
||||
?: (~(has in priority) b)
|
||||
%.n
|
||||
$(priorities t.priorities)
|
||||
--
|
||||
::
|
||||
++ se-adze :: update connections
|
||||
^+ .
|
||||
%+ roll ~(tap in eel)
|
||||
%+ roll
|
||||
%+ sort
|
||||
~(tap in eel)
|
||||
|= [[@ a=term] [@ b=term]]
|
||||
?: =(a %dojo) %.n
|
||||
?: =(b %dojo) %.y
|
||||
(aor a b)
|
||||
=< .(con +>)
|
||||
|: $:{gil/gill:gall con/_.} ^+ con
|
||||
=. +>.$ con
|
||||
|
50
pkg/arvo/lib/s3-json.hoon
Normal file
50
pkg/arvo/lib/s3-json.hoon
Normal file
@ -0,0 +1,50 @@
|
||||
/- *s3
|
||||
|%
|
||||
++ json-to-action
|
||||
|= =json
|
||||
^- action
|
||||
=, format
|
||||
|^ (parse-json json)
|
||||
++ parse-json
|
||||
%- of:dejs
|
||||
:~ [%set-endpoint so:dejs]
|
||||
[%set-access-key-id so:dejs]
|
||||
[%set-secret-access-key so:dejs]
|
||||
[%add-bucket so:dejs]
|
||||
[%remove-bucket so:dejs]
|
||||
[%set-current-bucket so:dejs]
|
||||
==
|
||||
--
|
||||
::
|
||||
++ update-to-json
|
||||
|= upd=update
|
||||
^- json
|
||||
=, format
|
||||
%+ frond:enjs %s3-update
|
||||
%- pairs:enjs
|
||||
:~ ?- -.upd
|
||||
%set-current-bucket [%'setCurrentBucket' s+bucket.upd]
|
||||
%add-bucket [%'addBucket' s+bucket.upd]
|
||||
%remove-bucket [%'removeBucket' s+bucket.upd]
|
||||
%set-endpoint [%'setEndpoint' s+endpoint.upd]
|
||||
%set-access-key-id [%'setAccessKeyId' s+access-key-id.upd]
|
||||
%set-secret-access-key
|
||||
[%'setSecretAccessKey' s+secret-access-key.upd]
|
||||
::
|
||||
%credentials
|
||||
:- %credentials
|
||||
%- pairs:enjs
|
||||
:~ [%endpoint s+endpoint.credentials.upd]
|
||||
[%'accessKeyId' s+access-key-id.credentials.upd]
|
||||
[%'secretAccessKey' s+secret-access-key.credentials.upd]
|
||||
==
|
||||
::
|
||||
%configuration
|
||||
:- %configuration
|
||||
%- pairs:enjs
|
||||
:~ [%buckets a+(turn ~(tap in buckets.configuration.upd) |=(a=@t s+a))]
|
||||
[%'currentBucket' s+current-bucket.configuration.upd]
|
||||
==
|
||||
==
|
||||
==
|
||||
--
|
13
pkg/arvo/mar/contact/hook-update.hoon
Normal file
13
pkg/arvo/mar/contact/hook-update.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *contact-json
|
||||
|_ upd=contact-hook-update
|
||||
++ grow
|
||||
|%
|
||||
++ json (hook-update-to-json upd)
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun contact-hook-update
|
||||
--
|
||||
::
|
||||
--
|
@ -32,6 +32,7 @@
|
||||
subscribe+subscribe
|
||||
unsubscribe+unsubscribe
|
||||
read+read
|
||||
groupify+groupify
|
||||
==
|
||||
::
|
||||
++ new-book
|
||||
@ -114,6 +115,12 @@
|
||||
book+so
|
||||
note+so
|
||||
==
|
||||
++ groupify
|
||||
%- ot
|
||||
:~ book+so
|
||||
target+(mu pa)
|
||||
inclusive+bo
|
||||
==
|
||||
++ group-info
|
||||
%- ot
|
||||
:~ group-path+pa
|
||||
|
8
pkg/arvo/mar/s3/action.hoon
Normal file
8
pkg/arvo/mar/s3/action.hoon
Normal file
@ -0,0 +1,8 @@
|
||||
/+ *s3-json
|
||||
|_ act=action
|
||||
++ grab
|
||||
|%
|
||||
++ noun action
|
||||
++ json json-to-action
|
||||
--
|
||||
--
|
12
pkg/arvo/mar/s3/update.hoon
Normal file
12
pkg/arvo/mar/s3/update.hoon
Normal file
@ -0,0 +1,12 @@
|
||||
/+ *s3-json
|
||||
|_ upd=update
|
||||
++ grow
|
||||
|%
|
||||
++ json (update-to-json upd)
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun update
|
||||
--
|
||||
--
|
@ -12,4 +12,7 @@
|
||||
::
|
||||
[%remove =path]
|
||||
==
|
||||
::
|
||||
+$ synced (map path ship)
|
||||
+$ contact-hook-update [%initial =synced]
|
||||
--
|
||||
|
@ -1,10 +1,11 @@
|
||||
/- *identity
|
||||
|%
|
||||
+$ rolodex (map path contacts)
|
||||
::
|
||||
+$ contacts (map ship contact)
|
||||
::
|
||||
+$ avatar [content-type=@t octs=[p=@ud q=@t]]
|
||||
+$ rolodex (map path contacts)
|
||||
+$ contacts (map ship contact)
|
||||
+$ avatar
|
||||
$% [%octt content-type=@t octs=[p=@ud q=@t]]
|
||||
[%url url=@t]
|
||||
==
|
||||
::
|
||||
+$ contact
|
||||
$: nickname=@t
|
||||
|
@ -25,6 +25,8 @@
|
||||
[%unsubscribe who=@p book=@tas]
|
||||
::
|
||||
[%read who=@p book=@tas note=@tas]
|
||||
::
|
||||
[%groupify book=@tas target=(unit path) inclusive=?]
|
||||
==
|
||||
::
|
||||
+$ comment comment-3
|
||||
|
27
pkg/arvo/sur/s3.hoon
Normal file
27
pkg/arvo/sur/s3.hoon
Normal file
@ -0,0 +1,27 @@
|
||||
|%
|
||||
+$ credentials
|
||||
$: endpoint=@t
|
||||
access-key-id=@t
|
||||
secret-access-key=@t
|
||||
==
|
||||
::
|
||||
+$ configuration
|
||||
$: buckets=(set @t)
|
||||
current-bucket=@t
|
||||
==
|
||||
::
|
||||
+$ action
|
||||
$% [%set-endpoint endpoint=@t]
|
||||
[%set-access-key-id access-key-id=@t]
|
||||
[%set-secret-access-key secret-access-key=@t]
|
||||
[%add-bucket bucket=@t]
|
||||
[%remove-bucket bucket=@t]
|
||||
[%set-current-bucket bucket=@t]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% [%credentials =credentials]
|
||||
[%configuration =configuration]
|
||||
action
|
||||
==
|
||||
--
|
@ -12085,6 +12085,7 @@
|
||||
^- (list term)
|
||||
?+ typ ~
|
||||
{$hold *} $(typ ~(repo ut typ))
|
||||
{$hint *} $(typ ~(repo ut typ))
|
||||
{$core *}
|
||||
%- zing
|
||||
%+ turn ~(tap by q.r.q.typ)
|
||||
|
@ -78,7 +78,7 @@ instance FromNoun H.StdMethod where
|
||||
|
||||
-- Http Server Configuration ---------------------------------------------------
|
||||
|
||||
newtype PEM = PEM { unPEM :: Cord }
|
||||
newtype PEM = PEM { unPEM :: Wain }
|
||||
deriving newtype (Eq, Ord, Show, ToNoun, FromNoun)
|
||||
|
||||
type Key = PEM
|
||||
|
@ -24,6 +24,7 @@ type Life = Word -- Number of Azimoth key revs.
|
||||
type Bloq = Atom -- TODO
|
||||
type Oath = Atom -- Signature
|
||||
|
||||
|
||||
-- Parsed URLs -----------------------------------------------------------------
|
||||
|
||||
type Host = Each Turf Ipv4
|
||||
@ -169,7 +170,7 @@ data HttpServerReq = HttpServerReq
|
||||
data HttpClientEv
|
||||
= HttpClientEvReceive (KingId, ()) ServerId HttpEvent
|
||||
| HttpClientEvBorn (KingId, ()) ()
|
||||
| HttpClientEvCrud Path Cord Tang
|
||||
| HttpClientEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
data HttpServerEv
|
||||
@ -178,7 +179,7 @@ data HttpServerEv
|
||||
| HttpServerEvRequestLocal (ServId, UD, UD, ()) HttpServerReq
|
||||
| HttpServerEvLive (ServId, ()) Port (Maybe Port)
|
||||
| HttpServerEvBorn (KingId, ()) ()
|
||||
| HttpServerEvCrud Path Cord Tang
|
||||
| HttpServerEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
deriveNoun ''Address
|
||||
@ -193,7 +194,7 @@ deriveNoun ''HttpServerReq
|
||||
data AmesEv
|
||||
= AmesEvHear () AmesDest Bytes
|
||||
| AmesEvHole () AmesDest Bytes
|
||||
| AmesEvCrud Path Cord Tang
|
||||
| AmesEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
deriveNoun ''AmesEv
|
||||
@ -202,10 +203,10 @@ deriveNoun ''AmesEv
|
||||
-- Arvo Events -----------------------------------------------------------------
|
||||
|
||||
data ArvoEv
|
||||
= ArvoEvWhom () Ship
|
||||
| ArvoEvWack () Word512
|
||||
= ArvoEvWhom () Ship
|
||||
| ArvoEvWack () Word512
|
||||
| ArvoEvWarn Path Noun
|
||||
| ArvoEvCrud Path Cord Tang
|
||||
| ArvoEvCrud Path Noun
|
||||
| ArvoEvVeer Atom Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
@ -216,7 +217,7 @@ deriveNoun ''ArvoEv
|
||||
|
||||
data BoatEv
|
||||
= BoatEvBoat () ()
|
||||
| BoatEvCrud Path Cord Tang
|
||||
| BoatEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
deriveNoun ''BoatEv
|
||||
@ -227,7 +228,7 @@ deriveNoun ''BoatEv
|
||||
data BehnEv
|
||||
= BehnEvWake () ()
|
||||
| BehnEvBorn (KingId, ()) ()
|
||||
| BehnEvCrud Path Cord Tang
|
||||
| BehnEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
deriveNoun ''BehnEv
|
||||
@ -237,7 +238,7 @@ deriveNoun ''BehnEv
|
||||
|
||||
data NewtEv
|
||||
= NewtEvBorn (KingId, ()) ()
|
||||
| NewtEvCrud Path Cord Tang
|
||||
| NewtEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
deriveNoun ''NewtEv
|
||||
@ -247,7 +248,7 @@ deriveNoun ''NewtEv
|
||||
|
||||
data SyncEv
|
||||
= SyncEvInto (Nullable (KingId, ())) Desk Bool [(Path, Maybe Mime)]
|
||||
| SyncEvCrud Path Cord Tang
|
||||
| SyncEvCrud Path Noun
|
||||
deriving (Eq, Ord, Show)
|
||||
|
||||
deriveNoun ''SyncEv
|
||||
@ -278,7 +279,7 @@ data TermEv
|
||||
| TermEvBlew (UD, ()) Word Word
|
||||
| TermEvBoot (UD, ()) Bool LegacyBootEvent
|
||||
| TermEvHail (UD, ()) ()
|
||||
| TermEvCrud Path Cord Tang
|
||||
| TermEvCrud Path Noun
|
||||
deriving (Eq, Show)
|
||||
|
||||
deriveNoun ''LegacyBootEvent
|
||||
|
@ -8,7 +8,7 @@ module Urbit.Noun.Conversions
|
||||
, Bytes(..), Octs(..), File(..)
|
||||
, Cord(..), Knot(..), Term(..), Tape(..), Tour(..)
|
||||
, BigTape(..), BigCord(..)
|
||||
, Wall, Each(..)
|
||||
, Wain(..), Wall, Each(..)
|
||||
, UD(..), UV(..), UW(..), cordToUW
|
||||
, Mug(..), Path(..), EvilPath(..), Ship(..)
|
||||
, Lenient(..), pathToFilePath, filePathToPath
|
||||
@ -442,6 +442,20 @@ instance FromNoun Tape where
|
||||
Right tx -> pure (Tape tx)
|
||||
|
||||
|
||||
-- Wain -- List of Lines -------------------------------------------------------
|
||||
|
||||
newtype Wain = Wain { unWain :: Text }
|
||||
deriving newtype (Eq, Ord, Show, IsString, NFData)
|
||||
|
||||
instance ToNoun Wain where
|
||||
toNoun (Wain t) = toNoun (Cord <$> lines t)
|
||||
|
||||
instance FromNoun Wain where
|
||||
parseNoun n = named "Wain" $ do
|
||||
tx :: [Cord] <- parseNoun n
|
||||
pure $ Wain $ unlines (unCord <$> tx)
|
||||
|
||||
|
||||
-- Wall -- Text Lines ----------------------------------------------------------
|
||||
|
||||
type Wall = [Tape]
|
||||
|
@ -33,6 +33,7 @@ import Urbit.Vere.Pier.Types
|
||||
|
||||
import Data.Binary.Builder (Builder, fromByteString)
|
||||
import Data.Bits (shiftL, (.|.))
|
||||
import Data.PEM (pemParseBS, pemWriteBS)
|
||||
import Network.Socket (SockAddr(..))
|
||||
import System.Directory (doesFileExist, removeFile)
|
||||
import System.Random (randomIO)
|
||||
@ -216,6 +217,9 @@ writePortsFile f = writeFile f . encodeUtf8 . portsFileText
|
||||
cordBytes :: Cord -> ByteString
|
||||
cordBytes = encodeUtf8 . unCord
|
||||
|
||||
wainBytes :: Wain -> ByteString
|
||||
wainBytes = encodeUtf8 . unWain
|
||||
|
||||
pass :: Monad m => m ()
|
||||
pass = pure ()
|
||||
|
||||
@ -499,14 +503,22 @@ httpServerPorts fak = do
|
||||
|
||||
pure (PortsToTry { .. })
|
||||
|
||||
parseCerts :: ByteString -> Maybe (ByteString, [ByteString])
|
||||
parseCerts bs = do
|
||||
pems <- pemParseBS bs & either (const Nothing) Just
|
||||
case pems of
|
||||
[] -> Nothing
|
||||
p:ps -> pure (pemWriteBS p, pemWriteBS <$> ps)
|
||||
|
||||
startServ :: (HasPierConfig e, HasLogFunc e, HasNetworkConfig e)
|
||||
=> Bool -> HttpServerConf -> (Ev -> STM ())
|
||||
-> RIO e Serv
|
||||
startServ isFake conf plan = do
|
||||
logDebug "startServ"
|
||||
|
||||
let tls = hscSecure conf <&> \(PEM key, PEM cert) ->
|
||||
(W.tlsSettingsMemory (cordBytes cert) (cordBytes key))
|
||||
let tls = do (PEM key, PEM certs) <- hscSecure conf
|
||||
(cert, chain) <- parseCerts (wainBytes certs)
|
||||
pure $ W.tlsSettingsChainMemory cert chain $ wainBytes key
|
||||
|
||||
sId <- io $ ServId . UV . fromIntegral <$> (randomIO :: IO Word32)
|
||||
liv <- newTVarIO emptyLiveReqs
|
||||
|
@ -67,6 +67,7 @@ dependencies:
|
||||
- network
|
||||
- optparse-applicative
|
||||
- para
|
||||
- pem
|
||||
- pretty-show
|
||||
- primitive
|
||||
- process
|
||||
|
@ -71,7 +71,7 @@ h2 {
|
||||
}
|
||||
|
||||
.clamp-attachment {
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
max-height: 10em;
|
||||
max-width: 100%;
|
||||
}
|
||||
@ -169,6 +169,15 @@ h2 {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.shadow-6 {
|
||||
box-shadow: 2px 4px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.brt2 {
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
}
|
||||
|
||||
|
||||
.green3 {
|
||||
color: #7ea899;
|
||||
}
|
||||
@ -363,6 +372,9 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
|
||||
.b--white-d {
|
||||
border-color: #fff;
|
||||
}
|
||||
.b--green2-d {
|
||||
border-color: #2aa779;
|
||||
}
|
||||
.bb-d {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
|
@ -72,7 +72,7 @@ class UrbitApi {
|
||||
|
||||
addPendingMessage(msg) {
|
||||
if (store.state.pendingMessages.has(msg.path)) {
|
||||
store.state.pendingMessages.get(msg.path).push(msg.envelope);
|
||||
store.state.pendingMessages.get(msg.path).unshift(msg.envelope);
|
||||
} else {
|
||||
store.state.pendingMessages.set(msg.path, [msg.envelope]);
|
||||
}
|
||||
|
@ -22,6 +22,37 @@ function getNumPending(props) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const ACTIVITY_TIMEOUT = 60000; // a minute
|
||||
const DEFAULT_BACKLOG_SIZE = 300;
|
||||
|
||||
function scrollIsAtTop(container) {
|
||||
if ((navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return container.scrollTop === 0;
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
return container.scrollHeight + Math.round(container.scrollTop) <=
|
||||
container.clientHeight + 10;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollIsAtBottom(container) {
|
||||
if ((navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
return container.scrollHeight - Math.round(container.scrollTop) <=
|
||||
container.clientHeight + 10;
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
return container.scrollTop === 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -29,9 +60,10 @@ export class ChatScreen extends Component {
|
||||
this.state = {
|
||||
numPages: 1,
|
||||
scrollLocked: false,
|
||||
read: props.read,
|
||||
active: true,
|
||||
// only for FF
|
||||
lastScrollHeight: null,
|
||||
scrollBottom: true
|
||||
};
|
||||
|
||||
this.hasAskedForMessages = false;
|
||||
@ -41,6 +73,12 @@ export class ChatScreen extends Component {
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
|
||||
this.unreadMarker = null;
|
||||
this.scrolledToMarker = false;
|
||||
this.setUnreadMarker = this.setUnreadMarker.bind(this);
|
||||
|
||||
this.activityTimeout = true;
|
||||
this.handleActivity = this.handleActivity.bind(this);
|
||||
this.setInactive = this.setInactive.bind(this);
|
||||
|
||||
moment.updateLocale('en', {
|
||||
calendar: {
|
||||
@ -56,10 +94,72 @@ export class ChatScreen extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.askForMessages();
|
||||
this.scrollToBottom();
|
||||
document.addEventListener("mousemove", this.handleActivity, false);
|
||||
document.addEventListener("mousedown", this.handleActivity, false);
|
||||
document.addEventListener("keypress", this.handleActivity, false);
|
||||
document.addEventListener("touchmove", this.handleActivity, false);
|
||||
this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("mousemove", this.handleActivity, false);
|
||||
document.removeEventListener("mousedown", this.handleActivity, false);
|
||||
document.removeEventListener("keypress", this.handleActivity, false);
|
||||
document.removeEventListener("touchmove", this.handleActivity, false);
|
||||
if(this.activityTimeout) {
|
||||
clearTimeout(this.activityTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleActivity() {
|
||||
if(!this.state.active) {
|
||||
this.setState({ active: true });
|
||||
}
|
||||
|
||||
if(this.activityTimeout) {
|
||||
clearTimeout(this.activityTimeout);
|
||||
}
|
||||
|
||||
this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
setInactive() {
|
||||
this.activityTimeout = null;
|
||||
this.setState({ active: false, scrollLocked: true });
|
||||
}
|
||||
|
||||
receivedNewChat() {
|
||||
const { props } = this;
|
||||
this.hasAskedForMessages = false;
|
||||
|
||||
this.unreadMarker = null;
|
||||
this.scrolledToMarker = false;
|
||||
|
||||
this.setState({ read: props.read });
|
||||
|
||||
const unread = props.length - props.read;
|
||||
const unreadUnloaded = unread - props.envelopes.length;
|
||||
|
||||
if(unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) {
|
||||
this.askForMessages(unreadUnloaded + 20);
|
||||
} else {
|
||||
this.askForMessages(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
|
||||
if(props.read === props.length){
|
||||
this.scrolledToMarker = true;
|
||||
this.setState(
|
||||
{
|
||||
scrollLocked: false,
|
||||
},
|
||||
() => {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.setState({ scrollLocked: true, numPages: Math.ceil(unread/100) });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
@ -68,18 +168,7 @@ export class ChatScreen extends Component {
|
||||
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();
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{ scrollLocked: false },
|
||||
() => {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
);
|
||||
this.receivedNewChat();
|
||||
} else if (props.chatInitialized &&
|
||||
!(props.station in props.inbox) &&
|
||||
(!!props.chatSynced && !(props.station in props.chatSynced))) {
|
||||
@ -89,21 +178,26 @@ export class ChatScreen extends Component {
|
||||
props.envelopes.length >= prevProps.envelopes.length + 10
|
||||
) {
|
||||
this.hasAskedForMessages = false;
|
||||
} else if(props.length !== prevProps.length &&
|
||||
prevProps.length === prevState.read &&
|
||||
state.active
|
||||
) {
|
||||
this.setState({ read: props.length });
|
||||
this.props.api.chat.read(this.props.station);
|
||||
}
|
||||
|
||||
if(!prevProps.chatInitialized && props.chatInitialized) {
|
||||
this.receivedNewChat();
|
||||
}
|
||||
|
||||
// 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.scrollToBottom();
|
||||
if(navigator.userAgent.includes("Firefox")) {
|
||||
this.recalculateScrollTop();
|
||||
}
|
||||
|
||||
@ -111,16 +205,9 @@ export class ChatScreen extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
askForMessages() {
|
||||
askForMessages(size) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (props.envelopes.length === 0) {
|
||||
setTimeout(() => {
|
||||
this.askForMessages();
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
props.envelopes.length >= props.length ||
|
||||
this.hasAskedForMessages ||
|
||||
@ -132,7 +219,7 @@ export class ChatScreen extends Component {
|
||||
let start =
|
||||
props.length - props.envelopes[props.envelopes.length - 1].number;
|
||||
if (start > 0) {
|
||||
let end = start + 300 < props.length ? start + 300 : props.length;
|
||||
const end = start + size < props.length ? start + size : props.length;
|
||||
this.hasAskedForMessages = true;
|
||||
props.subscription.fetchMessages(start + 1, end, props.station);
|
||||
}
|
||||
@ -161,80 +248,51 @@ export class ChatScreen extends Component {
|
||||
}
|
||||
|
||||
onScroll(e) {
|
||||
if (
|
||||
(navigator.userAgent.includes("Safari") &&
|
||||
navigator.userAgent.includes("Chrome")) ||
|
||||
navigator.userAgent.includes("Firefox")
|
||||
) {
|
||||
// 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
|
||||
})
|
||||
if(scrollIsAtTop(e.target)) {
|
||||
// Save scroll position for FF
|
||||
if (navigator.userAgent.includes('Firefox')) {
|
||||
this.setState({
|
||||
lastScrollHeight: e.target.scrollHeight
|
||||
});
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
numPages: this.state.numPages + 1,
|
||||
scrollLocked: true
|
||||
},
|
||||
() => {
|
||||
this.askForMessages(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
numPages: this.state.numPages + 1,
|
||||
scrollLocked: true
|
||||
},
|
||||
() => {
|
||||
this.askForMessages();
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
e.target.scrollHeight - Math.round(e.target.scrollTop) ===
|
||||
e.target.clientHeight
|
||||
) {
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false,
|
||||
scrollBottom: true
|
||||
});
|
||||
} else if (navigator.userAgent.includes('Firefox')) {
|
||||
this.setState({ scrollBottom: false });
|
||||
}
|
||||
} else if (navigator.userAgent.includes("Safari")) {
|
||||
// Safari
|
||||
if (e.target.scrollTop === 0) {
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
});
|
||||
} else if (
|
||||
e.target.scrollHeight + Math.round(e.target.scrollTop) <=
|
||||
e.target.clientHeight + 10
|
||||
) {
|
||||
this.setState(
|
||||
{
|
||||
numPages: this.state.numPages + 1,
|
||||
scrollLocked: true
|
||||
},
|
||||
() => {
|
||||
this.askForMessages();
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("Your browser is not supported.");
|
||||
);
|
||||
} else if (scrollIsAtBottom(e.target)) {
|
||||
this.dismissUnread();
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setUnreadMarker(ref) {
|
||||
if(ref && !this.scrolledToMarker) {
|
||||
this.setState({ scrollLocked: true }, () => {
|
||||
ref.scrollIntoView({ block: 'center' });
|
||||
if(ref.offsetParent &&
|
||||
scrollIsAtBottom(ref.offsetParent)) {
|
||||
this.dismissUnread();
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
});
|
||||
}
|
||||
});
|
||||
this.scrolledToMarker = true;
|
||||
}
|
||||
this.unreadMarker = ref;
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
this.props.api.chat.read(this.props.station);
|
||||
}
|
||||
|
||||
chatWindow(unread) {
|
||||
@ -288,14 +346,15 @@ export class ChatScreen extends Component {
|
||||
paddingTop={paddingTop}
|
||||
paddingBot={paddingBot}
|
||||
pending={!!msg.pending}
|
||||
group={props.association}
|
||||
/>
|
||||
);
|
||||
if(unread > 0 && i === unread) {
|
||||
if(unread > 0 && i === unread - 1) {
|
||||
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" />
|
||||
<div key={'unreads'+ msg.uid} ref={this.setUnreadMarker} className="mv2 green2 flex items-center f9">
|
||||
<hr className="dn-s ma0 w2 b--green2 bt-0" />
|
||||
<p className="mh4">
|
||||
New messages below
|
||||
</p>
|
||||
@ -327,7 +386,7 @@ export class ChatScreen extends Component {
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
return (
|
||||
<div className="overflow-y-scroll h-100" onScroll={this.onScroll} ref={e => { this.scrollContainer = e; }}>
|
||||
<div className="relative 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" }}
|
||||
@ -358,7 +417,7 @@ export class ChatScreen extends Component {
|
||||
else {
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
||||
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse relative"
|
||||
style={{ height: "100%", resize: "vertical" }}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
@ -409,10 +468,13 @@ export class ChatScreen extends Component {
|
||||
: props.station.substr(1);
|
||||
}
|
||||
|
||||
const unread = props.length - props.read;
|
||||
const unread = props.length - state.read;
|
||||
|
||||
const unreadMsg = unread > 0 && messages[unread - 1];
|
||||
|
||||
|
||||
const showUnreadNotice = props.length !== props.read && props.read === state.read;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={props.station}
|
||||
@ -448,11 +510,11 @@ export class ChatScreen extends Component {
|
||||
api={props.api}
|
||||
/>
|
||||
</div>
|
||||
{ !!unreadMsg && (
|
||||
{ !!unreadMsg && showUnreadNotice && (
|
||||
<UnreadNotice
|
||||
unread={unread}
|
||||
unreadMsg={unreadMsg}
|
||||
onRead={() => props.api.chat.read(props.station)}
|
||||
onRead={() => this.dismissUnread()}
|
||||
/>
|
||||
) }
|
||||
{this.chatWindow(unread)}
|
||||
@ -464,6 +526,8 @@ export class ChatScreen extends Component {
|
||||
ownerContact={ownerContact}
|
||||
envelopes={props.envelopes}
|
||||
contacts={props.contacts}
|
||||
onEnter={() => this.setState({ scrollLocked: false })}
|
||||
s3={props.s3}
|
||||
placeholder="Message..."
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,129 +1,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 { 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 { S3Upload } from '/components/lib/s3-upload';
|
||||
|
||||
import { uuid, uxToHex, hexToRgba } from '/lib/util';
|
||||
import { uxToHex } from '/lib/util';
|
||||
|
||||
const MARKDOWN_CONFIG = {
|
||||
name: "markdown",
|
||||
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",
|
||||
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;
|
||||
|
||||
const INPUT_TOP_PADDING = 3;
|
||||
|
||||
|
||||
function getAdvance(a, b) {
|
||||
let res = '';
|
||||
if(!a) {
|
||||
return b;
|
||||
}
|
||||
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"
|
||||
let nickname;
|
||||
let nameStyle = {};
|
||||
const isSelected = ship === selected;
|
||||
if (contact) {
|
||||
const hex = uxToHex(contact.color);
|
||||
color = `#${hex}`;
|
||||
nameStyle.color = hexToRgba(hex, .7);
|
||||
nameStyle.textShadow = '0px 0px 0px #000';
|
||||
nameStyle.filter = 'contrast(1.3) saturate(1.5)';
|
||||
sigilClass = "v-mid";
|
||||
nickname = contact.nickname;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onSelect(ship)}
|
||||
className={cn(
|
||||
'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,
|
||||
}
|
||||
)}
|
||||
key={ship}
|
||||
>
|
||||
<Sigil
|
||||
ship={'~' + ship}
|
||||
size={24}
|
||||
color={color}
|
||||
classes={sigilClass}
|
||||
/>
|
||||
{ nickname && (
|
||||
<p style={nameStyle} className="dib ml4 b" >{nickname}</p>)
|
||||
}
|
||||
<div className="mono gray2 ml4">
|
||||
{'~' + ship}
|
||||
</div>
|
||||
<p className="nowrap ml4">
|
||||
{status}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function ChatInputSuggestions({ suggestions, onSelect, selected, contacts }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
bottom: '90%',
|
||||
left: '48px'
|
||||
}}
|
||||
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} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export class ChatInput extends Component {
|
||||
constructor(props) {
|
||||
@ -131,8 +39,7 @@ export class ChatInput extends Component {
|
||||
|
||||
this.state = {
|
||||
message: '',
|
||||
patpSuggestions: [],
|
||||
selectedSuggestion: null
|
||||
patpSearch: null
|
||||
};
|
||||
|
||||
this.textareaRef = React.createRef();
|
||||
@ -141,18 +48,15 @@ export class ChatInput extends Component {
|
||||
this.messageChange = this.messageChange.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);
|
||||
this.clearSearch = this.clearSearch.bind(this);
|
||||
|
||||
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++;
|
||||
@ -174,21 +78,21 @@ 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'
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -206,56 +110,20 @@ export class ChatInput extends Component {
|
||||
this.setState({ selectedSuggestion: patpSuggestions[idx] });
|
||||
}
|
||||
|
||||
|
||||
patpAutocomplete(message, fresh = false) {
|
||||
patpAutocomplete(message) {
|
||||
const match = /~([a-zA-Z\-]*)$/.exec(message);
|
||||
|
||||
if (!match ) {
|
||||
this.setState({ patpSuggestions: [] })
|
||||
this.setState({ patpSearch: null });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const needle = match[1].toLowerCase();
|
||||
|
||||
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 }))
|
||||
.filter(({ nickname, ship }) => matchString(nickname) || matchString(ship))
|
||||
.map('ship')
|
||||
.value()
|
||||
|
||||
const suggestions = _.chain(this.props.envelopes)
|
||||
.defaultTo([])
|
||||
.map("author")
|
||||
.uniq()
|
||||
.reverse()
|
||||
.filter(matchString)
|
||||
.union(contacts)
|
||||
.filter(s => s.length < 28) // exclude comets
|
||||
.take(5)
|
||||
.value();
|
||||
|
||||
let newState = {
|
||||
patpSuggestions: suggestions,
|
||||
selectedSuggestion: suggestions[0]
|
||||
};
|
||||
|
||||
this.setState(newState);
|
||||
this.setState({ patpSearch: match[1].toLowerCase() });
|
||||
}
|
||||
|
||||
clearSuggestions() {
|
||||
clearSearch() {
|
||||
this.setState({
|
||||
patpSuggestions: []
|
||||
})
|
||||
patpSearch: null
|
||||
});
|
||||
}
|
||||
|
||||
completePatp(suggestion) {
|
||||
@ -271,23 +139,20 @@ export class ChatInput extends Component {
|
||||
const lastCol = this.editor.getLineHandle(lastRow).text.length;
|
||||
this.editor.setCursor(lastRow, lastCol);
|
||||
this.setState({
|
||||
patpSuggestions: []
|
||||
patpSearch: null
|
||||
});
|
||||
}
|
||||
|
||||
messageChange(editor, data, value) {
|
||||
|
||||
const { patpSuggestions } = this.state;
|
||||
if(patpSuggestions.length !== 0) {
|
||||
const { patpSearch } = this.state;
|
||||
if(patpSearch !== null) {
|
||||
this.patpAutocomplete(value, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
getLetterType(letter) {
|
||||
if (letter.startsWith('/me')) {
|
||||
letter = letter.slice(3);
|
||||
if (letter.startsWith('/me ')) {
|
||||
letter = letter.slice(4);
|
||||
// remove insignificant leading whitespace.
|
||||
// aces might be relevant to style.
|
||||
while (letter[0] === '\n') {
|
||||
@ -296,22 +161,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) {
|
||||
@ -330,6 +194,8 @@ export class ChatInput extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onEnter();
|
||||
|
||||
if(state.code) {
|
||||
props.api.chat.message(props.station, `~${window.ship}`, Date.now(), {
|
||||
code: {
|
||||
@ -341,10 +207,10 @@ export class ChatInput extends Component {
|
||||
return;
|
||||
}
|
||||
let message = [];
|
||||
editorMessage.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,
|
||||
@ -354,22 +220,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,
|
||||
@ -380,11 +244,10 @@ export class ChatInput extends Component {
|
||||
message = [];
|
||||
}
|
||||
|
||||
// perf:
|
||||
//setTimeout(this.closure, 2000);
|
||||
// perf:
|
||||
// setTimeout(this.closure, 2000);
|
||||
|
||||
this.editor.setValue('');
|
||||
|
||||
}
|
||||
|
||||
toggleCode() {
|
||||
@ -395,7 +258,7 @@ export class ChatInput extends Component {
|
||||
} else {
|
||||
this.setState({ code: true });
|
||||
this.editor.setOption('mode', null);
|
||||
this.editor.setOption('placeholder', "Code...");
|
||||
this.editor.setOption('placeholder', 'Code...');
|
||||
}
|
||||
const value = this.editor.getValue();
|
||||
|
||||
@ -404,19 +267,46 @@ export class ChatInput extends Component {
|
||||
this.editor.setValue(' ');
|
||||
this.editor.setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
const { props } = this;
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
{ url }
|
||||
);
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
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 img = (props.ownerContact && (props.ownerContact.avatar !== null))
|
||||
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
|
||||
: <Sigil
|
||||
ship={window.ship}
|
||||
size={24}
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
/>;
|
||||
|
||||
const candidates = _.chain(this.props.envelopes)
|
||||
.defaultTo([])
|
||||
.map('author')
|
||||
.uniq()
|
||||
.reverse()
|
||||
.value();
|
||||
|
||||
const codeTheme = state.code ? ' code' : '';
|
||||
|
||||
@ -427,83 +317,80 @@ export class ChatInput extends Component {
|
||||
lineWrapping: true,
|
||||
scrollbarStyle: 'native',
|
||||
cursorHeight: 0.85,
|
||||
placeholder: state.code ? "Code..." : props.placeholder,
|
||||
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) =>
|
||||
this.toggleCode()
|
||||
Tab: cm =>
|
||||
this.patpAutocomplete(cm.getValue(), true),
|
||||
'Enter': () => {
|
||||
this.messageSubmit();
|
||||
if (this.state.code) {
|
||||
this.toggleCode();
|
||||
}
|
||||
},
|
||||
'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 }}>
|
||||
{state.patpSuggestions.length !== 0 && (
|
||||
<ChatInputSuggestions
|
||||
onSelect={this.completePatp}
|
||||
suggestions={state.patpSuggestions}
|
||||
selected={state.selectedSuggestion}
|
||||
contacts={props.contacts}
|
||||
/>
|
||||
)}
|
||||
|
||||
style={{ flexGrow: 1 }}
|
||||
>
|
||||
<ShipSearch
|
||||
popover
|
||||
onSelect={this.completePatp}
|
||||
onClear={this.clearSearch}
|
||||
contacts={props.contacts}
|
||||
candidates={candidates}
|
||||
searchTerm={this.state.patpSearch}
|
||||
cm={this.editor}
|
||||
/>
|
||||
<div
|
||||
className="fl"
|
||||
style={{
|
||||
marginTop: 6,
|
||||
flexBasis: 24,
|
||||
height: 24
|
||||
}}>
|
||||
<Sigil
|
||||
ship={window.ship}
|
||||
size={24}
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
/>
|
||||
}}
|
||||
>
|
||||
{img}
|
||||
</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)' }}>
|
||||
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}
|
||||
>
|
||||
<CodeEditor
|
||||
options={options}
|
||||
editorDidMount={editor => { this.editor = editor; }}
|
||||
editorDidMount={(editor) => {
|
||||
this.editor = editor;
|
||||
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||
navigator.userAgent
|
||||
)) {
|
||||
editor.focus();
|
||||
}
|
||||
}}
|
||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: '24px', width: '24px', flexBasis: 24, marginTop: 6 }}>
|
||||
<div className="ml2 mr2"
|
||||
style={{ height: '16px', width: '16px', flexBasis: 16, marginTop: 10 }}>
|
||||
<S3Upload
|
||||
configuration={props.s3.configuration}
|
||||
credentials={props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ height: '16px', width: '16px', flexBasis: 16, marginTop: 10 }}>
|
||||
<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"
|
||||
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,32 +1,48 @@
|
||||
import React, { Component } from 'react';
|
||||
import { sigil, reactRenderer } from 'urbit-sigil-js';
|
||||
|
||||
|
||||
export class Sigil extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let classes = props.classes || "";
|
||||
const classes = props.classes || '';
|
||||
|
||||
const rgb = {
|
||||
r: parseInt(props.color.slice(1, 3), 16),
|
||||
g: parseInt(props.color.slice(3, 5), 16),
|
||||
b: parseInt(props.color.slice(5, 7), 16)
|
||||
};
|
||||
const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000;
|
||||
const whiteBrightness = 255;
|
||||
|
||||
let foreground = 'white';
|
||||
|
||||
if ((whiteBrightness - brightness) < 50) {
|
||||
foreground = 'black';
|
||||
}
|
||||
|
||||
if (props.ship.length > 14) {
|
||||
return (
|
||||
<div
|
||||
className={"bg-black dib " + classes}
|
||||
style={{width: props.size, height: props.size}}>
|
||||
</div>
|
||||
className={'bg-black dib ' + classes}
|
||||
style={{ width: props.size, height: props.size }}
|
||||
></div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={"dib " + classes} style={{ flexBasis: props.size, backgroundColor: props.color }}>
|
||||
<div
|
||||
className={'dib ' + classes}
|
||||
style={{ flexBasis: props.size, backgroundColor: props.color }}
|
||||
>
|
||||
{sigil({
|
||||
patp: props.ship,
|
||||
renderer: reactRenderer,
|
||||
size: props.size,
|
||||
colors: [props.color, "white"]
|
||||
colors: [props.color, foreground],
|
||||
class: props.svgClass
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -289,7 +289,7 @@ export class InviteSearch extends Component {
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
let searchDisabled = false;
|
||||
let searchDisabled = props.disabled;
|
||||
if (props.invites.groups) {
|
||||
if (props.invites.groups.length > 0) {
|
||||
searchDisabled = true;
|
||||
|
@ -1,11 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { uxToHex, cite } from '/lib/util';
|
||||
|
||||
|
||||
export class MemberElement extends Component {
|
||||
|
||||
onRemove() {
|
||||
const { props } = this;
|
||||
props.api.groups.remove([`~${props.ship}`], props.path);
|
||||
@ -24,7 +21,8 @@ export class MemberElement extends Component {
|
||||
} else if (window.ship !== props.ship && window.ship === props.owner) {
|
||||
actionElem = (
|
||||
<a onClick={this.onRemove.bind(this)}
|
||||
className="w-20 dib list-ship black white-d f8 pointer">
|
||||
className="w-20 dib list-ship black white-d f8 pointer"
|
||||
>
|
||||
Ban
|
||||
</a>
|
||||
);
|
||||
@ -34,20 +32,24 @@ export class MemberElement extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
let name = !!props.contact
|
||||
const name = props.contact
|
||||
? `${props.contact.nickname} (${cite(props.ship)})` : `${cite(props.ship)}`;
|
||||
let color = !!props.contact ? uxToHex(props.contact.color) : '000000';
|
||||
const color = props.contact ? uxToHex(props.contact.color) : '000000';
|
||||
|
||||
const img = (props.contact && (props.contact.avatar !== null))
|
||||
? <img src={props.contact.avatar} height={32} width={32} className="dib" />
|
||||
: <Sigil ship={props.ship} size={32} color={`#${color}`} />;
|
||||
|
||||
return (
|
||||
<div className="flex mb2">
|
||||
<Sigil ship={props.ship} size={32} color={`#${color}`} />
|
||||
{img}
|
||||
<p className={
|
||||
"w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8"
|
||||
}>{name}</p>
|
||||
'w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8'
|
||||
}
|
||||
>{name}</p>
|
||||
{actionElem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { ProfileOverlay } from '/components/lib/profile-overlay';
|
||||
import { OverlaySigil } from '/components/lib/overlay-sigil';
|
||||
import classnames from 'classnames';
|
||||
import { Route, Link } from 'react-router-dom'
|
||||
import { uxToHex, cite, writeText } from '/lib/util';
|
||||
@ -53,6 +55,7 @@ export class Message extends Component {
|
||||
iframe.setAttribute('src', iframe.getAttribute('data-src'));
|
||||
}
|
||||
|
||||
|
||||
renderContent() {
|
||||
const { props } = this;
|
||||
let letter = props.msg.letter;
|
||||
@ -62,21 +65,21 @@ export class Message extends Component {
|
||||
(!!letter.code.output &&
|
||||
letter.code.output.length && letter.code.output.length > 0) ?
|
||||
(
|
||||
<pre className="f7 clamp-attachment pa1 mt0 mb0">
|
||||
<pre className="f7 clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb">
|
||||
{letter.code.output[0].join('\n')}
|
||||
</pre>
|
||||
) : null;
|
||||
return (
|
||||
<span>
|
||||
<pre className="f7 clamp-attachment pa1 mt0 mb0 bg-light-gray">
|
||||
<div className="mv2">
|
||||
<pre className="f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba">
|
||||
{letter.code.expression}
|
||||
</pre>
|
||||
{outputElement}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
} else if ('url' in letter) {
|
||||
let imgMatch =
|
||||
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/
|
||||
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM|svg|SVG)$/
|
||||
.exec(letter.url);
|
||||
let youTubeRegex = new RegExp(''
|
||||
+ /(?:https?:\/\/(?:[a-z]+.)?)/.source // protocol
|
||||
@ -190,20 +193,20 @@ export class Message extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={
|
||||
"w-100 f7 pl3 pt4 pr3 cf flex lh-copy " + " " + pending
|
||||
}
|
||||
style={{
|
||||
minHeight: "min-content"
|
||||
}}>
|
||||
<div className="fl mr3 v-top bg-white bg-gray0-d">
|
||||
<Sigil
|
||||
ship={props.msg.author}
|
||||
size={24}
|
||||
color={color}
|
||||
classes={sigilClass}
|
||||
/>
|
||||
</div>
|
||||
<OverlaySigil
|
||||
ship={props.msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
group={props.group}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d" />
|
||||
<div
|
||||
className="fr clamp-message white-d"
|
||||
style={{ flexGrow: 1, marginTop: -8 }}>
|
||||
|
102
pkg/interface/chat/src/js/components/lib/overlay-sigil.js
Normal file
102
pkg/interface/chat/src/js/components/lib/overlay-sigil.js
Normal file
@ -0,0 +1,102 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import {
|
||||
ProfileOverlay,
|
||||
OVERLAY_HEIGHT
|
||||
} from '/components/lib/profile-overlay';
|
||||
|
||||
export class OverlaySigil extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
clicked: false,
|
||||
captured: false,
|
||||
topSpace: 0,
|
||||
bottomSpace: 0
|
||||
};
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
|
||||
this.profileShow = this.profileShow.bind(this);
|
||||
this.profileHide = this.profileHide.bind(this);
|
||||
this.updateContainerInterval = setInterval(
|
||||
this.updateContainerOffset.bind(this),
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateContainerOffset();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.updateContainerInterval) {
|
||||
clearInterval(this.updateContainerInterval);
|
||||
this.updateContainerInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
profileShow() {
|
||||
this.setState({ profileClicked: true });
|
||||
}
|
||||
|
||||
profileHide() {
|
||||
this.setState({ profileClicked: false });
|
||||
}
|
||||
|
||||
updateContainerOffset() {
|
||||
if (this.containerRef && this.containerRef.current) {
|
||||
const parent = this.containerRef.current.offsetParent;
|
||||
const { offsetTop } = this.containerRef.current;
|
||||
|
||||
let bottomSpace, topSpace;
|
||||
|
||||
if(navigator.userAgent.includes('Firefox')) {
|
||||
topSpace = offsetTop - parent.scrollTop - OVERLAY_HEIGHT / 2;
|
||||
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
|
||||
} else {
|
||||
topSpace = offsetTop + parent.scrollHeight - parent.clientHeight - parent.scrollTop;
|
||||
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
|
||||
}
|
||||
this.setState({
|
||||
topSpace,
|
||||
bottomSpace
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const img = (props.contact && (props.contact.avatar !== null))
|
||||
? <img src={props.contact.avatar} height={24} width={24} className="dib" />
|
||||
: <Sigil
|
||||
ship={props.ship}
|
||||
size={24}
|
||||
color={props.color}
|
||||
classes={props.sigilClass}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={this.profileShow}
|
||||
className={props.className + ' pointer relative'}
|
||||
ref={this.containerRef}
|
||||
style={{ height: '24px' }}
|
||||
>
|
||||
{state.profileClicked && (
|
||||
<ProfileOverlay
|
||||
ship={props.ship}
|
||||
contact={props.contact}
|
||||
color={props.color}
|
||||
topSpace={state.topSpace}
|
||||
bottomSpace={state.bottomSpace}
|
||||
group={props.group}
|
||||
onDismiss={this.profileHide}
|
||||
/>
|
||||
)}
|
||||
{img}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
101
pkg/interface/chat/src/js/components/lib/profile-overlay.js
Normal file
101
pkg/interface/chat/src/js/components/lib/profile-overlay.js
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cite } from '/lib/util';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
|
||||
export const OVERLAY_HEIGHT = 250;
|
||||
|
||||
export class ProfileOverlay extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.popoverRef = React.createRef();
|
||||
this.onDocumentClick = this.onDocumentClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.onDocumentClick);
|
||||
document.addEventListener('touchstart', this.onDocumentClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mousedown', this.onDocumentClick);
|
||||
document.removeEventListener('touchstart', this.onDocumentClick);
|
||||
}
|
||||
|
||||
onDocumentClick(event) {
|
||||
const { popoverRef } = this;
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!popoverRef.current || popoverRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onDismiss();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { contact, ship, color, topSpace, bottomSpace, group } = this.props;
|
||||
|
||||
let top, bottom;
|
||||
if (topSpace < OVERLAY_HEIGHT / 2) {
|
||||
top = '0px';
|
||||
}
|
||||
if (bottomSpace < OVERLAY_HEIGHT / 2) {
|
||||
bottom = '0px';
|
||||
}
|
||||
if (!(top || bottom)) {
|
||||
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
|
||||
}
|
||||
const containerStyle = { top, bottom, left: '100%' };
|
||||
|
||||
const isOwn = window.ship === ship;
|
||||
|
||||
const identityHref = group['group-path'].startsWith('/~/')
|
||||
? '/~groups/me'
|
||||
: `/~groups/view${group['group-path']}/${window.ship}`;
|
||||
|
||||
const img = (contact && (contact.avatar !== null))
|
||||
? <img src={contact.avatar} height={160} width={160} className="brt2 dib" />
|
||||
: <Sigil
|
||||
ship={ship}
|
||||
size={160}
|
||||
color={color}
|
||||
classes="brt2"
|
||||
svgClass="brt2"
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.popoverRef}
|
||||
style={containerStyle}
|
||||
className="flex-col shadow-6 br2 bg-white bg-gray0-d inter absolute z-1 f9 lh-solid"
|
||||
>
|
||||
<div style={{ height: '160px', width: '160px' }}>
|
||||
{img}
|
||||
</div>
|
||||
<div className="pv3 pl3 pr2">
|
||||
{contact && contact.nickname && (
|
||||
<div className="b white-d">{contact.nickname}</div>
|
||||
)}
|
||||
<div className="mono gray2">{cite(`~${ship}`)}</div>
|
||||
{!isOwn && (
|
||||
<Link
|
||||
to={`/~chat/new/dm/~${ship}`}
|
||||
className="b--green0 b--green2-d b--solid ba green2 mt3 tc pa2 pointer db"
|
||||
>
|
||||
Send Message
|
||||
</Link>
|
||||
)}
|
||||
{isOwn && (
|
||||
<a
|
||||
href={identityHref}
|
||||
className="b--black b--white-d ba black white-d mt3 tc pa2 pointer db"
|
||||
>
|
||||
Edit Group Identity
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
96
pkg/interface/chat/src/js/components/lib/s3-upload.js
Normal file
96
pkg/interface/chat/src/js/components/lib/s3-upload.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { Component } from 'react'
|
||||
import S3Client from '/lib/s3';
|
||||
|
||||
export class S3Upload extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.s3 = new S3Client();
|
||||
this.setCredentials(props.credentials, props.configuration);
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
isReady(creds, config) {
|
||||
return (
|
||||
!!creds &&
|
||||
'endpoint' in creds &&
|
||||
'accessKeyId' in creds &&
|
||||
'secretAccessKey' in creds &&
|
||||
creds.endpoint !== '' &&
|
||||
creds.accessKeyId !== '' &&
|
||||
creds.secretAccessKey !== '' &&
|
||||
!!config &&
|
||||
'currentBucket' in config &&
|
||||
config.currentBucket !== ''
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { props } = this;
|
||||
this.setCredentials(props.credentials, props.configuration);
|
||||
}
|
||||
|
||||
setCredentials(credentials, configuration) {
|
||||
if (!this.isReady(credentials, configuration)) { return; }
|
||||
this.s3.setCredentials(
|
||||
credentials.endpoint,
|
||||
credentials.accessKeyId,
|
||||
credentials.secretAccessKey
|
||||
);
|
||||
}
|
||||
|
||||
getFileUrl(endpoint, filename) {
|
||||
return endpoint + '/' + filename;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const { props } = this;
|
||||
if (!this.inputRef.current) { return; }
|
||||
let files = this.inputRef.current.files;
|
||||
if (files.length <= 0) { return; }
|
||||
|
||||
let file = files.item(0);
|
||||
let bucket = props.configuration.currentBucket;
|
||||
|
||||
this.s3.upload(bucket, file.name, file).then((data) => {
|
||||
if (!data || !('Location' in data)) {
|
||||
return;
|
||||
}
|
||||
this.props.uploadSuccess(data.Location);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.props.uploadError(err);
|
||||
});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
if (!this.inputRef.current) { return; }
|
||||
this.inputRef.current.click();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
if (!this.isReady(props.credentials, props.configuration)) {
|
||||
return <div></div>;
|
||||
} else {
|
||||
let classes = !!props.className ?
|
||||
"pointer " + props.className : "pointer";
|
||||
return (
|
||||
<div className={classes}>
|
||||
<input className="dn"
|
||||
type="file"
|
||||
id="fileElement"
|
||||
ref={this.inputRef}
|
||||
accept="image/*"
|
||||
onChange={this.onChange.bind(this)} />
|
||||
<img className="invert-d"
|
||||
src="/~chat/img/ImageUpload.png"
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={this.onClick.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
371
pkg/interface/chat/src/js/components/lib/ship-search.js
Normal file
371
pkg/interface/chat/src/js/components/lib/ship-search.js
Normal file
@ -0,0 +1,371 @@
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import urbitOb from 'urbit-ob';
|
||||
import Mousetrap from 'mousetrap';
|
||||
|
||||
import cn from 'classnames';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { hexToRgba, uxToHex, deSig } from '/lib/util';
|
||||
|
||||
function ShipSearchItem({ ship, contacts, selected, onSelect }) {
|
||||
const contact = contacts[ship];
|
||||
let color = '#000000';
|
||||
let sigilClass = 'v-mid mix-blend-diff';
|
||||
let nickname;
|
||||
const nameStyle = {};
|
||||
const isSelected = ship === selected;
|
||||
if (contact) {
|
||||
const hex = uxToHex(contact.color);
|
||||
color = `#${hex}`;
|
||||
nameStyle.color = hexToRgba(hex, 0.7);
|
||||
nameStyle.textShadow = '0px 0px 0px #000';
|
||||
nameStyle.filter = 'contrast(1.3) saturate(1.5)';
|
||||
nameStyle.maxWidth = '200px';
|
||||
sigilClass = 'v-mid';
|
||||
nickname = contact.nickname;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => onSelect(ship)}
|
||||
className={cn(
|
||||
'f9 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
|
||||
}
|
||||
)}
|
||||
key={ship}
|
||||
>
|
||||
<Sigil ship={'~' + ship} size={24} color={color} classes={sigilClass} />
|
||||
{nickname && (
|
||||
<p style={nameStyle} className="dib ml4 b truncate">
|
||||
{nickname}
|
||||
</p>
|
||||
)}
|
||||
<div className="mono gray2 gray4-d ml4">{'~' + ship}</div>
|
||||
<p className="nowrap ml4">{status}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class ShipSearch extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
selected: null,
|
||||
suggestions: [],
|
||||
bound: false
|
||||
};
|
||||
|
||||
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, 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)
|
||||
) {
|
||||
this.updateSuggestions();
|
||||
} else if (prevProps.searchTerm !== props.searchTerm) {
|
||||
this.updateSuggestions(true);
|
||||
}
|
||||
}
|
||||
|
||||
updateSuggestions(isStale = false) {
|
||||
const needle = this.props.searchTerm;
|
||||
const matchString = (hay) => {
|
||||
hay = hay.toLowerCase();
|
||||
|
||||
return (
|
||||
hay.startsWith(needle) ||
|
||||
_.some(_.words(hay), s => s.startsWith(needle))
|
||||
);
|
||||
};
|
||||
|
||||
let candidates = this.state.suggestions;
|
||||
|
||||
if (isStale || this.state.suggestions.length === 0) {
|
||||
const contacts = _.chain(this.props.contacts)
|
||||
.defaultTo({})
|
||||
.map((details, ship) => ({ ...details, ship }))
|
||||
.filter(
|
||||
({ nickname, ship }) => matchString(nickname) || matchString(ship)
|
||||
)
|
||||
.map('ship')
|
||||
.value();
|
||||
|
||||
const exactMatch = urbitOb.isValidPatp(`~${needle}`) ? [needle] : [];
|
||||
|
||||
candidates = _.chain(this.props.candidates)
|
||||
.defaultTo([])
|
||||
.union(contacts)
|
||||
.union(exactMatch)
|
||||
.value();
|
||||
}
|
||||
|
||||
const suggestions = _.chain(candidates)
|
||||
.filter(matchString)
|
||||
.filter(s => s.length < 28) // exclude comets
|
||||
.value();
|
||||
|
||||
this.bindShortcuts();
|
||||
this.setState({ suggestions, selected: suggestions[0] });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
this.mousetrap.bind('enter', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.state.selected) {
|
||||
this.unbindShortcuts();
|
||||
this.props.onSelect(this.state.selected);
|
||||
}
|
||||
});
|
||||
|
||||
this.mousetrap.bind('tab', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.nextAutocompleteSuggestion(false);
|
||||
});
|
||||
this.mousetrap.bind(['up', 'shift+tab'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.nextAutocompleteSuggestion(true);
|
||||
});
|
||||
this.mousetrap.bind('down', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.nextAutocompleteSuggestion(false);
|
||||
});
|
||||
this.mousetrap.bind('esc', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
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');
|
||||
this.mousetrap.unbind(['up', 'shift+tab']);
|
||||
this.mousetrap.unbind('down');
|
||||
this.mousetrap.unbind('esc');
|
||||
}
|
||||
|
||||
nextAutocompleteSuggestion(backward = false) {
|
||||
const { suggestions } = this.state;
|
||||
let idx = suggestions.findIndex(s => s === this.state.selected);
|
||||
|
||||
idx = backward ? idx - 1 : idx + 1;
|
||||
idx = idx % Math.min(suggestions.length, 5);
|
||||
if (idx < 0) {
|
||||
idx = suggestions.length - 1;
|
||||
}
|
||||
|
||||
this.setState({ selected: suggestions[idx] });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onSelect, contacts, popover, className } = this.props;
|
||||
const { selected, suggestions } = this.state;
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const popoverClasses = (popover && ' absolute ') || ' ';
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
popover
|
||||
? {
|
||||
bottom: '90%',
|
||||
left: '48px'
|
||||
}
|
||||
: {}
|
||||
}
|
||||
className={
|
||||
'black white-d bg-white bg-gray0-d ' +
|
||||
'w7 pv3 z-1 mt1 ba b--gray1-d b--gray4' +
|
||||
popoverClasses +
|
||||
className || ''
|
||||
}
|
||||
>
|
||||
{suggestions.slice(0, 5).map(ship => (
|
||||
<ShipSearchItem
|
||||
onSelect={onSelect}
|
||||
key={ship}
|
||||
selected={selected}
|
||||
contacts={contacts}
|
||||
ship={ship}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShipSearchInput extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
searchTerm: ''
|
||||
};
|
||||
|
||||
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 || popoverRef.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onClear();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.onClick);
|
||||
document.addEventListener('touchstart', this.onClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mousedown', this.onClick);
|
||||
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 });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state, props } = this;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref => (this.popoverRef = ref)}
|
||||
style={{ top: '150%', left: '-80px' }}
|
||||
className="b--gray2 b--solid ba absolute bg-white bg-gray0-d"
|
||||
>
|
||||
<textarea
|
||||
style={{ resize: 'none', maxWidth: '200px' }}
|
||||
className="ma2 pa2 b--gray4 ba b--solid w7 db bg-gray0-d white-d"
|
||||
rows={1}
|
||||
autocapitalise="none"
|
||||
autoFocus={
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
? false
|
||||
: true
|
||||
}
|
||||
placeholder="Search for a ship"
|
||||
value={state.searchTerm}
|
||||
onChange={this.search}
|
||||
ref={this.setInputRef}
|
||||
/>
|
||||
<ShipSearch
|
||||
contacts={props.contacts}
|
||||
candidates={props.candidates}
|
||||
searchTerm={deSig(state.searchTerm)}
|
||||
inputRef={this.inputRef}
|
||||
onSelect={props.onSelect}
|
||||
onClear={props.onClear}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
106
pkg/interface/chat/src/js/components/new-dm.js
Normal file
106
pkg/interface/chat/src/js/components/new-dm.js
Normal file
@ -0,0 +1,106 @@
|
||||
import React, { Component } from "react";
|
||||
import classnames from "classnames";
|
||||
import { InviteSearch } from "./lib/invite-search";
|
||||
import { Spinner } from "./lib/icons/icon-spinner";
|
||||
import { Route, Link } from "react-router-dom";
|
||||
import { uuid, isPatTa, deSig } from "/lib/util";
|
||||
import urbitOb from "urbit-ob";
|
||||
|
||||
export class NewDmScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ship: null,
|
||||
station: null,
|
||||
awaiting: false
|
||||
};
|
||||
|
||||
this.onClickCreate = this.onClickCreate.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { props } = this;
|
||||
if (props.autoCreate && urbitOb.isValidPatp(props.autoCreate)) {
|
||||
this.setState(
|
||||
{
|
||||
ship: props.autoCreate.slice(1),
|
||||
awaiting: true
|
||||
},
|
||||
this.onClickCreate
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (prevProps !== props) {
|
||||
const { station } = this.state;
|
||||
if (station && station in props.inbox) {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push(`/~chat/room${station}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickCreate() {
|
||||
const { props, state } = this;
|
||||
|
||||
let station = `/~/~${window.ship}/dm--${state.ship}`;
|
||||
|
||||
let theirStation = `/~/~${state.ship}/dm--${window.ship}`;
|
||||
|
||||
if (station in props.inbox) {
|
||||
props.history.push(`/~chat/room${station}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (theirStation in props.inbox) {
|
||||
props.history.push(`/~chat/room${theirStation}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
station
|
||||
},
|
||||
() => {
|
||||
let groupPath = station;
|
||||
props.api.chatView.create(
|
||||
`~${window.ship} <-> ~${state.ship}`,
|
||||
"",
|
||||
station,
|
||||
groupPath,
|
||||
"village",
|
||||
state.ship !== window.ship ? [`~${state.ship}`] : [],
|
||||
true
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"h-100 w-100 mw6 pa3 pt4 overflow-x-hidden " +
|
||||
"bg-gray0-d white-d flex flex-column"
|
||||
}
|
||||
>
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||
<Link to="/~chat/">{"⟵ All Chats"}</Link>
|
||||
</div>
|
||||
<h2 className="mb3 f8">New DM</h2>
|
||||
<div className="w-100">
|
||||
<Spinner
|
||||
awaiting={this.state.awaiting}
|
||||
classes="mt4"
|
||||
text="Creating chat..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ export class NewScreen extends Component {
|
||||
idName: '',
|
||||
groups: [],
|
||||
ships: [],
|
||||
security: 'village',
|
||||
security: 'channel',
|
||||
idError: false,
|
||||
inviteError: false,
|
||||
allowHistory: true,
|
||||
@ -26,7 +26,6 @@ export class NewScreen extends Component {
|
||||
|
||||
this.titleChange = this.titleChange.bind(this);
|
||||
this.descriptionChange = this.descriptionChange.bind(this);
|
||||
this.securityChange = this.securityChange.bind(this);
|
||||
this.allowHistoryChange = this.allowHistoryChange.bind(this);
|
||||
this.setInvite = this.setInvite.bind(this);
|
||||
this.createGroupChange = this.createGroupChange.bind(this);
|
||||
@ -65,17 +64,6 @@ export class NewScreen extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
securityChange(event) {
|
||||
if (this.state.createGroup) {
|
||||
return;
|
||||
}
|
||||
if (event.target.checked) {
|
||||
this.setState({security: "village"});
|
||||
} else if (!event.target.checked) {
|
||||
this.setState({security: "channel"});
|
||||
}
|
||||
}
|
||||
|
||||
createGroupChange(event) {
|
||||
if (event.target.checked) {
|
||||
this.setState({
|
||||
@ -85,6 +73,7 @@ export class NewScreen extends Component {
|
||||
} else {
|
||||
this.setState({
|
||||
createGroup: !!event.target.checked,
|
||||
security: 'channel'
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -124,6 +113,10 @@ export class NewScreen extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
if(state.ships.length === 1 && state.security === 'village' && !state.createGroup) {
|
||||
props.history.push(`/~chat/new/dm/${aud[0]}`);
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
this.setState({
|
||||
inviteError: true,
|
||||
@ -279,18 +272,6 @@ export class NewScreen extends Component {
|
||||
setInvite={this.setInvite}
|
||||
/>
|
||||
{createGroupToggle}
|
||||
<div className="mv7">
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ WebkitAppearance: "none", width: 28 }}
|
||||
className={inviteSwitchClasses}
|
||||
onChange={this.securityChange}
|
||||
/>
|
||||
<span className="dib f9 white-d inter ml3">Invite Only Chat</span>
|
||||
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
|
||||
Chat participants must be invited to see chat content
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={this.onClickCreate.bind(this)}
|
||||
className={createClasses}>
|
||||
|
@ -13,6 +13,7 @@ import { MemberScreen } from '/components/member';
|
||||
import { SettingsScreen } from '/components/settings';
|
||||
import { NewScreen } from '/components/new';
|
||||
import { JoinScreen } from '/components/join';
|
||||
import { NewDmScreen } from '/components/new-dm';
|
||||
|
||||
|
||||
export class Root extends Component {
|
||||
@ -20,6 +21,7 @@ export class Root extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = store.state;
|
||||
this.totalUnreads = 0;
|
||||
store.setStateHandler(this.setState.bind(this));
|
||||
}
|
||||
|
||||
@ -33,6 +35,7 @@ export class Root extends Component {
|
||||
|
||||
let messagePreviews = {};
|
||||
let unreads = {};
|
||||
let totalUnreads = 0;
|
||||
Object.keys(state.inbox).forEach((stat) => {
|
||||
let envelopes = state.inbox[stat].envelopes;
|
||||
|
||||
@ -42,14 +45,22 @@ export class Root extends Component {
|
||||
messagePreviews[stat] = envelopes[0];
|
||||
}
|
||||
|
||||
unreads[stat] =
|
||||
state.inbox[stat].config.length > state.inbox[stat].config.read;
|
||||
const unread = Math.max(state.inbox[stat].config.length - state.inbox[stat].config.read, 0)
|
||||
unreads[stat] = !!unread;
|
||||
if(unread) {
|
||||
totalUnreads += unread;
|
||||
}
|
||||
});
|
||||
if(totalUnreads !== this.totalUnreads) {
|
||||
document.title = totalUnreads > 0 ? `Chat - (${totalUnreads})` : 'Chat';
|
||||
this.totalUnreads = totalUnreads;
|
||||
}
|
||||
|
||||
let invites = !!state.invites ? state.invites : {'/chat': {}, '/contacts': {}};
|
||||
|
||||
let contacts = !!state.contacts ? state.contacts : {};
|
||||
let associations = !!state.associations ? state.associations : {chat: {}, contacts: {}};
|
||||
let s3 = !!state.s3 ? state.s3 : {};
|
||||
|
||||
const renderChannelSidebar = (props, station) => (
|
||||
<Sidebar
|
||||
@ -92,6 +103,34 @@ export class Root extends Component {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/~chat/new/dm/:ship"
|
||||
render={props => {
|
||||
const ship = props.match.params.ship;
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
sidebarHideOnMobile={true}
|
||||
sidebar={renderChannelSidebar(props)}
|
||||
sidebarShown={state.sidebarShown}
|
||||
>
|
||||
<NewDmScreen
|
||||
api={api}
|
||||
inbox={state.inbox || {}}
|
||||
permissions={state.permissions || {}}
|
||||
contacts={state.contacts || {}}
|
||||
associations={associations.contacts}
|
||||
chatSynced={state.chatSynced || {}}
|
||||
autoCreate={ship}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/~chat/new"
|
||||
@ -206,6 +245,7 @@ export class Root extends Component {
|
||||
inbox={state.inbox}
|
||||
contacts={roomContacts}
|
||||
permission={permission}
|
||||
s3={s3}
|
||||
pendingMessages={state.pendingMessages}
|
||||
popout={popout}
|
||||
sidebarShown={state.sidebarShown}
|
||||
|
@ -5,17 +5,35 @@ import Welcome from '/components/lib/welcome.js';
|
||||
import { alphabetiseAssociations } from '../lib/util';
|
||||
import { SidebarInvite } from '/components/lib/sidebar-invite';
|
||||
import { GroupItem } from '/components/lib/group-item';
|
||||
import { ShipSearchInput } from '/components/lib/ship-search';
|
||||
|
||||
|
||||
export class Sidebar extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
dmOverlay: false
|
||||
};
|
||||
}
|
||||
onClickNew() {
|
||||
this.props.history.push('/~chat/new');
|
||||
}
|
||||
|
||||
onClickDm() {
|
||||
this.setState(({ dmOverlay }) => ({ dmOverlay: !dmOverlay }) )
|
||||
}
|
||||
|
||||
onClickJoin() {
|
||||
this.props.history.push('/~chat/join')
|
||||
}
|
||||
|
||||
goDm(ship) {
|
||||
this.setState({ dmOverlay: false }, () => {
|
||||
this.props.history.push(`/~chat/new/dm/~${ship}`)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
@ -97,6 +115,14 @@ export class Sidebar extends Component {
|
||||
/>
|
||||
)
|
||||
}
|
||||
const candidates = state.dmOverlay
|
||||
? _.chain(this.props.contacts)
|
||||
.values()
|
||||
.map(_.keys)
|
||||
.flatten()
|
||||
.uniq()
|
||||
.value()
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -108,6 +134,25 @@ export class Sidebar extends Component {
|
||||
onClick={this.onClickNew.bind(this)}>
|
||||
New Chat
|
||||
</a>
|
||||
|
||||
<div className="dib relative mr4">
|
||||
{ state.dmOverlay && (
|
||||
<ShipSearchInput
|
||||
className="absolute"
|
||||
contacts={{}}
|
||||
candidates={candidates}
|
||||
onSelect={this.goDm.bind(this)}
|
||||
onClear={this.onClickDm.bind(this)}
|
||||
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
className="f9 pointer green2 gray4-d"
|
||||
onClick={this.onClickDm.bind(this)}>
|
||||
DM
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
className="dib f9 pointer gray4-d"
|
||||
onClick={this.onClickJoin.bind(this)}>
|
||||
|
53
pkg/interface/chat/src/js/lib/s3.js
Normal file
53
pkg/interface/chat/src/js/lib/s3.js
Normal file
@ -0,0 +1,53 @@
|
||||
export default class S3Client {
|
||||
constructor() {
|
||||
this.s3 = null;
|
||||
|
||||
this.endpoint = "";
|
||||
this.accessKeyId = "";
|
||||
this.secretAccesskey = "";
|
||||
}
|
||||
|
||||
setCredentials(endpoint, accessKeyId, secretAccessKey) {
|
||||
if (!window.AWS) {
|
||||
setTimeout(() => {
|
||||
this.setCredentials(endpoint, accessKeyId, secretAccessKey);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
this.endpoint = new window.AWS.Endpoint(endpoint);
|
||||
this.accessKeyId = accessKeyId;
|
||||
this.secretAccessKey = secretAccessKey;
|
||||
|
||||
this.s3 =
|
||||
new window.AWS.S3({
|
||||
endpoint: this.endpoint,
|
||||
credentials: new window.AWS.Credentials({
|
||||
accessKeyId: this.accessKeyId,
|
||||
secretAccessKey: this.secretAccessKey
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
upload(bucket, filename, buffer) {
|
||||
let params = {
|
||||
Bucket: bucket,
|
||||
Key: filename,
|
||||
Body: buffer,
|
||||
ACL: 'public-read'
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.s3) {
|
||||
reject({ error: 'S3 not initialized!' });
|
||||
return;
|
||||
}
|
||||
this.s3.upload(params, (error, data) => {
|
||||
if (error) {
|
||||
reject({ error });
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
81
pkg/interface/chat/src/js/reducers/s3.js
Normal file
81
pkg/interface/chat/src/js/reducers/s3.js
Normal file
@ -0,0 +1,81 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class S3Reducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 's3-update', false);
|
||||
if (data) {
|
||||
this.credentials(data, state);
|
||||
this.configuration(data, state);
|
||||
this.currentBucket(data, state);
|
||||
this.addBucket(data, state);
|
||||
this.removeBucket(data, state);
|
||||
this.endpoint(data, state);
|
||||
this.accessKeyId(data, state);
|
||||
this.secretAccessKey(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
credentials(json, state) {
|
||||
let data = _.get(json, 'credentials', false);
|
||||
if (data) {
|
||||
state.s3.credentials = data;
|
||||
}
|
||||
}
|
||||
|
||||
configuration(json, state) {
|
||||
let data = _.get(json, 'configuration', false);
|
||||
if (data) {
|
||||
state.s3.configuration = {
|
||||
buckets: new Set(data.buckets),
|
||||
currentBucket: data.currentBucket
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
currentBucket(json, state) {
|
||||
let data = _.get(json, 'setCurrentBucket', false);
|
||||
if (data) {
|
||||
state.s3.configuration.currentBucket = data;
|
||||
}
|
||||
}
|
||||
|
||||
addBucket(json, state) {
|
||||
let data = _.get(json, 'addBucket', false);
|
||||
if (data) {
|
||||
state.s3.configuration.buckets =
|
||||
state.s3.configuration.buckets.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
removeBucket(json, state) {
|
||||
let data = _.get(json, 'removeBucket', false);
|
||||
if (data) {
|
||||
state.s3.configuration.buckets =
|
||||
state.s3.configuration.buckets.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
endpoint(json, state) {
|
||||
let data = _.get(json, 'setEndpoint', false);
|
||||
if (data) {
|
||||
state.s3.credentials.endpoint = data;
|
||||
}
|
||||
}
|
||||
|
||||
accessKeyId(json, state) {
|
||||
let data = _.get(json, 'setAccessKeyId', false);
|
||||
if (data) {
|
||||
state.s3.credentials.accessKeyId = data;
|
||||
}
|
||||
}
|
||||
|
||||
secretAccessKey(json, state) {
|
||||
let data = _.get(json, 'setSecretAccessKey', false);
|
||||
if (data) {
|
||||
state.s3.credentials.secretAccessKey = data;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { ChatUpdateReducer } from '/reducers/chat-update';
|
||||
import { InviteUpdateReducer } from '/reducers/invite-update';
|
||||
import { PermissionUpdateReducer } from '/reducers/permission-update';
|
||||
import { MetadataReducer } from '/reducers/metadata-update.js';
|
||||
import { S3Reducer } from '/reducers/s3.js';
|
||||
import { LocalReducer } from '/reducers/local.js';
|
||||
|
||||
|
||||
@ -17,6 +18,7 @@ class Store {
|
||||
this.chatUpdateReducer = new ChatUpdateReducer();
|
||||
this.inviteUpdateReducer = new InviteUpdateReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.s3Reducer = new S3Reducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.setState = () => {};
|
||||
}
|
||||
@ -32,6 +34,7 @@ class Store {
|
||||
chat: {},
|
||||
contacts: {}
|
||||
},
|
||||
s3: {},
|
||||
selectedGroups: [],
|
||||
sidebarShown: true,
|
||||
pendingMessages: new Map([]),
|
||||
@ -58,6 +61,7 @@ class Store {
|
||||
this.chatUpdateReducer.reduce(json, this.state);
|
||||
this.inviteUpdateReducer.reduce(json, this.state);
|
||||
this.metadataReducer.reduce(json, this.state);
|
||||
this.s3Reducer.reduce(json, this.state);
|
||||
this.localReducer.reduce(json, this.state);
|
||||
|
||||
this.setState(this.state);
|
||||
|
@ -54,6 +54,7 @@ export class Subscription {
|
||||
this.subscribe('/primary', 'contact-view');
|
||||
this.subscribe('/app-name/chat', 'metadata-store');
|
||||
this.subscribe('/app-name/contacts', 'metadata-store');
|
||||
this.subscribe('/all', 's3-store');
|
||||
}
|
||||
|
||||
handleEvent(diff) {
|
||||
|
@ -11,13 +11,13 @@ class UrbitApi {
|
||||
this.bindPaths = [];
|
||||
|
||||
this.contactHook = {
|
||||
edit: this.contactEdit.bind(this),
|
||||
remove: this.contactRemove.bind(this)
|
||||
edit: this.contactEdit.bind(this)
|
||||
};
|
||||
|
||||
this.contactView = {
|
||||
create: this.contactCreate.bind(this),
|
||||
delete: this.contactDelete.bind(this),
|
||||
remove: this.contactRemove.bind(this),
|
||||
share: this.contactShare.bind(this)
|
||||
};
|
||||
|
||||
@ -104,16 +104,12 @@ class UrbitApi {
|
||||
return this.contactViewAction({ delete: { path }});
|
||||
}
|
||||
|
||||
contactHookAction(data) {
|
||||
return this.action("contact-hook", "contact-action", data);
|
||||
contactRemove(path, ship) {
|
||||
return this.contactViewAction({ remove: { path, ship } });
|
||||
}
|
||||
|
||||
contactRemove(path, ship) {
|
||||
return this.contactHookAction({
|
||||
remove: {
|
||||
path, ship
|
||||
}
|
||||
});
|
||||
contactHookAction(data) {
|
||||
return this.action("contact-hook", "contact-action", data);
|
||||
}
|
||||
|
||||
contactEdit(path, ship, editField) {
|
||||
@ -125,7 +121,7 @@ class UrbitApi {
|
||||
{notes: ''}
|
||||
{color: 'fff'} // with no 0x prefix
|
||||
{avatar: null}
|
||||
{avatar: {p: length, q: bytestream}}
|
||||
{avatar: {url: ''}}
|
||||
*/
|
||||
return this.contactHookAction({
|
||||
edit: {
|
||||
|
@ -2,10 +2,11 @@ import React, { Component } from 'react';
|
||||
import { Sigil } from './icons/sigil';
|
||||
|
||||
import { api } from '/api';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { EditElement } from '/components/lib/edit-element';
|
||||
import { Spinner } from './icons/icon-spinner';
|
||||
import { uxToHex } from '/lib/util';
|
||||
import { S3Upload } from '/components/lib/s3-upload';
|
||||
|
||||
export class ContactCard extends Component {
|
||||
constructor(props) {
|
||||
@ -17,9 +18,10 @@ export class ContactCard extends Component {
|
||||
emailToSet: null,
|
||||
phoneToSet: null,
|
||||
websiteToSet: null,
|
||||
avatarToSet: null,
|
||||
notesToSet: null,
|
||||
awaiting: false,
|
||||
type: "Saving to group"
|
||||
type: 'Saving to group'
|
||||
};
|
||||
this.editToggle = this.editToggle.bind(this);
|
||||
this.sigilColorSet = this.sigilColorSet.bind(this);
|
||||
@ -27,10 +29,12 @@ export class ContactCard extends Component {
|
||||
this.emailToSet = this.emailToSet.bind(this);
|
||||
this.phoneToSet = this.phoneToSet.bind(this);
|
||||
this.websiteToSet = this.websiteToSet.bind(this);
|
||||
this.avatarToSet = this.avatarToSet.bind(this);
|
||||
this.notesToSet = this.notesToSet.bind(this);
|
||||
this.setField = this.setField.bind(this);
|
||||
this.shareWithGroup = this.shareWithGroup.bind(this);
|
||||
this.removeFromGroup = this.removeFromGroup.bind(this);
|
||||
this.removeSelfFromGroup = this.removeSelfFromGroup.bind(this);
|
||||
this.removeOtherFromGroup = this.removeOtherFromGroup.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -43,6 +47,7 @@ export class ContactCard extends Component {
|
||||
emailToSet: null,
|
||||
phoneToSet: null,
|
||||
websiteToSet: null,
|
||||
avatarToSet: null,
|
||||
notesToSet: null
|
||||
});
|
||||
return;
|
||||
@ -50,10 +55,9 @@ export class ContactCard extends Component {
|
||||
}
|
||||
|
||||
editToggle() {
|
||||
const { props } = this;
|
||||
let editSwitch = this.state.edit;
|
||||
editSwitch = !editSwitch;
|
||||
this.setState({edit: editSwitch});
|
||||
this.setState({ edit: editSwitch });
|
||||
}
|
||||
|
||||
emailToSet(value) {
|
||||
@ -76,178 +80,211 @@ export class ContactCard extends Component {
|
||||
this.setState({ websiteToSet: value });
|
||||
}
|
||||
|
||||
avatarToSet(value) {
|
||||
this.setState({ avatarToSet: value });
|
||||
}
|
||||
|
||||
sigilColorSet(event) {
|
||||
this.setState({ colorToSet: event.target.value });
|
||||
}
|
||||
|
||||
shipParser(ship) {
|
||||
switch (ship.length) {
|
||||
case 3: return "Galaxy";
|
||||
case 6: return "Star";
|
||||
case 13: return "Planet";
|
||||
case 56: return "Comet";
|
||||
default: return "Unknown";
|
||||
case 3: return 'Galaxy';
|
||||
case 6: return 'Star';
|
||||
case 13: return 'Planet';
|
||||
case 56: return 'Comet';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
setField(field) {
|
||||
const { props, state } = this;
|
||||
let ship = "~" + props.ship;
|
||||
let emailTest = new RegExp(''
|
||||
+ /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*/.source
|
||||
const ship = '~' + props.ship;
|
||||
const emailTest = new RegExp(String(/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*/.source)
|
||||
+ /@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.source
|
||||
);
|
||||
|
||||
let phoneTest = new RegExp(''
|
||||
+ /^\s*(?:\+?(\d{1,3}))?/.source
|
||||
const phoneTest = new RegExp(String(/^\s*(?:\+?(\d{1,3}))?/.source)
|
||||
+ /([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/.source
|
||||
);
|
||||
|
||||
let websiteTest = new RegExp(''
|
||||
+ /[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}/.source
|
||||
const websiteTest = new RegExp(String(/[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}/.source)
|
||||
+ /\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.source
|
||||
);
|
||||
|
||||
switch (field) {
|
||||
case "color": {
|
||||
let currentColor = (props.contact.color) ? props.contact.color : "000000";
|
||||
currentColor = uxToHex(currentColor);
|
||||
let hexExp = /([0-9A-Fa-f]{6})/
|
||||
let hexTest = hexExp.exec(this.state.colorToSet);
|
||||
|
||||
if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
|
||||
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
|
||||
api.contactEdit(props.path, `~${props.ship}`, { color: hexTest[1] }).then(() => {
|
||||
case 'avatar': {
|
||||
if (
|
||||
(state.avatarToSet === '') ||
|
||||
(
|
||||
Boolean(props.contact.avatar) &&
|
||||
state.avatarToSet === props.contact.avatar
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const avatarTestResult = websiteTest.exec(state.avatarToSet);
|
||||
if (avatarTestResult) {
|
||||
this.setState({
|
||||
awaiting: true,
|
||||
type: 'Saving to group'
|
||||
}, (() => {
|
||||
console.log(state.avatarToSet);
|
||||
api.contactEdit(props.path, ship, {
|
||||
avatar: {
|
||||
url: state.avatarToSet
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "email": {
|
||||
case 'color': {
|
||||
let currentColor = (props.contact.color) ? props.contact.color : '000000';
|
||||
currentColor = uxToHex(currentColor);
|
||||
const hexExp = /([0-9A-Fa-f]{6})/;
|
||||
const hexTest = hexExp.exec(this.state.colorToSet);
|
||||
|
||||
if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
|
||||
this.setState({ awaiting: true, type: 'Saving to group' }, (() => {
|
||||
api.contactEdit(props.path, `~${props.ship}`, { color: hexTest[1] }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'email': {
|
||||
if (
|
||||
(state.emailToSet === "") ||
|
||||
(state.emailToSet === '') ||
|
||||
(state.emailToSet === props.contact.email)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let emailTestResult = emailTest.exec(state.emailToSet);
|
||||
const emailTestResult = emailTest.exec(state.emailToSet);
|
||||
if (emailTestResult) {
|
||||
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
|
||||
this.setState({ awaiting: true, type: 'Saving to group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { email: state.emailToSet }).then(() => {
|
||||
this.setState({awaiting: false});
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "nickname": {
|
||||
case 'nickname': {
|
||||
if (
|
||||
(state.nickNameToSet === "") ||
|
||||
(state.nickNameToSet === '') ||
|
||||
(state.nickNameToSet === props.contact.nickname)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
|
||||
this.setState({ awaiting: true, type: 'Saving to group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { nickname: state.nickNameToSet }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
case "notes": {
|
||||
case 'notes': {
|
||||
if (
|
||||
(state.notesToSet === "") ||
|
||||
(state.notesToSet === '') ||
|
||||
(state.notesToSet === props.contact.notes)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
|
||||
this.setState({ awaiting: true, type: 'Saving to group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { notes: state.notesToSet }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "phone": {
|
||||
case 'phone': {
|
||||
if (
|
||||
(state.phoneToSet === "") ||
|
||||
(state.phoneToSet === '') ||
|
||||
(state.phoneToSet === props.contact.phone)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let phoneTestResult = phoneTest.exec(state.phoneToSet);
|
||||
const phoneTestResult = phoneTest.exec(state.phoneToSet);
|
||||
if (phoneTestResult) {
|
||||
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
|
||||
this.setState({ awaiting: true, type: 'Saving to group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { phone: state.phoneToSet }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "website": {
|
||||
case 'website': {
|
||||
if (
|
||||
(state.websiteToSet === "") ||
|
||||
(state.websiteToSet === '') ||
|
||||
(state.websiteToSet === props.contact.website)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let websiteTestResult = websiteTest.exec(state.websiteToSet);
|
||||
const websiteTestResult = websiteTest.exec(state.websiteToSet);
|
||||
if (websiteTestResult) {
|
||||
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
|
||||
this.setState({ awaiting: true, type: 'Saving to group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { website: state.websiteToSet }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "removeAvatar": {
|
||||
this.setState({ awaiting: true, type: "Removing from group" }, (() => {
|
||||
api.contactEdit(props.path, ship, { avatar: null }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}))
|
||||
break;
|
||||
}
|
||||
case "removeEmail": {
|
||||
this.setState({ emailToSet: "", awaiting: true, type: "Removing from group" }, (() => {
|
||||
api.contactEdit(props.path, ship, { email: "" }).then(() => {
|
||||
this.setState({awaiting: false});
|
||||
case 'removeEmail': {
|
||||
this.setState({ emailToSet: '', awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { email: '' }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "removeNickname": {
|
||||
this.setState({ nicknameToSet: "", awaiting: true, type: "Removing from group" }, (() => {
|
||||
api.contactEdit(props.path, ship, { nickname: "" }).then(() => {
|
||||
this.setState({awaiting: false});
|
||||
case 'removeNickname': {
|
||||
this.setState({ nicknameToSet: '', awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { nickname: '' }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "removePhone": {
|
||||
this.setState({ phoneToSet: "", awaiting: true, type: "Removing from group" }, (() => {
|
||||
api.contactEdit(props.path, ship, { phone: "" }).then(() => {
|
||||
this.setState({awaiting: false});
|
||||
case 'removePhone': {
|
||||
this.setState({ phoneToSet: '', awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { phone: '' }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "removeWebsite": {
|
||||
this.setState({ websiteToSet: "", awaiting: true, type: "Removing from group" }, (() => {
|
||||
api.contactEdit(props.path, ship, { website: "" }).then(() => {
|
||||
this.setState({awaiting: false});
|
||||
case 'removeWebsite': {
|
||||
this.setState({ websiteToSet: '', awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { website: '' }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case "removeNotes": {
|
||||
this.setState({ notesToSet: "", awaiting: true, type: "Removing from group" }, (() => {
|
||||
api.contactEdit(props.path, ship, { notes: "" }).then(() => {
|
||||
this.setState({awaiting: false});
|
||||
case 'removeAvatar': {
|
||||
this.setState({
|
||||
avatarToSet: null,
|
||||
awaiting: true,
|
||||
type: 'Removing from group'
|
||||
}, (() => {
|
||||
api.contactEdit(props.path, ship, { avatar: null }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case 'removeNotes': {
|
||||
this.setState({ notesToSet: '', awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactEdit(props.path, ship, { notes: '' }).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
break;
|
||||
@ -264,11 +301,12 @@ export class ContactCard extends Component {
|
||||
|
||||
shareWithGroup() {
|
||||
const { props, state } = this;
|
||||
let defaultVal = props.share ? {
|
||||
const defaultVal = props.share ? {
|
||||
nickname: props.rootIdentity.nickname,
|
||||
email: props.rootIdentity.email,
|
||||
phone: props.rootIdentity.phone,
|
||||
website: props.rootIdentity.website,
|
||||
avatar: !!props.rootIdentity.avatar ? { url: props.rootIdentity.avatar } : null,
|
||||
notes: props.rootIdentity.notes,
|
||||
color: uxToHex(props.rootIdentity.color)
|
||||
} : {
|
||||
@ -276,39 +314,44 @@ export class ContactCard extends Component {
|
||||
email: props.contact.email,
|
||||
phone: props.contact.phone,
|
||||
website: props.contact.website,
|
||||
avatar: !!props.contact.avatar ? { url: props.contact.avatar } : null,
|
||||
notes: props.contact.notes,
|
||||
color: props.contact.color
|
||||
};
|
||||
|
||||
let contact = {
|
||||
|
||||
const contact = {
|
||||
nickname: this.pickFunction(state.nickNameToSet, defaultVal.nickname),
|
||||
email: this.pickFunction(state.emailToSet, defaultVal.email),
|
||||
phone: this.pickFunction(state.phoneToSet, defaultVal.phone),
|
||||
website: this.pickFunction(state.websiteToSet, defaultVal.website),
|
||||
notes: this.pickFunction(state.notesToSet, defaultVal.notes),
|
||||
color: this.pickFunction(state.colorToSet, defaultVal.color),
|
||||
avatar: null
|
||||
avatar: this.pickFunction(
|
||||
!!state.avatarToSet ? { url: state.avatarToSet } : null,
|
||||
defaultVal.avatar
|
||||
)
|
||||
};
|
||||
this.setState({awaiting: true, type: "Sharing with group"}, (() => {
|
||||
|
||||
this.setState({ awaiting: true, type: 'Sharing with group' }, (() => {
|
||||
api.contactView.share(
|
||||
`~${props.ship}`, props.path, `~${window.ship}`, contact
|
||||
).then(() => {
|
||||
props.history.push(`/~groups/view${props.path}/${window.ship}`)
|
||||
props.history.push(`/~groups/view${props.path}/${window.ship}`);
|
||||
});
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
removeFromGroup() {
|
||||
removeSelfFromGroup() {
|
||||
const { props } = this;
|
||||
// share empty contact so that we can remove ourselves from group
|
||||
// if we haven't shared yet
|
||||
let contact = {
|
||||
nickname: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
website: "",
|
||||
notes: "",
|
||||
color: "000000",
|
||||
const contact = {
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
website: '',
|
||||
notes: '',
|
||||
color: '000000',
|
||||
avatar: null
|
||||
};
|
||||
|
||||
@ -316,24 +359,46 @@ export class ContactCard extends Component {
|
||||
`~${props.ship}`, props.path, `~${window.ship}`, contact
|
||||
);
|
||||
|
||||
this.setState({awaiting: true, type: "Removing from group"}, (() => {
|
||||
api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
|
||||
let destination = (props.ship === window.ship)
|
||||
? "" : props.path;
|
||||
this.setState({awaiting: false});
|
||||
props.history.push(`/~groups${destination}`);
|
||||
this.setState({ awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactView.delete(props.path).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push(`/~groups`);
|
||||
});
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
removeOtherFromGroup() {
|
||||
const { props } = this;
|
||||
|
||||
this.setState({ awaiting: true, type: 'Removing from group' }, (() => {
|
||||
api.contactView.remove(props.path, `~${props.ship}`).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push(`/~groups${props.path}`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
this.setState({
|
||||
avatarToSet: url
|
||||
}, () => {
|
||||
this.setField('avatar');
|
||||
});
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
renderEditCard() {
|
||||
const { props, state } = this;
|
||||
// if this is our first edit in a new group, propagate from root identity
|
||||
let defaultValue = props.share ? {
|
||||
const defaultValue = props.share ? {
|
||||
nickname: props.rootIdentity.nickname,
|
||||
email: props.rootIdentity.email,
|
||||
phone: props.rootIdentity.phone,
|
||||
website: props.rootIdentity.website,
|
||||
avatar: props.rootIdentity.avatar,
|
||||
notes: props.rootIdentity.notes,
|
||||
color: props.rootIdentity.color
|
||||
} : {
|
||||
@ -341,67 +406,83 @@ export class ContactCard extends Component {
|
||||
email: props.contact.email,
|
||||
phone: props.contact.phone,
|
||||
website: props.contact.website,
|
||||
avatar: props.contact.avatar,
|
||||
notes: props.contact.notes,
|
||||
color: props.contact.color
|
||||
};
|
||||
|
||||
let shipType = this.shipParser(props.ship);
|
||||
const shipType = this.shipParser(props.ship);
|
||||
|
||||
let defaultColor = !!defaultValue.color ? defaultValue.color : "000000";
|
||||
let defaultColor = defaultValue.color ? defaultValue.color : '000000';
|
||||
defaultColor = uxToHex(defaultColor);
|
||||
let currentColor = !!state.colorToSet ? state.colorToSet : defaultColor;
|
||||
let currentColor = state.colorToSet ? state.colorToSet : defaultColor;
|
||||
currentColor = uxToHex(currentColor);
|
||||
|
||||
let sigilColor = "";
|
||||
let hasAvatar =
|
||||
'avatar' in props.contact && props.contact.avatar !== "TODO";
|
||||
const avatar = ('avatar' in props.contact && props.contact.avatar !== null)
|
||||
? <img className="dib h-auto"
|
||||
width={128}
|
||||
src={props.contact.avatar}
|
||||
/>
|
||||
: <span className="dn"></span>;
|
||||
|
||||
if (!hasAvatar) {
|
||||
sigilColor = (
|
||||
<div className="tl mt4 mb4 w-auto ml-auto mr-auto"
|
||||
style={{ width: "fit-content" }}>
|
||||
<p className="f9 gray2 lh-copy">Sigil Color</p>
|
||||
<textarea
|
||||
className={"b--gray4 b--gray2-d black white-d bg-gray0-d f7 ba db pl2 " +
|
||||
"focus-b--black focus-b--white-d"}
|
||||
onChange={this.sigilColorSet}
|
||||
defaultValue={defaultColor}
|
||||
key={"default" + defaultColor}
|
||||
onBlur={(() => this.setField("color"))}
|
||||
style={{
|
||||
resize: "none",
|
||||
height: 40,
|
||||
paddingTop: 10,
|
||||
width: 114
|
||||
}}>
|
||||
</textarea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let removeImage = hasAvatar ? (
|
||||
<div>
|
||||
<button className="f9 black pointer db"
|
||||
onClick={() => this.setField("removeAvatar")}>
|
||||
Remove photo
|
||||
</button>
|
||||
</div>
|
||||
) : "";
|
||||
|
||||
let avatar = (hasAvatar)
|
||||
? <img className="dib h-auto" width={128} src={props.contact.avatar} />
|
||||
: <Sigil
|
||||
ship={props.ship}
|
||||
size={128}
|
||||
color={"#" + currentColor}
|
||||
key={"avatar" + currentColor} />;
|
||||
const imageSetter = (!props.share) ? (
|
||||
<span className="db">
|
||||
<p className="f9 gray2 db pb1">Avatar image url</p>
|
||||
<span className="cf db">
|
||||
<span className="w-20 fl pt1">
|
||||
<S3Upload
|
||||
className="fr pr3"
|
||||
configuration={props.s3.configuration}
|
||||
credentials={props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
/>
|
||||
</span>
|
||||
<EditElement
|
||||
className="fr w-80"
|
||||
defaultValue={defaultValue.avatar}
|
||||
onChange={this.avatarToSet}
|
||||
onDeleteClick={() => this.setField('removeAvatar')}
|
||||
onSaveClick={() => this.setField('avatar')}
|
||||
showButtons={!props.share}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
) : (<span className="dn"></span>);
|
||||
|
||||
return (
|
||||
<div className="w-100 mt8 flex justify-center pa4 pt8 pt0-l pa0-xl pt4-xl pb8">
|
||||
<div className="w-100 mw6 tc">
|
||||
{avatar}
|
||||
{sigilColor}
|
||||
{removeImage}
|
||||
{imageSetter}
|
||||
<Sigil
|
||||
ship={props.ship}
|
||||
size={128}
|
||||
color={'#' + currentColor}
|
||||
key={'avatar' + currentColor}
|
||||
/>
|
||||
<div className="tl mt4 mb4 w-auto ml-auto mr-auto"
|
||||
style={{ width: 'fit-content' }}
|
||||
>
|
||||
<p className="f9 gray2 lh-copy">Sigil Color</p>
|
||||
<textarea
|
||||
className={'b--gray4 b--gray2-d black white-d bg-gray0-d f7 ba db pl2 ' +
|
||||
'focus-b--black focus-b--white-d'}
|
||||
onChange={this.sigilColorSet}
|
||||
defaultValue={defaultColor}
|
||||
key={'default' + defaultColor}
|
||||
onKeyPress={ e => !e.key.match(/[0-9a-f]/i) ? e.preventDefault() : null}
|
||||
onBlur={(() => this.setField('color'))}
|
||||
maxLength={6}
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40,
|
||||
paddingTop: 10,
|
||||
width: 114
|
||||
}}
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div className="w-100 pt8 pb8 lh-copy tl">
|
||||
<p className="f9 gray2">Ship Name</p>
|
||||
<p className="f8 mono">~{props.ship}</p>
|
||||
@ -412,38 +493,43 @@ export class ContactCard extends Component {
|
||||
title="Nickname"
|
||||
defaultValue={defaultValue.nickname}
|
||||
onChange={this.nickNameToSet}
|
||||
onDeleteClick={() => this.setField("removeNickname")}
|
||||
onSaveClick={() => this.setField("nickname")}
|
||||
showButtons={!props.share} />
|
||||
onDeleteClick={() => this.setField('removeNickname')}
|
||||
onSaveClick={() => this.setField('nickname')}
|
||||
showButtons={!props.share}
|
||||
/>
|
||||
<EditElement
|
||||
title="Email"
|
||||
defaultValue={defaultValue.email}
|
||||
onChange={this.emailToSet}
|
||||
onDeleteClick={() => this.setField("removeEmail")}
|
||||
onSaveClick={() => this.setField("email")}
|
||||
showButtons={!props.share} />
|
||||
onDeleteClick={() => this.setField('removeEmail')}
|
||||
onSaveClick={() => this.setField('email')}
|
||||
showButtons={!props.share}
|
||||
/>
|
||||
<EditElement
|
||||
title="Phone"
|
||||
defaultValue={defaultValue.phone}
|
||||
onChange={this.phoneToSet}
|
||||
onDeleteClick={() => this.setField("removePhone")}
|
||||
onSaveClick={() => this.setField("phone")}
|
||||
showButtons={!props.share} />
|
||||
onDeleteClick={() => this.setField('removePhone')}
|
||||
onSaveClick={() => this.setField('phone')}
|
||||
showButtons={!props.share}
|
||||
/>
|
||||
<EditElement
|
||||
title="Website"
|
||||
defaultValue={defaultValue.website}
|
||||
onChange={this.websiteToSet}
|
||||
onDeleteClick={() => this.setField("removeWebsite")}
|
||||
onSaveClick={() => this.setField("website")}
|
||||
showButtons={!props.share} />
|
||||
onDeleteClick={() => this.setField('removeWebsite')}
|
||||
onSaveClick={() => this.setField('website')}
|
||||
showButtons={!props.share}
|
||||
/>
|
||||
<EditElement
|
||||
title="Notes"
|
||||
defaultValue={defaultValue.notes}
|
||||
onChange={this.notesToSet}
|
||||
onDeleteClick={() => this.setField("removeNotes")}
|
||||
onSaveClick={() => this.setField("notes")}
|
||||
onDeleteClick={() => this.setField('removeNotes')}
|
||||
onSaveClick={() => this.setField('notes')}
|
||||
resizable={true}
|
||||
showButtons={!props.share} />
|
||||
showButtons={!props.share}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -452,22 +538,23 @@ export class ContactCard extends Component {
|
||||
|
||||
renderCard() {
|
||||
const { props } = this;
|
||||
let shipType = this.shipParser(props.ship);
|
||||
let currentColor = props.contact.color ? props.contact.color : "0x0";
|
||||
let hexColor = uxToHex(currentColor);
|
||||
const shipType = this.shipParser(props.ship);
|
||||
const currentColor = props.contact.color ? props.contact.color : '0x0';
|
||||
const hexColor = uxToHex(currentColor);
|
||||
|
||||
let avatar =
|
||||
('avatar' in props.contact && props.contact.avatar !== "TODO") ?
|
||||
const avatar =
|
||||
('avatar' in props.contact && props.contact.avatar !== null) ?
|
||||
<img className="dib h-auto" width={128} src={props.contact.avatar} /> :
|
||||
<Sigil
|
||||
ship={props.ship}
|
||||
size={128}
|
||||
color={"#" + hexColor}
|
||||
key={hexColor} />;
|
||||
color={'#' + hexColor}
|
||||
key={hexColor}
|
||||
/>;
|
||||
|
||||
let websiteHref =
|
||||
(props.contact.website && props.contact.website.includes("://")) ?
|
||||
props.contact.website : "http://" + props.contact.website;
|
||||
const websiteHref =
|
||||
(props.contact.website && props.contact.website.includes('://')) ?
|
||||
props.contact.website : 'http://' + props.contact.website;
|
||||
|
||||
return (
|
||||
<div className="w-100 mt8 flex justify-center pa4 pt8 pt0-l pa0-xl pt4-xl">
|
||||
@ -480,39 +567,41 @@ export class ContactCard extends Component {
|
||||
<p className="f8">{shipType}</p>
|
||||
<hr className="mv8 gray4 b--gray4 bb-0 b--solid" />
|
||||
<div>
|
||||
{ !!props.contact.nickname ? (
|
||||
{ props.contact.nickname ? (
|
||||
<div>
|
||||
<p className="f9 gray2">Nickname</p>
|
||||
<p className="f8">{props.contact.nickname}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{ !!props.contact.email ? (
|
||||
{ props.contact.email ? (
|
||||
<div>
|
||||
<p className="f9 mt6 gray2">Email</p>
|
||||
<p className="f8">{props.contact.email}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{ !!props.contact.phone ? (
|
||||
{ props.contact.phone ? (
|
||||
<div>
|
||||
<p className="f9 mt6 gray2">Phone</p>
|
||||
<p className="f8">{props.contact.phone}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{ !!props.contact.website ? (
|
||||
{ props.contact.website ? (
|
||||
<div>
|
||||
<p className="f9 mt6 gray2">website</p>
|
||||
<a target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bb b--black f8"
|
||||
href={websiteHref}>
|
||||
href={websiteHref}
|
||||
>
|
||||
{props.contact.website}
|
||||
</a>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{ !!props.contact.notes ? (
|
||||
{ props.contact.notes ? (
|
||||
<div>
|
||||
<p className="f9 mt6 gray2">notes</p>
|
||||
<p className="f8">{props.contact.notes}</p>
|
||||
@ -530,32 +619,33 @@ export class ContactCard extends Component {
|
||||
const { props, state } = this;
|
||||
|
||||
let editInfoText =
|
||||
state.edit ? "Finish" : "Edit";
|
||||
state.edit ? 'Finish' : 'Edit';
|
||||
if (props.share && state.edit) {
|
||||
editInfoText = "Share";
|
||||
editInfoText = 'Share';
|
||||
}
|
||||
|
||||
let ourOpt = (props.ship === window.ship) ? "dib" : "dn";
|
||||
const ourOpt = (props.ship === window.ship) ? 'dib' : 'dn';
|
||||
|
||||
let adminOpt =
|
||||
const adminOpt =
|
||||
((props.path.includes(`~${window.ship}/`)) || ((props.ship === window.ship) &&
|
||||
!(props.path.includes('/~/default'))))
|
||||
? "dib" : "dn";
|
||||
? 'dib' : 'dn';
|
||||
|
||||
let meLink = (props.path === "/~/default")
|
||||
? `/~groups` : `/~groups/detail${props.path}`;
|
||||
const meLink = (props.path === '/~/default')
|
||||
? '/~groups' : `/~groups/detail${props.path}`;
|
||||
|
||||
let card = state.edit ? this.renderEditCard() : this.renderCard();
|
||||
const card = state.edit ? this.renderEditCard() : this.renderCard();
|
||||
return (
|
||||
<div className="w-100 h-100 overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
"flex justify-between w-100 bg-white bg-gray0-d " +
|
||||
"bb b--gray4 b--gray1-d "
|
||||
}>
|
||||
'flex justify-between w-100 bg-white bg-gray0-d ' +
|
||||
'bb b--gray4 b--gray1-d '
|
||||
}
|
||||
>
|
||||
<div className="f9 mv4 mh3 pt1 dib w-100">
|
||||
<Link to={meLink}>
|
||||
{"⟵"}
|
||||
{'⟵'}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex">
|
||||
@ -568,20 +658,22 @@ export class ContactCard extends Component {
|
||||
}
|
||||
}}
|
||||
className={
|
||||
`white-d bg-gray0-d mv4 mh3 f9 pa1 pointer flex-shrink-0 ` +
|
||||
'white-d bg-gray0-d mv4 mh3 f9 pa1 pointer flex-shrink-0 ' +
|
||||
ourOpt
|
||||
}>
|
||||
}
|
||||
>
|
||||
{editInfoText}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={
|
||||
`bg-gray0-d mv4 mh3 pa1 f9 red2 pointer flex-shrink-0 ` + adminOpt
|
||||
'bg-gray0-d mv4 mh3 pa1 f9 red2 pointer flex-shrink-0 ' + adminOpt
|
||||
}
|
||||
onClick={this.removeFromGroup}>
|
||||
onClick={props.ship === window.ship ? this.removeSelfFromGroup : this.removeOtherFromGroup}
|
||||
>
|
||||
{props.ship === window.ship
|
||||
? "Leave Group"
|
||||
: "Remove from Group"}
|
||||
? 'Leave Group'
|
||||
: 'Remove from Group'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-100 w-100 overflow-x-hidden pb8 white-d">{card}</div>
|
||||
|
@ -3,33 +3,39 @@ import { Route, Link } from 'react-router-dom';
|
||||
import { Sigil } from '../lib/icons/sigil';
|
||||
import { uxToHex, cite } from '../../lib/util';
|
||||
|
||||
|
||||
export class ContactItem extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let selectedClass = (props.selected) ? "bg-gray4 bg-gray1-d" : "";
|
||||
let hexColor = uxToHex(props.color);
|
||||
let name = (props.nickname) ? props.nickname : cite(props.ship);
|
||||
const selectedClass = (props.selected) ? 'bg-gray4 bg-gray1-d' : '';
|
||||
const hexColor = uxToHex(props.color);
|
||||
const name = (props.nickname) ? props.nickname : cite(props.ship);
|
||||
|
||||
const prefix = props.share ? 'share' : 'view';
|
||||
const suffix = !props.share ? `/${props.ship}` : '';
|
||||
|
||||
const img = (props.avatar !== null)
|
||||
? <img className="dib" src={props.avatar} height={32} width={32} />
|
||||
: <Sigil
|
||||
ship={props.ship}
|
||||
color={'#' + hexColor}
|
||||
size={32}
|
||||
key={`${props.ship}.sidebar.${hexColor}`}
|
||||
/>;
|
||||
|
||||
let prefix = props.share ? 'share' : 'view';
|
||||
let suffix = !props.share ? `/${props.ship}` : '';
|
||||
return (
|
||||
<Link to={`/~groups/${prefix}` + props.path + suffix}>
|
||||
<div className=
|
||||
{"pl4 pt1 pb1 f9 flex justify-start content-center " + selectedClass}
|
||||
{'pl4 pt1 pb1 f9 flex justify-start content-center ' + selectedClass}
|
||||
>
|
||||
<Sigil
|
||||
ship={props.ship}
|
||||
color={"#" + hexColor}
|
||||
size={32}
|
||||
key={`${props.ship}.sidebar.${hexColor}`} />
|
||||
{img}
|
||||
<p
|
||||
className={
|
||||
"f9 w-70 dib v-mid ml2 nowrap " +
|
||||
((props.nickname) ? "" : "mono")}
|
||||
'f9 w-70 dib v-mid ml2 nowrap ' +
|
||||
((props.nickname) ? '' : 'mono')}
|
||||
style={{ paddingTop: 6 }}
|
||||
title={props.ship}>
|
||||
title={props.ship}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ContactItem } from '/components/lib/contact-item';
|
||||
import { ShareSheet } from '/components/lib/share-sheet';
|
||||
import { Sigil } from '../lib/icons/sigil';
|
||||
@ -11,29 +11,30 @@ export class ContactSidebar extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
awaiting: false
|
||||
}
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let group = new Set(Array.from(props.group));
|
||||
let responsiveClasses =
|
||||
props.activeDrawer === "contacts" ? "db" : "dn db-ns";
|
||||
const group = new Set(Array.from(props.group));
|
||||
const responsiveClasses =
|
||||
props.activeDrawer === 'contacts' ? 'db' : 'dn db-ns';
|
||||
|
||||
let me = (window.ship in props.contacts)
|
||||
const me = (window.ship in props.contacts)
|
||||
? props.contacts[window.ship]
|
||||
: (window.ship in props.defaultContacts)
|
||||
? props.defaultContacts[window.ship]
|
||||
: { color: '0x0', nickname: null };
|
||||
: { color: '0x0', nickname: null, avatar: null };
|
||||
|
||||
let shareSheet =
|
||||
const shareSheet =
|
||||
!(window.ship in props.contacts) ?
|
||||
( <ShareSheet
|
||||
ship={window.ship}
|
||||
nickname={me.nickname}
|
||||
avatar={me.avatar}
|
||||
color={me.color}
|
||||
path={props.path}
|
||||
selected={props.path + "/" + window.ship === props.selectedContact}
|
||||
selected={props.path + '/' + window.ship === props.selectedContact}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -41,27 +42,29 @@ export class ContactSidebar extends Component {
|
||||
<ContactItem
|
||||
ship={window.ship}
|
||||
nickname={me.nickname}
|
||||
avatar={me.avatar}
|
||||
color={me.color}
|
||||
path={props.path}
|
||||
selected={props.path + "/" + window.ship === props.selectedContact}
|
||||
selected={props.path + '/' + window.ship === props.selectedContact}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
group.delete(window.ship);
|
||||
|
||||
let contactItems =
|
||||
const contactItems =
|
||||
Object.keys(props.contacts)
|
||||
.filter(c => c !== window.ship)
|
||||
.map((contact) => {
|
||||
group.delete(contact);
|
||||
let path = props.path + "/" + contact;
|
||||
let obj = props.contacts[contact];
|
||||
const path = props.path + '/' + contact;
|
||||
const obj = props.contacts[contact];
|
||||
return (
|
||||
<ContactItem
|
||||
key={contact}
|
||||
ship={contact}
|
||||
nickname={obj.nickname}
|
||||
color={obj.color}
|
||||
avatar={obj.avatar}
|
||||
path={props.path}
|
||||
selected={path === props.selectedContact}
|
||||
share={false}
|
||||
@ -69,62 +72,68 @@ export class ContactSidebar extends Component {
|
||||
);
|
||||
});
|
||||
|
||||
let adminOpt = (props.path.includes(`~${window.ship}/`))
|
||||
? "dib" : "dn";
|
||||
const adminOpt = (props.path.includes(`~${window.ship}/`))
|
||||
? 'dib' : 'dn';
|
||||
|
||||
let groupItems =
|
||||
const groupItems =
|
||||
Array.from(group).map((member) => {
|
||||
return (
|
||||
<div
|
||||
key={member}
|
||||
className={"pl4 pt1 pb1 f9 flex justify-start content-center " +
|
||||
"bg-white bg-gray0-d relative"}>
|
||||
className={'pl4 pt1 pb1 f9 flex justify-start content-center ' +
|
||||
'bg-white bg-gray0-d relative'}
|
||||
>
|
||||
<Sigil
|
||||
ship={member}
|
||||
color="#000000"
|
||||
size={32}
|
||||
classes="mix-blend-diff"
|
||||
/>
|
||||
/>
|
||||
<p className="f9 w-70 dib v-mid ml2 nowrap mono truncate"
|
||||
style={{ paddingTop: 6, color: '#aaaaaa' }}
|
||||
title={member}>
|
||||
title={member}
|
||||
>
|
||||
{cite(member)}
|
||||
</p>
|
||||
<p className={"v-mid f9 mh3 red2 pointer " + adminOpt}
|
||||
style={{paddingTop: 6}}
|
||||
<p className={'v-mid f9 mh3 red2 pointer ' + adminOpt}
|
||||
style={{ paddingTop: 6 }}
|
||||
onClick={() => {
|
||||
this.setState({awaiting: true}, (() => {
|
||||
this.setState({ awaiting: true }, (() => {
|
||||
props.api.groupRemove(props.path, [`~${member}`])
|
||||
.then(() => {
|
||||
this.setState({awaiting: false})
|
||||
})
|
||||
}))
|
||||
}}>
|
||||
this.setState({ awaiting: false });
|
||||
});
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
let detailHref = `/~groups/detail${props.path}`
|
||||
const detailHref = `/~groups/detail${props.path}`;
|
||||
|
||||
return (
|
||||
<div className={"bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 " +
|
||||
"flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative " +
|
||||
"overflow-hidden flex-shrink-0 " + responsiveClasses}>
|
||||
<div className={'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
|
||||
'flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative ' +
|
||||
'overflow-hidden flex-shrink-0 ' + responsiveClasses}
|
||||
>
|
||||
<div className="pt3 pb5 pl3 f8 db dn-m dn-l dn-xl">
|
||||
<Link to="/~groups/">{"⟵ All Groups"}</Link>
|
||||
<Link to="/~groups/">{'⟵ All Groups'}</Link>
|
||||
</div>
|
||||
<div className="overflow-auto h-100">
|
||||
<Link
|
||||
to={"/~groups/add" + props.path}
|
||||
to={'/~groups/add' + props.path}
|
||||
className={((props.path.includes(window.ship))
|
||||
? "dib"
|
||||
: "dn")}>
|
||||
? 'dib'
|
||||
: 'dn')}
|
||||
>
|
||||
<p className="f9 pl4 pt0 pt4-m pt4-l pt4-xl green2 bn">Add to Group</p>
|
||||
</Link>
|
||||
<Link to={detailHref}
|
||||
className="dib dn-m dn-l dn-xl f9 pl4 pt0 pt4-m pt4-l pt4-xl gray2 bn">Channels</Link>
|
||||
className="dib dn-m dn-l dn-xl f9 pl4 pt0 pt4-m pt4-l pt4-xl gray2 bn"
|
||||
>Channels</Link>
|
||||
{shareSheet}
|
||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Members</h2>
|
||||
{contactItems}
|
||||
|
@ -23,8 +23,10 @@ export class EditElement extends Component {
|
||||
? { resize: "vertical", height: 40, paddingTop: 10 }
|
||||
: { resize: "none", height: 40, paddingTop: 10 }
|
||||
|
||||
let classes = !!props.className ? "pb4 " + props.className : "pb4";
|
||||
|
||||
return (
|
||||
<div className="pb4">
|
||||
<div className={classes}>
|
||||
<p className="f9 gray2">{props.title}</p>
|
||||
<div className="w-100 flex">
|
||||
<textarea
|
||||
|
@ -1,28 +1,44 @@
|
||||
import React, { Component } from "react";
|
||||
import { sigil, reactRenderer } from "urbit-sigil-js";
|
||||
import React, { Component } from 'react';
|
||||
import { sigil, reactRenderer } from 'urbit-sigil-js';
|
||||
|
||||
export class Sigil extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let classes = props.classes || "";
|
||||
const classes = props.classes || '';
|
||||
|
||||
const rgb = {
|
||||
r: parseInt(props.color.slice(1, 3), 16),
|
||||
g: parseInt(props.color.slice(3, 5), 16),
|
||||
b: parseInt(props.color.slice(5, 7), 16)
|
||||
};
|
||||
const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000;
|
||||
const whiteBrightness = 255;
|
||||
|
||||
let foreground = 'white';
|
||||
|
||||
if ((whiteBrightness - brightness) < 50) {
|
||||
foreground = 'black';
|
||||
}
|
||||
|
||||
if (props.ship.length > 14) {
|
||||
return (
|
||||
<div
|
||||
className={"bg-black dib " + classes}
|
||||
style={{ width: props.size, height: props.size }}></div>
|
||||
className={'bg-black dib ' + classes}
|
||||
style={{ width: props.size, height: props.size }}
|
||||
></div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={"dib " + classes}
|
||||
style={{ flexBasis: 32, backgroundColor: props.color }}>
|
||||
className={'dib ' + classes}
|
||||
style={{ flexBasis: props.size, backgroundColor: props.color }}
|
||||
>
|
||||
{sigil({
|
||||
patp: props.ship,
|
||||
renderer: reactRenderer,
|
||||
size: props.size,
|
||||
colors: [props.color, "white"]
|
||||
colors: [props.color, foreground]
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
96
pkg/interface/groups/src/js/components/lib/s3-upload.js
Normal file
96
pkg/interface/groups/src/js/components/lib/s3-upload.js
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { Component } from 'react'
|
||||
import S3Client from '/lib/s3';
|
||||
|
||||
export class S3Upload extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.s3 = new S3Client();
|
||||
this.setCredentials(props.credentials, props.configuration);
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
isReady(creds, config) {
|
||||
return (
|
||||
!!creds &&
|
||||
'endpoint' in creds &&
|
||||
'accessKeyId' in creds &&
|
||||
'secretAccessKey' in creds &&
|
||||
creds.endpoint !== '' &&
|
||||
creds.accessKeyId !== '' &&
|
||||
creds.secretAccessKey !== '' &&
|
||||
!!config &&
|
||||
'currentBucket' in config &&
|
||||
config.currentBucket !== ''
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { props } = this;
|
||||
this.setCredentials(props.credentials, props.configuration);
|
||||
}
|
||||
|
||||
setCredentials(credentials, configuration) {
|
||||
if (!this.isReady(credentials, configuration)) { return; }
|
||||
this.s3.setCredentials(
|
||||
credentials.endpoint,
|
||||
credentials.accessKeyId,
|
||||
credentials.secretAccessKey
|
||||
);
|
||||
}
|
||||
|
||||
getFileUrl(endpoint, filename) {
|
||||
return endpoint + '/' + filename;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const { props } = this;
|
||||
if (!this.inputRef.current) { return; }
|
||||
let files = this.inputRef.current.files;
|
||||
if (files.length <= 0) { return; }
|
||||
|
||||
let file = files.item(0);
|
||||
let bucket = props.configuration.currentBucket;
|
||||
|
||||
this.s3.upload(bucket, file.name, file).then((data) => {
|
||||
if (!data || !('Location' in data)) {
|
||||
return;
|
||||
}
|
||||
this.props.uploadSuccess(data.Location);
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
this.props.uploadError(err);
|
||||
});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
if (!this.inputRef.current) { return; }
|
||||
this.inputRef.current.click();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
if (!this.isReady(props.credentials, props.configuration)) {
|
||||
return <div></div>;
|
||||
} else {
|
||||
let classes = !!props.className ?
|
||||
"pointer " + props.className : "pointer";
|
||||
return (
|
||||
<div className={classes}>
|
||||
<input className="dn"
|
||||
type="file"
|
||||
id="fileElement"
|
||||
ref={this.inputRef}
|
||||
accept="image/*"
|
||||
onChange={this.onChange.bind(this)} />
|
||||
<img className="invert-d"
|
||||
src="/~groups/img/ImageUpload.png"
|
||||
width="32"
|
||||
height="32"
|
||||
onClick={this.onClick.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ export class ShareSheet extends Component {
|
||||
<p className="pt4 pb2 pl4 pr4 f8 gray2 f9">Group Identity</p>
|
||||
<ContactItem
|
||||
key={props.ship}
|
||||
avatar={props.avatar}
|
||||
ship={props.ship}
|
||||
nickname={props.nickname}
|
||||
color={props.color}
|
||||
|
@ -33,7 +33,9 @@ export class Root extends Component {
|
||||
let defaultContacts =
|
||||
(!!state.contacts && '/~/default' in state.contacts) ?
|
||||
state.contacts['/~/default'] : {};
|
||||
|
||||
let groups = !!state.groups ? state.groups : {};
|
||||
let s3 = !!state.s3 ? state.s3 : {};
|
||||
|
||||
let invites =
|
||||
(!!state.invites && '/contacts' in state.invites) ?
|
||||
@ -211,6 +213,7 @@ export class Root extends Component {
|
||||
ship={window.ship}
|
||||
share={true}
|
||||
rootIdentity={rootIdentity}
|
||||
s3={s3}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
@ -259,6 +262,7 @@ export class Root extends Component {
|
||||
path={groupPath}
|
||||
ship={props.match.params.contact}
|
||||
rootIdentity={rootIdentity}
|
||||
s3={s3}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
@ -283,6 +287,7 @@ export class Root extends Component {
|
||||
path="/~/default"
|
||||
contact={me}
|
||||
ship={window.ship}
|
||||
s3={s3}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
|
53
pkg/interface/groups/src/js/lib/s3.js
Normal file
53
pkg/interface/groups/src/js/lib/s3.js
Normal file
@ -0,0 +1,53 @@
|
||||
export default class S3Client {
|
||||
constructor() {
|
||||
this.s3 = null;
|
||||
|
||||
this.endpoint = "";
|
||||
this.accessKeyId = "";
|
||||
this.secretAccesskey = "";
|
||||
}
|
||||
|
||||
setCredentials(endpoint, accessKeyId, secretAccessKey) {
|
||||
if (!window.AWS) {
|
||||
setTimeout(() => {
|
||||
this.setCredentials(endpoint, accessKeyId, secretAccessKey);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
this.endpoint = new window.AWS.Endpoint(endpoint);
|
||||
this.accessKeyId = accessKeyId;
|
||||
this.secretAccessKey = secretAccessKey;
|
||||
|
||||
this.s3 =
|
||||
new window.AWS.S3({
|
||||
endpoint: this.endpoint,
|
||||
credentials: new window.AWS.Credentials({
|
||||
accessKeyId: this.accessKeyId,
|
||||
secretAccessKey: this.secretAccessKey
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
upload(bucket, filename, buffer) {
|
||||
let params = {
|
||||
Bucket: bucket,
|
||||
Key: filename,
|
||||
Body: buffer,
|
||||
ACL: 'public-read'
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.s3) {
|
||||
reject({ error: 'S3 not initialized!' });
|
||||
return;
|
||||
}
|
||||
this.s3.upload(params, (error, data) => {
|
||||
if (error) {
|
||||
reject({ error });
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
81
pkg/interface/groups/src/js/reducers/s3.js
Normal file
81
pkg/interface/groups/src/js/reducers/s3.js
Normal file
@ -0,0 +1,81 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class S3Reducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 's3-update', false);
|
||||
if (data) {
|
||||
this.credentials(data, state);
|
||||
this.configuration(data, state);
|
||||
this.currentBucket(data, state);
|
||||
this.addBucket(data, state);
|
||||
this.removeBucket(data, state);
|
||||
this.endpoint(data, state);
|
||||
this.accessKeyId(data, state);
|
||||
this.secretAccessKey(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
credentials(json, state) {
|
||||
let data = _.get(json, 'credentials', false);
|
||||
if (data) {
|
||||
state.s3.credentials = data;
|
||||
}
|
||||
}
|
||||
|
||||
configuration(json, state) {
|
||||
let data = _.get(json, 'configuration', false);
|
||||
if (data) {
|
||||
state.s3.configuration = {
|
||||
buckets: new Set(data.buckets),
|
||||
currentBucket: data.currentBucket
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
currentBucket(json, state) {
|
||||
let data = _.get(json, 'setCurrentBucket', false);
|
||||
if (data) {
|
||||
state.s3.configuration.currentBucket = data;
|
||||
}
|
||||
}
|
||||
|
||||
addBucket(json, state) {
|
||||
let data = _.get(json, 'addBucket', false);
|
||||
if (data) {
|
||||
state.s3.configuration.buckets =
|
||||
state.s3.configuration.buckets.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
removeBucket(json, state) {
|
||||
let data = _.get(json, 'removeBucket', false);
|
||||
if (data) {
|
||||
state.s3.configuration.buckets =
|
||||
state.s3.configuration.buckets.delete(data);
|
||||
}
|
||||
}
|
||||
|
||||
endpoint(json, state) {
|
||||
let data = _.get(json, 'setEndpoint', false);
|
||||
if (data) {
|
||||
state.s3.credentials.endpoint = data;
|
||||
}
|
||||
}
|
||||
|
||||
accessKeyId(json, state) {
|
||||
let data = _.get(json, 'setAccessKeyId', false);
|
||||
if (data) {
|
||||
state.s3.credentials.accessKeyId = data;
|
||||
}
|
||||
}
|
||||
|
||||
secretAccessKey(json, state) {
|
||||
let data = _.get(json, 'setSecretAccessKey', false);
|
||||
if (data) {
|
||||
state.s3.credentials.secretAccessKey = data;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,19 +4,13 @@ import { GroupUpdateReducer } from '/reducers/group-update';
|
||||
import { InviteUpdateReducer } from '/reducers/invite-update';
|
||||
import { PermissionUpdateReducer } from '/reducers/permission-update';
|
||||
import { MetadataReducer } from '/reducers/metadata-update.js';
|
||||
import { S3Reducer } from '/reducers/s3.js';
|
||||
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();
|
||||
@ -24,10 +18,23 @@ class Store {
|
||||
this.contactUpdateReducer = new ContactUpdateReducer();
|
||||
this.inviteUpdateReducer = new InviteUpdateReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.s3Reducer = new S3Reducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.setState = () => {};
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
associations: {},
|
||||
permissions: {},
|
||||
invites: {},
|
||||
s3: {},
|
||||
selectedGroups: []
|
||||
};
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
this.setState = setState;
|
||||
}
|
||||
@ -35,6 +42,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);
|
||||
@ -42,6 +54,7 @@ class Store {
|
||||
this.contactUpdateReducer.reduce(json, this.state);
|
||||
this.inviteUpdateReducer.reduce(json, this.state);
|
||||
this.metadataReducer.reduce(json, this.state);
|
||||
this.s3Reducer.reduce(json, this.state);
|
||||
this.localReducer.reduce(json, this.state);
|
||||
|
||||
this.setState(this.state);
|
||||
|
@ -5,48 +5,72 @@ import urbitOb from 'urbit-ob';
|
||||
|
||||
|
||||
export class Subscription {
|
||||
|
||||
constructor() {
|
||||
this.firstRoundComplete = false;
|
||||
this.secondRoundComplete = false;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (api.authTokens) {
|
||||
this.initializeContacts();
|
||||
this.firstRound();
|
||||
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.firstRoundComplete = false;
|
||||
this.secondRoundComplete = 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);
|
||||
});
|
||||
}
|
||||
|
||||
firstRound() {
|
||||
this.subscribe('/primary', 'contact-view');
|
||||
}
|
||||
|
||||
secondRound() {
|
||||
this.subscribe('/all', 'group-store');
|
||||
this.subscribe('/all', 'metadata-store');
|
||||
}
|
||||
|
||||
thirdRound() {
|
||||
this.subscribe('/synced', 'contact-hook');
|
||||
this.subscribe('/primary', 'invite-view');
|
||||
this.subscribe('/all', 's3-store');
|
||||
}
|
||||
|
||||
handleEvent(diff) {
|
||||
if (!this.firstRoundComplete) {
|
||||
this.firstRoundComplete = true;
|
||||
this.secondRound();
|
||||
} else if (!this.secondRoundComplete) {
|
||||
this.secondRoundComplete = true;
|
||||
this.thirdRound();
|
||||
}
|
||||
store.handleEvent(diff);
|
||||
}
|
||||
|
||||
handleError(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
handleQuitSilently(quit) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
handleQuitAndResubscribe(quit) {
|
||||
// TODO: resubscribe
|
||||
}
|
||||
}
|
||||
|
||||
export let subscription = new Subscription();
|
||||
|
@ -17,7 +17,7 @@ export class Sigil extends Component {
|
||||
return (
|
||||
<div
|
||||
className={"dib " + classes}
|
||||
style={{ flexBasis: 16, backgroundColor: props.color }}>
|
||||
style={{ flexBasis: props.size, backgroundColor: props.color }}>
|
||||
{sigil({
|
||||
patp: props.ship,
|
||||
renderer: reactRenderer,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component } from 'react';
|
||||
import { Sigil } from './icons/sigil';
|
||||
import { cite } from '../../lib/util';
|
||||
import moment from 'moment';
|
||||
@ -13,7 +13,7 @@ export class CommentItem extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
|
||||
this.setState({timeSinceComment: this.getTimeSinceComment()});
|
||||
this.setState({ timeSinceComment: this.getTimeSinceComment() });
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
@ -25,30 +25,35 @@ export class CommentItem extends Component {
|
||||
}
|
||||
|
||||
getTimeSinceComment() {
|
||||
return !!this.props.time ?
|
||||
return this.props.time ?
|
||||
moment.unix(this.props.time / 1000).from(moment.utc())
|
||||
: '';
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
const props = this.props;
|
||||
|
||||
let member = this.props.member || false;
|
||||
const member = props.member || false;
|
||||
|
||||
let pending = !!this.props.pending ? "o-60" : "";
|
||||
const pending = props.pending ? 'o-60' : '';
|
||||
|
||||
const img = (props.avatar)
|
||||
? <img src={props.avatar} height={36} width={36} className="dib" />
|
||||
: <Sigil
|
||||
ship={'~' + props.ship}
|
||||
size={36}
|
||||
color={'#' + props.color}
|
||||
classes={(member ? 'mix-blend-diff' : '')}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div className={"w-100 pv3 " + pending}>
|
||||
<div className={'w-100 pv3 ' + pending}>
|
||||
<div className="flex bg-white bg-gray0-d">
|
||||
<Sigil
|
||||
ship={"~" + props.ship}
|
||||
size={36}
|
||||
color={"#" + props.color}
|
||||
classes={(member ? "mix-blend-diff" : "")}
|
||||
/>
|
||||
{img}
|
||||
<p className="gray2 f9 flex items-center ml2">
|
||||
<span className={"black white-d " + props.nameClass}
|
||||
title={props.ship}>
|
||||
<span className={'black white-d ' + props.nameClass}
|
||||
title={props.ship}
|
||||
>
|
||||
{props.nickname ? props.nickname : cite(props.ship)}
|
||||
</span>
|
||||
<span className="ml2">
|
||||
@ -58,8 +63,8 @@ export class CommentItem extends Component {
|
||||
</div>
|
||||
<p className="inter f8 pv3 white-d">{props.content}</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentItem
|
||||
export default CommentItem;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component } from 'react';
|
||||
import { CommentItem } from './comment-item';
|
||||
import { CommentsPagination } from './comments-pagination';
|
||||
|
||||
@ -12,12 +12,12 @@ export class Comments extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let page = this.props.commentPage;
|
||||
const page = this.props.commentPage;
|
||||
if (!this.props.comments ||
|
||||
!this.props.comments[page] ||
|
||||
this.props.comments.local[page]
|
||||
) {
|
||||
this.setState({requested: this.props.commentPage});
|
||||
this.setState({ requested: this.props.commentPage });
|
||||
api.getCommentsPage(
|
||||
this.props.resourcePath,
|
||||
this.props.url,
|
||||
@ -26,35 +26,34 @@ export class Comments extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
const props = this.props;
|
||||
|
||||
let page = props.commentPage;
|
||||
const page = props.commentPage;
|
||||
|
||||
let commentsObj = !!props.comments
|
||||
const commentsObj = props.comments
|
||||
? props.comments
|
||||
: {};
|
||||
|
||||
let commentsPage = !!commentsObj[page]
|
||||
const commentsPage = commentsObj[page]
|
||||
? commentsObj[page]
|
||||
: {};
|
||||
|
||||
let total = !!props.comments
|
||||
const total = props.comments
|
||||
? props.comments.totalPages
|
||||
: 1;
|
||||
|
||||
let commentsList = Object.keys(commentsPage)
|
||||
const commentsList = Object.keys(commentsPage)
|
||||
.map((entry) => {
|
||||
const commentObj = commentsPage[entry];
|
||||
const { ship, time, udon } = commentObj;
|
||||
|
||||
let commentObj = commentsPage[entry]
|
||||
let { ship, time, udon } = commentObj;
|
||||
|
||||
let contacts = !!props.contacts
|
||||
const contacts = props.contacts
|
||||
? props.contacts
|
||||
: {};
|
||||
|
||||
const {nickname, color, member} = getContactDetails(contacts[ship]);
|
||||
const { nickname, color, member, avatar } = getContactDetails(contacts[ship]);
|
||||
|
||||
let nameClass = nickname ? "inter" : "mono";
|
||||
const nameClass = nickname ? 'inter' : 'mono';
|
||||
|
||||
return(
|
||||
<CommentItem
|
||||
@ -65,10 +64,11 @@ export class Comments extends Component {
|
||||
nickname={nickname}
|
||||
nameClass={nameClass}
|
||||
color={color}
|
||||
avatar={avatar}
|
||||
member={member}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{commentsList}
|
||||
@ -80,10 +80,11 @@ export class Comments extends Component {
|
||||
linkIndex={props.linkIndex}
|
||||
url={props.url}
|
||||
commentPage={props.commentPage}
|
||||
total={total}/>
|
||||
total={total}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Comments;
|
||||
export default Comments;
|
||||
|
@ -1,28 +1,44 @@
|
||||
import React, { Component } from "react";
|
||||
import { sigil, reactRenderer } from "urbit-sigil-js";
|
||||
import React, { Component } from 'react';
|
||||
import { sigil, reactRenderer } from 'urbit-sigil-js';
|
||||
|
||||
export class Sigil extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let classes = props.classes || "";
|
||||
const classes = props.classes || '';
|
||||
|
||||
const rgb = {
|
||||
r: parseInt(props.color.slice(1, 3), 16),
|
||||
g: parseInt(props.color.slice(3, 5), 16),
|
||||
b: parseInt(props.color.slice(5, 7), 16)
|
||||
};
|
||||
const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000;
|
||||
const whiteBrightness = 255;
|
||||
|
||||
let foreground = 'white';
|
||||
|
||||
if ((whiteBrightness - brightness) < 50) {
|
||||
foreground = 'black';
|
||||
}
|
||||
|
||||
if (props.ship.length > 14) {
|
||||
return (
|
||||
<div
|
||||
className={"bg-black dib " + classes}
|
||||
style={{ width: props.size, height: props.size }}></div>
|
||||
className={'bg-black dib ' + classes}
|
||||
style={{ width: props.size, height: props.size }}
|
||||
></div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={"dib " + classes}
|
||||
style={{ flexBasis: props.size, backgroundColor: props.color }}>
|
||||
className={'dib ' + classes}
|
||||
style={{ flexBasis: props.size, backgroundColor: props.color }}
|
||||
>
|
||||
{sigil({
|
||||
patp: props.ship,
|
||||
renderer: reactRenderer,
|
||||
size: props.size,
|
||||
colors: [props.color, "white"]
|
||||
colors: [props.color, foreground]
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
@ -16,7 +16,7 @@ export class LinkItem extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTimeSinceNewestMessageInterval = setInterval( () => {
|
||||
this.setState({timeSinceLinkPost: this.getTimeSinceLinkPost()});
|
||||
this.setState({ timeSinceLinkPost: this.getTimeSinceLinkPost() });
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export class LinkItem extends Component {
|
||||
}
|
||||
|
||||
getTimeSinceLinkPost() {
|
||||
return !!this.props.timestamp ?
|
||||
return this.props.timestamp ?
|
||||
moment.unix(this.props.timestamp / 1000).from(moment.utc())
|
||||
: '';
|
||||
}
|
||||
@ -38,61 +38,68 @@ export class LinkItem extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
let props = this.props;
|
||||
const mono = (props.nickname) ? 'inter white-d' : 'mono white-d';
|
||||
|
||||
let mono = (props.nickname) ? "inter white-d" : "mono white-d";
|
||||
|
||||
let URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/);
|
||||
const URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/);
|
||||
|
||||
let hostname = URLparser.exec(props.url);
|
||||
|
||||
const seenState = props.seen
|
||||
? "gray2"
|
||||
: "green2 pointer";
|
||||
? 'gray2'
|
||||
: 'green2 pointer';
|
||||
const seenAction = props.seen
|
||||
? ()=>{}
|
||||
: this.markPostAsSeen
|
||||
? () => {}
|
||||
: this.markPostAsSeen;
|
||||
|
||||
if (hostname) {
|
||||
hostname = hostname[4];
|
||||
}
|
||||
|
||||
let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s");
|
||||
const comments = props.comments + ' comment' + ((props.comments === 1) ? '' : 's');
|
||||
|
||||
let member = this.props.member || false;
|
||||
const member = this.props.member || false;
|
||||
|
||||
const img = (this.props.avatar)
|
||||
? <img src={this.props.avatar} height={38} width={38} className="dib" />
|
||||
: <Sigil
|
||||
ship={'~' + props.ship}
|
||||
size={38}
|
||||
color={'#' + props.color}
|
||||
classes={(member ? 'mix-blend-diff' : '')}
|
||||
/>;
|
||||
return (
|
||||
<div className="w-100 pv3 flex bg-white bg-gray0-d">
|
||||
<Sigil
|
||||
ship={"~" + props.ship}
|
||||
size={38}
|
||||
color={"#" + props.color}
|
||||
classes={(member ? "mix-blend-diff" : "")}
|
||||
/>
|
||||
{img}
|
||||
<div className="flex flex-column ml2 flex-auto">
|
||||
<a href={props.url}
|
||||
className="w-100 flex"
|
||||
target="_blank"
|
||||
onClick={this.markPostAsSeen}>
|
||||
onClick={this.markPostAsSeen}
|
||||
>
|
||||
<p className="f8 truncate">{props.title}
|
||||
</p>
|
||||
<span className="gray2 dib v-btm ml2 f8 flex-shrink-0">{hostname} ↗</span>
|
||||
</a>
|
||||
<div className="w-100 pt1">
|
||||
<span className={"f9 pr2 dib " + mono}
|
||||
title={props.ship}>
|
||||
<span className={'f9 pr2 dib ' + mono}
|
||||
title={props.ship}
|
||||
>
|
||||
{(props.nickname)
|
||||
? props.nickname
|
||||
: cite(props.ship)}
|
||||
</span>
|
||||
<span
|
||||
className={seenState + " f9 inter pr3 dib"}
|
||||
onClick={this.markPostAsSeen}>
|
||||
className={seenState + ' f9 inter pr3 dib'}
|
||||
onClick={this.markPostAsSeen}
|
||||
>
|
||||
{this.state.timeSinceLinkPost}
|
||||
</span>
|
||||
<Link to=
|
||||
{makeRoutePath(props.resourcePath, props.popout, props.page, props.url, props.linkIndex)}
|
||||
onClick={this.markPostAsSeen}>
|
||||
onClick={this.markPostAsSeen}
|
||||
>
|
||||
<span className="f9 inter gray2 dib">
|
||||
{comments}
|
||||
</span>
|
||||
@ -100,8 +107,8 @@ export class LinkItem extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkItem
|
||||
export default LinkItem;
|
||||
|
@ -1,14 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { uxToHex, cite } from '/lib/util';
|
||||
|
||||
|
||||
export class MemberElement extends Component {
|
||||
|
||||
onRemove() {
|
||||
const { props } = this;
|
||||
//TODO don't really need to use link-view here, but should we anyway?
|
||||
api.groups.remove(props.groupPath, [`~${props.ship}`]);
|
||||
}
|
||||
|
||||
@ -25,7 +21,8 @@ export class MemberElement extends Component {
|
||||
} else if (props.amOwner && window.ship !== props.ship) {
|
||||
actionElem = (
|
||||
<a onClick={this.onRemove.bind(this)}
|
||||
className="w-20 dib list-ship black white-d f8 pointer">
|
||||
className="w-20 dib list-ship black white-d f8 pointer"
|
||||
>
|
||||
Ban
|
||||
</a>
|
||||
);
|
||||
@ -35,16 +32,21 @@ export class MemberElement extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
let name = !!props.contact
|
||||
const name = props.contact
|
||||
? `${props.contact.nickname} (${cite(props.ship)})`
|
||||
: `${cite(props.ship)}`;
|
||||
let color = !!props.contact ? uxToHex(props.contact.color) : '000000';
|
||||
const color = props.contact ? uxToHex(props.contact.color) : '000000';
|
||||
|
||||
const img = props.contact.avatar
|
||||
? <img src={props.contact.avatar} height={32} width={32} className="dib" />
|
||||
: <Sigil ship={props.ship} size={32} color={`#${color}`} />;
|
||||
|
||||
return (
|
||||
<div className="flex mb2">
|
||||
<Sigil ship={props.ship} size={32} color={`#${color}`} />
|
||||
<p className={"w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8"}
|
||||
title={props.ship}>
|
||||
{img}
|
||||
<p className={'w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8'}
|
||||
title={props.ship}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
{actionElem}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component } from 'react';
|
||||
import { LinksTabBar } from './lib/links-tabbar';
|
||||
import { LinkPreview } from './lib/link-detail-preview';
|
||||
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
||||
import { api } from '../api';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Comments } from './lib/comments';
|
||||
import { Spinner } from './lib/icons/icon-spinner';
|
||||
import { LoadingScreen } from './loading';
|
||||
@ -14,7 +14,7 @@ export class LinkDetail extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
comment: "",
|
||||
comment: '',
|
||||
data: props.data,
|
||||
commentFocus: false,
|
||||
pending: new Set(),
|
||||
@ -43,14 +43,14 @@ export class LinkDetail extends Component {
|
||||
if (this.props.url !== prevProps.url) {
|
||||
this.updateData(this.props.data);
|
||||
}
|
||||
if (prevProps.comments && prevProps.comments["0"] &&
|
||||
this.props.comments && this.props.comments["0"]) {
|
||||
let prevFirstComment = prevProps.comments["0"][0];
|
||||
let thisFirstComment = this.props.comments["0"][0];
|
||||
if (prevProps.comments && prevProps.comments['0'] &&
|
||||
this.props.comments && this.props.comments['0']) {
|
||||
const prevFirstComment = prevProps.comments['0'][0];
|
||||
const thisFirstComment = this.props.comments['0'][0];
|
||||
if ((prevFirstComment && prevFirstComment.udon) &&
|
||||
(thisFirstComment && thisFirstComment.udon)) {
|
||||
if (this.state.pending.has(thisFirstComment.udon)) {
|
||||
let pending = this.state.pending;
|
||||
const pending = this.state.pending;
|
||||
pending.delete(thisFirstComment.udon);
|
||||
this.setState({
|
||||
pending: pending
|
||||
@ -61,9 +61,9 @@ export class LinkDetail extends Component {
|
||||
}
|
||||
|
||||
onClickPost() {
|
||||
let url = this.props.url || "";
|
||||
const url = this.props.url || '';
|
||||
|
||||
let pending = this.state.pending;
|
||||
const pending = this.state.pending;
|
||||
pending.add(this.state.comment);
|
||||
this.setState({ pending: pending, disabled: true });
|
||||
|
||||
@ -72,9 +72,8 @@ export class LinkDetail extends Component {
|
||||
url,
|
||||
this.state.comment
|
||||
).then(() => {
|
||||
this.setState({ comment: "", disabled: false });
|
||||
this.setState({ comment: '', disabled: false });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
setComment(event) {
|
||||
@ -82,37 +81,37 @@ export class LinkDetail extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
const props = this.props;
|
||||
|
||||
const data = this.state.data || props.data;
|
||||
|
||||
if (!data.ship) {
|
||||
return <LoadingScreen/>;
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
let ship = data.ship || "zod";
|
||||
let title = data.title || "";
|
||||
let url = data.url || "";
|
||||
const ship = data.ship || 'zod';
|
||||
const title = data.title || '';
|
||||
const url = data.url || '';
|
||||
|
||||
const commentCount = props.comments
|
||||
? props.comments.totalItems
|
||||
: data.commentCount || 0;
|
||||
|
||||
let comments = commentCount + " comment" + (commentCount === 1 ? "" : "s");
|
||||
const comments = commentCount + ' comment' + (commentCount === 1 ? '' : 's');
|
||||
|
||||
const { nickname } = getContactDetails(props.contacts[ship]);
|
||||
|
||||
let activeClasses = this.state.comment
|
||||
? "black white-d pointer"
|
||||
: "gray2 b--gray2";
|
||||
const activeClasses = this.state.comment
|
||||
? 'black white-d pointer'
|
||||
: 'gray2 b--gray2';
|
||||
|
||||
let focus = (this.state.commentFocus)
|
||||
? "b--black b--white-d"
|
||||
: "b--gray4 b--gray2-d";
|
||||
const focus = (this.state.commentFocus)
|
||||
? 'b--black b--white-d'
|
||||
: 'b--gray4 b--gray2-d';
|
||||
|
||||
let our = getContactDetails(props.contacts[window.ship]);
|
||||
const our = getContactDetails(props.contacts[window.ship]);
|
||||
|
||||
let pendingArray = Array.from(this.state.pending).map((com, i) => {
|
||||
const pendingArray = Array.from(this.state.pending).map((com, i) => {
|
||||
return(
|
||||
<CommentItem
|
||||
key={i}
|
||||
@ -124,23 +123,25 @@ export class LinkDetail extends Component {
|
||||
member={our.member}
|
||||
time={new Date().getTime()}
|
||||
/>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-100 w-100 overflow-hidden flex flex-column">
|
||||
<div
|
||||
className={"pl4 pt2 flex relative overflow-x-scroll " +
|
||||
"overflow-x-auto-l overflow-x-auto-xl flex-shrink-0 " +
|
||||
"bb bn-m bn-l bn-xl b--gray4"}
|
||||
style={{ height: 48 }}>
|
||||
className={'pl4 pt2 flex relative overflow-x-scroll ' +
|
||||
'overflow-x-auto-l overflow-x-auto-xl flex-shrink-0 ' +
|
||||
'bb bn-m bn-l bn-xl b--gray4'}
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}
|
||||
/>
|
||||
<Link
|
||||
className="dib f9 fw4 pt2 gray2 lh-solid"
|
||||
to={makeRoutePath(props.resourcePath, props.popout, props.page)}>
|
||||
to={makeRoutePath(props.resourcePath, props.popout, props.page)}
|
||||
>
|
||||
{`<- ${props.resource.metadata.title}`}
|
||||
</Link>
|
||||
<LinksTabBar {...props} popout={props.popout} resourcePath={props.resourcePath} />
|
||||
@ -159,19 +160,19 @@ export class LinkDetail extends Component {
|
||||
time={this.state.data.time}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className={"relative ba br1 mt6 mb6 " + focus}>
|
||||
<div className={'relative ba br1 mt6 mb6 ' + focus}>
|
||||
<textarea
|
||||
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
|
||||
style={{
|
||||
resize: "none",
|
||||
resize: 'none',
|
||||
height: 75
|
||||
}}
|
||||
placeholder="Leave a comment on this link"
|
||||
onChange={this.setComment}
|
||||
onKeyDown={e => {
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
(e.getModifierState("Control") || e.metaKey) &&
|
||||
e.key === "Enter"
|
||||
(e.getModifierState('Control') || e.metaKey) &&
|
||||
e.key === 'Enter'
|
||||
) {
|
||||
this.onClickPost();
|
||||
}
|
||||
@ -182,14 +183,15 @@ export class LinkDetail extends Component {
|
||||
/>
|
||||
<button
|
||||
className={
|
||||
"f8 bg-gray0-d ml2 absolute " + activeClasses
|
||||
'f8 bg-gray0-d ml2 absolute ' + activeClasses
|
||||
}
|
||||
disabled={!this.state.comment || this.state.disabled}
|
||||
onClick={this.onClickPost.bind(this)}
|
||||
style={{
|
||||
bottom: 12,
|
||||
right: 8
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Post
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,16 +1,15 @@
|
||||
import React, { Component } from 'react'
|
||||
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";
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { LinkItem } from '/components/lib/link-item.js';
|
||||
import { LinkSubmit } from '/components/lib/link-submit.js';
|
||||
import { Pagination } from '/components/lib/pagination.js';
|
||||
|
||||
import { makeRoutePath, getContactDetails } from '../lib/util';
|
||||
|
||||
//TODO Avatar support once it's in
|
||||
export class Links extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -38,44 +37,44 @@ export class Links extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
const props = this.props;
|
||||
|
||||
if (!props.resource.metadata.title) {
|
||||
return <LoadingScreen/>;
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
let linkPage = props.page;
|
||||
const linkPage = props.page;
|
||||
|
||||
let links = !!props.links[linkPage]
|
||||
const links = props.links[linkPage]
|
||||
? props.links[linkPage]
|
||||
: {};
|
||||
|
||||
let currentPage = !!props.page
|
||||
const currentPage = props.page
|
||||
? Number(props.page)
|
||||
: 0;
|
||||
|
||||
let totalPages = !!props.links
|
||||
const totalPages = props.links
|
||||
? Number(props.links.totalPages)
|
||||
: 1;
|
||||
|
||||
let LinkList = (<LoadingScreen/>);
|
||||
let LinkList = (<LoadingScreen />);
|
||||
if (props.links && props.links.totalItems === 0) {
|
||||
LinkList = (
|
||||
<MessageScreen text="Start by posting a link to this collection."/>
|
||||
<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 linksObj = props.links[linkPage];
|
||||
const { title, url, time, ship } = linksObj[linkIndex];
|
||||
const seen = props.seen[url];
|
||||
let members = {};
|
||||
const members = {};
|
||||
|
||||
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, avatar } = getContactDetails(props.contacts[ship]);
|
||||
|
||||
return (
|
||||
<LinkItem
|
||||
@ -89,33 +88,38 @@ export class Links extends Component {
|
||||
nickname={nickname}
|
||||
ship={ship}
|
||||
color={color}
|
||||
avatar={avatar}
|
||||
member={member}
|
||||
comments={commentCount}
|
||||
resourcePath={props.resourcePath}
|
||||
popout={props.popout}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-100 w-100 overflow-hidden flex flex-column">
|
||||
className="h-100 w-100 overflow-hidden flex flex-column"
|
||||
>
|
||||
<div
|
||||
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||
style={{ height: "1rem" }}>
|
||||
<Link to="/~link">{"⟵ All Channels"}</Link>
|
||||
style={{ height: '1rem' }}
|
||||
>
|
||||
<Link to="/~link">{'⟵ All Channels'}</Link>
|
||||
</div>
|
||||
<div
|
||||
className={`pl4 pt2 flex relative overflow-x-scroll
|
||||
overflow-x-auto-l overflow-x-auto-xl flex-shrink-0
|
||||
bb b--gray4 b--gray1-d bg-gray0-d`}
|
||||
style={{ height: 48 }}>
|
||||
style={{ height: 48 }}
|
||||
>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}/>
|
||||
popout={props.popout}
|
||||
/>
|
||||
<Link to={makeRoutePath(props.resourcePath, props.popout, props.page)} className="pt2">
|
||||
<h2 className={`dib f9 fw4 lh-solid v-top`}>
|
||||
<h2 className={'dib f9 fw4 lh-solid v-top'}>
|
||||
{props.resource.metadata.title}
|
||||
</h2>
|
||||
</Link>
|
||||
@ -123,12 +127,13 @@ export class Links extends Component {
|
||||
{...props}
|
||||
popout={props.popout}
|
||||
page={props.page}
|
||||
resourcePath={props.resourcePath}/>
|
||||
resourcePath={props.resourcePath}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-100 mt6 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
<div className="flex">
|
||||
<LinkSubmit resourcePath={props.resourcePath}/>
|
||||
<LinkSubmit resourcePath={props.resourcePath} />
|
||||
</div>
|
||||
<div className="pb4">
|
||||
{LinkList}
|
||||
@ -144,8 +149,8 @@ export class Links extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Links;
|
||||
export default Links;
|
||||
|
@ -24,6 +24,7 @@ export class Root extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.totalUnseen = 0;
|
||||
this.state = store.state;
|
||||
store.setStateHandler(this.setState.bind(this));
|
||||
}
|
||||
@ -42,8 +43,23 @@ export class Root extends Component {
|
||||
const associations = !!state.associations ? state.associations : {link: {}, contacts: {}};
|
||||
let links = !!state.links ? state.links : {};
|
||||
let comments = !!state.comments ? state.comments : {};
|
||||
|
||||
|
||||
const seen = !!state.seen ? state.seen : {};
|
||||
|
||||
|
||||
|
||||
const totalUnseen = _.reduce(
|
||||
seen,
|
||||
(acc, links) => acc + _.reduce(links, (total, hasSeen) => total + (hasSeen ? 0 : 1), 0),
|
||||
0
|
||||
);
|
||||
|
||||
if(totalUnseen !== this.totalUnseen) {
|
||||
document.title = totalUnseen !== 0 ? `Links - (${totalUnseen})` : 'Links';
|
||||
this.totalUnseen = totalUnseen;
|
||||
}
|
||||
|
||||
const invites = state.invites ?
|
||||
state.invites : {};
|
||||
|
||||
|
@ -1,6 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export function makeRoutePath(
|
||||
resource, popout = false, page = 0, url = null, index = 0, compage = 0
|
||||
) {
|
||||
@ -19,7 +16,8 @@ export function makeRoutePath(
|
||||
}
|
||||
|
||||
export function amOwnerOfGroup(groupPath) {
|
||||
if (!groupPath) return false;
|
||||
if (!groupPath)
|
||||
return false;
|
||||
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)[2];
|
||||
return (window.ship === groupOwner);
|
||||
}
|
||||
@ -28,12 +26,13 @@ export function getContactDetails(contact) {
|
||||
const member = !contact;
|
||||
contact = contact || {
|
||||
'nickname': '',
|
||||
'avatar': 'TODO',
|
||||
'avatar': null,
|
||||
'color': '0x0'
|
||||
};
|
||||
const nickname = contact.nickname || '';
|
||||
const color = uxToHex(contact.color || '0x0');
|
||||
return {nickname, color, member};
|
||||
const avatar = contact.avatar || null;
|
||||
return { nickname, color, member, avatar };
|
||||
}
|
||||
|
||||
// encodes string into base64url,
|
||||
@ -55,8 +54,8 @@ export function base64urlDecode(string) {
|
||||
}
|
||||
|
||||
export function isPatTa(str) {
|
||||
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
|
||||
return !!r;
|
||||
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str);
|
||||
return Boolean(r);
|
||||
}
|
||||
|
||||
// encode the string into @ta-safe format, using logic from +wood.
|
||||
@ -86,7 +85,7 @@ export function stringToTa(string) {
|
||||
) {
|
||||
add = char;
|
||||
} else {
|
||||
//TODO behavior for unicode doesn't match +wood's,
|
||||
// TODO behavior for unicode doesn't match +wood's,
|
||||
// but we can probably get away with that for now.
|
||||
add = '~' + charCode.toString(16) + '.';
|
||||
}
|
||||
@ -103,13 +102,13 @@ export function stringToTa(string) {
|
||||
(javascript Date object)
|
||||
*/
|
||||
export function daToDate(st) {
|
||||
var dub = function(n) {
|
||||
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
||||
const dub = function(n) {
|
||||
return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
|
||||
};
|
||||
var da = st.split('..');
|
||||
var bigEnd = da[0].split('.');
|
||||
var lilEnd = da[1].split('.');
|
||||
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
const da = st.split('..');
|
||||
const bigEnd = da[0].split('.');
|
||||
const lilEnd = da[1].split('.');
|
||||
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
return new Date(ds);
|
||||
}
|
||||
|
||||
@ -121,8 +120,8 @@ export function daToDate(st) {
|
||||
*/
|
||||
|
||||
export function dateToDa(d, mil) {
|
||||
var fil = function(n) {
|
||||
return n >= 10 ? n : "0" + n;
|
||||
const fil = function(n) {
|
||||
return n >= 10 ? n : '0' + n;
|
||||
};
|
||||
return (
|
||||
`~${d.getUTCFullYear()}.` +
|
||||
@ -131,7 +130,7 @@ export function dateToDa(d, mil) {
|
||||
`${fil(d.getUTCHours())}.` +
|
||||
`${fil(d.getUTCMinutes())}.` +
|
||||
`${fil(d.getUTCSeconds())}` +
|
||||
`${mil ? "..0000" : ""}`
|
||||
`${mil ? '..0000' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
@ -149,41 +148,41 @@ export function uxToHex(ux) {
|
||||
|
||||
// trim patps to match dojo, chat-cli
|
||||
export function cite(ship) {
|
||||
let patp = ship, shortened = "";
|
||||
if (patp.startsWith("~")) {
|
||||
let patp = ship, shortened = '';
|
||||
if (patp.startsWith('~')) {
|
||||
patp = patp.substr(1);
|
||||
}
|
||||
// comet
|
||||
if (patp.length === 56) {
|
||||
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
|
||||
shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
|
||||
return shortened;
|
||||
}
|
||||
// moon
|
||||
if (patp.length === 27) {
|
||||
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
|
||||
shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27);
|
||||
return shortened;
|
||||
}
|
||||
return `~${patp}`;
|
||||
}
|
||||
|
||||
export function alphabetiseAssociations(associations) {
|
||||
let result = {};
|
||||
const result = {};
|
||||
Object.keys(associations).sort((a, b) => {
|
||||
let aName = a.substr(1);
|
||||
let bName = b.substr(1);
|
||||
if (associations[a].metadata && associations[a].metadata.title) {
|
||||
aName = associations[a].metadata.title !== ""
|
||||
aName = associations[a].metadata.title !== ''
|
||||
? associations[a].metadata.title
|
||||
: a.substr(1);
|
||||
}
|
||||
if (associations[b].metadata && associations[b].metadata.title) {
|
||||
bName = associations[b].metadata.title !== ""
|
||||
bName = associations[b].metadata.title !== ''
|
||||
? associations[b].metadata.title
|
||||
: b.substr(1);
|
||||
}
|
||||
return aName.toLowerCase().localeCompare(bName.toLowerCase());
|
||||
}).map((each) => {
|
||||
result[each] = associations[each];
|
||||
})
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -70,9 +70,11 @@ export class LinkUpdateReducer {
|
||||
// stub in a comment count, which is more or less guaranteed to be 0
|
||||
data.pages = data.pages.map(submission => {
|
||||
submission.commentCount = 0;
|
||||
state.seen[path][submission.url] = false;
|
||||
return submission;
|
||||
});
|
||||
|
||||
|
||||
// add the new submissions to state, update totals
|
||||
state.links[path] = this._addNewItems(
|
||||
data.pages, state.links[path]
|
||||
|
@ -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 {
|
||||
|
@ -3,10 +3,9 @@ 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){
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@ -20,7 +19,7 @@ export class CommentItem extends Component {
|
||||
past: function(input) {
|
||||
return input === 'just now'
|
||||
? input
|
||||
: input + ' ago'
|
||||
: input + ' ago';
|
||||
},
|
||||
s : 'just now',
|
||||
future : 'in %s',
|
||||
@ -33,15 +32,14 @@ export class CommentItem extends Component {
|
||||
M : '1 month',
|
||||
MM : '%d months',
|
||||
y : '1 year',
|
||||
yy : '%d years',
|
||||
yy : '%d years'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
commentEdit() {
|
||||
let commentPath = Object.keys(this.props.comment)[0];
|
||||
let commentBody = this.props.comment[commentPath].content;
|
||||
const commentPath = Object.keys(this.props.comment)[0];
|
||||
const commentBody = this.props.comment[commentPath].content;
|
||||
this.setState({ commentBody });
|
||||
this.props.onEdit();
|
||||
}
|
||||
@ -53,7 +51,7 @@ export class CommentItem extends Component {
|
||||
commentChange(e) {
|
||||
this.setState({
|
||||
commentBody: e.target.value
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onUpdate() {
|
||||
@ -61,28 +59,39 @@ export class CommentItem extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
let pending = !!this.props.pending ? "o-60" : "";
|
||||
let commentData = this.props.comment[Object.keys(this.props.comment)[0]];
|
||||
let content = commentData.content.split("\n").map((line, i)=> {
|
||||
const pending = this.props.pending ? 'o-60' : '';
|
||||
const commentData = this.props.comment[Object.keys(this.props.comment)[0]];
|
||||
const content = commentData.content.split('\n').map((line, i) => {
|
||||
return (
|
||||
<p className="mb2" key={i}>{line}</p>
|
||||
)
|
||||
);
|
||||
});
|
||||
let date = moment(commentData["date-created"]).fromNow();
|
||||
const date = moment(commentData['date-created']).fromNow();
|
||||
|
||||
let contact = !!(commentData.author.substr(1) in this.props.contacts)
|
||||
const contact = commentData.author.substr(1) in this.props.contacts
|
||||
? this.props.contacts[commentData.author.substr(1)] : false;
|
||||
|
||||
let name = commentData.author;
|
||||
let color = "#000000";
|
||||
let classes = "mix-blend-diff";
|
||||
let color = '#000000';
|
||||
let classes = 'mix-blend-diff';
|
||||
let avatar = null;
|
||||
if (contact) {
|
||||
name = (contact.nickname.length > 0)
|
||||
? contact.nickname : commentData.author;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
classes = "";
|
||||
classes = '';
|
||||
avatar = contact.avatar;
|
||||
}
|
||||
|
||||
const img = (avatar !== null)
|
||||
? <img src={avatar} height={24} width={24} className="dib" />
|
||||
: <Sigil
|
||||
ship={commentData.author}
|
||||
size={24}
|
||||
color={color}
|
||||
classes={classes}
|
||||
/>;
|
||||
|
||||
if (name === commentData.author) {
|
||||
name = cite(commentData.author);
|
||||
}
|
||||
@ -93,17 +102,13 @@ export class CommentItem extends Component {
|
||||
|| window.ship !== commentData.author.slice(1);
|
||||
|
||||
return (
|
||||
<div className={"mb8 " + pending}>
|
||||
<div className={'mb8 ' + pending}>
|
||||
<div className="flex mv3 bg-white bg-gray0-d">
|
||||
<Sigil
|
||||
ship={commentData.author}
|
||||
size={24}
|
||||
color={color}
|
||||
classes={classes}
|
||||
/>
|
||||
<div className={"f9 mh2 pt1 " +
|
||||
(contact.nickname ? null : "mono")}
|
||||
title={commentData.author}>
|
||||
{img}
|
||||
<div className={'f9 mh2 pt1 ' +
|
||||
(contact.nickname ? null : 'mono')}
|
||||
title={commentData.author}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="f9 gray3 pt1">{date}</div>
|
||||
@ -121,11 +126,14 @@ export class CommentItem extends Component {
|
||||
<div className="f8 lh-solid mb2">
|
||||
{ !editing && content }
|
||||
{ editing && (
|
||||
<CommentInput style={{resize:'vertical'}}
|
||||
ref={(el) => {this.focusTextArea(el)}}
|
||||
<CommentInput style={{ resize:'vertical' }}
|
||||
ref={(el) => {
|
||||
this.focusTextArea(el);
|
||||
}}
|
||||
onChange={this.commentChange}
|
||||
value={this.state.commentBody}
|
||||
onSubmit={this.onUpdate.bind(this)}>
|
||||
onSubmit={this.onUpdate.bind(this)}
|
||||
>
|
||||
</CommentInput>
|
||||
)}
|
||||
</div>
|
||||
@ -139,10 +147,9 @@ export class CommentItem extends Component {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentItem
|
||||
export default CommentItem;
|
||||
|
@ -1,32 +1,47 @@
|
||||
import React, { Component } from 'react';
|
||||
import { sigil, reactRenderer } from 'urbit-sigil-js';
|
||||
|
||||
|
||||
export class Sigil extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let classes = props.classes || "";
|
||||
const classes = props.classes || '';
|
||||
|
||||
const rgb = {
|
||||
r: parseInt(props.color.slice(1, 3), 16),
|
||||
g: parseInt(props.color.slice(3, 5), 16),
|
||||
b: parseInt(props.color.slice(5, 7), 16)
|
||||
};
|
||||
const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000;
|
||||
const whiteBrightness = 255;
|
||||
|
||||
let foreground = 'white';
|
||||
|
||||
if ((whiteBrightness - brightness) < 50) {
|
||||
foreground = 'black';
|
||||
}
|
||||
|
||||
if (props.ship.length > 14) {
|
||||
return (
|
||||
<div
|
||||
className={"bg-black dib " + classes}
|
||||
style={{ width: props.size, height: props.size }}>
|
||||
</div>
|
||||
className={'bg-black dib ' + classes}
|
||||
style={{ width: props.size, height: props.size }}
|
||||
></div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={"dib " + classes} style={{ flexBasis: props.size, backgroundColor: props.color }}>
|
||||
<div
|
||||
className={'dib ' + classes}
|
||||
style={{ backgroundColor: props.color }}
|
||||
>
|
||||
{sigil({
|
||||
patp: props.ship,
|
||||
renderer: reactRenderer,
|
||||
size: props.size,
|
||||
colors: [props.color, "white"]
|
||||
colors: [props.color, foreground]
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user