Skip to content

Commit 8bfed8e

Browse files
authored
Merge pull request #211 from GetStream/invitations
feat: Add channel invites #206
2 parents 7051a59 + 6061d42 commit 8bfed8e

19 files changed

+809
-87
lines changed
19.5 KB
Loading
2.14 MB
Loading
1.07 MB
Loading
38 KB
Loading

docusaurus/docs/Angular/code-examples/channel-invites.mdx

Lines changed: 454 additions & 0 deletions
Large diffs are not rendered by default.

projects/stream-chat-angular/.eslintrc.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"jsdoc/require-returns-type": 0,
5858
"jsdoc/newline-after-description": 0,
5959
"jsdoc/require-param-description": 0,
60+
"jsdoc/require-param": 2,
6061
"jsdoc/no-types": 2,
6162
"jsdoc/no-defaults": 2,
6263
"jsdoc/require-asterisk-prefix": 2,
@@ -100,6 +101,7 @@
100101
"jsdoc/require-returns-type": 0,
101102
"jsdoc/newline-after-description": 0,
102103
"jsdoc/require-param-description": 0,
104+
"jsdoc/require-param": 2,
103105
"jsdoc/no-types": 2,
104106
"jsdoc/no-defaults": 2,
105107
"jsdoc/require-asterisk-prefix": 2,

projects/stream-chat-angular/src/lib/channel-header/channel-header.component.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@
2020
translate:watcherCountParam) : ''}}
2121
</p>
2222
</div>
23+
<ng-container *ngIf="channelActionsTemplate">
24+
<ng-container
25+
*ngTemplateOutlet="
26+
channelActionsTemplate;
27+
context: { channel: activeChannel }
28+
"
29+
></ng-container>
30+
</ng-container>
2331
</div>

