+
+
+
+
+
diff --git a/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.js b/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.js
new file mode 100644
index 00000000000..4f782d9a9e7
--- /dev/null
+++ b/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.js
@@ -0,0 +1,17 @@
+/* @odoo-module */
+
+import { useState } from "@odoo/owl";
+import { patch } from "@web/core/utils/patch";
+import { useService } from "@web/core/utils/hooks";
+import { GifPicker } from "@mail/discuss/gif_picker/common/gif_picker";
+import { PickerContent } from "@mail/core/common/picker_content";
+
+Object.assign(PickerContent.components, { GifPicker });
+
+patch(PickerContent.prototype, {
+ setup() {
+ super.setup();
+ this.store = useState(useService("mail.store"));
+ this.gifPickerService = useState(useService("discuss.gifPicker"));
+ },
+});
diff --git a/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.scss b/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.scss
new file mode 100644
index 00000000000..2677089d9bc
--- /dev/null
+++ b/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.scss
@@ -0,0 +1,12 @@
+// o-mail-PickerContent handles the picker in popover/mobile view
+.o-mail-PickerContent {
+ .o-discuss-GifPicker {
+ width: 100% !important;
+ height: 100% !important;
+ }
+
+ .o-mail-PickerContent-bigEmoji {
+ font-size: 5rem !important;
+ filter: grayscale(0.25);
+ }
+}
diff --git a/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.xml b/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.xml
new file mode 100644
index 00000000000..6a4b5d775ef
--- /dev/null
+++ b/addons/mail/static/src/discuss/gif_picker/common/picker_content_patch.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 🧑🍳🌶️
+ Want to spice up your conversations with GIFs? Activate the feature in the settings!
+
+
+
+
+
diff --git a/addons/mail/static/src/discuss/gif_picker/common/picker_patch.js b/addons/mail/static/src/discuss/gif_picker/common/picker_patch.js
new file mode 100644
index 00000000000..bdfd2161b3f
--- /dev/null
+++ b/addons/mail/static/src/discuss/gif_picker/common/picker_patch.js
@@ -0,0 +1,31 @@
+/* @odoo-module */
+
+import { patch } from "@web/core/utils/patch";
+import { Picker } from "@mail/core/common/picker";
+import { isEventHandled } from "@web/core/utils/misc";
+
+patch(Picker.prototype, {
+ /**
+ * @param {Event} ev
+ * @returns {boolean}
+ */
+ isEventHandledByPicker(ev) {
+ return super.isEventHandledByPicker(ev) || isEventHandled(ev, "Composer.onClickAddGif");
+ },
+ async toggle(el, ev) {
+ // Let event be handled by bubbling handlers first.
+ await super.toggle(el, ev);
+ if (isEventHandled(ev, "Composer.onClickAddGif")) {
+ if (this.popover.isOpen) {
+ if (this.props.state.picker === this.props.PICKERS.GIF) {
+ this.close();
+ return;
+ }
+ this.props.state.picker = this.props.PICKERS.GIF;
+ } else {
+ this.props.state.picker = this.props.PICKERS.GIF;
+ this.popover.open(el, this.contentProps);
+ }
+ }
+ },
+});
diff --git a/addons/mail/static/src/utils/common/hooks.js b/addons/mail/static/src/utils/common/hooks.js
index f021eb5ad4d..c1bd8e03621 100644
--- a/addons/mail/static/src/utils/common/hooks.js
+++ b/addons/mail/static/src/utils/common/hooks.js
@@ -12,7 +12,7 @@ import {
import { useService } from "@web/core/utils/hooks";
-function useExternalListener(target, eventName, handler, eventParams) {
+export function useLazyExternalListener(target, eventName, handler, eventParams) {
const boundHandler = handler.bind(useComponent());
let t;
onMounted(() => {
@@ -75,7 +75,7 @@ export function useHover(refName, callback = () => {}) {
state.isHover = hovered;
callback(hovered);
}
- useExternalListener(
+ useLazyExternalListener(
() => ref.el,
"mouseenter",
(ev) => {
@@ -86,7 +86,7 @@ export function useHover(refName, callback = () => {}) {
},
true
);
- useExternalListener(
+ useLazyExternalListener(
() => ref.el,
"mouseleave",
(ev) => {
@@ -276,6 +276,7 @@ export function useMessageHighlight(duration = 2000) {
}
export function useSelection({ refName, model, preserveOnClickAwayPredicate = () => false }) {
+ const ui = useState(useService("ui"));
const ref = useRef(refName);
function onSelectionChange() {
if (document.activeElement && document.activeElement === ref.el) {
@@ -313,7 +314,11 @@ export function useSelection({ refName, model, preserveOnClickAwayPredicate = ()
},
moveCursor(position) {
model.start = model.end = position;
- ref.el.selectionStart = ref.el.selectionEnd = position;
+ if (!ui.isSmall) {
+ // In mobile, selection seems to adjust correctly.
+ // Don't programmatically adjust, otherwise it shows soft keyboard!
+ ref.el.selectionStart = ref.el.selectionEnd = position;
+ }
},
};
}
diff --git a/addons/mail/static/tests/composer/composer_tests.js b/addons/mail/static/tests/composer/composer_tests.js
index 8fded8e5b33..d93e8bd494c 100644
--- a/addons/mail/static/tests/composer/composer_tests.js
+++ b/addons/mail/static/tests/composer/composer_tests.js
@@ -237,16 +237,18 @@ QUnit.test(
});
openDiscuss(channelId);
await triggerEvent(getFixture(), null, "mousedown");
- await click("button[aria-label='Emojis']");
+ await click("[title='Add a Reaction']");
+ await contains(".o-EmojiPicker-content", { scroll: 0 });
await scroll(".o-EmojiPicker-content", 150);
await triggerEvent(getFixture(), null, "mousedown");
- await click("[title='Add a Reaction']");
+ await click("button[aria-label='Emojis']");
+ await contains(".o-mail-PickerContent .o-EmojiPicker-content", { scroll: 0 });
await scroll(".o-EmojiPicker-content", 200);
await triggerEvent(getFixture(), null, "mousedown");
- await click("button[aria-label='Emojis']");
+ await click("[title='Add a Reaction']");
await contains(".o-EmojiPicker-content", { scroll: 150 });
await triggerEvent(getFixture(), null, "mousedown");
- await click("[title='Add a Reaction']");
+ await click("button[aria-label='Emojis']");
await contains(".o-EmojiPicker-content", { scroll: 200 });
}
);
diff --git a/addons/mail/static/tests/emoji/emoji_tests.js b/addons/mail/static/tests/emoji/emoji_tests.js
index 7654d035876..7ff9b7baf93 100644
--- a/addons/mail/static/tests/emoji/emoji_tests.js
+++ b/addons/mail/static/tests/emoji/emoji_tests.js
@@ -65,16 +65,18 @@ QUnit.test("Basic keyboard navigation", async () => {
const { openDiscuss } = await start();
openDiscuss(channelId);
await click("button[aria-label='Emojis']");
- await contains(".o-EmojiPicker-content .o-Emoji[data-index=0].bg-200"); // bg-200 means active
+ await contains(".o-EmojiPicker-content .o-Emoji[data-index=0].o-active");
triggerHotkey("ArrowRight");
- await contains(".o-EmojiPicker-content .o-Emoji[data-index=1].bg-200");
+ await contains(".o-EmojiPicker-content .o-Emoji[data-index=1].o-active");
triggerHotkey("ArrowDown");
- await contains(`.o-EmojiPicker-content .o-Emoji[data-index=${EMOJI_PER_ROW + 1}].bg-200`);
+ await contains(`.o-EmojiPicker-content .o-Emoji[data-index=${EMOJI_PER_ROW + 1}].o-active`);
triggerHotkey("ArrowLeft");
- await contains(`.o-EmojiPicker-content .o-Emoji[data-index=${EMOJI_PER_ROW}].bg-200`);
+ await contains(`.o-EmojiPicker-content .o-Emoji[data-index=${EMOJI_PER_ROW}].o-active`);
triggerHotkey("ArrowUp");
- await contains(".o-EmojiPicker-content .o-Emoji[data-index=0].bg-200");
- const codepoints = $(".o-EmojiPicker-content .o-Emoji[data-index=0].bg-200").data("codepoints");
+ await contains(".o-EmojiPicker-content .o-Emoji[data-index=0].o-active");
+ const codepoints = $(".o-EmojiPicker-content .o-Emoji[data-index=0].o-active").data(
+ "codepoints"
+ );
triggerHotkey("Enter");
await contains(".o-EmojiPicker", { count: 0 });
await contains(".o-mail-Composer-input", { value: codepoints });
@@ -151,7 +153,7 @@ QUnit.test("first category should be highlighted by default", async () => {
const { openDiscuss } = await start();
openDiscuss(channelId);
await click("button[aria-label='Emojis']");
- await contains(".o-EmojiPicker-navbar .o-Emoji:eq(0).bg-300");
+ await contains(".o-EmojiPicker-navbar .o-Emoji:eq(0).o-active");
});
QUnit.test(
@@ -163,7 +165,7 @@ QUnit.test(
openDiscuss(channelId);
await click("button[aria-label='Emojis']");
(await contains(".o-EmojiPicker-content .o-Emoji:contains(👺)"))[0].dispatchEvent(
- new MouseEvent("click", { shiftKey: true })
+ new MouseEvent("click", { bubbles: true, shiftKey: true })
);
await contains(".o-EmojiPicker-navbar [title='Frequently used']");
await contains(".o-EmojiPicker");
diff --git a/addons/mail/static/tests/gif_picker/gif_picker_test.js b/addons/mail/static/tests/gif_picker/gif_picker_test.js
index 0af8281f75a..e3fa9b2473d 100644
--- a/addons/mail/static/tests/gif_picker/gif_picker_test.js
+++ b/addons/mail/static/tests/gif_picker/gif_picker_test.js
@@ -111,7 +111,7 @@ QUnit.test("Searching for a GIF", async () => {
});
openDiscuss(channelId);
await click("button[aria-label='GIFs']");
- insertText((await contains("input[placeholder='Search for a gif']"))[0], "search");
+ insertText((await contains("input[placeholder='Search for a GIF']"))[0], "search");
await contains("i[aria-label='back']");
await contains(".o-discuss-Gif", { count: 2 });
});
@@ -133,7 +133,7 @@ QUnit.test("Open a GIF category trigger the search for the category", async () =
await click("button[aria-label='GIFs']");
await click("img[data-src='https://media.tenor.com/6uIlQAHIkNoAAAAM/cry.gif']");
await contains(".o-discuss-Gif", { count: 2 });
- await contains("input[placeholder='Search for a gif']", { value: "cry" });
+ await contains("input[placeholder='Search for a GIF']", { value: "cry" });
});
QUnit.test("Reopen GIF category list when going back", async () => {
@@ -196,7 +196,9 @@ QUnit.test(
const { openDiscuss } = await start();
openDiscuss(channelId);
await click("span", { text: "General" });
- await click("button[aria-label='GIFs']");
+ await click("button[aria-label='Emojis']");
+ await contains(".o-mail-PickerContent-picker .o-mail-PickerContent-emojiPicker");
+ await click("button:contains('GIFs')");
await contains(".popover .o-discuss-GifPicker", { count: 0 });
await contains(".o-mail-Composer-footer .o-discuss-GifPicker");
}
@@ -217,6 +219,6 @@ QUnit.test("Searching for a GIF with a failling RPC should display an error", as
});
await openDiscuss(channelId);
await click("button[aria-label='GIFs']");
- await insertText("input[placeholder='Search for a gif']", "search");
+ await insertText("input[placeholder='Search for a GIF']", "search");
await contains(".o-discuss-GifPicker-error");
});
diff --git a/addons/web/static/src/core/emoji_picker/emoji_picker.dark.scss b/addons/web/static/src/core/emoji_picker/emoji_picker.dark.scss
new file mode 100644
index 00000000000..d81155e78e8
--- /dev/null
+++ b/addons/web/static/src/core/emoji_picker/emoji_picker.dark.scss
@@ -0,0 +1,3 @@
+.o-EmojiPicker {
+ --o-emoji-picker-active: #{$white};
+}
diff --git a/addons/web/static/src/core/emoji_picker/emoji_picker.js b/addons/web/static/src/core/emoji_picker/emoji_picker.js
index f9d3de548a7..78368a5d81f 100644
--- a/addons/web/static/src/core/emoji_picker/emoji_picker.js
+++ b/addons/web/static/src/core/emoji_picker/emoji_picker.js
@@ -20,7 +20,7 @@ import { browser } from "@web/core/browser/browser";
import { _t } from "@web/core/l10n/translation";
import { usePopover } from "@web/core/popover/popover_hook";
import { fuzzyLookup } from "@web/core/utils/search";
-import { useService } from "@web/core/utils/hooks";
+import { useAutofocus, useService } from "@web/core/utils/hooks";
/**
*
@@ -125,23 +125,22 @@ export async function loadEmoji() {
export const EMOJI_PER_ROW = 9;
export class EmojiPicker extends Component {
- static props = ["onSelect", "className?", "close?", "onClose?", "storeScroll?"];
+ static props = ["close?", "onClose?", "onSelect", "state?", "storeScroll?"];
static template = "web.EmojiPicker";
categories = null;
emojis = null;
shouldScrollElem = null;
- lastSearchStr;
+ lastSearchTerm;
setup() {
- this.inputRef = useRef("input");
this.gridRef = useRef("emoji-grid");
this.ui = useState(useService("ui"));
this.state = useState({
activeEmojiIndex: 0,
categoryId: null,
recent: JSON.parse(browser.localStorage.getItem("web.emoji.frequent") || "{}"),
- searchStr: "",
+ searchTerm: "",
});
const onStorage = (ev) => {
if (ev.key === "web.emoji.frequent") {
@@ -154,6 +153,7 @@ export class EmojiPicker extends Component {
onWillDestroy(() => {
browser.removeEventListener("storage", onStorage);
});
+ useAutofocus();
onWillStart(async () => {
const { categories, emojis } = await loadEmoji();
this.categories = categories;
@@ -173,7 +173,6 @@ export class EmojiPicker extends Component {
if (this.emojis.length === 0) {
return;
}
- this.inputRef.el.focus();
this.highlightActiveCategory();
if (this.props.storeScroll) {
this.gridRef.el.scrollTop = this.props.storeScroll.get();
@@ -199,18 +198,18 @@ export class EmojiPicker extends Component {
});
useEffect(
() => {
- if (this.state.searchStr) {
+ if (this.searchTerm) {
this.gridRef.el.scrollTop = 0;
this.state.categoryId = null;
} else {
- if (this.lastSearchStr) {
+ if (this.lastSearchTerm) {
this.gridRef.el.scrollTop = 0;
}
this.highlightActiveCategory();
}
- this.lastSearchStr = this.state.searchStr;
+ this.lastSearchTerm = this.searchTerm;
},
- () => [this.state.searchStr]
+ () => [this.searchTerm]
);
onWillUnmount(() => {
if (this.emojis.length === 0) {
@@ -222,6 +221,18 @@ export class EmojiPicker extends Component {
});
}
+ get searchTerm() {
+ return this.props.state ? this.props.state.searchTerm : this.state.searchTerm;
+ }
+
+ set searchTerm(value) {
+ if (this.props.state) {
+ this.props.state.searchTerm = value;
+ } else {
+ this.state.searchTerm = value;
+ }
+ }
+
get itemsNumber() {
return this.recentEmojis.length + this.getEmojis().length;
}
@@ -234,7 +245,6 @@ export class EmojiPicker extends Component {
}
onClick(ev) {
- markEventHandled(ev, "EmojiPicker.onClick");
markEventHandled(ev, "emoji.selectEmoji");
}
@@ -286,9 +296,8 @@ export class EmojiPicker extends Component {
}
getEmojis() {
- const search = this.state.searchStr;
- if (search.length > 1) {
- return fuzzyLookup(this.state.searchStr, this.emojis, (emoji) =>
+ if (this.searchTerm.length > 1) {
+ return fuzzyLookup(this.searchTerm, this.emojis, (emoji) =>
[emoji.name, ...emoji.keywords, ...emoji.emoticons, ...emoji.shortcodes].join(" ")
);
}
@@ -297,18 +306,19 @@ export class EmojiPicker extends Component {
selectCategory(ev) {
const id = Number(ev.currentTarget.dataset.id);
- this.state.searchStr = "";
+ this.searchTerm = "";
this.state.categoryId = id;
this.shouldScrollElem = true;
}
selectEmoji(ev) {
const codepoints = ev.currentTarget.dataset.codepoints;
- this.props.onSelect(codepoints);
+ const resetOnSelect = !ev.shiftKey && !this.ui.isSmall;
+ this.props.onSelect(codepoints, resetOnSelect);
this.state.recent[codepoints] ??= 0;
this.state.recent[codepoints]++;
browser.localStorage.setItem("web.emoji.frequent", JSON.stringify(this.state.recent));
- if (!ev.shiftKey) {
+ if (resetOnSelect) {
this.gridRef.el.scrollTop = 0;
this.props.close?.();
this.props.onClose?.();
diff --git a/addons/web/static/src/core/emoji_picker/emoji_picker.scss b/addons/web/static/src/core/emoji_picker/emoji_picker.scss
index 4aeb7a5a812..85d5ee108c2 100644
--- a/addons/web/static/src/core/emoji_picker/emoji_picker.scss
+++ b/addons/web/static/src/core/emoji_picker/emoji_picker.scss
@@ -4,36 +4,21 @@
}
.o-EmojiPicker {
-
- display: grid;
- grid-template-areas:
- "search"
- "content"
- "navbar";
-
- &:not(.o-small) {
- grid-template-rows: 40px 1fr 32px;
+ --o-emoji-picker-active: #{$gray-200};
+ .o-active {
+ background-color: var(--o-emoji-picker-active) !important;
}
.o-Emoji {
width: 30px;
- font-size: 1rem;
+ font-size: 0.8rem;
&:hover {
- background-color: $gray-200;
+ background-color: var(--o-emoji-picker-active) !important;
}
}
- &.o-small .o-Emoji {
- width: 40px;
- font-size: 1.5rem;
- }
-
- .o-EmojiPicker-content {
- grid-area: content;
- }
.o-EmojiPicker-navbar {
- grid-area: navbar;
.o-Emoji {
filter: grayscale(1);
}
@@ -43,22 +28,6 @@
filter: grayscale(1);
}
- .o-EmojiPicker-search {
- grid-area: search;
- background-color: $input-bg;
-
- input {
- &:focus, &:hover {
- & + .oi-search {
- @extend .bg-white !optional;
- }
- }
- &:not(:focus) + i.oi-search {
- opacity: 60%;
- }
- }
- }
-
.o-EmojiPicker-empty {
font-size: 5rem !important;
filter: grayscale(0.25);
@@ -69,3 +38,13 @@
// invisible character so that category has constant height, regardless of text content.
content: "\200b"; /* unicode zero width space character */
}
+
+.o-EmojiPicker-search {
+ input {
+ &:not(:focus) {
+ & + .oi-search {
+ @extend .text-muted !optional;
+ }
+ }
+ }
+}
diff --git a/addons/web/static/src/core/emoji_picker/emoji_picker.xml b/addons/web/static/src/core/emoji_picker/emoji_picker.xml
index 3b0fc0ad579..5380a487b29 100644
--- a/addons/web/static/src/core/emoji_picker/emoji_picker.xml
+++ b/addons/web/static/src/core/emoji_picker/emoji_picker.xml
@@ -2,23 +2,28 @@
-
+
😵💫
Failed to load emojis...
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
😢
No emoji match your search
-
+
@@ -31,21 +36,21 @@
-
+
-
+
-