diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 517964025b6..4e35b0eb595 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -81,6 +81,7 @@ For more specific needs, you may also assign custom-defined actions 'views/discuss_channel_member_views.xml', 'views/discuss_channel_rtc_session_views.xml', 'views/mail_link_preview_views.xml', + 'views/discuss/discuss_gif_favorite_views.xml', 'views/discuss_channel_views.xml', 'views/mail_shortcode_views.xml', 'views/mail_activity_views.xml', diff --git a/addons/mail/controllers/discuss/__init__.py b/addons/mail/controllers/discuss/__init__.py index e0ae1fd860e..dee3606d2c5 100644 --- a/addons/mail/controllers/discuss/__init__.py +++ b/addons/mail/controllers/discuss/__init__.py @@ -2,5 +2,6 @@ from . import binary from . import channel +from . import gif from . import public_page from . import rtc diff --git a/addons/mail/controllers/discuss/gif.py b/addons/mail/controllers/discuss/gif.py new file mode 100644 index 00000000000..d3ec8160aa4 --- /dev/null +++ b/addons/mail/controllers/discuss/gif.py @@ -0,0 +1,85 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import requests +import werkzeug.urls +from odoo.http import request, route, Controller + + +class DiscussGifController(Controller): + def _request_gifs(self, endpoint): + response = requests.get( + f"https://tenor.googleapis.com/v2/{endpoint}", timeout=3 + ) + response.raise_for_status() + return response + + @route("/discuss/gif/search", type="json", auth="user") + def search(self, search_term, locale="en", country="US", position=None): + ir_config = request.env["ir.config_parameter"].sudo() + query_string = werkzeug.urls.url_encode( + { + "q": search_term, + "key": ir_config.get_param("discuss.tenor_api_key"), + "client_key": request.env.cr.dbname, + "limit": ir_config.get_param("discuss.tenor_gif_limit"), + "contentfilter": ir_config.get_param("discuss.tenor_content_filter"), + "locale": locale, + "country": country, + "media_filter": "tinygif", + "pos": position, + } + ) + response = self._request_gifs(f"search?{query_string}") + if response: + return response.json() + + @route("/discuss/gif/categories", type="json", auth="user") + def categories(self, locale="en", country="US"): + ir_config = request.env["ir.config_parameter"].sudo() + query_string = werkzeug.urls.url_encode( + { + "key": ir_config.get_param("discuss.tenor_api_key"), + "client_key": request.env.cr.dbname, + "limit": ir_config.get_param("discuss.tenor_gif_limit"), + "contentfilter": ir_config.get_param("discuss.tenor_content_filter"), + "locale": locale, + "country": country, + } + ) + response = self._request_gifs(f"categories?{query_string}") + if response: + return response.json() + + @route("/discuss/gif/add_favorite", type="json", auth="user") + def add_favorite(self, tenor_gif_id): + request.env["discuss.gif.favorite"].create({"tenor_gif_id": tenor_gif_id}) + + def _gif_posts(self, ids): + ir_config = request.env["ir.config_parameter"].sudo() + query_string = werkzeug.urls.url_encode( + { + "ids": ",".join(ids), + "key": ir_config.get_param("discuss.tenor_api_key"), + "client_key": request.env.cr.dbname, + "media_filter": "tinygif", + } + ) + response = self._request_gifs(f"posts?{query_string}") + if response: + return response.json()["results"] + + @route("/discuss/gif/favorites", type="json", auth="user") + def get_favorites(self, offset=0): + tenor_gif_ids = request.env["discuss.gif.favorite"].search( + [("create_uid", "=", request.env.user.id)], limit=20, offset=offset + ) + return (self._gif_posts(tenor_gif_ids.mapped("tenor_gif_id")) or [],) + + @route("/discuss/gif/remove_favorite", type="json", auth="user") + def remove_favorite(self, tenor_gif_id): + request.env["discuss.gif.favorite"].search( + [ + ("create_uid", "=", request.env.user.id), + ("tenor_gif_id", "=", tenor_gif_id), + ] + ).unlink() diff --git a/addons/mail/models/discuss/__init__.py b/addons/mail/models/discuss/__init__.py index 3dd130faf9d..b42d4bc3029 100644 --- a/addons/mail/models/discuss/__init__.py +++ b/addons/mail/models/discuss/__init__.py @@ -7,6 +7,7 @@ from . import mail_message from . import discuss_channel_member from . import discuss_channel_rtc_session from . import discuss_channel +from . import discuss_gif_favorite from . import mail_guest # odoo models diff --git a/addons/mail/models/discuss/discuss_gif_favorite.py b/addons/mail/models/discuss/discuss_gif_favorite.py new file mode 100644 index 00000000000..e6c96f85067 --- /dev/null +++ b/addons/mail/models/discuss/discuss_gif_favorite.py @@ -0,0 +1,18 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class GifFavorite(models.Model): + _name = "discuss.gif.favorite" + _description = "Save favorite GIF from Tenor API" + + tenor_gif_id = fields.Char("GIF id from Tenor", required=True) + + _sql_constraints = [ + ( + "user_gif_favorite", + "unique(create_uid,tenor_gif_id)", + "User should not have duplicated favorite GIF", + ), + ] diff --git a/addons/mail/models/discuss/mail_guest.py b/addons/mail/models/discuss/mail_guest.py index f688e9c0f15..cc19362f504 100644 --- a/addons/mail/models/discuss/mail_guest.py +++ b/addons/mail/models/discuss/mail_guest.py @@ -112,6 +112,7 @@ class MailGuest(models.Model): 'current_partner': False, 'current_user_id': False, 'current_user_settings': False, + 'hasGifPickerFeature': bool(self.env["ir.config_parameter"].sudo().get_param("discuss.tenor_api_key")), 'hasLinkPreviewFeature': self.env['mail.link.preview']._is_link_preview_enabled(), 'initBusId': self.env['bus.bus'].sudo()._bus_last_id(), 'menu_id': False, diff --git a/addons/mail/models/discuss/res_users.py b/addons/mail/models/discuss/res_users.py index bb282379ec8..b127cb226cf 100644 --- a/addons/mail/models/discuss/res_users.py +++ b/addons/mail/models/discuss/res_users.py @@ -52,5 +52,6 @@ class ResUsers(models.Model): self.ensure_one() return { 'channels': self.partner_id._get_channels_as_member().channel_info(), + 'hasGifPickerFeature': bool(self.env["ir.config_parameter"].sudo().get_param("discuss.tenor_api_key")), **super()._init_messaging(), } diff --git a/addons/mail/models/res_config_settings.py b/addons/mail/models/res_config_settings.py index 319dbc207c4..b42e390076d 100644 --- a/addons/mail/models/res_config_settings.py +++ b/addons/mail/models/res_config_settings.py @@ -39,6 +39,27 @@ class ResConfigSettings(models.TransientModel): primary_color = fields.Char(related='company_id.primary_color', string="Header Color", readonly=False) secondary_color = fields.Char(related='company_id.secondary_color', string="Button Color", readonly=False) + tenor_api_key = fields.Char( + 'Tenor API key', + config_parameter='discuss.tenor_api_key', + help="Add a Tenor GIF API key to enable GIFs support. https://developers.google.com/tenor/guides/quickstart#setup", + ) + tenor_content_filter = fields.Selection( + [('high', 'High'), + ('medium', 'Medium'), + ('low', 'Low'), + ('off', 'Off')], + string='Tenor content filter', + help="https://developers.google.com/tenor/guides/content-filtering", + config_parameter='discuss.tenor_content_filter', + default='low', + ) + tenor_gif_limit = fields.Integer( + default=8, + config_parameter='discuss.tenor_gif_limit', + help="Fetch up to the specified number of GIF.", + ) + def _compute_fail_counter(self): previous_date = fields.Datetime.now() - datetime.timedelta(days=30) diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 3123352f48f..89559c8adb1 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -64,3 +64,4 @@ access_res_users_settings_volumes_user,res.users.settings.volumes,model_res_user access_mail_template_reset,access.mail.template.reset,model_mail_template_reset,mail.group_mail_template_editor,1,1,1,1 ir_actions_report_access_user,ir.actions.report.access.user,base.model_ir_actions_report,base.group_user,1,0,0,0 access_mail_link_preview_admin,mail.link.preview.admin,model_mail_link_preview,base.group_erp_manager,1,1,1,1 +access_discuss_gif_favorite,discuss.gif.favorite,model_discuss_gif_favorite,base.group_user,1,1,1,1 diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index 07ce32a7bcc..0cb01ebbfc0 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -27,6 +27,20 @@ [(1, '=', 1)] + + Discuss.gif.favorite: User access + + + [('create_uid', '=', user.id)] + + + + Discuss.gif.favorite: admin full access + + + [(1, '=', 1)] + + discuss.channel.member: write its own entries diff --git a/addons/mail/static/src/core/common/composer.js b/addons/mail/static/src/core/common/composer.js index b922a48a3e3..b0588ce35e1 100644 --- a/addons/mail/static/src/core/common/composer.js +++ b/addons/mail/static/src/core/common/composer.js @@ -3,7 +3,7 @@ import { AttachmentList } from "@mail/core/common/attachment_list"; import { useAttachmentUploader } from "@mail/core/common/attachment_uploader_hook"; import { useDropzone } from "@mail/core/common/dropzone_hook"; -import { useEmojiPicker } from "@mail/core/common/emoji_picker"; +import { EmojiPicker, useEmojiPicker } from "@mail/core/common/emoji_picker"; import { MessageConfirmDialog } from "@mail/core/common/message_confirm_dialog"; import { useMessaging, useStore } from "@mail/core/common/messaging_hook"; import { NavigableList } from "@mail/core/common/navigable_list"; @@ -22,10 +22,12 @@ import { onMounted, useChildSubEnv, useEffect, + useExternalListener, useRef, useState, } from "@odoo/owl"; +import { browser } from "@web/core/browser/browser"; import { _t } from "@web/core/l10n/translation"; import { useService } from "@web/core/utils/hooks"; import { sprintf } from "@web/core/utils/strings"; @@ -54,6 +56,7 @@ const EDIT_CLICK_TYPE = { export class Composer extends Component { static components = { AttachmentList, + EmojiPicker, FileUploader, NavigableList, }; @@ -84,6 +87,10 @@ export class Composer extends Component { send_keybind: this.sendKeybind, }) ); + this.KEYBOARD = { + NONE: "None", + EMOJI: "Emoji", + }; this.messaging = useMessaging(); this.store = useStore(); if (this.allowUpload) { @@ -103,6 +110,7 @@ export class Composer extends Component { this.state = useState({ autofocus: 0, active: true, + keyboard: this.KEYBOARD.NONE, }); this.selection = useSelection({ refName: "textarea", @@ -114,7 +122,7 @@ export class Composer extends Component { !this.isEventTrusted(ev) || isEventHandled(ev, "sidebar.openThread") || isEventHandled(ev, "emoji.selectEmoji") || - isEventHandled(ev, "composer.clickOnAddEmoji") || + isEventHandled(ev, "Composer.onClickAddEmoji") || isEventHandled(ev, "composer.clickOnAddAttachment") || isEventHandled(ev, "composer.selectSuggestion") ); @@ -141,10 +149,26 @@ export class Composer extends Component { useChildSubEnv({ inComposer: true, }); - useEmojiPicker(useRef("emoji-picker"), { - onSelect: (str) => this.addEmoji(str), - onClose: () => this.state.autofocus++, - }); + if (!this.ui.isSmall) { + useEmojiPicker(useRef("emoji-button"), { + onSelect: (str) => this.addEmoji(str), + onClose: () => this.state.autofocus++, + }); + } + useExternalListener( + browser, + "click", + async (ev) => { + if (this.state.keyboard === this.KEYBOARD.NONE) { + return; + } + await new Promise(setTimeout); // let bubbling to catch marked event handled + if (!this.isEventHandledByPicker(ev)) { + this.state.keyboard = this.KEYBOARD.NONE; + } + }, + true + ); useEffect( (focus) => { if (focus && this.ref.el) { @@ -233,6 +257,17 @@ export class Composer extends Component { return this.props.mode === "extended" ? _t("CTRL-Enter") : _t("Enter"); } + /** + * @param {Event} ev + * @returns {boolean} + */ + isEventHandledByPicker(ev) { + return ( + isEventHandled(ev, "Composer.onClickAddEmoji") || + isEventHandled(ev, "EmojiPicker.onClick") + ); + } + get thread() { return this.props.composer.thread ?? null; } @@ -434,7 +469,9 @@ export class Composer extends Component { } onClickAddEmoji(ev) { - markEventHandled(ev, "composer.clickOnAddEmoji"); + markEventHandled(ev, "Composer.onClickAddEmoji"); + this.state.keyboard = + this.state.keyboard === this.KEYBOARD.EMOJI ? this.KEYBOARD.NONE : this.KEYBOARD.EMOJI; } isEventTrusted(ev) { @@ -471,8 +508,6 @@ export class Composer extends Component { async sendMessage() { await this.processMessage(async (value) => { - const thread = - this.props.messageToReplyTo?.message?.originThread ?? this.props.composer.thread; const postData = { attachments: this.props.composer.attachments, isNote: @@ -482,19 +517,37 @@ export class Composer extends Component { cannedResponseIds: [...this.props.composer.cannedResponseIds], parentId: this.props.messageToReplyTo?.message?.id, }; - const message = await this.threadService.post(thread, value, postData); - if (this.props.composer.thread.type === "mailbox") { - this.env.services.notification.add( - sprintf(_t('Message posted on "%s"'), message.originThread.displayName), - { type: "info" } - ); - } - this.suggestion?.clearRawMentions(); - this.suggestion?.clearCannedReponses(); - this.props.messageToReplyTo?.cancel(); + await this._sendMessage(value, postData); }); } + /** + * @typedef postData + * @property {import('@mail/attachments/attachment_model').Attachment[]} attachments + * @property {boolean} isNote + * @property {number} parentId + * @property {import("@mail/composer/composer_model").RawMentions} rawMentions + */ + + /** + * @param {string} value message body + * @param {postData} postData Message meta data info + */ + async _sendMessage(value, postData) { + const thread = + this.props.messageToReplyTo?.message?.originThread ?? this.props.composer.thread; + const message = await this.threadService.post(thread, value, postData); + if (this.props.composer.thread.type === "mailbox") { + this.env.services.notification.add( + sprintf(_t('Message posted on "%s"'), message.originThread.displayName), + { type: "info" } + ); + } + this.suggestion?.clearRawMentions(); + this.suggestion?.clearCannedReponses(); + this.props.messageToReplyTo?.cancel(); + } + async editMessage() { if (this.ref.el.value || this.props.composer.message.attachments.length > 0) { await this.processMessage(async (value) => diff --git a/addons/mail/static/src/core/common/composer.scss b/addons/mail/static/src/core/common/composer.scss index 1c04322c361..35321ab6fb7 100644 --- a/addons/mail/static/src/core/common/composer.scss +++ b/addons/mail/static/src/core/common/composer.scss @@ -40,6 +40,10 @@ } } +.o-mail-Composer-bottomPicker { + height: 60vh; +} + .o-mail-Composer-input { padding-top: 10px; // carefully crafted to have the text in the middle in chat window padding-bottom: 10px; diff --git a/addons/mail/static/src/core/common/composer.xml b/addons/mail/static/src/core/common/composer.xml index f44bfa6a940..224e5d9661a 100644 --- a/addons/mail/static/src/core/common/composer.xml +++ b/addons/mail/static/src/core/common/composer.xml @@ -73,7 +73,7 @@ }" >
-
diff --git a/addons/mail/static/src/core/common/composer_model.js b/addons/mail/static/src/core/common/composer_model.js index e855b70ba2e..cf4b475a111 100644 --- a/addons/mail/static/src/core/common/composer_model.js +++ b/addons/mail/static/src/core/common/composer_model.js @@ -1,10 +1,15 @@ /* @odoo-module */ +/** + * @typedef {{partnerIds: Set, threadIds: Set}} RawMentions + */ + export class Composer { /** @type {import("@mail/core/common/attachment_model").Attachment[]} */ attachments = []; /** @type {import("@mail/core/common/message_model").Message} */ message; + /** @type {RawMentions} */ rawMentions = { partnerIds: new Set(), threadIds: new Set(), diff --git a/addons/mail/static/src/core/common/core.scss b/addons/mail/static/src/core/common/core.scss index bde404061a4..863218ac738 100644 --- a/addons/mail/static/src/core/common/core.scss +++ b/addons/mail/static/src/core/common/core.scss @@ -1,3 +1,7 @@ +.o-bg-black { + background-color: rgba(0, 0, 0, var(--bg-opacity, 1)); +} + .o-bg-inherit { background-color: inherit; } @@ -18,6 +22,10 @@ min-width: 0; } +.o-text-white { + color: #FFF; +} + .o-yellow { color: $yellow; } diff --git a/addons/mail/static/src/core/common/emoji_picker.js b/addons/mail/static/src/core/common/emoji_picker.js index 479d14a3b08..c75cc969cf2 100644 --- a/addons/mail/static/src/core/common/emoji_picker.js +++ b/addons/mail/static/src/core/common/emoji_picker.js @@ -20,6 +20,7 @@ import { _t } from "@web/core/l10n/translation"; import { usePopover } from "@web/core/popover/popover_hook"; import { memoize } from "@web/core/utils/functions"; import { fuzzyLookup } from "@web/core/utils/search"; +import { useService } from "@web/core/utils/hooks"; /** * @@ -124,8 +125,7 @@ export async function loadEmoji() { export const EMOJI_PER_ROW = 9; export class EmojiPicker extends Component { - static props = ["onSelect", "close", "onClose?", "storeScroll?"]; - static defaultProps = { onClose: () => {} }; + static props = ["onSelect", "className?", "close?", "onClose?", "storeScroll?"]; static template = "mail.EmojiPicker"; recent = []; @@ -136,6 +136,7 @@ export class EmojiPicker extends Component { setup() { this.inputRef = useRef("input"); this.gridRef = useRef("emoji-grid"); + this.ui = useState(useService("ui")); this.state = useState({ activeEmojiIndex: 0, categoryId: null, @@ -217,6 +218,7 @@ export class EmojiPicker extends Component { } onClick(ev) { + markEventHandled(ev, "EmojiPicker.onClick"); markEventHandled(ev, "emoji.selectEmoji"); } @@ -260,8 +262,8 @@ export class EmojiPicker extends Component { .click(); break; case "Escape": - this.props.close(); - this.props.onClose(); + this.props.close?.(); + this.props.onClose?.(); ev.stopPropagation(); } } @@ -291,8 +293,8 @@ export class EmojiPicker extends Component { browser.localStorage.setItem("mail.emoji.frequent", JSON.stringify(this.recent)); this.gridRef.el.scrollTop = 0; if (!ev.shiftKey) { - this.props.close(); - this.props.onClose(); + this.props.close?.(); + this.props.onClose?.(); } } diff --git a/addons/mail/static/src/core/common/emoji_picker.scss b/addons/mail/static/src/core/common/emoji_picker.scss index a27f3342abe..fef6a8176c4 100644 --- a/addons/mail/static/src/core/common/emoji_picker.scss +++ b/addons/mail/static/src/core/common/emoji_picker.scss @@ -1,13 +1,19 @@ -.o-mail-EmojiPicker { +.popover .o-mail-EmojiPicker { width: 285px; height: 350px; +} + +.o-mail-EmojiPicker { display: grid; grid-template-areas: - "header" - "search" - "content"; - grid-template-rows: 30px 35px 1fr; + "search" + "content" + "navbar"; + + &:not(.o-small) { + grid-template-rows: 40px 1fr 32px; + } .o-mail-Emoji { width: 30px; @@ -17,12 +23,17 @@ } } + &.o-small .o-mail-Emoji { + width: 40px; + font-size: 1.5rem; + } + .o-mail-EmojiPicker-content { grid-area: content; } - .o-mail-EmojiPicker-header { - grid-area: header; + .o-mail-EmojiPicker-navbar { + grid-area: navbar; .o-mail-Emoji { filter: grayscale(1); } @@ -32,27 +43,19 @@ filter: grayscale(1); } - .o-mail-EmojiPicker-searchContainer { - border: $input-border-width solid $border-color !important; - - &:focus-within { - border-color: $input-focus-border-color !important; - - i { - opacity: 100% !important; - } - } - } - .o-mail-EmojiPicker-search { grid-area: search; - border-radius: 0%; - background-color: $input-focus-bg; + background-color: $input-bg; input { - appearance: none; - color: inherit; - background: inherit; + &:focus, &:hover { + & + .oi-search { + @extend .bg-white !optional; + } + } + &:not(:focus) + i.oi-search { + opacity: 60%; + } } } @@ -60,16 +63,6 @@ font-size: 5rem !important; filter: grayscale(0.25); } - - input:not(:focus) { - &::placeholder { - opacity: 75%; - } - - & + i.oi-search { - opacity: 60%; - } - } } .o-mail-EmojiPicker-category:before { diff --git a/addons/mail/static/src/core/common/emoji_picker.xml b/addons/mail/static/src/core/common/emoji_picker.xml index 36e1faf7ca6..5f6885de8bf 100644 --- a/addons/mail/static/src/core/common/emoji_picker.xml +++ b/addons/mail/static/src/core/common/emoji_picker.xml @@ -2,27 +2,15 @@ -
+
😵‍💫 Failed to load emojis... -
- - - - - - - - -
-
- +
@@ -57,12 +45,22 @@
+
+ + + + + + + + +
- + @@ -73,7 +71,7 @@ - + diff --git a/addons/mail/static/src/core/common/thread_service.js b/addons/mail/static/src/core/common/thread_service.js index efc01a9ce29..058b465d4c3 100644 --- a/addons/mail/static/src/core/common/thread_service.js +++ b/addons/mail/static/src/core/common/thread_service.js @@ -10,7 +10,12 @@ import { replaceArrayWithCompare, } from "@mail/utils/common/arrays"; import { prettifyMessageContent } from "@mail/utils/common/format"; -import { assignDefined, createLocalId, onChange, nullifyClearCommands } from "@mail/utils/common/misc"; +import { + assignDefined, + createLocalId, + onChange, + nullifyClearCommands, +} from "@mail/utils/common/misc"; import { markup } from "@odoo/owl"; @@ -885,7 +890,7 @@ export class ThreadService { async post( thread, body, - { attachments = [], isNote = false, parentId, rawMentions, cannedResponseIds } + { attachments = [], isNote = false, parentId, rawMentions = {}, cannedResponseIds } = {} ) { let tmpMsg; const params = await this.getMessagePostParams({ diff --git a/addons/mail/static/src/discuss/gif_picker/common/composer_patch.js b/addons/mail/static/src/discuss/gif_picker/common/composer_patch.js new file mode 100644 index 00000000000..25b711e6662 --- /dev/null +++ b/addons/mail/static/src/discuss/gif_picker/common/composer_patch.js @@ -0,0 +1,57 @@ +/* @odoo-module */ + +import { GifPicker, useGifPicker } from "@mail/discuss/gif_picker/common/gif_picker"; +import { Composer } from "@mail/core/common/composer"; +import { onExternalClick } from "@mail/utils/common/hooks"; +import { isEventHandled, markEventHandled } from "@mail/utils/common/misc"; + +import { useState } from "@odoo/owl"; + +import { useService } from "@web/core/utils/hooks"; +import { patch } from "@web/core/utils/patch"; + +Object.assign(Composer.components, { GifPicker }); +/** @type {Composer} */ +const composerPatch = { + setup() { + this._super(); + Object.assign(this.KEYBOARD, { GIF: "Gif" }); + onExternalClick("gif-picker", () => (this.state.keyboard = this.KEYBOARD.NONE)); + this.ui = useState(useService("ui")); + /** @type {import('@mail/discuss/gif_picker/common/gif_picker_service').GifPickerService} */ + this.gifPickerService = useService("discuss.gifPicker"); + if (this.gifPickerService.hasGifPickerFeature && !this.ui.isSmall) { + this.gifPicker = useGifPicker("gif-button", { + onSelected: this.sendGifMessage.bind(this), + }); + } + }, + /** + * @param {Event} ev + * @returns {boolean} + */ + isEventHandledByPicker(ev) { + return ( + this._super(ev) || + isEventHandled(ev, "Composer.onClickAddGif") || + isEventHandled(ev, "GifPicker.onClick") + ); + }, + onClickAddGif(ev) { + markEventHandled(ev, "Composer.onClickAddGif"); + this.gifPicker?.toggle(); + if (!this.gifPicker) { + if (this.state.keyboard !== this.KEYBOARD.GIF) { + this.state.keyboard = this.KEYBOARD.GIF; + } else { + this.state.keyboard = this.KEYBOARD.NONE; + } + } + }, + async sendGifMessage(gif) { + await this._sendMessage(gif.url, { + parentId: this.props.messageToReplyTo?.message?.id, + }); + }, +}; +patch(Composer.prototype, "GifPicker", composerPatch); diff --git a/addons/mail/static/src/discuss/gif_picker/common/composer_patch.xml b/addons/mail/static/src/discuss/gif_picker/common/composer_patch.xml new file mode 100644 index 00000000000..a0d38b44303 --- /dev/null +++ b/addons/mail/static/src/discuss/gif_picker/common/composer_patch.xml @@ -0,0 +1,11 @@ + + + + +