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)

@github-actions
Copy link
Contributor

github-actions bot commented Sep 3, 2025

Size Change: +31.6 kB (+8.92%) 🔍

Total Size: 386 kB

Filename Size Change
dist/cjs/index.browser.js 129 kB +10.6 kB (+8.94%) 🔍
dist/cjs/index.node.js 130 kB +10.6 kB (+8.86%) 🔍
dist/esm/index.mjs 128 kB +10.5 kB (+8.95%) 🔍

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.

@MartinCupela MartinCupela force-pushed the feat/channel-paginator branch from cc9f72b to 83bd4ef Compare November 7, 2025 13:24
Support setting paginator items directly, optional request retries, offline DB in ChannelPaginator, identification of pagination restart based on query shape change.
@MartinCupela MartinCupela force-pushed the feat/channel-paginator branch from 83bd4ef to f63c7ca Compare November 7, 2025 13:26
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.

2 participants