@@ -7,7 +7,8 @@ use std::io::Cursor;
7
7
use std:: path:: Path ;
8
8
9
9
use anyhow:: { Context as _, anyhow} ;
10
- use arboard:: Clipboard ;
10
+ use arboard:: { Clipboard , ImageData } ;
11
+ use chrono:: { DateTime , Local , TimeZone } ;
11
12
use crokey:: Combiner ;
12
13
use crossterm:: event:: { KeyCode , KeyEvent } ;
13
14
use image:: codecs:: png:: PngEncoder ;
@@ -42,7 +43,7 @@ use crate::signal::{
42
43
SignalManager ,
43
44
} ;
44
45
use crate :: storage:: { MessageId , Storage } ;
45
- use crate :: util:: { self , ATTACHMENT_REGEX , LazyRegex , StatefulList , URL_REGEX } ;
46
+ use crate :: util:: { self , ATTACHMENT_REGEX , StatefulList , URL_REGEX } ;
46
47
47
48
pub struct App {
48
49
pub config : Config ,
@@ -53,8 +54,6 @@ pub struct App {
53
54
pub help_scroll : ( u16 , u16 ) ,
54
55
pub user_id : Uuid ,
55
56
pub should_quit : bool ,
56
- url_regex : LazyRegex ,
57
- attachment_regex : LazyRegex ,
58
57
display_help : bool ,
59
58
receipt_handler : ReceiptHandler ,
60
59
pub input : Input ,
@@ -116,8 +115,6 @@ impl App {
116
115
messages,
117
116
help_scroll : ( 0 , 0 ) ,
118
117
should_quit : false ,
119
- url_regex : LazyRegex :: new ( URL_REGEX ) ,
120
- attachment_regex : LazyRegex :: new ( ATTACHMENT_REGEX ) ,
121
118
display_help : false ,
122
119
receipt_handler : ReceiptHandler :: new ( ) ,
123
120
input : Default :: default ( ) ,
@@ -406,8 +403,7 @@ impl App {
406
403
let message = self
407
404
. storage
408
405
. message ( MessageId :: new ( * channel_id, * arrived_at) ) ?;
409
- let re = self . url_regex . compiled ( ) ;
410
- open_url ( & message, re) ?;
406
+ open_url ( & message, & URL_REGEX ) ?;
411
407
self . reset_message_selection ( ) ;
412
408
Some ( ( ) )
413
409
}
@@ -517,7 +513,9 @@ impl App {
517
513
518
514
fn send_input ( & mut self , channel_idx : usize ) {
519
515
let input = self . take_input ( ) ;
520
- let ( input, attachments) = self . extract_attachments ( & input) ;
516
+ let ( input, attachments) = Self :: extract_attachments ( & input, Local :: now ( ) , || {
517
+ self . clipboard . as_mut ( ) . map ( |c| c. get_image ( ) )
518
+ } ) ;
521
519
let channel_id = self . channels . items [ channel_idx] ;
522
520
let channel = self
523
521
. storage
@@ -1410,24 +1408,37 @@ impl App {
1410
1408
}
1411
1409
}
1412
1410
1413
- fn extract_attachments ( & mut self , input : & str ) -> ( String , Vec < ( AttachmentSpec , Vec < u8 > ) > ) {
1411
+ fn extract_attachments < Tz : TimeZone > (
1412
+ input : & str ,
1413
+ at : DateTime < Tz > ,
1414
+ mut get_clipboard_img : impl FnMut ( ) -> Option < Result < ImageData < ' static > , arboard:: Error > > ,
1415
+ ) -> ( String , Vec < ( AttachmentSpec , Vec < u8 > ) > )
1416
+ where
1417
+ Tz :: Offset : std:: fmt:: Display ,
1418
+ {
1414
1419
let mut offset = 0 ;
1415
1420
let mut clean_input = String :: new ( ) ;
1416
1421
1417
- let re = self . attachment_regex . compiled ( ) ;
1418
- let attachments = re. find_iter ( input) . filter_map ( |m| {
1422
+ let attachments = ATTACHMENT_REGEX . find_iter ( input) . filter_map ( |m| {
1419
1423
let path_str = m. as_str ( ) . strip_prefix ( "file://" ) ?;
1420
1424
1421
- let ( contents , content_type , file_name ) = if path_str . starts_with ( "clip" ) {
1422
- let img = self . clipboard . as_mut ( ) ? . get_image ( ) . ok ( ) ? ;
1425
+ clean_input . push_str ( input [ offset..m . start ( ) ] . trim_end ( ) ) ;
1426
+ offset = m . end ( ) ;
1423
1427
1424
- let png: ImageBuffer < Rgba < _ > , _ > =
1425
- ImageBuffer :: from_raw ( img. width as _ , img. height as _ , img. bytes ) ?;
1428
+ Some ( if path_str. starts_with ( "clip" ) {
1429
+ // clipboard
1430
+ let img = get_clipboard_img ( ) ?
1431
+ . inspect_err ( |error| error ! ( %error, "failed to get clipboard image" ) )
1432
+ . ok ( ) ?;
1433
+
1434
+ let width: u32 = img. width . try_into ( ) . ok ( ) ?;
1435
+ let height: u32 = img. height . try_into ( ) . ok ( ) ?;
1426
1436
1427
1437
let mut bytes = Vec :: new ( ) ;
1428
1438
let mut cursor = Cursor :: new ( & mut bytes) ;
1429
1439
let encoder = PngEncoder :: new ( & mut cursor) ;
1430
1440
1441
+ let png: ImageBuffer < Rgba < _ > , _ > = ImageBuffer :: from_raw ( width, height, img. bytes ) ?;
1431
1442
let data: Vec < _ > = png. into_raw ( ) . iter ( ) . map ( |b| b. swap_bytes ( ) ) . collect ( ) ;
1432
1443
encoder
1433
1444
. write_image (
@@ -1436,41 +1447,42 @@ impl App {
1436
1447
img. height as _ ,
1437
1448
image:: ExtendedColorType :: Rgba8 ,
1438
1449
)
1450
+ . inspect_err ( |error| error ! ( %error, "failed to encode image" ) )
1439
1451
. ok ( ) ?;
1440
1452
1441
- (
1442
- bytes,
1443
- "image/png" . to_string ( ) ,
1444
- Some ( "clipboard.png" . to_string ( ) ) ,
1445
- )
1453
+ let file_name = format ! ( "screenshot-{}.png" , at. format( "%Y-%m-%dT%H:%M:%S%z" ) ) ;
1454
+
1455
+ let spec = AttachmentSpec {
1456
+ content_type : "image/png" . to_owned ( ) ,
1457
+ length : bytes. len ( ) ,
1458
+ file_name : Some ( file_name) ,
1459
+ width : Some ( width) ,
1460
+ height : Some ( height) ,
1461
+ ..Default :: default ( )
1462
+ } ;
1463
+ ( spec, bytes)
1446
1464
} else {
1465
+ // path
1466
+
1467
+ // TODO: Show error to user if the file does not exist. This would prevent not
1468
+ // sending the attachment in the end.
1469
+
1447
1470
let path = Path :: new ( path_str) ;
1448
- let contents = std:: fs:: read ( path) . ok ( ) ?;
1471
+ let bytes = std:: fs:: read ( path) . ok ( ) ?;
1449
1472
let content_type = mime_guess:: from_path ( path)
1450
1473
. first ( )
1451
1474
. map ( |mime| mime. essence_str ( ) . to_string ( ) )
1452
1475
. unwrap_or_default ( ) ;
1453
1476
let file_name = path. file_name ( ) . map ( |f| f. to_string_lossy ( ) . into ( ) ) ;
1477
+ let spec = AttachmentSpec {
1478
+ content_type,
1479
+ length : bytes. len ( ) ,
1480
+ file_name,
1481
+ ..Default :: default ( )
1482
+ } ;
1454
1483
1455
- ( contents, content_type, file_name)
1456
- } ;
1457
-
1458
- clean_input. push_str ( input[ offset..m. start ( ) ] . trim_end ( ) ) ;
1459
- offset = m. end ( ) ;
1460
-
1461
- let spec = AttachmentSpec {
1462
- content_type,
1463
- length : contents. len ( ) ,
1464
- file_name,
1465
- preview : None ,
1466
- voice_note : None ,
1467
- borderless : None ,
1468
- width : None ,
1469
- height : None ,
1470
- caption : None ,
1471
- blur_hash : None ,
1472
- } ;
1473
- Some ( ( spec, contents) )
1484
+ ( spec, bytes)
1485
+ } )
1474
1486
} ) ;
1475
1487
1476
1488
let attachments = attachments. collect ( ) ;
@@ -1700,15 +1712,16 @@ fn add_emoji_from_sticker(body: &mut Option<String>, sticker: Option<Sticker>) {
1700
1712
1701
1713
#[ cfg( test) ]
1702
1714
pub ( crate ) mod tests {
1703
- use super :: * ;
1715
+ use chrono:: FixedOffset ;
1716
+ use std:: cell:: RefCell ;
1717
+ use std:: rc:: Rc ;
1704
1718
1705
1719
use crate :: config:: User ;
1706
1720
use crate :: data:: GroupData ;
1707
1721
use crate :: signal:: test:: SignalManagerMock ;
1708
1722
use crate :: storage:: { ForgetfulStorage , MemCache } ;
1709
1723
1710
- use std:: cell:: RefCell ;
1711
- use std:: rc:: Rc ;
1724
+ use super :: * ;
1712
1725
1713
1726
pub ( crate ) fn test_app ( ) -> (
1714
1727
App ,
@@ -1950,4 +1963,45 @@ pub(crate) mod tests {
1950
1963
assert_eq ! ( to_emoji( "☝🏿" ) , Some ( "☝🏿" ) ) ;
1951
1964
assert_eq ! ( to_emoji( "a" ) , None ) ;
1952
1965
}
1966
+
1967
+ #[ test]
1968
+ fn test_extract_attachments ( ) {
1969
+ let tempdir = tempfile:: tempdir ( ) . unwrap ( ) ;
1970
+ let image_png = tempdir. path ( ) . join ( "image.png" ) ;
1971
+ let image_jpg = tempdir. path ( ) . join ( "image.jpg" ) ;
1972
+
1973
+ std:: fs:: write ( & image_png, b"some png data" ) . unwrap ( ) ;
1974
+ std:: fs:: write ( & image_jpg, b"some jpg data" ) . unwrap ( ) ;
1975
+
1976
+ let clipboard_image = ImageData {
1977
+ width : 1 ,
1978
+ height : 1 ,
1979
+ bytes : vec ! [ 0 , 0 , 0 , 0 ] . into ( ) , // RGBA single pixel
1980
+ } ;
1981
+
1982
+ let message = format ! (
1983
+ "Hello, file://{} file://{} World! file://clip" ,
1984
+ image_png. display( ) ,
1985
+ image_jpg. display( ) ,
1986
+ ) ;
1987
+
1988
+ let at_str = "2023-01-01T00:00:00+0200" ;
1989
+ let at: DateTime < FixedOffset > = at_str. parse ( ) . unwrap ( ) ;
1990
+
1991
+ let ( cleaned_message, specs) =
1992
+ App :: extract_attachments ( & message, at, || Some ( Ok ( clipboard_image. clone ( ) ) ) ) ;
1993
+ assert_eq ! ( cleaned_message, "Hello, World!" ) ;
1994
+ dbg ! ( & specs) ;
1995
+
1996
+ assert_eq ! ( specs. len( ) , 3 ) ;
1997
+ assert_eq ! ( specs[ 0 ] . 0 . content_type, "image/png" ) ;
1998
+ assert_eq ! ( specs[ 0 ] . 0 . file_name, Some ( "image.png" . into( ) ) ) ;
1999
+ assert_eq ! ( specs[ 1 ] . 0 . content_type, "image/jpeg" ) ;
2000
+ assert_eq ! ( specs[ 1 ] . 0 . file_name, Some ( "image.jpg" . into( ) ) ) ;
2001
+ assert_eq ! ( specs[ 2 ] . 0 . content_type, "image/png" ) ;
2002
+ assert_eq ! (
2003
+ specs[ 2 ] . 0 . file_name,
2004
+ Some ( format!( "screenshot-{at_str}.png" ) )
2005
+ ) ;
2006
+ }
1953
2007
}
0 commit comments