ng g c invite-button
Channel invites
This guide gives you a step-by-step tutorial on how to use channel invites in your chat application.
Invite button
The CustomTemplatesService
has a property 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:
HTML template
We create a simplistic UI with an “Invite users” button that opens a modal where users can search for other users in the application. The NotificationList
component is used to display any error messages that may occur during the invite request.
<button *ngIf="canInviteUsers" (click)="isModalOpen = true">
Invite users
</button>
<stream-modal [(isOpen)]="isModalOpen">
<div class="modal-content">
<div class="title">Invite users</div>
<div class="invited-users">
<div *ngFor="let u of usersToInvite">{{ u.name || u.id }}</div>
</div>
<div>
<input #input type="search" (change)="addUser()" list="app-users" />
<datalist id="app-users">
<option (select)="addUser()" *ngFor="let o of autocompleteOptions">
{{ o.name || o.id }}
</option>
</datalist>
</div>
<button class="invite" (click)="inviteMembers()">Send invitation(s)</button>
<stream-notification-list class="notifications"></stream-notification-list>
</div>
</stream-modal>
Styling
We are using stream-chat theme variables to match the default chat theme. You can read more about theme variables in our theming guide.
.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;
background-color: var(--str-chat__message-textarea-background-color);
color: var(--str-chat__message-textarea-color);
border-radius: var(--str-chat__message-textarea-border-radius);
border-block-start: var(--str-chat__message-textarea-border-block-start);
border-block-end: var(--str-chat__message-textarea-border-block-end);
border-inline-start: var(--str-chat__message-textarea-border-inline-start);
border-inline-end: var(--str-chat__message-textarea-border-inline-end);
}
.invited-users {
text-align: center;
}
.add {
margin-left: 5px;
}
.notifications {
width: 100%;
}
}
button {
background-color: var(--str-chat__cta-button-background-color);
border: none;
border-radius: var(--str-chat__cta-button-border-radius);
color: var(--str-chat__cta-button-color);
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 theChannelHeader
component - We check if the current user has the
update-channel-members
capability to see if they can invite members (it’s important to note that not every channel can be extended with new members) - The
autocompleteUsers
method of theChatClientService
can be used to search for users in the application - The
inviteMembers
method of theChannel
can be used to invite one or more members to the channel - The
NotificationService
can be used to notify the user about the result of the invite request
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<HTMLInputElement>;
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
.
Create the template (for example in your AppComponent
):
<ng-template #channelActionsTemplate let-channel="channel">
<app-invite-button [channel]="channel"></app-invite-button>
</ng-template>
Register the template in your TypeScript code (for example in your AppComponent
).
These are the necessary steps:
- Create a reference to the custom template
- Import the CustomTemplatesService
- Register your custom template
import {
AfterViewInit,
Component,
TemplateRef,
ViewChild,
} from "@angular/core";
import {
ChatClientService,
ChannelService,
StreamI18nService,
} from "stream-chat-angular";
import {
CustomTemplatesService,
ChannelActionsContext,
} from "stream-chat-angular";
import { environment } from "../environments/environment";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements AfterViewInit {
// Create a reference to the custom template
@ViewChild("channelActionsTemplate")
private channelActionsTemplate!: TemplateRef<ChannelActionsContext>;
constructor(
private chatService: ChatClientService,
private channelService: ChannelService,
private streamI18nService: StreamI18nService,
private customTemplatesService: CustomTemplatesService, // Import customTemplatesService
) {
void this.chatService.init(
environment.apiKey,
environment.userId,
environment.userToken,
);
void this.channelService.init({
type: "messaging",
members: { $in: [environment.userId] },
});
this.streamI18nService.setTranslation();
}
ngAfterViewInit(): void {
// Register your custom template
this.customTemplatesService.channelActionsTemplate$.next(
this.channelActionsTemplate,
);
}
}
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 important parts of the component:
- The component will be displayed inside the
NotificationList
component - We create an input with
Channel
type, this will be provided by theNotificationList
- We create an input called
dismissFn
, this will also be provided by theNotificationList
and can be used to dismiss the notification - The
ChatClientService
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 thechannel
- The invite can be rejected with the
rejectInvite
method of thechannel
The component:
import { Component, Input, OnInit } from "@angular/core";
import { Channel } from "stream-chat";
import { ChatClientService, NotificationService } from "stream-chat-angular";
@Component({
selector: "app-invitation",
template: `
<div>
You have been invited to the
{{ channelName }} channel. <button (click)="accept()">Accept</button> |
<button (click)="decline()">Decline</button> |
<button (click)="dismissFn()">Dismiss</button>
</div>
`,
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
component.
Add this to your app.component.html
file:
<ng-template #inviteTemplate let-channel="channel" let-dismissFn="dismissFn">
<app-invitation [channel]="channel" [dismissFn]="dismissFn"></app-invitation>
</ng-template>
Add a reference to the template in your app.component.ts
:
@ViewChild('inviteTemplate') private inviteTemplate!: TemplateRef<{channel: Channel<DefaultStreamChatGenerics> | ChannelResponse<DefaultStreamChatGenerics>}>;
Displaying the invitations
The ChatClientService
can keep track of pending invites, to enable this you have to initialize the service with the following flag:
this.chatService.init("<API key>", "<user>", "<token provider>", {
trackPendingChannelInvites: true,
});
The pendingInvites$
Observable on the ChatClientService
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
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: [<user id>]}}
), channels with pending and rejected invites will be returned and displayed in the channel list as well.
If this is not what you need, you can use the joined
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
, here is an example:
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 which means that you’ll have to override the default channel list behavior 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:
private async customAddedToChannelNotificationHandler(
clientEvent: ClientEvent,
channelListSetter: (channels: Channel<DefaultStreamChatGenerics>[]) => void
) {
if (clientEvent.event.member?.invited) {
return;
}
const channelResponse = clientEvent!.event!.channel!;
const newChanel = this.chatService.chatClient.channel(
channelResponse.type,
channelResponse.id
);
try {
await newChanel.watch();
const existingChannels = this.channelService.channels;
channelListSetter([newChanel, ...existingChannels]);
} catch (error) {
console.error('Failed to watch channel', error);
}
}
Now register the handler to the channel service 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
signals that the user accepted an invitation to a channel, we should add the channel to the channel list, we can do this by re-initializing the channel list.
Add this to the constructor of your app.component.ts
:
this.chatService.events$
.pipe(filter((n) => n.eventType === "notification.invite_accepted"))
.subscribe(() => {
this.channelService.reset();
void this.channelService.init({
joined: true,
});
});
If you’re doing this in a component other than AppComponent
, don’t forget to unsubscribe from the events$
Observable.