I am working on chat application where i need to display bot messages one-by-one with some delay between them, such that it gives an impression that bot is typing instead of throwing all message together. I was trying this behaviour with RxJS but could not achieve the desired output.
query(): Observable<IChatResponse> {
const response = {
messages: [
{
text: 'Please give a valid domain name',
},
{
text: 'What domain do you want?',
},
{
text: 'Some other messages...',
},
],
};
return of(response).pipe(
switchMap((response: any) => this.convertToStream(response))
);
}
convertToStream(data: any): Observable<IChatResponse> {
let count = 0;
const messageDelayFn = (sme, idx): Observable<any> => {
const loaderStart$ = of(null).pipe(
tap((_) => console.log('idx ', idx)),
delay(500 * idx),
tap((_) => {
this.loading$.next(true);
})
);
const loaderStop$ = of(null).pipe(
delay(1000 * idx),
tap((_) => {
this.loading$.next(false);
})
);
const message$ = of(sme);
return concat(loaderStart$, loaderStop$, message$).pipe(share());
};
const transformedObservable = of(data).pipe(
map((chat) => {
return {
...chat,
messages: chat.messages.reduce((acc: Observable<any>[], message) => {
return [...acc, messageDelayFn(message, count)];
}, []),
};
})
);
return transformedObservable;
}
Behaviour i am expecting is something like this --,
- starts with loader for 500ms
- loader stops
- emits first message
- again loader starts for 500ms (before emitting second message)
- loader stops
- emits second message
- ...and so on
CodePudding user response:
Here is my solution. Using subject combined with concat, since concat will execute the next observable only after the previous observable completes, I have added a delay and it seems to work as per the requirements.
service
import { Injectable } from '@angular/core';
import {
concat,
map,
Observable,
of,
share,
switchMap,
tap,
delay,
Subject,
BehaviorSubject,
concatMap,
} from 'rxjs';
import { IChatResponse } from './app.component';
@Injectable({ providedIn: 'root' })
export default class AppService {
loading$ = new Subject<boolean>();
get loadingStream(): Observable<boolean> {
return this.loading$.asObservable();
}
query(): Observable<any> {
const subject = new BehaviorSubject(['loading']);
const response = {
messages: [
{
text: 'Please give a valid domain name',
},
{
text: 'What domain do you want?',
},
{
text: 'Some other messages...',
},
],
};
const observables$ = response.messages.map((x) => {
return of(x.text).pipe(delay(3000));
});
return concat(...observables$).pipe(
tap((x) => {
const arr = subject.getValue();
arr[arr.length - 1] = x;
subject.next(
arr.length === response.messages.length ? arr : [...arr, 'loading']
);
}),
concatMap(() => subject.asObservable())
);
}
}
ts
import { Component, OnDestroy, OnInit, VERSION } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import AppService from './app.service';
export interface IChatResponse {
messages: Observable<IMessage>[];
}
interface IMessage {
text: string;
}
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
name = 'Angular ' VERSION.major;
chatMessages$: any;
subscription = new Subscription();
loader$: Observable<boolean>;
constructor(private appService: AppService) {
this.loader$ = this.appService.loadingStream;
}
ngOnInit(): void {
this.chatMessages$ = <any>this.appService.query();
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
html
<hello name="{{ name }}"></hello>
<p *ngFor="let message of chatMessages$ | async">
<ng-container [ngSwitch]="message">
<ng-container *ngSwitchCase="'loading'">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
style="margin:auto;background:#fff;display:block;"
width="200px"
height="200px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<g transform="translate(20 50)">
<circle cx="0" cy="0" r="6" fill="#e15b64">
<animateTransform
attributeName="transform"
type="scale"
begin="-0.375s"
calcMode="spline"
keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
values="0;1;0"
keyTimes="0;0.5;1"
dur="1s"
repeatCount="indefinite"
></animateTransform>
</circle>
</g>
<g transform="translate(40 50)">
<circle cx="0" cy="0" r="6" fill="#f8b26a">
<animateTransform
attributeName="transform"
type="scale"
begin="-0.25s"
calcMode="spline"
keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
values="0;1;0"
keyTimes="0;0.5;1"
dur="1s"
repeatCount="indefinite"
></animateTransform>
</circle>
</g>
<g transform="translate(60 50)">
<circle cx="0" cy="0" r="6" fill="#abbd81">
<animateTransform
attributeName="transform"
type="scale"
begin="-0.125s"
calcMode="spline"
keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
values="0;1;0"
keyTimes="0;0.5;1"
dur="1s"
repeatCount="indefinite"
></animateTransform>
</circle>
</g>
<g transform="translate(80 50)">
<circle cx="0" cy="0" r="6" fill="#81a3bd">
<animateTransform
attributeName="transform"
type="scale"
begin="0s"
calcMode="spline"
keySplines="0.3 0 0.7 1;0.3 0 0.7 1"
values="0;1;0"
keyTimes="0;0.5;1"
dur="1s"
repeatCount="indefinite"
></animateTransform>
</circle>
</g>
</svg>
</ng-container>
<ng-container *ngSwitchDefault>{{ message }}</ng-container>
</ng-container>
</p>
CodePudding user response:
service
export default class AppService {
query(): Observable<IMessage> {
const response = {
messages: [
{
text: 'Please give a valid domain name',
},
{
text: 'What domain do you want?',
},
{
text: 'Some other messages...',
},
],
};
return from(response.messages);
}
}
component
export class AppComponent {
delayTime$ = timer(1000).pipe(ignoreElements());
text$ = this.appService.query().pipe(
concatMap(({ text }) => {
const text$ = of(text).pipe(delay(1000), share());
return concat(this.delayTime$, of(text$), text$.pipe(ignoreElements()));
}),
scan((arr, v) => [...arr, v], [])
);
constructor(private appService: AppService) {}
}
html
<div *ngFor="let item of text$ | async">
<div >
<span *ngIf="item | async as item; else loading">
{{ item }}
</span>
<ng-template #loading
><img src="https://c.tenor.com/VS20soWAM9AAAAAi/loading.gif" width="30"
/></ng-template>
</div>
</div>
https://stackblitz.com/edit/angular-ivy-ot6dcr
CodePudding user response:
Here's a function that matches your spec. You can try running this yourself and then hopefully adapt it to your use case as needed.
function query(): Observable<string> {
const response = {
messages: [
{
text: 'Please give a valid domain name',
},
{
text: 'What domain do you want?',
},
{
text: 'Some other messages...',
},
],
};
return concat(...response.messages.map(({text}) =>
timer(500).pipe(map(_ => text))
));
}
query().subscribe(console.log);