1 use crate::backend::{dw_media, media, room, ContentType, ThreadPool};
2 use fractal_api::identifiers::RoomId;
3 use fractal_api::r0::AccessToken;
4 use glib::clone;
5 use log::error;
6 use std::cell::RefCell;
7 use std::fs;
8 use std::process::Command;
9 use std::rc::Rc;
10 use std::sync::mpsc::channel;
11 use std::sync::mpsc::TryRecvError;
12 use std::sync::mpsc::{Receiver, Sender};
13 use std::thread;
14
15 use crate::actions::AppState;
16 use crate::backend::HandleError;
17 use crate::i18n::i18n;
18 use crate::types::Message;
19 use crate::uibuilder::UI;
20 use crate::App;
21 use fractal_api::url::Url;
22 use gio::ActionGroupExt;
23 use gio::ActionMapExt;
24 use gio::SimpleAction;
25 use gio::SimpleActionGroup;
26 use glib::source::Continue;
27 use gtk::prelude::*;
28
29 use super::global::{get_event_id, get_message_by_id, get_room_id};
30
31 use crate::widgets::ErrorDialog;
32 use crate::widgets::FileDialog::save;
33 use crate::widgets::SourceDialog;
34
35 /* This creates all actions the room history can perform */
new( thread_pool: ThreadPool, server_url: Url, access_token: AccessToken, ui: UI, back_history: Rc<RefCell<Vec<AppState>>>, ) -> gio::SimpleActionGroup36 pub fn new(
37 thread_pool: ThreadPool,
38 server_url: Url,
39 access_token: AccessToken,
40 ui: UI,
41 back_history: Rc<RefCell<Vec<AppState>>>,
42 ) -> gio::SimpleActionGroup {
43 let actions = SimpleActionGroup::new();
44 /* Action for each message */
45 let reply = SimpleAction::new("reply", glib::VariantTy::new("s").ok());
46 let open_with = SimpleAction::new("open_with", glib::VariantTy::new("s").ok());
47 let save_as = SimpleAction::new("save_as", glib::VariantTy::new("s").ok());
48 let copy_image = SimpleAction::new("copy_image", glib::VariantTy::new("s").ok());
49 let copy_text = SimpleAction::new("copy_text", glib::VariantTy::new("s").ok());
50 let delete = SimpleAction::new("delete", glib::VariantTy::new("s").ok());
51 let show_source = SimpleAction::new("show_source", glib::VariantTy::new("s").ok());
52
53 /* TODO: use stateful action to keep track if the user already requested new messages */
54 let load_more_messages =
55 SimpleAction::new("request_older_messages", glib::VariantTy::new("s").ok());
56
57 actions.add_action(&reply);
58 actions.add_action(&open_with);
59 actions.add_action(&save_as);
60 actions.add_action(©_image);
61 actions.add_action(©_text);
62 actions.add_action(&delete);
63 actions.add_action(&show_source);
64 actions.add_action(&load_more_messages);
65
66 let parent: gtk::Window = ui
67 .builder
68 .get_object("main_window")
69 .expect("Can't find main_window in ui file.");
70 show_source.connect_activate(clone!(@weak parent => move |_, data| {
71 let viewer = SourceDialog::new();
72 viewer.set_parent_window(&parent);
73 if let Some(m) = get_message(data) {
74 let error = i18n("This message has no source.");
75 let source = m.source.as_ref().unwrap_or(&error);
76
77 viewer.show(source);
78 }
79 }));
80
81 let window = ui
82 .builder
83 .get_object::<gtk::ApplicationWindow>("main_window")
84 .expect("Couldn't find main_window in ui file.");
85 reply.connect_activate(clone!(
86 @weak back_history,
87 @weak window,
88 @weak ui.sventry.view as msg_entry
89 => move |_, data| {
90 let state = back_history.borrow().last().cloned();
91 if let Some(AppState::MediaViewer) = state {
92 if let Some(action_group) = window.get_action_group("app") {
93 action_group.activate_action("back", None);
94 } else {
95 error!("The action group app is not attached to the main window.");
96 }
97 }
98 if let Some(buffer) = msg_entry.get_buffer() {
99 let mut start = buffer.get_start_iter();
100 if let Some(m) = get_message(data) {
101 let quote = m
102 .body
103 .lines()
104 .map(|l| "> ".to_owned() + l)
105 .collect::<Vec<String>>()
106 .join("\n")
107 + "\n"
108 + "\n";
109 buffer.insert(&mut start, "e);
110 msg_entry.grab_focus();
111 }
112 }
113 }));
114
115 open_with.connect_activate(clone!(@strong server_url => move |_, data| {
116 if let Some(m) = get_message(data) {
117 if let Some(url) = m.url {
118 thread::spawn(clone!(@strong server_url => move || {
119 match dw_media(server_url, &url, ContentType::Download, None) {
120 Ok(fname) => {
121 Command::new("xdg-open")
122 .arg(&fname)
123 .spawn()
124 .expect("failed to execute process");
125 }
126 Err(err) => {
127 err.handle_error()
128 }
129 }
130 }));
131 }
132 }
133 }));
134
135 save_as.connect_activate(clone!(
136 @strong server_url,
137 @strong thread_pool,
138 @weak parent as window
139 => move |_, data| {
140 if let Some(m) = get_message(data) {
141 if let Some(url) = m.url {
142 let name = m.body;
143
144 let (tx, rx): (
145 Sender<media::MediaResult>,
146 Receiver<media::MediaResult>,
147 ) = channel();
148
149 media::get_media_async(thread_pool.clone(), server_url.clone(), url, tx);
150
151 gtk::timeout_add(
152 50,
153 clone!(
154 @strong name,
155 @weak window
156 => @default-return Continue(true), move || match rx.try_recv() {
157 Err(TryRecvError::Empty) => Continue(true),
158 Err(TryRecvError::Disconnected) => {
159 let msg = i18n("Could not download the file");
160 ErrorDialog::new(false, &msg);
161
162 Continue(true)
163 },
164 Ok(Ok(fname)) => {
165 if let Some(path) = save(&window, &name, &[]) {
166 // TODO use glib to copy file
167 if fs::copy(fname, path).is_err() {
168 ErrorDialog::new(false, &i18n("Couldn’t save file"));
169 }
170 }
171 Continue(false)
172 }
173 Ok(Err(err)) => {
174 error!("Media path could not be found due to error: {:?}", err);
175 Continue(false)
176 }
177 }
178 ),
179 );
180 }
181 }
182 }));
183
184 copy_image.connect_activate(clone!(@strong server_url => move |_, data| {
185 if let Some(m) = get_message(data) {
186 if let Some(url) = m.url {
187 let (tx, rx): (
188 Sender<media::MediaResult>,
189 Receiver<media::MediaResult>,
190 ) = channel();
191
192 media::get_media_async(thread_pool.clone(), server_url.clone(), url, tx);
193
194 gtk::timeout_add(50, move || match rx.try_recv() {
195 Err(TryRecvError::Empty) => Continue(true),
196 Err(TryRecvError::Disconnected) => {
197 let msg = i18n("Could not download the file");
198 ErrorDialog::new(false, &msg);
199
200 Continue(true)
201 }
202 Ok(Ok(fname)) => {
203 if let Ok(pixbuf) = gdk_pixbuf::Pixbuf::new_from_file(fname) {
204 let atom = gdk::Atom::intern("CLIPBOARD");
205 let clipboard = gtk::Clipboard::get(&atom);
206
207 clipboard.set_image(&pixbuf);
208 }
209
210 Continue(false)
211 }
212 Ok(Err(err)) => {
213 error!("Image path could not be found due to error: {:?}", err);
214 Continue(false)
215 }
216 });
217 }
218 }
219 }));
220
221 copy_text.connect_activate(move |_, data| {
222 if let Some(m) = get_message(data) {
223 let atom = gdk::Atom::intern("CLIPBOARD");
224 let clipboard = gtk::Clipboard::get(&atom);
225
226 clipboard.set_text(&m.body);
227 }
228 });
229
230 let s = server_url.clone();
231 let tk = access_token.clone();
232 delete.connect_activate(clone!(
233 @weak back_history,
234 @weak window
235 => move |_, data| {
236 let state = back_history.borrow().last().cloned();
237 if let Some(AppState::MediaViewer) = state {
238 if let Some(action_group) = window.get_action_group("app") {
239 action_group.activate_action("back", None);
240 } else {
241 error!("The action group app is not attached to the main window.");
242 }
243 }
244 if let Some(msg) = get_message(data) {
245 let server = s.clone();
246 let access_token = tk.clone();
247 thread::spawn(move || {
248 let query = room::redact_msg(server, access_token, msg);
249 if let Err(err) = query {
250 err.handle_error();
251 }
252 });
253 }
254 }));
255
256 load_more_messages.connect_activate(clone!(
257 @strong server_url,
258 @strong access_token
259 => move |_, data| {
260 let id = get_room_id(data);
261 request_more_messages(server_url.clone(), access_token.clone(), id);
262 }));
263
264 actions
265 }
266
get_message(id: Option<&glib::Variant>) -> Option<Message>267 fn get_message(id: Option<&glib::Variant>) -> Option<Message> {
268 get_event_id(id).as_ref().and_then(get_message_by_id)
269 }
270
request_more_messages( server_url: Url, access_token: AccessToken, id: Option<RoomId>, ) -> Option<()>271 fn request_more_messages(
272 server_url: Url,
273 access_token: AccessToken,
274 id: Option<RoomId>,
275 ) -> Option<()> {
276 let op = App::get_op()?;
277 let op = op.lock().unwrap();
278 let id = id?;
279 let r = op.rooms.get(&id)?;
280 if let Some(prev_batch) = r.prev_batch.clone() {
281 thread::spawn(move || {
282 match room::get_room_messages(server_url, access_token, id, prev_batch) {
283 Ok((msgs, room, prev_batch)) => {
284 APPOP!(show_room_messages_top, (msgs, room, prev_batch));
285 }
286 Err(err) => {
287 err.handle_error();
288 }
289 }
290 });
291 } else if let Some(msg) = r.messages.iter().next().cloned() {
292 // no prev_batch so we use the last message to calculate that in the backend
293 thread::spawn(move || {
294 match room::get_room_messages_from_msg(server_url, access_token, id, msg) {
295 Ok((msgs, room, prev_batch)) => {
296 APPOP!(show_room_messages_top, (msgs, room, prev_batch));
297 }
298 Err(err) => {
299 err.handle_error();
300 }
301 }
302 });
303 } else if let Some(from) = op.since.clone() {
304 // no messages and no prev_batch so we use the last since
305 thread::spawn(
306 move || match room::get_room_messages(server_url, access_token, id, from) {
307 Ok((msgs, room, prev_batch)) => {
308 APPOP!(show_room_messages_top, (msgs, room, prev_batch));
309 }
310 Err(err) => {
311 err.handle_error();
312 }
313 },
314 );
315 }
316 None
317 }
318