From bc4f1968f2e64716b5109f2f0c5bd288b762e503 Mon Sep 17 00:00:00 2001 From: Logan Allen Date: Fri, 31 May 2019 11:32:13 -0700 Subject: [PATCH] Infinite scrolling works, localStorage works, progressive loading of information works, new subscription pattern works --- apps/chat/src/js/api.js | 9 + apps/chat/src/js/components/chat.js | 66 ++++- apps/chat/src/js/components/lib/chat-input.js | 40 ++- .../src/js/reducers/{chat.js => config.js} | 3 +- apps/chat/src/js/reducers/initial.js | 15 ++ apps/chat/src/js/reducers/update.js | 17 ++ apps/chat/src/js/store.js | 37 ++- apps/chat/src/js/subscription.js | 41 ++- apps/chat/urbit/app/chat.hoon | 159 +++++++++-- apps/chat/urbit/app/chat/js/index.js | 255 ++++++++++++++---- apps/chat/urbit/lib/chat.hoon | 4 +- .../mar/chat/{streams.hoon => config.hoon} | 17 +- apps/chat/urbit/mar/chat/initial.hoon | 56 ++++ apps/chat/urbit/mar/chat/update.hoon | 24 ++ 14 files changed, 611 insertions(+), 132 deletions(-) rename apps/chat/src/js/reducers/{chat.js => config.js} (77%) create mode 100644 apps/chat/src/js/reducers/initial.js rename apps/chat/urbit/mar/chat/{streams.hoon => config.hoon} (82%) create mode 100644 apps/chat/urbit/mar/chat/initial.hoon diff --git a/apps/chat/src/js/api.js b/apps/chat/src/js/api.js index 4dc4092c9..1d6ac9992 100644 --- a/apps/chat/src/js/api.js +++ b/apps/chat/src/js/api.js @@ -51,6 +51,15 @@ class UrbitApi { }); } + notify(aud, bool) { + this.hall({ + notify: { + aud, + pes: !!bool ? 'hear' : 'gone' + } + }); + } + permit(cir, aud, message) { this.hall({ permit: { diff --git a/apps/chat/src/js/components/chat.js b/apps/chat/src/js/components/chat.js index 38719cfdf..e1452c672 100644 --- a/apps/chat/src/js/components/chat.js +++ b/apps/chat/src/js/components/chat.js @@ -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 { ); } - return ( - - ); + + if (index % 50 === 0) { + let pageNum = index / 50; + return ( +
{ this.topMessage[pageNum] = el; }}> + +
+ ); + } else { + return ( + + ); + } } 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 {

{this.state.circle}

-
+
{chatMessages} +
{ this.scrollElement = el; }}>
{ + 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 (
@@ -114,8 +150,8 @@ export class ChatInput extends Component {
diff --git a/apps/chat/src/js/reducers/chat.js b/apps/chat/src/js/reducers/config.js similarity index 77% rename from apps/chat/src/js/reducers/chat.js rename to apps/chat/src/js/reducers/config.js index b427f8267..7e3397c2d 100644 --- a/apps/chat/src/js/reducers/chat.js +++ b/apps/chat/src/js/reducers/config.js @@ -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; diff --git a/apps/chat/src/js/reducers/initial.js b/apps/chat/src/js/reducers/initial.js new file mode 100644 index 000000000..769dab120 --- /dev/null +++ b/apps/chat/src/js/reducers/initial.js @@ -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; + } + } +} + diff --git a/apps/chat/src/js/reducers/update.js b/apps/chat/src/js/reducers/update.js index 60a3f8e66..42c47ef36 100644 --- a/apps/chat/src/js/reducers/update.js +++ b/apps/chat/src/js/reducers/update.js @@ -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; diff --git a/apps/chat/src/js/store.js b/apps/chat/src/js/store.js index eb86f5acc..e677888d9 100644 --- a/apps/chat/src/js/store.js +++ b/apps/chat/src/js/store.js @@ -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; diff --git a/apps/chat/src/js/subscription.js b/apps/chat/src/js/subscription.js index 33c27ac72..43a8c4469 100644 --- a/apps/chat/src/js/subscription.js +++ b/apps/chat/src/js/subscription.js @@ -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)); } diff --git a/apps/chat/urbit/app/chat.hoon b/apps/chat/urbit/app/chat.hoon index 81b6697af..ec272761b 100644 --- a/apps/chat/urbit/app/chat.hoon +++ b/apps/chat/urbit/app/chat.hoon @@ -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]~ :: diff --git a/apps/chat/urbit/app/chat/js/index.js b/apps/chat/urbit/app/chat/js/index.js index 382746e47..ed0d6d863 100644 --- a/apps/chat/urbit/app/chat/js/index.js +++ b/apps/chat/urbit/app/chat/js/index.js @@ -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)); } diff --git a/apps/chat/urbit/lib/chat.hoon b/apps/chat/urbit/lib/chat.hoon index 588cfca7b..a724bcf3c 100644 --- a/apps/chat/urbit/lib/chat.hoon +++ b/apps/chat/urbit/lib/chat.hoon @@ -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)] diff --git a/apps/chat/urbit/mar/chat/streams.hoon b/apps/chat/urbit/mar/chat/config.hoon similarity index 82% rename from apps/chat/urbit/mar/chat/streams.hoon rename to apps/chat/urbit/mar/chat/config.hoon index 2b33cb339..539091be2 100644 --- a/apps/chat/urbit/mar/chat/streams.hoon +++ b/apps/chat/urbit/mar/chat/config.hoon @@ -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)] + :: == -- :: diff --git a/apps/chat/urbit/mar/chat/initial.hoon b/apps/chat/urbit/mar/chat/initial.hoon new file mode 100644 index 000000000..99c7f8dc5 --- /dev/null +++ b/apps/chat/urbit/mar/chat/initial.hoon @@ -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 + -- +-- diff --git a/apps/chat/urbit/mar/chat/update.hoon b/apps/chat/urbit/mar/chat/update.hoon index 24ed4aa84..23a5c172a 100644 --- a/apps/chat/urbit/mar/chat/update.hoon +++ b/apps/chat/urbit/mar/chat/update.hoon @@ -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] ==