Fossil SCM

Replace /chat config popup with a friendlier and more flexible widget. Reintroduces ability to select from multiple alerts. Seems to work but needs more testing. [forum:d97c869900 | Forum post d97c869900].

stephan 2021-09-17 23:21 trunk
Commit 6f5e04b340dd4863c8e3600992f9fc94229e92643a1fd8113c129316c05601b8
3 files changed +8 -1 +49 -76 +31 -1
+8 -1
--- src/chat.c
+++ src/chat.c
@@ -51,11 +51,13 @@
5151
static void chat_emit_alert_list(void){
5252
/*Stmt q = empty_Stmt;*/
5353
unsigned int i;
5454
const char * azBuiltins[] = {
5555
"builtin/alerts/plunk.wav",
56
- "builtin/alerts/b-flat.wav"
56
+ "builtin/alerts/bflat2.wav",
57
+ "builtin/alerts/bflat3.wav",
58
+ "builtin/alerts/bloop.wav"
5759
};
5860
CX("window.fossil.config.chat.alerts = [\n");
5961
for(i=0; i < sizeof(azBuiltins)/sizeof(azBuiltins[0]); ++i){
6062
CX("%s%!j", i ? ", " : "", azBuiltins[i]);
6163
}
@@ -181,10 +183,15 @@
181183
@ </div>
182184
@ <div id="chat-drop-details"></div>
183185
@ </div>
184186
@ </div>
185187
@ </form>
188
+ @ <div id='chat-config' class='hidden'>
189
+ @ <div id='chat-config-options'></div>
190
+ /* ^^^populated client-side */
191
+ @ <button>Close</button>
192
+ @ </div>
186193
@ <div id='chat-messages-wrapper'>
187194
/* New chat messages get inserted immediately after this element */
188195
@ <span id='message-inject-point'></span>
189196
@ </div>
190197
fossil_free(zProjectName);
191198
--- src/chat.c
+++ src/chat.c
@@ -51,11 +51,13 @@
51 static void chat_emit_alert_list(void){
52 /*Stmt q = empty_Stmt;*/
53 unsigned int i;
54 const char * azBuiltins[] = {
55 "builtin/alerts/plunk.wav",
56 "builtin/alerts/b-flat.wav"
 
 
57 };
58 CX("window.fossil.config.chat.alerts = [\n");
59 for(i=0; i < sizeof(azBuiltins)/sizeof(azBuiltins[0]); ++i){
60 CX("%s%!j", i ? ", " : "", azBuiltins[i]);
61 }
@@ -181,10 +183,15 @@
181 @ </div>
182 @ <div id="chat-drop-details"></div>
183 @ </div>
184 @ </div>
185 @ </form>
 
 
 
 
 
186 @ <div id='chat-messages-wrapper'>
187 /* New chat messages get inserted immediately after this element */
188 @ <span id='message-inject-point'></span>
189 @ </div>
190 fossil_free(zProjectName);
191
--- src/chat.c
+++ src/chat.c
@@ -51,11 +51,13 @@
51 static void chat_emit_alert_list(void){
52 /*Stmt q = empty_Stmt;*/
53 unsigned int i;
54 const char * azBuiltins[] = {
55 "builtin/alerts/plunk.wav",
56 "builtin/alerts/bflat2.wav",
57 "builtin/alerts/bflat3.wav",
58 "builtin/alerts/bloop.wav"
59 };
60 CX("window.fossil.config.chat.alerts = [\n");
61 for(i=0; i < sizeof(azBuiltins)/sizeof(azBuiltins[0]); ++i){
62 CX("%s%!j", i ? ", " : "", azBuiltins[i]);
63 }
@@ -181,10 +183,15 @@
183 @ </div>
184 @ <div id="chat-drop-details"></div>
185 @ </div>
186 @ </div>
187 @ </form>
188 @ <div id='chat-config' class='hidden'>
189 @ <div id='chat-config-options'></div>
190 /* ^^^populated client-side */
191 @ <button>Close</button>
192 @ </div>
193 @ <div id='chat-messages-wrapper'>
194 /* New chat messages get inserted immediately after this element */
195 @ <span id='message-inject-point'></span>
196 @ </div>
197 fossil_free(zProjectName);
198
+49 -76
--- src/chat.js
+++ src/chat.js
@@ -109,11 +109,12 @@
109109
btnSubmit: E1('#chat-message-submit'),
110110
inputSingle: E1('#chat-input-single'),
111111
inputMulti: E1('#chat-input-multi'),
112112
inputCurrent: undefined/*one of inputSingle or inputMulti*/,
113113
inputFile: E1('#chat-input-file'),
114
- contentDiv: E1('div.content')
114
+ contentDiv: E1('div.content'),
115
+ configArea: E1('#chat-config')
115116
},
116117
me: F.user.name,
117118
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
118119
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
119120
pageIsActive: 'visible'===document.visibilityState,
@@ -950,14 +951,16 @@
950951
.replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu');
951952
};
952953
953954
(function(){/*Set up #chat-settings-button */
954955
const settingsButton = document.querySelector('#chat-settings-button');
955
- var popupSize = undefined/*placement workaround*/;
956
- const settingsPopup = new F.PopupWidget({
957
- cssClass: ['fossil-tooltip', 'chat-settings-popup']
958
- });
956
+ const optionsMenu = E1('#chat-config-options');
957
+ const cbToggle = function(){
958
+ D.toggleClass([Chat.e.messagesWrapper, Chat.e.configArea], 'hidden');
959
+ };
960
+ D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
961
+ Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false);
959962
/* Settings menu entries... */
960963
const settingsOps = [{
961964
label: "Multi-line input",
962965
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
963966
persistentSetting: 'edit-multiline',
@@ -992,11 +995,11 @@
992995
F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
993996
}
994997
}];
995998
996999
/** Set up selection list of notification sounds. */
997
- if(true/*flip this to false to enable selection of audio files*/){
1000
+ if(false/*flip this to false to enable selection of audio files*/){
9981001
settingsOps.push({
9991002
label: "Audible alerts",
10001003
boolValue: ()=>Chat.settings.getBool('audible-alert'),
10011004
callback: function(){
10021005
const v = Chat.settings.toggle('audible-alert');
@@ -1009,11 +1012,15 @@
10091012
Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false
10101013
);
10111014
}else{
10121015
/* Disabled per chatroom discussion: selection list of audio files for
10131016
chat notification. */
1014
- const selectSound = settingsOps.selectSound = D.addClass(D.select(), 'menu-entry');
1017
+ settingsOps.selectSound = D.addClass(D.div(), 'menu-entry');
1018
+ const selectSound = D.select();
1019
+ D.append(settingsOps.selectSound,
1020
+ D.append(D.span(),"Audio alert"),
1021
+ selectSound);
10151022
D.disable(D.option(selectSound, "0", "Audible alert..."));
10161023
D.option(selectSound, "", "(no audio)");
10171024
F.config.chat.alerts.forEach(function(a){
10181025
D.option(selectSound, a);
10191026
});
@@ -1029,83 +1036,49 @@
10291036
}
10301037
selectSound.addEventListener('change',function(){
10311038
const v = this.selectedIndex>1 ? this.value : '';
10321039
Chat.setNewMessageSound(v);
10331040
F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1034
- if(v) setTimeout(()=>Chat.playNewMessageSound(), 50);
1035
- settingsPopup.hide();
1041
+ if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
10361042
}, false);
10371043
Chat.setNewMessageSound(selectSound.value);
10381044
}/*audio notification config*/
10391045
/**
1040
- Rebuild the menu each time it's shown so that the toggles can
1041
- show their current values.
1042
- */
1043
- settingsPopup.options.refresh = function(){
1044
- D.clearElement(this.e);
1045
- settingsOps.forEach(function(op){
1046
- const line = D.addClass(D.span(), 'menu-entry');
1047
- const btn = D.append(D.addClass(D.span(), 'button'), op.label);
1048
- const callback = function(ev){
1049
- settingsPopup.hide();
1050
- op.callback(ev);
1051
- if(op.persistentSetting){
1052
- Chat.settings.set(op.persistentSetting, op.boolValue());
1053
- }
1054
- };
1055
- D.append(line, btn);
1056
- if(op.hasOwnProperty('boolValue')){
1057
- const check = D.attr(D.checkbox(1, op.boolValue()),
1058
- 'aria-label', op.label);
1059
- D.append(line, check);
1060
- }
1061
- D.append(settingsPopup.e, line);
1062
- line.addEventListener('click', callback);
1063
- });
1064
- if(settingsOps.selectSound){
1065
- D.append(settingsPopup.e, settingsOps.selectSound);
1066
- }
1067
- };
1068
- settingsPopup.installHideHandlers(
1069
- false, settingsOps.selectSound ? false : true,
1070
- true)
1071
- /** Reminder: click-to-hide interferes with "?" embedded within
1072
- the popup, so cannot be used together with those. Enabling
1073
- this means, however, that tapping the menu button to toggle
1074
- the menu cannot work because tapping the menu button while the
1075
- menu is opened will, because of the click-to-hide handler,
1076
- hide the menu before the button gets an event saying to toggle
1077
- it.
1078
-
1079
- Reminder: because we need a SELECT element for the audio file
1080
- selection (since that list can be arbitrarily long), we have
1081
- to disable tap-outside-the-popup-to-close-it via passing false
1082
- as the 2nd argument to installHideHandlers(). If we don't,
1083
- tapping on the select element is unreliable on desktop
1084
- browsers and doesn't seem to work at all on mobile. */;
1085
- D.attr(settingsButton, 'role', 'button');
1086
- settingsButton.addEventListener('click',function(ev){
1087
- //ev.preventDefault();
1088
- if(settingsPopup.isShown()) settingsPopup.hide();
1089
- else settingsPopup.show(settingsButton);
1090
- /* Reminder: we cannot toggle the visibility from her
1091
- */
1092
- }, false);
1093
-
1094
- /* Find an ideal X/Y position for the popup, directly above the settings
1095
- button, based on the size of the popup... */
1096
- settingsPopup.show(document.body);
1097
- popupSize = settingsPopup.e.getBoundingClientRect();
1098
- settingsPopup.hide();
1099
- settingsPopup.options.adjustX = function(x){
1100
- const rect = settingsButton.getBoundingClientRect();
1101
- return rect.right - popupSize.width;
1102
- };
1103
- settingsPopup.options.adjustY = function(y){
1104
- const rect = settingsButton.getBoundingClientRect();
1105
- return rect.top - popupSize.height -2;
1106
- };
1046
+ Build list of options...
1047
+ */
1048
+ settingsOps.forEach(function f(op){
1049
+ const line = D.addClass(D.div(), 'menu-entry');
1050
+ const btn = D.append(
1051
+ D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1052
+ op.label);
1053
+ const callback = function(ev){
1054
+ op.callback(ev);
1055
+ if(op.persistentSetting){
1056
+ Chat.settings.set(op.persistentSetting, op.boolValue());
1057
+ }
1058
+ };
1059
+ if(op.hasOwnProperty('boolValue')){
1060
+ if(undefined === f.$id) f.$id = 0;
1061
+ ++f.$id;
1062
+ const check = D.attr(D.checkbox(1, op.boolValue()),
1063
+ 'aria-label', op.label);
1064
+ const id = 'cfgopt'+f.$id;
1065
+ if(op.boolValue()) check.checked = true;
1066
+ D.attr(check, 'id', id);
1067
+ D.attr(btn, 'for', id);
1068
+ D.append(line, check);
1069
+ check.addEventListener('change', callback);
1070
+ }else{
1071
+ line.addEventListener('click', callback);
1072
+ }
1073
+ D.append(line, btn);
1074
+ D.append(optionsMenu, line);
1075
+ });
1076
+ if(settingsOps.selectSound){
1077
+ D.append(optionsMenu, settingsOps.selectSound);
1078
+ }
1079
+ settingsButton.click()/*for for development*/;
11071080
})()/*#chat-settings-button setup*/;
11081081
11091082
/** Callback for poll() to inject new content into the page. jx ==
11101083
the response from /chat-poll. If atEnd is true, the message is
11111084
appended to the end of the chat list (for loading older
11121085
--- src/chat.js
+++ src/chat.js
@@ -109,11 +109,12 @@
109 btnSubmit: E1('#chat-message-submit'),
110 inputSingle: E1('#chat-input-single'),
111 inputMulti: E1('#chat-input-multi'),
112 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
113 inputFile: E1('#chat-input-file'),
114 contentDiv: E1('div.content')
 
115 },
116 me: F.user.name,
117 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
118 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
119 pageIsActive: 'visible'===document.visibilityState,
@@ -950,14 +951,16 @@
950 .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu');
951 };
952
953 (function(){/*Set up #chat-settings-button */
954 const settingsButton = document.querySelector('#chat-settings-button');
955 var popupSize = undefined/*placement workaround*/;
956 const settingsPopup = new F.PopupWidget({
957 cssClass: ['fossil-tooltip', 'chat-settings-popup']
958 });
 
 
959 /* Settings menu entries... */
960 const settingsOps = [{
961 label: "Multi-line input",
962 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
963 persistentSetting: 'edit-multiline',
@@ -992,11 +995,11 @@
992 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
993 }
994 }];
995
996 /** Set up selection list of notification sounds. */
997 if(true/*flip this to false to enable selection of audio files*/){
998 settingsOps.push({
999 label: "Audible alerts",
1000 boolValue: ()=>Chat.settings.getBool('audible-alert'),
1001 callback: function(){
1002 const v = Chat.settings.toggle('audible-alert');
@@ -1009,11 +1012,15 @@
1009 Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false
1010 );
1011 }else{
1012 /* Disabled per chatroom discussion: selection list of audio files for
1013 chat notification. */
1014 const selectSound = settingsOps.selectSound = D.addClass(D.select(), 'menu-entry');
 
 
 
 
1015 D.disable(D.option(selectSound, "0", "Audible alert..."));
1016 D.option(selectSound, "", "(no audio)");
1017 F.config.chat.alerts.forEach(function(a){
1018 D.option(selectSound, a);
1019 });
@@ -1029,83 +1036,49 @@
1029 }
1030 selectSound.addEventListener('change',function(){
1031 const v = this.selectedIndex>1 ? this.value : '';
1032 Chat.setNewMessageSound(v);
1033 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1034 if(v) setTimeout(()=>Chat.playNewMessageSound(), 50);
1035 settingsPopup.hide();
1036 }, false);
1037 Chat.setNewMessageSound(selectSound.value);
1038 }/*audio notification config*/
1039 /**
1040 Rebuild the menu each time it's shown so that the toggles can
1041 show their current values.
1042 */
1043 settingsPopup.options.refresh = function(){
1044 D.clearElement(this.e);
1045 settingsOps.forEach(function(op){
1046 const line = D.addClass(D.span(), 'menu-entry');
1047 const btn = D.append(D.addClass(D.span(), 'button'), op.label);
1048 const callback = function(ev){
1049 settingsPopup.hide();
1050 op.callback(ev);
1051 if(op.persistentSetting){
1052 Chat.settings.set(op.persistentSetting, op.boolValue());
1053 }
1054 };
1055 D.append(line, btn);
1056 if(op.hasOwnProperty('boolValue')){
1057 const check = D.attr(D.checkbox(1, op.boolValue()),
1058 'aria-label', op.label);
1059 D.append(line, check);
1060 }
1061 D.append(settingsPopup.e, line);
1062 line.addEventListener('click', callback);
1063 });
1064 if(settingsOps.selectSound){
1065 D.append(settingsPopup.e, settingsOps.selectSound);
1066 }
1067 };
1068 settingsPopup.installHideHandlers(
1069 false, settingsOps.selectSound ? false : true,
1070 true)
1071 /** Reminder: click-to-hide interferes with "?" embedded within
1072 the popup, so cannot be used together with those. Enabling
1073 this means, however, that tapping the menu button to toggle
1074 the menu cannot work because tapping the menu button while the
1075 menu is opened will, because of the click-to-hide handler,
1076 hide the menu before the button gets an event saying to toggle
1077 it.
1078
1079 Reminder: because we need a SELECT element for the audio file
1080 selection (since that list can be arbitrarily long), we have
1081 to disable tap-outside-the-popup-to-close-it via passing false
1082 as the 2nd argument to installHideHandlers(). If we don't,
1083 tapping on the select element is unreliable on desktop
1084 browsers and doesn't seem to work at all on mobile. */;
1085 D.attr(settingsButton, 'role', 'button');
1086 settingsButton.addEventListener('click',function(ev){
1087 //ev.preventDefault();
1088 if(settingsPopup.isShown()) settingsPopup.hide();
1089 else settingsPopup.show(settingsButton);
1090 /* Reminder: we cannot toggle the visibility from her
1091 */
1092 }, false);
1093
1094 /* Find an ideal X/Y position for the popup, directly above the settings
1095 button, based on the size of the popup... */
1096 settingsPopup.show(document.body);
1097 popupSize = settingsPopup.e.getBoundingClientRect();
1098 settingsPopup.hide();
1099 settingsPopup.options.adjustX = function(x){
1100 const rect = settingsButton.getBoundingClientRect();
1101 return rect.right - popupSize.width;
1102 };
1103 settingsPopup.options.adjustY = function(y){
1104 const rect = settingsButton.getBoundingClientRect();
1105 return rect.top - popupSize.height -2;
1106 };
1107 })()/*#chat-settings-button setup*/;
1108
1109 /** Callback for poll() to inject new content into the page. jx ==
1110 the response from /chat-poll. If atEnd is true, the message is
1111 appended to the end of the chat list (for loading older
1112
--- src/chat.js
+++ src/chat.js
@@ -109,11 +109,12 @@
109 btnSubmit: E1('#chat-message-submit'),
110 inputSingle: E1('#chat-input-single'),
111 inputMulti: E1('#chat-input-multi'),
112 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
113 inputFile: E1('#chat-input-file'),
114 contentDiv: E1('div.content'),
115 configArea: E1('#chat-config')
116 },
117 me: F.user.name,
118 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
119 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
120 pageIsActive: 'visible'===document.visibilityState,
@@ -950,14 +951,16 @@
951 .replace('T',' ').replace(/\.\d+/,'').replace('Z', ' zulu');
952 };
953
954 (function(){/*Set up #chat-settings-button */
955 const settingsButton = document.querySelector('#chat-settings-button');
956 const optionsMenu = E1('#chat-config-options');
957 const cbToggle = function(){
958 D.toggleClass([Chat.e.messagesWrapper, Chat.e.configArea], 'hidden');
959 };
960 D.attr(settingsButton, 'role', 'button').addEventListener('click', cbToggle, false);
961 Chat.e.configArea.querySelector('button').addEventListener('click', cbToggle, false);
962 /* Settings menu entries... */
963 const settingsOps = [{
964 label: "Multi-line input",
965 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
966 persistentSetting: 'edit-multiline',
@@ -992,11 +995,11 @@
995 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
996 }
997 }];
998
999 /** Set up selection list of notification sounds. */
1000 if(false/*flip this to false to enable selection of audio files*/){
1001 settingsOps.push({
1002 label: "Audible alerts",
1003 boolValue: ()=>Chat.settings.getBool('audible-alert'),
1004 callback: function(){
1005 const v = Chat.settings.toggle('audible-alert');
@@ -1009,11 +1012,15 @@
1012 Chat.settings.getBool('audible-alert') ? F.config.chat.alertSound : false
1013 );
1014 }else{
1015 /* Disabled per chatroom discussion: selection list of audio files for
1016 chat notification. */
1017 settingsOps.selectSound = D.addClass(D.div(), 'menu-entry');
1018 const selectSound = D.select();
1019 D.append(settingsOps.selectSound,
1020 D.append(D.span(),"Audio alert"),
1021 selectSound);
1022 D.disable(D.option(selectSound, "0", "Audible alert..."));
1023 D.option(selectSound, "", "(no audio)");
1024 F.config.chat.alerts.forEach(function(a){
1025 D.option(selectSound, a);
1026 });
@@ -1029,83 +1036,49 @@
1036 }
1037 selectSound.addEventListener('change',function(){
1038 const v = this.selectedIndex>1 ? this.value : '';
1039 Chat.setNewMessageSound(v);
1040 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1041 if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
 
1042 }, false);
1043 Chat.setNewMessageSound(selectSound.value);
1044 }/*audio notification config*/
1045 /**
1046 Build list of options...
1047 */
1048 settingsOps.forEach(function f(op){
1049 const line = D.addClass(D.div(), 'menu-entry');
1050 const btn = D.append(
1051 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1052 op.label);
1053 const callback = function(ev){
1054 op.callback(ev);
1055 if(op.persistentSetting){
1056 Chat.settings.set(op.persistentSetting, op.boolValue());
1057 }
1058 };
1059 if(op.hasOwnProperty('boolValue')){
1060 if(undefined === f.$id) f.$id = 0;
1061 ++f.$id;
1062 const check = D.attr(D.checkbox(1, op.boolValue()),
1063 'aria-label', op.label);
1064 const id = 'cfgopt'+f.$id;
1065 if(op.boolValue()) check.checked = true;
1066 D.attr(check, 'id', id);
1067 D.attr(btn, 'for', id);
1068 D.append(line, check);
1069 check.addEventListener('change', callback);
1070 }else{
1071 line.addEventListener('click', callback);
1072 }
1073 D.append(line, btn);
1074 D.append(optionsMenu, line);
1075 });
1076 if(settingsOps.selectSound){
1077 D.append(optionsMenu, settingsOps.selectSound);
1078 }
1079 settingsButton.click()/*for for development*/;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1080 })()/*#chat-settings-button setup*/;
1081
1082 /** Callback for poll() to inject new content into the page. jx ==
1083 the response from /chat-poll. If atEnd is true, the message is
1084 appended to the end of the chat list (for loading older
1085
+31 -1
--- src/default.css
+++ src/default.css
@@ -1897,11 +1897,41 @@
18971897
18981898
body.chat #chat-drop-details img {
18991899
max-width: 45%;
19001900
max-height: 45%;
19011901
}
1902
-
1902
+body.chat #chat-config {
1903
+ /* /chat configuration widget */
1904
+ display: flex;
1905
+ flex-direction: column;
1906
+ flex: 1 0 auto;
1907
+ overflow: auto;
1908
+ flex: 2 1 auto;
1909
+ padding: 0 0.25em;
1910
+}
1911
+body.chat #chat-config > button {
1912
+ padding: 0.5em;
1913
+ flex: 0 1 auto;
1914
+ margin: 0.25em 0;
1915
+}
1916
+body.chat #chat-config #chat-config-options {
1917
+ /* /chat config options go here */
1918
+ flex: 1 1 auto;
1919
+ display: flex;
1920
+ flex-direction: column;
1921
+ overflow: auto;
1922
+}
1923
+body.chat #chat-config #chat-config-options .menu-entry {
1924
+ display: flex;
1925
+ align-items: center;
1926
+ flex-direction: row;
1927
+ flex-wrap: nowrap;
1928
+ padding: 1em;
1929
+}
1930
+body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
1931
+ margin-right: 1em;
1932
+}
19031933
input[type="checkbox"].diff-toggle {
19041934
float: right;
19051935
}
19061936
19071937
body.branch .brlist > table > tbody > tr:hover:not(.selected),
19081938
--- src/default.css
+++ src/default.css
@@ -1897,11 +1897,41 @@
1897
1898 body.chat #chat-drop-details img {
1899 max-width: 45%;
1900 max-height: 45%;
1901 }
1902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1903 input[type="checkbox"].diff-toggle {
1904 float: right;
1905 }
1906
1907 body.branch .brlist > table > tbody > tr:hover:not(.selected),
1908
--- src/default.css
+++ src/default.css
@@ -1897,11 +1897,41 @@
1897
1898 body.chat #chat-drop-details img {
1899 max-width: 45%;
1900 max-height: 45%;
1901 }
1902 body.chat #chat-config {
1903 /* /chat configuration widget */
1904 display: flex;
1905 flex-direction: column;
1906 flex: 1 0 auto;
1907 overflow: auto;
1908 flex: 2 1 auto;
1909 padding: 0 0.25em;
1910 }
1911 body.chat #chat-config > button {
1912 padding: 0.5em;
1913 flex: 0 1 auto;
1914 margin: 0.25em 0;
1915 }
1916 body.chat #chat-config #chat-config-options {
1917 /* /chat config options go here */
1918 flex: 1 1 auto;
1919 display: flex;
1920 flex-direction: column;
1921 overflow: auto;
1922 }
1923 body.chat #chat-config #chat-config-options .menu-entry {
1924 display: flex;
1925 align-items: center;
1926 flex-direction: row;
1927 flex-wrap: nowrap;
1928 padding: 1em;
1929 }
1930 body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
1931 margin-right: 1em;
1932 }
1933 input[type="checkbox"].diff-toggle {
1934 float: right;
1935 }
1936
1937 body.branch .brlist > table > tbody > tr:hover:not(.selected),
1938

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button