stream-chat-react icon indicating copy to clipboard operation
stream-chat-react copied to clipboard

bug: Confusion on allowNewMessagesFromUnfilteredChannels prop in ChannelList component

Open jawakarD opened this issue 2 years ago • 15 comments

Describe the bug

As the name suggests, when allowNewMessagesFromUnfilteredChannels is false, my thought process is when a new message arrives on a channel which is filtered out by the filters prop passes to ChannelList, the channel shouldn't pop up to the top of the list.

When looked into the code, if allowNewMessagesFromUnfilteredChannels is false, we just skip the whole step of popping the channel to the top. But the actual condition should check if the channel passes the filter that is being used, if it does, pop the channel to top, if not, don't. Right? So the channel which is not loaded yet just because of pagination should always be popped to the top even if allowNewMessagesFromUnfilteredChannels is false.

To Reproduce

Steps to reproduce the behavior:

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior

In Attatched screenrecording:

  • The first screen you see is the admin side with filtered channel list of one customer
  • Second screen you see a customer view
  • In admin side I have the the chat view of one customer - Akshay
  • I go to the customer side which is not Akshay, but a different customer Jawakar
  • From the customer side I send a message
  • In the admin side I don't expect the Jawakar's channel to popup because, that doesn't match the filter I have passed on to the channel list

I tried to fix this by passing allowNewMessagesFromUnfilteredChannels as false but now even if new message arrives for the same customer the admin has the chat open but the channels is not loaded yet because of pagination It doesn't pop to the top.

Screenshots

https://github.com/GetStream/stream-chat-react/assets/31589372/64dcf72d-20e8-457e-87e2-615f94a8fa3c

Package version

  • "stream-chat": "^8.9.0",
  • "stream-chat-react": "^10.8.3",

Desktop (please complete the following information):

  • OS: MacOs
  • Browser: Firefox
  • Version : 13.0
  • Firefox Version: 116.0

Additional context

This might also be the expected behavious from Stream, but it feels like it doesn't make sense. If the existing prop doesn't do what I expect, should there be a different prop which does this?

jawakarD avatar Aug 16 '23 10:08 jawakarD

@jawakarD thank you for raising this question. The filters provided to the ChannelList component are used to query the data from the database. They cannot be used on the front-end.

The prop allowNewMessagesFromUnfilteredChannels you mention leads to ignoring of WS events that refer to a new message being delivered to a channel that is not among those loaded. It literally does not allow new messages from unfiltered channels.

Therefore we do not intend to change its significance.

Could you maybe describe, whether there is a behavior you would like to achieve, but are not able?

Thank you

MartinCupela avatar Aug 16 '23 15:08 MartinCupela

Thank you for explaining @MartinCupela

Could you maybe describe, whether there is a behavior you would like to achieve, but are not able?

Sure, if new message delivered to the channel which doesn't match the filter applied to the list, I don't want the new channel to appear on the list? Is it possible somehow?

jawakarD avatar Aug 17 '23 05:08 jawakarD

@jawakarD the client does not parse the filter. The filter is used for the HTTP request payload that is later parsed by a DB.

Could you share an example of a filter you have in mind?

MartinCupela avatar Aug 17 '23 12:08 MartinCupela

Could you share an example of a filter you have in mind?

Yes, in our react web app we have two views.

Admin view and Client view. Client can have any number of users and any number of channels.

In admin view, an admin can view each client, for example /crm/<client_id> and the chat page of the respective client like /crm/<client_id>/chats/.

That chat page will only show the channels of that client, even though the logged in admin has access to all the channels in the whole platform.

Now if new message arrives on any channel, the admin view of the client chat, is flooding with other client channels.

jawakarD avatar Aug 18 '23 06:08 jawakarD

@jawakarD then you probably just need to filter channels, that you do not wish to display in the list. You can provide a filter function to ChanellList's prop channelRenderFilterFn. The fuction will receive an array of all loaded channels.

MartinCupela avatar Aug 18 '23 11:08 MartinCupela

