diff --git a/docusaurus/docs/Angular/assets/channel-invites-screenshot.png b/docusaurus/docs/Angular/assets/channel-invites-screenshot.png
new file mode 100644
index 00000000..c87cfd6c
Binary files /dev/null and b/docusaurus/docs/Angular/assets/channel-invites-screenshot.png differ
diff --git a/docusaurus/docs/Angular/assets/invite-button-screenshot.png b/docusaurus/docs/Angular/assets/invite-button-screenshot.png
new file mode 100644
index 00000000..20fefa63
Binary files /dev/null and b/docusaurus/docs/Angular/assets/invite-button-screenshot.png differ
diff --git a/docusaurus/docs/Angular/assets/invite-modal1-screenshot.png b/docusaurus/docs/Angular/assets/invite-modal1-screenshot.png
new file mode 100644
index 00000000..aa85ba66
Binary files /dev/null and b/docusaurus/docs/Angular/assets/invite-modal1-screenshot.png differ
diff --git a/docusaurus/docs/Angular/assets/invite-modal2-screenshot.png b/docusaurus/docs/Angular/assets/invite-modal2-screenshot.png
new file mode 100644
index 00000000..57db6ca7
Binary files /dev/null and b/docusaurus/docs/Angular/assets/invite-modal2-screenshot.png differ
diff --git a/docusaurus/docs/Angular/code-examples/channel-invites.mdx b/docusaurus/docs/Angular/code-examples/channel-invites.mdx
new file mode 100644
index 00000000..8d9769cd
--- /dev/null
+++ b/docusaurus/docs/Angular/code-examples/channel-invites.mdx
@@ -0,0 +1,454 @@
+---
+id: channel-invites
+title: Channel invites
+---
+
+import InviteButton from "../assets/invite-button-screenshot.png";
+import InviteModal1 from "../assets/invite-modal1-screenshot.png";
+import InviteModal2 from "../assets/invite-modal2-screenshot.png";
+import Invites from "../assets/channel-invites-screenshot.png";
+
+This guide gives you a step-by-step tutorial on how to use [channel invites](https://getstream.io/chat/docs/javascript/channel_invites/?language=javascript) in your chat application.
+
+## Invite button
+
+The [`ChannelHeader`](../components/ChannelHeaderComponent.mdx) component has an input called `channelActionsTemplate` that can be used to add action buttons to the channel header.
+
+Let's create a component for the invite button that we'll add to the channel header:
+
+```
+ng g c invite-button
+```
+
+### HTML template
+
+We create a simplistic UI with an "Invite users" button that opens a [modal](../components/ModalComponent.mdx) where users can search for other users in the application. The [`NotificationList` component](../components/NotificationListComponent.mdx) is used to display any error messages that may occur during the invite request.
+
+```html
+
+
+
+
Invite users
+
+
{{ u.name || u.id }}
+
+
+
+
+
+
+
+
+
+```
+
+### Styling
+
+We are using stream-chat theme variables to match the default chat theme. You can read more about theme variables in our [themeing guide](../concepts/themeing-and-css.mdx).
+
+```scss
+.modal-content {
+ width: 600px;
+ padding: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 15px;
+
+ .title {
+ font-size: 23px;
+ font-weight: 700;
+ }
+
+ input {
+ width: 200px;
+ padding: 10px;
+ border: none;
+ border-radius: var(--border-radius-md);
+ background-color: var(--grey-whisper);
+ }
+
+ .invited-users {
+ text-align: center;
+ }
+
+ .add {
+ margin-left: 5px;
+ }
+
+ .notifications {
+ width: 100%;
+ }
+}
+
+button {
+ background-color: var(--primary-color);
+ border: none;
+ border-radius: var(--border-radius-md);
+ color: white;
+ padding: 10px;
+ cursor: pointer;
+}
+```
+
+### Component logic
+
+Let's break down the most important parts of the component's logic:
+
+- We define an input with the type `Channel` to access the current active channel - this will be provided by the [`ChannelHeader`](../components/ChannelHeaderComponent.mdx) component
+- We check if the current user has the `update-channel-members` [capability](https://getstream.io/chat/docs/javascript/channel_capabilities/?language=javascript) to see if they can invite members (it's important to note that [not every channel can be extended with new members](https://getstream.io/chat/docs/javascript/creating_channels/?language=javascript#2.-creating-a-channel-for-a-list-of-members))
+- The `autocompleteUsers` method of the [`ChatClientService`](../services/ChatClientService.mdx) can be used to search for users in the application
+- The `inviteMembers` method of the `Channel` can be used to invite one or more members to the channel
+- The [`NotificationService`](../services/NotificationService.mdx) can be used to notify the user about the result of the invite request
+
+```typescript
+import {
+ Component,
+ ElementRef,
+ Input,
+ OnChanges,
+ OnInit,
+ SimpleChanges,
+ ViewChild,
+} from "@angular/core";
+import { fromEvent } from "rxjs";
+import { debounceTime, switchMap } from "rxjs/operators";
+import { Channel, UserResponse } from "stream-chat";
+import { ChatClientService, NotificationService } from "stream-chat-angular";
+
+@Component({
+ selector: "app-invite-button",
+ templateUrl: "./invite-button.component.html",
+ styleUrls: ["./invite-button.component.scss"],
+})
+export class InviteButtonComponent implements OnInit, OnChanges {
+ @Input() channel?: Channel;
+ usersToInvite: UserResponse[] = [];
+ canInviteUsers = false;
+ isModalOpen = false;
+ autocompleteOptions: UserResponse[] = [];
+ @ViewChild("input", { static: true })
+ private input!: ElementRef;
+
+ constructor(
+ private chatClientService: ChatClientService,
+ private notificationService: NotificationService
+ ) {}
+
+ ngOnInit(): void {
+ fromEvent(this.input.nativeElement, "input")
+ .pipe(
+ debounceTime(300),
+ switchMap(() =>
+ this.chatClientService.autocompleteUsers(
+ this.input.nativeElement.value
+ )
+ )
+ )
+ .subscribe((users) => (this.autocompleteOptions = users));
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.channel && this.channel) {
+ this.canInviteUsers = (
+ this.channel.data?.own_capabilities as string[]
+ ).includes("update-channel-members");
+ this.usersToInvite = [];
+ this.autocompleteOptions = [];
+ }
+ }
+
+ async inviteMembers() {
+ try {
+ await this.channel?.inviteMembers(this.usersToInvite.map((u) => u.id));
+ this.notificationService.addTemporaryNotification(
+ "User(s) successfully invited",
+ "success"
+ );
+ this.usersToInvite = [];
+ this.autocompleteOptions = [];
+ this.isModalOpen = false;
+ } catch {
+ this.notificationService.addTemporaryNotification(
+ `User(s) couldn't be invited`,
+ "error"
+ );
+ }
+ }
+
+ addUser() {
+ const inputValue = this.input.nativeElement.value;
+ const user = this.autocompleteOptions.find(
+ (u) => u.id === inputValue || u.name === inputValue
+ );
+ if (user) {
+ this.usersToInvite.push(user);
+ this.input.nativeElement.value = "";
+ this.autocompleteOptions = [];
+ }
+ }
+}
+```
+
+### Providing the invite button to the channel header
+
+Lastly, we provide the `InviteButton` component to the `ChannelHeader`
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+This is how the channel header looks with the invite button present:
+
+
+
+And this is how the invite modal turned out:
+
+
+
+
+
+## Pending invitations
+
+The next step is to show the pending invitations to the user.
+
+### Invitation notification component
+
+First we create a component that will display a pending invitation:
+
+```
+ng g c invitation --inline-template --inline-style
+```
+
+Here are the most importnat parts of the component:
+
+- The component will be displayed inside the [`NotificationList`](../components/NotificationListComponent.mdx) component
+- We create an input with `Channel` type, this will be provided by the `NotificationList`
+- We create an input called `dismissFn`, this will also be provided by the `NotificationList` and can be used to dismiss the notification
+- The [`ChatClientService`](../services/ChatClientService.mdx) can be used to get the current chat user's id, this will be necessary when accepting/rejecting the invite
+- The invite can be accepted with the `acceptInvite` method of the `channel`
+- The invite can be rejected with the `rejectInvite` method of the `channel`
+
+The component:
+
+```typescript
+import { Component, Input, OnInit } from "@angular/core";
+import { Channel } from "stream-chat";
+import { ChatClientService, NotificationService } from "stream-chat-angular";
+
+@Component({
+ selector: "app-invitation",
+ template: `
+
+ You have been invited to the
+ {{ channelName }} channel. |
+ |
+
+
+ `,
+ styles: [
+ "button {border: none; background-color: transparent; color: var(--blue); font-weight: bold; cursor: pointer}",
+ ],
+})
+export class InvitationComponent implements OnInit {
+ @Input() channel?: Channel;
+ @Input() dismissFn!: Function;
+
+ constructor(
+ private notificationService: NotificationService,
+ private chatClientService: ChatClientService
+ ) {}
+
+ ngOnInit(): void {}
+
+ accept() {
+ this.sendRequest("accept");
+ }
+
+ async decline() {
+ this.sendRequest("reject");
+ }
+
+ get channelName() {
+ return this.channel?.data?.name || this.channel?.id;
+ }
+
+ private async sendRequest(answer: "accept" | "reject") {
+ const payload = {
+ user_id: this.chatClientService?.chatClient.user?.id,
+ };
+ const request =
+ answer === "accept"
+ ? async () => await this.channel?.acceptInvite(payload)
+ : async () => await this.channel?.rejectInvite(payload);
+ try {
+ await request();
+ this.dismissFn();
+ this.notificationService.addTemporaryNotification(
+ `Invite from ${this.channelName} successfully ${answer}ed`,
+ "success"
+ );
+ } catch {
+ this.notificationService.addTemporaryNotification(
+ `An error occured during ${answer}ing the invitation from ${this.channelName}`,
+ "error"
+ );
+ }
+ }
+}
+```
+
+### Displaying the invitations
+
+The next step will be to display the invitations.
+
+#### Invitation template
+
+We will have to create the invitation template that can be passed to the [`NotificationList`](../components/NotificationListComponent.mdx) component.
+
+Add this to your `app.component.html` file:
+
+```html
+
+
+
+```
+
+Add a reference to the template in your `app.component.ts`:
+
+```typescript
+@ViewChild('inviteTemplate') private inviteTemplate!: TemplateRef<{
+ channel: Channel | ChannelResponse;
+}>;
+```
+
+#### Displaying the invitations
+
+The `pendingInvites$` Observable on the [`ChatClientService`](../services/ChatClientService.mdx) can notify us about the pending invitations of the current user. Let's subscribe to this Observable and display the invites in the `ngOnInit` method of the `app.component.ts`
+
+```typescript
+ngOnInit(): void {
+ this.chatService.pendingInvites$.pipe(pairwise()).subscribe((pair) => {
+ const [prevInvites, currentInvites] = pair;
+ const notShownInvites = currentInvites.filter(
+ (i) => !prevInvites.find((prevI) => prevI.cid === i.cid)
+ );
+ notShownInvites.forEach((i) =>
+ this.notificationService.addPermanentNotification(
+ this.inviteTemplate,
+ 'info',
+ undefined,
+ { channel: i }
+ )
+ );
+ });
+}
+```
+
+The above method will display all the pending invitations on page load and display every new invitation received later.
+
+This is how the invitation notifications look like:
+
+
+
+## Channel list
+
+### Channel filter
+
+If a user is invited to a channel they immediately become member of the channel (the membership applies even if the invite is rejected).
+This means that if you use a channel filter that is based on membership (for example `{members: {$in: []}}`), channels with pending and recejted invites will be returned and displayed in the channel list as well.
+If this is not what you need, you can use the [`joined`](https://getstream.io/chat/docs/javascript/query_channels/?language=javascript#channel-queryable-built-in-fields) flag to only list channels that the user was directly added to or the invitation was accepted by the user.
+
+The channel filter can be provided to the `init` method of the [`ChannelService`](../services/ChannelService.mdx), here is an example:
+
+```typescript
+this.channelService.init({
+ joined: true,
+});
+```
+
+### `notification.added_to_channel` event
+
+It's important to note that the filtering set above is not applied to [events](https://getstream.io/chat/docs/javascript/event_object/?language=javascript) which means that you'll have to override the [default channel list behavior](../services/ChannelService.mdx/#channels) if you don't want channels with pending invites to be added to the channel list when a `notification.added_to_channel` event is received.
+
+To override the default behavior create a custom event handler in `app.component.ts` that checks if the user was invited to the channel or added directly and only adds the channel to the list if the user was added directly:
+
+```typescript
+private customAddedToChannelNotificationHandler(
+ notification: Notification,
+ channelListSetter: (channels: (Channel | ChannelResponse)[]) => void
+ ): void {
+ let channels!: Channel[];
+ this.channelService.channels$
+ .pipe(take(1))
+ .subscribe((c) => (channels = c || []));
+ if (notification.event.member?.invited) {
+ return;
+ }
+ channelListSetter([notification!.event!.channel!, ...channels]);
+ }
+```
+
+:::important
+Make sure to add the following import to your code (your IDE might not be able to add this automatically):
+
+`import { Notification } from 'stream-chat-angular';`
+:::
+
+Now register the handler to the [channel service](../services/ChannelService.mdx) in the constructor of `app.component.ts`:
+
+```
+this.channelService.customAddedToChannelNotificationHandler =
+ this.customAddedToChannelNotificationHandler.bind(this);
+```
+
+### `notification.invite_accepted` event
+
+The `notification.invite_accepted` event emitted by the [`ChatClientService`](../services/ChatClientService.mdx) signals that the user accepted an invitation to a channel, we should add the channel to the channel list, we can do this by reinitializing the channel list.
+
+Add this to the constructor of your `app.component.ts`:
+
+```typescript
+this.chatService.notification$
+ .pipe(filter((n) => n.eventType === "notification.invite_accepted"))
+ .subscribe(() => {
+ this.channelService.reset();
+ void this.channelService.init({
+ joined: true,
+ });
+ });
+```
diff --git a/projects/stream-chat-angular/.eslintrc.json b/projects/stream-chat-angular/.eslintrc.json
index ab588d88..cae3081d 100644
--- a/projects/stream-chat-angular/.eslintrc.json
+++ b/projects/stream-chat-angular/.eslintrc.json
@@ -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,
@@ -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,
diff --git a/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.html b/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.html
index 14d685f2..6d66c3ba 100644
--- a/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.html
+++ b/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.html
@@ -20,4 +20,12 @@
translate:watcherCountParam) : ''}}
+
+
+
diff --git a/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.ts b/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.ts
index 8c5a5e2e..5961f86d 100644
--- a/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.ts
+++ b/projects/stream-chat-angular/src/lib/channel-header/channel-header.component.ts
@@ -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';
@@ -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;
diff --git a/projects/stream-chat-angular/src/lib/channel.service.ts b/projects/stream-chat-angular/src/lib/channel.service.ts
index 45250759..514f327f 100644
--- a/projects/stream-chat-angular/src/lib/channel.service.ts
+++ b/projects/stream-chat-angular/src/lib/channel.service.ts
@@ -11,6 +11,7 @@ import {
Channel,
ChannelFilters,
ChannelOptions,
+ ChannelResponse,
ChannelSort,
Event,
FormatMessageResponse,
@@ -98,7 +99,7 @@ export class ChannelService {
*/
usersTypingInThread$: Observable;
/**
- * 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 }>;
/**
@@ -106,21 +107,21 @@ export class ChannelService {
*/
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)
@@ -128,7 +129,7 @@ export class ChannelService {
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -218,8 +219,19 @@ export class ChannelService {
private usersTypingInChannelSubject = new BehaviorSubject([]);
private usersTypingInThreadSubject = new BehaviorSubject([]);
- 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[]) => {
@@ -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;
@@ -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);
+ }
}
}
}
@@ -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) {
diff --git a/projects/stream-chat-angular/src/lib/chat-client.service.spec.ts b/projects/stream-chat-angular/src/lib/chat-client.service.spec.ts
index e5b9c857..c15c811b 100644
--- a/projects/stream-chat-angular/src/lib/chat-client.service.spec.ts
+++ b/projects/stream-chat-angular/src/lib/chat-client.service.spec.ts
@@ -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 () => {
@@ -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();
+ });
});
diff --git a/projects/stream-chat-angular/src/lib/chat-client.service.ts b/projects/stream-chat-angular/src/lib/chat-client.service.ts
index f17948f3..168b7836 100644
--- a/projects/stream-chat-angular/src/lib/chat-client.service.ts
+++ b/projects/stream-chat-angular/src/lib/chat-client.service.ts
@@ -1,6 +1,11 @@
import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
-import { OwnUserResponse, UserResponse } from 'stream-chat';
+import {
+ Channel,
+ ChannelResponse,
+ OwnUserResponse,
+ UserResponse,
+} from 'stream-chat';
import { AppSettings, Event, StreamChat, TokenOrProvider } from 'stream-chat';
import { version } from '../assets/version';
import { NotificationService } from './notification.service';
@@ -36,11 +41,18 @@ export class ChatClientService {
* Emits the current connection state of the user (`online` or `offline`)
*/
connectionState$: Observable<'offline' | 'online'>;
+ /**
+ * Emits the list of pending invites of the user. It emits every pending invitation during initialization and then extends the list when a new invite is received. More information can be found in the [channel invitations](../code-examples/channel-invites.mdx) guide.
+ */
+ pendingInvites$: Observable<(ChannelResponse | Channel)[]>;
private notificationSubject = new ReplaySubject(1);
private connectionStateSubject = new ReplaySubject<'offline' | 'online'>(1);
private appSettingsSubject = new BehaviorSubject(
undefined
);
+ private pendingInvitesSubject = new BehaviorSubject<
+ (ChannelResponse | Channel)[]
+ >([]);
constructor(
private ngZone: NgZone,
@@ -49,6 +61,7 @@ export class ChatClientService {
this.notification$ = this.notificationSubject.asObservable();
this.connectionState$ = this.connectionStateSubject.asObservable();
this.appSettings$ = this.appSettingsSubject.asObservable();
+ this.pendingInvites$ = this.pendingInvitesSubject.asObservable();
}
/**
@@ -63,16 +76,23 @@ export class ChatClientService {
userTokenOrProvider: TokenOrProvider
) {
this.chatClient = StreamChat.getInstance(apiKey);
+ this.chatClient.devToken;
await this.ngZone.runOutsideAngular(async () => {
const user = typeof userOrId === 'string' ? { id: userOrId } : userOrId;
await this.chatClient.connectUser(user, userTokenOrProvider);
this.chatClient.setUserAgent(
`stream-chat-angular-${version}-${this.chatClient.getUserAgent()}`
);
- this.chatClient.getAppSettings;
});
+ const channels = await this.chatClient.queryChannels(
+ { invite: 'pending' },
+ {},
+ { user_id: this.chatClient.user?.id }
+ );
+ this.pendingInvitesSubject.next(channels);
this.appSettingsSubject.next(undefined);
this.chatClient.on((e) => {
+ this.updatePendingInvites(e);
this.notificationSubject.next({
eventType: e.type,
event: e,
@@ -101,6 +121,7 @@ export class ChatClientService {
* Disconnects the current user, and closes the WebSocket connection. Useful when disconnecting a chat user, use in combination with [`reset`](./ChannelService.mdx/#reset).
*/
async disconnectUser() {
+ this.pendingInvitesSubject.next([]);
await this.chatClient.disconnectUser();
}
@@ -141,4 +162,24 @@ export class ChatClientService {
});
return result.users;
}
+
+ private updatePendingInvites(e: Event) {
+ if (e.member?.user?.id === this.chatClient.user?.id && e.channel) {
+ const pendingInvites = this.pendingInvitesSubject.getValue();
+ if (e.type === 'notification.invited') {
+ this.pendingInvitesSubject.next([...pendingInvites, e.channel]);
+ } else if (
+ e.type === 'notification.invite_accepted' ||
+ e.type === 'notification.invite_rejected'
+ ) {
+ const index = pendingInvites.findIndex(
+ (i) => i?.cid === e.channel?.cid
+ );
+ if (index !== -1) {
+ pendingInvites.splice(index, 1);
+ this.pendingInvitesSubject.next([...pendingInvites]);
+ }
+ }
+ }
+ }
}
diff --git a/projects/stream-chat-angular/src/lib/mocks/index.ts b/projects/stream-chat-angular/src/lib/mocks/index.ts
index 64635aad..40c3e2ec 100644
--- a/projects/stream-chat-angular/src/lib/mocks/index.ts
+++ b/projects/stream-chat-angular/src/lib/mocks/index.ts
@@ -279,6 +279,7 @@ export type MockStreamChatClient = {
getUserAgent: () => string;
getAppSettings: jasmine.Spy;
disconnectUser: jasmine.Spy;
+ queryChannels: jasmine.Spy;
};
export const mockStreamChatClient = (): MockStreamChatClient => {
@@ -288,6 +289,7 @@ export const mockStreamChatClient = (): MockStreamChatClient => {
const flagMessage = jasmine.createSpy();
const setUserAgent = jasmine.createSpy();
const queryUsers = jasmine.createSpy();
+ const queryChannels = jasmine.createSpy().and.returnValue([]);
const getAppSettings = jasmine.createSpy().and.returnValue({
app: {
file_upload_config: {
@@ -336,6 +338,7 @@ export const mockStreamChatClient = (): MockStreamChatClient => {
queryUsers,
getAppSettings,
appSettings$,
+ queryChannels,
};
};
diff --git a/projects/stream-chat-angular/src/lib/notification-list/notification-list.component.html b/projects/stream-chat-angular/src/lib/notification-list/notification-list.component.html
index 03d71e8f..af450fff 100644
--- a/projects/stream-chat-angular/src/lib/notification-list/notification-list.component.html
+++ b/projects/stream-chat-angular/src/lib/notification-list/notification-list.component.html
@@ -1,9 +1,21 @@