src/lib/services/adapters/xmpp/xmpp-chat-adapter.service.ts
Properties |
|
Methods |
|
constructor(chatConnectionService: XmppChatConnectionService, logService: LogService, contactFactory: ContactFactoryService)
|
||||||||||||
Parameters :
|
addContact | ||||||
addContact(identifier: string)
|
||||||
Parameters :
Returns :
void
|
addPlugins | ||||||
addPlugins(plugins: ChatPlugin[])
|
||||||
Parameters :
Returns :
void
|
Private announceAvailability |
announceAvailability()
|
Returns :
void
|
getContactById | ||||||
getContactById(jidPlain: string)
|
||||||
Parameters :
Returns :
any
|
getOrCreateContactById |
getOrCreateContactById(jidPlain: string, name?: string)
|
Returns :
any
|
getPlugin | ||||
getPlugin(constructor)
|
||||
Type parameters :
|
||||
Parameters :
Returns :
T
|
Private handleInternalStateChange | ||||||
handleInternalStateChange(newState: XmppChatStates)
|
||||||
Parameters :
Returns :
void
|
loadCompleteHistory |
loadCompleteHistory()
|
Returns :
any
|
Async logIn | ||||||
logIn(logInRequest: LogInRequest)
|
||||||
Parameters :
Returns :
any
|
logOut |
logOut()
|
Returns :
Promise<void>
|
Private onOffline |
onOffline()
|
Returns :
void
|
Private onUnknownStanza | ||||||
onUnknownStanza(stanza: Stanza)
|
||||||
Parameters :
Returns :
void
|
reconnect |
reconnect()
|
Returns :
any
|
reconnectSilently |
reconnectSilently()
|
Returns :
void
|
reloadContacts |
reloadContacts()
|
Returns :
void
|
removeContact | ||||||
removeContact(identifier: string)
|
||||||
Parameters :
Returns :
void
|
Async sendMessage |
sendMessage(recipient: Recipient, body: string)
|
Returns :
any
|
Readonly blockedContactIds$ |
Default value : new BehaviorSubject<Set<string>>(new Set<string>())
|
Public chatConnectionService |
Type : XmppChatConnectionService
|
Readonly contactCreated$ |
Default value : new Subject<Contact>()
|
Readonly contactRequestsReceived$ |
Type : Observable<Contact[]>
|
Default value : this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.pendingIn$.getValue())))
|
Readonly contactRequestsSent$ |
Type : Observable<Contact[]>
|
Default value : this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.pendingOut$.getValue())))
|
Readonly contacts$ |
Default value : new BehaviorSubject<Contact[]>([])
|
Readonly contactsSubscribed$ |
Type : Observable<Contact[]>
|
Default value : this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.isSubscribed())))
|
Readonly contactsUnaffiliated$ |
Type : Observable<Contact[]>
|
Default value : this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.isUnaffiliated() && contact.messages.length > 0)))
|
enableDebugging |
Default value : false
|
Private lastLogInRequest |
Type : LogInRequest
|
Readonly message$ |
Default value : new Subject<Contact>()
|
Readonly messageSent$ |
Type : Subject<Contact>
|
Default value : new Subject()
|
Readonly plugins |
Type : ChatPlugin[]
|
Default value : []
|
Readonly state$ |
Default value : new BehaviorSubject<ConnectionStates>('disconnected')
|
translations |
Type : Translations
|
Default value : defaultTranslations()
|
Readonly userAvatar$ |
Default value : new BehaviorSubject(dummyAvatarContact)
|
import { Injectable } from '@angular/core';
import { jid as parseJid } from '@xmpp/client';
import { BehaviorSubject, combineLatest, merge, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { Contact } from '../../../core/contact';
import { dummyAvatarContact } from '../../../core/contact-avatar';
import { LogInRequest } from '../../../core/log-in-request';
import { ChatPlugin } from '../../../core/plugin';
import { Recipient } from '../../../core/recipient';
import { Stanza } from '../../../core/stanza';
import { Translations } from '../../../core/translations';
import { defaultTranslations } from '../../../core/translations-default';
import { ChatService, ConnectionStates } from '../../chat-service';
import { ContactFactoryService } from '../../contact-factory.service';
import { LogService } from '../../log.service';
import { MessageArchivePlugin } from './plugins/message-archive.plugin';
import { MessagePlugin } from './plugins/message.plugin';
import { MultiUserChatPlugin } from './plugins/multi-user-chat/multi-user-chat.plugin';
import { RosterPlugin } from './plugins/roster.plugin';
import { XmppChatConnectionService, XmppChatStates } from './xmpp-chat-connection.service';
export interface ChatAction<TChatWindow> {
cssClass: { [className: string]: boolean } | string | string[];
/**
* to identify actions
*/
id: string;
html: string;
onClick(chatActionContext: ChatActionContext<TChatWindow>): void;
}
export interface ChatActionContext<TChatWindow> {
contact: string;
chatWindow: TChatWindow;
}
@Injectable()
export class XmppChatAdapter implements ChatService {
readonly message$ = new Subject<Contact>();
readonly messageSent$: Subject<Contact> = new Subject();
readonly contacts$ = new BehaviorSubject<Contact[]>([]);
readonly contactCreated$ = new Subject<Contact>();
readonly blockedContactIds$ = new BehaviorSubject<Set<string>>(new Set<string>());
readonly blockedContacts$ = combineLatest([this.contacts$, this.blockedContactIds$])
.pipe(
map(
([contacts, blockedJids]) =>
contacts.filter(contact => blockedJids.has(contact.jidBare.toString())),
),
);
readonly notBlockedContacts$ = combineLatest([this.contacts$, this.blockedContactIds$])
.pipe(
map(
([contacts, blockedJids]) =>
contacts.filter(contact => !blockedJids.has(contact.jidBare.toString())),
),
);
readonly contactsSubscribed$: Observable<Contact[]> = this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.isSubscribed())));
readonly contactRequestsReceived$: Observable<Contact[]> = this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.pendingIn$.getValue())));
readonly contactRequestsSent$: Observable<Contact[]> = this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.pendingOut$.getValue())));
readonly contactsUnaffiliated$: Observable<Contact[]> = this.notBlockedContacts$.pipe(
map(contacts => contacts.filter(contact => contact.isUnaffiliated() && contact.messages.length > 0)));
readonly state$ = new BehaviorSubject<ConnectionStates>('disconnected');
readonly plugins: ChatPlugin[] = [];
enableDebugging = false;
readonly userAvatar$ = new BehaviorSubject(dummyAvatarContact);
translations: Translations = defaultTranslations();
chatActions = [{
id: 'sendMessage',
cssClass: 'chat-window-send',
html: '»',
onClick: (chatActionContext: ChatActionContext<{ sendMessage: () => void }>) => {
chatActionContext.chatWindow.sendMessage();
},
}];
private lastLogInRequest: LogInRequest;
constructor(
public chatConnectionService: XmppChatConnectionService,
private logService: LogService,
private contactFactory: ContactFactoryService,
) {
this.state$.subscribe((state) => this.logService.info('state changed to:', state));
chatConnectionService.state$
.pipe(filter(nextState => nextState !== this.state$.getValue()))
.subscribe((nextState) => {
this.handleInternalStateChange(nextState);
});
this.chatConnectionService.stanzaUnknown$.subscribe((stanza) => this.onUnknownStanza(stanza));
merge(this.messageSent$, this.message$).subscribe(() => {
// re-emit contacts when sending or receiving a message to refresh contact groups
// if the sending contact was in 'other', he still is in other now, but passes the 'messages.length > 0' predicate, so that
// he should be seen now.
this.contacts$.next(this.contacts$.getValue());
});
}
private handleInternalStateChange(newState: XmppChatStates) {
if (newState === 'online') {
this.state$.next('connecting');
Promise
.all(this.plugins.map(plugin => plugin.onBeforeOnline()))
.catch((e) => this.logService.error('error while connecting', e))
.finally(() => this.announceAvailability());
} else {
if (this.state$.getValue() === 'online') {
// clear data the first time we transition to a not-online state
this.onOffline();
}
this.state$.next('disconnected');
}
}
private onOffline() {
this.contacts$.next([]);
this.plugins.forEach(plugin => {
try {
plugin.onOffline();
} catch (e) {
this.logService.error('error while handling offline in ', plugin);
}
});
}
private announceAvailability() {
this.logService.info('announcing availability');
this.chatConnectionService.sendPresence();
this.state$.next('online');
}
addPlugins(plugins: ChatPlugin[]) {
plugins.forEach(plugin => {
this.plugins.push(plugin);
});
}
reloadContacts(): void {
this.getPlugin(RosterPlugin).refreshRosterContacts();
}
getContactById(jidPlain: string) {
const bareJidToFind = parseJid(jidPlain).bare();
return this.contacts$.getValue().find(contact => contact.jidBare.equals(bareJidToFind));
}
getOrCreateContactById(jidPlain: string, name?: string) {
let contact = this.getContactById(jidPlain);
if (!contact) {
contact = this.contactFactory.createContact(parseJid(jidPlain).bare().toString(), name);
this.contacts$.next([contact, ...this.contacts$.getValue()]);
this.contactCreated$.next(contact);
}
return contact;
}
addContact(identifier: string) {
this.getPlugin(RosterPlugin).addRosterContact(identifier);
}
removeContact(identifier: string) {
this.getPlugin(RosterPlugin).removeRosterContact(identifier);
}
async logIn(logInRequest: LogInRequest) {
this.lastLogInRequest = logInRequest;
if (this.state$.getValue() === 'disconnected') {
await this.chatConnectionService.logIn(logInRequest);
}
}
logOut(): Promise<void> {
return this.chatConnectionService.logOut();
}
async sendMessage(recipient: Recipient, body: string) {
const trimmedBody = body.trim();
if (trimmedBody.length === 0) {
return;
}
switch (recipient.recipientType) {
case 'room':
await this.getPlugin(MultiUserChatPlugin).sendMessage(recipient, trimmedBody);
break;
case 'contact':
this.getPlugin(MessagePlugin).sendMessage(recipient, trimmedBody);
this.messageSent$.next(recipient);
break;
default:
throw new Error('invalid recipient type: ' + (recipient as any)?.recipientType);
}
}
loadCompleteHistory() {
return this.getPlugin(MessageArchivePlugin).loadAllMessages();
}
getPlugin<T extends ChatPlugin>(constructor: new(...args: any[]) => T): T {
for (const plugin of this.plugins) {
if (plugin.constructor === constructor) {
return plugin as T;
}
}
throw new Error('plugin not found: ' + constructor);
}
private onUnknownStanza(stanza: Stanza) {
let handled = false;
for (const plugin of this.plugins) {
try {
if (plugin.handleStanza(stanza)) {
this.logService.debug(plugin.constructor.name, 'handled', stanza.toString());
handled = true;
}
} catch (e) {
this.logService.error('error handling stanza in ', plugin.constructor.name, e);
}
}
if (!handled) {
this.logService.warn('unknown stanza <=', stanza.toString());
}
}
reconnectSilently(): void {
this.chatConnectionService.reconnectSilently();
}
reconnect() {
return this.logIn(this.lastLogInRequest);
}
}