1/*
2 * Copyright © 2020 Michael Gratton <mike@vee.net>.
3 *
4 * This software is licensed under the GNU Lesser General Public License
5 * (version 2.1 or later). See the COPYING file in this distribution.
6 */
7
8[ModuleInit]
9public void peas_register_types(TypeModule module) {
10    Peas.ObjectModule obj = module as Peas.ObjectModule;
11    obj.register_extension_type(
12        typeof(Plugin.PluginBase),
13        typeof(Plugin.EmailTemplates)
14    );
15}
16
17/**
18 * Enables editing and sending email templates.
19 */
20public class Plugin.EmailTemplates :
21    PluginBase, FolderExtension, EmailExtension {
22
23
24    // Translators: Templates folder name alternatives. Separate names
25    // using a vertical bar and put the most common localized name to
26    // the front for the default. English names do not need to be
27    // included.
28    private const string LOC_NAMES = _(
29        "Templates | Template Mail | Template Email | Template E-Mail"
30    );
31    // This must be identical to he above except without translation
32    private const string UNLOC_NAMES = (
33        "Templates | Template Mail | Template Email | Template E-Mail"
34    );
35
36
37    private const string ACTION_NEW = "new-template";
38    private const string ACTION_EDIT = "edit-template";
39    private const string ACTION_SEND = "send-template";
40
41    private const int INFO_BAR_PRIORITY = 0;
42
43
44    public FolderContext folders {
45        get; set construct;
46    }
47
48    public EmailContext email {
49        get; set construct;
50    }
51
52
53    private FolderStore? folder_store = null;
54    private EmailStore? email_store = null;
55
56    private GLib.SimpleAction? new_action = null;
57    private GLib.SimpleAction? edit_action = null;
58    private GLib.SimpleAction? send_action = null;
59
60    private Gee.Map<Folder,InfoBar> info_bars =
61        new Gee.HashMap<Folder,InfoBar>();
62
63    private Gee.List<string> folder_names = new Gee.ArrayList<string>();
64
65    private GLib.Cancellable cancellable = new GLib.Cancellable();
66
67
68    public override async void activate(bool is_startup) throws GLib.Error {
69        // Add localised first, so if we need to create a folder it
70        // will be created localised.
71        Geary.iterate_array(LOC_NAMES.split("|")).map<string>(
72            (name) => name.strip()
73        ).add_all_to(this.folder_names);
74        Geary.iterate_array(UNLOC_NAMES.split("|")).map<string>(
75            (name) => name.strip()
76        ).add_all_to(this.folder_names);
77
78        this.folder_store = yield this.folders.get_folder_store();
79        this.folder_store.folders_available.connect(on_folders_available);
80        this.folder_store.folders_unavailable.connect(on_folders_unavailable);
81        this.folder_store.folders_type_changed.connect(on_folders_type_changed);
82        this.folder_store.folder_selected.connect(on_folder_selected);
83
84        this.email_store = yield this.email.get_email_store();
85        this.email_store.email_displayed.connect(on_email_displayed);
86
87        this.new_action = new GLib.SimpleAction(
88            ACTION_NEW, this.folder_store.folder_variant_type
89        );
90        this.new_action.activate.connect(on_new_activated);
91        this.plugin_application.register_action(this.new_action);
92
93        this.edit_action = new GLib.SimpleAction(
94            ACTION_EDIT, this.email_store.email_identifier_variant_type
95        );
96        this.edit_action.activate.connect(on_edit_activated);
97        this.plugin_application.register_action(this.edit_action);
98
99        this.send_action = new GLib.SimpleAction(
100            ACTION_SEND, this.email_store.email_identifier_variant_type
101        );
102        this.send_action.activate.connect(on_send_activated);
103        this.plugin_application.register_action(this.send_action);
104
105        add_folders(this.folder_store.get_folders());
106    }
107
108    public override async void deactivate(bool is_shutdown) throws GLib.Error {
109        this.cancellable.cancel();
110
111        // Take a copy of the keys so the collection doesn't asplode
112        // as it is being modified.
113        foreach (var folder in this.info_bars.keys.to_array()) {
114            unregister_folder(folder);
115        }
116        this.info_bars.clear();
117        this.folder_names.clear();
118
119        this.plugin_application.deregister_action(this.new_action);
120        this.new_action = null;
121
122        this.plugin_application.deregister_action(this.edit_action);
123        this.edit_action = null;
124
125        this.plugin_application.deregister_action(this.send_action);
126        this.send_action = null;
127
128        this.folder_store.folders_available.disconnect(on_folders_available);
129        this.folder_store.folders_unavailable.disconnect(on_folders_unavailable);
130        this.folder_store.folders_type_changed.disconnect(on_folders_type_changed);
131        this.folder_store.folder_selected.disconnect(on_folder_selected);
132        this.folder_store = null;
133
134        this.email_store.email_displayed.disconnect(on_email_displayed);
135        this.email_store = null;
136    }
137
138    private async void edit_email(Folder? target, EmailIdentifier? id, bool send) {
139        var account = (target != null) ? target.account : id.account;
140        try {
141            Plugin.Composer? composer = null;
142            if (id != null) {
143                composer = yield this.plugin_application.compose_with_context(
144                    id.account,
145                    Composer.ContextType.EDIT,
146                    id
147                );
148            } else {
149                composer = yield this.plugin_application.compose_blank(account);
150            }
151            if (!send) {
152                var folder = target;
153                if (folder == null && id != null) {
154                    var containing = yield this.folder_store.list_containing_folders(
155                        id, this.cancellable
156                    );
157                    folder = containing.first_match(
158                        (f) => this.info_bars.has_key(f)
159                    );
160                }
161                composer.save_to_folder(folder);
162                composer.can_send = false;
163            }
164
165            composer.present();
166        } catch (GLib.Error err) {
167            warning("Unable to construct composer: %s", err.message);
168        }
169    }
170
171    private void add_folders(Gee.Collection<Folder> to_add) {
172        Folder? inbox = null;
173        var found_templates = false;
174        foreach (var folder in to_add) {
175            if (folder.used_as == INBOX) {
176                inbox = folder;
177            } else if (folder.display_name in this.folder_names) {
178                register_folder(folder);
179                found_templates = true;
180            }
181        }
182
183        // XXX There is no way at the moment to determine when all
184        // local folders have been loaded, but since they are all done
185        // in once batch, it's a safe bet that if we've seen the
186        // Inbox, then the local folder set should contain a templates
187        // folder, if one is available. If there isn't, we need to
188        // create it.
189        if (!found_templates && inbox != null) {
190            debug("Creating templates folder");
191            this.create_folder.begin(inbox.account);
192        }
193    }
194
195    private void register_folder(Folder target) {
196        try {
197            this.folders.register_folder_used_as(
198                target,
199                // Translators: The name of the folder used to
200                // store email templates
201                _("Templates"),
202                "folder-templates-symbolic"
203            );
204            this.info_bars.set(
205                target,
206                new_templates_folder_info_bar(target)
207            );
208        } catch (GLib.Error err) {
209            warning(
210                "Failed to register %s as templates folder: %s",
211                target.persistent_id,
212                err.message
213            );
214        }
215    }
216
217    private void unregister_folder(Folder target) {
218        var info_bar = this.info_bars.get(target);
219        if (info_bar != null) {
220            try {
221                this.folders.unregister_folder_used_as(target);
222            } catch (GLib.Error err) {
223                warning(
224                    "Failed to unregister %s as templates folder: %s",
225                    target.persistent_id,
226                    err.message
227                );
228            }
229            this.folders.remove_folder_info_bar(target, info_bar);
230            this.info_bars.unset(target);
231        }
232    }
233
234    private async void create_folder(Account account) {
235        try {
236            yield this.folder_store.create_personal_folder(
237                account,
238                this.folder_names[0],
239                this.cancellable
240            );
241            // Don't need to explicitly register the folder here, it
242            // will get picked up via the available signal
243        } catch (GLib.Error err) {
244            warning("Failed to create templates folder: %s", err.message);
245        }
246    }
247
248    private void update_folder(Folder target) {
249        var info_bar = this.info_bars.get(target);
250        if (info_bar != null) {
251            this.folders.add_folder_info_bar(
252                target, info_bar, INFO_BAR_PRIORITY
253            );
254        }
255    }
256
257    private async void update_email(Email target) {
258        var containing = Gee.Collection.empty<Folder>();
259        try {
260            containing = yield this.folder_store.list_containing_folders(
261                target.identifier, this.cancellable
262            );
263        } catch (GLib.Error err) {
264            warning("Could not load folders for email: %s", err.message);
265        }
266        if (containing.any_match((f) => this.info_bars.has_key(f))) {
267            this.email.add_email_info_bar(
268                target.identifier,
269                new_template_email_info_bar(target.identifier),
270                INFO_BAR_PRIORITY
271            );
272        }
273    }
274
275    private InfoBar new_templates_folder_info_bar(Folder target) {
276        var bar = this.info_bars.get(target);
277        if (bar == null) {
278            bar = new InfoBar(target.display_name);
279            bar.primary_button = new Actionable(
280                // Translators: Info bar button label for creating a
281                // new email template
282                _("New"),
283                this.new_action,
284                target.to_variant()
285            );
286            this.info_bars.set(target, bar);
287        }
288        return bar;
289    }
290
291    private InfoBar new_template_email_info_bar(EmailIdentifier target) {
292        // Translators: Infobar status label for an email template
293        var bar = new InfoBar(_("Message template"));
294        bar.primary_button = new Actionable(
295            // Translators: Info bar button label for sending an
296            // email template
297            _("Send"),
298            this.send_action,
299            target.to_variant()
300        );
301        bar.secondary_buttons.add(
302            new Actionable(
303                // Translators: Info bar button label for editing an
304                // existing email template
305                _("Edit"),
306                this.edit_action,
307                target.to_variant()
308            )
309        );
310        return bar;
311    }
312
313    private void on_folders_available(Gee.Collection<Folder> available) {
314        add_folders(available);
315    }
316
317    private void on_folders_unavailable(Gee.Collection<Folder> unavailable) {
318        foreach (var folder in unavailable) {
319            unregister_folder(folder);
320        }
321    }
322
323    private void on_folders_type_changed(Gee.Collection<Folder> changed) {
324        foreach (var folder in changed) {
325            unregister_folder(folder);
326            if (folder.display_name in this.folder_names) {
327                register_folder(folder);
328            }
329            update_folder(folder);
330        }
331    }
332
333    private void on_folder_selected(Folder selected) {
334        update_folder(selected);
335    }
336
337    private void on_new_activated(GLib.Action action, GLib.Variant? target) {
338        if (this.folder_store != null && target != null) {
339            Folder? folder = this.folder_store.get_folder_for_variant(target);
340            if (folder != null) {
341                this.edit_email.begin(folder, null, false);
342            }
343        }
344    }
345
346    private void on_edit_activated(GLib.Action action, GLib.Variant? target) {
347        if (this.email_store != null && target != null) {
348            EmailIdentifier? id =
349                this.email_store.get_email_identifier_for_variant(target);
350            if (id != null) {
351                this.edit_email.begin(null, id, false);
352            }
353        }
354    }
355
356    private void on_send_activated(GLib.Action action, GLib.Variant? target) {
357        if (this.email_store != null && target != null) {
358            EmailIdentifier? id =
359                this.email_store.get_email_identifier_for_variant(target);
360            if (id != null) {
361                this.edit_email.begin(null, id, true);
362            }
363        }
364    }
365
366    private void on_email_displayed(Email email) {
367        this.update_email.begin(email);
368    }
369
370}
371