diff --git a/mail_history_mark_unread/README.rst b/mail_history_mark_unread/README.rst new file mode 100644 index 0000000000..878c02a84e --- /dev/null +++ b/mail_history_mark_unread/README.rst @@ -0,0 +1,84 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=========================== +Mail History Mark as Unread +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3f8e69e93e8dfa080108bf1714a0349710c7ebdfc9988525a5a523218a1159fa + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/19.0/mail_history_mark_unread + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-19-0/social-19-0-mail_history_mark_unread + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/social&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module introduces a "Mark as Unread" button for messages in the +History mailbox and Search results of the Discuss App. When done the +message is restored back to the Inbox, just like if it were a new unread +message, and disappears from the History mailbox. + +The button is also available in the Search Messages panel (accessible +via the search icon in Discuss), allowing users to mark any searched +message as unread directly from the search results. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Contributors +------------ + +- `ForgeFlow `__: + + - Andreu Orensanz + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_history_mark_unread/__init__.py b/mail_history_mark_unread/__init__.py new file mode 100644 index 0000000000..83e553ac46 --- /dev/null +++ b/mail_history_mark_unread/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/mail_history_mark_unread/__manifest__.py b/mail_history_mark_unread/__manifest__.py new file mode 100644 index 0000000000..30a9304cd8 --- /dev/null +++ b/mail_history_mark_unread/__manifest__.py @@ -0,0 +1,21 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Mail History Mark as Unread", + "summary": """Add 'Mark as Unread' action to messages in History mailbox + and Search results""", + "version": "19.0.1.0.0", + "category": "Social Network", + "website": "https://github.com/OCA/social", + "author": "Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": ["mail"], + "assets": { + "web.assets_backend": [ + "mail_history_mark_unread/static/src/message_actions_patch.esm.js", + "mail_history_mark_unread/static/src/message_model_patch.esm.js", + "mail_history_mark_unread/static/src/mail_core_web_service_patch.esm.js", + "mail_history_mark_unread/static/src/message_card_list.xml", + ], + }, +} diff --git a/mail_history_mark_unread/i18n/es.po b/mail_history_mark_unread/i18n/es.po new file mode 100644 index 0000000000..ecedc74827 --- /dev/null +++ b/mail_history_mark_unread/i18n/es.po @@ -0,0 +1,23 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_history_mark_unread +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-12-10 14:56+0000\n" +"PO-Revision-Date: 2025-12-10 14:56+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mail_history_mark_unread +#. odoo-javascript +#: code:addons/mail_history_mark_unread/static/src/core/common/message_actions_patch.js:0 +#, python-format +msgid "Mark as Unread" +msgstr "Marcar como no leído" diff --git a/mail_history_mark_unread/i18n/it.po b/mail_history_mark_unread/i18n/it.po new file mode 100644 index 0000000000..9df621a848 --- /dev/null +++ b/mail_history_mark_unread/i18n/it.po @@ -0,0 +1,29 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_history_mark_unread +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-12-18 08:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: mail_history_mark_unread +#. odoo-javascript +#: code:addons/mail_history_mark_unread/static/src/message_actions_patch.esm.js:0 +#, python-format +msgid "Mark as Unread" +msgstr "Segna da leggere" + +#. module: mail_history_mark_unread +#: model:ir.model,name:mail_history_mark_unread.model_mail_message +msgid "Message" +msgstr "Messaggio" diff --git a/mail_history_mark_unread/i18n/mail_history_mark_unread.pot b/mail_history_mark_unread/i18n/mail_history_mark_unread.pot new file mode 100644 index 0000000000..9ea72f57ca --- /dev/null +++ b/mail_history_mark_unread/i18n/mail_history_mark_unread.pot @@ -0,0 +1,26 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_history_mark_unread +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mail_history_mark_unread +#. odoo-javascript +#: code:addons/mail_history_mark_unread/static/src/message_actions_patch.esm.js:0 +#, python-format +msgid "Mark as Unread" +msgstr "" + +#. module: mail_history_mark_unread +#: model:ir.model,name:mail_history_mark_unread.model_mail_message +msgid "Message" +msgstr "" diff --git a/mail_history_mark_unread/models/__init__.py b/mail_history_mark_unread/models/__init__.py new file mode 100644 index 0000000000..e5f3405d68 --- /dev/null +++ b/mail_history_mark_unread/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import mail_message diff --git a/mail_history_mark_unread/models/mail_message.py b/mail_history_mark_unread/models/mail_message.py new file mode 100644 index 0000000000..2dc6ad8030 --- /dev/null +++ b/mail_history_mark_unread/models/mail_message.py @@ -0,0 +1,42 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class MailMessage(models.Model): + _inherit = "mail.message" + + def set_message_undone(self): + """ + Reverses the effect of set_message_done() by: + - Setting is_read to False on the notification + - Moving the message from History back to Inbox + - Updating the needaction counter + """ + partner_id = self.env.user.partner_id + + notifications = ( + self.env["mail.notification"] + .sudo() + .search( + [ + ("mail_message_id", "in", self.ids), + ("res_partner_id", "=", partner_id.id), + ("is_read", "=", True), + ] + ) + ) + + if not notifications: + return + + notifications.write({"is_read": False}) + + counter = partner_id._get_needaction_count() + self.env.user._bus_send( + "mail.message/mark_as_unread", + { + "message_ids": notifications.mail_message_id.ids, + "needaction_inbox_counter": counter, + }, + ) diff --git a/mail_history_mark_unread/pyproject.toml b/mail_history_mark_unread/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/mail_history_mark_unread/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_history_mark_unread/readme/CONTRIBUTORS.md b/mail_history_mark_unread/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..313a35836a --- /dev/null +++ b/mail_history_mark_unread/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [ForgeFlow](https://www.forgeflow.com): + - Andreu Orensanz \<\> diff --git a/mail_history_mark_unread/readme/DESCRIPTION.md b/mail_history_mark_unread/readme/DESCRIPTION.md new file mode 100644 index 0000000000..7d7a72f78e --- /dev/null +++ b/mail_history_mark_unread/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This module introduces a "Mark as Unread" button for messages in the History mailbox and Search results of the Discuss App. +When done the message is restored back to the Inbox, just like if it were a new unread message, and disappears from the History mailbox. + +The button is also available in the Search Messages panel (accessible via the search icon in Discuss), allowing users to mark any searched message as unread directly from the search results. diff --git a/mail_history_mark_unread/static/description/icon.png b/mail_history_mark_unread/static/description/icon.png new file mode 100644 index 0000000000..1dcc49c24f Binary files /dev/null and b/mail_history_mark_unread/static/description/icon.png differ diff --git a/mail_history_mark_unread/static/description/index.html b/mail_history_mark_unread/static/description/index.html new file mode 100644 index 0000000000..454643175b --- /dev/null +++ b/mail_history_mark_unread/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Mail History Mark as Unread

+ +

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runboat

+

This module introduces a “Mark as Unread” button for messages in the +History mailbox and Search results of the Discuss App. When done the +message is restored back to the Inbox, just like if it were a new unread +message, and disappears from the History mailbox.

+

The button is also available in the Search Messages panel (accessible +via the search icon in Discuss), allowing users to mark any searched +message as unread directly from the search results.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/social project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/mail_history_mark_unread/static/src/mail_core_web_service_patch.esm.js b/mail_history_mark_unread/static/src/mail_core_web_service_patch.esm.js new file mode 100644 index 0000000000..b6b767f7e1 --- /dev/null +++ b/mail_history_mark_unread/static/src/mail_core_web_service_patch.esm.js @@ -0,0 +1,28 @@ +import {MailCoreWeb} from "@mail/core/web/mail_core_web_service"; +import {patch} from "@web/core/utils/patch"; + +patch(MailCoreWeb.prototype, { + setup() { + super.setup(); + this.busService.subscribe("mail.message/mark_as_unread", (payload) => { + const {message_ids: messageIds, needaction_inbox_counter} = payload; + const inbox = this.store.inbox; + const history = this.store.history; + for (const messageId of messageIds) { + const message = this.store["mail.message"].get(messageId); + if (!message) { + continue; + } + const thread = message.thread; + if (thread && !message.needaction) { + thread.message_needaction_counter++; + } + message.needaction = true; + // Move message from History back to Inbox + history.messages.delete({id: messageId}); + inbox.messages.add(message); + } + inbox.counter = needaction_inbox_counter; + }); + }, +}); diff --git a/mail_history_mark_unread/static/src/message_actions_patch.esm.js b/mail_history_mark_unread/static/src/message_actions_patch.esm.js new file mode 100644 index 0000000000..0147b2e93d --- /dev/null +++ b/mail_history_mark_unread/static/src/message_actions_patch.esm.js @@ -0,0 +1,33 @@ +import {_t} from "@web/core/l10n/translation"; +import {registerMessageAction} from "@mail/core/common/message_actions"; + +// Add "Mark as Unread" action for messages in History mailbox and Search results +registerMessageAction("mark-as-unread-history", { + condition: ({message, owner, store, thread}) => { + if (!message?.persistent || !store.self) { + return false; + } + if (thread?.eq(store.history)) { + return true; + } + if (owner.env.messageCard && !message.needaction) { + return true; + } + return false; + }, + icon: "fa fa-eye-slash", + name: _t("Mark as Unread"), + onSelected: async ({message, owner}) => { + await message.setUndone(); + // Remove from search results if in search mode + const messageSearch = owner.props.messageSearch; + if (messageSearch?.messages) { + const index = messageSearch.messages.findIndex((m) => m.id === message.id); + if (index !== -1) { + messageSearch.messages.splice(index, 1); + messageSearch.count = Math.max(0, messageSearch.count - 1); + } + } + }, + sequence: 15, +}); diff --git a/mail_history_mark_unread/static/src/message_card_list.xml b/mail_history_mark_unread/static/src/message_card_list.xml new file mode 100644 index 0000000000..879d27fa72 --- /dev/null +++ b/mail_history_mark_unread/static/src/message_card_list.xml @@ -0,0 +1,9 @@ + + + + + + props.mode === 'search' + + + diff --git a/mail_history_mark_unread/static/src/message_model_patch.esm.js b/mail_history_mark_unread/static/src/message_model_patch.esm.js new file mode 100644 index 0000000000..2f02971c7c --- /dev/null +++ b/mail_history_mark_unread/static/src/message_model_patch.esm.js @@ -0,0 +1,17 @@ +import {Message} from "@mail/core/common/message_model"; +import {patch} from "@web/core/utils/patch"; + +patch(Message.prototype, { + /** + * Mark this message as unread for the current user. + * This moves the message from History back to Inbox and is the inverse + * of the core setDone(). + */ + async setUndone() { + await this.store.env.services.orm.silent.call( + "mail.message", + "set_message_undone", + [[this.id]] + ); + }, +}); diff --git a/mail_history_mark_unread/tests/__init__.py b/mail_history_mark_unread/tests/__init__.py new file mode 100644 index 0000000000..2942e2a310 --- /dev/null +++ b/mail_history_mark_unread/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_set_message_undone diff --git a/mail_history_mark_unread/tests/test_set_message_undone.py b/mail_history_mark_unread/tests/test_set_message_undone.py new file mode 100644 index 0000000000..8c1f062822 --- /dev/null +++ b/mail_history_mark_unread/tests/test_set_message_undone.py @@ -0,0 +1,75 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from unittest.mock import patch + +from odoo.addons.base.tests.common import BaseCommon + + +class TestSetMessageUndone(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + + def test_01_set_message_undone_marks_notification_unread_and_sends_bus(self): + message = self.env["mail.message"].create( + { + "model": "res.partner", + "res_id": self.partner.id, + "body": "message in history", + "message_type": "comment", + "subtype_id": self.env.ref("mail.mt_comment").id, + } + ) + notification = self.env["mail.notification"].create( + { + "mail_message_id": message.id, + "notification_status": "sent", + "notification_type": "inbox", + "res_partner_id": self.env.user.partner_id.id, + "is_read": True, + } + ) + self.assertTrue(notification.is_read) + + with patch.object( + type(self.env.user), "_bus_send", autospec=True + ) as mocked_bus_send: + message.set_message_undone() + + notification.invalidate_recordset(["is_read"]) + self.assertFalse(notification.is_read) + + expected_counter = self.env.user.partner_id._get_needaction_count() + mocked_bus_send.assert_called_once() + _self, notification_type, payload = mocked_bus_send.call_args.args + self.assertEqual(notification_type, "mail.message/mark_as_unread") + self.assertEqual(payload.get("message_ids"), [message.id]) + self.assertEqual(payload.get("needaction_inbox_counter"), expected_counter) + + def test_02_set_message_undone_noop_when_no_read_notification(self): + message = self.env["mail.message"].create( + { + "model": "res.partner", + "res_id": self.partner.id, + "body": "message without read notification", + "message_type": "comment", + "subtype_id": self.env.ref("mail.mt_comment").id, + } + ) + # Notification exists but is already unread + self.env["mail.notification"].create( + { + "mail_message_id": message.id, + "notification_status": "sent", + "notification_type": "inbox", + "res_partner_id": self.env.user.partner_id.id, + "is_read": False, + } + ) + + with patch.object( + type(self.env.user), "_bus_send", autospec=True + ) as mocked_bus_send: + message.set_message_undone() + mocked_bus_send.assert_not_called()