1use ruma_macros::EventContent;
6use serde::{Deserialize, Serialize};
7
8use crate::{EmptyStateKey, message::TextContentBlock};
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 ruma_common::canonical_json::assert_to_canonical_json_eq;
105 use serde_json::{from_value as from_json_value, json};
106
107 use super::RoomTopicEventContent;
108 use crate::message::TextContentBlock;
109
110 #[test]
111 fn serialize_content() {
112 let mut content = RoomTopicEventContent::new("Hot Topic".to_owned());
114 assert_to_canonical_json_eq!(
115 content,
116 json!({
117 "topic": "Hot Topic",
118 "m.topic": {
119 "m.text": [
120 { "body": "Hot Topic" },
121 ],
122 }
123 })
124 );
125
126 content.topic_block.text = TextContentBlock::from(vec![]);
128 assert_to_canonical_json_eq!(
129 content,
130 json!({
131 "topic": "Hot Topic",
132 })
133 );
134
135 let content = RoomTopicEventContent::html("Hot Topic", "<strong>Hot</strong> Topic");
137 assert_to_canonical_json_eq!(
138 content,
139 json!({
140 "topic": "Hot Topic",
141 "m.topic": {
142 "m.text": [
143 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
144 { "body": "Hot Topic" },
145 ],
146 }
147 })
148 );
149 }
150
151 #[test]
152 fn deserialize_content() {
153 let json = json!({
154 "topic": "Hot Topic",
155 "m.topic": {
156 "m.text": [
157 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
158 { "body": "Hot Topic" },
159 ],
160 }
161 });
162
163 let content = from_json_value::<RoomTopicEventContent>(json).unwrap();
164 assert_eq!(content.topic, "Hot Topic");
165 assert_eq!(content.topic_block.text.find_html(), Some("<strong>Hot</strong> Topic"));
166 assert_eq!(content.topic_block.text.find_plain(), Some("Hot Topic"));
167
168 let content = serde_json::from_str::<RoomTopicEventContent>(
169 r#"{"topic":"Hot Topic","m.topic":{"m.text":[{"body":"Hot Topic"}]}}"#,
170 )
171 .unwrap();
172 assert_eq!(content.topic, "Hot Topic");
173 assert_eq!(content.topic_block.text.find_html(), None);
174 assert_eq!(content.topic_block.text.find_plain(), Some("Hot Topic"));
175 }
176
177 #[test]
178 fn deserialize_event() {
179 let json = json!({
180 "content": {
181 "topic": "Hot Topic",
182 "m.topic": {
183 "m.text": [
184 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
185 { "body": "Hot Topic" },
186 ],
187 },
188 },
189 "type": "m.room.topic",
190 "state_key": "",
191 "event_id": "$lkioKdioukshnlDDz",
192 "sender": "@alice:localhost",
193 "origin_server_ts": 309_998_934,
194 });
195
196 from_json_value::<super::SyncRoomTopicEvent>(json).unwrap();
197 }
198
199 #[test]
200 #[cfg(feature = "compat-lax-room-topic-deser")]
201 fn deserialize_invalid_content() {
202 let json = json!({
203 "topic": "Hot Topic",
204 "m.topic": [
205 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
206 { "body": "Hot Topic" },
207 ],
208 });
209
210 let content = from_json_value::<RoomTopicEventContent>(json).unwrap();
211 assert_eq!(content.topic, "Hot Topic");
212 assert_eq!(content.topic_block.text.find_html(), None);
213 assert_eq!(content.topic_block.text.find_plain(), None);
214
215 let content = serde_json::from_str::<RoomTopicEventContent>(
216 r#"{"topic":"Hot Topic","m.topic":[{"body":"Hot Topic"}]}"#,
217 )
218 .unwrap();
219 assert_eq!(content.topic, "Hot Topic");
220 assert_eq!(content.topic_block.text.find_html(), None);
221 assert_eq!(content.topic_block.text.find_plain(), None);
222 }
223
224 #[test]
225 #[cfg(feature = "compat-lax-room-topic-deser")]
226 fn deserialize_invalid_event() {
227 let json = json!({
228 "content": {
229 "topic": "Hot Topic",
230 "m.topic": [
231 { "body": "<strong>Hot</strong> Topic", "mimetype": "text/html" },
232 { "body": "Hot Topic" },
233 ],
234 },
235 "type": "m.room.topic",
236 "state_key": "",
237 "event_id": "$lkioKdioukshnlDDz",
238 "sender": "@alice:localhost",
239 "origin_server_ts": 309_998_934,
240 });
241
242 from_json_value::<super::SyncRoomTopicEvent>(json).unwrap();
243 }
244}