stream-chat 8.55.0 → 8.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios';
2
2
  import { StableWSConnection } from './connection';
3
3
  import { EVENT_MAP } from './events';
4
4
  import { Role } from './permissions';
5
+ import type { Channel } from './channel';
5
6
 
6
7
  /**
7
8
  * Utility Types
@@ -3796,3 +3797,14 @@ export type VelocityFilterConfig = {
3796
3797
  rules: VelocityFilterConfigRule[];
3797
3798
  async?: boolean;
3798
3799
  };
3800
+
3801
+ export type PromoteChannelParams<SCG extends ExtendableGenerics = DefaultGenerics> = {
3802
+ channels: Array<Channel<SCG>>;
3803
+ channelToMove: Channel<SCG>;
3804
+ sort: ChannelSort<SCG>;
3805
+ /**
3806
+ * If the index of the channel within `channels` list which is being moved upwards
3807
+ * (`channelToMove`) is known, you can supply it to skip extra calculation.
3808
+ */
3809
+ channelToMoveIndexWithinChannels?: number;
3810
+ };
package/src/utils.ts CHANGED
@@ -12,7 +12,15 @@ import {
12
12
  ReactionGroupResponse,
13
13
  MessageSet,
14
14
  MessagePaginationOptions,
15
+ ChannelQueryOptions,
16
+ QueryChannelAPIResponse,
17
+ ChannelSort,
18
+ ChannelFilters,
19
+ ChannelSortBase,
20
+ PromoteChannelParams,
15
21
  } from './types';
22
+ import { StreamChat } from './client';
23
+ import { Channel } from './channel';
16
24
  import { AxiosRequestConfig } from 'axios';
17
25
 
