stream-chat 8.55.0 → 8.56.1

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/store.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  export type Patch<T> = (value: T) => T;
2
+ export type ValueOrPatch<T> = T | Patch<T>;
2
3
  export type Handler<T> = (nextValue: T, previousValue: T | undefined) => void;
3
4
  export type Unsubscribe = () => void;
4
5
 
5
- function isPatch<T>(value: T | Patch<T>): value is Patch<T> {
6
+ export const isPatch = <T>(value: ValueOrPatch<T>): value is Patch<T> => {
6
7
  return typeof value === 'function';
7
- }
8
+ };
8
9
 
9
10
  export class StateStore<T extends Record<string, unknown>> {
10
11
  private handlerSet = new Set<Handler<T>>();
@@ -13,7 +14,7 @@ export class StateStore<T extends Record<string, unknown>> {
13
14
 
14
15
  constructor(private value: T) {}
15
16
 
16
- public next = (newValueOrPatch: T | Patch<T>): void => {
17
+ public next = (newValueOrPatch: ValueOrPatch<T>): void => {
17
18
  // newValue (or patch output) should never be mutated previous value
18
19
  const newValue = isPatch(newValueOrPatch) ? newValueOrPatch(this.value) : newValueOrPatch;
19
20
 
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
  /**
@@ -594,6 +602,27 @@ export const throttle = <T extends (...args: unknown[]) => unknown>(
594
602
  };
595
603
  };
596
604
 
605
+ const get = <T>(obj: T, path: string): unknown =>
606
+ path.split('.').reduce<unknown>((acc, key) => {
607
+ if (acc && typeof acc === 'object' && key in acc) {
608
+ return (acc as Record<string, unknown>)[key];
609
+ }
610
+ return undefined;
611
+ }, obj);
612
+
613
+ // works exactly the same as lodash.uniqBy
614
+ export const uniqBy = <T>(array: T[] | unknown, iteratee: ((item: T) => unknown) | keyof T): T[] => {
615
+ if (!Array.isArray(array)) return [];
616
+
617
+ const seen = new Set<unknown>();
618
+ return array.filter((item) => {
619
+ const key = typeof iteratee === 'function' ? iteratee(item) : get(item, iteratee as string);
620
+ if (seen.has(key)) return false;
621
+ seen.add(key);
622
+ return true;
623
+ });
624
+ };
625
+
597
626
  type MessagePaginationUpdatedParams<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
598
627
  parentSet: MessageSet;
599
628
  requestedPageSize: number;
@@ -823,3 +852,275 @@ export const messageSetPagination = <StreamChatGenerics extends ExtendableGeneri
823
852
  return messagePaginationLinear(params);
824
853
  }
825
854
  };
855
+
856
+ /**
857
+ * A utility object used to prevent duplicate invocation of channel.watch() to be triggered when
858
+ * 'notification.message_new' and 'notification.added_to_channel' events arrive at the same time.
859
+ */
860
+ const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record<string, Promise<QueryChannelAPIResponse> | undefined> = {};
861
+
862
+ type GetChannelParams<StreamChatGenerics extends ExtendableGenerics = DefaultGenerics> = {
863
+ client: StreamChat<StreamChatGenerics>;
864
+ channel?: Channel<StreamChatGenerics>;
865
+ id?: string;
866
+ members?: string[];
867
+ options?: ChannelQueryOptions<StreamChatGenerics>;
868
+ type?: string;
869
+ };
870
+ /**
871
+ * Calls channel.watch() if it was not already recently called. Waits for watch promise to resolve even if it was invoked previously.
872
+ * 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.
873
+ * @param client
874
+ * @param members
875
+ * @param options
876
+ * @param type
877
+ * @param id
878
+ * @param channel
879
+ */
880
+ export const getAndWatchChannel = async <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
881
+ channel,
882
+ client,
883
+ id,
884
+ members,
885
+ options,
886
+ type,
887
+ }: GetChannelParams<StreamChatGenerics>) => {
888
+ if (!channel && !type) {
889
+ throw new Error('Channel or channel type have to be provided to query a channel.');
890
+ }
891
+
892
+ // unfortunately typescript is not able to infer that if (!channel && !type) === false, then channel or type has to be truthy
893
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
894
+ const channelToWatch = channel || client.channel(type!, id, { members });
895
+
896
+ // need to keep as with call to channel.watch the id can be changed from undefined to an actual ID generated server-side
897
+ const originalCid = channelToWatch.id
898
+ ? channelToWatch.cid
899
+ : members && members.length
900
+ ? generateChannelTempCid(channelToWatch.type, members)
901
+ : undefined;
902
+
903
+ if (!originalCid) {
904
+ throw new Error('Channel ID or channel members array have to be provided to query a channel.');
905
+ }
906
+
907
+ const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
908
+
909
+ if (queryPromise) {
910
+ await queryPromise;
911
+ } else {
912
+ try {
913
+ WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = channelToWatch.watch(options);
914
+ await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
915
+ } finally {
916
+ delete WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
917
+ }
918
+ }
919
+
920
+ return channelToWatch;
921
+ };
922
+
923
+ /**
924
+ * Generates a temporary channel.cid for channels created without ID, as they need to be referenced
925
+ * by an identifier until the back-end generates the final ID. The cid is generated by its member IDs
926
+ * which are sorted and can be recreated the same every time given the same arguments.
927
+ * @param channelType
928
+ * @param members
929
+ */
930
+ export const generateChannelTempCid = (channelType: string, members: string[]) => {
931
+ if (!members) return;
932
+ const membersStr = [...members].sort().join(',');
933
+ if (!membersStr) return;
934
+ return `${channelType}:!members-${membersStr}`;
935
+ };
936
+
937
+ /**
938
+ * Checks if a channel is pinned or not. Will return true only if channel.state.membership.pinned_at exists.
939
+ * @param channel
940
+ */
941
+ export const isChannelPinned = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
942
+ channel: Channel<StreamChatGenerics>,
943
+ ) => {
944
+ if (!channel) return false;
945
+
946
+ const member = channel.state.membership;
947
+
948
+ return !!member?.pinned_at;
949
+ };
950
+
951
+ /**
952
+ * Checks if a channel is archived or not. Will return true only if channel.state.membership.archived_at exists.
953
+ * @param channel
954
+ */
955
+ export const isChannelArchived = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
956
+ channel: Channel<StreamChatGenerics>,
957
+ ) => {
958
+ if (!channel) return false;
959
+
960
+ const member = channel.state.membership;
961
+
962
+ return !!member?.archived_at;
963
+ };
964
+
965
+ /**
966
+ * A utility that tells us whether we should consider archived channels or not based
967
+ * on filters. Will return true only if filters.archived exists and is a boolean value.
968
+ * @param filters
969
+ */
970
+ export const shouldConsiderArchivedChannels = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
971
+ filters: ChannelFilters<StreamChatGenerics>,
972
+ ) => {
973
+ if (!filters) return false;
974
+
975
+ return typeof filters.archived === 'boolean';
976
+ };
977
+
978
+ /**
979
+ * Extracts the value of the sort parameter at a given index, for a targeted key. Can
980
+ * handle both array and object versions of sort. Will return null if the index/key
981
+ * combination does not exist.
982
+ * @param atIndex - the index at which we'll examine the sort value, if it's an array one
983
+ * @param sort - the sort value - both array and object notations are accepted
984
+ * @param targetKey - the target key which needs to exist for the sort at a certain index
985
+ */
986
+ export const extractSortValue = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
987
+ atIndex,
988
+ sort,
989
+ targetKey,
990
+ }: {
991
+ atIndex: number;
992
+ targetKey: keyof ChannelSortBase<StreamChatGenerics>;
993
+ sort?: ChannelSort<StreamChatGenerics>;
994
+ }) => {
995
+ if (!sort) return null;
996
+ let option: null | ChannelSortBase<StreamChatGenerics> = null;
997
+
998
+ if (Array.isArray(sort)) {
999
+ option = sort[atIndex] ?? null;
1000
+ } else {
1001
+ let index = 0;
1002
+ for (const key in sort) {
1003
+ if (index !== atIndex) {
1004
+ index++;
1005
+ continue;
1006
+ }
1007
+
1008
+ if (key !== targetKey) {
1009
+ return null;
1010
+ }
1011
+
1012
+ option = sort;
1013
+
1014
+ break;
1015
+ }
1016
+ }
1017
+
1018
+ return option?.[targetKey] ?? null;
1019
+ };
1020
+
1021
+ /**
1022
+ * Returns true only if `{ pinned_at: -1 }` or `{ pinned_at: 1 }` option is first within the `sort` array.
1023
+ */
1024
+ export const shouldConsiderPinnedChannels = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>(
1025
+ sort: ChannelSort<StreamChatGenerics>,
1026
+ ) => {
1027
+ const value = findPinnedAtSortOrder({ sort });
1028
+
1029
+ if (typeof value !== 'number') return false;
1030
+
1031
+ return Math.abs(value) === 1;
1032
+ };
1033
+
1034
+ /**
1035
+ * Checks whether the sort value of type object contains a pinned_at value or if
1036
+ * an array sort value type has the first value be an object containing pinned_at.
1037
+ * @param sort
1038
+ */
1039
+ export const findPinnedAtSortOrder = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
1040
+ sort,
1041
+ }: {
1042
+ sort: ChannelSort<StreamChatGenerics>;
1043
+ }) =>
1044
+ extractSortValue({
1045
+ atIndex: 0,
1046
+ sort,
1047
+ targetKey: 'pinned_at',
1048
+ });
1049
+
1050
+ /**
1051
+ * Finds the index of the last consecutively pinned channel, starting from the start of the
1052
+ * array. Will not consider any pinned channels after the contiguous subsequence at the
1053
+ * start of the array.
1054
+ * @param channels
1055
+ */
1056
+ export const findLastPinnedChannelIndex = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
1057
+ channels,
1058
+ }: {
1059
+ channels: Channel<StreamChatGenerics>[];
1060
+ }) => {
1061
+ let lastPinnedChannelIndex: number | null = null;
1062
+
1063
+ for (const channel of channels) {
1064
+ if (!isChannelPinned(channel)) break;
1065
+
1066
+ if (typeof lastPinnedChannelIndex === 'number') {
1067
+ lastPinnedChannelIndex++;
1068
+ } else {
1069
+ lastPinnedChannelIndex = 0;
1070
+ }
1071
+ }
1072
+
1073
+ return lastPinnedChannelIndex;
1074
+ };
1075
+
1076
+ /**
1077
+ * A utility used to move a channel towards the beginning of a list of channels (promote it to a higher position). It
1078
+ * considers pinned channels in the process if needed and makes sure to only update the list reference if the list
1079
+ * should actually change. It will try to move the channel as high as it can within the list.
1080
+ * @param channels - the list of channels we want to modify
1081
+ * @param channelToMove - the channel we want to promote
1082
+ * @param channelToMoveIndexWithinChannels - optionally, the index of the channel we want to move if we know it (will skip a manual check)
1083
+ * @param sort - the sort value used to check for pinned channels
1084
+ */
1085
+ export const promoteChannel = <StreamChatGenerics extends ExtendableGenerics = DefaultGenerics>({
1086
+ channels,
1087
+ channelToMove,
1088
+ channelToMoveIndexWithinChannels,
1089
+ sort,
1090
+ }: PromoteChannelParams<StreamChatGenerics>) => {
1091
+ // get index of channel to move up
1092
+ const targetChannelIndex =
1093
+ channelToMoveIndexWithinChannels ?? channels.findIndex((channel) => channel.cid === channelToMove.cid);
1094
+
1095
+ const targetChannelExistsWithinList = targetChannelIndex >= 0;
1096
+ const targetChannelAlreadyAtTheTop = targetChannelIndex === 0;
1097
+
1098
+ // pinned channels should not move within the list based on recent activity, channels which
1099
+ // receive messages and are not pinned should move upwards but only under the last pinned channel
1100
+ // in the list
1101
+ const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
1102
+ const isTargetChannelPinned = isChannelPinned<StreamChatGenerics>(channelToMove);
1103
+
1104
+ if (targetChannelAlreadyAtTheTop || (considerPinnedChannels && isTargetChannelPinned)) {
1105
+ return channels;
1106
+ }
1107
+
1108
+ const newChannels = [...channels];
1109
+
1110
+ // target channel index is known, remove it from the list
1111
+ if (targetChannelExistsWithinList) {
1112
+ newChannels.splice(targetChannelIndex, 1);
1113
+ }
1114
+
1115
+ // as position of pinned channels has to stay unchanged, we need to
1116
+ // find last pinned channel in the list to move the target channel after
1117
+ let lastPinnedChannelIndex: number | null = null;
1118
+ if (considerPinnedChannels) {
1119
+ lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
1120
+ }
1121
+
1122
+ // re-insert it at the new place (to specific index if pinned channels are considered)
1123
+ newChannels.splice(typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0, 0, channelToMove);
1124
+
1125
+ return newChannels;
1126
+ };