diff --git a/addons/hr_holidays/static/src/im_status_patch.xml b/addons/hr_holidays/static/src/im_status_patch.xml index 31fa8f87373..afc0998a6b5 100644 --- a/addons/hr_holidays/static/src/im_status_patch.xml +++ b/addons/hr_holidays/static/src/im_status_patch.xml @@ -1,10 +1,11 @@ - + - + $0 + diff --git a/addons/hr_holidays/static/tests/im_status_patch_tests.js b/addons/hr_holidays/static/tests/im_status_patch_tests.js index 1f61b6dab3f..33ea30ed881 100644 --- a/addons/hr_holidays/static/tests/im_status_patch_tests.js +++ b/addons/hr_holidays/static/tests/im_status_patch_tests.js @@ -1,61 +1,51 @@ /** @odoo-module **/ -import { UPDATE_BUS_PRESENCE_DELAY } from "@bus/im_status_service"; - import { start, startServer } from "@mail/../tests/helpers/test_utils"; +import { Command } from "@mail/../tests/helpers/command"; QUnit.module("im_status"); QUnit.test("on leave & online", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "leave_online" }); - const channelId = pyEnv["discuss.channel"].create({ name: "General" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - hasTimeControl: true, + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); + const { openDiscuss } = await start(); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i.fa-plane[title='Online']"); }); QUnit.test("on leave & away", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "leave_away" }); - const channelId = pyEnv["discuss.channel"].create({ name: "General" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - hasTimeControl: true, + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); + const { openDiscuss } = await start(); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i.fa-plane[title='Idle']"); }); QUnit.test("on leave & offline", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "leave_offline" }); - const channelId = pyEnv["discuss.channel"].create({ name: "General" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, - }); - const { advanceTime, afterNextRender, openDiscuss } = await start({ - hasTimeControl: true, + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); + const { openDiscuss } = await start(); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i.fa-plane[title='Out of office']"); }); diff --git a/addons/im_livechat/static/src/discuss_app/discuss_patch.xml b/addons/im_livechat/static/src/discuss_app/discuss_patch.xml new file mode 100644 index 00000000000..347509862a2 --- /dev/null +++ b/addons/im_livechat/static/src/discuss_app/discuss_patch.xml @@ -0,0 +1,8 @@ + + + + + thread and thread.model === 'discuss.channel' and thread.type !== 'livechat' + + + diff --git a/addons/mail/controllers/discuss/binary.py b/addons/mail/controllers/discuss/binary.py index bd421c7fea2..2220281bbe3 100644 --- a/addons/mail/controllers/discuss/binary.py +++ b/addons/mail/controllers/discuss/binary.py @@ -75,6 +75,26 @@ class BinaryController(http.Controller): raise NotFound() return request.env["ir.binary"]._get_stream_from(attachment_sudo).get_response(as_attachment=download) + @http.route( + "/discuss/channel//avatar_128", + methods=["GET"], + type="http", + auth="public", + ) + def discuss_channel_avatar_128(self, channel_id): + channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise( + request=request, channel_id=channel_id + ) + domain = [("id", "=", channel_id)] + channel_sudo = channel_member_sudo.env["discuss.channel"].search(domain, limit=1) + if not channel_sudo: + raise NotFound() + return ( + request.env["ir.binary"] + ._get_image_stream_from(channel_sudo, field_name="avatar_128") + .get_response() + ) + @http.route( [ "/discuss/channel//image/", @@ -86,11 +106,11 @@ class BinaryController(http.Controller): ) def fetch_image(self, channel_id, attachment_id, width=0, height=0, **kwargs): channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise( - request=request, channel_id=int(channel_id) + request=request, channel_id=channel_id ) domain = [ - ("id", "=", int(attachment_id)), - ("res_id", "=", int(channel_id)), + ("id", "=", attachment_id), + ("res_id", "=", channel_id), ("res_model", "=", "discuss.channel"), ] attachment_sudo = channel_member_sudo.env["ir.attachment"].search(domain, limit=1) @@ -98,6 +118,6 @@ class BinaryController(http.Controller): raise NotFound() return ( request.env["ir.binary"] - ._get_image_stream_from(attachment_sudo, width=int(width), height=int(height)) + ._get_image_stream_from(attachment_sudo, width=width, height=height) .get_response(as_attachment=kwargs.get("download")) ) diff --git a/addons/mail/controllers/discuss/channel.py b/addons/mail/controllers/discuss/channel.py index 4c5ea52eff6..5c3442b1692 100644 --- a/addons/mail/controllers/discuss/channel.py +++ b/addons/mail/controllers/discuss/channel.py @@ -38,6 +38,13 @@ class ChannelController(http.Controller): except UserError: raise NotFound() + @http.route("/discuss/channel/update_avatar", methods=["POST"], type="json", auth="public") + def discuss_channel_avatar_update(self, channel_id, data): + channel = request.env["discuss.channel"].browse(int(channel_id)).exists() + if not channel or not data: + raise NotFound() + channel.write({"image_128": data}) + @http.route("/discuss/channel/messages", methods=["POST"], type="json", auth="public") def discuss_channel_messages(self, channel_id, before=None, after=None, limit=30, around=None): channel_member_sudo = request.env["discuss.channel.member"]._get_as_sudo_from_request_or_raise( diff --git a/addons/mail/models/discuss/discuss_channel.py b/addons/mail/models/discuss/discuss_channel.py index eaf88aaede6..fff94698ff2 100644 --- a/addons/mail/models/discuss/discuss_channel.py +++ b/addons/mail/models/discuss/discuss_channel.py @@ -54,6 +54,7 @@ class Channel(models.Model): ('group', 'Group')], string='Channel Type', required=True, default='channel', readonly=True, help="Chat is private and unique between 2 persons. Group is private among invited persons. Channel can be freely joined (depending on its configuration).") is_chat = fields.Boolean(string='Is a chat', compute='_compute_is_chat') + is_editable = fields.Boolean('Is Editable', compute='_compute_is_editable') default_display_mode = fields.Selection(string="Default Display Mode", selection=[('video_full_screen', "Full screen video")], help="Determines how the channel will be displayed by default when opening it from its invitation link. No value means display text (no voice/video).") description = fields.Text('Description') image_128 = fields.Image("Image", max_width=128, max_height=128) @@ -109,6 +110,18 @@ class Channel(models.Model): for record in self: record.is_chat = record.channel_type == 'chat' + @api.depends('channel_type', 'is_member') + def _compute_is_editable(self): + for channel in self: + if channel.channel_type == 'channel': + channel.is_editable = self.sudo(False).env.is_admin() or channel.create_uid.id == self.env.user.id + elif channel.channel_type == 'chat': + channel.is_editable = True + elif channel.channel_type == 'group': + channel.is_editable = channel.is_member and not self.sudo(False).env.user._is_public() + else: + channel.is_editable = False + @api.depends('channel_type', 'image_128', 'uuid') def _compute_avatar_128(self): for record in self: @@ -776,6 +789,7 @@ class Channel(models.Model): 'description': channel.description, 'uuid': channel.uuid, 'state': 'open', + 'is_editable': channel.is_editable, 'is_minimized': False, 'group_based_subscription': bool(channel.group_ids), 'create_uid': channel.create_uid.id, diff --git a/addons/mail/static/src/core/thread_model.js b/addons/mail/static/src/core/thread_model.js index 660d0daab52..f9c482ae90b 100644 --- a/addons/mail/static/src/core/thread_model.js +++ b/addons/mail/static/src/core/thread_model.js @@ -124,6 +124,8 @@ export class Thread { last_interest_dt; /** @type {number} */ lastServerMessageId; + /** @type {Boolean} */ + is_editable; constructor(store, data) { Object.assign(this, { @@ -264,12 +266,8 @@ export class Thread { ); } - get isDescriptionChangeable() { - return !this._store.guest && ["channel", "group"].includes(this.type); - } - - get isRenameable() { - return !this._store.guest && ["chat", "channel", "group"].includes(this.type); + get allowDescription() { + return ["channel", "group"].includes(this.type); } get isTransient() { diff --git a/addons/mail/static/src/core/thread_service.js b/addons/mail/static/src/core/thread_service.js index 069fb535742..226d79dbbe1 100644 --- a/addons/mail/static/src/core/thread_service.js +++ b/addons/mail/static/src/core/thread_service.js @@ -699,6 +699,7 @@ export class ThreadService { "type", "group_based_subscription", "last_interest_dt", + "is_editable", "defaultDisplayMode", ]); if (serverData.channel && "message_unread_counter" in serverData.channel) { diff --git a/addons/mail/static/src/core_ui/message.js b/addons/mail/static/src/core_ui/message.js index ce9bd1a2d27..9b1ab2a4971 100644 --- a/addons/mail/static/src/core_ui/message.js +++ b/addons/mail/static/src/core_ui/message.js @@ -465,7 +465,7 @@ export class Message extends Component { } get imStatusClassName() { - let res = "position-absolute"; + let res = "position-absolute bottom-0 end-0"; if (this.hasOpenChatFeature) { res += " cursor-pointer"; } diff --git a/addons/mail/static/src/core_ui/message.xml b/addons/mail/static/src/core_ui/message.xml index 3e36c683d9a..a618927c850 100644 --- a/addons/mail/static/src/core_ui/message.xml +++ b/addons/mail/static/src/core_ui/message.xml @@ -31,7 +31,6 @@ t-att-title="authorText" t-att-src="authorAvatarUrl" t-att-class="{ 'o_redirect cursor-pointer': hasAuthorClickable }"/> - diff --git a/addons/mail/static/src/discuss/web/chat_window_patch.xml b/addons/mail/static/src/discuss/web/chat_window_patch.xml index 8eed76d6875..c668915e69e 100644 --- a/addons/mail/static/src/discuss/web/chat_window_patch.xml +++ b/addons/mail/static/src/discuss/web/chat_window_patch.xml @@ -2,15 +2,15 @@ -
+
-
+
-
+
diff --git a/addons/mail/static/src/discuss_app/autoresize_input.js b/addons/mail/static/src/discuss_app/autoresize_input.js index 56efa0a2d84..401f76bd98b 100644 --- a/addons/mail/static/src/discuss_app/autoresize_input.js +++ b/addons/mail/static/src/discuss_app/autoresize_input.js @@ -8,13 +8,14 @@ export class AutoresizeInput extends Component { static template = "mail.AutoresizeInput"; static props = { className: { type: String, optional: true }, - disabled: { type: Boolean, optional: true }, + enabled: { optional: true }, onValidate: { type: Function, optional: true }, placeholder: { type: String, optional: true }, value: { type: String }, }; static defaultProps = { className: "", + enabled: true, onValidate: () => {}, placeholder: "", }; diff --git a/addons/mail/static/src/discuss_app/autoresize_input.xml b/addons/mail/static/src/discuss_app/autoresize_input.xml index b619da05bfa..6484a15f4ba 100644 --- a/addons/mail/static/src/discuss_app/autoresize_input.xml +++ b/addons/mail/static/src/discuss_app/autoresize_input.xml @@ -3,10 +3,10 @@ { if ( @@ -100,6 +106,12 @@ export class Discuss extends Component { return this.store.threads[this.store.discuss.threadLocalId]; } + get channelAvatar() { + return this.props.public + ? url(`/discuss/channel/${this.thread.id}/avatar_128`) + : this.thread.imgUrl; + } + togglePinMenu() { this.state.activeMode = this.state.activeMode === this.MODES.PINNED_MESSAGES @@ -107,6 +119,11 @@ export class Discuss extends Component { : this.MODES.PINNED_MESSAGES; } + async onFileUploaded(file) { + await this.threadService.updateAvatar(this.thread.id, file.data); + this.notification.add(_t("The avatar has been updated!"), { type: "success" }); + } + async renameThread({ value: name }) { const newName = name.trim(); if ( diff --git a/addons/mail/static/src/discuss_app/discuss.scss b/addons/mail/static/src/discuss_app/discuss.scss index d77b31c6815..376ccd92f61 100644 --- a/addons/mail/static/src/discuss_app/discuss.scss +++ b/addons/mail/static/src/discuss_app/discuss.scss @@ -28,3 +28,24 @@ background-color: $gray-400; } } + +.o-mail-Discuss-threadAvatar { + img { + height: 36px; + width: 36px; + } + + a { + i { + transform: translate(-50%, -50%); + opacity: 0; + } + + &:hover { + background-color: rgba($black, 0.5); + i { + opacity: 1; + } + } + } +} diff --git a/addons/mail/static/src/discuss_app/discuss.xml b/addons/mail/static/src/discuss_app/discuss.xml index 4798502c675..40ed6520a1e 100644 --- a/addons/mail/static/src/discuss_app/discuss.xml +++ b/addons/mail/static/src/discuss_app/discuss.xml @@ -5,22 +5,36 @@
-
- +
+
+ Thread Image + + + + + + + +
+ + + +
- +
Add a description diff --git a/addons/mail/static/src/discuss_app/im_status.js b/addons/mail/static/src/discuss_app/im_status.js index a451c1855b8..434e33d88c1 100644 --- a/addons/mail/static/src/discuss_app/im_status.js +++ b/addons/mail/static/src/discuss_app/im_status.js @@ -3,7 +3,7 @@ import { Component } from "@odoo/owl"; export class ImStatus extends Component { - static props = ["persona", "className?"]; + static props = ["persona", "className?", "style?"]; static template = "mail.ImStatus"; - static defaultProps = { className: "" }; + static defaultProps = { className: "", style: "" }; } diff --git a/addons/mail/static/src/discuss_app/im_status.scss b/addons/mail/static/src/discuss_app/im_status.scss index 2b25307c894..b7894fa293f 100644 --- a/addons/mail/static/src/discuss_app/im_status.scss +++ b/addons/mail/static/src/discuss_app/im_status.scss @@ -1,8 +1,5 @@ .o-mail-ImStatus { - $-gap: $font-size-lg - $font-size-base; - - @include o-position-absolute($right: -$-gap, $bottom: -$-gap); width: $font-size-lg; height: $font-size-lg; } diff --git a/addons/mail/static/src/discuss_app/im_status.xml b/addons/mail/static/src/discuss_app/im_status.xml index 0e8647d122f..f53af536142 100644 --- a/addons/mail/static/src/discuss_app/im_status.xml +++ b/addons/mail/static/src/discuss_app/im_status.xml @@ -2,12 +2,13 @@ -
- +
+ - - - + + + +
diff --git a/addons/mail/static/src/discuss_app/thread_icon.xml b/addons/mail/static/src/discuss_app/thread_icon.xml index 10ccffff1e4..c5b2897d4c7 100644 --- a/addons/mail/static/src/discuss_app/thread_icon.xml +++ b/addons/mail/static/src/discuss_app/thread_icon.xml @@ -14,9 +14,7 @@
- -
- +
diff --git a/addons/mail/static/src/web/chat_window/chat_window.dark.scss b/addons/mail/static/src/web/chat_window/chat_window.dark.scss deleted file mode 100644 index a86542fcd3c..00000000000 --- a/addons/mail/static/src/web/chat_window/chat_window.dark.scss +++ /dev/null @@ -1,3 +0,0 @@ -.o-mail-ChatWindow { - --o-mail-ChatWindow-header-background-color: #{$o-gray-100}; -} \ No newline at end of file diff --git a/addons/mail/static/src/web/chat_window/chat_window.js b/addons/mail/static/src/web/chat_window/chat_window.js index 181985b3f2c..d09826385da 100644 --- a/addons/mail/static/src/web/chat_window/chat_window.js +++ b/addons/mail/static/src/web/chat_window/chat_window.js @@ -10,6 +10,7 @@ import { Component, useChildSubEnv, useRef, useState } from "@odoo/owl"; import { useService } from "@web/core/utils/hooks"; import { localization } from "@web/core/l10n/localization"; import { ThreadIcon } from "@mail/discuss_app/thread_icon"; +import { ImStatus } from "@mail/discuss_app/im_status"; import { isEventHandled } from "@mail/utils/misc"; import { ChannelSelector } from "@mail/discuss_app/channel_selector"; import { PinnedMessagesPanel } from "@mail/discuss_app/pinned_messages_panel"; @@ -28,6 +29,7 @@ export class ChatWindow extends Component { ChannelSelector, Composer, ThreadIcon, + ImStatus, PinnedMessagesPanel, }; static props = ["chatWindow", "right?"]; diff --git a/addons/mail/static/src/web/chat_window/chat_window.scss b/addons/mail/static/src/web/chat_window/chat_window.scss index 988139f7aa6..70d2eb165e5 100644 --- a/addons/mail/static/src/web/chat_window/chat_window.scss +++ b/addons/mail/static/src/web/chat_window/chat_window.scss @@ -1,21 +1,26 @@ .o-mail-ChatWindow { - height: 460px; + height: 480px; width: $o-mail-ChatWindow-width; z-index: 999; // messaging menu is dropdown (1000) - background-color: var(--o-mail-ChatWindow-header-background-color, #{$o-brand-odoo}); box-shadow: -5px -5px 10px rgba(#000000, 0.09); outline: none; &.o-folded { - height: 36px; + height: 48px; } .o-mail-ChatWindow-command:hover { - background-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.075); } } .o-mail-ChatWindow-header { - height: 36px; - color: #ffffff; + height: 48px; + + .o-mail-ChatWindow-threadAvatar { + img { + height: 28px; + width: 28px; + } + } } diff --git a/addons/mail/static/src/web/chat_window/chat_window.xml b/addons/mail/static/src/web/chat_window/chat_window.xml index 35057606aba..b970743ed83 100644 --- a/addons/mail/static/src/web/chat_window/chat_window.xml +++ b/addons/mail/static/src/web/chat_window/chat_window.xml @@ -13,11 +13,17 @@ t-on-keydown.stop="onKeydown" tabindex="1" > -
+
- +
+ Thread Image +
+ + + + )
-
+
-
+
-
+
-
+
diff --git a/addons/mail/static/src/web/messaging_menu/messaging_menu.xml b/addons/mail/static/src/web/messaging_menu/messaging_menu.xml index e52c9d28d88..e228c696f24 100644 --- a/addons/mail/static/src/web/messaging_menu/messaging_menu.xml +++ b/addons/mail/static/src/web/messaging_menu/messaging_menu.xml @@ -57,7 +57,7 @@ onClick="() => notification.requestPermission()" > - + @@ -89,7 +89,7 @@ onSwipeLeft="hasTouch() and threadService.canUnpin(thread) ? { action: () => this.threadService.unpin(thread), icon: 'fa-times-circle', bgColor: 'bg-danger' } : undefined" > - + diff --git a/addons/mail/static/src/web/thread_service_patch.js b/addons/mail/static/src/web/thread_service_patch.js index f54cf2c8ec7..10df3d3e0d5 100644 --- a/addons/mail/static/src/web/thread_service_patch.js +++ b/addons/mail/static/src/web/thread_service_patch.js @@ -199,6 +199,13 @@ patch(ThreadService.prototype, "mail/web", { } delete this.store.followers[follower.id]; }, + + async updateAvatar(threadId, data) { + return this.rpc("/discuss/channel/update_avatar", { + channel_id: threadId, + data, + }); + }, }); patch(threadService, "mail/web", { diff --git a/addons/mail/static/tests/chat_window/chat_window_tests.js b/addons/mail/static/tests/chat_window/chat_window_tests.js index 66f778056a0..50134d7fd7c 100644 --- a/addons/mail/static/tests/chat_window/chat_window_tests.js +++ b/addons/mail/static/tests/chat_window/chat_window_tests.js @@ -100,7 +100,7 @@ QUnit.test("chat window: basic rendering", async (assert) => { await click(".o-mail-NotificationItem"); assert.containsOnce($, ".o-mail-ChatWindow"); assert.containsOnce($, ".o-mail-ChatWindow-header"); - assert.containsOnce($(".o-mail-ChatWindow-header"), ".o-mail-ThreadIcon"); + assert.containsOnce($(".o-mail-ChatWindow-header"), ".o-mail-ChatWindow-threadAvatar"); assert.containsOnce($, ".o-mail-ChatWindow-name:contains(General)"); assert.containsN($, ".o-mail-ChatWindow-command", 5); assert.containsOnce($, ".o-mail-ChatWindow-command[title='Start a Call']"); diff --git a/addons/mail/static/tests/crosstab/crosstab_tests.js b/addons/mail/static/tests/crosstab/crosstab_tests.js index e331de1f8f3..3852aed1985 100644 --- a/addons/mail/static/tests/crosstab/crosstab_tests.js +++ b/addons/mail/static/tests/crosstab/crosstab_tests.js @@ -56,7 +56,10 @@ QUnit.test("Delete starred message updates counter", async (assert) => { QUnit.test("Thread rename", async (assert) => { const pyEnv = await startServer(); - const channelId = pyEnv["discuss.channel"].create({ name: "General" }); + const channelId = pyEnv["discuss.channel"].create({ + create_uid: pyEnv.currentPartnerId, + name: "General", + }); const tab1 = await start({ asTab: true }); const tab2 = await start({ asTab: true }); await tab1.openDiscuss(channelId); @@ -69,7 +72,10 @@ QUnit.test("Thread rename", async (assert) => { QUnit.test("Thread description update", async (assert) => { const pyEnv = await startServer(); - const channelId = pyEnv["discuss.channel"].create({ name: "General" }); + const channelId = pyEnv["discuss.channel"].create({ + create_uid: pyEnv.currentPartnerId, + name: "General", + }); const tab1 = await start({ asTab: true }); const tab2 = await start({ asTab: true }); await tab1.openDiscuss(channelId); diff --git a/addons/mail/static/tests/discuss_app/discuss_tests.js b/addons/mail/static/tests/discuss_app/discuss_tests.js index 12443fdd6a8..7e4a5a12b32 100644 --- a/addons/mail/static/tests/discuss_app/discuss_tests.js +++ b/addons/mail/static/tests/discuss_app/discuss_tests.js @@ -827,22 +827,25 @@ QUnit.test("rendering of inbox message", async (assert) => { assert.containsOnce($message, "[title='Mark as Read']"); }); -QUnit.test('Unfollow message', async function (assert) { +QUnit.test("Unfollow message", async function (assert) { assert.expect(12); const pyEnv = await startServer(); const currentPartnerId = pyEnv.currentPartnerId; - const [threadFollowedId, threadNotFollowedId] = pyEnv["res.partner"].create([{ - name: 'Thread followed', - }, { - name: 'Thread not followed', - }]); + const [threadFollowedId, threadNotFollowedId] = pyEnv["res.partner"].create([ + { + name: "Thread followed", + }, + { + name: "Thread not followed", + }, + ]); pyEnv["mail.followers"].create({ partner_id: currentPartnerId, res_id: threadFollowedId, res_model: "res.partner", }); - for (let threadId of [threadFollowedId, threadFollowedId, threadNotFollowedId]) { + for (const threadId of [threadFollowedId, threadFollowedId, threadNotFollowedId]) { const messageId = pyEnv["mail.message"].create({ body: "not empty", model: "res.partner", @@ -861,14 +864,17 @@ QUnit.test('Unfollow message', async function (assert) { await openDiscuss(); // 2 messages about "Thread followed" with unfollow button and 1 message about "Thread not followed" without it assert.containsN($, ".o-mail-Message", 3); - for (let messageN of [0, 1]) { + for (const messageN of [0, 1]) { const $message = $(`.o-mail-Message:eq(${messageN})`); assert.containsOnce($message, ".o-mail-Message-header:contains(on Thread followed)"); await afterNextRender(() => $message.find("[title='Expand']").click()); assert.containsOnce($message, "[title='Unfollow']"); } const $messageNotFollowed = $(".o-mail-Message:eq(2)"); - assert.containsOnce($messageNotFollowed, ".o-mail-Message-header:contains(on Thread not followed)"); + assert.containsOnce( + $messageNotFollowed, + ".o-mail-Message-header:contains(on Thread not followed)" + ); await afterNextRender(() => $messageNotFollowed.find("[title='Expand']").click()); assert.containsNone($messageNotFollowed, "[title='Unfollow']"); @@ -876,14 +882,22 @@ QUnit.test('Unfollow message', async function (assert) { await afterNextRender(() => $message0Followed.find("[title='Expand']").click()); await afterNextRender(() => $message0Followed.find("[title='Unfollow']").click()); - assert.containsN($, ".o-mail-Message", 2, "Unfollowing message 0 marks it as read -> Message removed"); + assert.containsN( + $, + ".o-mail-Message", + 2, + "Unfollowing message 0 marks it as read -> Message removed" + ); assert.containsOnce($, ".o-mail-Message-header:contains(on Thread followed)"); assert.containsOnce($, ".o-mail-Message-header:contains(on Thread not followed)"); - for (let messageN of [0, 1]) { + for (const messageN of [0, 1]) { const $message = $(`.o-mail-Message:eq(${messageN})`); await afterNextRender(() => $message.find("[title='Expand']").click()); - assert.containsNone($, "button[title='Unfollow']", - "Unfollowing message 0 -> unfollowing 'Thread followed' -> no more unfollow action on any message"); + assert.containsNone( + $, + "button[title='Unfollow']", + "Unfollowing message 0 -> unfollowing 'Thread followed' -> no more unfollow action on any message" + ); } }); @@ -1301,7 +1315,7 @@ QUnit.test( ); QUnit.test( - "'Hashtag' thread icon is displayed in top bar of channels of type 'channel' limited to a group", + "Thread avatar image is displayed in top bar of channels of type 'channel' limited to a group", async (assert) => { const pyEnv = await startServer(); const groupId = pyEnv["res.groups"].create({ name: "testGroup" }); @@ -1312,12 +1326,12 @@ QUnit.test( }); const { openDiscuss } = await start(); await openDiscuss(channelId); - assert.containsOnce($, ".o-mail-Discuss-content .fa-hashtag"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Discuss-threadAvatar"); } ); QUnit.test( - "'Globe' thread icon is displayed in top bar of channels of type 'channel' not limited to any group", + "Thread avatar image is displayed in top bar of channels of type 'channel' not limited to any group", async (assert) => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ @@ -1327,7 +1341,7 @@ QUnit.test( }); const { openDiscuss } = await start(); await openDiscuss(channelId); - assert.containsOnce($, ".o-mail-Discuss-content .fa-globe"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Discuss-threadAvatar"); } ); @@ -1338,7 +1352,7 @@ QUnit.test( const [partnerId_1, partnerId_2, partnerId_3, partnerId_4] = pyEnv["res.partner"].create([ { im_status: "online", name: "Michel Online" }, { im_status: "offline", name: "Jacqueline Offline" }, - { im_status: "away", name: "Nabuchodonosor Away" }, + { im_status: "away", name: "Nabuchodonosor Idle" }, { im_status: "im_partner", name: "Robert Fired" }, ]); pyEnv["discuss.channel"].create([ @@ -1381,29 +1395,29 @@ QUnit.test( const { openDiscuss } = await start(); await openDiscuss(); await click(".o-mail-DiscussCategoryItem:contains('Michel Online')"); - assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Online']"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Online']"); await click(".o-mail-DiscussCategoryItem:contains('Jacqueline Offline')"); - assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Offline']"); - await click(".o-mail-DiscussCategoryItem:contains('Nabuchodonosor Away')"); - assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Away']"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Offline']"); + await click(".o-mail-DiscussCategoryItem:contains('Nabuchodonosor Idle')"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Idle']"); await click(".o-mail-DiscussCategoryItem:contains('Robert Fired')"); assert.containsOnce( $, - ".o-mail-Discuss-header .o-mail-ThreadIcon [title='No IM status available']" + ".o-mail-Discuss-header .o-mail-ImStatus [title='No IM status available']" ); await click(".o-mail-DiscussCategoryItem:contains('OdooBot')"); - assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ThreadIcon [title='Bot']"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-ImStatus [title='Bot']"); } ); QUnit.test( - "'Users' thread icon is displayed in top bar of channels of type 'group'", + "Thread avatar image is displayed in top bar of channels of type 'group'", async (assert) => { const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ channel_type: "group" }); const { openDiscuss } = await start(); await openDiscuss(channelId); - assert.containsOnce($, ".o-mail-Discuss-header .fa-users[title='Grouped Chat']"); + assert.containsOnce($, ".o-mail-Discuss-header .o-mail-Discuss-threadAvatar"); } ); @@ -1430,7 +1444,10 @@ QUnit.test( "Do not trigger channel description server update when channel has no description and editing to empty description", async (assert) => { const pyEnv = await startServer(); - const channelId = pyEnv["discuss.channel"].create({ name: "General" }); + const channelId = pyEnv["discuss.channel"].create({ + create_uid: pyEnv.currentPartnerId, + name: "General", + }); const { openDiscuss } = await start({ mockRPC(route, args, originalRPC) { if (args.method === "channel_change_description") { diff --git a/addons/mail/static/tests/discuss_app/im_status_tests.js b/addons/mail/static/tests/discuss_app/im_status_tests.js index 856d5c3e15e..332b14e0a9e 100644 --- a/addons/mail/static/tests/discuss_app/im_status_tests.js +++ b/addons/mail/static/tests/discuss_app/im_status_tests.js @@ -11,65 +11,61 @@ QUnit.module("im status"); QUnit.test("initially online", async (assert) => { const pyEnv = await startServer(); - const partnerId = pyEnv["res.partner"].create({ im_status: "online" }); - const channelId = pyEnv["discuss.channel"].create({ name: "TestChanel" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, + const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "online" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); - const { advanceTime, openDiscuss } = await start({ hasTimeControl: true }); + const { openDiscuss } = await start(); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i[title='Online']"); }); QUnit.test("initially offline", async (assert) => { const pyEnv = await startServer(); - const partnerId = pyEnv["res.partner"].create({ im_status: "offline" }); - const channelId = pyEnv["discuss.channel"].create({ name: "TestChannel" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, + const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "offline" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); - const { advanceTime, openDiscuss } = await start({ hasTimeControl: true }); + const { openDiscuss } = await start(); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i[title='Offline']"); }); QUnit.test("initially away", async (assert) => { const pyEnv = await startServer(); - const partnerId = pyEnv["res.partner"].create({ im_status: "away" }); - const channelId = pyEnv["discuss.channel"].create({ name: "TestChanel" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, + const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "away" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); - const { advanceTime, openDiscuss } = await start({ hasTimeControl: true }); + const { openDiscuss } = await start(); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i[title='Idle']"); }); QUnit.test("change icon on change partner im_status", async (assert) => { const pyEnv = await startServer(); - const partnerId = pyEnv["res.partner"].create({ im_status: "online" }); - const channelId = pyEnv["discuss.channel"].create({ name: "TestChannel" }); - pyEnv["mail.message"].create({ - author_id: partnerId, - body: "not empty", - model: "discuss.channel", - res_id: channelId, + const partnerId = pyEnv["res.partner"].create({ name: "Demo", im_status: "online" }); + const channelId = pyEnv["discuss.channel"].create({ + channel_member_ids: [ + Command.create({ partner_id: pyEnv.currentPartnerId }), + Command.create({ partner_id: partnerId }), + ], + channel_type: "chat", }); const { advanceTime, openDiscuss } = await start({ hasTimeControl: true }); await openDiscuss(channelId); - await afterNextRender(() => advanceTime(UPDATE_BUS_PRESENCE_DELAY)); assert.containsOnce($, ".o-mail-ImStatus i[title='Online']"); pyEnv["res.partner"].write([partnerId], { im_status: "offline" }); diff --git a/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js b/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js index 2ee5e600f6c..6007c5bcbfa 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js +++ b/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js @@ -628,6 +628,22 @@ patch(MockServer.prototype, "mail/models/discuss_channel", { ], ]; } + let is_editable; + switch (channel.channel_type) { + case "channel": + is_editable = channel.create_uid === this.pyEnv.currentPartnerId; + break; + case "chat": + is_editable = true; + break; + case "group": + is_editable = memberOfCurrentUser; + break; + default: + is_editable = false; + break; + } + res.is_editable = is_editable; res["rtcSessions"] = [ [ "insert", diff --git a/addons/mail/static/tests/message/message_tests.js b/addons/mail/static/tests/message/message_tests.js index 244083337cd..cfba345848f 100644 --- a/addons/mail/static/tests/message/message_tests.js +++ b/addons/mail/static/tests/message/message_tests.js @@ -1134,36 +1134,6 @@ QUnit.test("chat with author should be opened after clicking on their name", asy assert.containsOnce($, ".o-mail-ChatWindow-header:contains(Demo User)"); }); -QUnit.test( - "chat with author should be opened after clicking on their im status icon", - async (assert) => { - const pyEnv = await startServer(); - const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([ - { name: "Partner_1" }, - { name: "Partner_2", im_status: "online" }, - ]); - pyEnv["res.users"].create({ - im_status: "online", - partner_id: partnerId_2, - }); - pyEnv["mail.message"].create({ - author_id: partnerId_2, - body: "not empty", - model: "res.partner", - res_id: partnerId_1, - }); - const { advanceTime, openFormView } = await start({ hasTimeControl: true }); - await openFormView("res.partner", partnerId_1); - await afterNextRender(() => advanceTime(50 * 1000)); // next fetch of im_status - assert.containsOnce($, ".o-mail-ImStatus"); - assert.hasClass($(".o-mail-ImStatus"), "cursor-pointer"); - - await click(".o-mail-ImStatus"); - assert.containsOnce($, ".o-mail-ChatWindow"); - assert.containsOnce($, ".o-mail-ChatWindow-header:contains(Partner_2)"); - } -); - QUnit.test( "subtype description should be displayed if it is different than body", async (assert) => { diff --git a/addons/mail/views/discuss_channel_views.xml b/addons/mail/views/discuss_channel_views.xml index efb29bd74e5..a7214a89f6e 100644 --- a/addons/mail/views/discuss_channel_views.xml +++ b/addons/mail/views/discuss_channel_views.xml @@ -51,6 +51,7 @@
+
diff --git a/addons/test_discuss_full/tests/test_performance.py b/addons/test_discuss_full/tests/test_performance.py index 2adc05d964e..9e075510df9 100644 --- a/addons/test_discuss_full/tests/test_performance.py +++ b/addons/test_discuss_full/tests/test_performance.py @@ -150,6 +150,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': True, 'id': self.channel_general.id, 'invitedMembers': [('insert', [])], + 'is_editable': False, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_general.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -199,6 +200,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_channel_public_1.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_channel_public_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -248,6 +250,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_channel_public_2.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_channel_public_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -297,6 +300,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_channel_group_1.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_channel_group_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -346,6 +350,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_channel_group_2.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_channel_group_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -417,6 +422,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_group_1.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_group_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -502,6 +508,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_chat_1.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_chat_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -587,6 +594,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_chat_2.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_chat_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -672,6 +680,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_chat_3.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_chat_3.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -757,6 +766,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_chat_4.id, 'invitedMembers': [('insert', [])], + 'is_editable': True, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_chat_4.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -836,6 +846,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_livechat_1.id, 'invitedMembers': [('insert', [])], + 'is_editable': False, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_livechat_1.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), @@ -915,6 +926,7 @@ class TestDiscussFullPerformance(TransactionCase): 'group_based_subscription': False, 'id': self.channel_livechat_2.id, 'invitedMembers': [('insert', [])], + 'is_editable': False, 'is_minimized': False, 'is_pinned': True, 'last_interest_dt': self.channel_livechat_2.channel_member_ids.filtered(lambda m: m.partner_id == self.users[0].partner_id).last_interest_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT), diff --git a/addons/web/static/src/views/fields/file_handler.js b/addons/web/static/src/views/fields/file_handler.js index 1d8f64c7c0c..dfd0255c4be 100644 --- a/addons/web/static/src/views/fields/file_handler.js +++ b/addons/web/static/src/views/fields/file_handler.js @@ -92,4 +92,8 @@ FileUploader.props = { fileUploadClass: { type: String, optional: true }, acceptedFileExtensions: { type: String, optional: true }, slots: { type: Object, optional: true }, + showUploadingText: { type: Boolean, optional: true }, +}; +FileUploader.defaultProps = { + showUploadingText: true, }; diff --git a/addons/web/static/src/views/fields/file_handler.xml b/addons/web/static/src/views/fields/file_handler.xml index 6cf817bc9ed..58d9d89e631 100644 --- a/addons/web/static/src/views/fields/file_handler.xml +++ b/addons/web/static/src/views/fields/file_handler.xml @@ -2,7 +2,7 @@ - Uploading... + Uploading...