mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-07 07:30:23 +03:00
Merge branch 'master' into lf/unread-day-indicators
This commit is contained in:
commit
0fc12b1456
@ -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
|
||||||
|
@ -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 ~]
|
||||||
--
|
--
|
||||||
|
@ -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
|
||||||
::
|
::
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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}
|
||||||
|
15
pkg/interface/link/src/js/components/lib/message-screen.js
Normal file
15
pkg/interface/link/src/js/components/lib/message-screen.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}} />
|
}} />
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user