J7PC7UGJE64XZIJXE5YETSHYZUUHSH2B52OA5E5JVQUIF6P72JAQC
checksum = "238ce071d267c5710f9d31451efec16c5ee22de34df17cc05e56cbc92e967117"
checksum = "b78a366903f506d2ad52ca8dc552102ffdd3e937ba8a227f024dc1d1eae28575"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
infer = { version = "0.3.0", default-features = false }
}
pub fn infer_mimetype(data: &[u8]) -> String {
infer::get(&data)
.map(|filetype| filetype.mime_type().to_string())
.unwrap_or_else(|| String::from("application/octet-stream"))
}
pub fn get_filename(path: impl AsRef<Path>) -> String {
path.as_ref()
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| String::from("unknown"))
fn download_thumbnail_for_event(tevent: &TimelineEvent) -> Option<(bool, Uri)> {
if let Some(thumbnail_url) = tevent.thumbnail_url() {
if make_content_path(&thumbnail_url).exists() {
Some((true, thumbnail_url))
} else {
Some((false, thumbnail_url))
}
} else if let (Some(content_size), Some(content_url)) =
(tevent.content_size(), tevent.content_url())
{
if make_content_path(&content_url).exists() {
Some((true, content_url))
} else if content_size < 1000 * 1000 {
Some((false, content_url))
} else {
None
}
} else {
None
}
}
for ev in events_after.iter().chain(events_before.iter()) {
if let Some(content_url) = ev.thumbnail_url() {
if make_content_path(&content_url).exists() {
read_urls.push(content_url)
} else {
download_urls.push(content_url);
}
}
}
thumbnails = events_after
.iter()
.chain(events_before.iter())
.flat_map(|tevent| Client::download_thumbnail_for_event(tevent))
.collect::<Vec<_>>();
pub fn process_sync_response(
&mut self,
response: sync_events::Response,
) -> (Vec<Uri>, Vec<Uri>) {
let mut download_urls = vec![];
let mut read_urls = vec![];
pub fn process_sync_response(&mut self, response: sync_events::Response) -> Vec<(bool, Uri)> {
let mut thumbnails = vec![];
if let Some(content_url) = tevent.thumbnail_url() {
if make_content_path(&content_url).exists() {
read_urls.push(content_url)
} else {
download_urls.push(content_url);
}
if let Some(thumbnail_data) = Client::download_thumbnail_for_event(&tevent) {
thumbnails.push(thumbnail_data);
}
}
None
}
pub fn content_size(&self) -> Option<usize> {
if let Some(content) = self.message_content() {
if let AnyMessageEventContent::RoomMessage(content) = content {
return match content {
MessageEventContent::Image(image) => {
image.info.map(|i| i.size.map(|s| u64::from(s) as usize))
}
MessageEventContent::Video(video) => {
video.info.map(|i| i.size.map(|s| u64::from(s) as usize))
}
MessageEventContent::Audio(audio) => {
audio.info.map(|i| i.size.map(|s| u64::from(s) as usize))
}
MessageEventContent::File(file) => {
file.info.map(|i| i.size.map(|s| u64::from(s) as usize))
}
_ => None,
}
.flatten();
let content_url = image.url;
image.info.map(|i| {
let thumbnail_url = i.thumbnail_url;
// TODO: check if thumbnail is below a size limit?
// Look at the spec
let content_size = i.size;
thumbnail_url.unwrap_or_else(|| {
if let Some(url) = content_url {
if let Some(size) = content_size {
if size < UInt::from(1000_u32 * 1000) {
return url;
}
}
}
String::new()
})
})
image.info.map(|i| i.thumbnail_url).flatten()
if let Some(thumbnail_image) = timeline_event
.thumbnail_url()
.map(|thumbnail_url| thumbnail_store.get_thumbnail(&thumbnail_url))
.unwrap_or(None)
.map(|handle| Image::new(handle.clone()).width(Length::Fill))
if let Some(thumbnail_image) = {
if is_thumbnail {
Some(content_url.clone())
} else {
timeline_event.thumbnail_url()
}
}
.map(|thumbnail_url| {
thumbnail_store
.get_thumbnail(&thumbnail_url)
.map(|handle| Image::new(handle.clone()).width(Length::Fill))
})
.flatten()
events::{room::message::MessageEventContent, AnyMessageEventContent},
events::room::message::FileMessageEventContent,
events::room::message::VideoInfo,
events::room::message::VideoMessageEventContent,
events::room::ThumbnailInfo,
events::{
room::{
message::{
AudioInfo, AudioMessageEventContent, FileInfo, ImageMessageEventContent,
MessageEventContent,
},
ImageInfo,
},
AnyMessageEventContent,
},
let path = make_content_path(&content_url);
return if path.exists() {
Command::perform(async move { Ok(path) }, process_path_result)
let content_path = media::make_content_path(&content_url);
return if content_path.exists() {
Command::perform(async move { Ok(content_path) }, process_path_result)
tokio::fs::write(&path, raw_data.as_slice()).await?;
Ok(if is_thumbnail {
Some((path, content_url, raw_data))
} else {
None
})
tokio::fs::write(&content_path, raw_data.as_slice()).await?;
Ok((
content_path,
if is_thumbnail {
Some((content_url, raw_data))
} else {
None
},
))
Message::SendFile => {
/*
TODO: Investigate implementing a file picker widget for iced
(we just put it in as an overlay)
TODO: actually implement this
1. Detect what type of file this is (and create a thumbnail if it's a video / image)
2. Upload the file to matrix (and the thumbnail if there is one)
3. Hardlink the source to our cache (or copy if FS doesn't support)
- this is so that even if the user deletes the file it will be in our cache
- (and we won't need to download it again)
4. Create `MessageEventContent::Image(ImageMessageEventContent {...});` for each file
- set `body` field to whatever is in `self.message`?,
- use the MXC URL(s) we got when we uploaded our file(s)
5. Send the message(s)!
*/
let file_select = tokio::task::spawn_blocking(
|| -> Result<Vec<PathBuf>, nfd2::error::NFDError> {
let paths = match nfd2::dialog_multiple().open()? {
Message::SendMessageComposer(room_id) => {
if !self.message.is_empty() {
let content =
MessageEventContent::text_plain(self.message.drain(..).collect::<String>());
scroll_to_bottom(self, room_id.clone());
self.event_history_state.scroll_to_bottom();
return Command::perform(
async move { (content, room_id) },
|(content, room_id)| {
super::Message::MainScreen(Message::SendMessage {
content: vec![content],
room_id,
})
},
);
}
}
Message::SendFile(room_id) => {
let file_select_task =
tokio::task::spawn_blocking(|| -> Result<Vec<PathBuf>, ClientError> {
let paths = match nfd2::dialog_multiple()
.open()
.map_err(|e| ClientError::Custom(e.to_string()))?
{
},
);
});
let inner = self.client.inner();
return Command::perform(
async move {
let paths = file_select_task
.await
.map_err(|e| ClientError::Custom(e.to_string()))??;
let mut content_urls_to_send = Vec::with_capacity(paths.len());
for path in paths {
match tokio::fs::read(&path).await {
Ok(data) => {
let file_mimetype = media::infer_mimetype(&data);
let filesize = data.len();
let filename = media::get_filename(&path);
// TODO: implement video thumbnailing
let (thumbnail, image_info) = if let ContentType::Image =
ContentType::new(&file_mimetype)
{
if let Ok(image) = image::load_from_memory(&data) {
let image_width = image.width();
let image_height = image.height();
let image_dimensions =
Some((image_height, image_width));
let thumbnail_scale = ((1000 * 1000) / filesize) as u32;
if thumbnail_scale <= 1 {
let new_width = image_width * thumbnail_scale;
let new_height = image_height * thumbnail_scale;
let thumbnail =
image.thumbnail(new_width, new_height);
let thumbnail_height = thumbnail.height();
let thumbnail_width = thumbnail.width();
let thumbnail_raw = thumbnail.to_bytes();
let thumbnail_size = thumbnail_raw.len();
let send_result = Client::send_content(
inner.clone(),
thumbnail_raw,
Some(file_mimetype.clone()),
Some(format!("thumbnail_{}", filename)),
)
.await;
match send_result {
Ok(thumbnail_url) => (
Some((
thumbnail_url,
thumbnail_size,
thumbnail_height,
thumbnail_width,
)),
image_dimensions,
),
Err(err) => {
log::error!("An error occured while uploading a thumbnail: {}", err);
(None, image_dimensions)
}
}
} else {
(None, image_dimensions)
}
} else {
(None, None)
}
} else {
(None, None)
};
let send_result = Client::send_content(
inner.clone(),
data,
Some(file_mimetype.clone()),
Some(filename.clone()),
)
.await;
// placeholder
return Command::perform(file_select, |result| {
match result {
Ok(file_picker_result) => {
if let Ok(paths) = file_picker_result {
println!("User selected paths: {:?}", paths);
match send_result {
Ok(content_url) => {
if let Err(err) = tokio::fs::create_dir_all(
media::make_content_folder(&content_url),
)
.await
{
log::warn!("An IO error occured while trying to create a folder to hard link a file you tried to upload: {}", err);
}
if let Err(err) = tokio::fs::hard_link(
&path,
media::make_content_path(&content_url),
)
.await
{
log::warn!("An IO error occured while hard linking a file you tried to upload (this may result in a duplication of the file): {}", err);
}
content_urls_to_send.push((
content_url,
filename,
file_mimetype,
filesize,
thumbnail,
image_info,
));
}
Err(err) => {
log::error!(
"An error occured while trying to upload a file: {}",
err
);
}
}
}
Err(err) => {
log::error!(
"An IO error occured while trying to upload a file: {}",
err
);
}
Err(err) => {
log::error!(
"Error occured while processing file picker task result: {}",
err
);
Ok((content_urls_to_send, room_id))
},
|result| match result {
Ok((content_urls_to_send, room_id)) => {
super::Message::MainScreen(Message::SendMessage {
content: content_urls_to_send
.into_iter()
.map(
|(
url,
filename,
file_mimetype,
filesize,
thumbnail,
image_dimensions,
)| {
let (thumbnail_url, thumbnail_info) =
if let Some((url, size, h, w)) = thumbnail {
(
Some(url.to_string()),
Some(Box::new(ThumbnailInfo {
height: Some(ruma::UInt::from(h)),
width: Some(ruma::UInt::from(w)),
mimetype: Some(file_mimetype.clone()),
size: ruma::UInt::new(size as u64),
})),
)
} else {
(None, None)
};
println!("thumbnail_url: {:?}", thumbnail_url);
let body = filename;
let mimetype = Some(file_mimetype.clone());
let url = Some(url.to_string());
match ContentType::new(&file_mimetype) {
ContentType::Image => MessageEventContent::Image(
ImageMessageEventContent {
body,
info: Some(Box::new(ImageInfo {
mimetype,
height: image_dimensions
.map(|(h, _)| ruma::UInt::from(h)),
width: image_dimensions
.map(|(_, w)| ruma::UInt::from(w)),
size: ruma::UInt::new(filesize as u64),
thumbnail_info,
thumbnail_url,
thumbnail_file: None,
})),
url,
file: None,
},
),
ContentType::Audio => MessageEventContent::Audio(
AudioMessageEventContent {
body,
info: Some(Box::new(AudioInfo {
duration: None,
mimetype,
size: ruma::UInt::new(filesize as u64),
})),
url,
file: None,
},
),
ContentType::Video => MessageEventContent::Video(
VideoMessageEventContent {
body,
info: Some(Box::new(VideoInfo {
mimetype,
height: None,
width: None,
duration: None,
size: ruma::UInt::new(filesize as u64),
thumbnail_info,
thumbnail_url,
thumbnail_file: None,
})),
url,
file: None,
},
),
ContentType::Other => MessageEventContent::File(
FileMessageEventContent {
body: body.clone(),
filename: Some(body),
info: Some(Box::new(FileInfo {
mimetype,
size: ruma::UInt::new(filesize as u64),
thumbnail_info,
thumbnail_url,
thumbnail_file: None,
})),
url,
file: None,
},
),
}
},
)
.collect(),
room_id,
})
Message::SendMessage => {
if !self.message.is_empty() {
let content =
MessageEventContent::text_plain(self.message.drain(..).collect::<String>());
if let Some(Some((inner, room_id))) = self.current_room_id.clone().map(|id| {
if self.client.has_room(&id) {
Some((self.client.inner(), id))
} else {
None
}
}) {
scroll_to_bottom(self, room_id.clone());
self.prev_scroll_perc = 1.0;
self.event_history_state.scroll_to_bottom();
Message::SendMessage { content, room_id } => {
let mut commands = Vec::with_capacity(content.len());
for content in content {
if self.client.has_room(&room_id) {
let inner = self.client.inner();
return Command::perform(
async move {
(
Client::send_message(
inner,
content,
room_id.clone(),
commands.push(Command::perform(
{
let room_id = room_id.clone();
async move {
(
Client::send_message(
inner,
content,
room_id.clone(),
transaction_id,
)
.await,
return Command::batch(
download_urls
.into_iter()
.map(|url| make_download_content_com(self.client.inner(), url))
.chain(
read_urls
.into_iter()
.map(|url| make_read_thumbnail_com(url)),
),
);
return Command::batch(thumbnail_urls.into_iter().map(
|(is_in_cache, thumbnail_url)| {
if is_in_cache {
make_read_thumbnail_com(thumbnail_url)
} else {
make_download_content_com(self.client.inner(), thumbnail_url)
}
},
));
return Command::batch(
download_urls
.into_iter()
.map(|url| make_download_content_com(self.client.inner(), url))
.chain(
read_urls
.into_iter()
.map(|url| make_read_thumbnail_com(url)),
),
);
return Command::batch(thumbnail_urls.into_iter().map(
|(is_in_cache, thumbnail_url)| {
if is_in_cache {
make_read_thumbnail_com(thumbnail_url)
} else {
make_download_content_com(self.client.inner(), thumbnail_url)
}
},
));