Fossil SCM
chat: initial implementation of marking @NAME references to make them more visible.
Commit
ee53e449a5086a6962a28be58dc67e5701b152851c9fc92daa77fa9dd4bcc428
Parent
ee6c70817d2f1bc…
1 file changed
+55
-18
+55
-18
| --- tools/chat.tcl | ||
| +++ tools/chat.tcl | ||
| @@ -102,10 +102,14 @@ | ||
| 102 | 102 | align-items: center; |
| 103 | 103 | } |
| 104 | 104 | \#chat-input-file > input { |
| 105 | 105 | flex: 1 0 auto; |
| 106 | 106 | } |
| 107 | +span.at-name { /* for @USERNAME references */ | |
| 108 | + text-decoration: underline; | |
| 109 | + font-weight: bold; | |
| 110 | +} | |
| 107 | 111 | </style> |
| 108 | 112 | } |
| 109 | 113 | set nonce [wapp-param FOSSIL_NONCE] |
| 110 | 114 | set submiturl [wapp-param SCRIPT_NAME]/send |
| 111 | 115 | set pollurl [wapp-param SCRIPT_NAME]/poll |
| @@ -127,52 +131,85 @@ | ||
| 127 | 131 | } |
| 128 | 132 | form.msg.value = ""; |
| 129 | 133 | form.file.value = ""; |
| 130 | 134 | form.msg.focus(); |
| 131 | 135 | }); |
| 136 | + const rxUrl = /\\b(?:https?|ftp):\\/\\/\[a-z0-9-+&@\#\\/%?=~_|!:,.;]*\[a-z0-9-+&@\#\\/%=~_|]/gim; | |
| 137 | + const rxAtName = /@\\w+/gmi; | |
| 138 | + // ^^^ achtung, extra backslashes needed for the outer TCL. | |
| 132 | 139 | // Converts a message string to a message-containing DOM element |
| 133 | 140 | // and returns that element, which may contain child elements. |
| 134 | 141 | const messageToDOM = function f(str){ |
| 135 | 142 | "use strict"; |
| 136 | 143 | if(!f.rxUrl){ |
| 137 | - f.rxUrl = /\\b(?:https?|ftp):\\/\\/\[a-z0-9-+&@\#\\/%?=~_|!:,.;]*\[a-z0-9-+&@\#\\/%=~_|]/gim; | |
| 138 | - // ^^^ achtung, extra backslashes needed for the outer TCL. | |
| 144 | + f.rxUrl = rxUrl; | |
| 145 | + f.rxAt = rxAtName; | |
| 146 | + f.rxNS = /\\S/; | |
| 139 | 147 | f.ce = (T)=>document.createElement(T); |
| 140 | 148 | f.ct = (T)=>document.createTextNode(T); |
| 141 | - f.replaceUrls = function ff(sub, off, whole){ | |
| 142 | - if(off > ff.prevStart){ | |
| 143 | - f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, off-1)+' '); | |
| 149 | + f.replaceUrls = function ff(sub, offset, whole){ | |
| 150 | + if(offset > ff.prevStart){ | |
| 151 | + f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' '); | |
| 144 | 152 | } |
| 145 | 153 | const a = f.ce('a'); |
| 146 | 154 | a.setAttribute('href',sub); |
| 147 | 155 | a.setAttribute('target','_blank'); |
| 148 | 156 | a.appendChild(f.ct(sub)); |
| 149 | 157 | f.accum.push(a); |
| 150 | - ff.prevStart = off + sub.length + 1; | |
| 151 | - return sub; | |
| 158 | + ff.prevStart = offset + sub.length + 1; | |
| 159 | + }; | |
| 160 | + f.replaceAtName = function ff(sub, offset,whole){ | |
| 161 | + if(offset > ff.prevStart){ | |
| 162 | + ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' '); | |
| 163 | + }else if(offset && f.rxNS.test(whole[offset-1])){ | |
| 164 | + // Sigh: https://stackoverflow.com/questions/52655367 | |
| 165 | + ff.accum.push(sub); | |
| 166 | + return; | |
| 167 | + } | |
| 168 | + const e = f.ce('span'); | |
| 169 | + e.classList.add('at-name'); | |
| 170 | + e.appendChild(f.ct(sub)); | |
| 171 | + ff.accum.push(e); | |
| 172 | + ff.prevStart = offset + sub.length + 1; | |
| 152 | 173 | }; |
| 153 | 174 | } |
| 154 | 175 | f.accum = []; // accumulate strings and DOM elements here. |
| 155 | - f.rxUrl.lastIndex = 0; // reset regex cursor | |
| 156 | - f.replaceUrls.prevStart = 0; | |
| 176 | + f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor | |
| 157 | 177 | str.replace(f.rxUrl, f.replaceUrls); |
| 178 | + // Push remaining non-URL part of the string to the queue... | |
| 158 | 179 | if(f.replaceUrls.prevStart < str.length){ |
| 159 | 180 | f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart)); |
| 160 | 181 | } |
| 182 | + // Pass 2: process @NAME references... | |
| 183 | + // TODO: only match NAME if it's the name of a currently participating | |
| 184 | + // user. Add a second class if NAME == current user, and style that one | |
| 185 | + // differently so that people can more easily see when they're spoken to. | |
| 186 | + const accum2 = f.replaceAtName.accum = []; | |
| 187 | + //console.debug("f.accum =",f.accum); | |
| 188 | + f.accum.forEach(function(v){ | |
| 189 | + //console.debug("v =",v); | |
| 190 | + if('string'===typeof v){ | |
| 191 | + f.rxAt.lastIndex = f.replaceAtName.prevStart = 0; | |
| 192 | + v.replace(f.rxAt, f.replaceAtName); | |
| 193 | + if(f.replaceAtName.prevStart < v.length){ | |
| 194 | + accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart)); | |
| 195 | + } | |
| 196 | + }else{ | |
| 197 | + accum2.push(v); | |
| 198 | + } | |
| 199 | + //console.debug("accum2 =",accum2); | |
| 200 | + }); | |
| 201 | + delete f.accum; | |
| 202 | + //console.debug("accum2 =",accum2); | |
| 161 | 203 | const span = f.ce('span'); |
| 162 | - f.accum.forEach(function(e){ | |
| 163 | - // append accumulated strings/DOM elements to target element | |
| 204 | + accum2.forEach(function(e){ | |
| 164 | 205 | if('string'===typeof e) e = f.ct(e); |
| 165 | 206 | span.appendChild(e); |
| 166 | 207 | }); |
| 167 | - delete f.accum; | |
| 168 | - // TODO: replace @WORD refs with <span class='at-me'>@WORD</span>, but | |
| 169 | - // only when WORD==current user name. That requires a separate pass | |
| 170 | - // over the remaining STRING entries and a separate array to accumulate | |
| 171 | - // the results to. | |
| 172 | - return span; | |
| 173 | - }; | |
| 208 | + //console.debug("span =",span.innerHTML); | |
| 209 | + return span; | |
| 210 | + }/*end messageToDOM()*/; | |
| 174 | 211 | function newcontent(jx){ |
| 175 | 212 | var tab = document.getElementById("dialog"); |
| 176 | 213 | var i; |
| 177 | 214 | for(i=0; i<jx.msgs.length; ++i){ |
| 178 | 215 | let m = jx.msgs[i]; |
| 179 | 216 |
| --- tools/chat.tcl | |
| +++ tools/chat.tcl | |
| @@ -102,10 +102,14 @@ | |
| 102 | align-items: center; |
| 103 | } |
| 104 | \#chat-input-file > input { |
| 105 | flex: 1 0 auto; |
| 106 | } |
| 107 | </style> |
| 108 | } |
| 109 | set nonce [wapp-param FOSSIL_NONCE] |
| 110 | set submiturl [wapp-param SCRIPT_NAME]/send |
| 111 | set pollurl [wapp-param SCRIPT_NAME]/poll |
| @@ -127,52 +131,85 @@ | |
| 127 | } |
| 128 | form.msg.value = ""; |
| 129 | form.file.value = ""; |
| 130 | form.msg.focus(); |
| 131 | }); |
| 132 | // Converts a message string to a message-containing DOM element |
| 133 | // and returns that element, which may contain child elements. |
| 134 | const messageToDOM = function f(str){ |
| 135 | "use strict"; |
| 136 | if(!f.rxUrl){ |
| 137 | f.rxUrl = /\\b(?:https?|ftp):\\/\\/\[a-z0-9-+&@\#\\/%?=~_|!:,.;]*\[a-z0-9-+&@\#\\/%=~_|]/gim; |
| 138 | // ^^^ achtung, extra backslashes needed for the outer TCL. |
| 139 | f.ce = (T)=>document.createElement(T); |
| 140 | f.ct = (T)=>document.createTextNode(T); |
| 141 | f.replaceUrls = function ff(sub, off, whole){ |
| 142 | if(off > ff.prevStart){ |
| 143 | f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, off-1)+' '); |
| 144 | } |
| 145 | const a = f.ce('a'); |
| 146 | a.setAttribute('href',sub); |
| 147 | a.setAttribute('target','_blank'); |
| 148 | a.appendChild(f.ct(sub)); |
| 149 | f.accum.push(a); |
| 150 | ff.prevStart = off + sub.length + 1; |
| 151 | return sub; |
| 152 | }; |
| 153 | } |
| 154 | f.accum = []; // accumulate strings and DOM elements here. |
| 155 | f.rxUrl.lastIndex = 0; // reset regex cursor |
| 156 | f.replaceUrls.prevStart = 0; |
| 157 | str.replace(f.rxUrl, f.replaceUrls); |
| 158 | if(f.replaceUrls.prevStart < str.length){ |
| 159 | f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart)); |
| 160 | } |
| 161 | const span = f.ce('span'); |
| 162 | f.accum.forEach(function(e){ |
| 163 | // append accumulated strings/DOM elements to target element |
| 164 | if('string'===typeof e) e = f.ct(e); |
| 165 | span.appendChild(e); |
| 166 | }); |
| 167 | delete f.accum; |
| 168 | // TODO: replace @WORD refs with <span class='at-me'>@WORD</span>, but |
| 169 | // only when WORD==current user name. That requires a separate pass |
| 170 | // over the remaining STRING entries and a separate array to accumulate |
| 171 | // the results to. |
| 172 | return span; |
| 173 | }; |
| 174 | function newcontent(jx){ |
| 175 | var tab = document.getElementById("dialog"); |
| 176 | var i; |
| 177 | for(i=0; i<jx.msgs.length; ++i){ |
| 178 | let m = jx.msgs[i]; |
| 179 |
| --- tools/chat.tcl | |
| +++ tools/chat.tcl | |
| @@ -102,10 +102,14 @@ | |
| 102 | align-items: center; |
| 103 | } |
| 104 | \#chat-input-file > input { |
| 105 | flex: 1 0 auto; |
| 106 | } |
| 107 | span.at-name { /* for @USERNAME references */ |
| 108 | text-decoration: underline; |
| 109 | font-weight: bold; |
| 110 | } |
| 111 | </style> |
| 112 | } |
| 113 | set nonce [wapp-param FOSSIL_NONCE] |
| 114 | set submiturl [wapp-param SCRIPT_NAME]/send |
| 115 | set pollurl [wapp-param SCRIPT_NAME]/poll |
| @@ -127,52 +131,85 @@ | |
| 131 | } |
| 132 | form.msg.value = ""; |
| 133 | form.file.value = ""; |
| 134 | form.msg.focus(); |
| 135 | }); |
| 136 | const rxUrl = /\\b(?:https?|ftp):\\/\\/\[a-z0-9-+&@\#\\/%?=~_|!:,.;]*\[a-z0-9-+&@\#\\/%=~_|]/gim; |
| 137 | const rxAtName = /@\\w+/gmi; |
| 138 | // ^^^ achtung, extra backslashes needed for the outer TCL. |
| 139 | // Converts a message string to a message-containing DOM element |
| 140 | // and returns that element, which may contain child elements. |
| 141 | const messageToDOM = function f(str){ |
| 142 | "use strict"; |
| 143 | if(!f.rxUrl){ |
| 144 | f.rxUrl = rxUrl; |
| 145 | f.rxAt = rxAtName; |
| 146 | f.rxNS = /\\S/; |
| 147 | f.ce = (T)=>document.createElement(T); |
| 148 | f.ct = (T)=>document.createTextNode(T); |
| 149 | f.replaceUrls = function ff(sub, offset, whole){ |
| 150 | if(offset > ff.prevStart){ |
| 151 | f.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' '); |
| 152 | } |
| 153 | const a = f.ce('a'); |
| 154 | a.setAttribute('href',sub); |
| 155 | a.setAttribute('target','_blank'); |
| 156 | a.appendChild(f.ct(sub)); |
| 157 | f.accum.push(a); |
| 158 | ff.prevStart = offset + sub.length + 1; |
| 159 | }; |
| 160 | f.replaceAtName = function ff(sub, offset,whole){ |
| 161 | if(offset > ff.prevStart){ |
| 162 | ff.accum.push((ff.prevStart?' ':'')+whole.substring(ff.prevStart, offset-1)+' '); |
| 163 | }else if(offset && f.rxNS.test(whole[offset-1])){ |
| 164 | // Sigh: https://stackoverflow.com/questions/52655367 |
| 165 | ff.accum.push(sub); |
| 166 | return; |
| 167 | } |
| 168 | const e = f.ce('span'); |
| 169 | e.classList.add('at-name'); |
| 170 | e.appendChild(f.ct(sub)); |
| 171 | ff.accum.push(e); |
| 172 | ff.prevStart = offset + sub.length + 1; |
| 173 | }; |
| 174 | } |
| 175 | f.accum = []; // accumulate strings and DOM elements here. |
| 176 | f.rxUrl.lastIndex = f.replaceUrls.prevStart = 0; // reset regex cursor |
| 177 | str.replace(f.rxUrl, f.replaceUrls); |
| 178 | // Push remaining non-URL part of the string to the queue... |
| 179 | if(f.replaceUrls.prevStart < str.length){ |
| 180 | f.accum.push((f.replaceUrls.prevStart?' ':'')+str.substring(f.replaceUrls.prevStart)); |
| 181 | } |
| 182 | // Pass 2: process @NAME references... |
| 183 | // TODO: only match NAME if it's the name of a currently participating |
| 184 | // user. Add a second class if NAME == current user, and style that one |
| 185 | // differently so that people can more easily see when they're spoken to. |
| 186 | const accum2 = f.replaceAtName.accum = []; |
| 187 | //console.debug("f.accum =",f.accum); |
| 188 | f.accum.forEach(function(v){ |
| 189 | //console.debug("v =",v); |
| 190 | if('string'===typeof v){ |
| 191 | f.rxAt.lastIndex = f.replaceAtName.prevStart = 0; |
| 192 | v.replace(f.rxAt, f.replaceAtName); |
| 193 | if(f.replaceAtName.prevStart < v.length){ |
| 194 | accum2.push((f.replaceAtName.prevStart?' ':'')+v.substring(f.replaceAtName.prevStart)); |
| 195 | } |
| 196 | }else{ |
| 197 | accum2.push(v); |
| 198 | } |
| 199 | //console.debug("accum2 =",accum2); |
| 200 | }); |
| 201 | delete f.accum; |
| 202 | //console.debug("accum2 =",accum2); |
| 203 | const span = f.ce('span'); |
| 204 | accum2.forEach(function(e){ |
| 205 | if('string'===typeof e) e = f.ct(e); |
| 206 | span.appendChild(e); |
| 207 | }); |
| 208 | //console.debug("span =",span.innerHTML); |
| 209 | return span; |
| 210 | }/*end messageToDOM()*/; |
| 211 | function newcontent(jx){ |
| 212 | var tab = document.getElementById("dialog"); |
| 213 | var i; |
| 214 | for(i=0; i<jx.msgs.length; ++i){ |
| 215 | let m = jx.msgs[i]; |
| 216 |