[IMP] mail: mark message as unread
task-3232911 Part-of: odoo/odoo#116085
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user