[IMP] mail: mark message as unread

task-3232911

Part-of: odoo/odoo#116085
This commit is contained in:
tsm-odoo
2023-03-22 14:52:13 +01:00
parent 859156905c
commit 14b0da3d0a
10 changed files with 74 additions and 37 deletions
+2 -2
View File
@@ -415,9 +415,9 @@ class DiscussController(http.Controller):
return messages.message_format()
@http.route('/mail/channel/set_last_seen_message', methods=['POST'], type='json', auth='public')
def mail_channel_mark_as_seen(self, channel_id, last_message_id, **kwargs):
def mail_channel_mark_as_seen(self, channel_id, last_message_id, allow_older=False, **kwargs):
channel_member_sudo = request.env['mail.channel.member']._get_as_sudo_from_request_or_raise(request=request, channel_id=int(channel_id))
return channel_member_sudo.channel_id._channel_seen(int(last_message_id))
return channel_member_sudo.channel_id._channel_seen(last_message_id, allow_older=allow_older)
@http.route('/mail/channel/notify_typing', methods=['POST'], type='json', auth='public')
def mail_channel_notify_typing(self, channel_id, is_typing, **kwargs):
+15 -8
View File
@@ -962,21 +962,26 @@ class Channel(models.Model):
else:
self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.channel/legacy_insert', self.channel_info()[0])
def _channel_seen(self, last_message_id=None):
def _channel_seen(self, last_message_id=None, allow_older=False):
"""
Mark channel as seen by updating seen message id of the current logged partner
:param last_message_id: the id of the message to be marked as seen, last message of the
thread by default. This param SHOULD be required, the default behaviour is DEPRECATED and
kept only for compatibility reasons.
:param allow_order: whether to allow setting and older message
as the last seen message.
"""
self.ensure_one()
domain = ["&", ("model", "=", "mail.channel"), ("res_id", "in", self.ids)]
if last_message_id:
domain = expression.AND([domain, [('id', '<=', last_message_id)]])
last_message = self.env['mail.message'].search(domain, order="id DESC", limit=1)
if not last_message:
domain = expression.AND([domain, [('id', '<=', int(last_message_id))]])
last_message = (
self.env["mail.message"] if last_message_id is False
else self.env['mail.message'].search(domain, order="id DESC", limit=1)
)
if last_message_id is not False and not last_message:
return
self._set_last_seen_message(last_message)
self._set_last_seen_message(last_message, allow_older=allow_older)
data = {
'channel_id': self.id,
'last_message_id': last_message.id,
@@ -986,22 +991,24 @@ class Channel(models.Model):
self.env['bus.bus']._sendone(target, 'mail.channel.member/seen', data)
return last_message.id
def _set_last_seen_message(self, last_message):
def _set_last_seen_message(self, last_message, allow_older=False):
"""
Set last seen message of `self` channels for the current user.
:param last_message: the message to set as last seen message
:param allow_order: whether to allow setting and older message
as the last seen message.
"""
channel_member_domain = expression.AND([
[('channel_id', 'in', self.ids)],
[('partner_id', '=', self.env.user.partner_id.id)],
expression.OR([
[] if allow_older else expression.OR([
[('seen_message_id', '=', False)],
[('seen_message_id', '<', last_message.id)]
])
])
member = self.env['mail.channel.member'].search(channel_member_domain)
member.write({
'fetched_message_id': last_message.id,
'fetched_message_id': max(member.fetched_message_id.id, last_message.id),
'seen_message_id': last_message.id,
'last_seen_dt': fields.Datetime.now(),
})
+9 -1
View File
@@ -65,7 +65,7 @@ export class Thread {
messages = [];
/** @type {string} */
name;
/** @type {number} */
/** @type {number|false} */
serverLastSeenMsgBySelf;
/** @type {'opened' | 'folded' | 'closed'} */
state;
@@ -403,4 +403,12 @@ export class Thread {
getMemberName(persona) {
return persona.name;
}
getPreviousMessage(message) {
const previousMessages = this.nonEmptyMessages.filter(({ id }) => id < message.id);
if (previousMessages.length === 0) {
return false;
}
return this._store.messages[Math.max(...previousMessages.map((m) => m.id))];
}
}
@@ -805,7 +805,7 @@ export class ThreadService {
if (message.id <= thread.serverLastSeenMsgBySelf) {
continue;
}
if (message.isSelfAuthored || message.isTransient) {
if (message.isTransient) {
lastSeenMessageId = message.id;
continue;
}
+14
View File
@@ -88,6 +88,7 @@ export class Message extends Component {
this.messageBody = useRef("body");
this.messaging = useMessaging();
this.store = useStore();
this.rpc = useService("rpc");
/** @type {import("@mail/core/thread_service").ThreadService} */
this.threadService = useState(useService("mail.thread"));
/** @type {import("@mail/core/message_service").MessageService} */
@@ -316,6 +317,19 @@ export class Message extends Component {
}
}
onClickMarkAsUnread() {
const previousMessageId =
this.message.originThread.getPreviousMessage(this.message)?.id ?? false;
if (this.threadService.lastSeenBySelfMessageId(this.props.thread) === previousMessageId) {
return;
}
return this.rpc("/mail/channel/set_last_seen_message", {
channel_id: this.message.originThread.id,
last_message_id: previousMessageId,
allow_older: true,
});
}
get authorText() {
return this.hasOpenChatFeature ? _t("Open chat") : "";
}
@@ -146,6 +146,8 @@
<i t-if="env.inChatWindow" class="cursor-pointer px-1 py-2 fa fa-lg fa-ellipsis-h order-1"
t-on-click="() => state.isActionListSquashed = !state.isActionListSquashed" role="button" tabindex="1" title="Compact" aria-label="Compact"/>
<t t-if="!env.inChatWindow or !state.isActionListSquashed">
<i t-if="props.thread.model === 'mail.channel'" class="cursor-pointer px-1 py-2 fa fa-lg fa-eye-slash" t-on-click="onClickMarkAsUnread"
role="button" tabindex="0" title="Mark as Unread" aria-label="Mark as Unread"/>
<i t-if="canToggleStar" class="cursor-pointer px-1 py-2 fa fa-lg"
t-att-class="message.isStarred ? 'fa-star o-mail-Message-starred' : 'fa-star-o'"
t-on-click="() => messageService.toggleStar(props.message)"
+5 -5
View File
@@ -20,6 +20,11 @@
</div>
<t t-set="currentDay" t-value="msg.dateDay"/>
</t>
<Transition visible="threadService.lastSeenBySelfMessageId(props.thread) === (prevMsg?.id ?? false)" name="'o-fade'" t-slot-scope="transition">
<div class="o-mail-Thread-newMessage d-flex align-items-center fw-bolder" t-att-class="{ 'opacity-0': transition.className.includes('o-fade-leave') }">
<hr class="flex-grow-1 border opacity-50"/><span class="px-3">New messages</span>
</div>
</Transition>
<t t-if="msg.isNotification">
<t t-call="mail.NotificationMessage"/>
</t>
@@ -33,11 +38,6 @@
thread="props.thread"
messageEdition="props.messageEdition"
/>
<Transition visible="threadService.lastSeenBySelfMessageId(props.thread) === msg.id and !msg_last" name="'o-fade'" t-slot-scope="transition">
<div class="o-mail-Thread-newMessage d-flex align-items-center fw-bolder" t-att-class="{ 'opacity-0': transition.className.includes('o-fade-leave') }">
<hr class="flex-grow-1 border opacity-50"/><span class="px-3">New messages</span>
</div>
</Transition>
<t t-set="prevMsg" t-value="msg"/>
</t>
<t t-if="props.order !== 'asc'">
@@ -596,7 +596,7 @@ QUnit.test(
);
QUnit.test(
"new message separator is not shown in a chat window of a chat on receiving new message if there is no history of conversation",
"new message separator is shown in chat window of chat on receiving new message when there was no history",
async (assert) => {
const pyEnv = await startServer();
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
@@ -621,7 +621,7 @@ QUnit.test(
uuid: "channel-10-uuid",
})
);
assert.containsNone($, "hr + span:contains(New messages)");
assert.containsOnce($, "hr + span:contains(New messages)");
}
);
@@ -644,27 +644,16 @@ patch(MockServer.prototype, "mail/models/mail_channel", {
throw new Error("Should only be one channel in channel_seen mock params");
}
const channel = this.getRecords("mail.channel", [["id", "=", channel_id]])[0];
const messagesBeforeGivenLastMessage = this.getRecords("mail.message", [
["id", "<=", last_message_id],
const messages = this.getRecords("mail.message", [
["model", "=", "mail.channel"],
["res_id", "=", channel.id],
]);
if (!messagesBeforeGivenLastMessage || messagesBeforeGivenLastMessage.length === 0) {
if (!messages || messages.length === 0) {
return;
}
if (!channel) {
return;
}
const [memberOfCurrentUser] = this.getRecords("mail.channel.member", [
["channel_id", "=", channel_id],
["partner_id", "=", this.currentPartnerId],
]);
if (
memberOfCurrentUser.seen_message_id &&
memberOfCurrentUser.seen_message_id >= last_message_id
) {
return;
}
this._mockMailChannel_SetLastSeenMessage([channel.id], last_message_id);
this.pyEnv["bus.bus"]._sendone(
channel.channel_type === "chat" ? channel : this.pyEnv.currentPartner,
@@ -940,9 +929,11 @@ patch(MockServer.prototype, "mail/models/mail_channel", {
["channel_id", "in", ids],
["partner_id", "=", this.currentPartnerId],
]);
this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], {
fetched_message_id: message_id,
seen_message_id: message_id,
});
if (memberOfCurrentUser) {
this.pyEnv["mail.channel.member"].write([memberOfCurrentUser.id], {
fetched_message_id: message_id,
seen_message_id: message_id,
});
}
},
});
@@ -515,7 +515,7 @@ QUnit.test("basic rendering of message", async (assert) => {
deserializeDateTime("2019-04-20 10:00:00").toLocaleString(DateTime.DATETIME_SHORT)
);
assert.containsOnce($message, ".o-mail-Message-actions");
assert.containsN($message, ".o-mail-Message-actions i", 3);
assert.containsN($message, ".o-mail-Message-actions i", 4);
assert.containsOnce($message, "i[aria-label='Add a Reaction']");
assert.containsOnce($message, "i[aria-label='Mark as Todo']");
assert.containsOnce($message, "i[aria-label='Reply']");
@@ -1385,3 +1385,18 @@ QUnit.test("Can reply to chatter messages from history", async (assert) => {
await openDiscuss("mail.box_history");
assert.containsOnce($, ".o-mail-Message-actions [title='Reply']");
});
QUnit.test("Mark as unread", async (assert) => {
const pyEnv = await startServer();
const channelId = pyEnv["mail.channel"].create({ channel_type: "chat", name: "General" });
pyEnv["mail.message"].create({
model: "mail.channel",
res_id: channelId,
body: "Hello World!",
});
const { openDiscuss } = await start();
await openDiscuss(channelId);
await click("[title='Mark as Unread']");
assert.containsOnce($, ".o-mail-Thread-newMessage");
assert.containsOnce($, ".o-mail-DiscussCategoryItem .badge:contains(1)");
});