Merge branch 'master' into lf/unread-day-indicators

This commit is contained in:
Liam Fitzgerald 2020-04-21 15:56:38 +10:00
commit 0fc12b1456
11 changed files with 411 additions and 168 deletions

View File

@ -235,7 +235,7 @@
=* path p.n.inbox =* path p.n.inbox
=* mailbox q.n.inbox =* mailbox q.n.inbox
=/ =target (path-to-target path) =/ =target (path-to-target path)
=^ cards-n state (read-envelopes target envelopes.mailbox) =^ cards-n state (read-envelopes target (flop envelopes.mailbox))
=^ cards-l state $(inbox l.inbox) =^ cards-l state $(inbox l.inbox)
=^ cards-r state $(inbox r.inbox) =^ cards-r state $(inbox r.inbox)
[:(weld cards-n cards-l cards-r) state] [:(weld cards-n cards-l cards-r) state]
@ -323,7 +323,7 @@
%create (notice-create (path-to-target path.upd)) %create (notice-create (path-to-target path.upd))
%delete [[(show-delete:sh-out (path-to-target path.upd)) ~] state] %delete [[(show-delete:sh-out (path-to-target path.upd)) ~] state]
%message (read-envelope (path-to-target path.upd) envelope.upd) %message (read-envelope (path-to-target path.upd) envelope.upd)
%messages (read-envelopes (path-to-target path.upd) envelopes.upd) %messages (read-envelopes (path-to-target path.upd) (flop envelopes.upd))
== ==
:: ::
++ read-envelopes ++ read-envelopes

View File

