├── src ├── version ├── chrome │ ├── content │ │ ├── hooks │ │ │ ├── shutdown.js │ │ │ └── init.js │ │ ├── images │ │ │ ├── icon.png │ │ │ ├── statusbar.png │ │ │ ├── statusbar-error.png │ │ │ └── statusbar-disabled.png │ │ ├── utils │ │ │ ├── sentmailidentities.mjs │ │ │ ├── strings.mjs │ │ │ ├── number.mjs │ │ │ ├── emailparser.mjs │ │ │ ├── references.mjs │ │ │ ├── date.mjs │ │ │ ├── preferenceskeys.mjs │ │ │ ├── threader.mjs │ │ │ └── preferences.mjs │ │ ├── legend.js │ │ ├── legend.xhtml │ │ ├── threadvispopup.xhtml │ │ ├── thread.mjs │ │ ├── threadvis.css │ │ ├── positionedcontainer.mjs │ │ ├── helpers │ │ │ └── notifyTools.js │ │ ├── message.mjs │ │ ├── container.mjs │ │ ├── timeline.mjs │ │ ├── arcvisualisation.mjs │ │ └── positionedthread.mjs │ └── locale │ │ ├── en-us │ │ ├── threadvis.dtd │ │ └── threadvis.properties │ │ └── de-de │ │ ├── threadvis.dtd │ │ └── threadvis.properties ├── settings │ ├── images │ │ ├── about.png │ │ ├── timescaling_disabled.png │ │ └── timescaling_enabled.png │ ├── options.css │ ├── options.html │ └── options.js ├── experiments │ ├── LegacyAccountsFolders │ │ ├── schema.json │ │ └── implementation.js │ ├── LegacyPref │ │ ├── schema.json │ │ └── implementation.js │ ├── NotifyTools │ │ ├── schema.json │ │ └── implementation.js │ └── WindowListener │ │ └── schema.json ├── background.js ├── defaults │ └── preferences │ │ └── threadvisdefault.js ├── manifest.json └── _locales │ ├── en │ └── messages.json │ └── de │ └── messages.json ├── .gitignore ├── README.md └── xpi.filelist /src/version: -------------------------------------------------------------------------------- 1 | 3.6.13 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /src/chrome/content/hooks/shutdown.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/settings/images/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/settings/images/about.png -------------------------------------------------------------------------------- /src/chrome/content/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/chrome/content/images/icon.png -------------------------------------------------------------------------------- /src/chrome/content/images/statusbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/chrome/content/images/statusbar.png -------------------------------------------------------------------------------- /src/settings/images/timescaling_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/settings/images/timescaling_disabled.png -------------------------------------------------------------------------------- /src/settings/images/timescaling_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/settings/images/timescaling_enabled.png -------------------------------------------------------------------------------- /src/chrome/content/images/statusbar-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/chrome/content/images/statusbar-error.png -------------------------------------------------------------------------------- /src/chrome/content/images/statusbar-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threadvis/ThreadVis/HEAD/src/chrome/content/images/statusbar-disabled.png -------------------------------------------------------------------------------- /src/experiments/LegacyAccountsFolders/schema.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "namespace": "LegacyAccountsFolders", 3 | "functions": [{ 4 | "name": "getAccounts", 5 | "type": "function", 6 | "description": "Gets all accounts.", 7 | "async": true, 8 | "parameters": [] 9 | }] 10 | }] -------------------------------------------------------------------------------- /src/experiments/LegacyPref/schema.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "namespace": "LegacyPref", 3 | "functions": [{ 4 | "name": "init", 5 | "type": "function", 6 | "description": "Init preferences", 7 | "async": true, 8 | "parameters": [] 9 | }, { 10 | "name": "get", 11 | "type": "function", 12 | "description": "Gets a preference.", 13 | "async": true, 14 | "parameters": [{ 15 | "name": "name", 16 | "type": "string", 17 | "description": "The preference name" 18 | }] 19 | }, { 20 | "name": "set", 21 | "type": "function", 22 | "description": "Sets a preference.", 23 | "async": true, 24 | "parameters": [{ 25 | "name": "name", 26 | "type": "string", 27 | "description": "The preference name" 28 | }, { 29 | "name": "value", 30 | "type": "any", 31 | "description": "The preference value" 32 | }] 33 | }] 34 | }] -------------------------------------------------------------------------------- /src/experiments/NotifyTools/schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "namespace": "NotifyTools", 4 | "events": [ 5 | { 6 | "name": "onNotifyBackground", 7 | "type": "function", 8 | "description": "Fired when a new notification from notifyTools.js in an Experiment has been received.", 9 | "parameters": [ 10 | { 11 | "name": "data", 12 | "type": "any", 13 | "description": "Restrictions of the structured clone algorithm apply." 14 | } 15 | ] 16 | } 17 | ], 18 | "functions": [ 19 | { 20 | "name": "notifyExperiment", 21 | "type": "function", 22 | "async": true, 23 | "description": "Notifies notifyTools.js in an Experiment and sends data.", 24 | "parameters": [ 25 | { 26 | "name": "data", 27 | "type": "any", 28 | "description": "Restrictions of the structured clone algorithm apply." 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ThreadVis 2 | ========= 3 | 4 | ThreadVis displays all messages in a conversation in a compact visual map, identifying the chronology of and contributors to a discussion and supporting navigating between messages in a thread. 5 | 6 | For details see the add-on's homepage at . 7 | 8 | --- 9 | Copyright © 2005-2007 Alexander C. Hubmann 10 | Copyright © 2007-2025 Alexander C. Hubmann-Haidvogel 11 | 12 | ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled [*ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client*](https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf) at Graz University of Technology, Austria. 13 | 14 | ThreadVis is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 15 | -------------------------------------------------------------------------------- /src/chrome/content/utils/sentmailidentities.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Sent mail identities 29 | **********************************************************************************************************************/ 30 | 31 | export const SentMailIdentities = {}; 32 | 33 | // remember all local accounts, for sent-mail comparison 34 | const accountManager = Components.classes["@mozilla.org/messenger/account-manager;1"] 35 | .getService(Components.interfaces.nsIMsgAccountManager); 36 | for (let identity in accountManager.allIdentities) { 37 | SentMailIdentities[identity.email] = true; 38 | } -------------------------------------------------------------------------------- /src/chrome/content/utils/strings.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Implements strings proxy 29 | **********************************************************************************************************************/ 30 | 31 | const strings = Services.strings.createBundle("chrome://threadvis/locale/threadvis.properties"); 32 | 33 | /** 34 | * Static strings object 35 | */ 36 | export const Strings = { 37 | /** 38 | * Get localized string 39 | * 40 | * @param {String} key - The key of the localized string 41 | * @return {String} - the localized string 42 | */ 43 | getString(key) { 44 | return strings.GetStringFromName(key); 45 | } 46 | }; -------------------------------------------------------------------------------- /src/chrome/content/legend.js: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * JavaScript file to visualise legend 29 | **********************************************************************************************************************/ 30 | 31 | /** 32 | * Clear the legend box 33 | */ 34 | var clearLegend = () => { 35 | const legendBox = document.getElementById("LegendContent"); 36 | while (legendBox.firstChild) { 37 | legendBox.removeChild(legendBox.firstChild); 38 | } 39 | }; 40 | 41 | /** 42 | * Display the legend 43 | */ 44 | var displayLegend = () => { 45 | clearLegend(); 46 | 47 | const legendBox = document.getElementById("LegendContent"); 48 | const legend = opener.ThreadVis.legend; 49 | legendBox.appendChild(legend); 50 | window.sizeToContent(); 51 | }; -------------------------------------------------------------------------------- /xpi.filelist: -------------------------------------------------------------------------------- 1 | background.js 2 | manifest.json 3 | 4 | experiments/LegacyPref/implementation.js 5 | experiments/LegacyPref/schema.json 6 | 7 | experiments/LegacyAccountsFolders/implementation.js 8 | experiments/LegacyAccountsFolders/schema.json 9 | 10 | experiments/NotifyTools/implementation.js 11 | experiments/NotifyTools/schema.json 12 | 13 | experiments/WindowListener/implementation.js 14 | experiments/WindowListener/schema.json 15 | 16 | chrome/content/helpers/notifyTools.js 17 | 18 | chrome/content/hooks/init.js 19 | chrome/content/hooks/shutdown.js 20 | 21 | defaults/preferences/threadvisdefault.js 22 | _locales/de/messages.json 23 | _locales/en/messages.json 24 | 25 | settings/options.css 26 | settings/options.html 27 | settings/options.js 28 | settings/images/about.png 29 | settings/images/timescaling_disabled.png 30 | settings/images/timescaling_enabled.png 31 | 32 | chrome/content/arcvisualisation.mjs 33 | chrome/content/container.mjs 34 | chrome/content/containervisualisation.mjs 35 | chrome/content/legend.js 36 | chrome/content/legend.xhtml 37 | chrome/content/message.mjs 38 | chrome/content/positionedcontainer.mjs 39 | chrome/content/positionedthread.mjs 40 | chrome/content/scrollbar.mjs 41 | chrome/content/thread.mjs 42 | chrome/content/threadvis.css 43 | chrome/content/threadvis.mjs 44 | chrome/content/threadvispopup.xhtml 45 | chrome/content/timeline.mjs 46 | chrome/content/visualisation.mjs 47 | 48 | chrome/content/utils/date.mjs 49 | chrome/content/utils/emailparser.mjs 50 | chrome/content/utils/number.mjs 51 | chrome/content/utils/preferences.mjs 52 | chrome/content/utils/preferenceskeys.mjs 53 | chrome/content/utils/references.mjs 54 | chrome/content/utils/sentmailidentities.mjs 55 | chrome/content/utils/strings.mjs 56 | chrome/content/utils/threader.mjs 57 | 58 | chrome/content/images/about.png 59 | chrome/content/images/arrowbottom.png 60 | chrome/content/images/arrowleft.png 61 | chrome/content/images/arrowright.png 62 | chrome/content/images/arrowtop.png 63 | chrome/content/images/icon.png 64 | chrome/content/images/legend.png 65 | chrome/content/images/minus.png 66 | chrome/content/images/plus.png 67 | chrome/content/images/statusbar.png 68 | chrome/content/images/statusbar-disabled.png 69 | chrome/content/images/statusbar-error.png 70 | chrome/content/images/window.png 71 | 72 | chrome/locale/en-us/threadvis.dtd 73 | chrome/locale/en-us/threadvis.properties 74 | chrome/locale/de-de/threadvis.dtd 75 | chrome/locale/de-de/threadvis.properties -------------------------------------------------------------------------------- /src/chrome/content/legend.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | 36 | 41 | 42 | 48 | 49 | 50 | 51 | 52 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/chrome/content/thread.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Thread utilities 29 | **********************************************************************************************************************/ 30 | 31 | import { PositionedThread } from "./positionedthread.mjs"; 32 | 33 | export class Thread { 34 | 35 | /** 36 | * List of containers 37 | */ 38 | #containers = []; 39 | 40 | #selectedContainer; 41 | 42 | #authors = {}; 43 | 44 | constructor(containers) { 45 | Object.seal(this); 46 | this.#containers = containers 47 | // filter all containers that neither have a parent nor children nor a message. 48 | // this would be caused by references pointing to nowhere 49 | .filter((container) => container.parent || container.children.length > 0 || container.message); 50 | this.#populateAuthors(); 51 | } 52 | 53 | select(messageId) { 54 | this.#selectedContainer = this.#containers 55 | .find((container) => container.id === messageId); 56 | } 57 | 58 | contains(messageId) { 59 | return this.#containers.some((container) => container.id === messageId); 60 | } 61 | 62 | get selected() { 63 | return this.#selectedContainer; 64 | } 65 | 66 | get size() { 67 | return this.#containers.length; 68 | } 69 | 70 | get root() { 71 | return this.#containers[0].root; 72 | } 73 | 74 | get authors() { 75 | return this.#authors; 76 | } 77 | 78 | getPositioned(width) { 79 | return new PositionedThread(this.#containers, this.#selectedContainer, this.#authors, width); 80 | } 81 | 82 | #populateAuthors() { 83 | this.#authors = this.#containers 84 | .filter((container) => container.message) 85 | .reduce((authors, container) => { 86 | if (authors[container.message.fromEmail]) { 87 | authors[container.message.fromEmail].count++; 88 | } else { 89 | authors[container.message.fromEmail] = { 90 | "name" : container.message.from, 91 | "count" : 1 92 | }; 93 | } 94 | return authors; 95 | }, {}); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | // * ******************************************************************************************************************* 2 | // * This file is part of ThreadVis. 3 | // * https://threadvis.github.io 4 | // * 5 | // * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | // * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | // * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | // * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | // * 10 | // * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | // * Copyright (C) 2007, 2008, 2009, 12 | // * 2010, 2011, 2013, 2018, 2019, 13 | // * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | // * 15 | // * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | // * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | // * or (at your option) any later version. 18 | // * 19 | // * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | // * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | // * See the GNU Affero General Public License for more details. 22 | // * 23 | // * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | // * If not, see . 25 | // * 26 | // * Version: $Id$ 27 | // * ******************************************************************************************************************* 28 | // * Install manifest for Thunderbird 29 | // ********************************************************************************************************************/ 30 | 31 | { 32 | "manifest_version": 2, 33 | "applications": { 34 | "gecko": { 35 | "id": "{A23E4120-431F-4753-AE53-5D028C42CFDC}", 36 | "strict_min_version": "128.0", 37 | "strict_max_version": "146.*" 38 | } 39 | }, 40 | "name": "__MSG_extensionName__", 41 | "description": "__MSG_extensionDescription__", 42 | "author": "Alexander C. Hubmann-Haidvogel", 43 | "homepage_url": "https://threadvis.github.io", 44 | "version": "[[version]]", 45 | "icons": { 46 | "32": "chrome/content/images/icon.png" 47 | }, 48 | "background": { 49 | "scripts": [ 50 | "background.js" 51 | ] 52 | }, 53 | "options_ui": { 54 | "page": "settings/options.html", 55 | "browser_style": true 56 | }, 57 | "experiment_apis": { 58 | "LegacyPref": { 59 | "schema": "experiments/LegacyPref/schema.json", 60 | "parent": { 61 | "scopes": ["addon_parent"], 62 | "paths": [["LegacyPref"]], 63 | "script": "experiments/LegacyPref/implementation.js" 64 | } 65 | }, 66 | "LegacyAccountsFolders": { 67 | "schema": "experiments/LegacyAccountsFolders/schema.json", 68 | "parent": { 69 | "scopes": ["addon_parent"], 70 | "paths": [["LegacyAccountsFolders"]], 71 | "script": "experiments/LegacyAccountsFolders/implementation.js" 72 | } 73 | }, 74 | "WindowListener": { 75 | "schema": "experiments/WindowListener/schema.json", 76 | "parent": { 77 | "scopes": ["addon_parent"], 78 | "paths": [["WindowListener"]], 79 | "script": "experiments/WindowListener/implementation.js" 80 | } 81 | }, 82 | "NotifyTools": { 83 | "schema": "experiments/NotifyTools/schema.json", 84 | "parent": { 85 | "scopes": ["addon_parent"], 86 | "paths": [["NotifyTools"]], 87 | "script": "experiments/NotifyTools/implementation.js", 88 | "events": ["startup"] 89 | } 90 | } 91 | }, 92 | "default_locale": "en" 93 | } 94 | -------------------------------------------------------------------------------- /src/chrome/content/threadvis.css: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * CSS file to layout visualisation 29 | **********************************************************************************************************************/ 30 | 31 | splitter.mousehidden { 32 | visibility: hidden; 33 | } 34 | 35 | splitter.mousehidden:hover { 36 | visibility: visible; 37 | } 38 | 39 | #ThreadVisStatusBarPanel.disabled #ThreadVisStatusText { 40 | color: #cccccc; 41 | } 42 | 43 | #ThreadVisStatusText .toolbarbutton-text { 44 | margin-left: 5px; 45 | } 46 | 47 | #ThreadVisHeaderBox { 48 | display: none; 49 | grid-row-start: 2; 50 | grid-column: 2/3; 51 | } 52 | 53 | #ThreadVis { 54 | display: grid; 55 | grid-template-columns: minmax(0, 1fr) 3px; 56 | grid-template-rows: minmax(0, 1fr) 3px; 57 | height: 100%; 58 | } 59 | 60 | #ThreadVisBox { 61 | grid-column: 1; 62 | grid-row: 1; 63 | } 64 | 65 | #ThreadVis:hover #ThreadVisHorizontalScrollbar, 66 | #ThreadVis:hover #ThreadVisVerticalScrollbar, 67 | #ThreadVis.hover #ThreadVisHorizontalScrollbar, 68 | #ThreadVis.hover #ThreadVisVerticalScrollbar { 69 | visibility: visible; 70 | } 71 | 72 | #ThreadVisHorizontalScrollbar { 73 | visibility: hidden; 74 | grid-row: 2; 75 | grid-column: 1; 76 | display: grid; 77 | grid-template-columns: 1fr; 78 | grid-template-rows: 2px; 79 | } 80 | 81 | #ThreadVisScrollbarHorizontalBox { 82 | border-radius: 2px; 83 | height: 100%; 84 | } 85 | 86 | #ThreadVisScrollbarHorizontal { 87 | height: 2px; 88 | background: #333333; 89 | border-radius: 2px; 90 | cursor: col-resize; 91 | position: relative; 92 | } 93 | 94 | #ThreadVisVerticalScrollbar { 95 | visibility: hidden; 96 | grid-row: 1; 97 | grid-column: 2; 98 | display: grid; 99 | grid-template-columns: 2px; 100 | grid-template-rows: 1fr; 101 | } 102 | 103 | #ThreadVisScrollbarVerticalBox { 104 | border-radius: 2px; 105 | width: 100%; 106 | } 107 | 108 | #ThreadVisScrollbarVertical { 109 | width: 2px; 110 | background: #333333; 111 | border-radius: 2px; 112 | cursor: row-resize; 113 | position: relative; 114 | } 115 | 116 | /** 117 | * compact attribute is set by "Compact Headers" add-on, don't 118 | * display ThreadVis if headers are compacted, as there's no 119 | * space available anyways and we'd mess around with the grid display 120 | */ 121 | #messageHeader:not([compact]).threadvis #ThreadVisHeaderBox { 122 | display: inherit; 123 | } 124 | 125 | #messageHeader:not([compact]).threadvis { 126 | grid-template-columns: auto minmax(250px, 1fr); 127 | column-gap: 2em; 128 | } 129 | 130 | #messageHeader:not([compact]).threadvis > .message-header-row:not([hidden]) { 131 | grid-column: 1/2; 132 | } 133 | 134 | #messageHeader:not([compact]).threadvis #headerSenderToolbarContainer { 135 | grid-column: 1/3 !important; 136 | } 137 | 138 | #ThreadVis .warning { 139 | position: relative; 140 | color: #999999; 141 | } 142 | 143 | #ThreadVis .warning.link { 144 | color: #0000ff; 145 | text-decoration: underline; 146 | cursor: pointer; 147 | } 148 | 149 | #ThreadVis .timeline.wrapper { 150 | display: flex; 151 | justify-content: center; 152 | position: relative; 153 | z-index: 1; 154 | height: 0; 155 | } -------------------------------------------------------------------------------- /src/experiments/NotifyTools/implementation.js: -------------------------------------------------------------------------------- 1 | /* This file is provided by the addon-developer-support repository at 2 | * https://github.com/thunderbird/addon-developer-support 3 | * 4 | * Version 1.6 5 | * - adjusted to TB128 (no longer loading Services and ExtensionCommon) 6 | * 7 | * Version 1.5 8 | * - adjusted to TB115 (Services is now in globalThis) 9 | * 10 | * Version 1.4 11 | * - updated implementation to not assign this anymore 12 | * 13 | * Version 1.3 14 | * - moved registering the observer into startup 15 | * 16 | * Version 1.1 17 | * - added startup event, to make sure API is ready as soon as the add-on is starting 18 | * NOTE: This requires to add the startup event to the manifest, see: 19 | * https://github.com/thunderbird/addon-developer-support/tree/master/auxiliary-apis/NotifyTools#usage 20 | * 21 | * Author: John Bieling (john@thunderbird.net) 22 | * 23 | * This Source Code Form is subject to the terms of the Mozilla Public 24 | * License, v. 2.0. If a copy of the MPL was not distributed with this 25 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 26 | */ 27 | 28 | /* global Services, ExtensionCommon */ 29 | 30 | "use strict"; 31 | 32 | (function (exports) { 33 | var observerTracker = new Set(); 34 | 35 | class NotifyTools extends ExtensionCommon.ExtensionAPI { 36 | getAPI(context) { 37 | return { 38 | NotifyTools: { 39 | 40 | notifyExperiment(data) { 41 | return new Promise(resolve => { 42 | Services.obs.notifyObservers( 43 | { data, resolve }, 44 | "NotifyExperimentObserver", 45 | context.extension.id 46 | ); 47 | }); 48 | }, 49 | 50 | onNotifyBackground: new ExtensionCommon.EventManager({ 51 | context, 52 | name: "NotifyTools.onNotifyBackground", 53 | register: (fire) => { 54 | observerTracker.add(fire.sync); 55 | return () => { 56 | observerTracker.delete(fire.sync); 57 | }; 58 | }, 59 | }).api(), 60 | 61 | } 62 | }; 63 | } 64 | 65 | // Force API to run at startup, otherwise event listeners might not be added at the requested time. Also needs 66 | // "events": ["startup"] in the experiment manifest 67 | onStartup() { 68 | this.onNotifyBackgroundObserver = async (aSubject, aTopic, aData) => { 69 | if ( 70 | observerTracker.size > 0 && 71 | aData == this.extension.id 72 | ) { 73 | let payload = aSubject.wrappedJSObject; 74 | 75 | // Make sure payload has a resolve function, which we use to resolve the 76 | // observer notification. 77 | if (payload.resolve) { 78 | let observerTrackerPromises = []; 79 | // Push listener into promise array, so they can run in parallel 80 | for (let listener of observerTracker.values()) { 81 | observerTrackerPromises.push(listener(payload.data)); 82 | } 83 | // We still have to await all of them but wait time is just the time needed 84 | // for the slowest one. 85 | let results = []; 86 | for (let observerTrackerPromise of observerTrackerPromises) { 87 | let rv = await observerTrackerPromise; 88 | if (rv != null) results.push(rv); 89 | } 90 | if (results.length == 0) { 91 | payload.resolve(); 92 | } else { 93 | if (results.length > 1) { 94 | console.warn( 95 | "Received multiple results from onNotifyBackground listeners. Using the first one, which can lead to inconsistent behavior.", 96 | results 97 | ); 98 | } 99 | payload.resolve(results[0]); 100 | } 101 | } else { 102 | // Older version of NotifyTools, which is not sending a resolve function, deprecated. 103 | console.log("Please update the notifyTools API and the notifyTools script to at least v1.5"); 104 | for (let listener of observerTracker.values()) { 105 | listener(payload.data); 106 | } 107 | } 108 | } 109 | }; 110 | 111 | // Add observer for notifyTools.js 112 | Services.obs.addObserver( 113 | this.onNotifyBackgroundObserver, 114 | "NotifyBackgroundObserver", 115 | false 116 | ); 117 | } 118 | 119 | onShutdown(isAppShutdown) { 120 | if (isAppShutdown) { 121 | return; // the application gets unloaded anyway 122 | } 123 | 124 | // Remove observer for notifyTools.js 125 | Services.obs.removeObserver( 126 | this.onNotifyBackgroundObserver, 127 | "NotifyBackgroundObserver" 128 | ); 129 | 130 | // Flush all caches 131 | Services.obs.notifyObservers(null, "startupcache-invalidate"); 132 | } 133 | }; 134 | 135 | exports.NotifyTools = NotifyTools; 136 | 137 | })(this) -------------------------------------------------------------------------------- /src/chrome/content/positionedcontainer.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Wraps a {Container} to make it positioned 29 | **********************************************************************************************************************/ 30 | 31 | export class PositionedContainer { 32 | 33 | /** 34 | * The wrapped {Container} 35 | */ 36 | #container; 37 | 38 | /** 39 | * Link to parent positioned container 40 | */ 41 | #parent; 42 | 43 | /** 44 | * True if this container is currently selected (focused) 45 | */ 46 | #selected = false; 47 | 48 | /** 49 | * True if this container is in the parent/child hierarchy of the selected container 50 | */ 51 | #inThread = false; 52 | 53 | /** 54 | * Height of incoming arc into this positioned container 55 | */ 56 | #arcHeight = 1; 57 | 58 | /** 59 | * Time difference towards _next_ positioned container 60 | */ 61 | #timeDifference = 0; 62 | 63 | /** 64 | * Time scaling factor 65 | */ 66 | #timeScaling = 1; 67 | 68 | /** 69 | * x position 70 | */ 71 | #x = 0; 72 | 73 | /** 74 | * Author 75 | */ 76 | #author; 77 | 78 | /** 79 | * Constructor 80 | * 81 | * @constructor 82 | * @param {Container} container - the container to wrap 83 | * @param {Number} index - position of the container 84 | * @param {Object} author - the author of the message in the container 85 | * @param {Boolean} selected - true if this container is selected 86 | * @param {Boolean} inThread - true if this container is in the parent/child chain of the selected container 87 | * @return A new positioned container 88 | */ 89 | constructor(container, author, selected, inThread) { 90 | Object.seal(this); 91 | this.#container = container; 92 | this.#author = author; 93 | this.#selected = selected; 94 | this.#inThread = inThread; 95 | } 96 | 97 | get id() { 98 | return this.#container.id; 99 | } 100 | 101 | get container() { 102 | return this.#container; 103 | } 104 | 105 | get parent() { 106 | return this.#parent; 107 | } 108 | 109 | set parent(parent) { 110 | this.#parent = parent; 111 | } 112 | 113 | get date() { 114 | return this.#container.date; 115 | } 116 | 117 | get timeDifference() { 118 | return this.#timeDifference; 119 | } 120 | 121 | set timeDifference(timeDifference) { 122 | this.#timeDifference = timeDifference; 123 | } 124 | 125 | get timeScaling() { 126 | return this.#timeScaling; 127 | } 128 | 129 | set timeScaling(timeScaling) { 130 | this.#timeScaling = timeScaling; 131 | } 132 | 133 | get x() { 134 | return this.#x; 135 | } 136 | 137 | set x(x) { 138 | this.#x = x; 139 | } 140 | 141 | get author() { 142 | return this.#author; 143 | } 144 | 145 | get message() { 146 | return this.#container.message; 147 | } 148 | 149 | get isSent() { 150 | return this.#container.message?.isSent === true; 151 | } 152 | 153 | get inThread() { 154 | return this.#inThread; 155 | } 156 | 157 | get arcHeight() { 158 | return this.#arcHeight; 159 | } 160 | 161 | set arcHeight(arcHeight) { 162 | this.#arcHeight = arcHeight; 163 | } 164 | 165 | get selected() { 166 | return this.#selected; 167 | } 168 | 169 | get depth() { 170 | return this.#container.depth; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/chrome/content/utils/date.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Various utility classes 29 | **********************************************************************************************************************/ 30 | 31 | import { Strings }from "./strings.mjs"; 32 | 33 | /** 34 | * Built-in date formatter service 35 | */ 36 | const dateFormatter = new Intl.DateTimeFormat(undefined, { 37 | "year": "2-digit", 38 | "month": "2-digit", 39 | "day": "2-digit", 40 | "hour": "2-digit", 41 | "minute": "2-digit" 42 | }); 43 | 44 | /** 45 | * Format the time difference for the label and the tooltip 46 | * 47 | * @param {integer} timeDifference - The time difference to display 48 | * @return {Object} - The formatted time difference for label and tooltip 49 | */ 50 | export const formatTimeDifference = (timeDifference) => { 51 | // timedifference is in miliseconds 52 | timeDifference = timeDifference - (timeDifference % 1000); 53 | timeDifference = timeDifference / 1000; 54 | const seconds = timeDifference % 60; 55 | timeDifference = timeDifference - seconds; 56 | timeDifference = timeDifference / 60; 57 | const minutes = timeDifference % 60; 58 | timeDifference = timeDifference - minutes; 59 | timeDifference = timeDifference / 60; 60 | const hours = timeDifference % 24; 61 | timeDifference = timeDifference - hours; 62 | timeDifference = timeDifference / 24; 63 | const days = timeDifference % 365; 64 | timeDifference = timeDifference - days; 65 | timeDifference = timeDifference / 365; 66 | const years = timeDifference; 67 | 68 | let string = ""; 69 | let toolTip = ""; 70 | 71 | // label 72 | // only display years if >= 1 73 | if (years >= 1) { 74 | string = (years + (Math.round((days / 365) * 10) / 10)) 75 | + Strings.getString("visualisation.timedifference.years.short"); 76 | // only display days if >= 1 77 | } else if (days >= 1) { 78 | string = Math.round(days + hours / 24) 79 | + Strings.getString("visualisation.timedifference.days.short"); 80 | // display hours if >= 1 81 | } else if (hours >= 1) { 82 | string = Math.round(hours + minutes / 60) + Strings.getString("visualisation.timedifference.hours.short"); 83 | // display minutes otherwise 84 | } else { 85 | string = minutes + Strings.getString("visualisation.timedifference.minutes.short"); 86 | } 87 | 88 | // tooltip 89 | if (years === 1) { 90 | toolTip = `${years} ${Strings.getString("visualisation.timedifference.year")}`; 91 | } 92 | if (years > 1) { 93 | toolTip = `${years} ${Strings.getString("visualisation.timedifference.years")}`; 94 | } 95 | if (days === 1) { 96 | toolTip += ` ${days} ${Strings.getString("visualisation.timedifference.day")}`; 97 | } 98 | if (days > 1) { 99 | toolTip += ` ${days} ${Strings.getString("visualisation.timedifference.days")}`; 100 | } 101 | if (hours === 1) { 102 | toolTip += ` ${hours} ${Strings.getString("visualisation.timedifference.hour")}`; 103 | } 104 | if (hours > 1) { 105 | toolTip += ` ${hours} ${Strings.getString("visualisation.timedifference.hours")}`; 106 | } 107 | if (minutes === 1) { 108 | toolTip += ` ${minutes} ${Strings.getString("visualisation.timedifference.minute")}`; 109 | } 110 | if (minutes > 1) { 111 | toolTip += ` ${minutes} ${Strings.getString("visualisation.timedifference.minutes")}`; 112 | } 113 | 114 | return { 115 | string: string.trim(), 116 | toolTip: toolTip.trim() 117 | }; 118 | }; 119 | 120 | /** 121 | * Format a datetime 122 | * 123 | * @param {Date} date - The date 124 | * @return {String} - The formatted date 125 | */ 126 | export const formatDate = (date) => dateFormatter.format(date); -------------------------------------------------------------------------------- /src/chrome/content/helpers/notifyTools.js: -------------------------------------------------------------------------------- 1 | // Set this to the ID of your add-on, or call notifyTools.setAddonID(). 2 | var ADDON_ID = ""; 3 | 4 | /* 5 | * This file is provided by the addon-developer-support repository at 6 | * https://github.com/thunderbird/addon-developer-support 7 | * 8 | * For usage descriptions, please check: 9 | * https://github.com/thunderbird/addon-developer-support/tree/master/scripts/notifyTools 10 | * 11 | * Version 1.7 12 | * - adjusted to TB128 (no longer loading Services) 13 | * 14 | * Version 1.6 15 | * - adjusted to Thunderbird 115 (Services is now in globalThis) 16 | * 17 | * Version 1.5 18 | * - deprecate enable(), disable() and registerListener() 19 | * - add setAddOnId() 20 | * 21 | * Version 1.4 22 | * - auto enable/disable 23 | * 24 | * Version 1.3 25 | * - registered listeners for notifyExperiment can return a value 26 | * - remove WindowListener from name of observer 27 | * 28 | * Author: John Bieling (john@thunderbird.net) 29 | * 30 | * This Source Code Form is subject to the terms of the Mozilla Public 31 | * License, v. 2.0. If a copy of the MPL was not distributed with this 32 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 33 | */ 34 | 35 | var notifyTools = { 36 | registeredCallbacks: {}, 37 | registeredCallbacksNextId: 1, 38 | addOnId: ADDON_ID, 39 | 40 | setAddOnId: function (addOnId) { 41 | this.addOnId = addOnId; 42 | }, 43 | 44 | onNotifyExperimentObserver: { 45 | observe: async function (aSubject, aTopic, aData) { 46 | if (notifyTools.addOnId == "") { 47 | throw new Error("notifyTools: ADDON_ID is empty!"); 48 | } 49 | if (aData != notifyTools.addOnId) { 50 | return; 51 | } 52 | let payload = aSubject.wrappedJSObject; 53 | 54 | // Make sure payload has a resolve function, which we use to resolve the 55 | // observer notification. 56 | if (payload.resolve) { 57 | let observerTrackerPromises = []; 58 | // Push listener into promise array, so they can run in parallel 59 | for (let registeredCallback of Object.values( 60 | notifyTools.registeredCallbacks 61 | )) { 62 | observerTrackerPromises.push(registeredCallback(payload.data)); 63 | } 64 | // We still have to await all of them but wait time is just the time needed 65 | // for the slowest one. 66 | let results = []; 67 | for (let observerTrackerPromise of observerTrackerPromises) { 68 | let rv = await observerTrackerPromise; 69 | if (rv != null) results.push(rv); 70 | } 71 | if (results.length == 0) { 72 | payload.resolve(); 73 | } else { 74 | if (results.length > 1) { 75 | console.warn( 76 | "Received multiple results from onNotifyExperiment listeners. Using the first one, which can lead to inconsistent behavior.", 77 | results 78 | ); 79 | } 80 | payload.resolve(results[0]); 81 | } 82 | } else { 83 | // Older version of NotifyTools, which is not sending a resolve function, deprecated. 84 | console.log("Please update the notifyTools API to at least v1.5"); 85 | for (let registeredCallback of Object.values( 86 | notifyTools.registeredCallbacks 87 | )) { 88 | registeredCallback(payload.data); 89 | } 90 | } 91 | }, 92 | }, 93 | 94 | addListener: function (listener) { 95 | if (Object.values(this.registeredCallbacks).length == 0) { 96 | Services.obs.addObserver( 97 | this.onNotifyExperimentObserver, 98 | "NotifyExperimentObserver", 99 | false 100 | ); 101 | } 102 | 103 | let id = this.registeredCallbacksNextId++; 104 | this.registeredCallbacks[id] = listener; 105 | return id; 106 | }, 107 | 108 | removeListener: function (id) { 109 | delete this.registeredCallbacks[id]; 110 | if (Object.values(this.registeredCallbacks).length == 0) { 111 | Services.obs.removeObserver( 112 | this.onNotifyExperimentObserver, 113 | "NotifyExperimentObserver" 114 | ); 115 | } 116 | }, 117 | 118 | removeAllListeners: function () { 119 | if (Object.values(this.registeredCallbacks).length != 0) { 120 | Services.obs.removeObserver( 121 | this.onNotifyExperimentObserver, 122 | "NotifyExperimentObserver" 123 | ); 124 | } 125 | this.registeredCallbacks = {}; 126 | }, 127 | 128 | notifyBackground: function (data) { 129 | if (this.addOnId == "") { 130 | throw new Error("notifyTools: ADDON_ID is empty!"); 131 | } 132 | return new Promise((resolve) => { 133 | Services.obs.notifyObservers( 134 | { data, resolve }, 135 | "NotifyBackgroundObserver", 136 | this.addOnId 137 | ); 138 | }); 139 | }, 140 | 141 | 142 | // Deprecated. 143 | 144 | enable: function () { 145 | console.log("Manually calling notifyTools.enable() is no longer needed."); 146 | }, 147 | 148 | disable: function () { 149 | console.log("notifyTools.disable() has been deprecated, use notifyTools.removeAllListeners() instead."); 150 | this.removeAllListeners(); 151 | }, 152 | 153 | registerListener: function (listener) { 154 | console.log("notifyTools.registerListener() has been deprecated, use notifyTools.addListener() instead."); 155 | this.addListener(listener); 156 | }, 157 | 158 | }; 159 | 160 | if (typeof window != "undefined" && window) { 161 | window.addEventListener( 162 | "unload", 163 | function (event) { 164 | notifyTools.removeAllListeners(); 165 | }, 166 | false 167 | ); 168 | } -------------------------------------------------------------------------------- /src/chrome/content/utils/preferenceskeys.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Names of preferences 29 | **********************************************************************************************************************/ 30 | 31 | export const PreferenceBranch = "extensions.threadvis."; 32 | 33 | /** 34 | * Preference names 35 | */ 36 | export const PreferenceKeys = { 37 | // list of disabled accounts 38 | DISABLED_ACCOUNTS: 39 | PreferenceBranch + "disabledaccounts", 40 | 41 | // list of disabled folders 42 | DISABLED_FOLDERS: 43 | PreferenceBranch + "disabledfolders", 44 | 45 | // check for "sent" by folder flag 46 | SENTMAIL_FOLDERFLAG: 47 | PreferenceBranch + "sentmail.folderflag", 48 | 49 | // check for "sent" by identity 50 | SENTMAIL_IDENTITY: 51 | PreferenceBranch + "sentmail.identity", 52 | 53 | // height of SVG export image 54 | SVG_HEIGHT: 55 | PreferenceBranch + "svg.height", 56 | 57 | // width of SVG export image 58 | SVG_WIDTH: 59 | PreferenceBranch + "svg.width", 60 | 61 | // show/hide statusbar icon 62 | STATUSBAR: 63 | PreferenceBranch + "statusbar.enabled", 64 | 65 | // show timeline 66 | TIMELINE: 67 | PreferenceBranch + "timeline.enabled", 68 | 69 | // font size of timeline 70 | TIMELINE_FONTSIZE: 71 | PreferenceBranch + "timeline.fontsize", 72 | 73 | // enable timescaling 74 | TIMESCALING: 75 | PreferenceBranch + "timescaling.enabled", 76 | 77 | // timescaling method 78 | TIMESCALING_METHOD: 79 | PreferenceBranch + "timescaling.method", 80 | 81 | // minimal timedifference to show 82 | TIMESCALING_MINTIMEDIFF: 83 | PreferenceBranch + "timescaling.mintimediff", 84 | 85 | // size of dot 86 | VIS_DOTSIZE: 87 | PreferenceBranch + "visualisation.dotsize", 88 | 89 | // minimum height of arc 90 | VIS_ARC_MINHEIGHT: 91 | PreferenceBranch + "visualisation.arcminheight", 92 | 93 | // radius of arc 94 | VIS_ARC_RADIUS: 95 | PreferenceBranch + "visualisation.arcradius", 96 | 97 | // height difference between two arcs 98 | VIS_ARC_DIFFERENCE: 99 | PreferenceBranch + "visualisation.arcdifference", 100 | 101 | // arc width 102 | VIS_ARC_WIDTH: 103 | PreferenceBranch + "visualisation.arcwidth", 104 | 105 | // spacing 106 | VIS_SPACING: 107 | PreferenceBranch + "visualisation.spacing", 108 | 109 | // message circles 110 | VIS_MESSAGE_CIRCLES: 111 | PreferenceBranch + "visualisation.messagecircles", 112 | 113 | // colour 114 | VIS_COLOUR: 115 | PreferenceBranch + "visualisation.colour", 116 | 117 | // background colour 118 | VIS_COLOURS_BACKGROUND: 119 | PreferenceBranch + "visualisation.colours.background", 120 | 121 | // border colour 122 | VIS_COLOURS_BORDER: 123 | PreferenceBranch + "visualisation.colours.border", 124 | 125 | // colours for received 126 | VIS_COLOURS_RECEIVED: 127 | PreferenceBranch + "visualisation.colours.received", 128 | 129 | // colours for sent 130 | VIS_COLOURS_SENT: 131 | PreferenceBranch + "visualisation.colours.sent", 132 | 133 | // colour for marking current message 134 | VIS_COLOURS_CURRENT: 135 | PreferenceBranch + "visualisation.colours.current", 136 | 137 | // hide if only one message shown 138 | VIS_HIDE_ON_SINGLE: 139 | PreferenceBranch + "visualisation.hideonsingle", 140 | 141 | // highlight message 142 | VIS_HIGHLIGHT: 143 | PreferenceBranch + "visualisation.highlight", 144 | 145 | // minimal width of visualisation 146 | VIS_MINIMAL_WIDTH: 147 | PreferenceBranch + "visualisation.minimalwidth", 148 | 149 | // opacity 150 | VIS_OPACITY: 151 | PreferenceBranch + "visualisation.opacity", 152 | 153 | // zoom 154 | VIS_ZOOM: 155 | PreferenceBranch + "visualisation.zoom", 156 | 157 | // global message index (Thunderbird internal) 158 | GLODA_ENABLED: 159 | "mailnews.database.global.indexer.enabled" 160 | }; -------------------------------------------------------------------------------- /src/chrome/content/message.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Wrap email message 29 | **********************************************************************************************************************/ 30 | 31 | import { Preferences } from "./utils/preferences.mjs"; 32 | import { References } from "./utils/references.mjs"; 33 | import { SentMailIdentities } from "./utils/sentmailidentities.mjs"; 34 | 35 | export class Message { 36 | /** 37 | * {GlodaMessage} object 38 | */ 39 | #glodaMessage; 40 | 41 | /** 42 | * Constructor 43 | * 44 | * @constructor 45 | * @param {GlodaMessage} glodaMessage - The gloda message object 46 | * @return {ThreadVis.Message} - A new message 47 | */ 48 | constructor(glodaMessage) { 49 | Object.seal(this); 50 | /** 51 | * Gloda message 52 | */ 53 | this.#glodaMessage = glodaMessage; 54 | } 55 | 56 | /** 57 | * Get date of message 58 | * 59 | * @return {Date} - The date of the message 60 | */ 61 | get date() { 62 | return this.#glodaMessage.date; 63 | } 64 | 65 | /** 66 | * Get folder message is in 67 | * 68 | * @return {String} - The folder of the message 69 | */ 70 | get folder() { 71 | return this.#glodaMessage.folderURI; 72 | } 73 | 74 | /** 75 | * Get sender of message 76 | * 77 | * @return {String} - The sender of the message 78 | */ 79 | get from() { 80 | if (this.#glodaMessage.folderMessage) { 81 | return this.#glodaMessage.folderMessage.mime2DecodedAuthor; 82 | } 83 | return this.#glodaMessage.from; 84 | } 85 | 86 | /** 87 | * Parse email address from "From" header 88 | * 89 | * @return {String} - The parsed email address 90 | */ 91 | get fromEmail() { 92 | return this.#glodaMessage.from.value; 93 | } 94 | 95 | /** 96 | * Get message id 97 | * 98 | * @return {String} - The message id 99 | */ 100 | get id() { 101 | return this.#glodaMessage.headerMessageID; 102 | } 103 | 104 | /** 105 | * Get references 106 | * 107 | * @return {Array} - The parsed references header 108 | */ 109 | get references() { 110 | if (this.#glodaMessage.folderMessage) { 111 | return References.get(this.#glodaMessage.folderMessage.getStringProperty("references")); 112 | } 113 | return []; 114 | } 115 | 116 | /** 117 | * Get original subject 118 | * 119 | * @return {String} - The subject 120 | */ 121 | get subject() { 122 | return this.#glodaMessage.subject; 123 | } 124 | 125 | /** 126 | * See if message is sent (i.e. in sent-mail folder) 127 | * 128 | * @return {Boolean} - True if the message was sent by the user, false if not 129 | */ 130 | get isSent() { 131 | let issent = false; 132 | // it is sent if it is stored in a folder that is marked as sent (if enabled) 133 | if (this.#glodaMessage.folderMessage) { 134 | issent ||= this.#glodaMessage.folderMessage.folder.isSpecialFolder(Components.interfaces.nsMsgFolderFlags.SentMail, true) 135 | && Preferences.get(Preferences.SENTMAIL_FOLDERFLAG); 136 | } 137 | // or it is sent if the sender address is a local identity (if enabled) 138 | issent ||= SentMailIdentities[this.#glodaMessage.from.value] 139 | && Preferences.get(Preferences.SENTMAIL_IDENTITY); 140 | return issent; 141 | } 142 | 143 | /** 144 | * Get body of message 145 | * 146 | * @return {String} - The body of the message 147 | */ 148 | get body() { 149 | return this.#glodaMessage.indexedBodyText; 150 | } 151 | 152 | /** 153 | * Return message as string 154 | * 155 | * @return {String} - The string representation of the message 156 | */ 157 | toString() { 158 | return "Message: Subject: '" + this.subject + "'." 159 | + " From: '" + this.from + "'." 160 | + " MsgId: '" + this.id + "'." 161 | + " Date: '" + this.date + "'. " 162 | + " Folder: '" + this.folder + "'. " 163 | + " Refs: '" + this.references + "'. " 164 | + " Sent: '" + this.isSent + "'"; 165 | } 166 | 167 | /** 168 | * Get the underlying nsIMsgDBHdr 169 | * 170 | * @return {nsIMsgDBHdr} - The original nsIMsgDBHdr or null if not found 171 | */ 172 | get msgDbHdr() { 173 | if (!this.#glodaMessage.folderMessage) { 174 | console.error(`ThreadVis: Unable to find nsIMsgDBHdr for message ${this.id}, probably in folder ${this.folder}. Either the message database (msf) for this folder is corrupt, or the global index is out-of-date.`); 175 | } 176 | return this.#glodaMessage.folderMessage; 177 | } 178 | 179 | get messageURI() { 180 | return this.#glodaMessage.folderMessageURI; 181 | } 182 | } -------------------------------------------------------------------------------- /src/chrome/content/container.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Implements container for messages 29 | **********************************************************************************************************************/ 30 | 31 | export class Container { 32 | 33 | /** 34 | * Unique id of the container 35 | */ 36 | #id; 37 | 38 | /** 39 | * Store message in this container 40 | * 41 | * @type {Message} 42 | */ 43 | #message; 44 | 45 | /** 46 | * Parent of this container 47 | * 48 | * @type {Container} 49 | */ 50 | #parent; 51 | 52 | /** 53 | * Children of this container 54 | * 55 | * @type {Array} 56 | */ 57 | #children = []; 58 | 59 | /** 60 | * Constructor 61 | * 62 | * @constructor 63 | * @return A new container 64 | */ 65 | constructor(id, message) { 66 | Object.seal(this); 67 | this.#id = id; 68 | this.#message = message; 69 | } 70 | 71 | /** 72 | * Get unique id of this container 73 | */ 74 | get id() { 75 | return this.#id; 76 | } 77 | 78 | /** 79 | * Add child to this container. 80 | * Removes child from old parent. 81 | * Inserts child and all its children. 82 | * 83 | * @param {Container} child - The child to add 84 | */ 85 | addChild(child) { 86 | // check if child is already our child. if so, do nothing 87 | if (child.#parent === this) { 88 | return; 89 | } 90 | 91 | // check to see if this container is a child of child. 92 | // this should never happen, because we would create a loop. that's why we don't allow it 93 | if (this.findParent(child)) { 94 | return; 95 | } 96 | 97 | // remove from previous parent 98 | child.#parent?.removeChild(child); 99 | // set new parent 100 | child.#parent = this; 101 | this.#children.push(child); 102 | } 103 | 104 | /** 105 | * See if this container or any of its parents contains a specific container 106 | * 107 | * @param {Container} target - The container to find 108 | * @returns {Boolean} - True if the container is contained in the parent list, false if not 109 | */ 110 | findParent(target) { 111 | const container = this.#parent; 112 | 113 | if (!container) { 114 | return false; 115 | } 116 | 117 | if (container === target) { 118 | return true; 119 | } 120 | 121 | return container.findParent(target); 122 | } 123 | 124 | /** 125 | * Get all children of this container as a flat array 126 | * 127 | * @return {Array} - All children of the current container, recursively 128 | */ 129 | get children() { 130 | return this.#children 131 | .map((container) => [container, container.children]) 132 | .flat(Infinity); 133 | } 134 | 135 | /** 136 | * Get recursive container count 137 | * 138 | * @return {Number} - The number of all children, counted recursively 139 | */ 140 | get count() { 141 | return this.#children.reduce((count, container) => count += container.count, 1); 142 | } 143 | 144 | /** 145 | * Get date of message 146 | * 147 | * @ {Date} - The date of the message 148 | */ 149 | get date() { 150 | if (this.message) { 151 | return this.message.date; 152 | } 153 | 154 | if (this.#children.length > 0) { 155 | return this.#children[0].date; 156 | } 157 | 158 | // we are dummy, we have NO child: this shouldn't happen 159 | return new Date(); 160 | } 161 | 162 | /** 163 | * Get depth of message in tree 164 | * 165 | * @return {Number} - The generational depth of the message 166 | */ 167 | get depth() { 168 | if (this.#parent) { 169 | return 1 + this.#parent.depth; 170 | } else { 171 | return 0; 172 | } 173 | } 174 | 175 | /** 176 | * Get message of this container 177 | * 178 | * @return {Message} - The message 179 | */ 180 | get message() { 181 | return this.#message; 182 | } 183 | 184 | /** 185 | * Set message of this container 186 | * 187 | * @param {GlodaMessage} message 188 | */ 189 | set message(message) { 190 | this.#message = message; 191 | } 192 | 193 | /** 194 | * Get parent of this container 195 | * 196 | * @return {Container} - The parent of the message 197 | */ 198 | get parent() { 199 | return this.#parent; 200 | } 201 | 202 | /** 203 | * Get topmost container 204 | * 205 | * @return {Container} - The topmost container of the thread 206 | */ 207 | get root() { 208 | if (this.#parent) { 209 | return this.#parent.root; 210 | } 211 | return this; 212 | } 213 | 214 | /** 215 | * Remove a child from the list 216 | * 217 | * @param {ThreadVis.Container} child - The child container to remove 218 | */ 219 | removeChild(child) { 220 | // check if child is in fact our child 221 | if (child.parent !== this) { 222 | return; 223 | } 224 | 225 | this.#children = this.#children.filter((container) => container !== child); 226 | child.#parent = undefined; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/chrome/content/timeline.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Draw the timeline. 29 | **********************************************************************************************************************/ 30 | 31 | import { Preferences } from "./utils/preferences.mjs"; 32 | import { formatTimeDifference } from "./utils/date.mjs"; 33 | 34 | export class Timeline { 35 | 36 | /** 37 | * XUL/DOM document to draw in 38 | */ 39 | #document; 40 | 41 | /** 42 | * XUL stack to draw timeline on 43 | */ 44 | #stack; 45 | 46 | /** 47 | * current thread 48 | */ 49 | #thread; 50 | 51 | /** 52 | * resize multiplicator 53 | */ 54 | #resize; 55 | 56 | /** 57 | * top position of center of visualisation in px 58 | */ 59 | #top; 60 | 61 | /** 62 | * cache timing info for containers 63 | */ 64 | #times = {}; 65 | 66 | /** 67 | * Constructor for timeline class 68 | * 69 | * @param {DOMDocument} document - The document to draw in 70 | * @param {XULStack} stack - The stack to draw the timeline on 71 | * @param {ThreadVis.Thread} thread - The current thread 72 | * @param {Number} resize - The resize parameter [0..1] 73 | * @param {Number} top - The top position of the timeline 74 | * @return A new timeline object 75 | */ 76 | constructor(document, stack, thread, resize, top) { 77 | Object.seal(this); 78 | this.#document = document; 79 | this.#stack = stack; 80 | this.#thread = thread; 81 | this.#resize = resize; 82 | this.#top = top; 83 | } 84 | 85 | /** 86 | * Draw the timeline 87 | */ 88 | draw() { 89 | // start with second container 90 | const containers = this.#thread.containers; 91 | for (let i = 1; i < containers.length; i++) { 92 | // look at two adjacent containers 93 | const first = containers[i - 1]; 94 | const second = containers[i]; 95 | 96 | // don't calculate time if one of them does not have a message 97 | if (!first.message || !second.message) { 98 | continue; 99 | } 100 | 101 | const timeDifference = first.timeDifference; 102 | 103 | // get the formatted strings 104 | const formatted = formatTimeDifference(timeDifference); 105 | 106 | // draw the labels and tooltips 107 | this.#drawTime(first.id, first.x, second.x, formatted.string, formatted.toolTip); 108 | } 109 | } 110 | 111 | /** 112 | * Draw the label and the tooltip 113 | * 114 | * @param {ThreadVis.Container.Id} containerId - The container to draw 115 | * @param {Number} left - The left position 116 | * @param {Number} right - The right position 117 | * @param {String} string - The string to display 118 | * @param {String} toolTip - The tooltip to add 119 | */ 120 | #drawTime(containerId, left, right, string, toolTip) { 121 | const dotSize = Preferences.get(Preferences.VIS_DOTSIZE); 122 | const minArcHeight = Preferences.get(Preferences.VIS_ARC_MINHEIGHT); 123 | const fontSize = Preferences.get(Preferences.TIMELINE_FONTSIZE); 124 | 125 | // check to see if we already created the label and the tooltip 126 | let elem = null; 127 | let wrapperElem = null; 128 | if (this.#times[containerId]) { 129 | ({ elem, wrapperElem } = this.#times[containerId]); 130 | } else { 131 | elem = this.#document.createXULElement("description"); 132 | elem.style.display = "inline-block"; 133 | wrapperElem = this.#document.createElement("div"); 134 | wrapperElem.classList.add("timeline", "wrapper"); 135 | this.#times[containerId] = { elem, wrapperElem }; 136 | 137 | // and add to stack only if we just created the element 138 | this.#stack.appendChild(wrapperElem); 139 | wrapperElem.appendChild(elem); 140 | 141 | // prevent mousedown event from bubbling to box object 142 | // prevent dragging of visualisation by clicking on message 143 | elem.addEventListener("mousedown", (event) => event.stopPropagation(), true); 144 | } 145 | 146 | // calculate position 147 | const posLeft = (left + dotSize / 2) * this.#resize; 148 | const posTop = (this.#top - dotSize / 2 - fontSize) * this.#resize - 1; 149 | const posWidth = ((right - left - dotSize) * this.#resize); 150 | 151 | // set style 152 | elem.style.fontSize = `${fontSize}px`; 153 | wrapperElem.style.left = `${posLeft}px`; 154 | wrapperElem.style.top = `${posTop}px`; 155 | wrapperElem.style.width = `${posWidth}px`; 156 | 157 | elem.setAttribute("value", string); 158 | elem.setAttribute("tooltiptext", toolTip); 159 | 160 | // force-show wrapper elem to calculate size 161 | wrapperElem.style.display = "flex"; 162 | 163 | if ((elem.clientWidth > wrapperElem.clientWidth) || (fontSize > minArcHeight * this.#resize)) { 164 | wrapperElem.style.display = "none"; 165 | } else { 166 | // not hidden, enough space. assign correct width to center text 167 | wrapperElem.style.display = "flex"; 168 | } 169 | } 170 | 171 | /** 172 | * Re-Draw the timeline 173 | * 174 | * @param {ThreadVis.Thread} thread - The current thread 175 | * @param {Number} resize - The resize parameter 176 | * @param {Number} top - The top position 177 | */ 178 | redraw(thread, resize, top) { 179 | this.#thread = thread; 180 | this.#resize = resize; 181 | this.#top = top; 182 | this.draw(); 183 | } 184 | } -------------------------------------------------------------------------------- /src/chrome/content/arcvisualisation.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * JavaScript file to visualise message arc in threadvis 29 | **********************************************************************************************************************/ 30 | 31 | import { Preferences } from "./utils/preferences.mjs"; 32 | 33 | export class ArcVisualisation { 34 | /** 35 | * XUL/DOM window to draw in 36 | */ 37 | #document; 38 | 39 | /** 40 | * XUL stack on which to draw 41 | */ 42 | #stack; 43 | 44 | /** 45 | * size of the dot representing a message in px 46 | */ 47 | #dotSize; 48 | 49 | /** 50 | * resize multiplicator 51 | */ 52 | #resize; 53 | 54 | /** 55 | * the minimum arc height in px 56 | */ 57 | #arcMinHeight; 58 | 59 | /** 60 | * the (height) difference between two arcs in px 61 | */ 62 | #arcDifference; 63 | 64 | /** 65 | * the corner radius for an arc in px 66 | */ 67 | #arcRadius; 68 | 69 | /** 70 | * width of an arc in px 71 | */ 72 | #arcWidth; 73 | 74 | /** 75 | * colour of the arc 76 | */ 77 | #colour; 78 | 79 | /** 80 | * vertical position of arc ("top" or "bottom") 81 | */ 82 | #vPosition; 83 | 84 | /** 85 | * height of arc (counting from 0) multiplied by arcDifference to get height in px 86 | */ 87 | #height; 88 | 89 | /** 90 | * left edge of arc in px 91 | */ 92 | #left; 93 | 94 | /** 95 | * right edge of arc in pc 96 | */ 97 | #right; 98 | 99 | /** 100 | * top edge of arc in px 101 | */ 102 | #top; 103 | 104 | /** 105 | * opacity of item 106 | */ 107 | #opacity; 108 | 109 | /** 110 | * XUL box element which visualises the arc 111 | */ 112 | #arc; 113 | 114 | /** 115 | * Constructor for visualisation class 116 | * 117 | * @constructor 118 | * @param {DOMElement} document - The document to draw on 119 | * @param {DOMElement} stack - The stack to draw on 120 | * @param {Number} resize - The resize parameter 121 | * @param {String} colour - The colour of the arc 122 | * @param {String} vPosition - The vertical position (top/bottom) 123 | * @param {Number} height - The height of the arc 124 | * @param {Number} left - The left position of the arc 125 | * @param {Number} right - The right position of the arc 126 | * @param {Number} top - The top position 127 | * @param {Number} opacity - The opacity 128 | * @return {ArcVisualisation} - A new arc visualisation object 129 | */ 130 | constructor(document, stack, resize, colour, vPosition, height, left, right, top, opacity) { 131 | Object.seal(this); 132 | 133 | this.#dotSize = Preferences.get(Preferences.VIS_DOTSIZE); 134 | this.#arcMinHeight = Preferences.get(Preferences.VIS_ARC_MINHEIGHT); 135 | this.#arcDifference = Preferences.get(Preferences.VIS_ARC_DIFFERENCE); 136 | this.#arcRadius = Preferences.get(Preferences.VIS_ARC_RADIUS); 137 | this.#arcWidth = Preferences.get(Preferences.VIS_ARC_WIDTH); 138 | 139 | this.#document = document; 140 | this.#stack = stack; 141 | this.#resize = resize; 142 | this.#colour = colour; 143 | this.#vPosition = vPosition; 144 | this.#height = height; 145 | this.#left = left; 146 | this.#right = right; 147 | this.#top = top; 148 | this.#opacity = opacity; 149 | this.#arc = null; 150 | 151 | this.#draw(); 152 | } 153 | 154 | /** 155 | * Draw arc 156 | */ 157 | #draw() { 158 | this.#arc = this.#document.createXULElement("box"); 159 | this.#arc.style.position = "relative"; 160 | 161 | this.#visualise(); 162 | this.#stack.appendChild(this.#arc); 163 | } 164 | 165 | /** 166 | * Re-Draw arc 167 | * 168 | * @param {Number} resize - The resize parameter 169 | * @param {Number} left - The left position 170 | * @param {Number} right - The right position 171 | * @param {Number} top - The top position 172 | * @param {String} colour - The colour 173 | * @param {Number} opacity - The opacity 174 | */ 175 | redraw(resize, left, right, top, colour, opacity) { 176 | this.#resize = resize; 177 | this.#left = left; 178 | this.#top = top; 179 | this.#right = right; 180 | this.#colour = colour; 181 | this.#opacity = opacity; 182 | 183 | this.#visualise(); 184 | } 185 | 186 | /** 187 | * Visualise arc 188 | */ 189 | #visualise() { 190 | let arcTop = 0; 191 | if (this.#vPosition === "top") { 192 | arcTop = (this.#top - ((this.#dotSize / 2) + this.#arcMinHeight + (this.#arcDifference * this.#height))) * this.#resize; 193 | } else { 194 | arcTop = (this.#top + (this.#dotSize / 2)) * this.#resize; 195 | } 196 | 197 | let posTop = arcTop; 198 | let posLeft = (this.#left - (this.#arcWidth / 2)) * this.#resize; 199 | let posHeight = (this.#arcMinHeight + this.#arcDifference * this.#height) * this.#resize; 200 | let posWidth = (this.#right - this.#left + this.#arcWidth) * this.#resize; 201 | let styleBackground = this.#colour; 202 | let styleOpacity = this.#opacity; 203 | 204 | this.#arc.style.top = `${posTop}px`; 205 | this.#arc.style.left = `${posLeft}px`; 206 | this.#arc.style.height = `${posHeight}px`; 207 | this.#arc.style.width = `${posWidth}px`; 208 | this.#arc.style.opacity = styleOpacity; 209 | if (this.#vPosition === "top") { 210 | this.#arc.style.borderTopLeftRadius = `${this.#arcRadius}px`; 211 | this.#arc.style.borderTopRightRadius = `${this.#arcRadius}px`; 212 | this.#arc.style.borderTop = `${this.#arcWidth * this.#resize}px solid ${styleBackground}`; 213 | this.#arc.style.borderLeft = `${this.#arcWidth * this.#resize}px solid ${styleBackground}`; 214 | this.#arc.style.borderRight = `${this.#arcWidth * this.#resize}px solid ${styleBackground}`; 215 | } else { 216 | this.#arc.style.borderBottomLeftRadius = `${this.#arcRadius}px`; 217 | this.#arc.style.borderBottomRightRadius = `${this.#arcRadius}px`; 218 | this.#arc.style.borderBottom = `${this.#arcWidth * this.#resize}px solid ${styleBackground}`; 219 | this.#arc.style.borderLeft = `${this.#arcWidth * this.#resize}px solid ${styleBackground}`; 220 | this.#arc.style.borderRight = `${this.#arcWidth * this.#resize}px solid ${styleBackground}`; 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | // * ******************************************************************************************************************* 2 | // * This file is part of ThreadVis. 3 | // * https://threadvis.github.io 4 | // * 5 | // * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | // * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | // * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | // * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | // * 10 | // * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | // * Copyright (C) 2007, 2008, 2009, 12 | // * 2010, 2011, 2013, 2018, 2019, 13 | // * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | // * 15 | // * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | // * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | // * or (at your option) any later version. 18 | // * 19 | // * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | // * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | // * See the GNU Affero General Public License for more details. 22 | // * 23 | // * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | // * If not, see . 25 | // * 26 | // * Version: $Id$ 27 | // * ******************************************************************************************************************* 28 | // * English translations 29 | // ********************************************************************************************************************/ 30 | 31 | { 32 | "extensionName": { 33 | "message": "ThreadVis", 34 | "description": "Name of the extension." 35 | }, 36 | "extensionDescription": { 37 | "message": "Displays a small graphic visualising the context (thread) of the currently selected email.", 38 | "description": "Description of the extension." 39 | }, 40 | "options.sentmail.caption": { 41 | "message": "Detect sent messages" 42 | }, 43 | "options.sentmail.folderflag": { 44 | "message": "Treat all emails in 'sent' folders as sent email." 45 | }, 46 | "options.sentmail.identity": { 47 | "message": "Treat all emails with a local identity as sender as a sent email." 48 | }, 49 | "options.statusbar.caption": { 50 | "message": "Statusbar icon" 51 | }, 52 | "options.statusbar.enabled": { 53 | "message": "Show ThreadVis icon in statusbar" 54 | }, 55 | "options.statusbar.description": { 56 | "message": "For this setting to take effect, please disable and re-enable the add-on." 57 | }, 58 | "options.visualisation.zoom.caption": { 59 | "message": "Default Size" 60 | }, 61 | "options.visualisation.zoom.fit": { 62 | "message": "fit (auto zoom)" 63 | }, 64 | "options.visualisation.zoom.full": { 65 | "message": "full (no zoom)" 66 | }, 67 | "options.visualisation.enabledaccounts.caption": { 68 | "message": "Enabled Accounts and Folders" 69 | }, 70 | "options.visualisation.enabledaccounts.description": { 71 | "message": "Only enable extension for selected accounts and folders." 72 | }, 73 | "options.visualisation.enabledaccounts.button.all": { 74 | "message": "All" 75 | }, 76 | "options.visualisation.enabledaccounts.button.none": { 77 | "message": "None" 78 | }, 79 | "options.visualisation.timescaling.caption": { 80 | "message": "Time Scaling" 81 | }, 82 | "options.visualisation.timescaling.enable": { 83 | "message": "Enable Time Scaling" 84 | }, 85 | "options.visualisation.timescaling.disable": { 86 | "message": "Disable Time Scaling" 87 | }, 88 | "options.visualisation.timescaling.description.disabled": { 89 | "message": "If Time Scaling is disabled, the horizontal spacing between two adjacent messages is always constant." 90 | }, 91 | "options.visualisation.timescaling.description.enabled": { 92 | "message": "If Time Scaling is enabled, the horizontal spacing between two adjacent messages is proportional to the time difference between those two messages." 93 | }, 94 | "options.visualisation.timescaling.method": { 95 | "message": "Time Scaling Method" 96 | }, 97 | "options.visualisation.timescaling.method.linear": { 98 | "message": "linear" 99 | }, 100 | "options.visualisation.timescaling.method.logarithmic": { 101 | "message": "logarithmic" 102 | }, 103 | "options.visualisation.timescaling.mintimediff": { 104 | "message": "Minimal displayed time difference" 105 | }, 106 | "options.visualisation.timescaling.mintimediff.automatic": { 107 | "message": "automatic" 108 | }, 109 | "options.visualisation.timescaling.mintimediff.1min": { 110 | "message": "1 minute" 111 | }, 112 | "options.visualisation.timescaling.mintimediff.10min": { 113 | "message": "10 minutes" 114 | }, 115 | "options.visualisation.timescaling.mintimediff.30min": { 116 | "message": "30 minutes" 117 | }, 118 | "options.visualisation.timescaling.mintimediff.1hour": { 119 | "message": "1 hour" 120 | }, 121 | "options.visualisation.timescaling.mintimediff.12hours": { 122 | "message": "12 hours" 123 | }, 124 | "options.visualisation.timescaling.mintimediff.1day": { 125 | "message": "1 day" 126 | }, 127 | "options.visualisation.timescaling.mintimediff.2days": { 128 | "message": "2 days" 129 | }, 130 | "options.visualisation.timescaling.mintimediff.10days": { 131 | "message": "10 days" 132 | }, 133 | "options.visualisation.timeline.enable": { 134 | "message": "Display the time between two messages." 135 | }, 136 | "options.visualisation.timeline.fontsize": { 137 | "message": "Timeline Font Size" 138 | }, 139 | "options.visualisation.size.caption": { 140 | "message": "Size" 141 | }, 142 | "options.visualisation.size.dotsize": { 143 | "message": "Size of Messages" 144 | }, 145 | "options.visualisation.size.arcminheight": { 146 | "message": "Minimum Arc Height" 147 | }, 148 | "options.visualisation.size.arcradius": { 149 | "message": "Arc Radius" 150 | }, 151 | "options.visualisation.size.arcdifference": { 152 | "message": "Spacing between Arcs" 153 | }, 154 | "options.visualisation.size.arcwidth": { 155 | "message": "Arc Width" 156 | }, 157 | "options.visualisation.size.spacing": { 158 | "message": "Spacing between Messages" 159 | }, 160 | "options.visualisation.size.messagecircles": { 161 | "message": "Draw messages as circles" 162 | }, 163 | "options.visualisation.colour.caption": { 164 | "message": "Colours" 165 | }, 166 | "options.visualisation.colour.single.caption": { 167 | "message": "Single Colour" 168 | }, 169 | "options.visualisation.colour.single.description": { 170 | "message": "Only use a single colour in the visualisation. The selected message is displayed in blue, other messages are displayed in grey." 171 | }, 172 | "options.visualisation.colour.author.caption": { 173 | "message": "Multiple Colours" 174 | }, 175 | "options.visualisation.colour.author.description": { 176 | "message": "Use different colours for different authors." 177 | }, 178 | "options.visualisation.colour.highlight": { 179 | "message": "Underline authors in header display with their colours." 180 | }, 181 | "options.visualisation.opacity": { 182 | "message": "Fading for nonrelevant subthreads:" 183 | }, 184 | "options.visualisation.colour.sent": { 185 | "message": "Colour for sent emails" 186 | }, 187 | "options.visualisation.colour.received": { 188 | "message": "Colours (comma-separated) for received emails" 189 | }, 190 | "options.visualisation.colour.current": { 191 | "message": "Colour to highlight current message" 192 | }, 193 | "options.about.version": { 194 | "message": "Version" 195 | }, 196 | "extension.copyright1": { 197 | "message": "\u00a9 2005-2007 Alexander C. Hubmann" 198 | }, 199 | "extension.copyright2": { 200 | "message": "\u00a9 2007-2025 Alexander C. Hubmann-Haidvogel" 201 | }, 202 | "extension.homepage": { 203 | "message": "https://threadvis.github.io" 204 | }, 205 | "extension.email": { 206 | "message": "ahubmann@gmail.com" 207 | }, 208 | "extension.version": { 209 | "message": "[[version]]" 210 | } 211 | } -------------------------------------------------------------------------------- /src/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | // * ******************************************************************************************************************* 2 | // * This file is part of ThreadVis. 3 | // * https://threadvis.github.io 4 | // * 5 | // * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | // * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | // * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | // * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | // * 10 | // * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | // * Copyright (C) 2007, 2008, 2009, 12 | // * 2010, 2011, 2013, 2018, 2019, 13 | // * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | // * 15 | // * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | // * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | // * or (at your option) any later version. 18 | // * 19 | // * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | // * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | // * See the GNU Affero General Public License for more details. 22 | // * 23 | // * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | // * If not, see . 25 | // * 26 | // * Version: $Id$ 27 | // * ******************************************************************************************************************* 28 | // * German translations 29 | // ********************************************************************************************************************/ 30 | 31 | { 32 | "extensionName": { 33 | "message": "ThreadVis", 34 | "description": "Name of the extension." 35 | }, 36 | "extensionDescription": { 37 | "message": "Zeigt eine kleine Grafik, die den Kontext (Thread) der aktuell angezeigten Nachricht visualisiert.", 38 | "description": "Description of the extension." 39 | }, 40 | "options.sentmail.caption": { 41 | "message": "Erkennung gesendeter Nachrichten" 42 | }, 43 | "options.sentmail.folderflag": { 44 | "message": "Alle E-Mails in 'Gesendet' Ordnern als gesendet behandeln." 45 | }, 46 | "options.sentmail.identity": { 47 | "message": "Alle E-Mails mit einer lokalen Identität als Absender als gesendet behandeln." 48 | }, 49 | "options.statusbar.caption": { 50 | "message": "Icon in Statusleiste" 51 | }, 52 | "options.statusbar.enabled": { 53 | "message": "Zeige ThreadVis-Icon in Statusleiste" 54 | }, 55 | "options.statusbar.description": { 56 | "message": "Um diese Einstellung anzuwenden, bitte das Add-on deaktivieren und wieder aktivieren." 57 | }, 58 | "options.visualisation.zoom.caption": { 59 | "message": "Standard-Größe" 60 | }, 61 | "options.visualisation.zoom.fit": { 62 | "message": "angepasst (Auto-Zoom)" 63 | }, 64 | "options.visualisation.zoom.full": { 65 | "message": "normale Größe (kein Zoom)" 66 | }, 67 | "options.visualisation.enabledaccounts.caption": { 68 | "message": "Aktivierte Konten und Ordner" 69 | }, 70 | "options.visualisation.enabledaccounts.description": { 71 | "message": "Aktiviert die Erweiterung nur für die ausgewählten Konten und Ordner." 72 | }, 73 | "options.visualisation.timescaling.caption": { 74 | "message": "Time Scaling" 75 | }, 76 | "options.visualisation.timescaling.enable": { 77 | "message": "Time Scaling aktivieren" 78 | }, 79 | "options.visualisation.timescaling.disable": { 80 | "message": "Time Scaling deaktivieren" 81 | }, 82 | "options.visualisation.timescaling.description.disabled": { 83 | "message": "Wenn Time Scaling deaktiviert ist, ist der horizontale Abstand zwischen benachbarten Nachrichten immer konstant." 84 | }, 85 | "options.visualisation.timescaling.description.enabled": { 86 | "message": "Wenn Time Scaling aktiviert ist, ist der horizontale Abstand zwischen benachbarten Nachrichten proportional zum zeitlichen Abstand dieser Nachrichten." 87 | }, 88 | "options.visualisation.timescaling.method": { 89 | "message": "Time Scaling-Methode" 90 | }, 91 | "options.visualisation.timescaling.method.linear": { 92 | "message": "linear" 93 | }, 94 | "options.visualisation.timescaling.method.logarithmic": { 95 | "message": "logarithmisch" 96 | }, 97 | "options.visualisation.timescaling.mintimediff": { 98 | "message": "Minimal dargestellte Zeitdifferenz" 99 | }, 100 | "options.visualisation.timescaling.mintimediff.automatic": { 101 | "message": "automatisch" 102 | }, 103 | "options.visualisation.timescaling.mintimediff.1min": { 104 | "message": "1 Minute" 105 | }, 106 | "options.visualisation.timescaling.mintimediff.10min": { 107 | "message": "10 Minuten" 108 | }, 109 | "options.visualisation.timescaling.mintimediff.30min": { 110 | "message": "30 Minuten" 111 | }, 112 | "options.visualisation.timescaling.mintimediff.1hour": { 113 | "message": "1 Stunde" 114 | }, 115 | "options.visualisation.timescaling.mintimediff.12hours": { 116 | "message": "12 Stunden" 117 | }, 118 | "options.visualisation.timescaling.mintimediff.1day": { 119 | "message": "1 Tag" 120 | }, 121 | "options.visualisation.timescaling.mintimediff.2days": { 122 | "message": "2 Tage" 123 | }, 124 | "options.visualisation.timescaling.mintimediff.10days": { 125 | "message": "10 Tage" 126 | }, 127 | "options.visualisation.timeline.enable": { 128 | "message": "Die vergangene Zeit zwischen den einzelnen Nachrichten einblenden." 129 | }, 130 | "options.visualisation.timeline.fontsize": { 131 | "message": "Schriftgröße der Timeline" 132 | }, 133 | "options.visualisation.size.caption": { 134 | "message": "Größe" 135 | }, 136 | "options.visualisation.size.dotsize": { 137 | "message": "Nachrichtengröße" 138 | }, 139 | "options.visualisation.size.arcminheight": { 140 | "message": "Minimale Bogenhöhe" 141 | }, 142 | "options.visualisation.size.arcradius": { 143 | "message": "Bogenradius" 144 | }, 145 | "options.visualisation.size.arcdifference": { 146 | "message": "Abstand zwischen zwei Bögen" 147 | }, 148 | "options.visualisation.size.arcwidth": { 149 | "message": "Linienbreite" 150 | }, 151 | "options.visualisation.size.spacing": { 152 | "message": "Abstand zwischen zwei Nachrichten" 153 | }, 154 | "options.visualisation.size.messagecircles": { 155 | "message": "Nachrichten als Kreise zeichnen" 156 | }, 157 | "options.visualisation.colour.caption": { 158 | "message": "Farben" 159 | }, 160 | "options.visualisation.colour.single.caption": { 161 | "message": "Einzelne Farbe" 162 | }, 163 | "options.visualisation.colour.single.description": { 164 | "message": "Verwendet nur eine einzige Farbe in der Visualisierung. Die ausgewählte Nachricht wird blau dargestellt, andere Nachrichten in grau." 165 | }, 166 | "options.visualisation.colour.author.caption": { 167 | "message": "Mehrere Farben" 168 | }, 169 | "options.visualisation.colour.author.description": { 170 | "message": "Verwendet verschiedene Farben für verschiedene Autoren." 171 | }, 172 | "options.visualisation.colour.highlight": { 173 | "message": "Die E-Mail-Adressen der Autoren farbig unterstreichen." 174 | }, 175 | "options.visualisation.opacity": { 176 | "message": "Unwichtige Sub-Threads ausblenden:" 177 | }, 178 | "options.visualisation.colour.sent": { 179 | "message": "Farbe für gesendete E-Mails" 180 | }, 181 | "options.visualisation.colour.received": { 182 | "message": "Farben (durch Beistrich getrennt) für empfangene E-Mails" 183 | }, 184 | "options.visualisation.colour.current": { 185 | "message": "Farbe um aktuell angezeigte E-Mail hervorzuheben" 186 | }, 187 | "options.about.version": { 188 | "message": "Version" 189 | }, 190 | "extension.copyright1": { 191 | "message": "\u00a9 2005-2007 Alexander C. Hubmann" 192 | }, 193 | "extension.copyright2": { 194 | "message": "\u00a9 2007-2025 Alexander C. Hubmann-Haidvogel" 195 | }, 196 | "extension.homepage": { 197 | "message": "https://threadvis.github.io" 198 | }, 199 | "extension.email": { 200 | "message": "ahubmann@gmail.com" 201 | }, 202 | "extension.version": { 203 | "message": "[[version]]" 204 | } 205 | } -------------------------------------------------------------------------------- /src/chrome/content/utils/threader.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Implements cache for threaded messages. 29 | * Thread a list of messages by looking at the references header and additional information 30 | * 31 | * Based on the algorithm from Jamie Zawinski 32 | * www.jwz.org/doc/threading.html 33 | **********************************************************************************************************************/ 34 | 35 | import { Container } from "../container.mjs"; 36 | import { Message } from "../message.mjs"; 37 | import { Thread } from "../thread.mjs"; 38 | 39 | const { Gloda } = ChromeUtils.importESModule("resource:///modules/gloda/Gloda.sys.mjs"); 40 | 41 | /** 42 | * Return a threaded view for the given message header 43 | * 44 | * @param {nsIMsgDBHdr} messageHeader - The message header for which to create the thread 45 | * @return {ThreadVis.Container} - a thread of containers 46 | */ 47 | const get = async (messageHeader) => { 48 | // convert message header to gloda message 49 | const message = await getGlodaMessage(messageHeader); 50 | // get gloda thread 51 | const glodaThread = await getGlodaThread(message); 52 | // create the in-memory thread representation 53 | const containers = createContainers(glodaThread); 54 | const thread = new Thread(containers); 55 | thread.select(messageHeader.messageId); 56 | return thread; 57 | }; 58 | 59 | /** 60 | * Create an in-memory view of the thread 61 | * 62 | * @param messageCollection - the collection of all messages in the thread 63 | */ 64 | const createContainers = (messageCollection) => { 65 | return messageCollection 66 | // convert to "our" messages 67 | .map((item) => createMessage(item)) 68 | // and put all into thread 69 | .reduce(reduceMessages, []); 70 | }; 71 | 72 | /** 73 | * Get a Gloda message for message header 74 | * 75 | * @param {nsIMsgDBHdr} messageHeader - The message header for which to get the Gloda message 76 | * @return {GlodaMessage} - the Gloda message 77 | */ 78 | const getGlodaMessage = (messageHeader) => { 79 | return new Promise((resolve, reject) => { 80 | Gloda.getMessageCollectionForHeader(messageHeader, { 81 | onItemsAdded : (items, collection) => {}, 82 | onItemsModified : (items, collection) => {}, 83 | onItemsRemoved : (items, collection) => {}, 84 | onQueryCompleted : (collection) => { 85 | if (collection.items.length > 0) { 86 | resolve(collection.items[0]); 87 | } else { 88 | reject("Message not found in Gloda."); 89 | } 90 | } 91 | }, null); 92 | }); 93 | }; 94 | 95 | /** 96 | * Get all messages for a thread 97 | * 98 | * @param {GlodaMessage} message - The Gloda message 99 | * @return All Gloda messages in the thread 100 | */ 101 | const getGlodaThread = (message) => { 102 | return new Promise((resolve, reject) => { 103 | // get all messages in the thread from Gloda 104 | message.conversation.getMessagesCollection({ 105 | onItemsAdded : (items, collection) => {}, 106 | onItemsModified : (items, collection) => {}, 107 | onItemsRemoved : (items, collection) => {}, 108 | onQueryCompleted : (collection) => resolve(collection.items) 109 | }); 110 | }); 111 | }; 112 | 113 | /** 114 | * Create a ThreadVis message 115 | * 116 | * @param {GlodaMessage} glodaMessage - The gloda message to add 117 | * @return {ThreadVis.Message} - The wrapped message 118 | */ 119 | const createMessage = (glodaMessage) => { 120 | if (!glodaMessage.folderMessage) { 121 | console.error(`ThreadVis: Could not find "real" message for gloda message with msg-id "${glodaMessage.headerMessageID}" in folder "${glodaMessage.folderURI}"`); 122 | } 123 | 124 | return new Message(glodaMessage); 125 | }; 126 | 127 | /** 128 | * Put this message in a container 129 | * 130 | * @param {ThreadVis.Message} message - The message to put into a container 131 | */ 132 | const reduceMessages = (containers, message) => { 133 | // try to get message container 134 | let messageContainer = containers.find((container) => container.id === message.id); 135 | 136 | if (messageContainer) { 137 | // if we found a container for this message id, either it's a dummy or we have two mails with the same message-id 138 | // this should only happen if we sent a mail to a list and got back our sent-mail in the inbox 139 | // in that case we want that our sent-mail takes precedence over the other, 140 | // since we want to display it as sent, and we only want to display it once 141 | if (!messageContainer.message || (messageContainer.message && !messageContainer.message.isSent)) { 142 | // store message in this container 143 | messageContainer.message = message; 144 | } else if (messageContainer.message?.isSent) { 145 | // the message in messageContainer is a sent message, the new message is not the sent one 146 | // in this case we simply ignore the new message, since the sent message takes precedence 147 | return containers; 148 | } else { 149 | messageContainer = undefined; 150 | } 151 | } 152 | 153 | if (!messageContainer) { 154 | // no suitable container found, create new one 155 | messageContainer = new Container(message.id, message); 156 | // remember container 157 | containers.push(messageContainer); 158 | } 159 | 160 | // for each element in references field of message 161 | let parentReferenceContainer = undefined; 162 | 163 | message.references.forEach((reference) => { 164 | // somehow, Thunderbird does not correctly filter invalid ids 165 | if (reference.indexOf("@") === -1) { 166 | // invalid message id, ignore 167 | return; 168 | } 169 | 170 | // try to find container for referenced message 171 | let referenceContainer = containers.find((container) => container.id === reference); 172 | if (!referenceContainer) { 173 | // no container found, create new one 174 | referenceContainer = new Container(reference); 175 | // index container 176 | containers.push(referenceContainer); 177 | } 178 | 179 | // link reference container together 180 | 181 | // if we have a parent container and current container does not have a parent 182 | // and we are not looking at the same container see if we are already a child of parent 183 | if (parentReferenceContainer && !referenceContainer.parent 184 | && parentReferenceContainer !== referenceContainer 185 | && !referenceContainer.findParent(parentReferenceContainer)) { 186 | parentReferenceContainer.addChild(referenceContainer); 187 | } 188 | parentReferenceContainer = referenceContainer; 189 | }); 190 | 191 | // set parent of current message to last element in references 192 | 193 | // if we have a suitable parent container, and the parent container is the current container 194 | // or the parent container is a child of the current container, discard it as parent 195 | if (parentReferenceContainer 196 | && (parentReferenceContainer === messageContainer || parentReferenceContainer.findParent(messageContainer))) { 197 | parentReferenceContainer = undefined; 198 | } 199 | 200 | // if current message already has a parent 201 | if (messageContainer.parent && parentReferenceContainer) { 202 | // remove us from this parent 203 | messageContainer.parent.removeChild(messageContainer); 204 | } 205 | 206 | // if we have a suitable parent 207 | if (parentReferenceContainer) { 208 | // add us as child 209 | parentReferenceContainer.addChild(messageContainer); 210 | } 211 | 212 | return containers; 213 | }; 214 | 215 | 216 | export const Threader = { 217 | get 218 | }; -------------------------------------------------------------------------------- /src/chrome/content/hooks/init.js: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * JS file to load XUL to display ThreadVis extension and include all scripts. 29 | **********************************************************************************************************************/ 30 | 31 | const { Preferences } = ChromeUtils.importESModule("chrome://threadvis/content/utils/preferences.mjs"); 32 | const { ThreadVis } = ChromeUtils.importESModule("chrome://threadvis/content/threadvis.mjs"); 33 | 34 | const { ExtensionParent } = ChromeUtils.importESModule("resource://gre/modules/ExtensionParent.sys.mjs"); 35 | 36 | const notify = {}; 37 | const extension = ExtensionParent.GlobalManager.getExtension(ThreadVis.ADD_ON_ID); 38 | Services.scriptloader.loadSubScript( 39 | extension.rootURI.resolve("chrome/content/helpers/notifyTools.js"), 40 | notify, 41 | "UTF-8" 42 | ); 43 | // Set add-on id in notify tools 44 | notify.notifyTools.setAddOnId(ThreadVis.ADD_ON_ID); 45 | 46 | const openOptionsPage = () => { 47 | WL.messenger.runtime.openOptionsPage(); 48 | }; 49 | 50 | let ThreadVisInstance; 51 | 52 | /** 53 | * Activate add-on, called by WindowListener experiment 54 | * 55 | * @param {boolean} isAddonActivation 56 | */ 57 | var onLoad = async (isAddonActivation) => { 58 | WL.injectCSS("chrome://threadvis/content/threadvis.css"); 59 | 60 | // wait for preferences to be available 61 | await notify.notifyTools.notifyBackground({command: "initPref"}); 62 | 63 | // inject status bar icon into main window (if configured) 64 | if (window.location.href === "chrome://messenger/content/messenger.xhtml" && Preferences.get(Preferences.STATUSBAR)) { 65 | injectStatusbar(); 66 | document.getElementById("ThreadVisOpenOptionsDialog") 67 | .addEventListener("command", () => WL.messenger.runtime.openOptionsPage()); 68 | return; 69 | } 70 | 71 | // inject visualisation into message 72 | injectVisualisation(); 73 | 74 | ThreadVisInstance = new ThreadVis(window, openOptionsPage); 75 | 76 | window.ThreadVis = ThreadVisInstance; 77 | // attach event listeners 78 | document.getElementById("ThreadVisPopUpOpenOptionsDialog") 79 | .addEventListener("command", () => WL.messenger.runtime.openOptionsPage()); 80 | document.getElementById("ThreadVisPopUpOpenVisualisation") 81 | .addEventListener("command", () => ThreadVisInstance.displayVisualisationWindow()); 82 | document.getElementById("ThreadVisOpenLegendWindow") 83 | .addEventListener("command", () => ThreadVisInstance.displayLegendWindow()); 84 | document.getElementById("ThreadVisExportSVG") 85 | .addEventListener("command", () => ThreadVisInstance.visualisation.exportToSVG()); 86 | }; 87 | 88 | /** 89 | * Deactivate add-on, called by WindowListener experiment 90 | * 91 | * @param {boolean} isAddonDeactivation 92 | */ 93 | var onUnload = (isAddonDeactivation) => { 94 | if (ThreadVisInstance) { 95 | ThreadVisInstance.shutdown(); 96 | ThreadVisInstance = null; 97 | delete window.ThreadVis; 98 | } 99 | }; 100 | 101 | /** 102 | * Inject the visualization's XUL code into the user interface 103 | */ 104 | const injectVisualisation = () => { 105 | WL.injectElements(` 106 | 107 | 108 | 109 | 111 | 113 | 115 | 117 | 118 | 119 | 120 | 121 | `, 122 | ["chrome://threadvis/locale/threadvis.dtd"]); 123 | 124 | WL.injectElements(` 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | `, 144 | ["chrome://threadvis/locale/threadvis.dtd"]); 145 | }; 146 | 147 | /** 148 | * Inject the statusbar XUL code into the user interface 149 | */ 150 | const injectStatusbar = () => { 151 | WL.injectElements(` 152 | 153 | 154 | 155 | 170 | 173 | 178 | 179 | 181 | 182 | 184 | 188 | 192 | 193 | 194 | 195 | 197 | 201 | 205 | 206 | 207 | 208 | 210 | 211 | 212 | 213 | 214 | `, 215 | ["chrome://threadvis/locale/threadvis.dtd"]); 216 | }; -------------------------------------------------------------------------------- /src/chrome/content/utils/preferences.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * JavaScript file to react to preference changing events 29 | **********************************************************************************************************************/ 30 | 31 | // shared key strings for our preferences 32 | import { PreferenceKeys, PreferenceBranch } from "./preferenceskeys.mjs"; 33 | 34 | const PREF_BOOL = Services.prefs.PREF_BOOL; 35 | const PREF_INT = Services.prefs.PREF_INT; 36 | const PREF_STRING = Services.prefs.PREF_STRING; 37 | 38 | class PreferencesClass { 39 | 40 | /** 41 | * Internal preferences object 42 | */ 43 | #preferences = {}; 44 | 45 | /** 46 | * Branch of threadvis preferences 47 | */ 48 | #threadVisPrefBranch = Services.prefs.getBranch(PreferenceBranch); 49 | 50 | /** 51 | * Branch for gloda preference 52 | */ 53 | #glodaPrefBranch = Services.prefs.getBranch("mailnews.database.global.indexer.enabled"); 54 | 55 | /** 56 | * Constructor 57 | */ 58 | constructor() { 59 | } 60 | 61 | /** 62 | * Do callbacks after preference change 63 | * 64 | * @param {String} pref - The pref that changed 65 | */ 66 | #doCallback(pref) { 67 | const value = this.#preferences[pref].value; 68 | const callbacks = this.#preferences[pref].callbacks; 69 | for (let key in callbacks) { 70 | callbacks[key](value); 71 | } 72 | } 73 | 74 | /** 75 | * Get preference value for given preference 76 | * 77 | * @param {String} pref - The preference to get 78 | * @return {String} - The value of the preference 79 | */ 80 | get(pref) { 81 | return this.#preferences[pref].value; 82 | } 83 | 84 | /** 85 | * Load a preference from the store 86 | * 87 | * @param {String} pref - The preference to load 88 | * @param {PrefType} type - The type of the preference (bool, string, int) 89 | * @param {String} def - The default value 90 | * @param {nsIPrefBranch} prefBranch - The branch to use to read the value 91 | */ 92 | #load(pref, type, def, prefBranch) { 93 | if (! prefBranch) { 94 | prefBranch = this.#threadVisPrefBranch; 95 | } 96 | if (!this.#preferences[pref]) { 97 | this.#preferences[pref] = { 98 | value: def, 99 | callbacks: [], 100 | type: type, 101 | branch: prefBranch 102 | }; 103 | } 104 | 105 | // remove leading branch from pref name 106 | const loadPref = pref.substring(this.#preferences[pref].branch.root.length); 107 | 108 | // check if we are loading right pref type 109 | if (this.#preferences[pref].branch.getPrefType(loadPref) !== type) { 110 | return; 111 | } 112 | 113 | switch (type) { 114 | case PREF_BOOL: 115 | this.#preferences[pref].value = this.#preferences[pref].branch.getBoolPref(loadPref); 116 | break; 117 | case PREF_STRING: 118 | this.#preferences[pref].value = this.#preferences[pref].branch.getCharPref(loadPref); 119 | break; 120 | case PREF_INT: 121 | this.#preferences[pref].value = this.#preferences[pref].branch.getIntPref(loadPref); 122 | break; 123 | } 124 | } 125 | 126 | /** 127 | * Reload preferences 128 | */ 129 | reload() { 130 | this.#load(PreferenceKeys.DISABLED_ACCOUNTS, PREF_STRING, ""); 131 | this.#load(PreferenceKeys.DISABLED_FOLDERS, PREF_STRING, ""); 132 | this.#load(PreferenceKeys.SENTMAIL_FOLDERFLAG, PREF_BOOL, true); 133 | this.#load(PreferenceKeys.SENTMAIL_IDENTITY, PREF_BOOL, true); 134 | this.#load(PreferenceKeys.SVG_HEIGHT, PREF_INT, 1000); 135 | this.#load(PreferenceKeys.SVG_WIDTH, PREF_INT, 1000); 136 | this.#load(PreferenceKeys.STATUSBAR, PREF_BOOL, true); 137 | this.#load(PreferenceKeys.TIMELINE, PREF_BOOL, true); 138 | this.#load(PreferenceKeys.TIMELINE_FONTSIZE, PREF_INT, 9); 139 | this.#load(PreferenceKeys.TIMESCALING, PREF_BOOL, true,); 140 | this.#load(PreferenceKeys.TIMESCALING_METHOD, PREF_STRING, "linear"); 141 | this.#load(PreferenceKeys.TIMESCALING_MINTIMEDIFF, PREF_INT, 0); 142 | this.#load(PreferenceKeys.VIS_DOTSIZE, PREF_INT, 12); 143 | this.#load(PreferenceKeys.VIS_ARC_MINHEIGHT, PREF_INT, 12); 144 | this.#load(PreferenceKeys.VIS_ARC_RADIUS, PREF_INT, 32); 145 | this.#load(PreferenceKeys.VIS_ARC_DIFFERENCE, PREF_INT, 6); 146 | this.#load(PreferenceKeys.VIS_ARC_WIDTH, PREF_INT, 2); 147 | this.#load(PreferenceKeys.VIS_SPACING, PREF_INT, 24); 148 | this.#load(PreferenceKeys.VIS_MESSAGE_CIRCLES, PREF_BOOL, true); 149 | this.#load(PreferenceKeys.VIS_COLOUR, PREF_STRING, "author"); 150 | this.#load(PreferenceKeys.VIS_COLOURS_BACKGROUND, PREF_STRING, ""); 151 | this.#load(PreferenceKeys.VIS_COLOURS_BORDER, PREF_STRING, ""); 152 | this.#load(PreferenceKeys.VIS_COLOURS_RECEIVED, PREF_STRING, 153 | "#7FFF00,#00FFFF,#7F00FF,#997200,#009926,#002699,#990072,#990000,#4C9900,#009999,#4C0099,#FFBF00,#00FF3F,#003FFF,#FF00BF"); 154 | this.#load(PreferenceKeys.VIS_COLOURS_SENT, PREF_STRING, "#ff0000"); 155 | this.#load(PreferenceKeys.VIS_COLOURS_CURRENT, PREF_STRING, "#000000"); 156 | this.#load(PreferenceKeys.VIS_HIDE_ON_SINGLE, PREF_BOOL, false); 157 | this.#load(PreferenceKeys.VIS_HIGHLIGHT, PREF_BOOL, true); 158 | this.#load(PreferenceKeys.VIS_MINIMAL_WIDTH, PREF_INT, 0); 159 | this.#load(PreferenceKeys.VIS_OPACITY, PREF_INT, 30); 160 | this.#load(PreferenceKeys.VIS_ZOOM, PREF_STRING, "full"); 161 | 162 | this.#load(PreferenceKeys.GLODA_ENABLED, PREF_BOOL, true, this.#glodaPrefBranch); 163 | } 164 | 165 | /** 166 | * Register as preference changing observer 167 | */ 168 | register() { 169 | // add observer for our own branch 170 | this.#threadVisPrefBranch.addObserver("", this, false); 171 | 172 | // add observer for gloda 173 | this.#glodaPrefBranch.addObserver("", this, false); 174 | } 175 | 176 | /** 177 | * Observe a pref change 178 | * @param {String} subject 179 | * @param {String} topic 180 | * @param {*} data 181 | */ 182 | observe(subject, topic, data) { 183 | if (topic !== "nsPref:changed") { 184 | return; 185 | } 186 | // reload preferences 187 | this.reload(); 188 | if (subject.root === "mailnews.database.global.indexer.enabled") { 189 | this.#doCallback("mailnews.database.global.indexer.enabled"); 190 | } else { 191 | this.#doCallback(PreferenceBranch + data); 192 | } 193 | } 194 | 195 | /** 196 | * Register a callback hook 197 | * 198 | * @param {String} preference - The preference 199 | * @param {Function} func - The function that has to be called if the preference value changes 200 | */ 201 | callback(preference, func) { 202 | this.#preferences[preference].callbacks.push(func); 203 | } 204 | 205 | /** 206 | * Set preference value for given preference 207 | * 208 | * @param {String} pref - The name of the preference 209 | * @param {String} val - The value of the preference 210 | */ 211 | set(pref, val) { 212 | this.#preferences[pref].value = val; 213 | this.#storePreference(pref, val); 214 | } 215 | 216 | /** 217 | * Store a preference to the store 218 | * 219 | * @param {String} pref - The name of the preference 220 | * @param {String} val - The value of the preference 221 | */ 222 | #storePreference(pref, val) { 223 | const branch = this.#preferences[pref].branch; 224 | const type = this.#preferences[pref].type; 225 | // remove leading branch from pref name 226 | pref = pref.substring(branch.root.length); 227 | 228 | switch (type) { 229 | case PREF_BOOL: 230 | branch.setBoolPref(pref, val); 231 | break; 232 | case PREF_STRING: 233 | branch.setCharPref(pref, val); 234 | break; 235 | case PREF_INT: 236 | branch.setIntPref(pref, val); 237 | break; 238 | } 239 | } 240 | 241 | /** 242 | * Unregister observer 243 | */ 244 | unregister() { 245 | this.#threadVisPrefBranch.removeObserver("", this); 246 | this.#glodaPrefBranch.removeObserver("", this); 247 | } 248 | } 249 | 250 | export const Preferences = Object.assign(new PreferencesClass(), PreferenceKeys); 251 | Object.seal(Preferences); 252 | -------------------------------------------------------------------------------- /src/settings/options.html: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | __MSG_options.about.version__ __MSG_extension.version__ 47 |
48 |
49 | __MSG_extension.copyright1__ 50 |
51 |
52 | __MSG_extension.copyright2__ 53 |
54 | __MSG_extension.email__ 55 | __MSG_extension.homepage__ 56 |
57 | 58 |
59 | 60 | 61 | 62 |
63 |

__MSG_options.visualisation.timescaling.caption__

64 | 65 |
66 | 67 | 68 |
69 | 70 |
71 | __MSG_options.visualisation.timescaling.description.disabled__ 72 |
73 |
74 |
75 | 76 |
77 | 78 | 79 |
80 | 81 |
82 | __MSG_options.visualisation.timescaling.description.enabled__ 83 |
84 | 85 | 89 |
90 |
91 | 92 | 103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 |
111 |
112 | 113 | 114 |
115 |

__MSG_options.visualisation.size.caption__

116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 |
__MSG_options.visualisation.size.dotsize__
__MSG_options.visualisation.size.arcminheight__
__MSG_options.visualisation.size.arcradius__
__MSG_options.visualisation.size.arcdifference__
__MSG_options.visualisation.size.arcwidth__
__MSG_options.visualisation.size.spacing__
__MSG_options.visualisation.timeline.fontsize__
148 |
149 | 150 |
151 | 152 | 156 |
157 | 158 |
159 | 160 | 161 |
162 |
163 | 164 | 165 |
166 |

__MSG_options.visualisation.colour.caption__

167 | 168 |
169 | 170 | 171 |
__MSG_options.visualisation.colour.single.description__
172 |
173 | 174 |
175 | 176 | 177 |
__MSG_options.visualisation.colour.author.description__
178 |
179 | 180 | 181 |
182 |
183 | 184 |
185 | 186 | 198 |
199 | 200 |
201 | 202 | 203 |
204 | 205 |
206 | 207 | 208 |
209 | 210 |
211 | 212 | 213 |
214 | 215 | 216 |
217 |

__MSG_options.sentmail.caption__

218 |
219 | 220 | 221 |
222 |
223 | 224 | 225 |
226 |
227 | 228 | 229 |
230 |

__MSG_options.statusbar.caption__

231 |
232 | 233 | 234 |
__MSG_options.statusbar.description__
235 |
236 |
237 | 238 | 239 |
240 |

__MSG_options.visualisation.enabledaccounts.caption__

241 | 242 | 245 | 248 | 249 |
250 |
251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /src/chrome/content/positionedthread.mjs: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * Wrap {Thread} to add chronological order and positions 29 | **********************************************************************************************************************/ 30 | 31 | import { PositionedContainer } from "./positionedcontainer.mjs"; 32 | import { Preferences } from "./utils/preferences.mjs"; 33 | 34 | export class PositionedThread { 35 | 36 | /** 37 | * List of positioned containers 38 | */ 39 | #containers = []; 40 | 41 | /** 42 | * Available width to the visualisation 43 | */ 44 | #width = 0; 45 | 46 | /** 47 | * Stacked arcs height (top) 48 | */ 49 | #topHeight = 0; 50 | 51 | /** 52 | * Stacked arcs height (bottom) 53 | */ 54 | #bottomHeight = 0; 55 | 56 | /** 57 | * Calculated minimal time difference between two messages 58 | */ 59 | #minimalTimeDifference = Number.MAX_VALUE; 60 | 61 | constructor(containers, selectedContainer, authors, width) { 62 | Object.seal(this); 63 | this.#containers = containers 64 | .toSorted(PositionedThread.#sortFunction) 65 | .map((container) => { 66 | const author = authors[container.message?.fromEmail]; 67 | const isSelected = container === selectedContainer; 68 | const inThread = container.findParent(selectedContainer) || selectedContainer.findParent(container); 69 | return new PositionedContainer(container, author, isSelected, inThread); 70 | }); 71 | 72 | // now that we have all positioned containers, link parents 73 | this.#containers = this.#containers.map((item, _index, array) => { 74 | if (item.container.parent) { 75 | item.parent = array.find((i) => i.id === item.container.parent.id); 76 | } 77 | return item; 78 | }); 79 | 80 | this.#width = width; 81 | 82 | this.#calculateSize(); 83 | this.#timeScaling(); 84 | this.#position(); 85 | } 86 | 87 | get containers() { 88 | return this.#containers; 89 | } 90 | 91 | get selected() { 92 | return this.#containers.find((container) => container.selected); 93 | } 94 | 95 | get topHeight() { 96 | return this.#topHeight; 97 | } 98 | 99 | get bottomHeight() { 100 | return this.#bottomHeight; 101 | } 102 | 103 | get maxX() { 104 | return this.#containers.reduce((maxX, container) => Math.max(maxX, container.x), 0); 105 | } 106 | 107 | /** 108 | * Calculate size 109 | */ 110 | #calculateSize() { 111 | this.#calculateArcHeights(); 112 | 113 | this.#topHeight = 0; 114 | this.#bottomHeight = 0; 115 | this.#minimalTimeDifference = Number.MAX_VALUE; 116 | this.#containers.forEach((container, index, array) => { 117 | const parent = container.parent; 118 | if (parent) { 119 | // also keep track of the current maximal stacked arc height, 120 | // so that we can resize the whole extension 121 | if (parent.depth % 2 === 0 && container.arcHeight > this.#topHeight) { 122 | this.#topHeight = container.arcHeight; 123 | } 124 | 125 | if (parent.depth %2 !== 0 && container.arcHeight > this.#bottomHeight) { 126 | this.#bottomHeight = container.arcHeight; 127 | } 128 | } 129 | 130 | // also keep track of the time difference between two adjacent messages 131 | if (index < array.length - 1) { 132 | const timeDifference = array[index + 1].date.getTime() - container.date.getTime(); 133 | // timeDifference stores the time difference to the _next_ message 134 | container.timeDifference = timeDifference; 135 | 136 | // since we could have dummy containers that have the same time as 137 | // the next message, or malformed threads where the answer is _before_ the parent, 138 | // skip any time difference <= 0 139 | if (timeDifference < this.#minimalTimeDifference && timeDifference > 0) { 140 | this.#minimalTimeDifference = timeDifference; 141 | } 142 | } 143 | }); 144 | } 145 | 146 | /** 147 | * Calculate heights for all arcs. 148 | */ 149 | #calculateArcHeights() { 150 | // init heights 151 | const currentArcHeightIncoming = {}; 152 | const currentArcHeightOutgoing = {}; 153 | 154 | this.#containers.forEach((container, index) => { 155 | currentArcHeightIncoming[container.id] = []; 156 | currentArcHeightOutgoing[container.id] = []; 157 | 158 | const parent = container.parent; 159 | if (parent) { 160 | let parentIndex = this.#containers.findIndex((i) => i.container.id === parent.id); 161 | 162 | // find a free arc height between the parent message and this one 163 | // since we want to draw an arc between this message and its parent, 164 | // and we do not want any arcs to overlap 165 | let freeHeight = 1; 166 | while ( 167 | this.#containers.slice(parentIndex, index).some((containerBetween) => { 168 | if ( 169 | containerBetween.depth % 2 === parent.depth % 2 170 | && currentArcHeightOutgoing[containerBetween.id][freeHeight] === 1 171 | ) { 172 | return true; 173 | } 174 | if ( 175 | containerBetween.depth % 2 !== parent.depth % 2 176 | && currentArcHeightIncoming[containerBetween.id][freeHeight] === 1 177 | ) { 178 | return true; 179 | } 180 | return false; 181 | }) 182 | ) { 183 | freeHeight++; 184 | } 185 | currentArcHeightOutgoing[parent.id][freeHeight] = 1; 186 | currentArcHeightIncoming[container.id][freeHeight] = 1; 187 | 188 | container.arcHeight = freeHeight; 189 | } 190 | }); 191 | } 192 | 193 | /** 194 | * If time scaling is enabled, we want to layout the messages so that their 195 | * horizontal spacing is proportional to the time difference between those 196 | * two messages 197 | */ 198 | #timeScaling() { 199 | if (! Preferences.get(Preferences.TIMESCALING)) { 200 | return; 201 | } 202 | const prefSpacing = Preferences.get(Preferences.VIS_SPACING); 203 | const prefTimescalingMethod = Preferences.get(Preferences.TIMESCALING_METHOD); 204 | const prefTimescalingMinimalTimeDifference = Preferences.get(Preferences.TIMESCALING_MINTIMEDIFF); 205 | 206 | // we want to scale the messages horizontally according to their time difference 207 | // therefore we calculate the overall scale factor 208 | const minimalTimeDifference = Math.max(this.#minimalTimeDifference, prefTimescalingMinimalTimeDifference); 209 | 210 | let totalTimeScale = 0; 211 | this.#containers.forEach((container) => { 212 | let xScaling = 1; 213 | if (container.timeDifference > 0) { 214 | xScaling = container.timeDifference / minimalTimeDifference; 215 | // instead of linear scaling, we might use other scaling factor 216 | if (prefTimescalingMethod === "log") { 217 | xScaling = Math.log(xScaling) / Math.log(2) + 1; 218 | } 219 | // check if we might encounter a dummy container, see above 220 | xScaling = Math.max(xScaling, 1); 221 | } 222 | totalTimeScale += xScaling; 223 | container.timeScaling = xScaling; 224 | }); 225 | 226 | // max_count_x tells us how many messages we could display if all are laid out with the minimal horizontal spacing 227 | // e.g. 228 | // |---|---|---| 229 | // width / spacing would lead to 3 230 | const maxCountX = this.#width / prefSpacing; 231 | 232 | // if the time scaling factor is bigger than what we can display, we have a problem 233 | // this means, we have to scale the timing factor down 234 | let scaling = 0.9; 235 | let iteration = 0; 236 | while (totalTimeScale > maxCountX && iteration < 10000) { 237 | iteration++; 238 | totalTimeScale = 0; 239 | this.#containers.forEach((container) => { 240 | let xScaling = container.timeScaling; 241 | if (container.timeDifferences === 0) { 242 | xScaling = 1; 243 | } else { 244 | if (prefTimescalingMethod === "linear") { 245 | xScaling = xScaling * scaling; 246 | } else if (prefTimescalingMethod === "log") { 247 | xScaling = Math.log(container.timeDifference / minimalTimeDifference) / Math.log(2 / Math.pow(scaling, iteration)) + 1; 248 | } 249 | xScaling = Math.max(xScaling, 1); 250 | } 251 | totalTimeScale += xScaling; 252 | container.timeScaling = xScaling; 253 | }); 254 | // if the totalTimeScale === containers.length, we reduced every horizontal spacing to its minimum and 255 | // we can't do anything more 256 | // this means we have to lay out more messages than we can 257 | // this is dealt with later in resizing 258 | if (totalTimeScale === this.#containers.length - 1) { 259 | break; 260 | } 261 | } 262 | } 263 | 264 | #position() { 265 | const prefSpacing = Preferences.get(Preferences.VIS_SPACING); 266 | let x = (prefSpacing / 2); 267 | 268 | this.#containers.forEach((item) => { 269 | item.x = x; 270 | // calculate position for next container 271 | x = x + (item.timeScaling * prefSpacing); 272 | }); 273 | return x; 274 | } 275 | 276 | /** 277 | * Sort function for sorting javascript array 278 | * Sort by date, but never sort child before parent 279 | * 280 | * @param {ThreadVis.Container} one - The first container 281 | * @param {ThreadVis.Container} two - The second container 282 | * @return {Number} - -1 to sort one before two, +1 to sort two before one 283 | */ 284 | static #sortFunction(one, two) { 285 | // just to be sure, we never want to sort a child before one of its parents 286 | // (could happen if time information in mail is wrong, e.g. time of mailclient is off) 287 | if (two.findParent(one)) { 288 | return -1; 289 | } 290 | if (one.findParent(two)) { 291 | return 1; 292 | } 293 | 294 | // sort all others by date 295 | // if one of the containers is a dummy, date returns the date of its first child. 296 | // this should be enough to ensure the timeline 297 | const difference = one.date.getTime() - two.date.getTime(); 298 | 299 | if (difference < 0) { 300 | return -1; 301 | } else { 302 | return 1; 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/settings/options.js: -------------------------------------------------------------------------------- 1 | /* ********************************************************************************************************************* 2 | * This file is part of ThreadVis. 3 | * https://threadvis.github.io 4 | * 5 | * ThreadVis started as part of Alexander C. Hubmann-Haidvogel's Master's Thesis titled 6 | * "ThreadVis for Thunderbird: A Thread Visualisation Extension for the Mozilla Thunderbird Email Client" 7 | * at Graz University of Technology, Austria. An electronic version of the thesis is available online at 8 | * https://ftp.isds.tugraz.at/pub/theses/ahubmann.pdf 9 | * 10 | * Copyright (C) 2005, 2006, 2007 Alexander C. Hubmann 11 | * Copyright (C) 2007, 2008, 2009, 12 | * 2010, 2011, 2013, 2018, 2019, 13 | * 2020, 2021, 2022, 2023, 2024, 2025 Alexander C. Hubmann-Haidvogel 14 | * 15 | * ThreadVis is free software: you can redistribute it and/or modify it under the terms of the 16 | * GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, 17 | * or (at your option) any later version. 18 | * 19 | * ThreadVis is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 20 | * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 21 | * See the GNU Affero General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Affero General Public License along with ThreadVis. 24 | * If not, see . 25 | * 26 | * Version: $Id$ 27 | * ********************************************************************************************************************* 28 | * JavaScript file for settings dialog 29 | **********************************************************************************************************************/ 30 | 31 | import { PreferenceKeys } from "../chrome/content/utils/preferenceskeys.mjs"; 32 | 33 | const getPref = async (pref) => 34 | (await messenger.runtime.getBackgroundPage()).messenger.LegacyPref.get(pref); 35 | 36 | const setPref = async (pref, value) => 37 | (await messenger.runtime.getBackgroundPage()).messenger.LegacyPref.set(pref, value); 38 | 39 | const getAccounts = async () => 40 | (await messenger.runtime.getBackgroundPage()).messenger.LegacyAccountsFolders.getAccounts(); 41 | 42 | const querySelector = (name, value) => `[name="${name}"]` + (value ? `[value="${value}"]` : ""); 43 | 44 | const prefDisabled = (pref, value) => pref !== "" && pref.indexOf(" " + value + " ") > -1; 45 | 46 | const init = async () => { 47 | await Promise.all([ 48 | { key: PreferenceKeys.TIMESCALING, type: "bool"}, 49 | { key: PreferenceKeys.TIMESCALING_METHOD, type: "string"}, 50 | { key: PreferenceKeys.VIS_MESSAGE_CIRCLES, type: "bool"}, 51 | { key: PreferenceKeys.TIMESCALING_MINTIMEDIFF, type: "string"}, 52 | { key: PreferenceKeys.TIMELINE, type: "bool"}, 53 | { key: PreferenceKeys.VIS_DOTSIZE, type: "integer"}, 54 | { key: PreferenceKeys.VIS_ARC_MINHEIGHT, type: "integer"}, 55 | { key: PreferenceKeys.VIS_ARC_RADIUS, type: "integer"}, 56 | { key: PreferenceKeys.VIS_ARC_DIFFERENCE, type: "integer"}, 57 | { key: PreferenceKeys.VIS_ARC_WIDTH, type: "integer"}, 58 | { key: PreferenceKeys.VIS_SPACING, type: "integer"}, 59 | { key: PreferenceKeys.TIMELINE_FONTSIZE, type: "integer"}, 60 | { key: PreferenceKeys.VIS_ZOOM, type: "string"}, 61 | { key: PreferenceKeys.VIS_HIGHLIGHT, type: "bool"}, 62 | { key: PreferenceKeys.VIS_OPACITY, type: "string"}, 63 | { key: PreferenceKeys.VIS_COLOUR, type: "string"}, 64 | { key: PreferenceKeys.VIS_COLOURS_SENT, type: "string"}, 65 | { key: PreferenceKeys.VIS_COLOURS_RECEIVED, type: "string"}, 66 | { key: PreferenceKeys.VIS_COLOURS_CURRENT, type: "string"}, 67 | { key: PreferenceKeys.SENTMAIL_FOLDERFLAG, type: "bool"}, 68 | { key: PreferenceKeys.SENTMAIL_IDENTITY, type: "bool"}, 69 | { key: PreferenceKeys.STATUSBAR, type: "bool"}, 70 | { key: PreferenceKeys.DISABLED_ACCOUNTS, type: "string"}, 71 | { key: PreferenceKeys.DISABLED_FOLDERS, type: "string"} 72 | ].map((pref) => 73 | getPref(pref.key).then((value) => { 74 | const elems = document.querySelectorAll(querySelector(pref.key)); 75 | if (elems.length === 1) { 76 | const elem = elems[0]; 77 | if (pref.type === "bool") { 78 | elem.checked = value; 79 | } else { 80 | elem.value = value; 81 | } 82 | elem.addEventListener("change", function() { 83 | if (pref.type === "bool") { 84 | setPref(pref.key, this.checked); 85 | } else { 86 | setPref(pref.key, this.value); 87 | } 88 | }); 89 | } else if (elems.length > 1) { 90 | // note: special handling for prefs which map to multiple elements, assume each elem is a boolean 91 | elems.forEach((elem) => { 92 | const equals = elem.value === (pref.type === "bool" ? (value === true ? "true" : "false") : value); 93 | if (equals) { 94 | elem.checked = true; 95 | } 96 | elem.addEventListener("change", function() { 97 | if (this.checked) { 98 | setPref(pref.key, pref.type === "bool" ? (this.value === "true" ? true : false) : this.value); 99 | } 100 | }); 101 | }); 102 | } 103 | }) 104 | )); 105 | 106 | // build account list 107 | await buildAccountList(); 108 | }; 109 | 110 | /** 111 | * Build the account list. 112 | * Get all accounts, display checkbox for each 113 | */ 114 | const buildAccountList = async () => { 115 | const accountBox = document.getElementById("ThreadVisEnableAccounts"); 116 | const pref = document.getElementById("ThreadVisHiddenDisabledAccounts").value; 117 | 118 | const accounts = await getAccounts(); 119 | accounts.forEach((account) => { 120 | const checkbox = document.createElement("input"); 121 | checkbox.setAttribute("id", "ThreadVis-Account-" + account.id); 122 | checkbox.setAttribute("type", "checkbox"); 123 | checkbox.setAttribute("data-type", "account"); 124 | checkbox.setAttribute("data-account", account.id); 125 | checkbox.addEventListener("change", function() { 126 | buildAccountPreference(); 127 | document.querySelectorAll("[data-type=folder][data-account=" + account.id + "]").forEach((box) => { 128 | box.disabled = ! this.checked; 129 | }); 130 | }); 131 | const accountDisabled = prefDisabled(pref, account.id); 132 | if (accountDisabled) { 133 | checkbox.checked = false; 134 | } else { 135 | checkbox.checked = true; 136 | } 137 | const label = document.createElement("label"); 138 | label.setAttribute("for", "ThreadVis-Account-" + account.id); 139 | label.textContent = account.name; 140 | 141 | const buttonAll = document.createElement("button"); 142 | buttonAll.textContent = "__MSG_options.visualisation.enabledaccounts.button.all__"; 143 | buttonAll.addEventListener("click", function() { 144 | document.querySelectorAll("[data-type=folder][data-account=" + account.id + "]").forEach((box) => { 145 | box.checked = true; 146 | box.dispatchEvent(new Event("change")); 147 | }); 148 | }); 149 | const buttonNone = document.createElement("button"); 150 | buttonNone.textContent = "__MSG_options.visualisation.enabledaccounts.button.none__"; 151 | buttonNone.addEventListener("click", function() { 152 | document.querySelectorAll("[data-type=folder][data-account=" + account.id + "]").forEach((box) => { 153 | box.checked = false; 154 | box.dispatchEvent(new Event("change")); 155 | }); 156 | }); 157 | 158 | const hbox = document.createElement("div"); 159 | hbox.setAttribute("class", "account"); 160 | hbox.appendChild(checkbox); 161 | hbox.appendChild(label); 162 | hbox.appendChild(buttonAll); 163 | hbox.appendChild(buttonNone); 164 | accountBox.appendChild(hbox); 165 | 166 | buildFolderCheckboxes(accountBox, account.folders, account.id, accountDisabled, 1); 167 | }); 168 | }; 169 | 170 | /** 171 | * Create checkbox elements for all folders 172 | * 173 | * @param {DOMElement} box - The box to which to add the checkbox elements to 174 | * @param {Array} folders - All folders for which to create checkboxes 175 | * @param {Account} account - The account for which the checkboxes are created 176 | * @param {Boolean} disabled - The account is disabled, so disable all checkboxes 177 | * @param {Number} indent - The amount of indentation 178 | */ 179 | const buildFolderCheckboxes = (box, folders, account, disabled, indent) => { 180 | const pref = document.getElementById("ThreadVisHiddenDisabledFolders").value; 181 | 182 | folders.forEach((folder) => { 183 | const div = document.createElement("div"); 184 | const checkbox = document.createElement("input"); 185 | checkbox.setAttribute("id", "ThreadVis-Account-" + account + "-Folder-" + folder.url); 186 | checkbox.setAttribute("type", "checkbox"); 187 | checkbox.setAttribute("data-type", "folder"); 188 | checkbox.setAttribute("data-account", account); 189 | checkbox.setAttribute("data-folder", folder.url); 190 | checkbox.addEventListener("change", function() { 191 | buildFolderPreference(); 192 | }); 193 | div.style.paddingLeft = indent + "em"; 194 | if (prefDisabled(pref, folder.url)) { 195 | checkbox.checked = false; 196 | } else { 197 | checkbox.checked = true; 198 | } 199 | if (disabled) { 200 | checkbox.disabled = true; 201 | } 202 | const label = document.createElement("label"); 203 | label.setAttribute("for", "ThreadVis-Account-" + account + "-Folder-" + folder.url); 204 | label.textContent = folder.name; 205 | div.appendChild(checkbox); 206 | div.appendChild(label); 207 | box.appendChild(div); 208 | 209 | // descend into subfolders 210 | if (folder.folders) { 211 | buildFolderCheckboxes(box, folder.folders, account, disabled, indent + 1); 212 | } 213 | }); 214 | }; 215 | 216 | /** 217 | * Create a string preference of all deselected accounts 218 | */ 219 | const buildAccountPreference = () => { 220 | const accountBox = document.getElementById("ThreadVisEnableAccounts"); 221 | const prefElement = document.getElementById("ThreadVisHiddenDisabledAccounts"); 222 | 223 | let pref = ""; 224 | 225 | const checkboxes = accountBox.querySelectorAll("[data-type=account]"); 226 | checkboxes.forEach((checkbox) => { 227 | if (! checkbox.checked) { 228 | pref += " " + checkbox.getAttribute("data-account") + " "; 229 | } 230 | }); 231 | prefElement.value = pref; 232 | prefElement.dispatchEvent(new Event("change")); 233 | }; 234 | 235 | /** 236 | * Create a string preference of all deselected folders 237 | */ 238 | const buildFolderPreference = () => { 239 | const accountBox = document.getElementById("ThreadVisEnableAccounts"); 240 | const prefElement = document.getElementById("ThreadVisHiddenDisabledFolders"); 241 | 242 | let pref = ""; 243 | 244 | const checkboxes = accountBox.querySelectorAll("[data-type=folder]"); 245 | checkboxes.forEach((checkbox) => { 246 | if (! checkbox.checked) { 247 | pref += " " + checkbox.getAttribute("data-folder") + " "; 248 | } 249 | }); 250 | prefElement.value = pref; 251 | prefElement.dispatchEvent(new Event("change")); 252 | }; 253 | 254 | const localize = () => { 255 | const keyPrefix = "__MSG_"; 256 | 257 | const localization = { 258 | updateString(string) { 259 | const re = new RegExp(keyPrefix + "(.+?)__", "g"); 260 | return string.replace(re, (matched) => { 261 | const key = matched.slice(keyPrefix.length, -2); 262 | return messenger.i18n.getMessage(key) || matched; 263 | }); 264 | }, 265 | 266 | updateSubtree(node) { 267 | const texts = document.evaluate( 268 | "descendant::text()[contains(self::text(), \"" + keyPrefix + "\")]", 269 | node, 270 | null, 271 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 272 | null 273 | ); 274 | for (let i = 0, maxi = texts.snapshotLength; i < maxi; i++) { 275 | const text = texts.snapshotItem(i); 276 | if (text.nodeValue.includes(keyPrefix)) { 277 | text.nodeValue = this.updateString(text.nodeValue); 278 | } 279 | } 280 | 281 | const attributes = document.evaluate( 282 | "descendant::*/attribute::*[contains(., \"" + keyPrefix + "\")]", 283 | node, 284 | null, 285 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 286 | null 287 | ); 288 | for (let i = 0, maxi = attributes.snapshotLength; i < maxi; i++) { 289 | const attribute = attributes.snapshotItem(i); 290 | if (attribute.value.includes(keyPrefix)) { 291 | attribute.value = this.updateString(attribute.value); 292 | } 293 | } 294 | }, 295 | 296 | async updateDocument() { 297 | this.updateSubtree(document); 298 | } 299 | }; 300 | 301 | localization.updateDocument(); 302 | }; 303 | 304 | (async () => { 305 | await init(); 306 | localize(); 307 | })(); --------------------------------------------------------------------------------