[FIX] bus, mail, im_livechat: open discuss from livechat history
The kanban view used to display message history of livechats sessions is replaced by Discuss for a smooth and consistent user experience. Minimum viable version for Task-3497801 closes odoo/odoo#146800 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
parent
9351b2a5eb
commit
bc0b090cd6
@@ -65,6 +65,7 @@ export class WebsocketWorker {
|
||||
this.lastNotificationId = 0;
|
||||
this.messageWaitQueue = [];
|
||||
this._forceUpdateChannels = debounce(this._forceUpdateChannels, 300);
|
||||
this._updateChannels = debounce(this._updateChannels, 0);
|
||||
|
||||
this._onWebsocketClose = this._onWebsocketClose.bind(this);
|
||||
this._onWebsocketError = this._onWebsocketError.bind(this);
|
||||
|
||||
@@ -25,7 +25,7 @@ QUnit.test("notifications received from the channel", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const env = await makeTestEnv({ activateMockServer: true });
|
||||
env.services["bus_service"].addChannel("lambda");
|
||||
await waitForChannels(["lambda"]);
|
||||
await waitUntilSubscribe("lambda");
|
||||
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||||
pyEnv["bus.bus"]._sendone("lambda", "notifType", "epsilon");
|
||||
await waitNotifications([env, "notifType", "beta"], [env, "notifType", "epsilon"]);
|
||||
@@ -39,7 +39,7 @@ QUnit.test("notifications not received after stoping the service", async () => {
|
||||
firstTabEnv.services["bus_service"].start();
|
||||
secondTabEnv.services["bus_service"].start();
|
||||
firstTabEnv.services["bus_service"].addChannel("lambda");
|
||||
await waitForChannels(["lambda"]);
|
||||
await waitUntilSubscribe("lambda");
|
||||
// both tabs should receive the notification
|
||||
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||||
await waitNotifications(
|
||||
@@ -76,7 +76,7 @@ QUnit.test("tabs share message from a channel", async () => {
|
||||
mainEnv.services["bus_service"].addChannel("lambda");
|
||||
const slaveEnv = await makeTestEnv();
|
||||
slaveEnv.services["bus_service"].addChannel("lambda");
|
||||
await waitForChannels(["lambda"]);
|
||||
await waitUntilSubscribe("lambda");
|
||||
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||||
await waitNotifications([mainEnv, "notifType", "beta"], [slaveEnv, "notifType", "beta"]);
|
||||
});
|
||||
@@ -97,7 +97,7 @@ QUnit.test("second tab still receives notifications after main pagehide", async
|
||||
});
|
||||
const secondEnv = await makeTestEnv({ activateMockServer: true });
|
||||
secondEnv.services["bus_service"].addChannel("lambda");
|
||||
await waitForChannels(["lambda"]);
|
||||
await waitUntilSubscribe("lambda");
|
||||
pyEnv["bus.bus"]._sendone("lambda", "notifType", "beta");
|
||||
await waitNotifications([mainEnv, "notifType", "beta"], [secondEnv, "notifType", "beta"]);
|
||||
// simulate unloading main
|
||||
@@ -117,7 +117,7 @@ QUnit.test("two tabs adding a different channel", async () => {
|
||||
const secondTabEnv = await makeTestEnv({ activateMockServer: true });
|
||||
firstTabEnv.services["bus_service"].addChannel("alpha");
|
||||
secondTabEnv.services["bus_service"].addChannel("beta");
|
||||
await waitForChannels(["alpha", "beta"]);
|
||||
await waitUntilSubscribe("alpha", "beta");
|
||||
pyEnv["bus.bus"]._sendmany([
|
||||
["alpha", "notifType", "alpha"],
|
||||
["beta", "notifType", "beta"],
|
||||
@@ -138,8 +138,8 @@ QUnit.test("channel management from multiple tabs", async (assert) => {
|
||||
super._sendToServer(...arguments);
|
||||
},
|
||||
});
|
||||
const firstTabEnv = await makeTestEnv();
|
||||
const secTabEnv = await makeTestEnv();
|
||||
const firstTabEnv = await makeTestEnv({ activateMockServer: true });
|
||||
const secTabEnv = await makeTestEnv({ activateMockServer: true });
|
||||
firstTabEnv.services["bus_service"].addChannel("channel1");
|
||||
await waitForChannels(["channel1"]);
|
||||
// this should not trigger a subscription since the channel1 was
|
||||
@@ -154,7 +154,7 @@ QUnit.test("channel management from multiple tabs", async (assert) => {
|
||||
// this should trigger a subscription since the channel2 was not
|
||||
// known.
|
||||
secTabEnv.services["bus_service"].addChannel("channel2");
|
||||
await waitForChannels(["channel2"]);
|
||||
await waitUntilSubscribe("channel2");
|
||||
assert.verifySteps(["subscribe - [channel1]", "subscribe - [channel1,channel2]"]);
|
||||
});
|
||||
|
||||
@@ -188,7 +188,7 @@ QUnit.test("Last notification id is passed to the worker on service start", asyn
|
||||
const env1 = await makeTestEnv();
|
||||
env1.services["bus_service"].start();
|
||||
env1.services["bus_service"].addChannel("lambda");
|
||||
await waitForChannels(["lambda"]);
|
||||
await waitUntilSubscribe("lambda");
|
||||
await updateLastNotificationDeferred;
|
||||
// First bus service has never received notifications thus the
|
||||
// default is 0.
|
||||
|
||||
+22
-6
@@ -1,20 +1,36 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useState } from "@odoo/owl";
|
||||
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class DiscussChannelListController extends ListController {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.threadService = useService("mail.thread");
|
||||
this.store = useState(useService("mail.store"));
|
||||
this.ui = useState(useService("ui"));
|
||||
}
|
||||
|
||||
async openRecord(record) {
|
||||
if (!record.data.is_member) {
|
||||
return super.openRecord(record);
|
||||
if (!this.ui.isSmall) {
|
||||
return this.actionService.doAction("mail.action_discuss", {
|
||||
name: _t("Discuss"),
|
||||
additionalContext: { active_id: record.resId },
|
||||
});
|
||||
}
|
||||
this.actionService.doAction("mail.action_discuss", {
|
||||
name: _t("Discuss"),
|
||||
additionalContext: { active_id: record.resId },
|
||||
let thread = this.store.Thread.get({
|
||||
model: "discuss.channel",
|
||||
id: record.resId,
|
||||
});
|
||||
if (!thread?.type) {
|
||||
thread = await this.threadService.fetchChannel(record.resId);
|
||||
}
|
||||
if (thread) {
|
||||
return this.threadService.open(thread);
|
||||
}
|
||||
return super.openRecord(record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ChannelController(http.Controller):
|
||||
|
||||
@http.route("/discuss/channel/update_avatar", methods=["POST"], type="json")
|
||||
def discuss_channel_avatar_update(self, channel_id, data):
|
||||
channel = request.env["discuss.channel"].browse(int(channel_id)).exists()
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel or not data:
|
||||
raise NotFound()
|
||||
channel.write({"image_128": data})
|
||||
@@ -64,7 +64,10 @@ class ChannelController(http.Controller):
|
||||
"""Mute notifications for the given number of minutes.
|
||||
:param minutes: (integer) number of minutes to mute notifications, -1 means mute until the user unmutes
|
||||
"""
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise request.not_found()
|
||||
if minutes == -1:
|
||||
@@ -83,7 +86,10 @@ class ChannelController(http.Controller):
|
||||
|
||||
@http.route("/discuss/channel/update_custom_notifications", methods=["POST"], type="json", auth="user")
|
||||
def discuss_channel_update_custom_notifications(self, channel_id, custom_notifications):
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise request.not_found()
|
||||
member.custom_notifications = custom_notifications
|
||||
@@ -105,7 +111,10 @@ class ChannelController(http.Controller):
|
||||
@http.route("/discuss/channel/notify_typing", methods=["POST"], type="json", auth="public")
|
||||
@add_guest_to_context
|
||||
def discuss_channel_notify_typing(self, channel_id, is_typing):
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise NotFound()
|
||||
member._notify_typing(is_typing)
|
||||
|
||||
@@ -64,7 +64,10 @@ class RtcController(http.Controller):
|
||||
"""Joins the RTC call of a channel if the user is a member of that channel
|
||||
:param int channel_id: id of the channel to join
|
||||
"""
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise request.not_found()
|
||||
member = channel._find_or_create_member_for_self()
|
||||
if not member:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel.rtc.session - member of current user can join call
|
||||
@@ -89,11 +92,11 @@ class RtcController(http.Controller):
|
||||
:param member_ids: members whose invitation is to cancel
|
||||
:type member_ids: list(int) or None
|
||||
"""
|
||||
member = request.env["discuss.channel.member"].search([("channel_id", "=", channel_id), ("is_self", "=", True)])
|
||||
if not member:
|
||||
channel = request.env["discuss.channel"].search([("id", "=", channel_id)])
|
||||
if not channel:
|
||||
raise NotFound()
|
||||
# sudo: discuss.channel.rtc.session - member of current user can cancel invitations
|
||||
return member.sudo().channel_id._rtc_cancel_invitations(member_ids=member_ids)
|
||||
# sudo: discuss.channel.rtc.session - can cancel invitations in accessible channel
|
||||
return channel.sudo()._rtc_cancel_invitations(member_ids=member_ids)
|
||||
|
||||
@http.route("/mail/rtc/audio_worklet_processor", methods=["GET"], type="http", auth="public")
|
||||
def audio_worklet_processor(self):
|
||||
|
||||
@@ -345,6 +345,7 @@ class Channel(models.Model):
|
||||
partners = self.env['res.partner'].browse(partner_ids or []).exists()
|
||||
guests = self.env['mail.guest'].browse(guest_ids or []).exists()
|
||||
notifications = []
|
||||
all_new_members = self.env["discuss.channel.member"]
|
||||
for channel in self:
|
||||
members_to_create = []
|
||||
existing_members = self.env['discuss.channel.member'].search(expression.AND([
|
||||
@@ -363,6 +364,7 @@ class Channel(models.Model):
|
||||
'channel_id': channel.id,
|
||||
} for guest in guests - existing_members.guest_id]
|
||||
new_members = self.env['discuss.channel.member'].create(members_to_create)
|
||||
all_new_members += new_members
|
||||
for member in new_members.filtered(lambda member: member.partner_id):
|
||||
# notify invited members through the bus
|
||||
user = member.partner_id.user_ids[0] if member.partner_id.user_ids else self.env['res.users']
|
||||
@@ -416,6 +418,7 @@ class Channel(models.Model):
|
||||
# sudo: discuss.channel.rtc.session - current user can invite new members in call
|
||||
current_channel_member.sudo()._rtc_invite_members(member_ids=new_members.ids)
|
||||
self.env['bus.bus']._sendmany(notifications)
|
||||
return all_new_members
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# RTC
|
||||
@@ -690,6 +693,19 @@ class Channel(models.Model):
|
||||
}
|
||||
self.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
|
||||
|
||||
def _find_or_create_member_for_self(self):
|
||||
self.ensure_one()
|
||||
domain = [("channel_id", "=", self.id), ("is_self", "=", True)]
|
||||
member = self.env["discuss.channel.member"].search(domain)
|
||||
if member:
|
||||
return member
|
||||
if not self.env.user._is_public():
|
||||
return self.add_members(partner_ids=self.env.user.partner_id.ids)
|
||||
guest = self.env["mail.guest"]._get_guest_from_context()
|
||||
if guest:
|
||||
return self.add_members(guest_ids=guest.ids)
|
||||
return self.env["discuss.channel.member"]
|
||||
|
||||
def _find_or_create_persona_for_channel(self, guest_name, timezone, country_code, post_joined_message=True):
|
||||
"""
|
||||
:param channel: channel to add the persona to
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import re
|
||||
|
||||
from odoo import models
|
||||
from odoo.addons.mail.models.discuss.mail_guest import add_guest_to_context
|
||||
|
||||
@@ -22,16 +24,23 @@ class IrWebsocket(models.AbstractModel):
|
||||
@add_guest_to_context
|
||||
def _build_bus_channel_list(self, channels):
|
||||
channels = list(channels) # do not alter original list
|
||||
discuss_channel_ids = list()
|
||||
for channel in list(channels):
|
||||
if isinstance(channel, str) and channel.startswith("mail.guest_"):
|
||||
channels.remove(channel)
|
||||
guest = self.env["mail.guest"]._get_guest_from_token(channel.split("_")[1])
|
||||
if guest:
|
||||
self = self.with_context(guest=guest)
|
||||
if isinstance(channel, str):
|
||||
match = re.findall(r'discuss\.channel_(\d+)', channel)
|
||||
if match:
|
||||
channels.remove(channel)
|
||||
discuss_channel_ids.append(int(match[0]))
|
||||
guest = self.env["mail.guest"]._get_guest_from_context()
|
||||
if guest:
|
||||
channels.append(guest)
|
||||
channels.extend(self.env["discuss.channel"].search([("is_member", "=", True)]))
|
||||
domain = ["|", ("is_member", "=", True), ("id", "in", discuss_channel_ids)]
|
||||
channels.extend(self.env["discuss.channel"].search(domain))
|
||||
return super()._build_bus_channel_list(channels)
|
||||
|
||||
@add_guest_to_context
|
||||
|
||||
@@ -67,6 +67,7 @@ export class ChatWindow extends Record {
|
||||
autofocus = 0;
|
||||
folded = false;
|
||||
hidden = false;
|
||||
openMessagingMenuOnClose = false;
|
||||
|
||||
get displayName() {
|
||||
return this.thread?.displayName ?? _t("New message");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { assignDefined } from "@mail/utils/common/misc";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
@@ -41,12 +43,12 @@ export class ChatWindowService {
|
||||
return chatWindow;
|
||||
}
|
||||
|
||||
openNewMessage() {
|
||||
openNewMessage({ openMessagingMenuOnClose } = {}) {
|
||||
if (this.store.discuss.chatWindows.some(({ thread }) => !thread)) {
|
||||
// New message chat window is already opened.
|
||||
return;
|
||||
}
|
||||
this.store.ChatWindow.insert();
|
||||
this.store.ChatWindow.insert(assignDefined({}, { openMessagingMenuOnClose }));
|
||||
}
|
||||
|
||||
closeNewMessage() {
|
||||
|
||||
@@ -207,8 +207,11 @@ export class Store extends BaseStore {
|
||||
const ids = Object.keys(this.Thread.records).sort(); // Ensure channels processed in same order.
|
||||
for (const id of ids) {
|
||||
const thread = this.Thread.records[id];
|
||||
if (thread.model === "discuss.channel" && thread.hasSelfAsMember) {
|
||||
if (thread.model === "discuss.channel") {
|
||||
channelIds.push(id);
|
||||
if (!thread.hasSelfAsMember) {
|
||||
this.env.services["bus_service"].addChannel(`discuss.channel_${thread.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const channels = JSON.stringify(channelIds);
|
||||
|
||||
@@ -281,6 +281,8 @@ export class Thread extends Record {
|
||||
mute_until_dt;
|
||||
/** @type {Boolean} */
|
||||
isLocallyPinned = false;
|
||||
/** @type {"not_fetched"|"pending"|"fetched"} */
|
||||
fetchMembersState = "not_fetched";
|
||||
|
||||
get accessRestrictedToGroupText() {
|
||||
if (!this.authorizedGroupFullName) {
|
||||
|
||||
@@ -61,11 +61,23 @@ export class ThreadService {
|
||||
}
|
||||
|
||||
async fetchChannelMembers(thread) {
|
||||
if (thread.fetchMembersState === "pending") {
|
||||
return;
|
||||
}
|
||||
const previousState = thread.fetchMembersState;
|
||||
thread.fetchMembersState = "pending";
|
||||
const known_member_ids = thread.channelMembers.map((channelMember) => channelMember.id);
|
||||
const results = await this.rpc("/discuss/channel/members", {
|
||||
channel_id: thread.id,
|
||||
known_member_ids: known_member_ids,
|
||||
});
|
||||
let results;
|
||||
try {
|
||||
results = await this.rpc("/discuss/channel/members", {
|
||||
channel_id: thread.id,
|
||||
known_member_ids: known_member_ids,
|
||||
});
|
||||
} catch (e) {
|
||||
thread.fetchMembersState = previousState;
|
||||
throw e;
|
||||
}
|
||||
thread.fetchMembersState = "fetched";
|
||||
let channelMembers = [];
|
||||
if (
|
||||
results["channelMembers"] &&
|
||||
@@ -438,8 +450,9 @@ export class ThreadService {
|
||||
/**
|
||||
* @param {import("models").Thread} thread
|
||||
* @param {boolean} replaceNewMessageChatWindow
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
open(thread, replaceNewMessageChatWindow) {
|
||||
open(thread, replaceNewMessageChatWindow, options) {
|
||||
this.setDiscussThread(thread);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ChatWindowService.prototype, {
|
||||
async _onClose(chatWindow, options) {
|
||||
if (this.ui.isSmall && !this.store.discuss.isActive) {
|
||||
if (
|
||||
this.ui.isSmall &&
|
||||
!this.store.discuss.isActive &&
|
||||
chatWindow.openMessagingMenuOnClose
|
||||
) {
|
||||
// If we are in mobile and discuss is not open, it means the
|
||||
// chat window was opened from the messaging menu. In that
|
||||
// case it should be re-opened to simulate it was always
|
||||
|
||||
@@ -152,7 +152,7 @@ export class MessagingMenu extends Component {
|
||||
}
|
||||
|
||||
openDiscussion(thread) {
|
||||
this.threadService.open(thread);
|
||||
this.threadService.open(thread, undefined, { openMessagingMenuOnClose: true });
|
||||
this.close();
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ export class MessagingMenu extends Component {
|
||||
if (this.ui.isSmall || this.env.inDiscussApp) {
|
||||
this.state.addingChat = true;
|
||||
} else {
|
||||
this.chatWindowService.openNewMessage();
|
||||
this.chatWindowService.openNewMessage({ openMessagingMenuOnClose: true });
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -191,7 +191,7 @@ export class MessagingMenu extends Component {
|
||||
// and the chat window does not look good.
|
||||
this.store.discuss.chatWindows.find(({ thr }) => thr?.eq(thread))?.close();
|
||||
} else {
|
||||
this.threadService.open(thread);
|
||||
this.threadService.open(thread, undefined, { openMessagingMenuOnClose: true });
|
||||
}
|
||||
this.close();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseEmail } from "@mail/js/utils";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { Record } from "@mail/core/common/record";
|
||||
import { compareDatetime } from "@mail/utils/common/misc";
|
||||
import { assignDefined, compareDatetime } from "@mail/utils/common/misc";
|
||||
|
||||
let nextId = 1;
|
||||
|
||||
@@ -186,13 +186,14 @@ patch(ThreadService.prototype, {
|
||||
}
|
||||
});
|
||||
},
|
||||
open(thread, replaceNewMessageChatWindow) {
|
||||
/** @override */
|
||||
open(thread, replaceNewMessageChatWindow, options) {
|
||||
if (!this.store.discuss.isActive && !this.ui.isSmall) {
|
||||
this._openChatWindow(thread, replaceNewMessageChatWindow);
|
||||
this._openChatWindow(thread, replaceNewMessageChatWindow, options);
|
||||
return;
|
||||
}
|
||||
if (this.ui.isSmall && thread.model === "discuss.channel") {
|
||||
this._openChatWindow(thread, replaceNewMessageChatWindow);
|
||||
this._openChatWindow(thread, replaceNewMessageChatWindow, options);
|
||||
return;
|
||||
}
|
||||
if (thread.model !== "discuss.channel") {
|
||||
@@ -227,12 +228,19 @@ patch(ThreadService.prototype, {
|
||||
}
|
||||
super.unpin(...arguments);
|
||||
},
|
||||
_openChatWindow(thread, replaceNewMessageChatWindow) {
|
||||
const chatWindow = this.store.ChatWindow.insert({
|
||||
folded: false,
|
||||
thread,
|
||||
replaceNewMessageChatWindow,
|
||||
});
|
||||
_openChatWindow(thread, replaceNewMessageChatWindow, { openMessagingMenuOnClose } = {}) {
|
||||
const chatWindow = this.store.ChatWindow.insert(
|
||||
assignDefined(
|
||||
{
|
||||
folded: false,
|
||||
replaceNewMessageChatWindow,
|
||||
thread,
|
||||
},
|
||||
{
|
||||
openMessagingMenuOnClose,
|
||||
}
|
||||
)
|
||||
);
|
||||
chatWindow.autofocus++;
|
||||
if (thread) {
|
||||
thread.state = "open";
|
||||
|
||||
@@ -17,9 +17,13 @@ export class ChannelMemberList extends Component {
|
||||
this.store = useState(useService("mail.store"));
|
||||
this.channelMemberService = useService("discuss.channel.member");
|
||||
this.threadService = useState(useService("mail.thread"));
|
||||
onWillStart(() => this.threadService.fetchChannelMembers(this.props.thread));
|
||||
onWillStart(() => {
|
||||
if (this.props.thread.fetchMembersState === "not_fetched") {
|
||||
this.threadService.fetchChannelMembers(this.props.thread);
|
||||
}
|
||||
});
|
||||
onWillUpdateProps((nextProps) => {
|
||||
if (nextProps.thread.channelMembers.length === 0) {
|
||||
if (nextProps.thread.fetchMembersState === "not_fetched") {
|
||||
this.threadService.fetchChannelMembers(nextProps.thread);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user