1use ruma_macros::EventContent;
6use serde::{Deserialize, Serialize};
7
8use crate::{message::TextContentBlock, EmptyStateKey};
9
10#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
14#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
15#[ruma_event(type = "m.room.topic", kind = State, state_key_type = EmptyStateKey)]
16pub struct RoomTopicEventContent {
17 pub topic: String,
22
23 #[serde(rename = "m.topic", default, skip_serializing_if = "TopicContentBlock::is_empty")]
28 #[cfg_attr(
29 feature = "compat-lax-room-topic-deser",
30 serde(deserialize_with = "ruma_common::serde::default_on_error")
31 )]
32 pub topic_block: TopicContentBlock,
33}
34
35impl RoomTopicEventContent {
36 pub fn new(topic: String) -> Self {
38 Self { topic_block: TopicContentBlock::plain(topic.clone()), topic }
39 }
40
41 pub fn html(plain: impl Into<String>, html: impl Into<String>) -> Self {
43 let plain = plain.into();
44 Self { topic: plain.clone(), topic_block: TopicContentBlock::html(plain, html) }
45 }
46
47 #[cfg(feature = "markdown")]
52 pub fn markdown(topic: impl AsRef<str> + Into<String>) -> Self {
53 let plain = topic.as_ref().to_owned();
54 Self { topic: plain, topic_block: TopicContentBlock::markdown(topic) }
55 }
56}
57
58#[derive(Clone, Debug, Default, Serialize, Deserialize)]
63#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
64pub struct TopicContentBlock {
65 #[serde(rename = "m.text")]
67 pub text: TextContentBlock,
68}
69
70impl TopicContentBlock {
71 pub fn plain(body: impl Into<String>) -> Self {
73 Self { text: TextContentBlock::plain(body) }
74 }
75
76 pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
78 Self { text: TextContentBlock::html(body, html_body) }
79 }
80
81 #[cfg(feature = "markdown")]
86 pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
87 Self { text: TextContentBlock::markdown(body) }
88 }
89
90 fn is_empty(&self) -> bool {
92 self.text.is_empty()
93 }
94}
95
96impl From<TextContentBlock> for TopicContentBlock {
97 fn from(text: TextContentBlock) -> Self {
98 Self { text }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
105
106 use super::RoomTopicEventContent;
107 use crate::message::TextContentBlock;
108
109 #[test]
110 fn serialize_content() {
111 let mut content = RoomTopicEventContent::new("Hot Topic".to_owned());
113 assert_eq!(
114 to_json_value(&content).unwrap(),
115 json!({
116 "topic": "Hot Topic",
117 "m.topic": {
118 "m.text": [
119 { "body": "Hot Topic" },
120 ],
121 }
122 })
123 );
124
125 content.topic_block.text = TextContentBlock::from(vec![]);
127 assert_eq!(
128 to_json_value(&content).unwrap(),
129 json!({
130 "topic": "Hot Topic",
131 })
132 );
133
134 let content = RoomTopicEventContent::html("Hot Topic", "<strong>Hot</strong> Topic");
136 assert_eq!(
137 to_json_value(&content).unwrap(),
138 json!({
139 "topic": "Hot Topic",
140 "m.topic": {
141 "m.text": [
142 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
143 { "body": "Hot Topic" },
144 ],
145 }
146 })
147 );
148 }
149
150 #[test]
151 fn deserialize_content() {
152 let json = json!({
153 "topic": "Hot Topic",
154 "m.topic": {
155 "m.text": [
156 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
157 { "body": "Hot Topic" },
158 ],
159 }
160 });
161
162 let content = from_json_value::<RoomTopicEventContent>(json).unwrap();
163 assert_eq!(content.topic, "Hot Topic");
164 assert_eq!(content.topic_block.text.find_html(), Some("<strong>Hot</strong> Topic"));
165 assert_eq!(content.topic_block.text.find_plain(), Some("Hot Topic"));
166
167 let content = serde_json::from_str::<RoomTopicEventContent>(
168 r#"{"topic":"Hot Topic","m.topic":{"m.text":[{"body":"Hot Topic"}]}}"#,
169 )
170 .unwrap();
171 assert_eq!(content.topic, "Hot Topic");
172 assert_eq!(content.topic_block.text.find_html(), None);
173 assert_eq!(content.topic_block.text.find_plain(), Some("Hot Topic"));
174 }
175
176 #[test]
177 fn deserialize_event() {
178 let json = json!({
179 "content": {
180 "topic": "Hot Topic",
181 "m.topic": {
182 "m.text": [
183 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
184 { "body": "Hot Topic" },
185 ],
186 },
187 },
188 "type": "m.room.topic",
189 "state_key": "",
190 "event_id": "$lkioKdioukshnlDDz",
191 "sender": "@alice:localhost",
192 "origin_server_ts": 309_998_934,
193 });
194
195 from_json_value::<super::SyncRoomTopicEvent>(json).unwrap();
196 }
197
198 #[test]
199 #[cfg(feature = "compat-lax-room-topic-deser")]
200 fn deserialize_invalid_content() {
201 let json = json!({
202 "topic": "Hot Topic",
203 "m.topic": [
204 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
205 { "body": "Hot Topic" },
206 ],
207 });
208
209 let content = from_json_value::<RoomTopicEventContent>(json).unwrap();
210 assert_eq!(content.topic, "Hot Topic");
211 assert_eq!(content.topic_block.text.find_html(), None);
212 assert_eq!(content.topic_block.text.find_plain(), None);
213
214 let content = serde_json::from_str::<RoomTopicEventContent>(
215 r#"{"topic":"Hot Topic","m.topic":[{"body":"Hot Topic"}]}"#,
216 )
217 .unwrap();
218 assert_eq!(content.topic, "Hot Topic");
219 assert_eq!(content.topic_block.text.find_html(), None);
220 assert_eq!(content.topic_block.text.find_plain(), None);
221 }
222
223 #[test]
224 #[cfg(feature = "compat-lax-room-topic-deser")]
225 fn deserialize_invalid_event() {
226 let json = json!({
227 "content": {
228 "topic": "Hot Topic",
229 "m.topic": [
230 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
231 { "body": "Hot Topic" },
232 ],
233 },
234 "type": "m.room.topic",
235 "state_key": "",
236 "event_id": "$lkioKdioukshnlDDz",
237 "sender": "@alice:localhost",
238 "origin_server_ts": 309_998_934,
239 });
240
241 from_json_value::<super::SyncRoomTopicEvent>(json).unwrap();
242 }
243}