But I will end up building the same filtering mechanism that filters prop provides, no? (Even though It's used by the backend)

What I explained is one of the scenario, if I'm using the ChnnelList is multiple places with custom filters prop, then this issue will crop up there also.

Building the channelRenderFilterFn for all of those places seems unscalable.

I feel like what I'm expecting is reasonable, if I'm using a filter i don't expect other channels to pop up there right?

I'm happy to help, if I have to get on a call to explain more.

jawakarD avatar Aug 20 '23 18:08 jawakarD

The function allows you to ignore the updates invoked by WS events to channels, that you do not want to reflect.

I feel like what I'm expecting is reasonable, if I'm using a filter i don't expect other channels to pop up there right?

If you request to watch all those channels, you will be getting WS for all of them.

Could you please share the filters and options object that you pass to respective props of th ChannelList component?

MartinCupela avatar Aug 21 '23 09:08 MartinCupela

If you request to watch all those channels, you will be getting WS for all of them.

I'll look into watchers, how to stop watching channels that are not passing the filter. Haven't looked into this much.

Could you please share the filters and options object that you pass to respective props of th ChannelList component?

Sure.


Default

 const filters = filterFromProps || {
    type: "messaging",
    members: { $in: [chatMemberId] },
  };

Where we get the customer channels

 const filters: ChannelFilters<DefaultStreamChatGenerics> = useMemo(() => {
    const filters: ChannelFilters<DefaultStreamChatGenerics> = {
      type: "messaging",
      $and: [
        {
          members: { $in: groupUserIds },
        },
        { members: { $in: [client.userID!] } },
      ],
    };

    return filters;
  }, [client.userID, groupUserIds]);

Where we filter by some our meta data and the default filter

const filters = {
        type: "messaging",
        assigned_agent: authtoken.uuid,
        members: { $in: [client.userID!] },
      }
const filters = {
        type: "messaging",
        custom_type: ANNOUNCEMENTS_CHANNEL,
        members: { $in: [client.userID!] },
      }
const filters = {
              type: "messaging",
              ...(isCustomer
                ? {}
                : {
                    custom_type: {
                      $nin: [ANNOUNCEMENTS_CHANNEL],
                    },
                  }),

              members: { $in: [client.userID!] },
            }
const filters = {
              type: "messaging",
              needs_reply: true,
              members: { $in: [client.userID!] }
}
            

Sort

 let sort: ChannelSort = { last_message_at: unreplied ? 1 : -1 };

  if (isUnreadFilterActive) {
    sort = {
      has_unread: -1,
    };
  }

options we don't use much.

options={{ limit: 10 }}

We also expect the filters to expand later.

jawakarD avatar Aug 21 '23 11:08 jawakarD

For admin user to impersonate a customer you could use filters:

const filters = {
        type: "messaging",
        members: { $in: [customer.userID!] },
      }

That way you will be getting updates for the given customer. Would that work for you?

MartinCupela avatar Aug 22 '23 08:08 MartinCupela

Yup that's what we're trying to achieve here. But couldn't get it working with what we have. I'll explain more on the product level.

Our product is a three way product. Customer group, admins and providers.

Each customer group will have multiple people.

So a cusomer gorup can have

  • Channels with Customers and admins (eg: "Group A - General help")
  • Channels with Cusomers, admins and providers ("Group A - Task A")
  • Channels with Admins and provider ("Group A - Task A - Private")

Both admins and providers can impersonate customer channels. Admin view all the channels. But provider can only see channels they have access to.

To achieve this, this is what we ended up with.

 const filters: ChannelFilters<DefaultStreamChatGenerics> = useMemo(() => {
    const filters: ChannelFilters<DefaultStreamChatGenerics> = {
      type: "messaging",
      $and: [
        {
          members: { $in: groupUserIds },
        },
        { members: { $in: [client.userID!] } },
      ],
    };

    return filters;
  }, [client.userID, groupUserIds]);

We filter with groupUserIds which are customers of a group and client.userID the current logged in admin/provider

Filter can be

{
  $and: { $in: [user1, user2] }, { $in: [adminUser/providerUser] }
}

So if there is a message in a channel which doesn't qualify in the first object of $and and has the member structure as [user3, user4, user5], then also the channel pops up in the admin/provider view.

jawakarD avatar Aug 23 '23 10:08 jawakarD

Any update here folks?

jawakarD avatar Aug 28 '23 06:08 jawakarD

Nudge!

jawakarD avatar Sep 05 '23 11:09 jawakarD

Also interested.

mjd avatar Sep 06 '23 20:09 mjd

Workaround: Copy ChannelList.tsx and use this funciton before updating the channels.

import { ChannelFilters, Event, StreamChat } from "stream-chat";
import { DefaultStreamChatGenerics } from "stream-chat-react/dist/types/types";

export const shouldChannelListUpdate = async <
  StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>({
  client,
  event,
  filters = {},
}: {
  client: StreamChat<StreamChatGenerics>;
  event: Event<StreamChatGenerics>;
  filters?: ChannelFilters<StreamChatGenerics>;
}) => {
  let update = true;

  try {
    const [queriedChannel] = await client.queryChannels(
      {
        cid: event.cid,
        ...filters,
      },
      [],
      { limit: 1 }
    );

    if (queriedChannel.cid !== event.cid) {
      update = false;
    }
  } catch (error) {
    update = false;
  }

  return update;
};

Example usage in a hook

import { useEffect } from "react";
import uniqBy from "lodash.uniqby";
import { Channel, ChannelFilters, Event } from "stream-chat";
import { useChatContext, getChannel } from "stream-chat-react";
import { DefaultStreamChatGenerics } from "stream-chat-react/dist/types/types";
import { shouldChannelListUpdate } from "utils/shouldChannelListUpdate";

export const useNotificationAddedToChannelListener = <
  StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
>(
  setChannels: React.Dispatch<
    React.SetStateAction<Array<Channel<StreamChatGenerics>>>
  >,
  customHandler?: (
    setChannels: React.Dispatch<
      React.SetStateAction<Array<Channel<StreamChatGenerics>>>
    >,
    event: Event<StreamChatGenerics>
  ) => void,
  allowNewMessagesFromUnfilteredChannels = true,
  filters?: ChannelFilters<StreamChatGenerics>
) => {
  const { client } = useChatContext<StreamChatGenerics>(
    "useNotificationAddedToChannelListener"
  );

  const comparableFilter = JSON.stringify(filters);

  useEffect(() => {
    const handleEvent = async (event: Event<StreamChatGenerics>) => {
      const update = await shouldChannelListUpdate({ client, event, filters });

      if (customHandler && typeof customHandler === "function") {
        customHandler(setChannels, event);
      } else if (
        allowNewMessagesFromUnfilteredChannels &&
        event.channel?.type
      ) {
        const channel = await getChannel(
          client,
          event.channel.type,
          event.channel.id
        );
        setChannels((channels) => {
          if (!update) {
            return channels;
          }

          return uniqBy([channel, ...channels], "cid");
        });
      }
    };

    client.on("notification.added_to_channel", handleEvent);

    return () => {
      client.off("notification.added_to_channel", handleEvent);
    };
  }, [customHandler, comparableFilter]);
};

jawakarD avatar Sep 11 '23 13:09 jawakarD

@jawakarD apologies for the delay. Let me review your proposals in the following days. Thank you

MartinCupela avatar Sep 13 '23 10:09 MartinCupela