egui_charts/services/
profile_directory.rs1use std::collections::{BTreeMap, HashMap};
4use std::sync::Arc;
5
6use super::{Listener, Profile, ProfileEvent, ProfileId, Profiles};
7use crate::data::FeedError;
8use crate::data::inbox::Inbox;
9
10#[derive(Clone, Debug, PartialEq)]
12#[non_exhaustive]
13pub enum FollowStatus {
14 Pending,
16 Failed(String),
19}
20
21#[derive(Clone, Debug, PartialEq)]
23pub struct FollowOperation {
24 pub follow: bool,
26 pub status: FollowStatus,
28}
29
30#[derive(Default)]
38pub struct ProfileDirectory {
39 service: Option<Arc<dyn Profiles>>,
40 inbox: Arc<Inbox>,
41 listener: Option<Listener>,
42 profiles: BTreeMap<ProfileId, Profile>,
43 operations: HashMap<ProfileId, FollowOperation>,
44 requests: HashMap<u64, ProfileId>,
46 sent: u64,
48}
49
50impl ProfileDirectory {
51 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn set_service(&mut self, service: Arc<dyn Profiles>) {
58 self.service = Some(service);
59 self.watch();
60 }
61
62 fn watch(&mut self) {
63 self.profiles.clear();
64 self.operations.clear();
65 self.requests.clear();
66 self.sent = 0;
67 self.inbox = Arc::default();
69 let (listener, events) = Listener::new(&self.inbox, 0);
70 self.listener = Some(listener);
71 if let Some(service) = &self.service {
72 service.watch(events);
73 }
74 }
75
76 pub fn poll(&mut self, ctx: &egui::Context) {
78 if self.listener.as_ref().is_some_and(|l| !l.is_active()) {
79 self.watch();
80 }
81 let repaint = ctx.clone();
82 self.inbox.set_waker(move || repaint.request_repaint());
83 for (key, answer) in self.inbox.drain().answers {
84 if key == 0 {
85 match answer.downcast::<ProfileEvent>().map(|event| *event) {
86 Ok(ProfileEvent::Upsert(profile)) => {
87 self.profiles.insert(profile.id.clone(), profile);
88 }
89 Ok(ProfileEvent::Remove(id)) => {
90 self.profiles.remove(&id);
91 self.operations.remove(&id);
92 self.requests.retain(|_, profile| profile != &id);
93 }
94 Ok(ProfileEvent::Clear) => {
95 self.profiles.clear();
96 self.operations.clear();
97 self.requests.clear();
98 }
99 Err(_) => {}
100 }
101 } else if let Some(id) = self.requests.remove(&key)
102 && let Ok(answer) = answer.downcast::<Result<(), FeedError>>()
103 {
104 match *answer {
105 Ok(()) => {
106 self.operations.remove(&id);
107 }
108 Err(error) => {
109 if let Some(operation) = self.operations.get_mut(&id) {
110 operation.status = FollowStatus::Failed(error.to_string());
111 }
112 }
113 }
114 }
115 }
116 }
117
118 pub fn profile(&self, id: &ProfileId) -> Option<&Profile> {
120 self.profiles.get(id)
121 }
122
123 pub fn profiles(&self) -> impl Iterator<Item = &Profile> {
125 self.profiles.values()
126 }
127
128 pub fn operation(&self, id: &ProfileId) -> Option<&FollowOperation> {
130 self.operations.get(id)
131 }
132
133 pub fn follow(&mut self, id: &ProfileId, follow: bool) {
137 let pending = self
138 .operations
139 .get(id)
140 .is_some_and(|operation| operation.status == FollowStatus::Pending);
141 if pending || self.profiles.get(id).is_none_or(|p| p.followed == follow) {
142 return;
143 }
144 let Some(service) = &self.service else {
145 return;
146 };
147 self.sent += 1;
148 let key = self.sent;
149 self.requests.insert(key, id.clone());
150 self.operations.insert(
151 id.clone(),
152 FollowOperation {
153 follow,
154 status: FollowStatus::Pending,
155 },
156 );
157 service.follow(id.clone(), follow, self.inbox.reply(key));
158 }
159
160 pub fn dismiss(&mut self, id: &ProfileId) {
162 if self
163 .operations
164 .get(id)
165 .is_some_and(|operation| operation.status != FollowStatus::Pending)
166 {
167 self.operations.remove(id);
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::data::Reply;
176 use crate::services::Events;
177 use std::sync::Mutex;
178
179 #[derive(Default)]
181 struct Host(Mutex<Held>);
182
183 type Held = (Option<Events<ProfileEvent>>, Vec<(bool, Reply<()>)>);
185
186 impl Profiles for Host {
187 fn watch(&self, events: Events<ProfileEvent>) {
188 events.push(ProfileEvent::Upsert(Profile::new(
189 ProfileId("1".into()),
190 "Ada",
191 )));
192 self.0.lock().unwrap().0 = Some(events);
193 }
194 fn follow(&self, _: ProfileId, follow: bool, reply: Reply<()>) {
195 self.0.lock().unwrap().1.push((follow, reply));
196 }
197 }
198
199 #[test]
200 fn follow_requests_show_pending_and_failed_and_a_new_service_forgets_the_old_account() {
201 let ctx = egui::Context::default();
202 let (old, id) = (Arc::new(Host::default()), ProfileId("1".into()));
203 let mut directory = ProfileDirectory::new();
204 directory.set_service(old.clone());
205 directory.poll(&ctx);
206 assert_eq!(directory.profile(&id).map(|p| p.followed), Some(false));
207 directory.follow(&id, false);
209 directory.follow(&id, true);
210 directory.follow(&id, true);
211 let (follow, reply) = old.0.lock().unwrap().1.pop().unwrap();
212 assert!(follow && old.0.lock().unwrap().1.is_empty(), "one request");
213 assert_eq!(
214 directory.operation(&id).unwrap().status,
215 FollowStatus::Pending
216 );
217 reply.send(Err(FeedError::Other("blocked".into())));
218 directory.poll(&ctx);
219 assert!(matches!(
220 &directory.operation(&id).unwrap().status,
221 FollowStatus::Failed(message) if message.contains("blocked")
222 ));
223 assert_eq!(directory.profile(&id).map(|p| p.followed), Some(false));
224 directory.follow(&id, true);
226 let (_, reply) = old.0.lock().unwrap().1.pop().unwrap();
227 let mut followed = Profile::new(id.clone(), "Ada");
228 followed.followed = true;
229 old.0
230 .lock()
231 .unwrap()
232 .0
233 .as_ref()
234 .unwrap()
235 .push(ProfileEvent::Upsert(followed));
236 reply.send(Ok(()));
237 directory.poll(&ctx);
238 assert!(directory.operation(&id).is_none());
239 assert_eq!(directory.profile(&id).map(|p| p.followed), Some(true));
240 directory.follow(&id, false);
242 let (_, late) = old.0.lock().unwrap().1.pop().unwrap();
243 let next = Arc::new(Host::default());
244 directory.set_service(next.clone());
245 late.send(Ok(()));
246 directory.poll(&ctx);
247 assert_eq!(directory.profile(&id).map(|p| p.followed), Some(false));
248 assert!(directory.operation(&id).is_none());
249
250 for event in [ProfileEvent::Remove(id.clone()), ProfileEvent::Clear] {
252 directory.follow(&id, true);
253 let (_, stale) = next.0.lock().unwrap().1.pop().unwrap();
254 let held = next.0.lock().unwrap();
255 let events = held.0.as_ref().unwrap();
256 events.push(event);
257 events.push(ProfileEvent::Upsert(Profile::new(id.clone(), "Ada")));
258 drop(held);
259 directory.poll(&ctx);
260 assert!(directory.operation(&id).is_none());
261 directory.follow(&id, true);
262 let (_, current) = next.0.lock().unwrap().1.pop().unwrap();
263 stale.send(Err(FeedError::Other("stale refusal".into())));
264 directory.poll(&ctx);
265 assert_eq!(
266 directory.operation(&id).unwrap().status,
267 FollowStatus::Pending
268 );
269 current.send(Err(FeedError::Other("current refusal".into())));
270 directory.poll(&ctx);
271 directory.dismiss(&id);
272 }
273 }
274}