diff options
author | sualko <klaus@jsxc.org> | 2021-12-31 02:22:46 +0300 |
---|---|---|
committer | sualko <klaus@jsxc.org> | 2021-12-31 02:30:45 +0300 |
commit | 49e1ad7c81d76cf3af54ae9ddb1c9e08b4bae5a0 (patch) | |
tree | bcb8f7649193412079698c0a3dc8608faad166b7 | |
parent | b49f4deae29a4a3e4c0a83c9a7733286ce10e1b5 (diff) |
feat: add XEP-0308 as text command
Last Message Correction
-rw-r--r-- | scss/partials/_window.scss | 7 | ||||
-rw-r--r-- | src/Account.ts | 2 | ||||
-rw-r--r-- | src/CommandRepository.ts | 8 | ||||
-rw-r--r-- | src/Contact.ts | 3 | ||||
-rw-r--r-- | src/Message.interface.ts | 14 | ||||
-rw-r--r-- | src/Message.ts | 50 | ||||
-rw-r--r-- | src/Transcript.ts | 26 | ||||
-rw-r--r-- | src/bootstrap/plugins.ts | 4 | ||||
-rw-r--r-- | src/connection/xmpp/MessageElement.ts | 2 | ||||
-rw-r--r-- | src/connection/xmpp/handlers/chatMessage.ts | 4 | ||||
-rw-r--r-- | src/plugins/LastMessageCorrectionPlugin.ts | 151 | ||||
-rw-r--r-- | src/ui/ChatWindow.ts | 19 | ||||
-rw-r--r-- | src/ui/ChatWindowMessage.ts | 43 | ||||
-rw-r--r-- | template/chat-window-message.hbs | 1 |
14 files changed, 312 insertions, 22 deletions
diff --git a/scss/partials/_window.scss b/scss/partials/_window.scss index 7eea2cb5..8dd31dc6 100644 --- a/scss/partials/_window.scss +++ b/scss/partials/_window.scss @@ -526,6 +526,13 @@ } } +.jsxc-chatmessage.jsxc-edited { + .jsxc-version::before { + @extend %jsxc-icon-font; + content: map-get($jsxc-icons-map, "edit"); + } +} + .jsxc-mark::before { height: 1em; diff --git a/src/Account.ts b/src/Account.ts index 8ebacf5b..6a007ec9 100644 --- a/src/Account.ts +++ b/src/Account.ts @@ -51,7 +51,7 @@ export default class Account { private ownDiscoInfo: DiscoInfoChangeable; - private hookRepository = new HookRepository<any>(); + private hookRepository = new HookRepository(); private contactManager: ContactManager; diff --git a/src/CommandRepository.ts b/src/CommandRepository.ts index 90148d7d..97c3753b 100644 --- a/src/CommandRepository.ts +++ b/src/CommandRepository.ts @@ -13,7 +13,11 @@ export class ArgumentError extends Error { } } -export type CommandAction = (args: string[], contact: IContact | MultiUserContact) => Promise<boolean>; +export type CommandAction = ( + args: string[], + contact: IContact | MultiUserContact, + message?: string +) => Promise<boolean>; export default class CommandRepository { private commands: { @@ -47,7 +51,7 @@ export default class CommandRepository { return Promise.resolve(false); } - return this.commands[command].action(args, contact); + return this.commands[command].action(args, contact, message); } public getHelp() { diff --git a/src/Contact.ts b/src/Contact.ts index 12a415d8..a77d5b64 100644 --- a/src/Contact.ts +++ b/src/Contact.ts @@ -15,6 +15,7 @@ import ChatWindow from './ui/ChatWindow'; import ContactProvider from './ContactProvider'; import DiscoInfo from './DiscoInfo'; import { IJID } from './JID.interface'; +import { IMessage } from './Message.interface'; export default class Contact implements IIdentifiable, IContact { protected storage: Storage; @@ -111,7 +112,7 @@ export default class Contact implements IIdentifiable, IContact { return this.account; } - public addSystemMessage(messageString: string): Message { + public addSystemMessage(messageString: string): IMessage { let message = new Message({ peer: this.getJid(), direction: Message.DIRECTION.SYS, diff --git a/src/Message.interface.ts b/src/Message.interface.ts index 2a44ece8..ef5e7483 100644 --- a/src/Message.interface.ts +++ b/src/Message.interface.ts @@ -43,6 +43,8 @@ export interface IMessagePayload { chatMarkersReceived?: boolean; chatMarkersDisplayed?: boolean; chatMarkersAcknowledged?: boolean; + replacedBy?: string; + original?: string; } export interface IMessage { @@ -145,4 +147,16 @@ export interface IMessage { getErrorMessage(): string; updateProgress(transferred: number, complete: number); + + getLastVersion(): IMessage; + + getReplacedBy(): IMessage; + + setReplacedBy(message: IMessage): void; + + getOriginal(): IMessage; + + setOriginal(message: IMessage): void; + + isReplacement(): boolean; } diff --git a/src/Message.ts b/src/Message.ts index a66272d9..e3ab0d7d 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -54,6 +54,10 @@ export default class Message implements IIdentifiable, IMessage { private attachment: Attachment; + private replacedBy: IMessage; + + private original: IMessage; + public static readonly DIRECTION = DIRECTION; public static readonly MSGTYPE = ContactType; @@ -367,6 +371,52 @@ export default class Message implements IIdentifiable, IMessage { public updateProgress(transferred: number, size: number) { this.data.set('progress', transferred / size); } + + public getLastVersion(): IMessage { + let replacedBy = this.getReplacedBy(); + + while (replacedBy && replacedBy.getReplacedBy()) { + replacedBy = replacedBy.getReplacedBy(); + } + + return replacedBy || this; + } + + public getReplacedBy(): IMessage { + if (this.replacedBy) { + return this.replacedBy; + } + + const replacedByUid = this.data.get('replacedBy'); + + this.replacedBy = replacedByUid ? new Message(replacedByUid) : undefined; + + return this.replacedBy; + } + + public setReplacedBy(message: IMessage): void { + this.data.set('replacedBy', message.getUid()); + } + + public getOriginal(): IMessage { + if (this.original) { + return this.original; + } + + const originalUid = this.data.get('original'); + + this.original = originalUid ? new Message(originalUid) : undefined; + + return this.original; + } + + public setOriginal(message: IMessage): void { + this.data.set('original', message.getUid()); + } + + public isReplacement(): boolean { + return !!this.data.get('original'); + } } function convertUrlToLink(text: string) { diff --git a/src/Transcript.ts b/src/Transcript.ts index dcb9d883..cf00f29d 100644 --- a/src/Transcript.ts +++ b/src/Transcript.ts @@ -55,7 +55,23 @@ export default class Transcript { public getFirstChatMessage(): IMessage { for (let message of this.getGenerator()) { - if (!message.isSystem()) { + if (!message.isSystem() && !message.isReplacement()) { + return message; + } + } + } + + public getFirstIncomingMessage(): IMessage { + for (let message of this.getGenerator()) { + if (message.isIncoming() && !message.isReplacement()) { + return message; + } + } + } + + public getFirstOutgoingMessage(): IMessage { + for (let message of this.getGenerator()) { + if (message.isOutgoing() && !message.isReplacement()) { return message; } } @@ -69,6 +85,14 @@ export default class Transcript { return this.firstMessage; } + public getFirstOriginalMessage(): IMessage { + for (let message of this.getGenerator()) { + if (!message.isReplacement()) { + return message; + } + } + } + public getLastMessage(): IMessage { if (this.lastMessage) { return this.lastMessage; diff --git a/src/bootstrap/plugins.ts b/src/bootstrap/plugins.ts index 933ef013..2ad52c77 100644 --- a/src/bootstrap/plugins.ts +++ b/src/bootstrap/plugins.ts @@ -17,7 +17,8 @@ import CommandPlugin from '@src/plugins/CommandPlugin'; import VersionPlugin from '@src/plugins/VersionPlugin'; import TimePlugin from '../plugins/TimePlugin'; import JingleMessageInitiationPlugin from '../plugins/JingleMessageInitiationPlugin'; -import AvatarPEPPlugin from '../plugins/AvatarPEPPlugin'; +import AvatarPEPPlugin from '@src/plugins/AvatarPEPPlugin'; +import LastMessageCorrectionPlugin from '@src/plugins/LastMessageCorrectionPlugin'; Client.addPlugin(OTRPlugin); Client.addPlugin(OMEMOPlugin); @@ -38,3 +39,4 @@ Client.addPlugin(VersionPlugin); Client.addPlugin(TimePlugin); Client.addPlugin(JingleMessageInitiationPlugin); Client.addPlugin(AvatarPEPPlugin); +Client.addPlugin(LastMessageCorrectionPlugin); diff --git a/src/connection/xmpp/MessageElement.ts b/src/connection/xmpp/MessageElement.ts index a4cd902c..933e37a7 100644 --- a/src/connection/xmpp/MessageElement.ts +++ b/src/connection/xmpp/MessageElement.ts @@ -71,7 +71,7 @@ export class MessageElement { return this.element.find(selector); } - public get(index?) { + public get(index?: number) { return this.element.get(index); } diff --git a/src/connection/xmpp/handlers/chatMessage.ts b/src/connection/xmpp/handlers/chatMessage.ts index 6b3b670b..d95fdec0 100644 --- a/src/connection/xmpp/handlers/chatMessage.ts +++ b/src/connection/xmpp/handlers/chatMessage.ts @@ -51,6 +51,10 @@ export default class extends AbstractHandler { let pipe = this.account.getPipe('afterReceiveMessage'); pipe.run(peerContact, message, messageElement.get(0)).then(([contact, message]) => { + if (!message) { + return; + } + if (message.getPlaintextMessage() || message.getHtmlMessage() || message.hasAttachment()) { contact.getTranscript().pushMessage(message); } else { diff --git a/src/plugins/LastMessageCorrectionPlugin.ts b/src/plugins/LastMessageCorrectionPlugin.ts new file mode 100644 index 00000000..8aa1d013 --- /dev/null +++ b/src/plugins/LastMessageCorrectionPlugin.ts @@ -0,0 +1,151 @@ +import { AbstractPlugin, IMetaData } from '../plugin/AbstractPlugin'; +import PluginAPI from '../plugin/PluginAPI'; +import Translation from '@util/Translation'; +import * as Namespace from '@connection/xmpp/namespace'; +import { IContact } from '@src/Contact.interface'; +import { IMessage } from '@src/Message.interface'; +import Message from '@src/Message'; + +/** + * XEP-0308: Last Message Correction + * + * @version: 1.2.0 + * @see: https://xmpp.org/extensions/xep-0308.html + * + */ + +const CORRECTION_CMD = '/fix'; +const LMC = 'urn:xmpp:message-correct:0'; + +const MIN_VERSION = '4.3.0'; +const MAX_VERSION = '99.0.0'; + +Namespace.register('LAST_MSG_CORRECTION', LMC); + +export default class LastMessageCorrectionPlugin extends AbstractPlugin { + public static getId(): string { + return 'lmc'; + } + + public static getName(): string { + return 'Last Message Correction'; + } + + public static getMetaData(): IMetaData { + return { + description: Translation.t('setting-lmc-enable'), + xeps: [ + { + id: 'XEP-0308', + name: 'Last Message Correction', + version: '1.2.0', + }, + ], + }; + } + + private correctionRequests: { [contactUid: string]: IMessage } = {}; + + constructor(pluginAPI: PluginAPI) { + super(MIN_VERSION, MAX_VERSION, pluginAPI); + + pluginAPI.addAfterReceiveMessageProcessor(this.checkMessageCorrection, 90); + + pluginAPI.registerCommand( + CORRECTION_CMD, + async (args, contact, messageString) => { + const originalMessage = contact.getTranscript().getFirstOutgoingMessage(); + + this.correctionRequests[contact.getUid()] = originalMessage; + + if (!originalMessage) { + return false; + } + + const chatWindow = contact.getChatWindow(); + const message = new Message({ + peer: contact.getJid(), + direction: Message.DIRECTION.OUT, + type: contact.getType(), + plaintextMessage: messageString.replace(/^\/fix /, ''), + attachment: chatWindow.getAttachment(), + unread: false, + original: originalMessage.getUid(), + }); + + contact.getTranscript().pushMessage(message); + + chatWindow.clearAttachment(); + + let pipe = contact.getAccount().getPipe('preSendMessage'); + + return pipe + .run(contact, message) + .then(([contact, message]) => { + originalMessage.getLastVersion().setReplacedBy(message); + + contact.getAccount().getConnection().sendMessage(message); + + return true; + }) + .catch(err => { + this.pluginAPI.Log.warn('Error during preSendMessage pipe', err); + + return false; + }); + }, + 'cmd_correction' + ); + + pluginAPI.addPreSendMessageStanzaProcessor(async (message, xmlMsg) => { + const contact = this.pluginAPI.getContact(message.getPeer()); + const originalMessage = this.correctionRequests[contact.getUid()]; + + if (!originalMessage || originalMessage.getLastVersion().getUid() !== message.getUid()) { + return [message, xmlMsg]; + } + + xmlMsg + .c('replace', { + xmlns: LMC, + id: originalMessage.getAttrId(), + }) + .up(); + + return [message, xmlMsg]; + }, 90); + } + + // review carbon copy + private checkMessageCorrection = async ( + contact: IContact, + message: IMessage, + stanza: Element + ): Promise<[IContact, IMessage, Element]> => { + const replaceElement = $(stanza).find(`>replace[xmlns="${LMC}"]`); + + if (replaceElement.length === 0) { + return [contact, message, stanza]; + } + + const attrIdToBeReplaced = replaceElement.attr('id'); + + if (!attrIdToBeReplaced) { + return [contact, message, stanza]; + } + + const firstIncomingMessage = contact.getTranscript().getFirstIncomingMessage(); + + if (firstIncomingMessage && firstIncomingMessage.getAttrId() === attrIdToBeReplaced) { + message.setOriginal(firstIncomingMessage); + + contact.getTranscript().pushMessage(message); + + firstIncomingMessage.getLastVersion().setReplacedBy(message); + + return [contact, undefined, stanza]; + } + + return [contact, message, stanza]; + }; +} diff --git a/src/ui/ChatWindow.ts b/src/ui/ChatWindow.ts index ce03e97e..ba25c670 100644 --- a/src/ui/ChatWindow.ts +++ b/src/ui/ChatWindow.ts @@ -183,6 +183,15 @@ export default class ChatWindow { this.element.find('.jsxc-bar__caption__secondary').text(text); } + public getInput(): string { + return this.inputElement.val().toString(); + } + + public setInput(text: string) { + this.inputElement.val(text); + this.inputElement.trigger('focus'); + } + public appendTextToInput(text: string = '') { let value = this.inputElement.val(); @@ -228,6 +237,10 @@ export default class ChatWindow { return this.settingsMenu.addEntry(label, cb, className); } + public getAttachment(): Attachment { + return this.attachmentDeposition; + } + public setAttachment(attachment: Attachment) { this.attachmentDeposition = attachment; @@ -627,7 +640,7 @@ export default class ChatWindow { } private restoreLocalHistory() { - let firstMessage = this.getTranscript().getFirstMessage(); + let firstMessage = this.getTranscript().getFirstOriginalMessage(); if (!firstMessage) { return; @@ -668,7 +681,9 @@ export default class ChatWindow { let message = this.getTranscript().getMessage(firstMessageId); - this.postMessage(message); + if (!message.isReplacement()) { + this.postMessage(message); + } }); } diff --git a/src/ui/ChatWindowMessage.ts b/src/ui/ChatWindowMessage.ts index 3e646a3a..8547fc5f 100644 --- a/src/ui/ChatWindowMessage.ts +++ b/src/ui/ChatWindowMessage.ts @@ -10,9 +10,13 @@ import Translation from '@util/Translation'; let chatWindowMessageTemplate = require('../../template/chat-window-message.hbs'); export default class ChatWindowMessage { - private element; + private element: JQuery<HTMLElement>; + + private message: IMessage; + + constructor(private originalMessage: IMessage, private chatWindow: ChatWindow) { + this.message = originalMessage.getLastVersion(); - constructor(private message: IMessage, private chatWindow: ChatWindow) { this.generateElement(); this.registerHooks(); } @@ -36,20 +40,22 @@ export default class ChatWindowMessage { } private getNextMessage() { - let nextId = this.message.getNextId(); + let nextId = this.originalMessage.getNextId(); - if (!nextId) { - return; - } + while (nextId) { + let nextMessage = this.chatWindow.getTranscript().getMessage(nextId); - let nextMessage = this.chatWindow.getTranscript().getMessage(nextId); + if (!nextMessage) { + Log.warn('Couldnt find next message.'); + return; + } - if (!nextMessage) { - Log.warn('Couldnt find next message.'); - return; - } + if (!nextMessage.isReplacement()) { + return nextMessage; + } - return nextMessage; + nextId = nextMessage.getNextId(); + } } private async generateElement() { @@ -64,7 +70,7 @@ export default class ChatWindowMessage { LinkHandlerGeo.get().detect(bodyElement); - this.element.find('.jsxc-content').html(bodyElement); + this.element.find('.jsxc-content').html(bodyElement.get(0)); let timestampElement = this.element.find('.jsxc-timestamp'); DateTime.stringify(this.message.getStamp().getTime(), timestampElement); @@ -85,6 +91,10 @@ export default class ChatWindowMessage { this.element.addClass('jsxc-unread'); } + if (this.message.isReplacement()) { + this.element.addClass('jsxc-edited'); + } + if (this.message.getErrorMessage()) { this.element.addClass('jsxc-error'); this.element.find('.jsxc-error-content').text(Translation.t(this.message.getErrorMessage())); @@ -219,6 +229,13 @@ export default class ChatWindowMessage { } private registerHooks() { + this.message.registerHook('replacedBy', () => { + const messageReplacement = this.message.getReplacedBy(); + const chatWindowMessageReplacement = new ChatWindowMessage(messageReplacement, this.chatWindow); + + this.element.replaceWith(chatWindowMessageReplacement.getElement()); + }); + this.message.registerHook('encrypted', encrypted => { if (encrypted) { this.element.addClass('jsxc-encrypted'); diff --git a/template/chat-window-message.hbs b/template/chat-window-message.hbs index f0db247a..a16d9f2e 100644 --- a/template/chat-window-message.hbs +++ b/template/chat-window-message.hbs @@ -3,6 +3,7 @@ <div class="jsxc-chatmessage__footer"> <div class="jsxc-sender"></div> <div class="jsxc-timestamp"></div> + <div class="jsxc-version"></div> <div class="jsxc-transfer"></div> <div class="jsxc-mark"></div> </div> |