Skip to content

Commit 34d71b0

Browse files
Johennesandybalaam
authored andcommitted
feat(composer): add support for attachments in drafts
Signed-off-by: Johannes Marbach <[email protected]>
1 parent 430304f commit 34d71b0

File tree

8 files changed

+372
-16
lines changed

8 files changed

+372
-16
lines changed

bindings/matrix-sdk-ffi/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ All notable changes to this project will be documented in this file.
2727
current device scanning or generating the QR code. Additionally, new errors `HumanQrLoginError::CheckCodeAlreadySent`
2828
and `HumanQrLoginError::CheckCodeCannotBeSent` were added.
2929
([#5786](https://github.com/matrix-org/matrix-rust-sdk/pull/5786))
30+
- `ComposerDraft` now includes attachments alongside the text message.
31+
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
3032

3133
### Features:
3234

bindings/matrix-sdk-ffi/src/room/mod.rs

Lines changed: 248 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, pin::pin, sync::Arc};
1+
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
22

33
use anyhow::{Context, Result};
44
use futures_util::{pin_mut, StreamExt};
@@ -9,7 +9,8 @@ use matrix_sdk::{
99
TryFromReportedContentScoreError,
1010
},
1111
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
12-
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
12+
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
13+
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
1314
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
1415
SuccessorRoom as SdkSuccessorRoom,
1516
};
@@ -45,11 +46,14 @@ use crate::{
4546
live_location_share::{LastLocation, LiveLocationShare},
4647
room_member::{RoomMember, RoomMemberWithSenderInfo},
4748
room_preview::RoomPreview,
48-
ruma::{ImageInfo, LocationContent, MediaSource},
49+
ruma::{
50+
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
51+
},
4952
runtime::get_runtime_handle,
5053
timeline::{
5154
configuration::{TimelineConfiguration, TimelineFilter},
5255
AbstractProgress, EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
56+
UploadSource,
5357
},
5458
utils::{u64_to_uint, AsyncRuntimeDropped},
5559
TaskHandle,
@@ -1442,21 +1446,257 @@ pub struct ComposerDraft {
14421446
pub html_text: Option<String>,
14431447
/// The type of draft.
14441448
pub draft_type: ComposerDraftType,
1449+
/// Attachments associated with this draft.
1450+
pub attachments: Vec<DraftAttachment>,
14451451
}
14461452

14471453
impl From<SdkComposerDraft> for ComposerDraft {
14481454
fn from(value: SdkComposerDraft) -> Self {
1449-
let SdkComposerDraft { plain_text, html_text, draft_type } = value;
1450-
Self { plain_text, html_text, draft_type: draft_type.into() }
1455+
let SdkComposerDraft { plain_text, html_text, draft_type, attachments } = value;
1456+
Self {
1457+
plain_text,
1458+
html_text,
1459+
draft_type: draft_type.into(),
1460+
attachments: attachments.into_iter().map(|a| a.into()).collect(),
1461+
}
14511462
}
14521463
}
14531464

14541465
impl TryFrom<ComposerDraft> for SdkComposerDraft {
1455-
type Error = ruma::IdParseError;
1466+
type Error = ClientError;
14561467

14571468
fn try_from(value: ComposerDraft) -> std::result::Result<Self, Self::Error> {
1458-
let ComposerDraft { plain_text, html_text, draft_type } = value;
1459-
Ok(Self { plain_text, html_text, draft_type: draft_type.try_into()? })
1469+
let ComposerDraft { plain_text, html_text, draft_type, attachments } = value;
1470+
Ok(Self {
1471+
plain_text,
1472+
html_text,
1473+
draft_type: draft_type.try_into()?,
1474+
attachments: attachments
1475+
.into_iter()
1476+
.map(|a| a.try_into())
1477+
.collect::<std::result::Result<Vec<_>, _>>()?,
1478+
})
1479+
}
1480+
}
1481+
1482+
/// An attachment stored with a composer draft.
1483+
#[derive(uniffi::Enum)]
1484+
pub enum DraftAttachment {
1485+
Audio { audio_info: AudioInfo, source: UploadSource },
1486+
File { file_info: FileInfo, source: UploadSource },
1487+
Image { image_info: ImageInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
1488+
Video { video_info: VideoInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
1489+
}
1490+
1491+
impl From<SdkDraftAttachment> for DraftAttachment {
1492+
fn from(value: SdkDraftAttachment) -> Self {
1493+
match value.content {
1494+
DraftAttachmentContent::Image {
1495+
data,
1496+
mimetype,
1497+
size,
1498+
width,
1499+
height,
1500+
blurhash,
1501+
thumbnail,
1502+
} => {
1503+
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
1504+
bytes: t.data.clone(),
1505+
filename: t.filename.clone(),
1506+
});
1507+
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
1508+
width: t.width,
1509+
height: t.height,
1510+
mimetype: t.mimetype,
1511+
size: t.size,
1512+
});
1513+
DraftAttachment::Image {
1514+
image_info: ImageInfo {
1515+
height,
1516+
width,
1517+
mimetype,
1518+
size,
1519+
thumbnail_info,
1520+
thumbnail_source: None,
1521+
blurhash,
1522+
is_animated: None,
1523+
},
1524+
source: UploadSource::Data { bytes: data, filename: value.filename },
1525+
thumbnail_source,
1526+
}
1527+
}
1528+
DraftAttachmentContent::Video {
1529+
data,
1530+
mimetype,
1531+
size,
1532+
width,
1533+
height,
1534+
duration,
1535+
blurhash,
1536+
thumbnail,
1537+
} => {
1538+
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
1539+
bytes: t.data.clone(),
1540+
filename: t.filename.clone(),
1541+
});
1542+
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
1543+
width: t.width,
1544+
height: t.height,
1545+
mimetype: t.mimetype,
1546+
size: t.size,
1547+
});
1548+
DraftAttachment::Video {
1549+
video_info: VideoInfo {
1550+
duration,
1551+
height,
1552+
width,
1553+
mimetype,
1554+
size,
1555+
thumbnail_info,
1556+
thumbnail_source: None,
1557+
blurhash,
1558+
},
1559+
source: UploadSource::Data { bytes: data, filename: value.filename },
1560+
thumbnail_source,
1561+
}
1562+
}
1563+
DraftAttachmentContent::Audio { data, mimetype, size, duration } => {
1564+
DraftAttachment::Audio {
1565+
audio_info: AudioInfo { duration, size, mimetype },
1566+
source: UploadSource::Data { bytes: data, filename: value.filename },
1567+
}
1568+
}
1569+
DraftAttachmentContent::File { data, mimetype, size } => DraftAttachment::File {
1570+
file_info: FileInfo {
1571+
mimetype,
1572+
size,
1573+
thumbnail_info: None,
1574+
thumbnail_source: None,
1575+
},
1576+
source: UploadSource::Data { bytes: data, filename: value.filename },
1577+
},
1578+
}
1579+
}
1580+
}
1581+
1582+
/// Resolve the bytes and filename from an `UploadSource`, reading the file
1583+
/// contents if needed.
1584+
fn read_upload_source(source: UploadSource) -> Result<(Vec<u8>, String), ClientError> {
1585+
match source {
1586+
UploadSource::Data { bytes, filename } => Ok((bytes, filename)),
1587+
UploadSource::File { filename } => {
1588+
let path: PathBuf = filename.into();
1589+
let filename = path
1590+
.file_name()
1591+
.ok_or(ClientError::Generic {
1592+
msg: "Invalid attachment path".to_owned(),
1593+
details: None,
1594+
})?
1595+
.to_str()
1596+
.ok_or(ClientError::Generic {
1597+
msg: "Invalid attachment path".to_owned(),
1598+
details: None,
1599+
})?
1600+
.to_owned();
1601+
1602+
let bytes = fs::read(&path).map_err(|_| ClientError::Generic {
1603+
msg: "Could not load file".to_owned(),
1604+
details: None,
1605+
})?;
1606+
1607+
Ok((bytes, filename))
1608+
}
1609+
}
1610+
}
1611+
1612+
impl TryFrom<DraftAttachment> for SdkDraftAttachment {
1613+
type Error = ClientError;
1614+
1615+
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
1616+
match value {
1617+
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
1618+
let (data, filename) = read_upload_source(source)?;
1619+
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
1620+
(Some(info), Some(source)) => {
1621+
let (data, filename) = read_upload_source(source)?;
1622+
Some(DraftThumbnail {
1623+
filename,
1624+
data,
1625+
mimetype: info.mimetype,
1626+
width: info.width,
1627+
height: info.height,
1628+
size: info.size,
1629+
})
1630+
}
1631+
_ => None,
1632+
};
1633+
Ok(Self {
1634+
filename,
1635+
content: DraftAttachmentContent::Image {
1636+
data,
1637+
mimetype: image_info.mimetype,
1638+
size: image_info.size,
1639+
width: image_info.width,
1640+
height: image_info.height,
1641+
blurhash: image_info.blurhash,
1642+
thumbnail,
1643+
},
1644+
})
1645+
}
1646+
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
1647+
let (data, filename) = read_upload_source(source)?;
1648+
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
1649+
(Some(info), Some(source)) => {
1650+
let (data, filename) = read_upload_source(source)?;
1651+
Some(DraftThumbnail {
1652+
filename,
1653+
data,
1654+
mimetype: info.mimetype,
1655+
width: info.width,
1656+
height: info.height,
1657+
size: info.size,
1658+
})
1659+
}
1660+
_ => None,
1661+
};
1662+
Ok(Self {
1663+
filename,
1664+
content: DraftAttachmentContent::Video {
1665+
data,
1666+
mimetype: video_info.mimetype,
1667+
size: video_info.size,
1668+
width: video_info.width,
1669+
height: video_info.height,
1670+
duration: video_info.duration,
1671+
blurhash: video_info.blurhash,
1672+
thumbnail,
1673+
},
1674+
})
1675+
}
1676+
DraftAttachment::Audio { audio_info, source, .. } => {
1677+
let (data, filename) = read_upload_source(source)?;
1678+
Ok(Self {
1679+
filename,
1680+
content: DraftAttachmentContent::Audio {
1681+
data,
1682+
mimetype: audio_info.mimetype,
1683+
size: audio_info.size,
1684+
duration: audio_info.duration,
1685+
},
1686+
})
1687+
}
1688+
DraftAttachment::File { file_info, source, .. } => {
1689+
let (data, filename) = read_upload_source(source)?;
1690+
Ok(Self {
1691+
filename,
1692+
content: DraftAttachmentContent::File {
1693+
data,
1694+
mimetype: file_info.mimetype,
1695+
size: file_info.size,
1696+
},
1697+
})
1698+
}
1699+
}
14601700
}
14611701
}
14621702

