Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
454 changes: 454 additions & 0 deletions docusaurus/docs/Angular/code-examples/channel-invites.mdx

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions projects/stream-chat-angular/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"jsdoc/require-returns-type": 0,
"jsdoc/newline-after-description": 0,
"jsdoc/require-param-description": 0,
"jsdoc/require-param": 2,
"jsdoc/no-types": 2,
"jsdoc/no-defaults": 2,
"jsdoc/require-asterisk-prefix": 2,
Expand Down Expand Up @@ -100,6 +101,7 @@
"jsdoc/require-returns-type": 0,
"jsdoc/newline-after-description": 0,
"jsdoc/require-param-description": 0,
"jsdoc/require-param": 2,
"jsdoc/no-types": 2,
"jsdoc/no-defaults": 2,
"jsdoc/require-asterisk-prefix": 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@
translate:watcherCountParam) : ''}}
</p>
</div>
<ng-container *ngIf="channelActionsTemplate">
<ng-container
*ngTemplateOutlet="
channelActionsTemplate;
context: { channel: activeChannel }
"
></ng-container>
</ng-container>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, Input, TemplateRef } from '@angular/core';
import { Channel } from 'stream-chat';
import { ChannelListToggleService } from '../channel-list/channel-list-toggle.service';
import { ChannelService } from '../channel.service';
Expand All @@ -12,6 +12,10 @@ import { ChannelService } from '../channel.service';
styles: [],
})
export class ChannelHeaderComponent {
/**
* Template that can be used to add actions (such as edit, invite) to the channel header
*/
@Input() channelActionsTemplate?: TemplateRef<{ channel: Channel }>;
activeChannel: Channel | undefined;
canReceiveConnectEvents: boolean | undefined;

Expand Down
96 changes: 60 additions & 36 deletions projects/stream-chat-angular/src/lib/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Channel,
ChannelFilters,
ChannelOptions,
ChannelResponse,
ChannelSort,
Event,
FormatMessageResponse,
Expand Down Expand Up @@ -98,37 +99,37 @@ export class ChannelService {
*/
usersTypingInThread$: Observable<UserResponse[]>;
/**
* 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)
* 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)
*/
latestMessageDateByUserByChannels$: Observable<{ [key: string]: Date }>;
/**
* 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)
*/
customNewMessageNotificationHandler?: (
notification: Notification,
channelListSetter: (channels: Channel[]) => void
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
) => void;
/**
* 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)
*/
customAddedToChannelNotificationHandler?: (
notification: Notification,
channelListSetter: (channels: Channel[]) => void
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
) => void;
/**
* 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)
*/
customRemovedFromChannelNotificationHandler?: (
notification: Notification,
channelListSetter: (channels: Channel[]) => void
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
) => void;
/**
* 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)
*/
customChannelDeletedHandler?: (
event: Event,
channel: Channel,
channelListSetter: (channels: Channel[]) => void,
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
messageListSetter: (messages: StreamMessage[]) => void,
threadListSetter: (messages: StreamMessage[]) => void,
parentMessageSetter: (message: StreamMessage | undefined) => void
Expand All @@ -139,7 +140,7 @@ export class ChannelService {
customChannelUpdatedHandler?: (
event: Event,
channel: Channel,
channelListSetter: (channels: Channel[]) => void,
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
messageListSetter: (messages: StreamMessage[]) => void,
threadListSetter: (messages: StreamMessage[]) => void,
parentMessageSetter: (message: StreamMessage | undefined) => void
Expand All @@ -150,7 +151,7 @@ export class ChannelService {
customChannelTruncatedHandler?: (
event: Event,
channel: Channel,
channelListSetter: (channels: Channel[]) => void,
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
messageListSetter: (messages: StreamMessage[]) => void,
threadListSetter: (messages: StreamMessage[]) => void,
parentMessageSetter: (message: StreamMessage | undefined) => void
Expand All @@ -161,7 +162,7 @@ export class ChannelService {
customChannelHiddenHandler?: (
event: Event,
channel: Channel,
channelListSetter: (channels: Channel[]) => void,
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
messageListSetter: (messages: StreamMessage[]) => void,
threadListSetter: (messages: StreamMessage[]) => void,
parentMessageSetter: (message: StreamMessage | undefined) => void
Expand All @@ -172,7 +173,7 @@ export class ChannelService {
customChannelVisibleHandler?: (
event: Event,
channel: Channel,
channelListSetter: (channels: Channel[]) => void,
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
messageListSetter: (messages: StreamMessage[]) => void,
threadListSetter: (messages: StreamMessage[]) => void,
parentMessageSetter: (message: StreamMessage | undefined) => void
Expand All @@ -183,7 +184,7 @@ export class ChannelService {
customNewMessageHandler?: (
event: Event,
channel: Channel,
channelListSetter: (channels: Channel[]) => void,
channelListSetter: (channels: (Channel | ChannelResponse)[]) => void,
messageListSetter: (messages: StreamMessage[]) => void,
threadListSetter: (messages: StreamMessage[]) => void,
parentMessageSetter: (message: StreamMessage | undefined) => void
Expand Down Expand Up @@ -218,8 +219,19 @@ export class ChannelService {
private usersTypingInChannelSubject = new BehaviorSubject<UserResponse[]>([]);
private usersTypingInThreadSubject = new BehaviorSubject<UserResponse[]>([]);

private channelListSetter = (channels: Channel[]) => {
this.channelsSubject.next(channels);
private channelListSetter = (channels: (Channel | ChannelResponse)[]) => {
const currentChannels = this.channelsSubject.getValue() || [];
const newChannels = channels.filter(
(c) => !currentChannels.find((channel) => channel.cid === c.cid)
);
const deletedChannels = currentChannels.filter(
(c) => !channels?.find((channel) => channel.cid === c.cid)
);
this.addChannelsFromNotification(newChannels as ChannelResponse[]);
this.removeChannelsFromChannelList(deletedChannels.map((c) => c.cid));
if (!newChannels.length && !deletedChannels.length) {
this.channelsSubject.next(channels as Channel[]);
}
};

private messageListSetter = (messages: StreamMessage[]) => {
Expand Down Expand Up @@ -681,30 +693,30 @@ export class ChannelService {
}
}

private async handleNotification(notification: Notification) {
private handleNotification(notification: Notification) {
switch (notification.eventType) {
case 'notification.message_new': {
await this.ngZone.run(async () => {
this.ngZone.run(() => {
if (this.customNewMessageNotificationHandler) {
this.customNewMessageNotificationHandler(
notification,
this.channelListSetter
);
} else {
await this.handleNewMessageNotification(notification);
this.handleNewMessageNotification(notification);
}
});
break;
}
case 'notification.added_to_channel': {
await this.ngZone.run(async () => {
this.ngZone.run(() => {
if (this.customAddedToChannelNotificationHandler) {
this.customAddedToChannelNotificationHandler(
notification,
this.channelListSetter
);
} else {
await this.handleAddedToChannelNotification(notification);
this.handleAddedToChannelNotification(notification);
}
});
break;
Expand All @@ -726,36 +738,48 @@ export class ChannelService {

private handleRemovedFromChannelNotification(notification: Notification) {
const channelIdToBeRemoved = notification.event.channel!.cid;
this.removeFromChannelList(channelIdToBeRemoved);
this.removeChannelsFromChannelList([channelIdToBeRemoved]);
}

private async handleNewMessageNotification(notification: Notification) {
await this.addChannelFromNotification(notification);
private handleNewMessageNotification(notification: Notification) {
if (notification.event.channel) {
this.addChannelsFromNotification([notification.event.channel]);
}
}

private async handleAddedToChannelNotification(notification: Notification) {
await this.addChannelFromNotification(notification);
private handleAddedToChannelNotification(notification: Notification) {
if (notification.event.channel) {
this.addChannelsFromNotification([notification.event.channel]);
}
}

private async addChannelFromNotification(notification: Notification) {
const channel = this.chatClientService.chatClient.channel(
notification.event.channel?.type!,
notification.event.channel?.id
);
await channel.watch();
this.watchForChannelEvents(channel);
private addChannelsFromNotification(channelResponses: ChannelResponse[]) {
const newChannels: Channel[] = [];
channelResponses.forEach((channelResponse) => {
const channel = this.chatClientService.chatClient.channel(
channelResponse.type,
channelResponse.id
);
void channel.watch();
this.watchForChannelEvents(channel);
newChannels.push(channel);
});
this.channelsSubject.next([
channel,
...newChannels,
...(this.channelsSubject.getValue() || []),
]);
}

private removeFromChannelList(cid: string) {
const channels = this.channels.filter((c) => c.cid !== cid);
private removeChannelsFromChannelList(cids: string[]) {
const channels = this.channels.filter((c) => !cids.includes(c.cid || ''));
if (channels.length < this.channels.length) {
this.channelsSubject.next(channels);
if (this.activeChannelSubject.getValue()!.cid === cid) {
this.setAsActiveChannel(channels[0]);
if (cids.includes(this.activeChannelSubject.getValue()?.cid || '')) {
if (channels.length > 0) {
this.setAsActiveChannel(channels[0]);
} else {
this.activeChannelSubject.next(undefined);
}
}
}
}
Expand Down Expand Up @@ -1059,11 +1083,11 @@ export class ChannelService {
}

private handleChannelHidden(event: Event) {
this.removeFromChannelList(event.channel!.cid);
this.removeChannelsFromChannelList([event.channel!.cid]);
}

private handleChannelDeleted(event: Event) {
this.removeFromChannelList(event.channel!.cid);
this.removeChannelsFromChannelList([event.channel!.cid]);
}

private handleChannelVisible(event: Event, channel: Channel) {
Expand Down
83 changes: 83 additions & 0 deletions projects/stream-chat-angular/src/lib/chat-client.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ describe('ChatClientService', () => {
});

it('should disconnect user', async () => {
const pendingInvitesSpy = jasmine.createSpy();
service.pendingInvites$.subscribe(pendingInvitesSpy);
pendingInvitesSpy.calls.reset();
await service.disconnectUser();

expect(mockChatClient.disconnectUser).toHaveBeenCalledWith();
expect(pendingInvitesSpy).toHaveBeenCalledWith([]);
});

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

expect(result.length).toBe(2);
});

it('should initialize pending invites', async () => {
const channelsWithPendingInvites = [{ cid: 'cat-lovers' }];
mockChatClient.queryChannels.and.resolveTo(channelsWithPendingInvites);
const invitesSpy = jasmine.createSpy();
service.pendingInvites$.subscribe(invitesSpy);
invitesSpy.calls.reset();
await service.init(apiKey, userId, userToken);

expect(mockChatClient.queryChannels).toHaveBeenCalledWith(
{
invite: 'pending',
},
{},
{ user_id: mockChatClient.user.id }
);

expect(invitesSpy).toHaveBeenCalledWith(channelsWithPendingInvites);
});

it('should emit pending invitations of user', () => {
const invitesSpy = jasmine.createSpy();
service.pendingInvites$.subscribe(invitesSpy);
const event1 = {
id: 'mockevent',
type: 'notification.invited',
channel: { cid: 'what-i-ate-for-lunch' },
member: { user: mockChatClient.user },
} as any as Event;
mockChatClient.handleEvent(event1.type, event1);

expect(invitesSpy).toHaveBeenCalledWith([event1.channel]);

invitesSpy.calls.reset();
const event2 = {
id: 'mockevent',
type: 'notification.invited',
channel: { cid: 'gardening' },
member: { user: mockChatClient.user },
} as any as Event;
mockChatClient.handleEvent(event2.type, event2);

expect(invitesSpy).toHaveBeenCalledWith([event1.channel, event2.channel]);

invitesSpy.calls.reset();
const event3 = {
id: 'mockevent',
type: 'notification.invite_accepted',
channel: { cid: 'what-i-ate-for-lunch' },
member: { user: mockChatClient.user },
} as any as Event;
mockChatClient.handleEvent(event3.type, event3);

expect(invitesSpy).toHaveBeenCalledWith([event2.channel]);

invitesSpy.calls.reset();
const event4 = {
id: 'mockevent',
type: 'notification.invite_rejected',
channel: { cid: 'gardening' },
member: { user: mockChatClient.user },
} as any as Event;
mockChatClient.handleEvent(event4.type, event4);

expect(invitesSpy).toHaveBeenCalledWith([]);

invitesSpy.calls.reset();
const event5 = {
id: 'mockevent',
type: 'notification.invite_rejected',
channel: { cid: 'gardening' },
member: {
user: { id: `not${mockChatClient.user.id}` },
},
} as any as Event;
mockChatClient.handleEvent(event5.type, event5);

expect(invitesSpy).not.toHaveBeenCalled();
});
});
Loading