Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | When chat view is filtered on a single user, the per-message popup now offers the option to jump to that message in the larger unfiltered context. When toggling the active user timestamps on, also toggle the active user setting on if it's not already on. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | chat-user-filter |
| Files: | files | file ages | folders |
| SHA3-256: |
5aac6ae058f9d777664a83e26cbd7740 |
| User & Date: | stephan 2021-09-24 08:37:42.050 |
Context
|
2021-09-24
| ||
| 09:29 | Changed the "message in context" animation to something more eye-catching and less stuttery. ... (check-in: fc27d6a333 user: stephan tags: chat-user-filter) | |
| 08:37 | When chat view is filtered on a single user, the per-message popup now offers the option to jump to that message in the larger unfiltered context. When toggling the active user timestamps on, also toggle the active user setting on if it's not already on. ... (check-in: 5aac6ae058 user: stephan tags: chat-user-filter) | |
| 07:16 | Added a description of the user activity list to www/chat.md. ... (check-in: d046ab687d user: stephan tags: chat-user-filter) | |
Changes
Changes to src/chat.c.
| ︙ | ︙ | |||
180 181 182 183 184 185 186 | @ </div> @ <input type="file" name="file" id="chat-input-file"> @ </div> @ <div id="chat-drop-details"></div> @ </div> @ </div> @ <div id='chat-user-list-wrapper' class='hidden'> | | | 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | @ </div> @ <input type="file" name="file" id="chat-input-file"> @ </div> @ <div id="chat-drop-details"></div> @ </div> @ </div> @ <div id='chat-user-list-wrapper' class='hidden'> @ <legend>Active users (sorted by last message time)</legend> @ <div id='chat-user-list'> @ <div class='help-buttonlet'> @ Users who have messages in the currently-loaded list.<br> @ Tap a user name to filter messages on that user and @ tap again to clear the filter. @ </div> @ </div> |
| ︙ | ︙ |
Changes to src/chat.js.
| ︙ | ︙ | |||
899 900 901 902 903 904 905 |
this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
/*if(eXFrom){
eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
}*/
return this;
},
/* Event handler for clicking .message-user elements to show their
| | | | 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 |
this.e.tab.firstElementChild.addEventListener('click', this._handleLegendClicked, false);
/*if(eXFrom){
eXFrom.addEventListener('click', ()=>this.e.tab.click(), false);
}*/
return this;
},
/* Event handler for clicking .message-user elements to show their
timestamps and a set of actions. */
_handleLegendClicked: function f(ev){
if(!f.popup){
/* "Popup" widget */
f.popup = {
e: D.addClass(D.div(), 'chat-message-popup'),
refresh:function(){
const eMsg = this.$eMsg/*.message-widget element*/;
if(!eMsg) return;
D.clearElement(this.e);
const d = new Date(eMsg.dataset.timestamp);
|
| ︙ | ︙ | |||
971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 |
D.a(F.repoUrl('timeline',{
u: eMsg.dataset.xfrom,
y: 'a'
}), "User's Timeline"),
'target', '_blank'
);
D.append(toolbar2, timelineLink);
}
const tab = eMsg.querySelector('.message-widget-tab');
D.append(tab, this.e);
D.removeClass(this.e, 'hidden');
}/*refresh()*/,
hide: function(){
D.addClass(D.clearElement(this.e), 'hidden');
delete this.$eMsg;
| > > > > > > > > > > > > > > > > > > > > | 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 |
D.a(F.repoUrl('timeline',{
u: eMsg.dataset.xfrom,
y: 'a'
}), "User's Timeline"),
'target', '_blank'
);
D.append(toolbar2, timelineLink);
if(Chat.filterState.activeUser &&
Chat.filterState.match(eMsg.dataset.xfrom)){
/* Add a button to jump to clear user filter
and jump to this message in context. */
D.append(
this.e,
D.append(
D.addClass(D.div(), 'toolbar'),
D.button(
"Message in context",
function(){
self.hide();
Chat.setUserFilter(false);
eMsg.scrollIntoView(false);
D.flashNTimes(eMsg, 3);
})
)
);
}/*jump-to button*/
}
const tab = eMsg.querySelector('.message-widget-tab');
D.append(tab, this.e);
D.removeClass(this.e, 'hidden');
}/*refresh()*/,
hide: function(){
D.addClass(D.clearElement(this.e), 'hidden');
delete this.$eMsg;
|
| ︙ | ︙ | |||
1174 1175 1176 1177 1178 1179 1180 |
ev.stopPropagation();
Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig
? Chat.e.viewMessages : Chat.e.viewConfig);
return false;
};
D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
| > > > > > > > > > > > > > > > > > > > > > > > | | | > | 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 |
ev.stopPropagation();
Chat.setCurrentView(Chat.e.currentView===Chat.e.viewConfig
? Chat.e.viewMessages : Chat.e.viewConfig);
return false;
};
D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
Chat.e.viewConfig.querySelector('button').addEventListener('click', cbToggle, false);
/** Internal acrobatics to allow certain settings toggles to access
related toggles. */
const namedOptions = {
activeUsers:{
label: "Show active users list",
boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
persistentSetting: 'active-user-list',
callback: function(){
D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
/* When hiding this element, undo all filtering */
D.removeClass(Chat.e.viewMessages.querySelectorAll('.message-widget.hidden'), 'hidden');
/*Ideally we'd scroll the final message into view
now, but because viewMessages is currently hidden behind
viewConfig, scrolling is a no-op. */
Chat.scrollMessagesTo(1);
}else{
Chat.updateActiveUserList();
}
}
}
};
/* Settings menu entries... Remember that they will be rendered in
reverse order and the most frequently-needed ones "should"
(arguably) be closer to the start of this list so that they
will be rendered within easier reach of the settings button. */
const settingsOps = [{
label: "Multi-line input",
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
persistentSetting: 'edit-multiline',
callback: function(){
Chat.inputToggleSingleMulti();
}
|
| ︙ | ︙ | |||
1198 1199 1200 1201 1202 1203 1204 |
label: "Show images inline",
boolValue: ()=>Chat.settings.getBool('images-inline'),
callback: function(){
const v = Chat.settings.toggle('images-inline');
F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
}
},{
| | < < < < < | | < < < | < | < > > | | > | 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 |
label: "Show images inline",
boolValue: ()=>Chat.settings.getBool('images-inline'),
callback: function(){
const v = Chat.settings.toggle('images-inline');
F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
}
},{
label: "Timestamps in active users list",
boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
persistentSetting: 'active-user-list-timestamps',
callback: function(){
D.toggleClass(Chat.e.activeUserList,'timestamps');
/* If the timestamp option is activated but optActiveUsers is not
currently checked then toggle that option on as well. */
if(Chat.e.activeUserList.classList.contains('timestamps')
&& !namedOptions.activeUsers.boolValue()){
namedOptions.activeUsers.checkbox.checked = true;
namedOptions.activeUsers.callback();
}
}
},
namedOptions.activeUsers,{
label: "Monospace message font",
boolValue: ()=>document.body.classList.contains('monospace-messages'),
persistentSetting: 'monospace-messages',
callback: function(){
document.body.classList.toggle('monospace-messages');
}
},{
|
| ︙ | ︙ | |||
1285 1286 1287 1288 1289 1290 1291 |
};
if(op.hasOwnProperty('select')){
D.append(line, btn, op.select);
op.select.addEventListener('change', callback, false);
}else if(op.hasOwnProperty('boolValue')){
if(undefined === f.$id) f.$id = 0;
++f.$id;
| > | | | 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 |
};
if(op.hasOwnProperty('select')){
D.append(line, btn, op.select);
op.select.addEventListener('change', callback, false);
}else if(op.hasOwnProperty('boolValue')){
if(undefined === f.$id) f.$id = 0;
++f.$id;
const check = op.checkbox
= D.attr(D.checkbox(1, op.boolValue()),
'aria-label', op.label);
const id = 'cfgopt'+f.$id;
if(op.boolValue()) check.checked = true;
D.attr(check, 'id', id);
D.attr(btn, 'for', id);
D.append(line, check);
check.addEventListener('change', callback);
D.append(line, btn);
|
| ︙ | ︙ |
Changes to src/fossil.dom.js.
| ︙ | ︙ | |||
117 118 119 120 121 122 123 |
dom.hr = dom.createElemFactory('hr');
dom.br = dom.createElemFactory('br');
/** Returns a new TEXT node which contains the text of all of the
arguments appended together. */
dom.text = function(/*...*/){
return document.createTextNode(argsToArray(arguments).join(''));
};
| > > | > > > | 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
dom.hr = dom.createElemFactory('hr');
dom.br = dom.createElemFactory('br');
/** Returns a new TEXT node which contains the text of all of the
arguments appended together. */
dom.text = function(/*...*/){
return document.createTextNode(argsToArray(arguments).join(''));
};
/** Returns a new Button element with the given optional
label and on-click event listener function. */
dom.button = function(label,callback){
const b = this.create('button');
if(label) b.appendChild(this.text(label));
if('function' === typeof callback){
b.addEventListener('click', callback, false);
}
return b;
};
/**
Returns a TEXTAREA element.
Usages:
|
| ︙ | ︙ | |||
675 676 677 678 679 680 681 682 683 684 685 686 687 688 |
dom.flashOnce.defaultTimeMs = 400;
/**
A DOM event handler which simply passes event.target
to dom.flashOnce().
*/
dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
/**
Attempts to copy the given text to the system clipboard. Returns
true if it succeeds, else false.
*/
dom.copyTextToClipboard = function(text){
if( window.clipboardData && window.clipboardData.setData ){
window.clipboardData.setData('Text',text);
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 |
dom.flashOnce.defaultTimeMs = 400;
/**
A DOM event handler which simply passes event.target
to dom.flashOnce().
*/
dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
/**
This variant of flashOnce() flashes the element e n times
for a duration of howLongMs milliseconds then calls the
afterFlashCallback() callback. It may also be called with 2
or 3 arguments, in which case:
2 arguments: default flash time and no callback.
3 arguments: 3rd may be a flash delay time or a callback
function.
Returns this object but the flashing is asynchronous.
*/
dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
const args = argsToArray(arguments);
args.splice(1,1);
if(arguments.length===3 && 'function'===typeof howLongMs){
afterFlashCallback = howLongMs;
howLongMs = args[1] = this.flashOnce.defaultTimeMs;
}else if(arguments.length<3){
args[1] = this.flashOnce.defaultTimeMs;
}
n = +n;
const self = this;
const cb = args[2] = function f(){
if(--n){
setTimeout(()=>self.flashOnce(e, howLongMs, f),
howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
}else if(afterFlashCallback){
afterFlashCallback();
}
};
this.flashOnce.apply(this, args);
return this;
};
/**
Attempts to copy the given text to the system clipboard. Returns
true if it succeeds, else false.
*/
dom.copyTextToClipboard = function(text){
if( window.clipboardData && window.clipboardData.setData ){
window.clipboardData.setData('Text',text);
|
| ︙ | ︙ |