crates/matrix-sdk-base/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ All notable changes to this project will be documented in this file.
1111
- `Client::sync_lock` has been renamed `Client::state_store_lock`.
1212
([#5707](https://github.com/matrix-org/matrix-rust-sdk/pull/5707))
1313

14+
### Features
15+
16+
- `ComposerDraft` can now store attachments alongside text messages.
17+
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
18+
1419
## [0.14.1] - 2025-09-10
1520

1621
### Security Fixes

crates/matrix-sdk-base/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ pub use room::{
6464
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
6565
};
6666
pub use store::{
67-
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
68-
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
67+
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
68+
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
69+
ThreadSubscriptionCatchupToken,
6970
};
7071
pub use utils::{
7172
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,

crates/matrix-sdk-base/src/store/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ pub use self::{
9494
SentMediaInfo, SentRequestKey, SerializableEventContent,
9595
},
9696
traits::{
97-
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerInfo, StateStore,
98-
StateStoreDataKey, StateStoreDataValue, StateStoreExt, ThreadSubscriptionCatchupToken,
99-
WellKnownResponse,
97+
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
98+
DynStateStore, IntoStateStore, ServerInfo, StateStore, StateStoreDataKey,
99+
StateStoreDataValue, StateStoreExt, ThreadSubscriptionCatchupToken, WellKnownResponse,
100100
},
101101
};
102102

0 commit comments

Comments
 (0)