18
26
  /**
@@ -823,3 +831,275 @@ export const messageSetPagination = <StreamChatGenerics extends ExtendableGeneri
823
831
  return messagePaginationLinear(params);
824
832
  }
825
833
  };
834
+
835
+ /**
836
+ * A utility object used to prevent duplicate invocation of channel.watch() to be triggered when
837
+ * 'notification.message_new' and 'notification.added_to_channel' events arrive at the same time.
838
+ */
839
+ const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record<string, Promise<QueryChannelAPIResponse> | undefined> = {};
840
+
841
+ type GetChannelParams<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
842
+ client: StreamChat<StreamChatGenerics>;
843
+ channel?: Channel<StreamChatGenerics>;
844
+ id?: string;
845
+ members?: string[];
846
+ options?: ChannelQueryOptions<StreamChatGenerics>;
847
+ type?: string;
848
+ };
849
+ /**
850
+ * Calls channel.watch() if it was not already recently called. Waits for watch promise to resolve even if it was invoked previously.
851
+ * If the channel is not passed as a property, it will get it either by its channel.cid or by its members list and do the same.
852
+ * @param client
853
+ * @param members
854
+ * @param options
855
+ * @param type
856
+ * @param id
857
+ * @param channel
858
+ */
859
+ export const getAndWatchChannel = async <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
860
+ channel,
861
+ client,
862
+ id,
863
+ members,
864
+ options,
865
+ type,
866
+ }: GetChannelParams<StreamChatGenerics>) => {
867
+ if (!channel && !type) {
868
+ throw new Error('Channel or channel type have to be provided to query a channel.');
869
+ }
870
+
871
+ // unfortunately typescript is not able to infer that if (!channel && !type) === false, then channel or type has to be truthy
872
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
873
+ const channelToWatch = channel || client.channel(type!, id, { members });
874
+
875
+ // need to keep as with call to channel.watch the id can be changed from undefined to an actual ID generated server-side
876
+ const originalCid = channelToWatch.id
877
+ ? channelToWatch.cid
878
+ : members && members.length
879
+ ? generateChannelTempCid(channelToWatch.type, members)
880
+ : undefined;
881
+
882
+ if (!originalCid) {
883
+ throw new Error('Channel ID or channel members array have to be provided to query a channel.');
884
+ }
885
+
886
+ const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
887
+
888
+ if (queryPromise) {
889
+ await queryPromise;
890
+ } else {
891
+ try {
892
+ WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = channelToWatch.watch(options);
893
+ await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
894
+ } finally {
895
+ delete WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
896
+ }
897
+ }
898
+
899
+ return channelToWatch;
900
+ };
901
+
902
+ /**
903
+ * Generates a temporary channel.cid for channels created without ID, as they need to be referenced
904
+ * by an identifier until the back-end generates the final ID. The cid is generated by its member IDs
905
+ * which are sorted and can be recreated the same every time given the same arguments.
906
+ * @param channelType
907
+ * @param members
908
+ */
909
+ export const generateChannelTempCid = (channelType: string, members: string[]) => {
910
+ if (!members) return;
911
+ const membersStr = [...members].sort().join(',');
912
+ if (!membersStr) return;
913
+ return `${channelType}:!members-${membersStr}`;
914
+ };
915
+
916
+ /**
917
+ * Checks if a channel is pinned or not. Will return true only if channel.state.membership.pinned_at exists.
918
+ * @param channel
919
+ */
920
+ export const isChannelPinned = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
921
+ channel: Channel<StreamChatGenerics>,
922
+ ) => {
923
+ if (!channel) return false;
924
+
925
+ const member = channel.state.membership;
926
+
927
+ return !!member?.pinned_at;
928
+ };
929
+
930
+ /**
931
+ * Checks if a channel is archived or not. Will return true only if channel.state.membership.archived_at exists.
932
+ * @param channel
933
+ */
934
+ export const isChannelArchived = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
935
+ channel: Channel<StreamChatGenerics>,
936
+ ) => {
937
+ if (!channel) return false;
938
+
939
+ const member = channel.state.membership;
940
+
941
+ return !!member?.archived_at;
942
+ };
943
+
944
+ /**
945
+ * A utility that tells us whether we should consider archived channels or not based
946
+ * on filters. Will return true only if filters.archived exists and is a boolean value.
947
+ * @param filters
948
+ */
949
+ export const shouldConsiderArchivedChannels = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
950
+ filters: ChannelFilters<StreamChatGenerics>,
951
+ ) => {
952
+ if (!filters) return false;
953
+
954
+ return typeof filters.archived === 'boolean';
955
+ };
956
+
957
+ /**
958
+ * Extracts the value of the sort parameter at a given index, for a targeted key. Can
959
+ * handle both array and object versions of sort. Will return null if the index/key
960
+ * combination does not exist.
961
+ * @param atIndex - the index at which we'll examine the sort value, if it's an array one
962
+ * @param sort - the sort value - both array and object notations are accepted
963
+ * @param targetKey - the target key which needs to exist for the sort at a certain index
964
+ */
965
+ export const extractSortValue = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
966
+ atIndex,
967
+ sort,
968
+ targetKey,
969
+ }: {
970
+ atIndex: number;
971
+ targetKey: keyof ChannelSortBase<StreamChatGenerics>;
972
+ sort?: ChannelSort<StreamChatGenerics>;
973
+ }) => {
974
+ if (!sort) return null;
975
+ let option: null | ChannelSortBase<StreamChatGenerics> = null;
976
+
977
+ if (Array.isArray(sort)) {
978
+ option = sort[atIndex] ?? null;
979
+ } else {
980
+ let index = 0;
981
+ for (const key in sort) {
982
+ if (index !== atIndex) {
983
+ index++;
984
+ continue;
985
+ }
986
+
987
+ if (key !== targetKey) {
988
+ return null;
989
+ }
990
+
991
+ option = sort;
992
+
993
+ break;
994
+ }
995
+ }
996
+
997
+ return option?.[targetKey] ?? null;
998
+ };
999
+
1000
+ /**
1001
+ * Returns true only if `{ pinned_at: -1 }` or `{ pinned_at: 1 }` option is first within the `sort` array.
1002
+ */
1003
+ export const shouldConsiderPinnedChannels = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
1004
+ sort: ChannelSort<StreamChatGenerics>,
1005
+ ) => {
1006
+ const value = findPinnedAtSortOrder({ sort });
1007
+
1008
+ if (typeof value !== 'number') return false;
1009
+
1010
+ return Math.abs(value) === 1;
1011
+ };
1012
+
1013
+ /**
1014
+ * Checks whether the sort value of type object contains a pinned_at value or if
1015
+ * an array sort value type has the first value be an object containing pinned_at.
1016
+ * @param sort
1017
+ */
1018
+ export const findPinnedAtSortOrder = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
1019
+ sort,
1020
+ }: {
1021
+ sort: ChannelSort<StreamChatGenerics>;
1022
+ }) =>
1023
+ extractSortValue({
1024
+ atIndex: 0,
1025
+ sort,
1026
+ targetKey: 'pinned_at',
1027
+ });
1028
+
1029
+ /**
1030
+ * Finds the index of the last consecutively pinned channel, starting from the start of the
1031
+ * array. Will not consider any pinned channels after the contiguous subsequence at the
1032
+ * start of the array.
1033
+ * @param channels
1034
+ */
1035
+ export const findLastPinnedChannelIndex = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
1036
+ channels,
1037
+ }: {
1038
+ channels: Channel<StreamChatGenerics>[];
1039
+ }) => {
1040
+ let lastPinnedChannelIndex: number | null = null;
1041
+
1042
+ for (const channel of channels) {
1043
+ if (!isChannelPinned(channel)) break;
1044
+
1045
+ if (typeof lastPinnedChannelIndex === 'number') {
1046
+ lastPinnedChannelIndex++;
1047
+ } else {
1048
+ lastPinnedChannelIndex = 0;
1049
+ }
1050
+ }
1051
+
1052
+ return lastPinnedChannelIndex;
1053
+ };
1054
+
1055
+ /**
1056
+ * A utility used to move a channel towards the beginning of a list of channels (promote it to a higher position). It
1057
+ * considers pinned channels in the process if needed and makes sure to only update the list reference if the list
1058
+ * should actually change. It will try to move the channel as high as it can within the list.
1059
+ * @param channels - the list of channels we want to modify
1060
+ * @param channelToMove - the channel we want to promote
1061
+ * @param channelToMoveIndexWithinChannels - optionally, the index of the channel we want to move if we know it (will skip a manual check)
1062
+ * @param sort - the sort value used to check for pinned channels
1063
+ */
1064
+ export const promoteChannel = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
1065
+ channels,
1066
+ channelToMove,
1067
+ channelToMoveIndexWithinChannels,
1068
+ sort,
1069
+ }: PromoteChannelParams<StreamChatGenerics>) => {
1070
+ // get index of channel to move up
1071
+ const targetChannelIndex =
1072
+ channelToMoveIndexWithinChannels ?? channels.findIndex((channel) => channel.cid === channelToMove.cid);
1073
+
1074
+ const targetChannelExistsWithinList = targetChannelIndex >= 0;
1075
+ const targetChannelAlreadyAtTheTop = targetChannelIndex === 0;
1076
+
1077
+ // pinned channels should not move within the list based on recent activity, channels which
1078
+ // receive messages and are not pinned should move upwards but only under the last pinned channel
1079
+ // in the list
1080
+ const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
1081
+ const isTargetChannelPinned = isChannelPinned<StreamChatGenerics>(channelToMove);
1082
+
1083
+ if (targetChannelAlreadyAtTheTop || (considerPinnedChannels && isTargetChannelPinned)) {
1084
+ return channels;
1085
+ }
1086
+
1087
+ const newChannels = [...channels];
1088
+
1089
+ // target channel index is known, remove it from the list
1090
+ if (targetChannelExistsWithinList) {
1091
+ newChannels.splice(targetChannelIndex, 1);
1092
+ }
1093
+
1094
+ // as position of pinned channels has to stay unchanged, we need to
1095
+ // find last pinned channel in the list to move the target channel after
1096
+ let lastPinnedChannelIndex: number | null = null;
1097
+ if (considerPinnedChannels) {
1098
+ lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
1099
+ }
1100
+
1101
+ // re-insert it at the new place (to specific index if pinned channels are considered)
1102
+ newChannels.splice(typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0, 0, channelToMove);
1103
+
1104
+ return newChannels;
1105
+ };