projects/stream-chat-angular/src/lib/channel-header/channel-header.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from '@angular/core';
1+
import { Component, Input, TemplateRef } from '@angular/core';
22
import { Channel } from 'stream-chat';
33
import { ChannelListToggleService } from '../channel-list/channel-list-toggle.service';
44
import { ChannelService } from '../channel.service';
@@ -12,6 +12,10 @@ import { ChannelService } from '../channel.service';
1212
styles: [],
1313
})
1414
export class ChannelHeaderComponent {
15+
/**
16+
* Template that can be used to add actions (such as edit, invite) to the channel header
17+
*/
18+
@Input() channelActionsTemplate?: TemplateRef<{ channel: Channel }>;
1519
activeChannel: Channel | undefined;
1620
canReceiveConnectEvents: boolean | undefined;
1721

projects/stream-chat-angular/src/lib/channel.service.ts

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Channel,
1212
ChannelFilters,
1313
ChannelOptions,
14+
ChannelResponse,
1415
ChannelSort,
1516
Event,
1617
FormatMessageResponse,
@@ -98,37 +99,37 @@ export class ChannelService {
9899
*/
99100
usersTypingInThread$: Observable<UserResponse[]>;
100101
/**
101-
* Emits a map that contains the date of the latest message sent by the current user by channels (this is used to detect is slow mode countdown should be started)
102+
* Emits a map that contains the date of the latest message sent by the current user by channels (this is used to detect if slow mode countdown should be started)
102103
*/
103104
latestMessageDateByUserByChannels$: Observable<{ [key: string]: Date }>;
104105
/**
105106
* Custom event handler to call if a new message received from a channel that is not being watched, provide an event handler if you want to override the [default channel list ordering](./ChannelService.mdx/#channels)
106107
*/
107108
customNewMessageNotificationHandler?: (
108109
notification: Notification,
109-
channelListSetter: (channels: Channel[]) => void
110+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
110111
) => void;
111112
/**
112113
* Custom event handler to call when the user is added to a channel, provide an event handler if you want to override the [default channel list ordering](./ChannelService.mdx/#channels)
113114
*/
114115
customAddedToChannelNotificationHandler?: (
115116
notification: Notification,
116-
channelListSetter: (channels: Channel[]) => void
117+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
117118
) => void;
118119
/**
119120
* Custom event handler to call when the user is removed from a channel, provide an event handler if you want to override the [default channel list ordering](./ChannelService.mdx/#channels)
120121
*/
121122
customRemovedFromChannelNotificationHandler?: (
122123
notification: Notification,
123-
channelListSetter: (channels: Channel[]) => void
124+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
124125
) => void;
125126
/**
126127
* Custom event handler to call when a channel is deleted, provide an event handler if you want to override the [default channel list ordering](./ChannelService.mdx/#channels)
127128
*/
128129
customChannelDeletedHandler?: (
129130
event: Event,
130131
channel: Channel,
131-
channelListSetter: (channels: Channel[]) => void,
132+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
132133
messageListSetter: (messages: StreamMessage[]) => void,
133134
threadListSetter: (messages: StreamMessage[]) => void,
134135
parentMessageSetter: (message: StreamMessage | undefined) => void
@@ -139,7 +140,7 @@ export class ChannelService {
139140
customChannelUpdatedHandler?: (
140141
event: Event,
141142
channel: Channel,
142-
channelListSetter: (channels: Channel[]) => void,
143+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
143144
messageListSetter: (messages: StreamMessage[]) => void,
144145
threadListSetter: (messages: StreamMessage[]) => void,
145146
parentMessageSetter: (message: StreamMessage | undefined) => void
@@ -150,7 +151,7 @@ export class ChannelService {
150151
customChannelTruncatedHandler?: (
151152
event: Event,
152153
channel: Channel,
153-
channelListSetter: (channels: Channel[]) => void,
154+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
154155
messageListSetter: (messages: StreamMessage[]) => void,
155156
threadListSetter: (messages: StreamMessage[]) => void,
156157
parentMessageSetter: (message: StreamMessage | undefined) => void
@@ -161,7 +162,7 @@ export class ChannelService {
161162
customChannelHiddenHandler?: (
162163
event: Event,
163164
channel: Channel,
164-
channelListSetter: (channels: Channel[]) => void,
165+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
165166
messageListSetter: (messages: StreamMessage[]) => void,
166167
threadListSetter: (messages: StreamMessage[]) => void,
167168
parentMessageSetter: (message: StreamMessage | undefined) => void
@@ -172,7 +173,7 @@ export class ChannelService {
172173
customChannelVisibleHandler?: (
173174
event: Event,
174175
channel: Channel,
175-
channelListSetter: (channels: Channel[]) => void,
176+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
176177
messageListSetter: (messages: StreamMessage[]) => void,
177178
threadListSetter: (messages: StreamMessage[]) => void,
178179
parentMessageSetter: (message: StreamMessage | undefined) => void
@@ -183,7 +184,7 @@ export class ChannelService {
183184
customNewMessageHandler?: (
184185
event: Event,
185186
channel: Channel,
186-
channelListSetter: (channels: Channel[]) => void,
187+
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
187188
messageListSetter: (messages: StreamMessage[]) => void,
188189
threadListSetter: (messages: StreamMessage[]) => void,
189190
parentMessageSetter: (message: StreamMessage | undefined) => void
@@ -218,8 +219,19 @@ export class ChannelService {
218219
private usersTypingInChannelSubject = new BehaviorSubject<UserResponse[]>([]);
219220
private usersTypingInThreadSubject = new BehaviorSubject<UserResponse[]>([]);
220221

221-
private channelListSetter = (channels: Channel[]) => {
222-
this.channelsSubject.next(channels);
222+
private channelListSetter = (channels: (Channel | ChannelResponse)[]) => {
223+
const currentChannels = this.channelsSubject.getValue() || [];
224+
const newChannels = channels.filter(
225+
(c) => !currentChannels.find((channel) => channel.cid === c.cid)
226+
);
227+
const deletedChannels = currentChannels.filter(
228+
(c) => !channels?.find((channel) => channel.cid === c.cid)
229+
);
230+
this.addChannelsFromNotification(newChannels as ChannelResponse[]);
231+
this.removeChannelsFromChannelList(deletedChannels.map((c) => c.cid));
232+
if (!newChannels.length && !deletedChannels.length) {
233+
this.channelsSubject.next(channels as Channel[]);
234+
}
223235
};
224236

225237
private messageListSetter = (messages: StreamMessage[]) => {
@@ -681,30 +693,30 @@ export class ChannelService {
681693
}
682694
}
683695

684-
private async handleNotification(notification: Notification) {
696+
private handleNotification(notification: Notification) {
685697
switch (notification.eventType) {
686698
case 'notification.message_new': {
687-
await this.ngZone.run(async () => {
699+
this.ngZone.run(() => {
688700
if (this.customNewMessageNotificationHandler) {
689701
this.customNewMessageNotificationHandler(
690702
notification,
691703
this.channelListSetter
692704
);
693705
} else {
694-
await this.handleNewMessageNotification(notification);
706+
this.handleNewMessageNotification(notification);
695707
}
696708
});
697709
break;
698710
}
699711
case 'notification.added_to_channel': {
700-
await this.ngZone.run(async () => {
712+
this.ngZone.run(() => {
701713
if (this.customAddedToChannelNotificationHandler) {
702714
this.customAddedToChannelNotificationHandler(
703715
notification,
704716
this.channelListSetter
705717
);
706718
} else {
707-
await this.handleAddedToChannelNotification(notification);
719+
this.handleAddedToChannelNotification(notification);
708720
}
709721
});
710722
break;
@@ -726,36 +738,48 @@ export class ChannelService {
726738

727739
private handleRemovedFromChannelNotification(notification: Notification) {
728740
const channelIdToBeRemoved = notification.event.channel!.cid;
729-
this.removeFromChannelList(channelIdToBeRemoved);
741+
this.removeChannelsFromChannelList([channelIdToBeRemoved]);
730742
}
731743

732-
private async handleNewMessageNotification(notification: Notification) {
733-
await this.addChannelFromNotification(notification);
744+
private handleNewMessageNotification(notification: Notification) {
745+
if (notification.event.channel) {
746+
this.addChannelsFromNotification([notification.event.channel]);
747+
}
734748
}
735749

736-
private async handleAddedToChannelNotification(notification: Notification) {
737-
await this.addChannelFromNotification(notification);
750+
private handleAddedToChannelNotification(notification: Notification) {
751+
if (notification.event.channel) {
752+
this.addChannelsFromNotification([notification.event.channel]);
753+
}
738754
}
739755

740-
private async addChannelFromNotification(notification: Notification) {
741-
const channel = this.chatClientService.chatClient.channel(
742-
notification.event.channel?.type!,
743-
notification.event.channel?.id
744-
);
745-
await channel.watch();
746-
this.watchForChannelEvents(channel);
756+
private addChannelsFromNotification(channelResponses: ChannelResponse[]) {
757+
const newChannels: Channel[] = [];
758+
channelResponses.forEach((channelResponse) => {
759+
const channel = this.chatClientService.chatClient.channel(
760+
channelResponse.type,
761+
channelResponse.id
762+
);
763+
void channel.watch();
764+
this.watchForChannelEvents(channel);
765+
newChannels.push(channel);
766+
});
747767
this.channelsSubject.next([
748-
channel,
768+
...newChannels,
749769
...(this.channelsSubject.getValue() || []),
750770
]);
751771
}
752772

753-
private removeFromChannelList(cid: string) {
754-
const channels = this.channels.filter((c) => c.cid !== cid);
773+
private removeChannelsFromChannelList(cids: string[]) {
774+
const channels = this.channels.filter((c) => !cids.includes(c.cid || ''));
755775
if (channels.length < this.channels.length) {
756776
this.channelsSubject.next(channels);
757-
if (this.activeChannelSubject.getValue()!.cid === cid) {
758-
this.setAsActiveChannel(channels[0]);
777+
if (cids.includes(this.activeChannelSubject.getValue()?.cid || '')) {
778+
if (channels.length > 0) {
779+
this.setAsActiveChannel(channels[0]);
780+
} else {
781+
this.activeChannelSubject.next(undefined);
782+
}
759783
}
760784
}
761785
}
@@ -1059,11 +1083,11 @@ export class ChannelService {
10591083
}
10601084

10611085
private handleChannelHidden(event: Event) {
1062-
this.removeFromChannelList(event.channel!.cid);
1086+
this.removeChannelsFromChannelList([event.channel!.cid]);
10631087
}
10641088

10651089
private handleChannelDeleted(event: Event) {
1066-
this.removeFromChannelList(event.channel!.cid);
1090+
this.removeChannelsFromChannelList([event.channel!.cid]);
10671091
}
10681092

10691093
private handleChannelVisible(event: Event, channel: Channel) {

projects/stream-chat-angular/src/lib/chat-client.service.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ describe('ChatClientService', () => {
3333
});
3434

3535
it('should disconnect user', async () => {
36+
const pendingInvitesSpy = jasmine.createSpy();
37+
service.pendingInvites$.subscribe(pendingInvitesSpy);
38+
pendingInvitesSpy.calls.reset();
3639
await service.disconnectUser();
3740

3841
expect(mockChatClient.disconnectUser).toHaveBeenCalledWith();
42+
expect(pendingInvitesSpy).toHaveBeenCalledWith([]);
3943
});
4044

4145
it('should init with user meta data', async () => {
@@ -180,4 +184,83 @@ describe('ChatClientService', () => {
180184

181185
expect(result.length).toBe(2);
182186
});
187+
188+
it('should initialize pending invites', async () => {
189+
const channelsWithPendingInvites = [{ cid: 'cat-lovers' }];
190+
mockChatClient.queryChannels.and.resolveTo(channelsWithPendingInvites);
191+
const invitesSpy = jasmine.createSpy();
192+
service.pendingInvites$.subscribe(invitesSpy);
193+
invitesSpy.calls.reset();
194+
await service.init(apiKey, userId, userToken);
195+
196+
expect(mockChatClient.queryChannels).toHaveBeenCalledWith(
197+
{
198+
invite: 'pending',
199+
},
200+
{},
201+
{ user_id: mockChatClient.user.id }
202+
);
203+
204+
expect(invitesSpy).toHaveBeenCalledWith(channelsWithPendingInvites);
205+
});
206+
207+
it('should emit pending invitations of user', () => {
208+
const invitesSpy = jasmine.createSpy();
209+
service.pendingInvites$.subscribe(invitesSpy);
210+
const event1 = {
211+
id: 'mockevent',
212+
type: 'notification.invited',
213+
channel: { cid: 'what-i-ate-for-lunch' },
214+
member: { user: mockChatClient.user },
215+
} as any as Event;
216+
mockChatClient.handleEvent(event1.type, event1);
217+
218+
expect(invitesSpy).toHaveBeenCalledWith([event1.channel]);
219+
220+
invitesSpy.calls.reset();
221+
const event2 = {
222+
id: 'mockevent',
223+
type: 'notification.invited',
224+
channel: { cid: 'gardening' },
225+
member: { user: mockChatClient.user },
226+
} as any as Event;
227+
mockChatClient.handleEvent(event2.type, event2);
228+
229+
expect(invitesSpy).toHaveBeenCalledWith([event1.channel, event2.channel]);
230+
231+
invitesSpy.calls.reset();
232+
const event3 = {
233+
id: 'mockevent',
234+
type: 'notification.invite_accepted',
235+
channel: { cid: 'what-i-ate-for-lunch' },
236+
member: { user: mockChatClient.user },
237+
} as any as Event;
238+
mockChatClient.handleEvent(event3.type, event3);
239+
240+
expect(invitesSpy).toHaveBeenCalledWith([event2.channel]);
241+
242+
invitesSpy.calls.reset();
243+
const event4 = {
244+
id: 'mockevent',
245+
type: 'notification.invite_rejected',
246+
channel: { cid: 'gardening' },
247+
member: { user: mockChatClient.user },
248+
} as any as Event;
249+
mockChatClient.handleEvent(event4.type, event4);
250+
251+
expect(invitesSpy).toHaveBeenCalledWith([]);
252+
253+
invitesSpy.calls.reset();
254+
const event5 = {
255+
id: 'mockevent',
256+
type: 'notification.invite_rejected',
257+
channel: { cid: 'gardening' },
258+
member: {
259+
user: { id: `not${mockChatClient.user.id}` },
260+
},
261+
} as any as Event;
262+
mockChatClient.handleEvent(event5.type, event5);
263+
264+
expect(invitesSpy).not.toHaveBeenCalled();
265+
});
183266
});

0 commit comments

Comments
 (0)