Skip to content

Conversation

MartinCupela
Copy link
Contributor

@MartinCupela MartinCupela commented Sep 3, 2025

Goal

Allow integrators to keep multiple parallel channel listings that keep their own pagination state but probably differ in filters.

The feature brings support of:

  1. parsing the query filters
  2. parsing the query sorts
  3. paginator and orchestrator reactive state
  4. support for existing ChannelFilters including filters that do not translate directly from field name to the field value (member.user.name, pinned, members) - custom filter-to-value resolvers
  5. possibility to augment the default WS handler logic (and not having to override the defaults with the copied default + some custom lines) - event handler pipelines

Example use

  1. Register paginators (lists you want to see in the UI) & custom event handlers
const channelPaginatorsOrchestrator = useMemo(() => {
    if (!chatClient) return;
    const defaultEventHandlers = ChannelPaginatorsOrchestrator.getDefaultHandlers();

    return new ChannelPaginatorsOrchestrator({
      client: chatClient,
      paginators: [
        new ChannelPaginator({
          client: chatClient,
          id: 'channels:archived',
          filters: { ...filters, archived: true, pinned: false },
          sort: { last_message_at: -1, updated_at: -1 },
        }),
        new ChannelPaginator({
          client: chatClient,
          id: 'channels:pinned',
          filters: { members: { $in: [userId] }, type: 'messaging', pinned: true },
          sort: { pinned_at: -1, last_message_at: -1, updated_at: -1 },
        }),
        new ChannelPaginator({
          client: chatClient,
          id: 'channels:blocked',
          filters: { ...filters, blocked: true },
          sort,
        }),
        new ChannelPaginator({
          client: chatClient,
          id: 'channels:default',
          filters: { ...filters },
          sort,
        }),
      ],
      eventHandlers: {
        'member.updated': [
          // if you want to keep the defaults - in case they are defined
          ...(defaultEventHandlers?.['member.updated'] ?? []),
          {
            handle: ({ event, ctx: { orchestrator } }) => {
              // custom event handling logic
              // check if the channel matches this paginator's filters and ingest it or remove it
            },
            id: 'custom:channel-list:member.updated',
          },
          // another handler could be added for 'member.updated'
        ],
      },
    });
  }, [chatClient]);
  1. Consume the data in the components (e.g. React):

A list of lists (ChannelLists):

export const ChannelLists = ({ channelPaginatorsOrchestrator }: ChannelListsProps) => {
  const { client } = useChatContext();
  const orchestrator = useMemo(
    () => channelPaginatorsOrchestrator ?? new ChannelPaginatorsOrchestrator({ client }),
    [channelPaginatorsOrchestrator, client],
  );
  const { paginators } = useStateStore(
    orchestrator.state,
    channelPaginatorsOrchestratorStateSelector,
  );

  useEffect(() => orchestrator.registerSubscriptions(), [orchestrator]);

  return (
    <div>
      {paginators.map((paginator) => (
        <ChannelList key={paginator.id} paginator={paginator} />
      ))}
    </div>
  );
};

Specific ChannelList:

export type ChannelListProps = {
  paginator: ChannelPaginator;
  loadMoreDebounceMs?: number;
  loadMoreThresholdPx?: number;
};

const channelPaginatorStateSelector = (state: ChannelPaginatorState) => ({
  channels: state.items,
  hasNext: state.hasNext,
  isLoading: state.isLoading,
  lastQueryError: state.lastQueryError,
});

export const ChannelList = ({
  loadMoreDebounceMs,
  loadMoreThresholdPx,
  paginator,
}: ChannelListProps) => {
  const { client } = useChatContext();
  const { t } = useTranslationContext();
  const {
    channels = [],
    hasNext,
    isLoading,
    lastQueryError,
  } = useStateStore(paginator.state, channelPaginatorStateSelector);
  //const { ChannelListLoadingIndicator, ChannelPreview, EmptyChannelList } =  useComponentContext();

  useEffect(() => {
    if (paginator.items) return;
    paginator.nextDebounced();
  }, [paginator]);

  useEffect(() => {
    if (!lastQueryError) return;
    client.notifications.addError({
      message: lastQueryError.message,
      origin: { context: { reason: 'channel query error' }, emitter: 'ChannelList' },
    });
  }, [client, lastQueryError]);

  return (
    <div style={{ paddingBlock: '1rem' }}>
      <div>
        <strong>{paginator.id}</strong>
      </div>
      {channels?.length || isLoading ? (
        <InfiniteScrollPaginator
          loadNextDebounceMs={loadMoreDebounceMs}
          loadNextOnScrollToBottom={paginator.next}
          threshold={loadMoreThresholdPx}
        >
          {channels.map((channel) => (
            <ChannelPreview channel={channel} key={channel.cid} />
          ))}
          <div
            className='str-chat__search-source-result-list__footer'
            data-testid='search-footer'
          >
            {isLoading ? (
              <ChannelListLoadingIndicator />
            ) : !hasNext ? (
              <div className='str-chat__search-source-results---empty'>
                {t('All results loaded')}
              </div>
            ) : null}
          </div>
        </InfiniteScrollPaginator>
      ) : (
        <EmptyChannelList />
      )}
    </div>
  );
};

const EmptyChannelList = () => <div>There are no channels</div>;
const ChannelListLoadingIndicator = () => <div>Loading...</div>;

Things to consider / improvements:

  1. Keep paginator items as an array of ids and the actual objects inside a cache (e.g. client.activeChannels)

Copy link
Contributor

github-actions bot commented Sep 3, 2025

Size Change: +24 kB (+5.26%) 🔍

Total Size: 480 kB

Filename Size Change
dist/cjs/index.browser.cjs 138 kB +8.01 kB (+6.15%) 🔍
dist/cjs/index.node.cjs 191 kB +8.01 kB (+4.38%)
dist/esm/index.js 151 kB +7.97 kB (+5.58%) 🔍

compressed-size-action

};

// fixme: is it ok, remove item just because its property hidden is switched to hidden: true? What about offset cursor, should we update it?
const channelHiddenHandler: LabeledEventHandler<ChannelPaginatorsOrchestratorEventHandlerContext> =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct - channel should not be removed from all the lists. There may be lists that have filter {hidden: true} - meaning, show me hidden channels. I will remove this from the orchestrator and move it to the legacy React ChannelList implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant