mirror of
https://github.com/urbit/shrub.git
synced 2024-12-15 21:03:10 +03:00
Infinite scrolling works, localStorage works, progressive loading of information works, new subscription pattern works
This commit is contained in:
parent
2a2221f03a
commit
bc4f1968f2
@ -51,6 +51,15 @@ class UrbitApi {
|
||||
});
|
||||
}
|
||||
|
||||
notify(aud, bool) {
|
||||
this.hall({
|
||||
notify: {
|
||||
aud,
|
||||
pes: !!bool ? 'hear' : 'gone'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
permit(cir, aud, message) {
|
||||
this.hall({
|
||||
permit: {
|
||||
|
@ -16,14 +16,47 @@ export class ChatScreen extends Component {
|
||||
station: props.match.params.ship + "/" + props.match.params.station,
|
||||
circle: props.match.params.station,
|
||||
host: props.match.params.ship,
|
||||
numPeople: 0
|
||||
numPeople: 0,
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
};
|
||||
|
||||
this.topMessage = {};
|
||||
this.buildMessage = this.buildMessage.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateNumPeople();
|
||||
this.scrollElement.scrollIntoView(false);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (!this.state.scrollLocked) {
|
||||
console.log('scroll to bottom');
|
||||
this.scrollElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
onScroll(e) {
|
||||
if (e.target.scrollTop === 0) {
|
||||
let topMessage = this.topMessage;
|
||||
|
||||
this.setState({
|
||||
numPages: this.state.numPages + 1,
|
||||
scrollLocked: true
|
||||
}, () => {
|
||||
this.topMessage[1].scrollIntoView(true);
|
||||
});
|
||||
} else if (
|
||||
(e.target.scrollHeight - Math.round(e.target.scrollTop)) ===
|
||||
e.target.clientHeight
|
||||
) {
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@ -41,6 +74,7 @@ export class ChatScreen extends Component {
|
||||
this.updateReadNumber();
|
||||
this.updateNumPeople();
|
||||
this.updateNumMessagesLoaded(prevProps, prevState);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
updateReadNumber() {
|
||||
@ -77,7 +111,7 @@ export class ChatScreen extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
buildMessage(msg) {
|
||||
buildMessage(msg, index) {
|
||||
let details = msg.printship ? null : getMessageContent(msg.gam);
|
||||
|
||||
if (msg.printship) {
|
||||
@ -89,13 +123,29 @@ export class ChatScreen extends Component {
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Message key={msg.gam.uid} msg={msg.gam} details={details} />
|
||||
);
|
||||
|
||||
if (index % 50 === 0) {
|
||||
let pageNum = index / 50;
|
||||
return (
|
||||
<div ref={ el => { this.topMessage[pageNum] = el; }}>
|
||||
<Message
|
||||
key={msg.gam.uid} msg={msg.gam} details={details} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Message key={msg.gam.uid} msg={msg.gam} details={details} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
let messages = this.props.messages[this.state.station] || [];
|
||||
if (messages.length > 50 * state.numPages) {
|
||||
messages =
|
||||
messages.slice(messages.length - (50 * state.numPages), messages.length);
|
||||
}
|
||||
let chatMessages = messages.map(this.buildMessage);
|
||||
|
||||
return (
|
||||
@ -104,8 +154,12 @@ export class ChatScreen extends Component {
|
||||
<h2>{this.state.circle}</h2>
|
||||
<ChatTabBar {...this.props} station={this.state.station} />
|
||||
</div>
|
||||
<div className="overflow-y-scroll" style={{ flexGrow: 1 }}>
|
||||
<div
|
||||
className="overflow-y-scroll"
|
||||
style={{ flexGrow: 1 }}
|
||||
onScroll={this.onScroll}>
|
||||
{chatMessages}
|
||||
<div ref={ el => { this.scrollElement = el; }}></div>
|
||||
</div>
|
||||
<ChatInput
|
||||
api={this.props.api}
|
||||
|
@ -105,6 +105,42 @@ export class ChatInput extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { props, state } = this;
|
||||
/*let closure = () => {
|
||||
let aud, sep;
|
||||
let wen = Date.now();
|
||||
let uid = uuid();
|
||||
let aut = window.ship;
|
||||
|
||||
let config = props.configs[state.station];
|
||||
|
||||
aud = [props.station];
|
||||
sep = {
|
||||
lin: {
|
||||
msg: Date.now().toString(),
|
||||
pat: false
|
||||
}
|
||||
}
|
||||
|
||||
let message = {
|
||||
uid,
|
||||
aut,
|
||||
wen,
|
||||
aud,
|
||||
sep,
|
||||
};
|
||||
|
||||
props.api.hall({
|
||||
convey: [message]
|
||||
});
|
||||
|
||||
setTimeout(closure, 1000);
|
||||
};
|
||||
|
||||
closure();*/
|
||||
|
||||
|
||||
return (
|
||||
<div className="mt2 pa3 cf flex black bt">
|
||||
<div className="fl" style={{ flexBasis: 35, height: 40 }}>
|
||||
@ -114,8 +150,8 @@ export class ChatInput extends Component {
|
||||
<input className="ml2 bn"
|
||||
style={{ flexGrow: 1 }}
|
||||
ref={this.textareaRef}
|
||||
placeholder={this.props.placeholder}
|
||||
value={this.state.message}
|
||||
placeholder={props.placeholder}
|
||||
value={state.message}
|
||||
onChange={this.messageChange} />
|
||||
<div className="pointer" onClick={this.messageSubmit}>
|
||||
<IconSend />
|
||||
|
@ -1,11 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class ChatReducer {
|
||||
export class ConfigReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'chat', false);
|
||||
if (data) {
|
||||
state.messages = data.messages;
|
||||
state.inbox = data.inbox;
|
||||
state.configs = data.configs;
|
||||
state.circles = data.circles;
|
15
apps/chat/src/js/reducers/initial.js
Normal file
15
apps/chat/src/js/reducers/initial.js
Normal file
@ -0,0 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class InitialReducer {
|
||||
reduce(json, state) {
|
||||
let data = _.get(json, 'initial', false);
|
||||
if (data) {
|
||||
state.messages = data.messages;
|
||||
state.inbox = data.inbox;
|
||||
state.configs = data.configs;
|
||||
state.circles = data.circles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export class UpdateReducer {
|
||||
if (data) {
|
||||
this.reduceInbox(_.get(data, 'inbox', false), state);
|
||||
this.reduceMessage(_.get(data, 'message', false), state);
|
||||
this.reduceMessages(_.get(data, 'messages', false), state);
|
||||
this.reduceConfig(_.get(data, 'config', false), state);
|
||||
this.reduceCircles(_.get(data, 'circles', false), state);
|
||||
}
|
||||
@ -26,6 +27,22 @@ export class UpdateReducer {
|
||||
}
|
||||
}
|
||||
|
||||
reduceMessages(messages, state) {
|
||||
if (messages.circle in state.messages) {
|
||||
let station = state.messages[messages.circle];
|
||||
if (
|
||||
station.length > 0 &&
|
||||
station[station.length - 1].num === station.length - 1 &&
|
||||
messages.start === station.length
|
||||
) {
|
||||
state.messages[messages.circle] =
|
||||
state.messages[messages.circle].concat(messages.envelopes);
|
||||
} else {
|
||||
console.error('%messages has indices inconsistent with localStorage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reduceConfig(config, state) {
|
||||
if (config) {
|
||||
state.configs[config.circle] = config.config;
|
||||
|
@ -1,20 +1,34 @@
|
||||
import _ from 'lodash';
|
||||
import { ChatReducer } from '/reducers/chat';
|
||||
import { InitialReducer } from '/reducers/initial';
|
||||
import { ConfigReducer } from '/reducers/config';
|
||||
import { UpdateReducer } from '/reducers/update';
|
||||
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {
|
||||
inbox: {},
|
||||
messages: [],
|
||||
configs: {},
|
||||
circles: []
|
||||
};
|
||||
let state = localStorage.getItem('store');
|
||||
|
||||
this.chatReducer = new ChatReducer();
|
||||
if (!state) {
|
||||
this.state = {
|
||||
inbox: {},
|
||||
messages: [],
|
||||
configs: {},
|
||||
circles: [],
|
||||
local: false
|
||||
};
|
||||
} else {
|
||||
this.state = JSON.parse(state);
|
||||
// TODO: wtf???
|
||||
delete this.state.messages[undefined];
|
||||
console.log(this.state);
|
||||
this.state.local = true;
|
||||
}
|
||||
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.configReducer = new ConfigReducer();
|
||||
this.updateReducer = new UpdateReducer();
|
||||
this.setState = () => {};
|
||||
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
@ -22,14 +36,17 @@ class Store {
|
||||
}
|
||||
|
||||
handleEvent(data) {
|
||||
console.log(data);
|
||||
let json = data.data;
|
||||
|
||||
this.chatReducer.reduce(json, this.state);
|
||||
this.initialReducer.reduce(json, this.state);
|
||||
this.configReducer.reduce(json, this.state);
|
||||
this.updateReducer.reduce(json, this.state);
|
||||
|
||||
this.setState(this.state);
|
||||
localStorage.setItem('store', JSON.stringify(this.state));
|
||||
}
|
||||
}
|
||||
|
||||
export let store = new Store();
|
||||
|
||||
window.store = store;
|
||||
|
@ -7,33 +7,32 @@ export class Subscription {
|
||||
start() {
|
||||
if (api.authTokens) {
|
||||
this.initializeChat();
|
||||
//this.setCleanupTasks();
|
||||
} else {
|
||||
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
|
||||
}
|
||||
}
|
||||
|
||||
/*setCleanupTasks() {
|
||||
window.addEventListener("beforeunload", e => {
|
||||
api.bindPaths.forEach(p => {
|
||||
this.wipeSubscription(p);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
wipeSubscription(path) {
|
||||
api.hall({
|
||||
wipe: {
|
||||
sub: [{
|
||||
hos: api.authTokens.ship,
|
||||
pax: path
|
||||
}]
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
initializeChat() {
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
|
||||
if (store.state.local) {
|
||||
let path = [];
|
||||
let msg = Object.keys(store.state.messages);
|
||||
for (let i = 0; i < msg.length; i++) {
|
||||
let cir = msg[i];
|
||||
let len = store.state.messages[cir].length;
|
||||
path.push(`${cir}/${len}`);
|
||||
}
|
||||
path = path.join('/');
|
||||
|
||||
api.bind(`/primary/${path}`, 'PUT', api.authTokens.ship, 'chat',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this));
|
||||
} else {
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this));
|
||||
}
|
||||
|
||||
api.bind('/updates', 'PUT', api.authTokens.ship, 'chat',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this));
|
||||
}
|
||||
|
@ -44,9 +44,9 @@
|
||||
|= old=(unit state)
|
||||
^- (quip move _this)
|
||||
?~ old
|
||||
=/ inboxpat /circle/inbox/config/group
|
||||
=/ inboxpat /circle/inbox/config-l/config-r/group-r/group-l
|
||||
=/ circlespat /circles/[(scot %p our.bol)]
|
||||
=/ inboxwir /circle/[(scot %p our.bol)]/inbox/config/group
|
||||
=/ inboxwir /circle/[(scot %p our.bol)]/inbox/config/group-r/group-l
|
||||
=/ inboxi/poke
|
||||
:- %hall-action
|
||||
[%source %inbox %.y (silt [[our.bol %i] ~]~)]
|
||||
@ -58,13 +58,97 @@
|
||||
==
|
||||
[~ this(sta u.old)]
|
||||
::
|
||||
:: +peer-primary: subscribe to our data and updates
|
||||
:: +peer-messages: subscribe to subset of messages and updates
|
||||
::
|
||||
+$ internal-state
|
||||
$:
|
||||
lis=(list [circle:hall @])
|
||||
item=[cir=circle:hall count=@ud]
|
||||
index=@
|
||||
==
|
||||
++ generate-circle-indices
|
||||
|= wir=wire
|
||||
^- (list [circle:hall @])
|
||||
=/ data
|
||||
%^ spin (swag [0 (lent wir)] wir) *internal-state
|
||||
|= [a=@ta b=internal-state]
|
||||
^- [* out=internal-state]
|
||||
=/ switch (dvr index.b 3)
|
||||
?: =(q.switch 0) :: remainder 0, should be a ship
|
||||
?: =(index.b 0) :: if item is null, don't add to list
|
||||
:- 0
|
||||
%= b
|
||||
hos.cir.item (slav %p a)
|
||||
index +(index.b)
|
||||
==
|
||||
:: if item is not null, add to list
|
||||
:- 0
|
||||
%= b
|
||||
hos.cir.item (slav %p a)
|
||||
nom.cir.item *name:hall
|
||||
count.item 0
|
||||
lis (snoc lis.b item.b)
|
||||
index +(index.b)
|
||||
==
|
||||
?: =(q.switch 1) :: remainder 1, should be a circle name
|
||||
:- 0
|
||||
%= b
|
||||
nom.cir.item a
|
||||
index +(index.b)
|
||||
==
|
||||
?: =(q.switch 2) :: remainder 2, should be a number
|
||||
:- 0
|
||||
%= b
|
||||
count.item (need (rush a dem))
|
||||
index +(index.b)
|
||||
==
|
||||
!! :: impossible
|
||||
?: =(index.q.data 0)
|
||||
~
|
||||
(snoc lis.q.data item.q.data)
|
||||
::
|
||||
++ peer-primary
|
||||
|= wir=wire
|
||||
^- (quip move _this)
|
||||
=/ indices (generate-circle-indices wir)
|
||||
?~ indices
|
||||
:_ this
|
||||
[ost.bol %diff %chat-initial str.sta]~
|
||||
=* messages messages.str.sta
|
||||
=/ lisunitmov/(list (unit move))
|
||||
%+ turn indices
|
||||
|= [cir=circle:hall start=@ud]
|
||||
^- (unit move)
|
||||
=/ wholelist/(unit (list envelope:hall)) (~(get by messages) cir)
|
||||
?~ wholelist
|
||||
~
|
||||
=/ end/@ (lent u.wholelist)
|
||||
?: (gte start end)
|
||||
~
|
||||
:- ~
|
||||
:* ost.bol
|
||||
%diff
|
||||
%chat-update
|
||||
[%messages cir start end (swag [start end] u.wholelist)]
|
||||
==
|
||||
=/ lismov/(list move)
|
||||
%+ turn
|
||||
%+ skim lisunitmov
|
||||
|= umov=(unit move)
|
||||
^- ?
|
||||
?~ umov
|
||||
%.n
|
||||
%.y
|
||||
need
|
||||
:_ this
|
||||
[ost.bol %diff %chat-streams str.sta]~
|
||||
%+ weld
|
||||
[ost.bol %diff %chat-config str.sta]~
|
||||
lismov
|
||||
::
|
||||
++ peer-updates
|
||||
|= wir=wire
|
||||
^- (quip move _this)
|
||||
[~ this]
|
||||
::
|
||||
++ poke-noun
|
||||
|= a=*
|
||||
@ -93,7 +177,7 @@
|
||||
++ send-chat-update
|
||||
|= upd=update
|
||||
^- (list move)
|
||||
%+ turn (prey:pubsub:userlib /primary bol)
|
||||
%+ turn (prey:pubsub:userlib /updates bol)
|
||||
|= [=bone *]
|
||||
[bone %diff %chat-update upd]
|
||||
::
|
||||
@ -123,19 +207,12 @@
|
||||
:: %circle wire
|
||||
::
|
||||
%circle
|
||||
:: ?+ -.piz
|
||||
:: ::
|
||||
:: :: %peers prize
|
||||
:: ::
|
||||
:::: %peers
|
||||
:::: ?> ?=(%peers -.piz)
|
||||
:::: [~ this]
|
||||
:: ::
|
||||
:: :: %circle prize
|
||||
:: ::
|
||||
:: %circle
|
||||
?> ?=(%circle -.piz)
|
||||
~& piz
|
||||
~& pes.piz
|
||||
=/ circle/circle:hall [our.bol &2:wir]
|
||||
?: =(circle [our.bol %inbox])
|
||||
::
|
||||
@ -145,27 +222,46 @@
|
||||
%- ~(uni in configs.str.sta)
|
||||
^- (map circle:hall (unit config:hall))
|
||||
(~(run by rem.cos.piz) |=(a=config:hall `a))
|
||||
~& pes.piz
|
||||
::
|
||||
=/ circles/(list circle:hall)
|
||||
%+ turn ~(tap in src.loc.cos.piz)
|
||||
|= src=source:hall
|
||||
^- circle:hall
|
||||
cir.src
|
||||
::
|
||||
=/ meslis/(list [circle:hall (list envelope:hall)])
|
||||
%+ turn circles
|
||||
|= cir=circle:hall
|
||||
^- [circle:hall (list envelope:hall)]
|
||||
[cir ~]
|
||||
::
|
||||
=/ localpeers/(set @p)
|
||||
%- silt %+ turn ~(tap by loc.pes.piz)
|
||||
|= [shp=@p stat=status:hall]
|
||||
shp
|
||||
::
|
||||
=/ peers/(map circle:hall (set @p))
|
||||
%- ~(rep by rem.pes.piz)
|
||||
|= [[cir=circle:hall grp=group:hall] acc=(map circle:hall (set @p))]
|
||||
^- (map circle:hall (set @p))
|
||||
=/ newset
|
||||
%- silt %+ turn ~(tap by grp)
|
||||
|= [shp=@p stat=status:hall]
|
||||
shp
|
||||
(~(put by acc) cir newset)
|
||||
::
|
||||
:-
|
||||
%+ turn ~(tap in (~(del in (silt circles)) [our.bol %inbox]))
|
||||
|= cir=circle:hall
|
||||
^- move
|
||||
=/ pat/path /circle/[nom.cir]/config/grams
|
||||
=/ pat/path /circle/[nom.cir]/config-l/config-r/group-r/group-l
|
||||
[ost.bol %peer pat [our.bol %hall] pat]
|
||||
::
|
||||
%= this
|
||||
inbox.str.sta loc.cos.piz
|
||||
configs.str.sta configs
|
||||
messages.str.sta (molt meslis)
|
||||
peers.str.sta (~(put by peers) [our.bol %inbox] localpeers)
|
||||
==
|
||||
::
|
||||
:: fill remote configs with message data
|
||||
@ -224,22 +320,27 @@
|
||||
messages.str.sta (~(put by messages) circle (snoc nes nev.sto))
|
||||
==
|
||||
::
|
||||
:: %peer:
|
||||
:: %status:
|
||||
::
|
||||
%peer
|
||||
?> ?=(%peer -.sto)
|
||||
~& add.sto
|
||||
~& who.sto
|
||||
~& qer.sto
|
||||
[~ this]
|
||||
%status
|
||||
?> ?=(%status -.sto)
|
||||
=/ peers/(set @p)
|
||||
?: =(%remove -.dif.sto)
|
||||
(~(del in (~(got by peers.str.sta) cir.sto)) who.sto)
|
||||
(~(put in (~(got by peers.str.sta) cir.sto)) who.sto)
|
||||
:- (send-chat-update [%peers cir.sto peers])
|
||||
%= this
|
||||
peers.str.sta (~(put by peers.str.sta) cir.sto peers)
|
||||
==
|
||||
|
||||
::
|
||||
:: %config: config has changed
|
||||
::
|
||||
%config
|
||||
=* circ cir.sto
|
||||
::
|
||||
?+ -.dif.sto
|
||||
[~ this]
|
||||
=* circ cir.sto
|
||||
::
|
||||
?+ -.dif.sto
|
||||
[~ this]
|
||||
::
|
||||
:: %full: set all of config without side effects
|
||||
::
|
||||
@ -277,8 +378,8 @@
|
||||
[~ this]
|
||||
=* affectedcir cir.src.dif.sto
|
||||
=/ newwir/wire
|
||||
/circle/[(scot %p hos.affectedcir)]/[nom.affectedcir]/grams/config
|
||||
=/ pat/path /circle/[nom.affectedcir]/grams/config
|
||||
/circle/[(scot %p hos.affectedcir)]/[nom.affectedcir]/grams/config/group-r/group-l
|
||||
=/ pat/path /circle/[nom.affectedcir]/grams/config/group-r/group-l
|
||||
:: we've added a source to our inbox
|
||||
::
|
||||
?: add.dif.sto
|
||||
@ -411,7 +512,7 @@
|
||||
::
|
||||
%circle
|
||||
=/ shp/@p (slav %p &2:wir)
|
||||
=/ pat /circle/[&3:wir]/grams/config
|
||||
=/ pat /circle/[&3:wir]/grams/config/group-r/group-l
|
||||
:_ this
|
||||
[ost.bol %peer wir [shp %hall] wir]~
|
||||
::
|
||||
|
@ -17,6 +17,10 @@
|
||||
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
||||
}
|
||||
|
||||
function getCjsExportFromNamespace (n) {
|
||||
return n && n.default || n;
|
||||
}
|
||||
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@ -47458,6 +47462,8 @@
|
||||
isBuffer: isBuffer
|
||||
});
|
||||
|
||||
var require$$0 = getCjsExportFromNamespace(bufferEs6);
|
||||
|
||||
var bn = createCommonjsModule(function (module) {
|
||||
(function (module, exports) {
|
||||
|
||||
@ -47510,7 +47516,7 @@
|
||||
|
||||
var Buffer;
|
||||
try {
|
||||
Buffer = bufferEs6.Buffer;
|
||||
Buffer = require$$0.Buffer;
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
@ -51734,6 +51740,15 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
});
|
||||
}
|
||||
|
||||
notify(aud, bool) {
|
||||
this.hall({
|
||||
notify: {
|
||||
aud,
|
||||
pes: !!bool ? 'hear' : 'gone'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
permit(cir, aud, message) {
|
||||
this.hall({
|
||||
permit: {
|
||||
@ -51814,11 +51829,22 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
let api = new UrbitApi();
|
||||
window.api = api;
|
||||
|
||||
class ChatReducer {
|
||||
class InitialReducer {
|
||||
reduce(json, state) {
|
||||
let data = lodash.get(json, 'initial', false);
|
||||
if (data) {
|
||||
state.messages = data.messages;
|
||||
state.inbox = data.inbox;
|
||||
state.configs = data.configs;
|
||||
state.circles = data.circles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigReducer {
|
||||
reduce(json, state) {
|
||||
let data = lodash.get(json, 'chat', false);
|
||||
if (data) {
|
||||
state.messages = data.messages;
|
||||
state.inbox = data.inbox;
|
||||
state.configs = data.configs;
|
||||
state.circles = data.circles;
|
||||
@ -51832,6 +51858,7 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
if (data) {
|
||||
this.reduceInbox(lodash.get(data, 'inbox', false), state);
|
||||
this.reduceMessage(lodash.get(data, 'message', false), state);
|
||||
this.reduceMessages(lodash.get(data, 'messages', false), state);
|
||||
this.reduceConfig(lodash.get(data, 'config', false), state);
|
||||
this.reduceCircles(lodash.get(data, 'circles', false), state);
|
||||
}
|
||||
@ -51851,6 +51878,22 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
}
|
||||
}
|
||||
|
||||
reduceMessages(messages, state) {
|
||||
if (messages.circle in state.messages) {
|
||||
let station = state.messages[messages.circle];
|
||||
if (
|
||||
station.length > 0 &&
|
||||
station[station.length - 1].num === station.length - 1 &&
|
||||
messages.start === station.length
|
||||
) {
|
||||
state.messages[messages.circle] =
|
||||
state.messages[messages.circle].concat(messages.envelopes);
|
||||
} else {
|
||||
console.error('%messages has indices inconsistent with localStorage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reduceConfig(config, state) {
|
||||
if (config) {
|
||||
state.configs[config.circle] = config.config;
|
||||
@ -51867,16 +51910,29 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {
|
||||
inbox: {},
|
||||
messages: [],
|
||||
configs: {},
|
||||
circles: []
|
||||
};
|
||||
let state = localStorage.getItem('store');
|
||||
|
||||
this.chatReducer = new ChatReducer();
|
||||
if (!state) {
|
||||
this.state = {
|
||||
inbox: {},
|
||||
messages: [],
|
||||
configs: {},
|
||||
circles: [],
|
||||
local: false
|
||||
};
|
||||
} else {
|
||||
this.state = JSON.parse(state);
|
||||
// TODO: wtf???
|
||||
delete this.state.messages[undefined];
|
||||
console.log(this.state);
|
||||
this.state.local = true;
|
||||
}
|
||||
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.configReducer = new ConfigReducer();
|
||||
this.updateReducer = new UpdateReducer();
|
||||
this.setState = () => {};
|
||||
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
@ -51884,16 +51940,20 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
}
|
||||
|
||||
handleEvent(data) {
|
||||
console.log(data);
|
||||
let json = data.data;
|
||||
|
||||
this.chatReducer.reduce(json, this.state);
|
||||
this.initialReducer.reduce(json, this.state);
|
||||
this.configReducer.reduce(json, this.state);
|
||||
this.updateReducer.reduce(json, this.state);
|
||||
|
||||
this.setState(this.state);
|
||||
localStorage.setItem('store', JSON.stringify(this.state));
|
||||
}
|
||||
}
|
||||
|
||||
let store = new Store();
|
||||
window.store = store;
|
||||
|
||||
const _jsxFileName = "/Users/logan/Dev/interface/apps/chat/src/js/components/skeleton.js";
|
||||
|
||||
@ -56957,20 +57017,56 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { props, state } = this;
|
||||
/*let closure = () => {
|
||||
let aud, sep;
|
||||
let wen = Date.now();
|
||||
let uid = uuid();
|
||||
let aut = window.ship;
|
||||
|
||||
let config = props.configs[state.station];
|
||||
|
||||
aud = [props.station];
|
||||
sep = {
|
||||
lin: {
|
||||
msg: Date.now().toString(),
|
||||
pat: false
|
||||
}
|
||||
}
|
||||
|
||||
let message = {
|
||||
uid,
|
||||
aut,
|
||||
wen,
|
||||
aud,
|
||||
sep,
|
||||
};
|
||||
|
||||
props.api.hall({
|
||||
convey: [message]
|
||||
});
|
||||
|
||||
setTimeout(closure, 1000);
|
||||
};
|
||||
|
||||
closure();*/
|
||||
|
||||
|
||||
return (
|
||||
react.createElement('div', { className: "mt2 pa3 cf flex black bt" , __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 109}}
|
||||
, react.createElement('div', { className: "fl", style: { flexBasis: 35, height: 40 }, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 110}}
|
||||
, react.createElement(Sigil, { ship: window.ship, size: 32, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 111}} )
|
||||
react.createElement('div', { className: "mt2 pa3 cf flex black bt" , __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 145}}
|
||||
, react.createElement('div', { className: "fl", style: { flexBasis: 35, height: 40 }, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 146}}
|
||||
, react.createElement(Sigil, { ship: window.ship, size: 32, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 147}} )
|
||||
)
|
||||
, react.createElement('div', { className: "fr h-100 flex" , style: { flexGrow: 1, height: 40 }, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 113}}
|
||||
, react.createElement('div', { className: "fr h-100 flex" , style: { flexGrow: 1, height: 40 }, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 149}}
|
||||
, react.createElement('input', { className: "ml2 bn" ,
|
||||
style: { flexGrow: 1 },
|
||||
ref: this.textareaRef,
|
||||
placeholder: this.props.placeholder,
|
||||
value: this.state.message,
|
||||
onChange: this.messageChange, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 114}} )
|
||||
, react.createElement('div', { className: "pointer", onClick: this.messageSubmit, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 120}}
|
||||
, react.createElement(IconSend, {__self: this, __source: {fileName: _jsxFileName$9, lineNumber: 121}} )
|
||||
placeholder: props.placeholder,
|
||||
value: state.message,
|
||||
onChange: this.messageChange, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 150}} )
|
||||
, react.createElement('div', { className: "pointer", onClick: this.messageSubmit, __self: this, __source: {fileName: _jsxFileName$9, lineNumber: 156}}
|
||||
, react.createElement(IconSend, {__self: this, __source: {fileName: _jsxFileName$9, lineNumber: 157}} )
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -56987,14 +57083,47 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
station: props.match.params.ship + "/" + props.match.params.station,
|
||||
circle: props.match.params.station,
|
||||
host: props.match.params.ship,
|
||||
numPeople: 0
|
||||
numPeople: 0,
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
};
|
||||
|
||||
this.topMessage = {};
|
||||
this.buildMessage = this.buildMessage.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateNumPeople();
|
||||
this.scrollElement.scrollIntoView(false);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (!this.state.scrollLocked) {
|
||||
console.log('scroll to bottom');
|
||||
this.scrollElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
onScroll(e) {
|
||||
if (e.target.scrollTop === 0) {
|
||||
let topMessage = this.topMessage;
|
||||
|
||||
this.setState({
|
||||
numPages: this.state.numPages + 1,
|
||||
scrollLocked: true
|
||||
}, () => {
|
||||
this.topMessage[1].scrollIntoView(true);
|
||||
});
|
||||
} else if (
|
||||
(e.target.scrollHeight - Math.round(e.target.scrollTop)) ===
|
||||
e.target.clientHeight
|
||||
) {
|
||||
this.setState({
|
||||
numPages: 1,
|
||||
scrollLocked: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
@ -57012,6 +57141,7 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
this.updateReadNumber();
|
||||
this.updateNumPeople();
|
||||
this.updateNumMessagesLoaded(prevProps, prevState);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
updateReadNumber() {
|
||||
@ -57048,42 +57178,62 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
}
|
||||
}
|
||||
|
||||
buildMessage(msg) {
|
||||
buildMessage(msg, index) {
|
||||
let details = msg.printship ? null : getMessageContent(msg.gam);
|
||||
|
||||
if (msg.printship) {
|
||||
return (
|
||||
react.createElement('a', {
|
||||
className: "vanilla hoverline text-600 text-mono" ,
|
||||
href: prettyShip(msg.gam.aut)[1], __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 85}}
|
||||
href: prettyShip(msg.gam.aut)[1], __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 119}}
|
||||
, prettyShip(`~${msg.gam.aut}`)[0]
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
react.createElement(Message, { key: msg.gam.uid, msg: msg.gam, details: details, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 93}} )
|
||||
);
|
||||
|
||||
if (index % 50 === 0) {
|
||||
let pageNum = index / 50;
|
||||
return (
|
||||
react.createElement('div', { ref: el => { this.topMessage[pageNum] = el; }, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 130}}
|
||||
, react.createElement(Message, {
|
||||
key: msg.gam.uid, msg: msg.gam, details: details, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 131}} )
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
react.createElement(Message, { key: msg.gam.uid, msg: msg.gam, details: details, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 137}} )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
let messages = this.props.messages[this.state.station] || [];
|
||||
if (messages.length > 50 * state.numPages) {
|
||||
messages =
|
||||
messages.slice(messages.length - (50 * state.numPages), messages.length);
|
||||
}
|
||||
let chatMessages = messages.map(this.buildMessage);
|
||||
|
||||
return (
|
||||
react.createElement('div', { className: "h-100 w-100 overflow-hidden flex flex-column" , __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 102}}
|
||||
, react.createElement('div', { className: "pl2 pt2 bb mb3" , __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 103}}
|
||||
, react.createElement('h2', {__self: this, __source: {fileName: _jsxFileName$a, lineNumber: 104}}, this.state.circle)
|
||||
, react.createElement(ChatTabBar, { ...this.props, station: this.state.station, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 105}} )
|
||||
react.createElement('div', { className: "h-100 w-100 overflow-hidden flex flex-column" , __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 152}}
|
||||
, react.createElement('div', { className: "pl2 pt2 bb mb3" , __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 153}}
|
||||
, react.createElement('h2', {__self: this, __source: {fileName: _jsxFileName$a, lineNumber: 154}}, this.state.circle)
|
||||
, react.createElement(ChatTabBar, { ...this.props, station: this.state.station, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 155}} )
|
||||
)
|
||||
, react.createElement('div', { className: "overflow-y-scroll", style: { flexGrow: 1 }, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 107}}
|
||||
, react.createElement('div', {
|
||||
className: "overflow-y-scroll",
|
||||
style: { flexGrow: 1 },
|
||||
onScroll: this.onScroll, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 157}}
|
||||
, chatMessages
|
||||
, react.createElement('div', { ref: el => { this.scrollElement = el; }, __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 162}})
|
||||
)
|
||||
, react.createElement(ChatInput, {
|
||||
api: this.props.api,
|
||||
configs: this.props.configs,
|
||||
station: this.state.station,
|
||||
circle: this.state.circle,
|
||||
placeholder: "Message...", __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 110}} )
|
||||
placeholder: "Message...", __self: this, __source: {fileName: _jsxFileName$a, lineNumber: 164}} )
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -57545,33 +57695,32 @@ lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\
|
||||
start() {
|
||||
if (api.authTokens) {
|
||||
this.initializeChat();
|
||||
//this.setCleanupTasks();
|
||||
} else {
|
||||
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
|
||||
}
|
||||
}
|
||||
|
||||
/*setCleanupTasks() {
|
||||
window.addEventListener("beforeunload", e => {
|
||||
api.bindPaths.forEach(p => {
|
||||
this.wipeSubscription(p);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
wipeSubscription(path) {
|
||||
api.hall({
|
||||
wipe: {
|
||||
sub: [{
|
||||
hos: api.authTokens.ship,
|
||||
pax: path
|
||||
}]
|
||||
}
|
||||
});
|
||||
}*/
|
||||
|
||||
initializeChat() {
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
|
||||
if (store.state.local) {
|
||||
let path = [];
|
||||
let msg = Object.keys(store.state.messages);
|
||||
for (let i = 0; i < msg.length; i++) {
|
||||
let cir = msg[i];
|
||||
let len = store.state.messages[cir].length;
|
||||
path.push(`${cir}/${len}`);
|
||||
}
|
||||
path = path.join('/');
|
||||
|
||||
api.bind(`/primary/${path}`, 'PUT', api.authTokens.ship, 'chat',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this));
|
||||
} else {
|
||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this));
|
||||
}
|
||||
|
||||
api.bind('/updates', 'PUT', api.authTokens.ship, 'chat',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this));
|
||||
}
|
||||
|
@ -16,8 +16,9 @@
|
||||
::
|
||||
+$ diff
|
||||
$% [%hall-rumor rumor:hall]
|
||||
[%chat-streams streams]
|
||||
[%chat-initial streams]
|
||||
[%chat-update update]
|
||||
[%chat-config streams]
|
||||
==
|
||||
::
|
||||
+$ poke
|
||||
@ -49,6 +50,7 @@
|
||||
+$ update
|
||||
$% [%inbox con=config:hall]
|
||||
[%message cir=circle:hall env=envelope:hall]
|
||||
[%messages cir=circle:hall start=@ud end=@ud env=(list envelope:hall)]
|
||||
[%config cir=circle:hall con=config:hall]
|
||||
[%circles cir=(set name:hall)]
|
||||
[%peers cir=circle:hall per=(set @p)]
|
||||
|
@ -24,19 +24,20 @@
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
?~(con ~ (conf:enjs:hall-json u.con))
|
||||
::
|
||||
:- %messages
|
||||
%- pairs
|
||||
%+ turn ~(tap by messages.str)
|
||||
|= [cir=circle:hall lis=(list envelope:hall)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
[%a (turn lis enve:enjs:hall-json)]
|
||||
::
|
||||
:- %circles :- %a
|
||||
%+ turn ~(tap in circles.str)
|
||||
|= nom=name:hall
|
||||
[%s nom]
|
||||
::
|
||||
:- %peers
|
||||
%- pairs
|
||||
%+ turn ~(tap by peers.str)
|
||||
|= [cir=circle:hall per=(set @p)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
[%a (turn ~(tap in per) ship)]
|
||||
::
|
||||
==
|
||||
--
|
||||
::
|
56
apps/chat/urbit/mar/chat/initial.hoon
Normal file
56
apps/chat/urbit/mar/chat/initial.hoon
Normal file
@ -0,0 +1,56 @@
|
||||
::
|
||||
::
|
||||
/? 309
|
||||
::
|
||||
/- hall
|
||||
/+ chat, hall-json
|
||||
::
|
||||
|_ str=streams:chat
|
||||
++ grow
|
||||
|%
|
||||
++ json
|
||||
=, enjs:format
|
||||
^- ^json
|
||||
%+ frond %initial
|
||||
%- pairs
|
||||
:~
|
||||
::
|
||||
[%inbox (conf:enjs:hall-json inbox.str)]
|
||||
::
|
||||
:- %configs
|
||||
%- pairs
|
||||
%+ turn ~(tap by configs.str)
|
||||
|= [cir=circle:hall con=(unit config:hall)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
?~(con ~ (conf:enjs:hall-json u.con))
|
||||
::
|
||||
:- %messages
|
||||
%- pairs
|
||||
%+ turn ~(tap by messages.str)
|
||||
|= [cir=circle:hall lis=(list envelope:hall)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
[%a (turn lis enve:enjs:hall-json)]
|
||||
::
|
||||
:- %circles :- %a
|
||||
%+ turn ~(tap in circles.str)
|
||||
|= nom=name:hall
|
||||
[%s nom]
|
||||
::
|
||||
:- %peers
|
||||
%- pairs
|
||||
%+ turn ~(tap by peers.str)
|
||||
|= [cir=circle:hall per=(set @p)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
[%a (turn ~(tap in per) ship)]
|
||||
::
|
||||
==
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun streams:chat
|
||||
--
|
||||
--
|
@ -30,6 +30,18 @@
|
||||
[%envelope (enve:enjs:hall-json env.upd)]
|
||||
==
|
||||
::
|
||||
:: %messages
|
||||
?: =(%messages -.upd)
|
||||
?> ?=(%messages -.upd)
|
||||
:- %messages
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%start (numb start.upd)]
|
||||
[%end (numb end.upd)]
|
||||
[%envelopes [%a (turn env.upd enve:enjs:hall-json)]]
|
||||
==
|
||||
::
|
||||
:: %config
|
||||
?: =(%config -.upd)
|
||||
?> ?=(%config -.upd)
|
||||
@ -39,6 +51,8 @@
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%config (conf:enjs:hall-json con.upd)]
|
||||
==
|
||||
::
|
||||
:: %circles
|
||||
?: =(%circles -.upd)
|
||||
?> ?=(%circles -.upd)
|
||||
:- %circles
|
||||
@ -51,6 +65,16 @@
|
||||
[%s nom]
|
||||
==
|
||||
::
|
||||
:: %peers
|
||||
?: =(%peers -.upd)
|
||||
?> ?=(%peers -.upd)
|
||||
:- %peers
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%peers [%a (turn ~(tap in per.upd) ship:enjs:format)]]
|
||||
==
|
||||
::
|
||||
:: %noop
|
||||
[*@t *^json]
|
||||
==
|
||||
|
Loading…
Reference in New Issue
Block a user