1use js_int::{UInt, uint};
6use ruma_macros::EventContent;
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
13#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
14#[ruma_event(type = "m.recent_emoji", kind = GlobalAccountData)]
15pub struct RecentEmojiEventContent {
16 #[serde(default, deserialize_with = "ruma_common::serde::ignore_invalid_vec_items")]
18 pub recent_emoji: Vec<RecentEmoji>,
19}
20
21impl RecentEmojiEventContent {
22 pub const RECOMMENDED_MAX_LEN: usize = 100;
24
25 pub fn new(recent_emoji: Vec<RecentEmoji>) -> Self {
27 Self { recent_emoji }
28 }
29
30 pub fn increment_emoji_total(&mut self, emoji: &str) {
41 self.recent_emoji.truncate(Self::RECOMMENDED_MAX_LEN);
44
45 if let Some(position) = self.recent_emoji.iter().position(|e| e.emoji == emoji) {
46 let total = &mut self.recent_emoji[position].total;
47 *total = (*total).saturating_add(uint!(1));
48
49 if position > 0 {
50 let emoji = self.recent_emoji.remove(position);
51 self.recent_emoji.insert(0, emoji);
52 }
53 } else {
54 let emoji = RecentEmoji::new(emoji.to_owned());
55 self.recent_emoji.insert(0, emoji);
56
57 self.recent_emoji.truncate(Self::RECOMMENDED_MAX_LEN);
59 }
60 }
61
62 pub fn recent_emoji_sorted_by_total(&self) -> Vec<RecentEmoji> {
68 let mut recent_emoji =
69 self.recent_emoji.iter().take(Self::RECOMMENDED_MAX_LEN).cloned().collect::<Vec<_>>();
70 recent_emoji.sort_by(|lhs, rhs| rhs.total.cmp(&lhs.total));
72 recent_emoji
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
79pub struct RecentEmoji {
80 pub emoji: String,
82
83 pub total: UInt,
85}
86
87impl RecentEmoji {
88 pub fn new(emoji: String) -> Self {
92 Self { emoji, total: uint!(1) }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use assert_matches2::assert_matches;
99 use js_int::uint;
100 use ruma_common::canonical_json::assert_to_canonical_json_eq;
101 use serde_json::{from_value as from_json_value, json};
102
103 use super::{RecentEmoji, RecentEmojiEventContent};
104 use crate::AnyGlobalAccountDataEvent;
105
106 #[test]
107 fn recent_emoji_serialization() {
108 let content = RecentEmojiEventContent::new([RecentEmoji::new("😎".to_owned())].into());
109
110 assert_to_canonical_json_eq!(
111 content,
112 json!({
113 "recent_emoji": [{
114 "emoji": "😎",
115 "total": 1,
116 }],
117 }),
118 );
119 }
120
121 #[test]
122 fn recent_emoji_deserialization() {
123 let json = json!({
124 "content": {
125 "recent_emoji": [
126 {
127 "emoji": "😎",
128 "total": 1,
129 },
130 {
132 "emoji": "🏠",
133 "total": -1,
134 },
135 ],
136 },
137 "type": "m.recent_emoji",
138 });
139
140 assert_matches!(
141 from_json_value::<AnyGlobalAccountDataEvent>(json),
142 Ok(AnyGlobalAccountDataEvent::RecentEmoji(ev))
143 );
144 assert_eq!(ev.content.recent_emoji, [RecentEmoji::new("😎".to_owned())]);
145 }
146
147 #[test]
148 fn recent_emoji_increment() {
149 let json = json!({
150 "recent_emoji": [
151 {
152 "emoji": "😎",
153 "total": 1,
154 },
155 {
156 "emoji": "🏠",
157 "total": 5,
158 },
159 {
160 "emoji": "🧑💻",
161 "total": 2,
162 },
163 ],
164 });
165 let mut content = from_json_value::<RecentEmojiEventContent>(json).unwrap();
166
167 let mut iter = content.recent_emoji.iter();
169 assert_eq!(iter.next().unwrap().emoji, "😎");
170 assert_eq!(iter.next().unwrap().emoji, "🏠");
171 assert_eq!(iter.next().unwrap().emoji, "🧑💻");
172 assert_eq!(iter.next(), None);
173
174 content.increment_emoji_total("🏠");
176 assert_eq!(content.recent_emoji.first().unwrap().total, uint!(6));
177
178 let mut iter = content.recent_emoji.iter();
179 assert_eq!(iter.next().unwrap().emoji, "🏠");
180 assert_eq!(iter.next().unwrap().emoji, "😎");
181 assert_eq!(iter.next().unwrap().emoji, "🧑💻");
182 assert_eq!(iter.next(), None);
183
184 content.increment_emoji_total("💩");
186 assert_eq!(content.recent_emoji.first().unwrap().total, uint!(1));
187
188 let mut iter = content.recent_emoji.iter();
189 assert_eq!(iter.next().unwrap().emoji, "💩");
190 assert_eq!(iter.next().unwrap().emoji, "🏠");
191 assert_eq!(iter.next().unwrap().emoji, "😎");
192 assert_eq!(iter.next().unwrap().emoji, "🧑💻");
193 assert_eq!(iter.next(), None);
194
195 let first_emoji = "\u{2700}";
197 let first_emoji_u32 = 0x2700_u32;
198 let mut content = RecentEmojiEventContent::new(
199 std::iter::repeat_n(first_emoji_u32, 110)
200 .enumerate()
201 .map(|(n, start)| {
202 let char = char::from_u32(start + (n as u32)).unwrap();
203 RecentEmoji::new(char.into())
204 })
205 .collect(),
206 );
207 assert_eq!(content.recent_emoji.len(), 110);
208
209 content.increment_emoji_total(first_emoji);
211 assert_eq!(content.recent_emoji.first().unwrap().total, uint!(2));
212 assert_eq!(content.recent_emoji.len(), 100);
213 }
214
215 #[test]
216 fn recent_emoji_sorted_by_total() {
217 let json = json!({
218 "recent_emoji": [
219 {
220 "emoji": "😎",
221 "total": 1,
222 },
223 {
224 "emoji": "🏠",
225 "total": 5,
226 },
227 {
228 "emoji": "🧑💻",
229 "total": 2,
230 },
231 {
232 "emoji": "🚀",
233 "total": 1,
234 },
235 ],
236 });
237 let content = from_json_value::<RecentEmojiEventContent>(json).unwrap();
238
239 let mut iter = content.recent_emoji.iter();
241 assert_eq!(iter.next().unwrap().emoji, "😎");
242 assert_eq!(iter.next().unwrap().emoji, "🏠");
243 assert_eq!(iter.next().unwrap().emoji, "🧑💻");
244 assert_eq!(iter.next().unwrap().emoji, "🚀");
245 assert_eq!(iter.next(), None);
246
247 let sorted = content.recent_emoji_sorted_by_total();
249 let mut iter = sorted.iter();
250 assert_eq!(iter.next().unwrap().emoji, "🏠");
251 assert_eq!(iter.next().unwrap().emoji, "🧑💻");
252 assert_eq!(iter.next().unwrap().emoji, "😎");
253 assert_eq!(iter.next().unwrap().emoji, "🚀");
254 assert_eq!(iter.next(), None);
255 }
256}