diff --git a/addons/im_livechat/static/src/embed/core/disabled_features.js b/addons/im_livechat/static/src/embed/core/disabled_features.js
index 6a4c3cacffd..84fa35c94d8 100644
--- a/addons/im_livechat/static/src/embed/core/disabled_features.js
+++ b/addons/im_livechat/static/src/embed/core/disabled_features.js
@@ -41,6 +41,9 @@ patch(ThreadService.prototype, "im_livechat/disabled", {
async fetchNewMessages(thread) {
return;
},
+ async loadAround() {
+ return;
+ },
});
patch(Store.prototype, "im_livechat/disabled", {
diff --git a/addons/mail/static/src/core/common/chat_window.js b/addons/mail/static/src/core/common/chat_window.js
index 88c0e68bd5f..6cd295d8a92 100644
--- a/addons/mail/static/src/core/common/chat_window.js
+++ b/addons/mail/static/src/core/common/chat_window.js
@@ -48,7 +48,7 @@ export class ChatWindow extends Component {
this.messageEdition = useMessageEdition();
this.messageHighlight = useMessageHighlight();
this.messageToReplyTo = useMessageToReplyTo();
- this.state = useState({ moreActionsExpanded: false });
+ this.state = useState({ moreActionsExpanded: false, jumpThreadPresent: 0 });
this.ui = useState(useService("ui"));
this.contentRef = useRef("content");
this.threadActions = useThreadActions();
diff --git a/addons/mail/static/src/core/common/chat_window.xml b/addons/mail/static/src/core/common/chat_window.xml
index 290cfce2186..3239e3e3130 100644
--- a/addons/mail/static/src/core/common/chat_window.xml
+++ b/addons/mail/static/src/core/common/chat_window.xml
@@ -60,8 +60,8 @@
-
-
+
+
diff --git a/addons/mail/static/src/core/common/composer.js b/addons/mail/static/src/core/common/composer.js
index 23569eeed7e..9b47dd20a96 100644
--- a/addons/mail/static/src/core/common/composer.js
+++ b/addons/mail/static/src/core/common/composer.js
@@ -16,13 +16,26 @@ import {
markEventHandled,
} from "@mail/utils/common/misc";
-import { Component, onMounted, useChildSubEnv, useEffect, useRef, useState } from "@odoo/owl";
+import {
+ Component,
+ markup,
+ onMounted,
+ useChildSubEnv,
+ useEffect,
+ useRef,
+ useState,
+} from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
import { sprintf } from "@web/core/utils/strings";
import { FileUploader } from "@web/views/fields/file_handler";
+const EDIT_CLICK_TYPE = {
+ CANCEL: "cancel",
+ SAVE: "save",
+};
+
/**
* @typedef {Object} Props
* @property {import("@mail/core/common/composer_model").Composer} composer
@@ -47,6 +60,7 @@ export class Composer extends Component {
static defaultProps = {
mode: "normal",
className: "",
+ sidebar: true,
};
static props = [
"composer",
@@ -60,10 +74,16 @@ export class Composer extends Component {
"messageEdition?",
"messageComponent?",
"className?",
+ "sidebar?",
];
static template = "mail.Composer";
setup() {
+ this.SEND_KEYBIND_TO_SEND = markup(
+ sprintf(_t("%(send_keybind)s to send"), {
+ send_keybind: this.sendKeybind,
+ })
+ );
this.messaging = useMessaging();
this.store = useStore();
if (this.allowUpload) {
@@ -180,6 +200,39 @@ export class Composer extends Component {
return "";
}
+ onClickCancelOrSaveEditText(ev) {
+ if (this.props.composer.message && ev.target.dataset?.type === EDIT_CLICK_TYPE.CANCEL) {
+ this.props.onDiscardCallback(ev);
+ }
+ if (this.props.composer.message && ev.target.dataset?.type === EDIT_CLICK_TYPE.SAVE) {
+ this.editMessage(ev);
+ }
+ }
+
+ get CANCEL_OR_SAVE_EDIT_TEXT() {
+ return markup(
+ sprintf(
+ _t(
+ "%(cancel_keybind)s to cancel, %(save_keybind)s to save"
+ ),
+ {
+ cancel_keybind: _t("Escape"),
+ cancel_type: EDIT_CLICK_TYPE.CANCEL,
+ save_keybind: this.sendKeybind,
+ save_type: EDIT_CLICK_TYPE.SAVE,
+ }
+ )
+ );
+ }
+
+ get SEND_TEXT() {
+ return this.props.composer.type === "note" ? _t("Log") : _t("Send");
+ }
+
+ get sendKeybind() {
+ return this.props.mode === "extended" ? _t("CTRL-Enter") : _t("Enter");
+ }
+
get thread() {
return this.props.composer.thread ?? null;
}
diff --git a/addons/mail/static/src/core/common/composer.scss b/addons/mail/static/src/core/common/composer.scss
index 4283d5b5a90..1c04322c361 100644
--- a/addons/mail/static/src/core/common/composer.scss
+++ b/addons/mail/static/src/core/common/composer.scss
@@ -46,10 +46,14 @@
max-height: 100px;
resize: none;
- .o-extended & {
+ .o-extended:not(.o-isUiSmall) & {
max-height: 400px;
}
+ .o-extended.o-isUiSmall & {
+ max-height: Min(400px, 30vh);
+ }
+
&::placeholder {
@include text-truncate();
}
diff --git a/addons/mail/static/src/core/common/composer.xml b/addons/mail/static/src/core/common/composer.xml
index c9c0ff95a45..4119cd02a4e 100644
--- a/addons/mail/static/src/core/common/composer.xml
+++ b/addons/mail/static/src/core/common/composer.xml
@@ -8,11 +8,15 @@
- escape to cancel, enter to save
+
diff --git a/addons/mail/static/src/core/common/discuss.js b/addons/mail/static/src/core/common/discuss.js
index 200f38225ac..7c611fcd6a0 100644
--- a/addons/mail/static/src/core/common/discuss.js
+++ b/addons/mail/static/src/core/common/discuss.js
@@ -51,6 +51,7 @@ export class Discuss extends Component {
this.messageToReplyTo = useMessageToReplyTo();
this.contentRef = useRef("content");
this.root = useRef("root");
+ this.state = useState({ jumpThreadPresent: 0 });
this.orm = useService("orm");
this.effect = useService("effect");
this.ui = useState(useService("ui"));
diff --git a/addons/mail/static/src/core/common/discuss.xml b/addons/mail/static/src/core/common/discuss.xml
index acffddc9519..5de23cb70f8 100644
--- a/addons/mail/static/src/core/common/discuss.xml
+++ b/addons/mail/static/src/core/common/discuss.xml
@@ -62,8 +62,8 @@
diff --git a/addons/mail/static/src/core/common/message.js b/addons/mail/static/src/core/common/message.js
index 9f7c346a6f4..fe1aa6a12d3 100644
--- a/addons/mail/static/src/core/common/message.js
+++ b/addons/mail/static/src/core/common/message.js
@@ -238,8 +238,10 @@ export class Message extends Component {
return Boolean(!this.message.isTransient && this.message.resId);
}
- get canUnfollow() {
- return Boolean(this.message.originThread?.followerOfSelf);
+ get showUnfollow() {
+ return Boolean(
+ this.message.originThread?.followerOfSelf && this.props.thread?.model === "mail.box"
+ );
}
get isActive() {
diff --git a/addons/mail/static/src/core/common/message.scss b/addons/mail/static/src/core/common/message.scss
index 4f60775c365..b3322222861 100644
--- a/addons/mail/static/src/core/common/message.scss
+++ b/addons/mail/static/src/core/common/message.scss
@@ -51,10 +51,10 @@
}
.o-mail-Message-actions {
- z-index: $o-mail-NavigableList-zIndex - 2;
+ z-index: $o-mail-NavigableList-zIndex - 3;
&.o-expanded {
- z-index: $o-mail-NavigableList-zIndex - 1;
+ z-index: $o-mail-NavigableList-zIndex - 2;
}
button:hover, .focus {
diff --git a/addons/mail/static/src/core/common/message.xml b/addons/mail/static/src/core/common/message.xml
index 1d351fd0856..c0d51294878 100644
--- a/addons/mail/static/src/core/common/message.xml
+++ b/addons/mail/static/src/core/common/message.xml
@@ -79,7 +79,7 @@
-
+
Subject:
@@ -131,7 +131,7 @@
View Reactions
Mark as Read
- Unfollow
+ Unfollow
Mark as Unread
Edit
Delete
diff --git a/addons/mail/static/src/core/common/messaging_service.js b/addons/mail/static/src/core/common/messaging_service.js
index 9edb6ddfc08..b1957d23f5f 100644
--- a/addons/mail/static/src/core/common/messaging_service.js
+++ b/addons/mail/static/src/core/common/messaging_service.js
@@ -237,6 +237,7 @@ export class Messaging {
Object.assign(notif.payload, { body: markup(notif.payload.body) })
);
channel.messages.push(message);
+ channel.transientMessages.push(message);
break;
}
case "mail.link.preview/delete":
diff --git a/addons/mail/static/src/core/common/thread.js b/addons/mail/static/src/core/common/thread.js
index c2d942e7d88..8597889378c 100644
--- a/addons/mail/static/src/core/common/thread.js
+++ b/addons/mail/static/src/core/common/thread.js
@@ -28,6 +28,7 @@ export const PRESENT_THRESHOLD = 2500;
/**
* @typedef {Object} Props
* @property {boolean} [isInChatWindow=false]
+ * @property {number} [jumpPresent=0]
* @property {import("@mail/utils/common/hooks").MessageEdition} [messageEdition]
* @property {import("@mail/utils/common/hooks").MessageToReplyTo} [messageToReplyTo]
* @property {"asc"|"desc"} [order="asc"]
@@ -39,6 +40,7 @@ export class Thread extends Component {
static props = [
"isInChatWindow?",
"hasScrollAdjust?",
+ "jumpPresent?",
"thread",
"messageEdition?",
"messageToReplyTo?",
@@ -47,6 +49,7 @@ export class Thread extends Component {
static defaultProps = {
isInChatWindow: false,
hasScrollAdjust: true,
+ jumpPresent: 0,
order: "asc", // 'asc' or 'desc'
};
static template = "mail.Thread";
@@ -127,6 +130,15 @@ export class Thread extends Component {
() => this.updateShowJumpPresent(),
() => [this.props.thread.loadNewer]
);
+ useEffect(
+ () => {
+ if (this.props.jumpPresent !== this.lastJumpPresent) {
+ this.jumpToPresent("instant");
+ this.lastJumpPresent = this.props.jumpPresent;
+ }
+ },
+ () => [this.props.jumpPresent]
+ );
onMounted(() => {
this.oldestPersistentMessage = this.props.thread.oldestPersistentMessage?.id;
if (!this.env.inChatter || !this.props.hasScrollAdjust) {
@@ -135,9 +147,13 @@ export class Thread extends Component {
}
});
onWillStart(() => {
+ this.lastJumpPresent = this.props.jumpPresent;
this.threadService.fetchNewMessages(this.props.thread);
});
onWillUpdateProps((nextProps) => {
+ if (nextProps.thread !== this.props.thread) {
+ this.lastJumpPresent = nextProps.jumpPresent;
+ }
this.threadService.fetchNewMessages(nextProps.thread);
});
}
@@ -155,12 +171,12 @@ export class Thread extends Component {
this.threadService.fetchMoreMessages(this.props.thread);
}
- async onClickJumpPresent() {
+ async jumpToPresent(behavior) {
this.isJumpingRecent = true;
await this.threadService.loadAround(this.props.thread);
this.props.thread.loadNewer = false;
- this.present.el.scrollIntoView({
- behavior: this.props.order === "asc" ? "smooth" : "instant", // FIXME somehow smooth not working in desc mode
+ this.present.el?.scrollIntoView({
+ behavior: behavior ?? (this.props.order === "asc" ? "smooth" : "instant"), // FIXME somehow smooth not working in desc mode
block: "center",
});
// Let smooth scroll a bit so load more is not visible
diff --git a/addons/mail/static/src/core/common/thread.scss b/addons/mail/static/src/core/common/thread.scss
index e43d0e6ff78..a1de9a0c285 100644
--- a/addons/mail/static/src/core/common/thread.scss
+++ b/addons/mail/static/src/core/common/thread.scss
@@ -27,7 +27,7 @@
.o-mail-Thread-jumpPresent {
background-color: mix(map-get($theme-colors, 'info'), $o-webclient-background-color, 5%);
- z-index: $o-mail-NavigableList-zIndex - 2;
+ z-index: $o-mail-NavigableList-zIndex - 1;
}
.o-mail-NotificationMessage p {
diff --git a/addons/mail/static/src/core/common/thread.xml b/addons/mail/static/src/core/common/thread.xml
index 24e9c03d5d7..73659a60c02 100644
--- a/addons/mail/static/src/core/common/thread.xml
+++ b/addons/mail/static/src/core/common/thread.xml
@@ -3,7 +3,7 @@
-
+
@@ -90,7 +90,7 @@
-
+
You're viewing older messages
Jump to Present
diff --git a/addons/mail/static/src/core/common/thread_model.js b/addons/mail/static/src/core/common/thread_model.js
index 8e4821c194d..3941215efea 100644
--- a/addons/mail/static/src/core/common/thread_model.js
+++ b/addons/mail/static/src/core/common/thread_model.js
@@ -106,6 +106,7 @@ export class Thread {
/** @type {ScrollPosition} */
scrollPosition = new ScrollPosition();
showOnlyVideo = false;
+ transientMessages = [];
/** @type {import("@mail/core/common/store_service").Store} */
_store;
/** @type {string} */
diff --git a/addons/mail/static/src/core/common/thread_service.js b/addons/mail/static/src/core/common/thread_service.js
index 1f881c53e18..a41172d0b26 100644
--- a/addons/mail/static/src/core/common/thread_service.js
+++ b/addons/mail/static/src/core/common/thread_service.js
@@ -322,7 +322,7 @@ export class ThreadService {
body: message.body ? markup(message.body) : message.body,
})
);
- thread.loadNewer = true;
+ thread.loadNewer = messageId ? true : false;
thread.loadOlder = true;
if (messages.length < FETCH_LIMIT) {
const olderMessagesCount = messages.filter(({ id }) => id < messageId).length;
@@ -332,6 +332,7 @@ export class ThreadService {
thread.loadNewer = false;
}
}
+ this._enrichMessagesWithTransient(thread);
// Give some time to the UI to update.
await new Promise((resolve) => setTimeout(() => requestAnimationFrame(resolve)));
}
@@ -417,6 +418,7 @@ export class ThreadService {
}
}
}
+ this._enrichMessagesWithTransient(thread);
} catch {
// handled in fetchMessages
}
@@ -1076,6 +1078,28 @@ export class ThreadService {
data,
});
}
+
+ /**
+ * Following a load more or load around, listing of messages contains persistent messages.
+ * Transient messages are missing, so this function puts known transient messages at the
+ * right place in message list of thread.
+ *
+ * @param {Thread} thread
+ */
+ _enrichMessagesWithTransient(thread) {
+ for (const message of thread.transientMessages) {
+ if (message.id < thread.oldestPersistentMessage && !thread.loadOlder) {
+ thread.messages.unshift(message);
+ } else if (message.id > thread.newestPersistentMessage && !thread.loadNewer) {
+ thread.messages.push(message);
+ } else {
+ const afterIndex = thread.messages.findIndex(
+ (msg) => msg.id > message.id && !msg.isTransient
+ );
+ thread.messages.splice(afterIndex - 1, 0, message);
+ }
+ }
+ }
}
export const threadService = {
diff --git a/addons/mail/static/src/core/web/chatter.js b/addons/mail/static/src/core/web/chatter.js
index 71ace15fa74..092d96ce35f 100644
--- a/addons/mail/static/src/core/web/chatter.js
+++ b/addons/mail/static/src/core/web/chatter.js
@@ -20,6 +20,7 @@ import {
onWillStart,
onWillUpdateProps,
useChildSubEnv,
+ useEffect,
useRef,
useState,
} from "@odoo/owl";
@@ -28,6 +29,7 @@ import { Dropdown } from "@web/core/dropdown/dropdown";
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
import { escape, sprintf } from "@web/core/utils/strings";
+import { useThrottleForAnimation } from "@web/core/utils/timing";
import { FileUploader } from "@web/views/fields/file_handler";
/**
@@ -80,6 +82,7 @@ export class Chatter extends Component {
setup() {
this.action = useService("action");
+ this.attachmentBox = useRef("attachment-box");
this.messaging = useMessaging();
/** @type {import("@mail/activity/activity_service").ActivityService} */
this.activityService = useState(useService("mail.activity"));
@@ -90,6 +93,7 @@ export class Chatter extends Component {
this.rpc = useService("rpc");
this.state = useState({
isAttachmentBoxOpened: this.props.isAttachmentBoxVisibleInitially,
+ jumpThreadPresent: 0,
showActivities: true,
/** @type {import("@mail/core/common/thread_model").Thread} */
thread: undefined,
@@ -98,8 +102,9 @@ export class Chatter extends Component {
this.attachmentUploader = useAttachmentUploader(
this.threadService.getThread(this.props.threadModel, this.props.threadId)
);
- this.scrollPosition = useScrollPosition("scrollable", undefined, "top");
+ this.scrollPosition = useScrollPosition("root", undefined, "top");
this.rootRef = useRef("root");
+ this.onScrollDebounced = useThrottleForAnimation(this.onScroll);
useChildSubEnv({ inChatter: true });
useDropzone(
this.rootRef,
@@ -153,6 +158,14 @@ export class Chatter extends Component {
}
}
});
+ useEffect(
+ (opened) => {
+ if (opened) {
+ this.attachmentBox.el.scrollIntoView({ block: "center" });
+ }
+ },
+ () => [this.state.isAttachmentBoxOpened]
+ );
}
/**
@@ -292,6 +305,7 @@ export class Chatter extends Component {
this.reloadParentView();
}
this.toggleComposer();
+ this.state.jumpThreadPresent++;
// Load new messages to fetch potential new messages from other users (useful due to lack of auto-sync in chatter).
this.load(this.props.threadId, ["followers", "messages", "suggestedRecipients"]);
}
@@ -380,7 +394,6 @@ export class Chatter extends Component {
return;
}
this.state.isAttachmentBoxOpened = !this.state.isAttachmentBoxOpened;
- this.scrollPosition.ref.el.scrollTop = 0;
}
async onClickAttachFile(ev) {
@@ -392,4 +405,8 @@ export class Chatter extends Component {
return false;
}
}
+
+ onScroll() {
+ this.state.isTopStickyPinned = this.rootRef.el.scrollTop !== 0;
+ }
}
diff --git a/addons/mail/static/src/core/web/chatter.scss b/addons/mail/static/src/core/web/chatter.scss
index 634bc003ec1..5f4ea2fa7f4 100644
--- a/addons/mail/static/src/core/web/chatter.scss
+++ b/addons/mail/static/src/core/web/chatter.scss
@@ -10,6 +10,15 @@
width: $o-statusbar-height;
}
+.o-mail-Chatter-top {
+ z-index: $o-mail-NavigableList-zIndex - 2;
+ background-color: $o-webclient-background-color;
+
+ &.shadow-sm {
+ background-image: linear-gradient(90deg, transparent, $o-view-background-color, transparent);
+ }
+}
+
.o-mail-Followers-button:focus {
background-color: $gray-200;
}
diff --git a/addons/mail/static/src/core/web/chatter.xml b/addons/mail/static/src/core/web/chatter.xml
index 8577b9d4c58..345da4f8e5a 100644
--- a/addons/mail/static/src/core/web/chatter.xml
+++ b/addons/mail/static/src/core/web/chatter.xml
@@ -2,101 +2,111 @@
-
-
-
-
-
-