@@ -26,6 +26,8 @@ import { TileShape } from "../rooms/EventTile";
26
26
import { presentableTextForFile } from "../../../utils/FileUtils" ;
27
27
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent" ;
28
28
import { IBodyProps } from "./IBodyProps" ;
29
+ import { FileDownloader } from "../../../utils/FileDownloader" ;
30
+ import TextWithTooltip from "../elements/TextWithTooltip" ;
29
31
30
32
export let DOWNLOAD_ICON_URL ; // cached copy of the download.svg asset for the sandboxed iframe later on
31
33
@@ -111,13 +113,40 @@ export default class MFileBody extends React.Component<IProps, IState> {
111
113
private iframe : React . RefObject < HTMLIFrameElement > = createRef ( ) ;
112
114
private dummyLink : React . RefObject < HTMLAnchorElement > = createRef ( ) ;
113
115
private userDidClick = false ;
116
+ private fileDownloader : FileDownloader = new FileDownloader ( ( ) => this . iframe . current ) ;
114
117
115
118
public constructor ( props : IProps ) {
116
119
super ( props ) ;
117
120
118
121
this . state = { } ;
119
122
}
120
123
124
+ private get content ( ) : IMediaEventContent {
125
+ return this . props . mxEvent . getContent < IMediaEventContent > ( ) ;
126
+ }
127
+
128
+ private get fileName ( ) : string {
129
+ return this . content . body && this . content . body . length > 0 ? this . content . body : _t ( "Attachment" ) ;
130
+ }
131
+
132
+ private get linkText ( ) : string {
133
+ return presentableTextForFile ( this . content ) ;
134
+ }
135
+
136
+ private downloadFile ( fileName : string , text : string ) {
137
+ this . fileDownloader . download ( {
138
+ blob : this . state . decryptedBlob ,
139
+ name : fileName ,
140
+ autoDownload : this . userDidClick ,
141
+ opts : {
142
+ imgSrc : DOWNLOAD_ICON_URL ,
143
+ imgStyle : null ,
144
+ style : computedStyle ( this . dummyLink . current ) ,
145
+ textContent : _t ( "Download %(text)s" , { text } ) ,
146
+ } ,
147
+ } ) ;
148
+ }
149
+
121
150
private getContentUrl ( ) : string {
122
151
const media = mediaFromContent ( this . props . mxEvent . getContent ( ) ) ;
123
152
return media . srcHttp ;
@@ -129,24 +158,56 @@ export default class MFileBody extends React.Component<IProps, IState> {
129
158
}
130
159
}
131
160
161
+ private decryptFile = async ( ) : Promise < void > => {
162
+ if ( this . state . decryptedBlob ) {
163
+ return ;
164
+ }
165
+ try {
166
+ this . userDidClick = true ;
167
+ this . setState ( {
168
+ decryptedBlob : await this . props . mediaEventHelper . sourceBlob . value ,
169
+ } ) ;
170
+ } catch ( err ) {
171
+ console . warn ( "Unable to decrypt attachment: " , err ) ;
172
+ Modal . createTrackedDialog ( 'Error decrypting attachment' , '' , ErrorDialog , {
173
+ title : _t ( "Error" ) ,
174
+ description : _t ( "Error decrypting attachment" ) ,
175
+ } ) ;
176
+ }
177
+ } ;
178
+
179
+ private onPlaceholderClick = async ( ) => {
180
+ const mediaHelper = this . props . mediaEventHelper ;
181
+ if ( mediaHelper . media . isEncrypted ) {
182
+ await this . decryptFile ( ) ;
183
+ this . downloadFile ( this . fileName , this . linkText ) ;
184
+ } else {
185
+ // As a button we're missing the `download` attribute for styling reasons, so
186
+ // download with the file downloader.
187
+ this . fileDownloader . download ( {
188
+ blob : await mediaHelper . sourceBlob . value ,
189
+ name : this . fileName ,
190
+ } ) ;
191
+ }
192
+ } ;
193
+
132
194
public render ( ) {
133
- const content = this . props . mxEvent . getContent < IMediaEventContent > ( ) ;
134
- const text = presentableTextForFile ( content ) ;
135
195
const isEncrypted = this . props . mediaEventHelper . media . isEncrypted ;
136
- const fileName = content . body && content . body . length > 0 ? content . body : _t ( "Attachment" ) ;
137
196
const contentUrl = this . getContentUrl ( ) ;
138
- const fileSize = content . info ? content . info . size : null ;
139
- const fileType = content . info ? content . info . mimetype : "application/octet-stream" ;
197
+ const fileSize = this . content . info ? this . content . info . size : null ;
198
+ const fileType = this . content . info ? this . content . info . mimetype : "application/octet-stream" ;
140
199
141
- let placeholder = null ;
200
+ let placeholder : React . ReactNode = null ;
142
201
if ( this . props . showGenericPlaceholder ) {
143
202
placeholder = (
144
- < div className = "mx_MediaBody mx_MFileBody_info" >
203
+ < AccessibleButton className = "mx_MediaBody mx_MFileBody_info" onClick = { this . onPlaceholderClick } >
145
204
< span className = "mx_MFileBody_info_icon" />
146
- < span className = "mx_MFileBody_info_filename" >
147
- { presentableTextForFile ( content , _t ( "Attachment" ) , false ) }
148
- </ span >
149
- </ div >
205
+ < TextWithTooltip tooltip = { presentableTextForFile ( this . content , _t ( "Attachment" ) , true ) } >
206
+ < span className = "mx_MFileBody_info_filename" >
207
+ { presentableTextForFile ( this . content , _t ( "Attachment" ) , true , true ) }
208
+ </ span >
209
+ </ TextWithTooltip >
210
+ </ AccessibleButton >
150
211
) ;
151
212
}
152
213
@@ -157,52 +218,21 @@ export default class MFileBody extends React.Component<IProps, IState> {
157
218
// Need to decrypt the attachment
158
219
// Wait for the user to click on the link before downloading
159
220
// and decrypting the attachment.
160
- const decrypt = async ( ) => {
161
- try {
162
- this . userDidClick = true ;
163
- this . setState ( {
164
- decryptedBlob : await this . props . mediaEventHelper . sourceBlob . value ,
165
- } ) ;
166
- } catch ( err ) {
167
- console . warn ( "Unable to decrypt attachment: " , err ) ;
168
- Modal . createTrackedDialog ( 'Error decrypting attachment' , '' , ErrorDialog , {
169
- title : _t ( "Error" ) ,
170
- description : _t ( "Error decrypting attachment" ) ,
171
- } ) ;
172
- }
173
- } ;
174
221
175
222
// This button should actually Download because usercontent/ will try to click itself
176
223
// but it is not guaranteed between various browsers' settings.
177
224
return (
178
225
< span className = "mx_MFileBody" >
179
226
{ placeholder }
180
227
{ showDownloadLink && < div className = "mx_MFileBody_download" >
181
- < AccessibleButton onClick = { decrypt } >
182
- { _t ( "Decrypt %(text)s" , { text : text } ) }
228
+ < AccessibleButton onClick = { this . decryptFile } >
229
+ { _t ( "Decrypt %(text)s" , { text : this . linkText } ) }
183
230
</ AccessibleButton >
184
231
</ div > }
185
232
</ span >
186
233
) ;
187
234
}
188
235
189
- // When the iframe loads we tell it to render a download link
190
- const onIframeLoad = ( ev ) => {
191
- ev . target . contentWindow . postMessage ( {
192
- imgSrc : DOWNLOAD_ICON_URL ,
193
- imgStyle : null , // it handles this internally for us. Useful if a downstream changes the icon.
194
- style : computedStyle ( this . dummyLink . current ) ,
195
- blob : this . state . decryptedBlob ,
196
- // Set a download attribute for encrypted files so that the file
197
- // will have the correct name when the user tries to download it.
198
- // We can't provide a Content-Disposition header like we would for HTTP.
199
- download : fileName ,
200
- textContent : _t ( "Download %(text)s" , { text : text } ) ,
201
- // only auto-download if a user triggered this iframe explicitly
202
- auto : this . userDidClick ,
203
- } , "*" ) ;
204
- } ;
205
-
206
236
const url = "usercontent/" ; // XXX: this path should probably be passed from the skin
207
237
208
238
// If the attachment is encrypted then put the link inside an iframe.
@@ -218,9 +248,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
218
248
*/ }
219
249
< a ref = { this . dummyLink } />
220
250
</ div >
251
+ { /*
252
+ TODO: Move iframe (and dummy link) into FileDownloader.
253
+ We currently have it set up this way because of styles applied to the iframe
254
+ itself which cannot be easily handled/overridden by the FileDownloader. In
255
+ future, the download link may disappear entirely at which point it could also
256
+ be suitable to just remove this bit of code.
257
+ */ }
221
258
< iframe
222
259
src = { url }
223
- onLoad = { onIframeLoad }
260
+ onLoad = { ( ) => this . downloadFile ( this . fileName , this . linkText ) }
224
261
ref = { this . iframe }
225
262
sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation" />
226
263
</ div > }
@@ -259,7 +296,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
259
296
260
297
// We have to create an anchor to download the file
261
298
const tempAnchor = document . createElement ( 'a' ) ;
262
- tempAnchor . download = fileName ;
299
+ tempAnchor . download = this . fileName ;
263
300
tempAnchor . href = blobUrl ;
264
301
document . body . appendChild ( tempAnchor ) ; // for firefox: https://stackoverflow.com/a/32226068
265
302
tempAnchor . click ( ) ;
@@ -268,7 +305,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
268
305
} ;
269
306
} else {
270
307
// Else we are hoping the browser will do the right thing
271
- downloadProps [ "download" ] = fileName ;
308
+ downloadProps [ "download" ] = this . fileName ;
272
309
}
273
310
274
311
return (
@@ -277,16 +314,16 @@ export default class MFileBody extends React.Component<IProps, IState> {
277
314
{ showDownloadLink && < div className = "mx_MFileBody_download" >
278
315
< a { ...downloadProps } >
279
316
< span className = "mx_MFileBody_download_icon" />
280
- { _t ( "Download %(text)s" , { text : text } ) }
317
+ { _t ( "Download %(text)s" , { text : this . linkText } ) }
281
318
</ a >
282
319
{ this . props . tileShape === TileShape . FileGrid && < div className = "mx_MImageBody_size" >
283
- { content . info && content . info . size ? filesize ( content . info . size ) : "" }
320
+ { this . content . info && this . content . info . size ? filesize ( this . content . info . size ) : "" }
284
321
</ div > }
285
322
</ div > }
286
323
</ span >
287
324
) ;
288
325
} else {
289
- const extra = text ? ( ': ' + text ) : '' ;
326
+ const extra = this . linkText ? ( ': ' + this . linkText ) : '' ;
290
327
return < span className = "mx_MFileBody" >
291
328
{ placeholder }
292
329
{ _t ( "Invalid file%(extra)s" , { extra : extra } ) }
0 commit comments