Fossil SCM

Enhance /chat to enable embedding of HTML/text/image attachments via iframes, the motivating use case being embedding of attached diff files.

stephan 2021-12-30 19:17 trunk merge
Commit 432ff8d8c1b1896948d16aacd4a34e84835aca9cc99beeb15e1dc3f753f75654
--- src/diffcmd.c
+++ src/diffcmd.c
@@ -193,24 +193,33 @@
193193
@ <!DOCTYPE html>
194194
@ <html>
195195
@ <head>
196196
@ <meta charset="UTF-8">
197197
@ <style>
198
+@ body {
199
+@ background-color: white;
200
+@ }
198201
@ h1 {
199202
@ font-size: 150%;
200203
@ }
201204
@
202205
@ table.diff {
203206
@ width: 100%;
204207
@ border-spacing: 0;
205208
@ border: 1px solid black;
209
+@ line-height: inherit;
210
+@ font-size: inherit;
206211
@ }
207212
@ table.diff td {
208213
@ vertical-align: top;
214
+@ line-height: inherit;
215
+@ font-size: inherit;
209216
@ }
210217
@ table.diff pre {
211218
@ margin: 0 0 0 0;
219
+@ line-height: inherit;
220
+@ font-size: inherit;
212221
@ }
213222
@ td.diffln {
214223
@ width: 1px;
215224
@ text-align: right;
216225
@ padding: 0 1em 0 0;
@@ -219,25 +228,37 @@
219228
@ padding-bottom: 0.4em;
220229
@ }
221230
@ td.diffsep {
222231
@ width: 1px;
223232
@ padding: 0 0.3em 0 1em;
233
+@ line-height: inherit;
234
+@ font-size: inherit;
235
+@ }
236
+@ td.diffsep pre {
237
+@ line-height: inherit;
238
+@ font-size: inherit;
224239
@ }
225240
@ td.difftxt pre {
226241
@ overflow-x: auto;
227242
@ }
228243
@ td.diffln ins {
229244
@ background-color: #a0e4b2;
230245
@ text-decoration: none;
246
+@ line-height: inherit;
247
+@ font-size: inherit;
231248
@ }
232249
@ td.diffln del {
233250
@ background-color: #ffc0c0;
234251
@ text-decoration: none;
252
+@ line-height: inherit;
253
+@ font-size: inherit;
235254
@ }
236255
@ td.difftxt del {
237256
@ background-color: #ffe8e8;
238257
@ text-decoration: none;
258
+@ line-height: inherit;
259
+@ font-size: inherit;
239260
@ }
240261
@ td.difftxt del > del {
241262
@ background-color: #ffc0c0;
242263
@ text-decoration: none;
243264
@ font-weight: bold;
@@ -248,10 +269,12 @@
248269
@ font-weight: bold;
249270
@ }
250271
@ td.difftxt ins {
251272
@ background-color: #dafbe1;
252273
@ text-decoration: none;
274
+@ line-height: inherit;
275
+@ font-size: inherit;
253276
@ }
254277
@ td.difftxt ins > ins {
255278
@ background-color: #a0e4b2;
256279
@ text-decoration: none;
257280
@ font-weight: bold;
258281
--- src/diffcmd.c
+++ src/diffcmd.c
@@ -193,24 +193,33 @@
193 @ <!DOCTYPE html>
194 @ <html>
195 @ <head>
196 @ <meta charset="UTF-8">
197 @ <style>
 
 
 
198 @ h1 {
199 @ font-size: 150%;
200 @ }
201 @
202 @ table.diff {
203 @ width: 100%;
204 @ border-spacing: 0;
205 @ border: 1px solid black;
 
 
206 @ }
207 @ table.diff td {
208 @ vertical-align: top;
 
 
209 @ }
210 @ table.diff pre {
211 @ margin: 0 0 0 0;
 
 
212 @ }
213 @ td.diffln {
214 @ width: 1px;
215 @ text-align: right;
216 @ padding: 0 1em 0 0;
@@ -219,25 +228,37 @@
219 @ padding-bottom: 0.4em;
220 @ }
221 @ td.diffsep {
222 @ width: 1px;
223 @ padding: 0 0.3em 0 1em;
 
 
 
 
 
 
224 @ }
225 @ td.difftxt pre {
226 @ overflow-x: auto;
227 @ }
228 @ td.diffln ins {
229 @ background-color: #a0e4b2;
230 @ text-decoration: none;
 
 
231 @ }
232 @ td.diffln del {
233 @ background-color: #ffc0c0;
234 @ text-decoration: none;
 
 
235 @ }
236 @ td.difftxt del {
237 @ background-color: #ffe8e8;
238 @ text-decoration: none;
 
 
239 @ }
240 @ td.difftxt del > del {
241 @ background-color: #ffc0c0;
242 @ text-decoration: none;
243 @ font-weight: bold;
@@ -248,10 +269,12 @@
248 @ font-weight: bold;
249 @ }
250 @ td.difftxt ins {
251 @ background-color: #dafbe1;
252 @ text-decoration: none;
 
 
253 @ }
254 @ td.difftxt ins > ins {
255 @ background-color: #a0e4b2;
256 @ text-decoration: none;
257 @ font-weight: bold;
258
--- src/diffcmd.c
+++ src/diffcmd.c
@@ -193,24 +193,33 @@
193 @ <!DOCTYPE html>
194 @ <html>
195 @ <head>
196 @ <meta charset="UTF-8">
197 @ <style>
198 @ body {
199 @ background-color: white;
200 @ }
201 @ h1 {
202 @ font-size: 150%;
203 @ }
204 @
205 @ table.diff {
206 @ width: 100%;
207 @ border-spacing: 0;
208 @ border: 1px solid black;
209 @ line-height: inherit;
210 @ font-size: inherit;
211 @ }
212 @ table.diff td {
213 @ vertical-align: top;
214 @ line-height: inherit;
215 @ font-size: inherit;
216 @ }
217 @ table.diff pre {
218 @ margin: 0 0 0 0;
219 @ line-height: inherit;
220 @ font-size: inherit;
221 @ }
222 @ td.diffln {
223 @ width: 1px;
224 @ text-align: right;
225 @ padding: 0 1em 0 0;
@@ -219,25 +228,37 @@
228 @ padding-bottom: 0.4em;
229 @ }
230 @ td.diffsep {
231 @ width: 1px;
232 @ padding: 0 0.3em 0 1em;
233 @ line-height: inherit;
234 @ font-size: inherit;
235 @ }
236 @ td.diffsep pre {
237 @ line-height: inherit;
238 @ font-size: inherit;
239 @ }
240 @ td.difftxt pre {
241 @ overflow-x: auto;
242 @ }
243 @ td.diffln ins {
244 @ background-color: #a0e4b2;
245 @ text-decoration: none;
246 @ line-height: inherit;
247 @ font-size: inherit;
248 @ }
249 @ td.diffln del {
250 @ background-color: #ffc0c0;
251 @ text-decoration: none;
252 @ line-height: inherit;
253 @ font-size: inherit;
254 @ }
255 @ td.difftxt del {
256 @ background-color: #ffe8e8;
257 @ text-decoration: none;
258 @ line-height: inherit;
259 @ font-size: inherit;
260 @ }
261 @ td.difftxt del > del {
262 @ background-color: #ffc0c0;
263 @ text-decoration: none;
264 @ font-weight: bold;
@@ -248,10 +269,12 @@
269 @ font-weight: bold;
270 @ }
271 @ td.difftxt ins {
272 @ background-color: #dafbe1;
273 @ text-decoration: none;
274 @ line-height: inherit;
275 @ font-size: inherit;
276 @ }
277 @ td.difftxt ins > ins {
278 @ background-color: #a0e4b2;
279 @ text-decoration: none;
280 @ font-weight: bold;
281
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -701,11 +701,11 @@
701701
e = this.getMessageElemById(id);
702702
}
703703
if(!e || !id) return false;
704704
else if(e.$isToggling) return;
705705
e.$isToggling = true;
706
- const content = e.querySelector('.message-widget-content');
706
+ const content = e.querySelector('.content-target');
707707
if(!content.$elems){
708708
content.$elems = [
709709
content.firstElementChild, // parsed elem
710710
undefined // plaintext elem
711711
];
@@ -890,10 +890,21 @@
890890
d.getHours(),":",
891891
(d.getMinutes()+100).toString().slice(1,3),
892892
' ', dowMap[d.getDay()]
893893
].join('');
894894
};
895
+
896
+ const canEmbedFile = function f(msg){
897
+ if(!f.$rx){
898
+ f.$rx = /\.((html?)|(txt))$/i;
899
+ }
900
+ return msg.fname && (
901
+ f.$rx.test(msg.fname)
902
+ || (msg.fmime
903
+ && msg.fmime.startsWith("image/"))
904
+ );
905
+ };
895906
896907
cf.prototype = {
897908
scrollIntoView: function(){
898909
this.e.content.scrollIntoView();
899910
},
@@ -936,27 +947,68 @@
936947
&& Chat.settings.getBool('images-inline',true)
937948
){
938949
contentTarget.appendChild(D.img("chat-download/" + m.msgid));
939950
ds.hasImage = 1;
940951
}else{
941
- const a = D.a(
942
- window.fossil.rootPath+
943
- 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
952
+ // Add a download link.
953
+ const downloadUri = window.fossil.rootPath+
954
+ 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname);
955
+ const w = D.addClass(D.div(), 'attachment-link');
956
+ const a = D.a(downloadUri,
944957
// ^^^ add m.fname to URL to cause downloaded file to have that name.
945958
"(" + m.fname + " " + m.fsize + " bytes)"
946959
)
947960
D.attr(a,'target','_blank');
948
- contentTarget.appendChild(a);
961
+ D.append(w, a);
962
+ if(canEmbedFile(m)){
963
+ /* Add an option to embed HTML attachments in an iframe. The primary
964
+ use case is attached diffs. */
965
+ D.addClass(contentTarget, 'wide');
966
+ const embedTarget = this.e.content;
967
+ const self = this;
968
+ const btnEmbed = D.attr(D.checkbox("1", false), 'id',
969
+ 'embed-'+ds.msgid);
970
+ const btnLabel = D.label(btnEmbed, "Embed");
971
+ btnEmbed.addEventListener('change',function(){
972
+ if(self.e.iframe){
973
+ if(btnEmbed.checked) D.removeClass(self.e.iframe, 'hidden');
974
+ else D.addClass(self.e.iframe, 'hidden');
975
+ return;
976
+ }
977
+ D.disable(btnEmbed);
978
+ const iframe = self.e.iframe = document.createElement('iframe');
979
+ D.append(embedTarget, iframe);
980
+ iframe.addEventListener('load', function(){
981
+ D.enable(btnEmbed);
982
+ const body = iframe.contentWindow.document.querySelector('body');
983
+ if(body && !body.style.fontSize){
984
+ /** _Attempt_ to force the iframe to inherit the message's text size
985
+ if the body has no explicit size set. On desktop systems
986
+ the size is apparently being inherited in that case, but on mobile
987
+ not. */
988
+ const cs = window.getComputedStyle(self.e.content);
989
+ body.style.fontSize = cs.fontSize;
990
+ }
991
+ iframe.style.maxHeight = iframe.style.height
992
+ = iframe.contentWindow.document.documentElement.scrollHeight + 'px';
993
+ });
994
+ iframe.setAttribute('src', downloadUri);
995
+ });
996
+ D.append(w, btnEmbed, btnLabel);
997
+ }
998
+ contentTarget.appendChild(w);
949999
}
9501000
}
9511001
if(m.xmsg){
9521002
if(m.fsize>0){
9531003
/* We have file/image content, so need another element for
9541004
the message text. */
9551005
contentTarget = D.div();
9561006
D.append(this.e.content, contentTarget);
9571007
}
1008
+ D.addClass(contentTarget, 'content-target'
1009
+ /*target element for the 'toggle text mode' feature*/);
9581010
// The m.xmsg text comes from the same server as this script and
9591011
// is guaranteed by that server to be "safe" HTML - safe in the
9601012
// sense that it is not possible for a malefactor to inject HTML
9611013
// or javascript or CSS. The m.xmsg content might contain
9621014
// hyperlinks, but otherwise it will be markup-free. See the
9631015
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -701,11 +701,11 @@
701 e = this.getMessageElemById(id);
702 }
703 if(!e || !id) return false;
704 else if(e.$isToggling) return;
705 e.$isToggling = true;
706 const content = e.querySelector('.message-widget-content');
707 if(!content.$elems){
708 content.$elems = [
709 content.firstElementChild, // parsed elem
710 undefined // plaintext elem
711 ];
@@ -890,10 +890,21 @@
890 d.getHours(),":",
891 (d.getMinutes()+100).toString().slice(1,3),
892 ' ', dowMap[d.getDay()]
893 ].join('');
894 };
 
 
 
 
 
 
 
 
 
 
 
