diff options
-rw-r--r-- | README.adoc | 2 | ||||
-rw-r--r-- | xC-proto | 18 | ||||
-rw-r--r-- | xC.c | 13 | ||||
-rw-r--r-- | xP/public/xP.css | 13 | ||||
-rw-r--r-- | xP/public/xP.js | 87 |
5 files changed, 114 insertions, 19 deletions
diff --git a/README.adoc b/README.adoc index 67a7ea5..6d61bcc 100644 --- a/README.adoc +++ b/README.adoc @@ -26,7 +26,7 @@ As a unique bonus, you can launch a full text editor from within. xP -- The web frontend for 'xC', making use of its networked relay interface. -So far it's quite basic, yet usable. +So far it's somewhat basic, yet usable. xF -- @@ -3,6 +3,8 @@ const VERSION = 1; // From the frontend to the relay. struct CommandMessage { + // The command sequence number will be repeated in responses + // in the respective fields. u32 command_seq; union CommandData switch (enum Command { HELLO, @@ -15,6 +17,9 @@ struct CommandMessage { } command) { case HELLO: u32 version; + // If the version check succeeds, the client will receive + // an initial stream of BUFFER_UPDATE, BUFFER_LINE, + // and finally a BUFFER_ACTIVATE message. case PING: void; case ACTIVE: @@ -51,6 +56,15 @@ struct EventMessage { void; case BUFFER_UPDATE: string buffer_name; + // These are cumulative, even for lines flushed out from buffers. + // Updates to these values aren't broadcasted, thus handle: + // - BUFFER_LINE by bumping/setting them as appropriate, + // - BUFFER_ACTIVATE by clearing them for the previous buffer + // (this way, they can be used to mark unread messages). + // Any updates received after the initial sync should be ignored. + u32 new_messages; + u32 new_unimportant_messages; + bool highlighted; case BUFFER_RENAME: string buffer_name; string new; @@ -60,6 +74,7 @@ struct EventMessage { string buffer_name; case BUFFER_LINE: string buffer_name; + // Whether the line should also be displayed in the active buffer. bool leak_to_active; bool is_unimportant; bool is_highlight; @@ -106,7 +121,8 @@ struct EventMessage { case BUFFER_CLEAR: string buffer_name; - // Restriction: command_seq is strictly increasing, across both of these. + // Restriction: command_seq strictly follows the sequence received + // by the relay, across both of these replies. case ERROR: u32 command_seq; string error; @@ -3068,6 +3068,11 @@ relay_prepare_buffer_update (struct app_context *ctx, struct buffer *buffer) struct relay_event_data_buffer_update *e = &m->data.buffer_update; e->event = RELAY_EVENT_BUFFER_UPDATE; e->buffer_name = str_from_cstr (buffer->name); + e->new_messages = MIN (UINT32_MAX, + buffer->new_messages_count - buffer->new_unimportant_count); + e->new_unimportant_messages = MIN (UINT32_MAX, + buffer->new_unimportant_count); + e->highlighted = buffer->highlighted; } static void @@ -5015,8 +5020,16 @@ buffer_merge (struct app_context *ctx, buffer->lines_count += n; // And since there is no log_*() call, send them to relays manually + buffer->highlighted |= merged->highlighted; LIST_FOR_EACH (struct buffer_line, line, start) { + if (buffer->new_messages_count) + { + buffer->new_messages_count++; + if (line->flags & BUFFER_LINE_UNIMPORTANT) + buffer->new_unimportant_count++; + } + relay_prepare_buffer_line (ctx, buffer, line, false); relay_broadcast (ctx); } diff --git a/xP/public/xP.css b/xP/public/xP.css index a13afc6..81025c9 100644 --- a/xP/public/xP.css +++ b/xP/public/xP.css @@ -58,7 +58,13 @@ body { padding: .05rem .3rem; cursor: default; } -.item.active { +.item.highlighted { + color: #ff5f00; +} +.item.activity { + font-weight: bold; +} +.item.current { font-style: italic; background: #f8f8f8; border-top: 1px solid #eee; @@ -97,6 +103,11 @@ body { grid-column: span 2; font-weight: bold; } +.unread { + height: 1px; + grid-column: span 2; + background: #ff5f00; +} .time { padding: .1rem .3rem; background: #f8f8f8; diff --git a/xP/public/xP.js b/xP/public/xP.js index a941752..42a7a55 100644 --- a/xP/public/xP.js +++ b/xP/public/xP.js @@ -156,10 +156,13 @@ rpc.addEventListener('close', event => { rpc.addEventListener('BufferUpdate', event => { let e = event.detail, b = buffers.get(e.bufferName) if (b === undefined) { - b = {lines: []} - buffers.set(e.bufferName, b) + buffers.set(e.bufferName, { + lines: [], + newMessages: e.newMessages, + newUnimportantMessages: e.newUnimportantMessages, + highlighted: e.highlighted, + }) } - // TODO: Update any buffer properties. }) rpc.addEventListener('BufferRename', event => { @@ -174,8 +177,14 @@ rpc.addEventListener('BufferRemove', event => { }) rpc.addEventListener('BufferActivate', event => { - let e = event.detail, b = buffers.get(e.bufferName) let old = buffers.get(bufferCurrent) + if (old !== undefined) { + old.newMessages = 0 + old.newUnimportantMessages = 0 + old.highlighted = false + } + + let e = event.detail, b = buffers.get(e.bufferName) bufferCurrent = e.bufferName bufferLog = undefined bufferAutoscroll = true @@ -195,12 +204,41 @@ rpc.addEventListener('BufferActivate', event => { }) rpc.addEventListener('BufferLine', event => { - let e = event.detail, b = buffers.get(e.bufferName), - line = {when: e.when, rendition: e.rendition, items: e.items} - if (b !== undefined) - b.lines.push({...line}) - if (e.leakToActive && (b = buffers.get(bufferCurrent)) !== undefined) - b.lines.push({leaked: true, ...line}) + let e = event.detail, b = buffers.get(e.bufferName), line = {...e} + delete line.event + delete line.leakToActive + if (b === undefined) + return + + // Initial sync: skip all other processing, let highlights be. + if (bufferCurrent === undefined) { + b.lines.push(line) + return + } + + let visible = e.bufferName == bufferCurrent || e.leakToActive + b.lines.push({...line}) + if (!visible || b.newMessages || b.newUnimportantMessages) { + if (line.isUnimportant) + b.newUnimportantMessages++ + else + b.newMessages++ + } + if (e.leakToActive) { + let bc = buffers.get(bufferCurrent) + bc.lines.push({...line, leaked: true}) + if (bc.newMessages || bc.newUnimportantMessages) { + if (line.isUnimportant) + bc.newUnimportantMessages++ + else + bc.newMessages++ + } + } + + // TODO: Find some way of highlighting the tab in a browser. + // TODO: Also highlight on unseen private messages, like xC does. + if (!visible && line.isHighlight) + b.highlighted = true }) rpc.addEventListener('BufferClear', event => { @@ -267,10 +305,21 @@ let BufferList = { view: vnode => { let items = Array.from(buffers, ([name, b]) => { - let attrs = {onclick: event => BufferList.activate(name)} - if (name == bufferCurrent) - attrs.class = 'active' - return m('.item', attrs, name) + let classes = [], displayName = name + if (name == bufferCurrent) { + classes.push('current') + } else { + if (b.highlighted) + classes.push('highlighted') + if (b.newMessages) { + classes.push('activity') + displayName += ` (${b.newMessages})` + } + } + return m('.item', { + onclick: event => BufferList.activate(name), + class: classes.join(' '), + }, displayName) }) return m('.list', {}, items) }, @@ -387,7 +436,9 @@ let Buffer = { return let lastDateMark = undefined - b.lines.forEach(line => { + let markBefore = b.lines.length + - b.newMessages - b.newUnimportantMessages + b.lines.forEach((line, i) => { let date = new Date(line.when) let dateMark = date.toLocaleDateString() if (dateMark !== lastDateMark) { @@ -395,16 +446,20 @@ let Buffer = { lastDateMark = dateMark } + if (i == markBefore) + lines.push(m('.unread')) + let attrs = {} if (line.leaked) attrs.class = 'leaked' + // TODO: Make use of isUnimportant. lines.push(m('.time', {...attrs}, date.toLocaleTimeString())) lines.push(m(Content, {...attrs}, line)) }) let dateMark = new Date().toLocaleDateString() - if (dateMark !== lastDateMark) + if (dateMark !== lastDateMark && lastDateMark !== undefined) lines.push(m('.date', {}, dateMark)) return m('.buffer', {}, lines) }, |