UNPKG

@hsaadawy/ngx-chat

Version:
1,075 lines (1,051 loc) 215 kB
import { __awaiter } from 'tslib'; import { EventEmitter, Component, Output, Input, HostListener, InjectionToken, Inject, ViewChild, Injectable, NgZone, ElementRef, Optional, ChangeDetectorRef, ViewChildren, ChangeDetectionStrategy, Directive, ComponentFactoryResolver, ViewContainerRef, NgModule } from '@angular/core'; import { BehaviorSubject, Subject, combineLatest, merge, of } from 'rxjs'; import { filter, debounceTime, first, map, takeUntil, delay as delay$1, distinctUntilChanged, share, timeout as timeout$1, mergeMap, catchError } from 'rxjs/operators'; import { xml, jid, client } from '@xmpp/client'; export { jid as parseJid } from '@xmpp/client'; import { HttpHeaders, HttpClient, HttpClientModule } from '@angular/common/http'; import { PlatformLocation, CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { TextFieldModule } from '@angular/cdk/text-field'; import { FormsModule } from '@angular/forms'; import { trigger, state, style, transition, animate } from '@angular/animations'; export { JID } from '@xmpp/jid'; class FileDropComponent { constructor() { this.fileUpload = new EventEmitter(); this.enabled = true; this.isDropTarget = false; } onDragOver(event) { if (this.enabled) { event.preventDefault(); event.stopPropagation(); this.isDropTarget = true; } } onDragLeave(event) { event.preventDefault(); event.stopPropagation(); this.isDropTarget = false; } onDrop(event) { return __awaiter(this, void 0, void 0, function* () { if (this.enabled) { event.preventDefault(); event.stopPropagation(); this.isDropTarget = false; // tslint:disable-next-line:prefer-for-of for (let i = 0; i < event.dataTransfer.items.length; i++) { const dataTransferItem = event.dataTransfer.items[i]; if (dataTransferItem.kind === 'file') { this.fileUpload.emit(dataTransferItem.getAsFile()); } } } }); } } FileDropComponent.decorators = [ { type: Component, args: [{ selector: 'ngx-chat-filedrop', template: "<div>\r\n <div class=\"drop-message\"\r\n [class.drop-message--visible]=\"isDropTarget\">\r\n {{dropMessage}}\r\n </div>\r\n <div>\r\n <ng-content></ng-content>\r\n </div>\r\n</div>\r\n", styles: [".drop-message{pointer-events:none;display:none}.drop-message--visible{position:absolute;top:0;bottom:0;left:0;right:0;display:flex;justify-content:center;align-content:center;flex-direction:column;text-align:center;font-size:1.5em;z-index:999;background-color:#fff9;padding:1em}\n"] },] } ]; FileDropComponent.propDecorators = { fileUpload: [{ type: Output }], dropMessage: [{ type: Input }], enabled: [{ type: Input }], onDragOver: [{ type: HostListener, args: ['dragover', ['$event'],] }, { type: HostListener, args: ['dragenter', ['$event'],] }], onDragLeave: [{ type: HostListener, args: ['dragleave', ['$event'],] }, { type: HostListener, args: ['dragexit', ['$event'],] }], onDrop: [{ type: HostListener, args: ['drop', ['$event'],] }] }; class ReplyMessageEvent { constructor() { this.replyMessageEmitter$ = new EventEmitter(); } changeReplyMessage(replyMessage) { debugger; this.replyMessageEmitter$.emit(replyMessage); } } /** * The chat service token gives you access to the main chat api and is implemented by default with an XMPP adapter, * you can always reuse the api and ui with a new service implementing the ChatService interface and providing the * said implementation with the token */ const CHAT_SERVICE_TOKEN = new InjectionToken('ngxChatService'); class ChatMessageInputComponent { constructor(chatService, replyMessageEvent) { this.chatService = chatService; this.replyMessageEvent = replyMessageEvent; this.messageSent = new EventEmitter(); this.message = ""; this.messageItem = ""; debugger; this.replyMessageEvent.replyMessageEmitter$.subscribe((item) => { // this.message = item this.messageItem = item; }); } ngOnInit() { } onSendMessage($event) { if ($event) { $event.preventDefault(); } if (this.Reply != '') { this.chatService.sendMessage(this.recipient, `<div class="messageItem">${this.Reply}</div>` + this.message); this.Reply = ''; } else { this.chatService.sendMessage(this.recipient, this.message); } this.message = ""; // this.messageSent.emit(); } focus() { this.chatInput.nativeElement.focus(); } delete() { debugger; this.Reply = ''; } } ChatMessageInputComponent.decorators = [ { type: Component, args: [{ selector: "ngx-chat-message-input", template: "<div *ngIf=\"Reply!=''\" style=\" background: #8e89896b;\r\ncolor: #fff;\r\nposition: relative;\r\ntop: -9px;\r\nwidth: 105%;\r\nleft: -3%;\r\npadding: 4px 6px;\r\nborder-radius: 4px 4px 0 0;\">\r\n\r\n <div [innerHTML]=\"Reply\"></div>\r\n <i class=\"fas fa-times\" style=\"font-family: 'Font Awesome 5 Pro' !important;float: right;padding: 0 5px; position: absolute;\r\n right: 0;\r\n top: 4px;\" (click)=\"delete()\" aria-hidden=\"true\"></i>\r\n\r\n</div>\r\n\r\n<textarea class=\"chat-input\" #chatInput [(ngModel)]=\"message\" (keydown.enter)=\"onSendMessage($event)\"\r\n cdkTextareaAutosize cdkAutosizeMinRows=\"1\" cdkAutosizeMaxRows=\"5\"\r\n placeholder=\"{{chatService.translations.placeholder}}\"></textarea>\r\n", styles: ["@keyframes ngx-chat-message-in{0%{transform:translate(50px);opacity:0}to{transform:none;opacity:1}}@keyframes ngx-chat-message-out{0%{transform:translate(-50px);opacity:0}to{transform:none;opacity:1}}.messageItem{background-color:red;width:100%;border-radius:2px}*{box-sizing:border-box;margin:0;padding:0;font-family:\"Helvetica\",\"Arial\",serif}.chat-input{border:none;width:100%;font-size:1em;padding:0;display:block;resize:none;overflow-x:hidden;outline:none}\n"] },] } ]; ChatMessageInputComponent.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [CHAT_SERVICE_TOKEN,] }] }, { type: ReplyMessageEvent, decorators: [{ type: Inject, args: [ReplyMessageEvent,] }] } ]; ChatMessageInputComponent.propDecorators = { recipient: [{ type: Input }], Reply: [{ type: Input }], messageSent: [{ type: Output }], chatInput: [{ type: ViewChild, args: ["chatInput",] }] }; var MessageState; (function (MessageState) { /** * Not yet sent */ MessageState["SENDING"] = "sending"; /** * Sent, but neither received nor seen by the recipient */ MessageState["SENT"] = "sent"; /** * The recipient client has received the message but the recipient has not seen it yet */ MessageState["RECIPIENT_RECEIVED"] = "recipientReceived"; /** * The message has been seen by the recipient */ MessageState["RECIPIENT_SEEN"] = "recipientSeen"; })(MessageState || (MessageState = {})); var Direction; (function (Direction) { Direction["in"] = "in"; Direction["out"] = "out"; })(Direction || (Direction = {})); class AbstractXmppPlugin { onBeforeOnline() { return Promise.resolve(); } onOffline() { } afterSendMessage(message, messageStanza) { return; } beforeSendMessage(messageStanza, message) { return; } handleStanza(stanza) { return false; } afterReceiveMessage(message, messageStanza, messageReceivedEvent) { return; } } /** * XEP-0191: Blocking Command * https://xmpp.org/extensions/xep-0191.html */ class BlockPlugin extends AbstractXmppPlugin { constructor(xmppChatAdapter, serviceDiscoveryPlugin) { super(); this.xmppChatAdapter = xmppChatAdapter; this.serviceDiscoveryPlugin = serviceDiscoveryPlugin; this.supportsBlock$ = new BehaviorSubject('unknown'); } onBeforeOnline() { return __awaiter(this, void 0, void 0, function* () { const supportsBlock = yield this.determineSupportForBlock(); this.supportsBlock$.next(supportsBlock); if (supportsBlock) { yield this.requestBlockedJids(); } }); } determineSupportForBlock() { return __awaiter(this, void 0, void 0, function* () { try { return yield this.serviceDiscoveryPlugin.supportsFeature(this.xmppChatAdapter.chatConnectionService.userJid.domain, 'urn:xmpp:blocking'); } catch (e) { return false; } }); } onOffline() { this.supportsBlock$.next('unknown'); this.xmppChatAdapter.blockedContactIds$.next(new Set()); } blockJid(jid) { return this.xmppChatAdapter.chatConnectionService.sendIq(xml('iq', { type: 'set' }, xml('block', { xmlns: 'urn:xmpp:blocking' }, xml('item', { jid })))); } unblockJid(jid) { return this.xmppChatAdapter.chatConnectionService.sendIq(xml('iq', { type: 'set' }, xml('unblock', { xmlns: 'urn:xmpp:blocking' }, xml('item', { jid })))); } requestBlockedJids() { return __awaiter(this, void 0, void 0, function* () { const blockListResponse = yield this.xmppChatAdapter.chatConnectionService.sendIq(xml('iq', { type: 'get' }, xml('blocklist', { xmlns: 'urn:xmpp:blocking' }))); const blockedJids = blockListResponse .getChild('blocklist') .getChildren('item') .map(e => e.attrs.jid); this.xmppChatAdapter.blockedContactIds$.next(new Set(blockedJids)); }); } handleStanza(stanza) { var _a; const { from } = stanza.attrs; if (from && from === ((_a = this.xmppChatAdapter.chatConnectionService.userJid) === null || _a === void 0 ? void 0 : _a.bare().toString())) { const blockPush = stanza.getChild('block', 'urn:xmpp:blocking'); const unblockPush = stanza.getChild('unblock', 'urn:xmpp:blocking'); const blockList = this.xmppChatAdapter.blockedContactIds$.getValue(); if (blockPush) { blockPush.getChildren('item') .map(e => e.attrs.jid) .forEach(jid => blockList.add(jid)); this.xmppChatAdapter.blockedContactIds$.next(blockList); return true; } else if (unblockPush) { const jidsToUnblock = unblockPush.getChildren('item').map(e => e.attrs.jid); if (jidsToUnblock.length === 0) { // unblock everyone blockList.clear(); } else { // unblock individually jidsToUnblock.forEach(jid => blockList.delete(jid)); } this.xmppChatAdapter.blockedContactIds$.next(blockList); return true; } } return false; } } class AbstractStanzaBuilder { } class IqResponseError extends Error { constructor(errorStanza) { super(IqResponseError.extractErrorTextFromErrorResponse(errorStanza, IqResponseError.extractErrorDataFromErrorResponse(errorStanza))); this.errorStanza = errorStanza; const { code, type, condition } = IqResponseError.extractErrorDataFromErrorResponse(errorStanza); this.errorCode = code; this.errorType = type; this.errorCondition = condition; } static extractErrorDataFromErrorResponse(stanza) { var _a; const errorElement = stanza.getChild('error'); const errorCode = Number(errorElement === null || errorElement === void 0 ? void 0 : errorElement.attrs.code) || undefined; const errorType = errorElement === null || errorElement === void 0 ? void 0 : errorElement.attrs.type; const errorCondition = (_a = errorElement === null || errorElement === void 0 ? void 0 : errorElement.children.filter(childElement => childElement.getName() !== 'text' && childElement.attrs.xmlns === IqResponseError.ERROR_ELEMENT_NS)[0]) === null || _a === void 0 ? void 0 : _a.getName(); return { code: errorCode, type: errorType, condition: errorCondition, }; } static extractErrorTextFromErrorResponse(stanza, { code, type, condition }) { var _a; const additionalData = [ `errorCode: ${code !== null && code !== void 0 ? code : '[unknown]'}`, `errorType: ${type !== null && type !== void 0 ? type : '[unknown]'}`, `errorCondition: ${condition !== null && condition !== void 0 ? condition : '[unknown]'}` ].join(', '); const errorText = ((_a = stanza.getChild('error')) === null || _a === void 0 ? void 0 : _a.getChildText('text', IqResponseError.ERROR_ELEMENT_NS)) || 'Unknown error'; return `IqResponseError: ${errorText}${additionalData ? ` (${additionalData})` : ''}`; } } IqResponseError.ERROR_ELEMENT_NS = 'urn:ietf:params:xml:ns:xmpp-stanzas'; const PUBSUB_EVENT_XMLNS = 'http://jabber.org/protocol/pubsub#event'; class PublishStanzaBuilder extends AbstractStanzaBuilder { constructor(options) { super(); this.publishOptions = { persistItems: false, }; if (options) { this.publishOptions = Object.assign(Object.assign({}, this.publishOptions), options); } } toStanza() { const { node, id, persistItems } = this.publishOptions; // necessary as a 'event-only' publish is currently broken in ejabberd, see // https://github.com/processone/ejabberd/issues/2799 const data = this.publishOptions.data || xml('data'); return xml('iq', { type: 'set' }, xml('pubsub', { xmlns: 'http://jabber.org/protocol/pubsub' }, xml('publish', { node }, xml('item', { id }, data)), xml('publish-options', {}, xml('x', { xmlns: 'jabber:x:data', type: 'submit' }, xml('field', { var: 'FORM_TYPE', type: 'hidden' }, xml('value', {}, 'http://jabber.org/protocol/pubsub#publish-options')), xml('field', { var: 'pubsub#persist_items' }, xml('value', {}, persistItems ? 1 : 0)), xml('field', { var: 'pubsub#access_model' }, xml('value', {}, 'whitelist')))))); } } class RetrieveDataStanzaBuilder extends AbstractStanzaBuilder { constructor(node) { super(); this.node = node; } toStanza() { return xml('iq', { type: 'get' }, xml('pubsub', { xmlns: 'http://jabber.org/protocol/pubsub' }, xml('items', { node: this.node }))); } } /** * XEP-0060 Publish Subscribe (https://xmpp.org/extensions/xep-0060.html) * XEP-0223 Persistent Storage of Private Data via PubSub (https://xmpp.org/extensions/xep-0223.html) */ class PublishSubscribePlugin extends AbstractXmppPlugin { constructor(xmppChatAdapter, serviceDiscoveryPlugin) { super(); this.xmppChatAdapter = xmppChatAdapter; this.serviceDiscoveryPlugin = serviceDiscoveryPlugin; this.publish$ = new Subject(); this.supportsPrivatePublish = new BehaviorSubject('unknown'); } onBeforeOnline() { return this.determineSupportForPrivatePublish(); } onOffline() { this.supportsPrivatePublish.next('unknown'); } storePrivatePayloadPersistent(node, id, data) { return new Promise((resolve, reject) => { this.supportsPrivatePublish .pipe(filter(support => support !== 'unknown')) .subscribe((support) => { if (!support) { reject(new Error('does not support private publish subscribe')); } else { resolve(this.xmppChatAdapter.chatConnectionService.sendIq(new PublishStanzaBuilder({ node, id, data, persistItems: true }).toStanza())); } }); }); } privateNotify(node, data, id) { return new Promise((resolve, reject) => { this.supportsPrivatePublish .pipe(filter(support => support !== 'unknown')) .subscribe((support) => { if (!support) { reject(new Error('does not support private publish subscribe')); } else { resolve(this.xmppChatAdapter.chatConnectionService.sendIq(new PublishStanzaBuilder({ node, id, data, persistItems: false }).toStanza())); } }); }); } handleStanza(stanza) { const eventElement = stanza.getChild('event', PUBSUB_EVENT_XMLNS); if (stanza.is('message') && eventElement) { this.publish$.next(eventElement); return true; } return false; } retrieveNodeItems(node) { return __awaiter(this, void 0, void 0, function* () { try { const iqResponseStanza = yield this.xmppChatAdapter.chatConnectionService.sendIq(new RetrieveDataStanzaBuilder(node).toStanza()); return iqResponseStanza.getChild('pubsub').getChild('items').getChildren('item'); } catch (e) { if (e instanceof IqResponseError && (e.errorCondition === 'item-not-found' || e.errorCode === 404)) { return []; } throw e; } }); } determineSupportForPrivatePublish() { return __awaiter(this, void 0, void 0, function* () { let isSupported; try { const service = yield this.serviceDiscoveryPlugin.findService('pubsub', 'pep'); isSupported = service.features.includes('http://jabber.org/protocol/pubsub#publish-options'); } catch (e) { isSupported = false; } this.supportsPrivatePublish.next(isSupported); }); } } const MUC_SUB_FEATURE_ID = 'urn:xmpp:mucsub:0'; var MUC_SUB_EVENT_TYPE; (function (MUC_SUB_EVENT_TYPE) { MUC_SUB_EVENT_TYPE["presence"] = "urn:xmpp:mucsub:nodes:presence"; MUC_SUB_EVENT_TYPE["messages"] = "urn:xmpp:mucsub:nodes:messages"; MUC_SUB_EVENT_TYPE["affiliations"] = "urn:xmpp:mucsub:nodes:affiliations"; MUC_SUB_EVENT_TYPE["subscribers"] = "urn:xmpp:mucsub:nodes:subscribers"; MUC_SUB_EVENT_TYPE["config"] = "urn:xmpp:mucsub:nodes:config"; MUC_SUB_EVENT_TYPE["subject"] = "urn:xmpp:mucsub:nodes:subject"; MUC_SUB_EVENT_TYPE["system"] = "urn:xmpp:mucsub:nodes:system"; })(MUC_SUB_EVENT_TYPE || (MUC_SUB_EVENT_TYPE = {})); /** * support for https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/ */ class MucSubPlugin extends AbstractXmppPlugin { constructor(xmppChatAdapter, serviceDiscoveryPlugin) { super(); this.xmppChatAdapter = xmppChatAdapter; this.serviceDiscoveryPlugin = serviceDiscoveryPlugin; this.supportsMucSub$ = new BehaviorSubject('unknown'); } onBeforeOnline() { return this.determineSupportForMucSub(); } determineSupportForMucSub() { return __awaiter(this, void 0, void 0, function* () { let isSupported; try { const service = yield this.serviceDiscoveryPlugin.findService('conference', 'text'); isSupported = service.features.includes(MUC_SUB_FEATURE_ID); } catch (e) { isSupported = false; } this.supportsMucSub$.next(isSupported); }); } onOffline() { this.supportsMucSub$.next('unknown'); } subscribeRoom(roomJid, nodes = []) { return __awaiter(this, void 0, void 0, function* () { const nick = this.xmppChatAdapter.chatConnectionService.userJid.local; yield this.xmppChatAdapter.chatConnectionService.sendIq(makeSubscribeRoomStanza(roomJid, nick, nodes)); }); } unsubscribeRoom(roomJid) { return __awaiter(this, void 0, void 0, function* () { yield this.xmppChatAdapter.chatConnectionService.sendIq(makeUnsubscribeRoomStanza(roomJid)); }); } /** * A room moderator can unsubscribe others providing the their jid as attribute to the information query (iq) * see: https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/#unsubscribing-from-a-muc-room * @param roomJid for the room to be unsubscribed from * @param jid user id to be unsubscribed */ unsubscribeJidFromRoom(roomJid, jid) { this.xmppChatAdapter.chatConnectionService.sendIq(xml('iq', { type: 'set', to: roomJid }, xml('unsubscribe', { xmlns: 'urn:xmpp:mucsub:0', jid }))); } /** * A user can query the MUC service to get their list of subscriptions. * see: https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/#g dd ddetting-list-of-subscribed-rooms */ getSubscribedRooms() { return __awaiter(this, void 0, void 0, function* () { const { local, domain } = this.xmppChatAdapter.chatConnectionService.userJid; const from = `${local}@${domain}`; const subscriptions = yield this.xmppChatAdapter.chatConnectionService.sendIq(xml('iq', { type: 'get', from, to: 'muc.' + domain }, xml('subscriptions', { xmlns: 'urn:xmpp:mucsub:0' }))); return subscriptions.getChildren('subscription').map(sub => sub.getAttr('jid')); }); } /** * A subscriber or room moderator can get the list of subscribers by sending <subscriptions/> request directly to the room JID. * see: https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/#getting-list-of-subscribers-of-a-room * @param roomJid of the room the get a subscriber list from */ getSubscribers(roomJid) { this.xmppChatAdapter.chatConnectionService.sendIq(xml('iq', { type: 'get', to: roomJid }, xml('subscriptions', { xmlns: 'urn:xmpp:mucsub:0' }))); } retrieveSubscriptions() { var _a, _b; return __awaiter(this, void 0, void 0, function* () { const service = yield this.serviceDiscoveryPlugin.findService('conference', 'text'); const result = yield this.xmppChatAdapter.chatConnectionService.sendIq(makeRetrieveSubscriptionsStanza(service.jid)); const subscriptions = (_b = (_a = result .getChild('subscriptions', MUC_SUB_FEATURE_ID)) === null || _a === void 0 ? void 0 : _a.getChildren('subscription')) === null || _b === void 0 ? void 0 : _b.map(subscriptionElement => { var _a, _b; const subscribedEvents = (_b = (_a = subscriptionElement .getChildren('event')) === null || _a === void 0 ? void 0 : _a.map(eventElement => eventElement.attrs.node)) !== null && _b !== void 0 ? _b : []; return [subscriptionElement.attrs.jid, subscribedEvents]; }); return new Map(subscriptions); }); } } function makeSubscribeRoomStanza(roomJid, nick, nodes) { return xml('iq', { type: 'set', to: roomJid }, xml('subscribe', { xmlns: MUC_SUB_FEATURE_ID, nick }, nodes.map(node => xml('event', { node })))); } function makeUnsubscribeRoomStanza(roomJid) { return xml('iq', { type: 'set', to: roomJid }, xml('unsubscribe', { xmlns: MUC_SUB_FEATURE_ID })); } function makeRetrieveSubscriptionsStanza(conferenceServiceJid) { return xml('iq', { type: 'get', to: conferenceServiceJid }, xml('subscriptions', { xmlns: MUC_SUB_FEATURE_ID })); } /** * https://xmpp.org/extensions/xep-0313.html * Message Archive Management */ class MessageArchivePlugin extends AbstractXmppPlugin { constructor(chatService, serviceDiscoveryPlugin, multiUserChatPlugin, logService, messagePlugin) { super(); this.chatService = chatService; this.serviceDiscoveryPlugin = serviceDiscoveryPlugin; this.multiUserChatPlugin = multiUserChatPlugin; this.logService = logService; this.messagePlugin = messagePlugin; this.mamMessageReceived$ = new Subject(); this.chatService.state$ .pipe(filter(state => state === 'online')) .subscribe(() => __awaiter(this, void 0, void 0, function* () { if (yield this.supportsMessageArchiveManagement()) { yield this.requestNewestMessages(); } })); // emit contacts to refresh contact list after receiving mam messages this.mamMessageReceived$ .pipe(debounceTime(10)) .subscribe(() => this.chatService.contacts$.next(this.chatService.contacts$.getValue())); } requestNewestMessages() { return __awaiter(this, void 0, void 0, function* () { yield this.chatService.chatConnectionService.sendIq(xml('iq', { type: 'set' }, xml('query', { xmlns: 'urn:xmpp:mam:2' }, xml('set', { xmlns: 'http://jabber.org/protocol/rsm' }, xml('max', {}, 250), xml('before'))))); }); } loadMostRecentUnloadedMessages(recipient) { return __awaiter(this, void 0, void 0, function* () { // for user-to-user chats no to-attribute is necessary, in case of multi-user-chats it has to be set to the bare room jid const to = recipient.recipientType === 'room' ? recipient.roomJid.toString() : undefined; const request = xml('iq', { type: 'set', to }, xml('query', { xmlns: 'urn:xmpp:mam:2' }, xml('x', { xmlns: 'jabber:x:data', type: 'submit' }, xml('field', { var: 'FORM_TYPE', type: 'hidden' }, xml('value', {}, 'urn:xmpp:mam:2')), recipient.recipientType === 'contact' ? xml('field', { var: 'with' }, xml('value', {}, recipient.jidBare)) : undefined, recipient.oldestMessage ? xml('field', { var: 'end' }, xml('value', {}, recipient.oldestMessage.datetime.toISOString())) : undefined), xml('set', { xmlns: 'http://jabber.org/protocol/rsm' }, xml('max', {}, 100), xml('before')))); yield this.chatService.chatConnectionService.sendIq(request); }); } loadAllMessages() { return __awaiter(this, void 0, void 0, function* () { if (!(yield this.supportsMessageArchiveManagement())) { throw new Error('message archive management not suppported'); } let lastMamResponse = yield this.chatService.chatConnectionService.sendIq(xml('iq', { type: 'set' }, xml('query', { xmlns: 'urn:xmpp:mam:2' }))); while (lastMamResponse.getChild('fin').attrs.complete !== 'true') { const lastReceivedMessageId = lastMamResponse.getChild('fin').getChild('set').getChildText('last'); lastMamResponse = yield this.chatService.chatConnectionService.sendIq(xml('iq', { type: 'set' }, xml('query', { xmlns: 'urn:xmpp:mam:2' }, xml('set', { xmlns: 'http://jabber.org/protocol/rsm' }, xml('max', {}, 250), xml('after', {}, lastReceivedMessageId))))); } }); } supportsMessageArchiveManagement() { return __awaiter(this, void 0, void 0, function* () { const supportsMessageArchiveManagement = yield this.serviceDiscoveryPlugin.supportsFeature(this.chatService.chatConnectionService.userJid.bare().toString(), 'urn:xmpp:mam:2'); if (!supportsMessageArchiveManagement) { this.logService.info('server doesnt support MAM'); } return supportsMessageArchiveManagement; }); } handleStanza(stanza) { if (this.isMamMessageStanza(stanza)) { this.handleMamMessageStanza(stanza); return true; } return false; } isMamMessageStanza(stanza) { const result = stanza.getChild('result'); return stanza.name === 'message' && (result === null || result === void 0 ? void 0 : result.attrs.xmlns) === 'urn:xmpp:mam:2'; } handleMamMessageStanza(stanza) { const forwardedElement = stanza.getChild('result').getChild('forwarded'); const messageElement = forwardedElement.getChild('message'); const delayElement = forwardedElement.getChild('delay'); const eventElement = messageElement.getChild('event', PUBSUB_EVENT_XMLNS); if (messageElement.getAttr('type') == null && eventElement != null) { this.handlePubSubEvent(eventElement, delayElement); } else { this.handleArchivedMessage(messageElement, delayElement); } } handleArchivedMessage(messageElement, delayEl) { const type = messageElement.getAttr('type'); if (type === 'chat') { const messageHandled = this.messagePlugin.handleStanza(messageElement, delayEl); if (messageHandled) { this.mamMessageReceived$.next(); } } else if (type === 'groupchat') { this.multiUserChatPlugin.handleStanza(messageElement, delayEl); } else { throw new Error(`unknown archived message type: ${type}`); } } handlePubSubEvent(eventElement, delayElement) { const itemsElement = eventElement.getChild('items'); const itemsNode = itemsElement === null || itemsElement === void 0 ? void 0 : itemsElement.attrs.node; if (itemsNode !== MUC_SUB_EVENT_TYPE.messages) { this.logService.warn(`Handling of MUC/Sub message types other than ${MUC_SUB_EVENT_TYPE.messages} isn't implemented yet!`); return; } const itemElements = itemsElement.getChildren('item'); itemElements.forEach((itemEl) => this.handleArchivedMessage(itemEl.getChild('message'), delayElement)); } } class ChatWindowState { constructor(recipient, isCollapsed) { this.recipient = recipient; this.isCollapsed = isCollapsed; } } /** * Used to open chat windows programmatically. */ class ChatListStateService { constructor(chatService) { this.chatService = chatService; this.openChats$ = new BehaviorSubject([]); this.openTracks$ = new BehaviorSubject([]); this.chatService.state$ .pipe(filter(newState => newState === 'disconnected')) .subscribe(() => { this.openChats$.next([]); }); this.chatService.contactRequestsReceived$.subscribe(contacts => { for (const contact of contacts) { this.openChat(contact); } }); } openChatCollapsed(recipient) { if (!this.isChatWithRecipientOpen(recipient)) { const openChats = this.openChats$.getValue(); const chatWindow = new ChatWindowState(recipient, true); const copyWithNewContact = [chatWindow].concat(openChats); this.openChats$.next(copyWithNewContact); } } openChat(recipient) { this.openChatCollapsed(recipient); this.findChatWindowStateByRecipient(recipient).isCollapsed = false; } closeChat(recipient) { const openChats = this.openChats$.getValue(); const index = this.findChatWindowStateIndexByRecipient(recipient); if (index >= 0) { const copyWithoutContact = openChats.slice(); copyWithoutContact.splice(index, 1); this.openChats$.next(copyWithoutContact); } } openTrack(track) { this.openTracks$.next(this.openTracks$.getValue().concat([track])); } closeTrack(track) { this.openTracks$.next(this.openTracks$.getValue().filter(s => s !== track)); } isChatWithRecipientOpen(recipient) { return this.findChatWindowStateByRecipient(recipient) !== undefined; } findChatWindowStateIndexByRecipient(recipient) { return this.openChats$.getValue() .findIndex((chatWindowState) => chatWindowState.recipient.equalsBareJid(recipient)); } findChatWindowStateByRecipient(recipient) { return this.openChats$.getValue().find(chat => chat.recipient.equalsBareJid(recipient)); } } ChatListStateService.decorators = [ { type: Injectable } ]; ChatListStateService.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [CHAT_SERVICE_TOKEN,] }] } ]; /** * Used to determine if a message component for a given recipient is open. */ class ChatMessageListRegistryService { constructor() { this.openChats$ = new BehaviorSubject(new Set()); this.chatOpened$ = new Subject(); this.recipientToOpenMessageListCount = new Map(); } isChatOpen(recipient) { return this.getOrDefault(recipient, 0) > 0; } incrementOpenWindowCount(recipient) { const wasWindowOpen = this.isChatOpen(recipient); this.recipientToOpenMessageListCount.set(recipient, this.getOrDefault(recipient, 0) + 1); const openWindowSet = this.openChats$.getValue(); openWindowSet.add(recipient); this.openChats$.next(openWindowSet); if (!wasWindowOpen) { this.chatOpened$.next(recipient); } } decrementOpenWindowCount(recipient) { const newValue = this.getOrDefault(recipient, 0) - 1; if (newValue <= 0) { this.recipientToOpenMessageListCount.set(recipient, 0); const openWindowSet = this.openChats$.getValue(); openWindowSet.delete(recipient); this.openChats$.next(openWindowSet); } else { this.recipientToOpenMessageListCount.set(recipient, newValue); } } getOrDefault(recipient, defaultValue) { return this.recipientToOpenMessageListCount.get(recipient) || defaultValue; } } ChatMessageListRegistryService.decorators = [ { type: Injectable } ]; ChatMessageListRegistryService.ctorParameters = () => []; // tslint:disable const dummyAvatarContact = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgdmlld0JveD0iMCAwIDYwMCA2MDAiPgogIDxkZWZzPgogICAgPGNsaXBQYXRoIGlkPSJjbGlwLV8xIj4KICAgICAgPHJlY3Qgd2lkdGg9IjYwMCIgaGVpZ2h0PSI2MDAiLz4KICAgIDwvY2xpcFBhdGg+CiAgPC9kZWZzPgogIDxnIGlkPSJfMSIgZGF0YS1uYW1lPSIxIiBjbGlwLXBhdGg9InVybCgjY2xpcC1fMSkiPgogICAgPHJlY3Qgd2lkdGg9IjYwMCIgaGVpZ2h0PSI2MDAiIGZpbGw9IiNmZmYiLz4KICAgIDxnIGlkPSJHcnVwcGVfNzcxNyIgZGF0YS1uYW1lPSJHcnVwcGUgNzcxNyI+CiAgICAgIDxyZWN0IGlkPSJSZWNodGVja18xMzk3IiBkYXRhLW5hbWU9IlJlY2h0ZWNrIDEzOTciIHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIiBmaWxsPSIjZTVlNmU4Ii8+CiAgICAgIDxlbGxpcHNlIGlkPSJFbGxpcHNlXzI4MyIgZGF0YS1uYW1lPSJFbGxpcHNlIDI4MyIgY3g9IjExNi4yMzEiIGN5PSIxMjUuNjcxIiByeD0iMTE2LjIzMSIgcnk9IjEyNS42NzEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE4NS4yMzEgMTExLjQ4NSkiIGZpbGw9IiNhZmI0YjgiLz4KICAgICAgPHBhdGggaWQ9IlBmYWRfMjQ5NjIiIGRhdGEtbmFtZT0iUGZhZCAyNDk2MiIgZD0iTTU0Ni4zNTksNTk1LjI3NnMwLTIxNy41NjMtMjQ0LjkwOS0yMTcuNTYzaC0xLjQ1N2MtMjQ0LjkwOSwwLTI0NC45MDksMjE3LjU2My0yNDQuOTA5LDIxNy41NjMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgNC43MjQpIiBmaWxsPSIjYWZiNGI4Ii8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K'; const dummyAvatarRoom = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNjAwIiBoZWlnaHQ9IjYwMCIgdmlld0JveD0iMCAwIDYwMCA2MDAiPgogIDxkZWZzPgogICAgPGNsaXBQYXRoIGlkPSJjbGlwLV8zIj4KICAgICAgPHJlY3Qgd2lkdGg9IjYwMCIgaGVpZ2h0PSI2MDAiLz4KICAgIDwvY2xpcFBhdGg+CiAgPC9kZWZzPgogIDxnIGlkPSJfMyIgZGF0YS1uYW1lPSIzIiBjbGlwLXBhdGg9InVybCgjY2xpcC1fMykiPgogICAgPHJlY3Qgd2lkdGg9IjYwMCIgaGVpZ2h0PSI2MDAiIGZpbGw9IiNmZmYiLz4KICAgIDxnIGlkPSJHcnVwcGVfNzcxOCIgZGF0YS1uYW1lPSJHcnVwcGUgNzcxOCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTc4MC42OTcgODgxLjUpIj4KICAgICAgPHJlY3QgaWQ9IlJlY2h0ZWNrXzEzOTgiIGRhdGEtbmFtZT0iUmVjaHRlY2sgMTM5OCIgd2lkdGg9IjYwMCIgaGVpZ2h0PSI1OTkuOTk1IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3ODAuNjk3IC04ODEuNSkiIGZpbGw9IiNlNWU2ZTgiLz4KICAgICAgPGVsbGlwc2UgaWQ9IkVsbGlwc2VfMjg0IiBkYXRhLW5hbWU9IkVsbGlwc2UgMjg0IiBjeD0iMTE2LjIzMSIgY3k9IjEyNS42NzEiIHJ4PSIxMTYuMjMxIiByeT0iMTI1LjY3MSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTY1LjkyNiAtNzY5LjA5MykiIGZpbGw9IiNhZmI0YjgiLz4KICAgICAgPGVsbGlwc2UgaWQ9IkVsbGlwc2VfMjg1IiBkYXRhLW5hbWU9IkVsbGlwc2UgMjg1IiBjeD0iNjcuOTk4IiBjeT0iNzMuNTIxIiByeD0iNjcuOTk4IiByeT0iNzMuNTIxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4MTYuMjEgLTY2NS40OTYpIiBmaWxsPSIjYWZiNGI4Ii8+CiAgICAgIDxlbGxpcHNlIGlkPSJFbGxpcHNlXzI4OSIgZGF0YS1uYW1lPSJFbGxpcHNlIDI4OSIgY3g9IjY3Ljk5OCIgY3k9IjczLjUyMSIgcng9IjY3Ljk5OCIgcnk9IjczLjUyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIxMi4xMDcgLTY2NS40OTYpIiBmaWxsPSIjYWZiNGI4Ii8+CiAgICAgIDxwYXRoIGlkPSJQZmFkXzI0OTYzIiBkYXRhLW5hbWU9IlBmYWQgMjQ5NjMiIGQ9Ik0xMzI3LjA1Mi0yODYuMjI1czAtMjE3LjU2My0yNDQuOTA3LTIxNy41NjNoLTEuNDU3Yy0yNDQuOTA3LDAtMjQ0LjkwNywyMTcuNTYzLTI0NC45MDcsMjE3LjU2M1oiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgNC43MjUpIiBmaWxsPSIjYWZiNGI4Ii8+CiAgICAgIDxwYXRoIGlkPSJQZmFkXzI0OTY0IiBkYXRhLW5hbWU9IlBmYWQgMjQ5NjQiIGQ9Ik05MzMuOTc3LTQ4My44Yy0xLjA1LjYtMi4xLDEuMjItMy4xNCwxLjg0LTMyLjM0LDE5LjM0LTU4LjI5LDQ2LjI3LTc3LjEyLDgwLjA1LTMxLjcsNTYuODgtMzQuMzU1LDExOC43MjgtMzQuMzU1LDEyMS4yNDhoLTQwLjkxTDc4MC43LTQ3MS4zMmMyMy4yOC0xOC44Miw1Ny4wNS0zMi40NywxMDYuMDQtMzIuNDdoLjk0YTIxNy43NTMsMjE3Ljc1MywwLDAsMSw0My44Myw0LjE4QTguNTQ5LDguNTQ5LDAsMCwxLDkzMy45NzctNDgzLjhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTAgLTAuNzI1KSIgZmlsbD0iI2FmYjRiOCIvPgogICAgICA8cGF0aCBpZD0iUGZhZF8yNDk2OCIgZGF0YS1uYW1lPSJQZmFkIDI0OTY4IiBkPSJNNzgyLjc5LTQ4My44YzEuMDUuNiwyLjEsMS4yMiwzLjE0LDEuODQsMzIuMzQsMTkuMzQsNTguMjksNDYuMjcsNzcuMTIsODAuMDUsMzEuNyw1Ni44OCwzNC4zNTUsMTE4LjcyOCwzNC4zNTUsMTIxLjI0OGg0MC45MUw5MzYuMDctNDcxLjMyYy0yMy4yOC0xOC44Mi01Ny4wNS0zMi40Ny0xMDYuMDQtMzIuNDdoLS45NGEyMTcuNzUzLDIxNy43NTMsMCwwLDAtNDMuODMsNC4xOEE4LjU0OSw4LjU0OSwwLDAsMCw3ODIuNzktNDgzLjhaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0NTcuNTQ3IC0wLjcyNikiIGZpbGw9IiNhZmI0YjgiLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo='; const chatAdminUserName = 'admin@chat.mahamma.com'; const chatAdminPassword = 'tDm2R&nMRr47w!dL'; const getAvatarUrl = 'https://chat.mahamma.com:5443/api/get_vcard'; const identity = elem => elem; const toString = elem => elem.toString(); /** * given a sorted list, insert the given item in place after the last matching item. * @param elemToInsert the item to insert * @param list the list in which the element should be inserted * @param keyExtractor an optional element mapper, defaults to toString */ function insertSortedLast(elemToInsert, list, keyExtractor = toString) { list.splice(findSortedInsertionIndexLast(keyExtractor(elemToInsert), list, keyExtractor), 0, elemToInsert); } /** * Find the highest possible index where the given element should be inserted so that the order of the list is preserved. * @param needle the needle to find * @param haystack the pre sorted list * @param keyExtractor an optional needle mapper, defaults to toString */ function findSortedInsertionIndexLast(needle, haystack, keyExtractor = toString) { let low = 0; let high = haystack.length; while (low !== high) { const cur = Math.floor(low + (high - low) / 2); if (needle < keyExtractor(haystack[cur])) { high = cur; } else { low = cur + 1; } } return low; } /** * Find the index of an element in a sorted list. If list contains no matching element, return -1. */ function findSortedIndex(needle, haystack, keyExtractor = toString) { let low = 0; let high = haystack.length; while (low !== high) { const cur = Math.floor(low + (high - low) / 2); const extractedKey = keyExtractor(haystack[cur]); if (needle < extractedKey) { high = cur; } else if (needle > extractedKey) { low = cur + 1; } else { return cur; } } return -1; } /** * Like {@link Array.prototype.findIndex} but finds the last index instead. */ function findLastIndex(arr, predicate) { for (let i = arr.length - 1; i >= 0; i--) { if (predicate(arr[i])) { return i; } } return -1; } /** * Like {@link Array.prototype.find} but finds the last matching element instead. */ function findLast(arr, predicate) { return arr[findLastIndex(arr, predicate)]; } /** * Return a new array, where all elements from the original array occur exactly once. */ function removeDuplicates(arr, eq = (x, y) => x === y) { const results = []; for (const arrElement of arr) { let duplicateFound = false; for (const resultElement of results) { if (eq(arrElement, resultElement)) { duplicateFound = true; break; } } if (!duplicateFound) { results.push(arrElement); } } return results; } /** * converts date objects to date strings like '2011-10-05' */ function extractDateStringFromDate(date) { const isoString = date.toISOString(); return isoString.slice(0, isoString.indexOf('T')); } class MessageStore { constructor(logService) { this.logService = logService; this.messages = []; this.dateMessageGroups = []; this.messageIdToMessage = new Map(); this.messages$ = new Subject(); } addMessage(message) { if (message.id && this.messageIdToMessage.has(message.id)) { if (this.logService) { this.logService.warn(`message with id ${message.id} already exists`); } return false; } insertSortedLast(message, this.messages, m => m.datetime); this.addToDateMessageGroups(message); this.messageIdToMessage.set(message.id, message); this.messages$.next(message); return true; } get oldestMessage() { return this.messages[0]; } get mostRecentMessage() { return this.messages[this.messages.length - 1]; } get mostRecentMessageReceived() { return findLast(this.messages, msg => msg.direction === Direction.in); } get mostRecentMessageSent() { return findLast(this.messages, msg => msg.direction === Direction.out); } addToDateMessageGroups(message) { const dateString = extractDateStringFromDate(message.datetime); const groupIndex = findSortedIndex(dateString, this.dateMessageGroups, group => extractDateStringFromDate(group.date)); if (groupIndex !== -1) { insertSortedLast(message, this.dateMessageGroups[groupIndex].messages, m => m.datetime); } else { const groupToInsert = { date: message.datetime, messages: [message] }; const insertIndex = findSortedInsertionIndexLast(dateString, this.dateMessageGroups, group => extractDateStringFromDate(group.date)); this.dateMessageGroups.splice(insertIndex, 0, groupToInsert); } } } var Presence; (function (Presence) { Presence["present"] = "present"; Presence["unavailable"] = "unavailable"; Presence["away"] = "away"; })(Presence || (Presence = {})); function isJid(o) { // due to unknown reasons, `o instanceof JID` does not work when // JID is instantiated by an application instead of ngx-chat return !!o.bare; } var ContactSubscription; (function (ContactSubscription) { ContactSubscription["to"] = "to"; ContactSubscription["from"] = "from"; ContactSubscription["both"] = "both"; ContactSubscription["none"] = "none"; })(ContactSubscription || (ContactSubscription = {})); class Contact { /** * Do not call directly, use {@link ContactFactoryService#createContact} instead. */ constructor(httpClinet, jidPlain, name, nick, logService, avatar) { this.httpClinet = httpClinet; this.name = name; this.nick = nick; this.recipientType = "contact"; this.avatar = dummyAvatarContact; this.chatUserName = chatAdminUserName; this.chatPassword = chatAdminPassword; this.avatarUrl = getAvatarUrl; this.metadata = {}; // private _httpHandler: HttpHandler; this.presence$ = new BehaviorSubject(Presence.unavailable); this.subscription$ = new BehaviorSubject(ContactSubscription.none); this.pendingOut$ = new BehaviorSubject(false); this.pendingIn$ = new BehaviorSubject(false); this.resources$ = new BehaviorSubject(new Map()); this._httpClient = httpClinet; const jid$1 = jid(jidPlain); this.jidFull = jid$1; this.jidBare = jid$1.bare(); let user = { user: jid$1.local, host: jid$1.domain, name: "URL", }; let credentials = this.chatUserName + ":" + this.chatPassword; debugger; const httpOptions = { headers: new HttpHeaders({ "Content-Type": "application/json", Authorization: //"Basic " + btoa("admin@chat.mahamma.com:tDm2R&nMRr47w!dL"), "Basic " + btoa(credentials), }), }; this._httpClient.post(this.avatarUrl, user, httpOptions) .subscribe((result) => { debugger; this.avatar = result.content; }); // this.avatar ="https://picsum.photos/200/300"; this.messageStore = new MessageStore(logService); } get messages$() { return this.messageStore.messages$; } get messages() { return this.messageStore.messages; } get dateMessagesGroups() { return this.messageStore.dateMessageGroups; } get oldestMessage() { return this.messageStore.oldestMessage; } get mostRecentMessage() { return this.messageStore.mostRecentMessage; } get mostRecentMessageReceived() { return this.messageStore.mostRecentMessageReceived; } get mostRecentMessageSent() { return this.messageStore.mostRecentMessageSent; } addMessage(message) { this.messageStore.addMessage(message); } equalsBareJid(other) { if (other instanceof Contact || isJid(other)) { const otherJid = other instanceof Contact ? other.jidBare : other.bare(); return this.jidBare.equals(otherJid); } return false; } isSubscribed() { const subscription = this.subscription$.getValue(); return (subscription === ContactSubscription.both || subscription === ContactSubscription.to); } isUnaffiliated() { return (!this.isSubscribed() && !this.pendingIn$.getValue() && !this.pendingOut$.getValue()); } updateResourcePresence(jid, presence) { const resources = this.resources$.getValue(); resources.set(jid, presence); this.presence$.next(this.determineOverallPresence(resources)); this.resources$.next(resources); } getMessageById(id) { return this.messageStore.messageIdToMessage.get(id); } determineOverallPresence(jidToPresence) { let result = Presence.unavailable; [...jidToPresence.values()].some((presence) => { if (presence === Presence.present) { result = presence; return true; } else if (presence === Presence.away) { result = Presence.away; } return false; }); return result; } } var LogLevel; (function (LogLevel) { LogLevel[LogLevel["Disabled"] = 0] = "Disabled"; LogLevel[LogLevel["Error"] = 1] = "Error"; LogLevel[LogLevel["Warn"] = 2] = "Warn"; LogLevel[LogLevel["Info"] = 3] = "Info"; LogLevel[LogLevel["Debug"] = 4] = "Debug"; })(LogLevel || (LogLevel = {})); class LogService { constructor() { this.logLevel = LogLevel.Info; this.writer = console; this.messagePrefix = () => 'ChatService:'; } error(...messages) { if (this.logLevel >= LogLevel.Error) { this.writer.error(this.messagePrefix(), ...messages); } } warn(...messages) { if (this.logLevel >= LogLevel.Warn) { this.writer.warn(this.messagePrefix(), ...messages); } } info(...messages) { if (this.logLevel >= LogLevel.Info) { this.writer.info(this.messagePrefix(), ...messages); } } debug(...messages) { if (this.logLevel >= LogLevel.Debug) { this.writer.debug(this.messagePrefix(), ...messages); } } } LogService.decorators = [ { type: Injectable } ]; class ContactFactoryService { constructor(logService, httpClien