895
896 cf.prototype = {
897 scrollIntoView: function(){
898 this.e.content.scrollIntoView();
899 },
@@ -936,27 +947,68 @@
936 && Chat.settings.getBool('images-inline',true)
937 ){
938 contentTarget.appendChild(D.img("chat-download/" + m.msgid));
939 ds.hasImage = 1;
940 }else{
941 const a = D.a(
942 window.fossil.rootPath+
943 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname),
 
 
944 // ^^^ add m.fname to URL to cause downloaded file to have that name.
945 "(" + m.fname + " " + m.fsize + " bytes)"
946 )
947 D.attr(a,'target','_blank');
948 contentTarget.appendChild(a);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949 }
950 }
951 if(m.xmsg){
952 if(m.fsize>0){
953 /* We have file/image content, so need another element for
954 the message text. */
955 contentTarget = D.div();
956 D.append(this.e.content, contentTarget);
957 }
 
 
958 // The m.xmsg text comes from the same server as this script and
959 // is guaranteed by that server to be "safe" HTML - safe in the
960 // sense that it is not possible for a malefactor to inject HTML
961 // or javascript or CSS. The m.xmsg content might contain
962 // hyperlinks, but otherwise it will be markup-free. See the
963
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -701,11 +701,11 @@
701 e = this.getMessageElemById(id);
702 }
703 if(!e || !id) return false;
704 else if(e.$isToggling) return;
705 e.$isToggling = true;
706 const content = e.querySelector('.content-target');
707 if(!content.$elems){
708 content.$elems = [
709 content.firstElementChild, // parsed elem
710 undefined // plaintext elem
711 ];
@@ -890,10 +890,21 @@
890 d.getHours(),":",
891 (d.getMinutes()+100).toString().slice(1,3),
892 ' ', dowMap[d.getDay()]
893 ].join('');
894 };
895
896 const canEmbedFile = function f(msg){
897 if(!f.$rx){
898 f.$rx = /\.((html?)|(txt))$/i;
899 }
900 return msg.fname && (
901 f.$rx.test(msg.fname)
902 || (msg.fmime
903 && msg.fmime.startsWith("image/"))
904 );
905 };
906
907 cf.prototype = {
908 scrollIntoView: function(){
909 this.e.content.scrollIntoView();
910 },
@@ -936,27 +947,68 @@
947 && Chat.settings.getBool('images-inline',true)
948 ){
949 contentTarget.appendChild(D.img("chat-download/" + m.msgid));
950 ds.hasImage = 1;
951 }else{
952 // Add a download link.
953 const downloadUri = window.fossil.rootPath+
954 'chat-download/' + m.msgid+'/'+encodeURIComponent(m.fname);
955 const w = D.addClass(D.div(), 'attachment-link');
956 const a = D.a(downloadUri,
957 // ^^^ add m.fname to URL to cause downloaded file to have that name.
958 "(" + m.fname + " " + m.fsize + " bytes)"
959 )
960 D.attr(a,'target','_blank');
961 D.append(w, a);
962 if(canEmbedFile(m)){
963 /* Add an option to embed HTML attachments in an iframe. The primary
964 use case is attached diffs. */
965 D.addClass(contentTarget, 'wide');
966 const embedTarget = this.e.content;
967 const self = this;
968 const btnEmbed = D.attr(D.checkbox("1", false), 'id',
969 'embed-'+ds.msgid);
970 const btnLabel = D.label(btnEmbed, "Embed");
971 btnEmbed.addEventListener('change',function(){
972 if(self.e.iframe){
973 if(btnEmbed.checked) D.removeClass(self.e.iframe, 'hidden');
974 else D.addClass(self.e.iframe, 'hidden');
975 return;
976 }
977 D.disable(btnEmbed);
978 const iframe = self.e.iframe = document.createElement('iframe');
979 D.append(embedTarget, iframe);
980 iframe.addEventListener('load', function(){
981 D.enable(btnEmbed);
982 const body = iframe.contentWindow.document.querySelector('body');
983 if(body && !body.style.fontSize){
984 /** _Attempt_ to force the iframe to inherit the message's text size
985 if the body has no explicit size set. On desktop systems
986 the size is apparently being inherited in that case, but on mobile
987 not. */
988 const cs = window.getComputedStyle(self.e.content);
989 body.style.fontSize = cs.fontSize;
990 }
991 iframe.style.maxHeight = iframe.style.height
992 = iframe.contentWindow.document.documentElement.scrollHeight + 'px';
993 });
994 iframe.setAttribute('src', downloadUri);
995 });
996 D.append(w, btnEmbed, btnLabel);
997 }
998 contentTarget.appendChild(w);
999 }
1000 }
1001 if(m.xmsg){
1002 if(m.fsize>0){
1003 /* We have file/image content, so need another element for
1004 the message text. */
1005 contentTarget = D.div();
1006 D.append(this.e.content, contentTarget);
1007 }
1008 D.addClass(contentTarget, 'content-target'
1009 /*target element for the 'toggle text mode' feature*/);
1010 // The m.xmsg text comes from the same server as this script and
1011 // is guaranteed by that server to be "safe" HTML - safe in the
1012 // sense that it is not possible for a malefactor to inject HTML
1013 // or javascript or CSS. The m.xmsg content might contain
1014 // hyperlinks, but otherwise it will be markup-free. See the
1015
--- src/style.chat.css
+++ src/style.chat.css
@@ -29,19 +29,43 @@
2929
/* Center-aligns a system-level notification message. */
3030
align-items: center;
3131
}
3232
/* The content area of a message. */
3333
body.chat .message-widget-content {
34
- display: inline-block;
3534
border-radius: 0.25em;
3635
border: 1px solid rgba(0,0,0,0.2);
3736
box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
3837
padding: 0.25em 0.5em;
3938
margin-top: 0;
4039
min-width: 9em /*avoid unsightly "underlap" with the neighboring
4140
.message-widget-tab element*/;
4241
white-space: normal;
42
+}
43
+body.chat .message-widget-content.wide {
44
+ /* Special case for when embedding content which we really want to
45
+ expand, namely iframes. */
46
+ width: 98%;
47
+}
48
+body.chat .message-widget-content label[for] {
49
+ cursor: pointer;
50
+}
51
+body.chat .message-widget-content > .attachment-link {
52
+ display: flex;
53
+ flex-direction: row;
54
+}
55
+body.chat .message-widget-content > .attachment-link > a {
56
+ margin-right: 1em;
57
+}
58
+body.chat .message-widget-content > iframe {
59
+ width: 100%;
60
+ max-width: 100%;
61
+ resize: both;
62
+}
63
+body.chat .message-widget-content> a {
64
+ /* Cosmetic: keep skin-induced on-hover underlining from shifting
65
+ content placed below this. */
66
+ border-bottom: 1px transparent;
4367
}
4468
body.chat.monospace-messages .message-widget-content,
4569
body.chat.monospace-messages .chat-input-field{
4670
font-family: monospace;
4771
}
4872
--- src/style.chat.css
+++ src/style.chat.css
@@ -29,19 +29,43 @@
29 /* Center-aligns a system-level notification message. */
30 align-items: center;
31 }
32 /* The content area of a message. */
33 body.chat .message-widget-content {
34 display: inline-block;
35 border-radius: 0.25em;
36 border: 1px solid rgba(0,0,0,0.2);
37 box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
38 padding: 0.25em 0.5em;
39 margin-top: 0;
40 min-width: 9em /*avoid unsightly "underlap" with the neighboring
41 .message-widget-tab element*/;
42 white-space: normal;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43 }
44 body.chat.monospace-messages .message-widget-content,
45 body.chat.monospace-messages .chat-input-field{
46 font-family: monospace;
47 }
48
--- src/style.chat.css
+++ src/style.chat.css
@@ -29,19 +29,43 @@
29 /* Center-aligns a system-level notification message. */
30 align-items: center;
31 }
32 /* The content area of a message. */
33 body.chat .message-widget-content {
 
34 border-radius: 0.25em;
35 border: 1px solid rgba(0,0,0,0.2);
36 box-shadow: 0.2em 0.2em 0.2em rgba(0, 0, 0, 0.29);
37 padding: 0.25em 0.5em;
38 margin-top: 0;
39 min-width: 9em /*avoid unsightly "underlap" with the neighboring
40 .message-widget-tab element*/;
41 white-space: normal;
42 }
43 body.chat .message-widget-content.wide {
44 /* Special case for when embedding content which we really want to
45 expand, namely iframes. */
46 width: 98%;
47 }
48 body.chat .message-widget-content label[for] {
49 cursor: pointer;
50 }
51 body.chat .message-widget-content > .attachment-link {
52 display: flex;
53 flex-direction: row;
54 }
55 body.chat .message-widget-content > .attachment-link > a {
56 margin-right: 1em;
57 }
58 body.chat .message-widget-content > iframe {
59 width: 100%;
60 max-width: 100%;
61 resize: both;
62 }
63 body.chat .message-widget-content> a {
64 /* Cosmetic: keep skin-induced on-hover underlining from shifting
65 content placed below this. */
66 border-bottom: 1px transparent;
67 }
68 body.chat.monospace-messages .message-widget-content,
69 body.chat.monospace-messages .chat-input-field{
70 font-family: monospace;
71 }
72

Keyboard Shortcuts

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