diff --git a/docusaurus/docs/Angular/assets/mention-screenshot.png b/docusaurus/docs/Angular/assets/mention-screenshot.png new file mode 100644 index 00000000..d403737a Binary files /dev/null and b/docusaurus/docs/Angular/assets/mention-screenshot.png differ diff --git a/docusaurus/docs/Angular/code-examples/_category_.json b/docusaurus/docs/Angular/code-examples/_category_.json new file mode 100644 index 00000000..5f766450 --- /dev/null +++ b/docusaurus/docs/Angular/code-examples/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Code examples", + "position": 6 +} diff --git a/docusaurus/docs/Angular/code-examples/mention-actions.mdx b/docusaurus/docs/Angular/code-examples/mention-actions.mdx new file mode 100644 index 00000000..3c5bcf97 --- /dev/null +++ b/docusaurus/docs/Angular/code-examples/mention-actions.mdx @@ -0,0 +1,138 @@ +--- +id: mention-actions +sidebar_position: 1 +title: Mention actions +--- + +import Screenshot from "../assets/mention-screenshot.png"; + +In this example, we will demonstrate how to create custom mention actions if a user clicks on or hovers over a mention in a message. + +## Custom mention template + +You can provide a custom message template to the `MessageList` component: + +```html +
+ + + + + + + +
+ + + @{{ user.name || user.id }} + +``` + +The `MessageList` component will provide the mentioned user in a variable called `user`, the object has a [`UserResponse`](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts) type and you can use it to display information about the user. + +This template looks and works like the default mention template, however you can add interactions to this element. + +## Display user information on click + +In this step we will create a popover with additional user information that will be displayed if the user clicks on a mention. We are using the [`ngx-popperjs`](https://www.npmjs.com/package/ngx-popperjs) library for the popover, but you can use a different library as well. + +### Install `ngx-popperjs` + +Let's install the necessary dependencies: + +```bash +npm install @popperjs/core ngx-popperjs --save +``` + +### Import the `NgxPopperjsModule` module + +```typescript +import {NgxPopperjsModule} from 'ngx-popperjs'; + +@NgModule({ + // ... + imports: [ + // ... + NgxPopperjsModule + ] +}) +``` + +### Import SCSS + +Add this to your `styles.scss`: + +```scss +@import "~ngx-popperjs/scss/theme-white"; +``` + +[CSS is also supported.](https://www.npmjs.com/package/ngx-popperjs) + +### Create the popover + +```html + + + @{{ user.name || user.id }} + + +
+ + {{ user.name || user.id }} +  {{ user.online ? "online" : "offline" }} +
+
+
+``` + +This is the popover that is displayed if we click on a mention in a message: + + + +[`ngx-popperjs`](https://www.npmjs.com/package/ngx-popperjs) has a lot of other configuration options, feel free to explore those. + +## Display user information on hover + +Let's modify our solution and display the popover on hover instead of click: + +We need to add a variable to our component class: + +```typescript +import { NgxPopperjsTriggers } from "ngx-popperjs"; + +popoverTrigger = NgxPopperjsTriggers.hover; +``` + +And modify our template: + +```html + + + @{{ user.name || user.id }} + + +
+ + {{ user.name || user.id }} +  {{ user.online ? "online" : "offline" }} +
+
+
+``` diff --git a/docusaurus/docs/Angular/components/channel-preview.mdx b/docusaurus/docs/Angular/components/channel-preview.mdx index 3f3cbb24..1a46ae9f 100644 --- a/docusaurus/docs/Angular/components/channel-preview.mdx +++ b/docusaurus/docs/Angular/components/channel-preview.mdx @@ -27,6 +27,8 @@ The channel list uses the `ChannelPreview` component to display channels, if you ``` +Your custom template can receive the same inputs (with the same name) as the default `ChannelPreview` component. + :::note If you want to build your own `ChannelPreview` component, you might find the following building blocks useful: diff --git a/docusaurus/docs/Angular/components/message-input.mdx b/docusaurus/docs/Angular/components/message-input.mdx index 4eeb2315..7cf838cd 100644 --- a/docusaurus/docs/Angular/components/message-input.mdx +++ b/docusaurus/docs/Angular/components/message-input.mdx @@ -38,6 +38,8 @@ If you want to create your own message input, here is how to use it: The default chat UI uses the message input in two different places: at the bottom of the channel to send new message, and in the message list to edit a message. +Your custom template can receive the same inputs (with the same name) as the default `MessageInput` component. + :::note If you want to create your own message input, you can use the following building blocks: diff --git a/docusaurus/docs/Angular/components/message-list.mdx b/docusaurus/docs/Angular/components/message-list.mdx index 3b432257..f9b75e67 100644 --- a/docusaurus/docs/Angular/components/message-list.mdx +++ b/docusaurus/docs/Angular/components/message-list.mdx @@ -67,6 +67,14 @@ The input used for message edit. By default, the [default message input componen | ----------- | | TemplateRef | +### mentionTemplate + +The template used to display a mention in a message. It receives the mentioned user in a variable called `user` with the type [`UserResponse`](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts). You can provide your own template if you want to [add actions to mentions](../code-examples/mention-actions.mdx). + +| Type | +| ----------- | +| TemplateRef | + ### areReactionsEnabled If true, the message reactions are displayed. Users can also react to messages if they have the necessary [channel capability](https://getstream.io/chat/docs/javascript/channel_capabilities/?language=javascript). diff --git a/docusaurus/docs/Angular/components/message.mdx b/docusaurus/docs/Angular/components/message.mdx index 6c686ae3..727bb8e7 100644 --- a/docusaurus/docs/Angular/components/message.mdx +++ b/docusaurus/docs/Angular/components/message.mdx @@ -53,6 +53,8 @@ The message list uses the `Message` component to display messages, if you want t ``` +Your custom template can receive the same inputs (with the same name) as the default `Message` component. + :::note If you want to build your own `Message` component, you might find the following building blocks useful: @@ -157,3 +159,11 @@ The input used for message edit. By default, the [default message input componen | Type | | ----------- | | TemplateRef | + +### mentionTemplate + +The template used to display a mention in a message. It receives the mentioned user in a variable called `user` with the type [`UserResponse`](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts). You can provide your own template if you want to [add actions to mentions](../code-examples/mention-actions.mdx). + +| Type | +| ----------- | +| TemplateRef | diff --git a/docusaurus/docs/Angular/pipes/_category_.json b/docusaurus/docs/Angular/pipes/_category_.json deleted file mode 100644 index 45de3d46..00000000 --- a/docusaurus/docs/Angular/pipes/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Pipes", - "position": 3 -} diff --git a/docusaurus/docs/Angular/pipes/mentions-highlight.mdx b/docusaurus/docs/Angular/pipes/mentions-highlight.mdx deleted file mode 100644 index c2586eb6..00000000 --- a/docusaurus/docs/Angular/pipes/mentions-highlight.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: mentions-hightlight -sidebar_position: 1 -title: Highlight mentions ---- - -The `HighlightMentions` pipe can be used to hightlight mentions (for example @Jack) in a message. - -## Basic usage - -You can use the `HighlightMentions` pipe if you want to create your own message component to override the default message component: - -```typescript -@Component({ - selector: "app-custom-message", - template: ` -
- - `, -}) -export class CustomMessageComponent { - @Input() message: StreamMessage; -} -``` - -## Inputs - -### message - -The text of the message (either `message.html` or `message.text` property of a [`StreamMessage`](../types/stream-message.mdx) object) - -| Type | -| ------ | -| string | - -### mentionedUsers - -The array of users mentioned in the message (`message.mentioned_users` property of a [`StreamMessage`](../types/stream-message.mdx) object) - -| Type | -| ---------------------------------------------------------------------------------------- | -| [`UserResponse[]`](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts) | - -# Output - -Returns a string where the user mentions will be bold. diff --git a/projects/stream-chat-angular/src/lib/message-list/message-list.component.html b/projects/stream-chat-angular/src/lib/message-list/message-list.component.html index bf695ebc..bb4efc2a 100644 --- a/projects/stream-chat-angular/src/lib/message-list/message-list.component.html +++ b/projects/stream-chat-angular/src/lib/message-list/message-list.component.html @@ -27,7 +27,8 @@ lastSentMessageId && message?.id === lastSentMessageId ), canReceiveReadEvents: canReceiveReadEvents, - messageInputTemplate: messageInputTemplate + messageInputTemplate: messageInputTemplate, + mentionTemplate: mentionTemplate } " > @@ -43,6 +44,7 @@ [enabledMessageActions]="enabledMessageActions" [canReceiveReadEvents]="canReceiveReadEvents" [messageInputTemplate]="messageInputTemplate" + [mentionTemplate]="mentionTemplate" > diff --git a/projects/stream-chat-angular/src/lib/message-list/message-list.component.spec.ts b/projects/stream-chat-angular/src/lib/message-list/message-list.component.spec.ts index 1575fd8b..3755be6f 100644 --- a/projects/stream-chat-angular/src/lib/message-list/message-list.component.spec.ts +++ b/projects/stream-chat-angular/src/lib/message-list/message-list.component.spec.ts @@ -10,7 +10,6 @@ import { TranslateModule } from '@ngx-translate/core'; import { Channel } from 'stream-chat'; import { ChannelService } from '../channel.service'; import { ChatClientService } from '../chat-client.service'; -import { HighlightMentionsPipe } from '../message/highlight-mentions.pipe'; import { MessageComponent } from '../message/message.component'; import { MockChannelService, @@ -35,11 +34,7 @@ describe('MessageListComponent', () => { channelServiceMock = mockChannelService(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], - declarations: [ - MessageComponent, - MessageListComponent, - HighlightMentionsPipe, - ], + declarations: [MessageComponent, MessageListComponent], providers: [ { provide: ChannelService, useValue: channelServiceMock }, { diff --git a/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts b/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts index a428bba8..dcc6a0da 100644 --- a/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts +++ b/projects/stream-chat-angular/src/lib/message-list/message-list.component.ts @@ -25,6 +25,7 @@ import { MessageActions } from '../message-actions-box/message-actions-box.compo export class MessageListComponent implements AfterViewChecked, OnChanges { @Input() messageTemplate: TemplateRef | undefined; @Input() messageInputTemplate: TemplateRef | undefined; + @Input() mentionTemplate: TemplateRef | undefined; @Input() areReactionsEnabled = true; /* eslint-disable-next-line @angular-eslint/no-input-rename */ @Input('enabledMessageActions') enabledMessageActionsInput: MessageActions[] = diff --git a/projects/stream-chat-angular/src/lib/message/highlight-mentions.pipe.spec.ts b/projects/stream-chat-angular/src/lib/message/highlight-mentions.pipe.spec.ts deleted file mode 100644 index 49ff42b2..00000000 --- a/projects/stream-chat-angular/src/lib/message/highlight-mentions.pipe.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HighlightMentionsPipe } from './highlight-mentions.pipe'; - -describe('HighlightMentionsPipe', () => { - it('should highlight mentioned users', () => { - const pipe = new HighlightMentionsPipe(); - const mentionedUsers = [{ id: 'jack', name: 'Jack' }, { id: 'sara' }]; - const text = - 'Hi @Jack! This is my email address: jack@gmail.com. Do you know anything about @sara? Bye @Jack'; - const hightlightedText = - 'Hi @Jack! This is my email address: jack@gmail.com. Do you know anything about @sara? Bye @Jack'; - - expect(pipe.transform(text, mentionedUsers)).toEqual(hightlightedText); - }); -}); diff --git a/projects/stream-chat-angular/src/lib/message/highlight-mentions.pipe.ts b/projects/stream-chat-angular/src/lib/message/highlight-mentions.pipe.ts deleted file mode 100644 index bba23c91..00000000 --- a/projects/stream-chat-angular/src/lib/message/highlight-mentions.pipe.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { UserResponse } from 'stream-chat'; - -@Pipe({ - name: 'highlightMentions', -}) -export class HighlightMentionsPipe implements PipeTransform { - transform(value?: string, mentionedUsers?: UserResponse[]): string { - if (!value || !mentionedUsers) { - return value || ''; - } - let result = value; - mentionedUsers.forEach((u) => { - result = result.replace( - new RegExp(`@${u.name || u.id}`, 'g'), - `@${u.name || u.id}` - ); - }); - - return result; - } -} diff --git a/projects/stream-chat-angular/src/lib/message/message.component.html b/projects/stream-chat-angular/src/lib/message/message.component.html index 764a523f..ff5e3ec6 100644 --- a/projects/stream-chat-angular/src/lib/message/message.component.html +++ b/projects/stream-chat-angular/src/lib/message/message.component.html @@ -144,11 +144,30 @@ (click)="textClicked()" (keyup.enter)="textClicked()" data-testid="text" - [innerHTML]=" - message?.html || message?.text - | highlightMentions: message?.mentioned_users - " - > + > +

+ + + + + + + + + {{ part.content }} + + + +

+
diff --git a/projects/stream-chat-angular/src/lib/message/message.component.spec.ts b/projects/stream-chat-angular/src/lib/message/message.component.spec.ts index 891f35e0..28528047 100644 --- a/projects/stream-chat-angular/src/lib/message/message.component.spec.ts +++ b/projects/stream-chat-angular/src/lib/message/message.component.spec.ts @@ -14,7 +14,7 @@ import { AttachmentListComponent } from '../attachment-list/attachment-list.comp import { MessageReactionsComponent } from '../message-reactions/message-reactions.component'; import { TranslateModule } from '@ngx-translate/core'; import { ChannelService } from '../channel.service'; -import { HighlightMentionsPipe } from './highlight-mentions.pipe'; +import { SimpleChange } from '@angular/core'; describe('MessageComponent', () => { let component: MessageComponent; @@ -58,7 +58,6 @@ describe('MessageComponent', () => { MessageActionsBoxComponent, AttachmentListComponent, MessageReactionsComponent, - HighlightMentionsPipe, ], providers: [ { @@ -104,6 +103,7 @@ describe('MessageComponent', () => { nativeElement.querySelector('[data-testid="message-actions-container"]'); message = mockMessage(); component.message = message; + component.ngOnChanges({ message: {} as any as SimpleChange }); fixture.detectChanges(); messageActionsBoxComponent = fixture.debugElement.query( By.directive(MessageActionsBoxComponent) @@ -627,9 +627,10 @@ describe('MessageComponent', () => { const htmlContent = 'https://getstream.io/'; component.message = { ...component.message!, ...{ html: htmlContent } }; + component.ngOnChanges({ message: {} as any as SimpleChange }); fixture.detectChanges(); - expect(queryText()?.innerHTML).toEqual(htmlContent); + expect(queryText()?.innerHTML).toContain(htmlContent); }); it('should resend message, if sending is failed', () => { @@ -711,4 +712,117 @@ describe('MessageComponent', () => { expect(component.isEditing).toBeTrue(); }); + + it('should create message parts', () => { + component.message = { + text: '', + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([]); + + component.message = { + text: 'This is a message without user mentions', + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { content: 'This is a message without user mentions', type: 'text' }, + ]); + + component.message = { + text: 'This is just an email, not a mention test@test.com', + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { + content: 'This is just an email, not a mention test@test.com', + type: 'text', + }, + ]); + + component.message = { + html: '

This is just an email, not a mention test@test.com

\n', + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { + content: 'This is just an email, not a mention test@test.com', + type: 'text', + }, + ]); + + component.message = { + text: 'Hello @Jack', + mentioned_users: [{ id: 'jack', name: 'Jack' }], + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { content: 'Hello ', type: 'text' }, + { + content: '@Jack', + type: 'mention', + user: { id: 'jack', name: 'Jack' }, + }, + ]); + + component.message = { + text: 'Hello @Jack, how are you?', + mentioned_users: [{ id: 'jack', name: 'Jack' }], + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { content: 'Hello ', type: 'text' }, + { + content: '@Jack', + type: 'mention', + user: { id: 'jack', name: 'Jack' }, + }, + { content: ', how are you?', type: 'text' }, + ]); + + component.message = { + text: 'Hello @Jack and @Lucie, how are you?', + mentioned_users: [ + { id: 'id2334', name: 'Jack' }, + { id: 'id3444', name: 'Lucie' }, + ], + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { content: 'Hello ', type: 'text' }, + { + content: '@Jack', + type: 'mention', + user: { id: 'id2334', name: 'Jack' }, + }, + { content: ' and ', type: 'text' }, + { + content: '@Lucie', + type: 'mention', + user: { id: 'id3444', name: 'Lucie' }, + }, + { content: ', how are you?', type: 'text' }, + ]); + + component.message = { + html: `

https://getstream.io/ this is the link @sara

\n`, + mentioned_users: [{ id: 'sara' }], + } as StreamMessage; + component.ngOnChanges({ message: {} as any as SimpleChange }); + + expect(component.messageTextParts).toEqual([ + { + content: + 'https://getstream.io/ this is the link ', + type: 'text', + }, + { content: '@sara', type: 'mention', user: { id: 'sara' } }, + ]); + }); }); diff --git a/projects/stream-chat-angular/src/lib/message/message.component.ts b/projects/stream-chat-angular/src/lib/message/message.component.ts index 693b73ef..125c3554 100644 --- a/projects/stream-chat-angular/src/lib/message/message.component.ts +++ b/projects/stream-chat-angular/src/lib/message/message.component.ts @@ -3,6 +3,8 @@ import { ElementRef, Input, TemplateRef, + OnChanges, + SimpleChanges, ViewChild, } from '@angular/core'; import { UserResponse } from 'stream-chat'; @@ -19,8 +21,9 @@ import { getReadByText } from './read-by-text'; templateUrl: './message.component.html', styles: [], }) -export class MessageComponent { +export class MessageComponent implements OnChanges { @Input() messageInputTemplate: TemplateRef | undefined; + @Input() mentionTemplate: TemplateRef | undefined; @Input() message: StreamMessage | undefined; @Input() enabledMessageActions: MessageActions[] = []; @Input() areReactionsEnabled: boolean | undefined; @@ -32,6 +35,11 @@ export class MessageComponent { isReactionSelectorOpen = false; isPressedOnMobile = false; visibleMessageActionsCount = 0; + messageTextParts: { + content: string; + type: 'text' | 'mention'; + user?: UserResponse; + }[] = []; private user: UserResponse | undefined; @ViewChild('container') private container: | ElementRef @@ -44,6 +52,49 @@ export class MessageComponent { this.user = this.chatClientService.chatClient.user; } + ngOnChanges(changes: SimpleChanges): void { + if (changes.message) { + let content = this.message?.html || this.message?.text; + if (!content) { + this.messageTextParts = []; + } else { + // Backend will wrap HTML content with

\n + if (content.startsWith('

')) { + content = content.replace('

', ''); + } + if (content.endsWith('

\n')) { + content = content.replace('

\n', ''); + } + if ( + !this.message!.mentioned_users || + this.message!.mentioned_users.length === 0 + ) { + this.messageTextParts = [{ content, type: 'text' }]; + } else { + this.messageTextParts = []; + let text = content; + this.message!.mentioned_users.forEach((user) => { + const mention = `@${user.name || user.id}`; + const precedingText = text.substring(0, text.indexOf(mention)); + this.messageTextParts.push({ + content: precedingText, + type: 'text', + }); + this.messageTextParts.push({ + content: mention, + type: 'mention', + user, + }); + text = text.replace(precedingText + mention, ''); + }); + if (text) { + this.messageTextParts.push({ content: text, type: 'text' }); + } + } + } + } + } + get isSentByCurrentUser() { return this.message?.user?.id === this.user?.id; } diff --git a/projects/stream-chat-angular/src/lib/stream-chat.module.ts b/projects/stream-chat-angular/src/lib/stream-chat.module.ts index b9adf6e6..3813ba1f 100644 --- a/projects/stream-chat-angular/src/lib/stream-chat.module.ts +++ b/projects/stream-chat-angular/src/lib/stream-chat.module.ts @@ -18,7 +18,6 @@ import { TranslateModule } from '@ngx-translate/core'; import { AttachmentPreviewListComponent } from './attachment-preview-list/attachment-preview-list.component'; import { ModalComponent } from './modal/modal.component'; import { TextareaDirective } from './message-input/textarea.directive'; -import { HighlightMentionsPipe } from './message/highlight-mentions.pipe'; import { StreamAvatarModule } from './stream-avatar.module'; @NgModule({ @@ -40,7 +39,6 @@ import { StreamAvatarModule } from './stream-avatar.module'; AttachmentPreviewListComponent, ModalComponent, TextareaDirective, - HighlightMentionsPipe, ], imports: [CommonModule, TranslateModule, StreamAvatarModule], exports: [ @@ -60,7 +58,7 @@ import { StreamAvatarModule } from './stream-avatar.module'; NotificationListComponent, AttachmentPreviewListComponent, ModalComponent, - HighlightMentionsPipe, + StreamAvatarModule, ], }) export class StreamChatModule {} diff --git a/projects/stream-chat-angular/src/public-api.ts b/projects/stream-chat-angular/src/public-api.ts index 4cc86c16..a67db211 100644 --- a/projects/stream-chat-angular/src/public-api.ts +++ b/projects/stream-chat-angular/src/public-api.ts @@ -19,7 +19,6 @@ export * from './lib/channel-list/channel-list-toggle.service'; export * from './lib/message/message.component'; export * from './lib/message/parse-date'; export * from './lib/message/read-by-text'; -export * from './lib/message/highlight-mentions.pipe'; export * from './lib/message-input/message-input.component'; export * from './lib/message-input/textarea/textarea.component'; export * from './lib/message-input/autocomplete-textarea/autocomplete-textarea.component'; @@ -42,6 +41,7 @@ export * from './lib/message-preview'; export * from './lib/notification.service'; export * from './lib/transliteration.service'; export * from './lib/stream-chat.module'; +export * from './lib/stream-avatar.module'; export * from './lib/stream-autocomplete-textarea.module'; export * from './lib/stream-textarea.module'; export * from './lib/injection-tokens';