@ -363,17 +363,23 @@
== ==
:: ::
%remove %remove
=/ ship (~(get by synced) path.act) =/ ship=(unit ship)
?~ ship [~ state] =/ ship (~(get by synced) path.act)
?^ ship ship
=? path.act ?=([%'~' *] path.act) t.path.act
?~ path.act ~
(slaw %p i.path.act)
?~ ship
~& [dap.bol %unknown-host-cannot-leave path.act]
[~ state]
?: &(!=(u.ship src.bol) ?!((team:title our.bol src.bol))) ?: &(!=(u.ship src.bol) ?!((team:title our.bol src.bol)))
[~ state] [~ state]
=. synced (~(del by synced) path.act) =. synced (~(del by synced) path.act)
:_ state :_ state
%- zing :* [%give %kick ~[[%mailbox path.act]] ~]
:~ (pull-wire [%backlog (weld path.act /0)]) [%give %fact [/synced]~ %chat-hook-update !>([%initial synced])]
(pull-wire [%mailbox path.act]) (pull-wire u.ship [%mailbox path.act])
[%give %kick ~[[%mailbox path.act]] ~]~ (pull-backlog-subscriptions u.ship path.act)
[%give %fact [/synced]~ %chat-hook-update !>([%initial synced])]~
== ==
== ==
:: ::
@ -575,15 +581,15 @@
[%pass chat-history %agent [ship %chat-hook] %watch chat-history]~ [%pass chat-history %agent [ship %chat-hook] %watch chat-history]~
:: ::
[%backlog @ @ *] [%backlog @ @ *]
=/ pax `path`(oust [(dec (lent t.wir)) 1] `(list @ta)`t.wir) =/ chat=path (oust [(dec (lent t.wir)) 1] `(list @ta)`t.wir)
?. (~(has by synced) pax) [~ state] ?. (~(has by synced) chat) [~ state]
=/ =ship =/ =ship
?: =('~' i.t.wir) ?: =('~' i.t.wir)
(slav %p i.t.t.wir) (slav %p i.t.t.wir)
(slav %p i.t.wir) (slav %p i.t.wir)
=. pax ?~((chat-scry pax) wir [%mailbox pax]) =/ =path ?~((chat-scry chat) wir [%mailbox chat])
:_ state :_ state
[%pass pax %agent [ship %chat-hook] %watch pax]~ [%pass path %agent [ship %chat-hook] %watch path]~
== ==
:: ::
++ watch-ack ++ watch-ack
@ -595,10 +601,10 @@
(poke-chat-hook-action %remove t.wir) (poke-chat-hook-action %remove t.wir)
:: ::
[%backlog @ @ @ *] [%backlog @ @ @ *]
=/ pax `path`(oust [(dec (lent t.wir)) 1] `(list @ta)`t.wir) =/ chat=path (oust [(dec (lent t.wir)) 1] `(list @ta)`t.wir)
%. (poke-chat-hook-action %remove pax) %. (poke-chat-hook-action %remove chat)
%- slog %- slog
:* leaf+"chat-hook failed subscribe on {(spud pax)}" :* leaf+"chat-hook failed subscribe on {(spud chat)}"
leaf+"stack trace:" leaf+"stack trace:"
u.saw u.saw
== ==
@ -708,13 +714,23 @@
(snoc `^path`path %noun) (snoc `^path`path %noun)
== ==
:: ::
++ pull-wire ++ pull-backlog-subscriptions
|= pax=path |= [target=ship chat=path]
^- (list card) ^- (list card)
?> ?=(^ pax) %+ murn ~(tap by wex.bol)
=/ shp (~(get by synced) t.pax) |= [[=wire =ship =term] [acked=? =path]]
?~ shp ~ ^- (unit card)
?: =(u.shp our.bol) ?. ?& =(ship target)
[%pass pax %agent [our.bol %chat-store] %leave ~]~ ?=([%backlog *] wire)
[%pass pax %agent [u.shp %chat-hook] %leave ~]~ =(`1 (find chat wire))
==
~
`(pull-wire target wire)
::
++ pull-wire
|= [=ship =wire]
^- card
?: =(ship our.bol)
[%pass wire %agent [our.bol %chat-store] %leave ~]
[%pass wire %agent [ship %chat-hook] %leave ~]
-- --

View File

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

View File

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

View File

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

View File

@ -14,17 +14,29 @@ import { ChatInput } from '/components/lib/chat-input';
import { UnreadNotice } from '/components/lib/unread-notice'; import { UnreadNotice } from '/components/lib/unread-notice';
import { deSig } from '/lib/util'; import { deSig } from '/lib/util';
function getNumPending(props) {
const result = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station).length
: 0;
return result;
}
export class ChatScreen extends Component { export class ChatScreen extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
numPages: 1, numPages: 1,
scrollLocked: false scrollLocked: false,
// only for FF
lastScrollHeight: null,
scrollBottom: true
}; };
this.hasAskedForMessages = false; this.hasAskedForMessages = false;
this.lastNumPending = 0;
this.scrollContainer = null;
this.onScroll = this.onScroll.bind(this); this.onScroll = this.onScroll.bind(this);
this.unreadMarker = null; this.unreadMarker = null;
@ -41,20 +53,22 @@ export class ChatScreen extends Component {
}); });
} }
componentDidMount() { componentDidMount() {
this.askForMessages(); this.askForMessages();
this.scrollToBottom();
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { props, state } = this; const { props, state } = this;
if ( if (
prevProps.match.params.station !== props.match.params.station || prevProps.match.params.station !== props.match.params.station ||
prevProps.match.params.ship !== props.match.params.ship prevProps.match.params.ship !== props.match.params.ship
) { ) {
this.hasAskedForMessages = false; this.hasAskedForMessages = false;
if (props.envelopes.length < 100) { if (props.envelopes.length < 100) {
this.askForMessages(); this.askForMessages();
} }
@ -75,8 +89,26 @@ export class ChatScreen extends Component {
) { ) {
this.hasAskedForMessages = false; this.hasAskedForMessages = false;
} }
}
// FF logic
if (
navigator.userAgent.includes("Firefox") &&
(props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages)
) {
if(state.scrollBottom) {
setTimeout(() => {
this.scrollToBottom();
})
} else {
this.recalculateScrollTop();
}
this.lastNumPending = getNumPending(props);
}
}
askForMessages() { askForMessages() {
const { props, state } = this; const { props, state } = this;
@ -104,20 +136,45 @@ export class ChatScreen extends Component {
props.subscription.fetchMessages(start + 1, end, props.station); props.subscription.fetchMessages(start + 1, end, props.station);
} }
} }
scrollToBottom() { scrollToBottom() {
if (!this.state.scrollLocked && this.scrollElement) { if (!this.state.scrollLocked && this.scrollElement) {
this.scrollElement.scrollIntoView({ behavior: "smooth" }); this.scrollElement.scrollIntoView();
} }
} }
// Restore chat position on FF when new messages come in
recalculateScrollTop() {
if(!this.scrollContainer) {
return;
}
const { lastScrollHeight } = this.state;
let target = this.scrollContainer;
let newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight;
if(target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
return;
}
target.scrollTop = target.scrollHeight - lastScrollHeight;
}
onScroll(e) { onScroll(e) {
if ( if (
navigator.userAgent.includes("Safari") && (navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome") navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
) { ) {
// Google Chrome // Google Chrome and Firefox
if (e.target.scrollTop === 0) { if (e.target.scrollTop === 0) {
// Save scroll position for FF
if (navigator.userAgent.includes('Firefox')) {
this.setState({
lastScrollHeight: e.target.scrollHeight
})
}
this.setState( this.setState(
{ {
numPages: this.state.numPages + 1, numPages: this.state.numPages + 1,
@ -133,8 +190,11 @@ export class ChatScreen extends Component {
) { ) {
this.setState({ this.setState({
numPages: 1, numPages: 1,
scrollLocked: false scrollLocked: false,
scrollBottom: true
}); });
} else if (navigator.userAgent.includes('Firefox')) {
this.setState({ scrollBottom: false });
} }
} else if (navigator.userAgent.includes("Safari")) { } else if (navigator.userAgent.includes("Safari")) {
// Safari // Safari
@ -160,38 +220,49 @@ export class ChatScreen extends Component {
} else { } else {
console.log("Your browser is not supported."); console.log("Your browser is not supported.");
} }
if(!!this.unreadMarker && if(!!this.unreadMarker) {
e.target.scrollHeight - e.target.scrollTop - (e.target.clientHeight * 1.5) + this.unreadMarker.offsetTop > 50 ) { 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);
}
this.props.api.chat.read(this.props.station);
} }
} }
render() { chatWindow(unread) {
// Replace with just the "not Firefox" implementation
// when Firefox #1042151 is patched.
const { props, state } = this; const { props, state } = this;
let messages = props.envelopes.slice(0); let messages = props.envelopes.slice(0);
let lastMsgNum = messages.length > 0 ? messages.length : 0; let lastMsgNum = messages.length > 0 ? messages.length : 0;
if (messages.length > 100 * state.numPages) { if (messages.length > 100 * state.numPages) {
messages = messages.slice(0, 100 * state.numPages); messages = messages.slice(0, 100 * state.numPages);
} }
let pendingMessages = props.pendingMessages.has(props.station) let pendingMessages = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station) ? props.pendingMessages.get(props.station)
: []; : [];
pendingMessages.map(function(value) {
pendingMessages.map(function (value) {
return (value.pending = true); return (value.pending = true);
}); });
const unread = props.length - props.read;
const unreadMsg = unread > 0 && messages[unread - 1];
let messageElements = pendingMessages.concat(messages).map((msg, i) => { messages = pendingMessages.concat(messages);
let messageElements = messages.map((msg, i) => {
// Render sigil if previous message is not by the same sender // Render sigil if previous message is not by the same sender
let aut = ["author"]; let aut = ["author"];
let renderSigil = let renderSigil =
@ -205,7 +276,7 @@ export class ChatScreen extends Component {
let when = ['when']; let when = ['when'];
let dayBreak = let dayBreak =
moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !== moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !==
moment(_.get(messages[i], when)).format('YYYY.MM.DD') moment(_.get(messages[i], when)).format('YYYY.MM.DD');
const messageElem = ( const messageElem = (
<Message <Message
@ -252,16 +323,76 @@ export class ChatScreen extends Component {
return messageElem; return messageElem;
} }
}); });
if (navigator.userAgent.includes("Firefox")) {
return (
<div className="overflow-y-scroll h-100" onScroll={this.onScroll} ref={e => { this.scrollContainer = e; }}>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: "vertical" }}
>
<div
ref={el => {
this.scrollElement = el;
}}></div>
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station} />
) : (<div />)
}
{messageElements}
</div>
</div>
)}
else {
return (
<div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}
>
<div
ref={el => {
this.scrollElement = el;
}}></div>
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station} />
) : (<div />)
}
{messageElements}
</div>
)}
}
render() {
const { props, state } = this;
let messages = props.envelopes.slice(0);
let lastMsgNum = messages.length > 0 ? messages.length : 0;
let group = Array.from(props.permission.who.values()); let group = Array.from(props.permission.who.values());
const isinPopout = props.popout ? "popout/" : ""; const isinPopout = props.popout ? "popout/" : "";
let ownerContact = (window.ship in props.contacts) let ownerContact = (window.ship in props.contacts)
? props.contacts[window.ship] : false; ? props.contacts[window.ship] : false;
let title = props.station.substr(1); let title = props.station.substr(1);
if (props.association && "metadata" in props.association) { if (props.association && "metadata" in props.association) {
title = title =
props.association.metadata.title !== "" props.association.metadata.title !== ""
@ -269,6 +400,9 @@ export class ChatScreen extends Component {
: props.station.substr(1); : props.station.substr(1);
} }
const unread = props.length - props.read;
const unreadMsg = unread > 0 && messages[unread - 1];
return ( return (
<div <div
@ -312,27 +446,7 @@ export class ChatScreen extends Component {
onRead={() => props.api.chat.read(props.station)} onRead={() => props.api.chat.read(props.station)}
/> />
) } ) }
<div {this.chatWindow(unread)}
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}>
<div
ref={el => {
this.scrollElement = el;
}}></div>
{ (
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station} />
) : (<div/>)
}
{messageElements}
</div>
<ChatInput <ChatInput
api={props.api} api={props.api}
numMsgs={lastMsgNum} numMsgs={lastMsgNum}

View File

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

View File

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

View File

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

View File

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

View File

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