Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/jsxc/jsxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKlaus <klaus@jsxc.org>2022-01-02 19:57:08 +0300
committerGitHub <noreply@github.com>2022-01-02 19:57:08 +0300
commit3259aae6d7e27d45d2f95a9e17566396603e0f10 (patch)
treec3fdbcba147720d1f2c3d34ff8679d1d93bbadfd
parentc63aa6823c54fa45027200f653d9802ec63c3798 (diff)
parentbcdb0adefda85029e5f63cd5421467d6e868126e (diff)
Merge pull request #1066 from jsxc/feat-message-correction
Last Message Correction (XEP-0308)
-rw-r--r--fonts/jsxc-icons.eotbin11572 -> 11752 bytes
-rw-r--r--fonts/jsxc-icons.scss18
-rw-r--r--fonts/jsxc-icons.woffbin6272 -> 6372 bytes
-rw-r--r--fonts/jsxc-icons.woff2bin5192 -> 5260 bytes
-rw-r--r--images/icons/quotation.svg60
-rw-r--r--scss/partials/_menu.scss27
-rw-r--r--scss/partials/_window.scss170
-rw-r--r--src/Account.ts13
-rw-r--r--src/CommandRepository.ts8
-rw-r--r--src/Contact.ts3
-rw-r--r--src/Menu.ts19
-rw-r--r--src/MenuChatMessage.ts22
-rw-r--r--src/MenuItemFactory.interface.ts11
-rw-r--r--src/MenuItemStaticFactory.ts19
-rw-r--r--src/Message.interface.ts14
-rw-r--r--src/Message.ts50
-rw-r--r--src/Transcript.ts26
-rw-r--r--src/bootstrap/plugins.ts4
-rw-r--r--src/connection/AbstractConnection.ts14
-rw-r--r--src/connection/xmpp/MessageElement.ts2
-rw-r--r--src/connection/xmpp/handlers/chatMessage.ts4
-rw-r--r--src/plugin/PluginAPI.interface.ts3
-rw-r--r--src/plugin/PluginAPI.ts7
-rw-r--r--src/plugins/LastMessageCorrectionPlugin.ts185
-rw-r--r--src/plugins/NotificationPlugin.ts14
-rw-r--r--src/ui/ChatWindow.ts21
-rw-r--r--src/ui/ChatWindowMessage.ts85
-rw-r--r--src/ui/MenuComponent.ts127
-rw-r--r--src/ui/dialogs/messageHistory.ts22
-rw-r--r--src/ui/util/LongPress.ts32
-rw-r--r--src/util/HookRepository.ts8
-rw-r--r--src/util/Pipe.ts26
-rw-r--r--template/chat-window-message.hbs1
-rw-r--r--template/menu.hbs7
-rw-r--r--template/messageHistory.hbs7
35 files changed, 906 insertions, 123 deletions
diff --git a/fonts/jsxc-icons.eot b/fonts/jsxc-icons.eot
index f8314df0..429e5b2d 100644
--- a/fonts/jsxc-icons.eot
+++ b/fonts/jsxc-icons.eot
Binary files differ
diff --git a/fonts/jsxc-icons.scss b/fonts/jsxc-icons.scss
index c3be0b55..89d4e8b3 100644
--- a/fonts/jsxc-icons.scss
+++ b/fonts/jsxc-icons.scss
@@ -2,9 +2,9 @@ $jsxc-icons-font: "jsxc-icons";
@font-face {
font-family: $jsxc-icons-font;
- src: url("../fonts/jsxc-icons.eot?1a3d16709418122adaef6fedd3d0c8fe#iefix") format("embedded-opentype"),
-url("../fonts/jsxc-icons.woff2?1a3d16709418122adaef6fedd3d0c8fe") format("woff2"),
-url("../fonts/jsxc-icons.woff?1a3d16709418122adaef6fedd3d0c8fe") format("woff");
+ src: url("../fonts/jsxc-icons.eot?d795960dd0062a30f89bb22c3b0df55e#iefix") format("embedded-opentype"),
+url("../fonts/jsxc-icons.woff2?d795960dd0062a30f89bb22c3b0df55e") format("woff2"),
+url("../fonts/jsxc-icons.woff?d795960dd0062a30f89bb22c3b0df55e") format("woff");
}
[class^="jsxc-icon-"]:before, [class*=" jsxc-icon-"]:before {
@@ -56,10 +56,11 @@ $jsxc-icons-map: (
"pick-up-disabled": "\f123",
"pick-up": "\f124",
"placeholder": "\f125",
- "resize": "\f126",
- "search": "\f127",
- "smiley": "\f128",
- "speech-balloon": "\f129",
+ "quotation": "\f126",
+ "resize": "\f127",
+ "search": "\f128",
+ "smiley": "\f129",
+ "speech-balloon": "\f12a",
);
.jsxc-icon-attachment:before {
@@ -173,6 +174,9 @@ $jsxc-icons-map: (
.jsxc-icon-placeholder:before {
content: map-get($jsxc-icons-map, "placeholder");
}
+.jsxc-icon-quotation:before {
+ content: map-get($jsxc-icons-map, "quotation");
+}
.jsxc-icon-resize:before {
content: map-get($jsxc-icons-map, "resize");
}
diff --git a/fonts/jsxc-icons.woff b/fonts/jsxc-icons.woff
index 3c29036b..3c9379d6 100644
--- a/fonts/jsxc-icons.woff
+++ b/fonts/jsxc-icons.woff
Binary files differ
diff --git a/fonts/jsxc-icons.woff2 b/fonts/jsxc-icons.woff2
index df7ebe09..4784938e 100644
--- a/fonts/jsxc-icons.woff2
+++ b/fonts/jsxc-icons.woff2
Binary files differ
diff --git a/images/icons/quotation.svg b/images/icons/quotation.svg
new file mode 100644
index 00000000..7a7b98c5
--- /dev/null
+++ b/images/icons/quotation.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="44"
+ height="44"
+ version="1.1"
+ id="svg4"
+ sodipodi:docname="quotation.svg"
+ inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1999"
+ inkscape:window-height="1265"
+ id="namedview6"
+ showgrid="false"
+ inkscape:zoom="5.3636364"
+ inkscape:cx="-5.5"
+ inkscape:cy="22"
+ inkscape:window-x="1562"
+ inkscape:window-y="280"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="svg4" />
+ <g
+ aria-label="&quot;"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:17.33333397px;line-height:125%;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="text819"
+ transform="matrix(7.8234615,0,0,7.8234615,-143.85981,-87.522429)">
+ <path
+ d="m 19.579639,16.321897 0.277334,-0.208 0.849333,-2.426667 0.052,-0.624 -0.06933,-0.485333 -0.208,-0.329333 -0.502667,-0.398667 -0.398667,-0.138667 h -0.294666 l -0.416,0.138667 -0.06933,0.173333 -0.138666,0.08667 -0.104,0.225333 -0.138667,0.814667 0.173333,0.208 0.364,0.208 0.416,0.416 0.173334,0.988 z m 3.224,-0.03467 0.277334,-0.208 0.849333,-2.426666 0.052,-0.624 -0.06933,-0.485334 -0.208,-0.329333 -0.502667,-0.398667 -0.398667,-0.138666 h -0.294666 l -0.416,0.138666 -0.06933,0.173334 -0.138666,0.08667 -0.104,0.225334 -0.138667,0.814666 0.173333,0.208 0.364,0.208 0.416,0.416 0.173334,0.988 z"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Bocadillo;-inkscape-font-specification:Bocadillo"
+ id="path821"
+ inkscape:connector-curvature="0" />
+ </g>
+</svg>
diff --git a/scss/partials/_menu.scss b/scss/partials/_menu.scss
index 51e38173..1b98fc26 100644
--- a/scss/partials/_menu.scss
+++ b/scss/partials/_menu.scss
@@ -4,11 +4,13 @@
&__button {
align-items: flex-start;
+ color: "inherit";
cursor: pointer;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
+ text-decoration: none;
user-select: none;
white-space: nowrap;
@@ -65,7 +67,8 @@
&--drop-bottom-left,
&--drop-bottom-right,
&--drop-right-top,
- &--vertical-left {
+ &--vertical-left
+ &--vertical-right {
.jsxc-menu__content {
border-radius: 3px;
max-height: 9999px;
@@ -133,6 +136,28 @@
}
}
+ &--vertical-right {
+ .jsxc-menu__content {
+ left: 100%;
+ top: 0;
+
+ &::after {
+ right: 100%;
+ top: 14px;
+ transform: rotate(90deg);
+ }
+ }
+
+ ul {
+ display: flex;
+ padding: 0;
+ }
+
+ li {
+ height: 42px;
+ }
+ }
+
&--drop-bottom-left {
.jsxc-menu__content {
bottom: 100%;
diff --git a/scss/partials/_window.scss b/scss/partials/_window.scss
index 7eea2cb5..334f76ce 100644
--- a/scss/partials/_window.scss
+++ b/scss/partials/_window.scss
@@ -346,82 +346,113 @@
}
}
- p {
- clear: both;
- font-size: 1em;
- margin: 0;
-
- +p {
- margin-top: 0.7em;
+ .jsxc-content {
+ p {
+ clear: both;
+ font-size: 1em;
+ margin: 0;
+
+ +p {
+ margin-top: 0.7em;
+ }
}
- }
- a {
- color: $chatmessage-a;
- display: inline-block;
- max-width: 100%;
- position: relative;
- text-decoration: underline;
-
- &[download]::before {
- background-color: rgba(255, 255, 255, 0.7);
- background-image: url("../images/download_icon_black.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: 3em;
- border-radius: 3px;
- bottom: 5px;
- content: " ";
- left: 0;
- opacity: 0;
- position: absolute;
- right: 0;
- top: 0;
- transition: opacity 0.5s;
- }
+ a {
+ color: $chatmessage-a;
+ display: inline-block;
+ max-width: 100%;
+ position: relative;
+ text-decoration: underline;
- &[download]:hover {
- &::before {
- opacity: 0.6;
+ &[download]::before {
+ background-color: rgba(255, 255, 255, 0.7);
+ background-image: url("../images/download_icon_black.svg");
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: 3em;
+ border-radius: 3px;
+ bottom: 5px;
+ content: " ";
+ left: 0;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: opacity 0.5s;
}
- }
- &.jsxc-geo {
- background-color: #fff;
- border-radius: 0.8em;
- display: block;
- padding: 1em 1em 1em 3em;
+ &[download]:hover {
+ &::before {
+ opacity: 0.6;
+ }
+ }
- &::before {
+ &.jsxc-geo {
background-color: #fff;
- background-image: url("../images/location_icon.svg");
- background-position: center center;
- background-repeat: no-repeat;
- background-size: contain;
- content: "";
- display: inline-block;
- height: 2em;
- margin-left: -2em;
- vertical-align: bottom;
- width: 2em;
+ border-radius: 0.8em;
+ display: block;
+ padding: 1em 1em 1em 3em;
+
+ &::before {
+ background-color: #fff;
+ background-image: url("../images/location_icon.svg");
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ content: "";
+ display: inline-block;
+ height: 2em;
+ margin-left: -2em;
+ vertical-align: bottom;
+ width: 2em;
+ }
}
}
+
+ img {
+ max-width: 100%;
+ }
+
+ .jsxc-avatar {
+ display: none;
+ }
+
+ .jsxc-quote {
+ border-left: 3px solid #999;
+ display: inline-block;
+ opacity: 0.7;
+ padding: 0 3px 0 5px;
+ width: 100%; // margin-bottom: 5px;
+ }
}
- img {
- max-width: 100%;
+ &:hover .jsxc-menu__button {
+ visibility: visible !important;
}
- .jsxc-avatar {
+ .jsxc-menu {
display: none;
- }
+ position: absolute;
+ top: 0;
- .jsxc-quote {
- border-left: 3px solid #999;
- display: inline-block;
- opacity: 0.7;
- padding: 0 3px 0 5px;
- width: 100%; // margin-bottom: 5px;
+ .jsxc-menu__button {
+ height: 42px;
+ width: 42px;
+ }
+
+ &:not(.jsxc-menu--opened) .jsxc-menu__button {
+ visibility: hidden;
+ }
+
+ &.jsxc-menu--vertical-right {
+ display: block;
+ right: 100%;
+ }
+
+ &.jsxc-menu--vertical-left {
+ display: block;
+ left: 100%;
+ }
}
}
@@ -526,6 +557,23 @@
}
}
+.jsxc-chatmessage.jsxc-edited {
+ .jsxc-version::before {
+ @extend %jsxc-icon-font;
+ content: map-get($jsxc-icons-map, "edit");
+ cursor: pointer;
+ }
+}
+
+ul.jsxc-message-history {
+ padding: 0.5em 0 2em;
+ width: 250px;
+
+ .jsxc-version {
+ display: none;
+ }
+}
+
.jsxc-mark::before {
height: 1em;
diff --git a/src/Account.ts b/src/Account.ts
index 8ebacf5b..ba9aa61e 100644
--- a/src/Account.ts
+++ b/src/Account.ts
@@ -25,6 +25,7 @@ import CommandRepository from './CommandRepository';
import AvatarSet from '@ui/AvatarSet';
import { IAvatar } from './Avatar.interface';
import CallManager from './CallManager';
+import MenuChatMessage from './MenuChatMessage';
type ConnectionCallback = (status: number, condition?: string) => void;
@@ -51,7 +52,7 @@ export default class Account {
private ownDiscoInfo: DiscoInfoChangeable;
- private hookRepository = new HookRepository<any>();
+ private hookRepository = new HookRepository();
private contactManager: ContactManager;
@@ -59,6 +60,8 @@ export default class Account {
private callManager: CallManager;
+ private chatMessageMenu: MenuChatMessage;
+
private options: Options;
private pipes = {};
@@ -262,6 +265,14 @@ export default class Account {
return this.callManager;
}
+ public getChatMessageMenu(): MenuChatMessage {
+ if (!this.chatMessageMenu) {
+ this.chatMessageMenu = new MenuChatMessage();
+ }
+
+ return this.chatMessageMenu;
+ }
+
public getContact(jid?: IJID): IContact {
return jid && jid.bare !== this.getJID().bare ? this.getContactManager().getContact(jid) : this.contact;
}
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/Menu.ts b/src/Menu.ts
new file mode 100644
index 00000000..a2c50be7
--- /dev/null
+++ b/src/Menu.ts
@@ -0,0 +1,19 @@
+import IMenuItemFactory, { MenuItem } from './MenuItemFactory.interface';
+
+export default class Menu<Params extends any[] = any[]> {
+ constructor(private menuItems: IMenuItemFactory<Params>[] = []) {}
+
+ public getMenuItems(...args: Params): MenuItem[] {
+ return this.menuItems
+ .map(menuItem => menuItem.generate(...args))
+ .filter(menuItem => menuItem !== false) as MenuItem[];
+ }
+
+ public registerMenuItem(menuItem: IMenuItemFactory<Params>) {
+ this.menuItems.push(menuItem);
+ }
+
+ public removeMenuItem(menuItem: IMenuItemFactory<Params>) {
+ this.menuItems = this.menuItems.filter(item => item !== menuItem);
+ }
+}
diff --git a/src/MenuChatMessage.ts b/src/MenuChatMessage.ts
new file mode 100644
index 00000000..1f72ea4a
--- /dev/null
+++ b/src/MenuChatMessage.ts
@@ -0,0 +1,22 @@
+import { IContact } from './Contact.interface';
+import Menu from './Menu';
+import MenuItemStaticFactory from './MenuItemStaticFactory';
+import { IMessage } from './Message.interface';
+
+function quoteMessage(contact: IContact, message: IMessage) {
+ const chatWindow = contact.getChatWindow();
+ const inputText = chatWindow.getInput();
+ const quote = message
+ .getPlaintextMessage()
+ .split('\n')
+ .map(line => '> ' + line)
+ .join('\n');
+
+ chatWindow.setInput(inputText + (!inputText || inputText.endsWith('\n\n') ? '' : '\n\n') + quote + '\n\n');
+}
+
+export default class MenuChatMessage extends Menu<[IContact, IMessage]> {
+ constructor() {
+ super([new MenuItemStaticFactory('core-quote', '', quoteMessage, 'quotation')]);
+ }
+}
diff --git a/src/MenuItemFactory.interface.ts b/src/MenuItemFactory.interface.ts
new file mode 100644
index 00000000..0d8f8b70
--- /dev/null
+++ b/src/MenuItemFactory.interface.ts
@@ -0,0 +1,11 @@
+export type MenuItem = {
+ id: string;
+ label: string;
+ icon?: string;
+ disabled?: boolean;
+ handler: (ev: Event) => void;
+};
+
+export default interface IMenuItemFactory<Params extends any[]> {
+ generate: (...args: Params) => MenuItem | false;
+}
diff --git a/src/MenuItemStaticFactory.ts b/src/MenuItemStaticFactory.ts
new file mode 100644
index 00000000..cbd44959
--- /dev/null
+++ b/src/MenuItemStaticFactory.ts
@@ -0,0 +1,19 @@
+import IMenuItemFactory, { MenuItem } from './MenuItemFactory.interface';
+
+export default class MenuItemStaticFactory<Params extends any[] = any[]> implements IMenuItemFactory<Params> {
+ constructor(
+ private id: string,
+ private label: string,
+ private handler: (...args: Params) => void,
+ private icon?: string
+ ) {}
+
+ public generate(...args: Params): MenuItem {
+ return {
+ id: this.id,
+ label: this.label,
+ handler: () => this.handler(...args),
+ icon: this.icon,
+ };
+ }
+}
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/AbstractConnection.ts b/src/connection/AbstractConnection.ts
index 38df8323..1e4eacf9 100644
--- a/src/connection/AbstractConnection.ts
+++ b/src/connection/AbstractConnection.ts
@@ -149,11 +149,13 @@ abstract class AbstractConnection {
})
.c('body', {
xmlns: Strophe.NS.XHTML,
- })
- .cnode($(htmlMessage).get(0))
- .up()
- .up()
- .up();
+ });
+
+ for (const node of $(htmlMessage).get()) {
+ xmlMsg.cnode(node).up();
+ }
+
+ xmlMsg.up().up();
}
let plaintextMessage = this.getMessage(
@@ -202,7 +204,7 @@ abstract class AbstractConnection {
});
}
- private getMessage(message: Message, getEncryptedMessage: () => string, getMessage: () => string) {
+ private getMessage(message: Message, getEncryptedMessage: () => string, getMessage: () => string): string {
if (message.isEncrypted() && getEncryptedMessage.call(message)) {
return getEncryptedMessage.call(message);
} else if (getMessage.call(message)) {
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/plugin/PluginAPI.interface.ts b/src/plugin/PluginAPI.interface.ts
index b65caddd..d9bb1bd7 100644
--- a/src/plugin/PluginAPI.interface.ts
+++ b/src/plugin/PluginAPI.interface.ts
@@ -12,6 +12,7 @@ import Pipe from '@util/Pipe';
import CommandRepository, { CommandAction } from '@src/CommandRepository';
import IStorage from '@src/Storage.interface';
import CallManager from '@src/CallManager';
+import IMenuItemFactory from '@src/MenuItemFactory.interface';
export interface IPluginAPI {
Log: ILog;
@@ -105,4 +106,6 @@ export interface IPluginAPI {
getAccountUid(): string;
getCallManager(): CallManager;
+
+ registerChatMessageMenuItem(menuItem: IMenuItemFactory<[IContact, IMessage]>): void;
}
diff --git a/src/plugin/PluginAPI.ts b/src/plugin/PluginAPI.ts
index f5faa6c9..bf332910 100644
--- a/src/plugin/PluginAPI.ts
+++ b/src/plugin/PluginAPI.ts
@@ -18,6 +18,7 @@ import IStorage from '@src/Storage.interface';
import CommandRepository, { CommandAction } from '@src/CommandRepository';
import { IAvatar } from '@src/Avatar.interface';
import CallManager from '@src/CallManager';
+import IMenuItemFactory from '@src/MenuItemFactory.interface';
export default class PluginAPI implements IPluginAPI {
private storage: IStorage;
@@ -120,7 +121,7 @@ export default class PluginAPI implements IPluginAPI {
}
public addPreSendMessageStanzaProcessor(
- processor: (message: Message, xmlMsg: Strophe.Builder) => Promise<[Message, Strophe.Builder]>,
+ processor: (message: IMessage, xmlMsg: Strophe.Builder) => Promise<[IMessage, Strophe.Builder]>,
position?: number
) {
this.account.getPipe('preSendMessageStanza').addProcessor(processor, position);
@@ -234,4 +235,8 @@ export default class PluginAPI implements IPluginAPI {
public getCallManager(): CallManager {
return this.account.getCallManager();
}
+
+ public registerChatMessageMenuItem(menuItem: IMenuItemFactory<[IContact, IMessage]>): void {
+ this.account.getChatMessageMenu().registerMenuItem(menuItem);
+ }
}
diff --git a/src/plugins/LastMessageCorrectionPlugin.ts b/src/plugins/LastMessageCorrectionPlugin.ts
new file mode 100644
index 00000000..c3db4c46
--- /dev/null
+++ b/src/plugins/LastMessageCorrectionPlugin.ts
@@ -0,0 +1,185 @@
+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.registerCommand(CORRECTION_CMD, this.commandHandler, 'cmd_correction');
+
+ pluginAPI.addPreSendMessageStanzaProcessor(this.addReplaceElementToStanza, 90);
+
+ pluginAPI.addAfterReceiveMessageProcessor(this.checkMessageCorrection, 90);
+
+ pluginAPI.registerChatMessageMenuItem({
+ generate: this.generateCorrectMessageMenuItem,
+ });
+ }
+
+ private commandHandler = async (args: string[], contact: IContact, messageString: string) => {
+ const originalMessage = contact.getTranscript().getFirstOutgoingMessage();
+
+ this.correctionRequests[contact.getUid()] = originalMessage;
+
+ if (!originalMessage || !contact.isChat()) {
+ 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;
+ });
+ };
+
+ private addReplaceElementToStanza = async (
+ message: IMessage,
+ xmlMsg: Strophe.Builder
+ ): Promise<[IMessage, Strophe.Builder]> => {
+ const contact = this.pluginAPI.getContact(message.getPeer());
+ const originalMessage = this.correctionRequests[contact.getUid()];
+
+ if (!originalMessage || originalMessage.getLastVersion().getUid() !== message.getUid()) {
+ return [message, xmlMsg];
+ }
+
+ delete this.correctionRequests[contact.getUid()];
+
+ xmlMsg
+ .c('replace', {
+ xmlns: LMC,
+ id: originalMessage.getAttrId(),
+ })
+ .up();
+
+ return [message, xmlMsg];
+ };
+
+ 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);
+
+ firstIncomingMessage.getLastVersion().setReplacedBy(message);
+ }
+
+ return [contact, message, stanza];
+ };
+
+ private generateCorrectMessageMenuItem = (contact: IContact, message: IMessage) => {
+ if (message.isOutgoing()) {
+ const lastOutgoingMessage = contact.getTranscript().getFirstOutgoingMessage().getLastVersion();
+
+ if (lastOutgoingMessage.getUid() === message.getUid()) {
+ const chatWindow = contact.getChatWindow();
+
+ return {
+ id: 'lmc-edit',
+ label: '',
+ icon: 'edit',
+ handler: () => {
+ let plaintextMessage = message.getPlaintextMessage();
+
+ if (message.hasAttachment()) {
+ const attachment = message.getAttachment();
+
+ chatWindow.setAttachment(attachment);
+ plaintextMessage = plaintextMessage.replace(attachment.getData() + '\n', '');
+ }
+
+ chatWindow.setInput(CORRECTION_CMD + ' ' + plaintextMessage);
+ },
+ };
+ }
+ }
+
+ return false;
+ };
+}
diff --git a/src/plugins/NotificationPlugin.ts b/src/plugins/NotificationPlugin.ts
index 471fc1b7..1ba6cccc 100644
--- a/src/plugins/NotificationPlugin.ts
+++ b/src/plugins/NotificationPlugin.ts
@@ -1,11 +1,11 @@
-import Message from '../Message';
import { AbstractPlugin } from '../plugin/AbstractPlugin';
import PluginAPI from '../plugin/PluginAPI';
-import Contact from '../Contact';
import Translation from '../util/Translation';
import Notification from '../Notification';
import { Presence } from '../connection/AbstractConnection';
import { SOUNDS } from '../CONST';
+import { IContact } from '@src/Contact.interface';
+import { IMessage } from '@src/Message.interface';
const MIN_VERSION = '4.0.0';
const MAX_VERSION = '99.0.0';
@@ -32,7 +32,11 @@ export default class NotificationPlugin extends AbstractPlugin {
pluginAPI.registerPresenceHook(this.onPresence);
}
- private afterReceiveMessageProcessor = (contact: Contact, message: Message): Promise<any> => {
+ private afterReceiveMessageProcessor = (
+ contact: IContact,
+ message: IMessage,
+ stanza: Element
+ ): Promise<[IContact, IMessage, Element]> => {
if ((message.getPlaintextMessage() || message.getAttachment()) && message.isIncoming()) {
Notification.notify({
title: Translation.t('New_message_from', {
@@ -44,10 +48,10 @@ export default class NotificationPlugin extends AbstractPlugin {
});
}
- return Promise.resolve([contact, message]);
+ return Promise.resolve([contact, message, stanza]);
};
- private onPresence = (contact: Contact, newPresence, oldPresence) => {
+ private onPresence = (contact: IContact, newPresence, oldPresence) => {
if (oldPresence !== Presence.offline || newPresence === Presence.offline) {
return;
}
diff --git a/src/ui/ChatWindow.ts b/src/ui/ChatWindow.ts
index ce03e97e..a3d6e429 100644
--- a/src/ui/ChatWindow.ts
+++ b/src/ui/ChatWindow.ts
@@ -183,6 +183,17 @@ 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');
+
+ this.resizeInputArea();
+ }
+
public appendTextToInput(text: string = '') {
let value = this.inputElement.val();
@@ -228,6 +239,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 +642,7 @@ export default class ChatWindow {
}
private restoreLocalHistory() {
- let firstMessage = this.getTranscript().getFirstMessage();
+ let firstMessage = this.getTranscript().getFirstOriginalMessage();
if (!firstMessage) {
return;
@@ -668,7 +683,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..60538dd5 100644
--- a/src/ui/ChatWindowMessage.ts
+++ b/src/ui/ChatWindowMessage.ts
@@ -6,15 +6,31 @@ import Log from '../util/Log';
import LinkHandlerGeo from '@src/LinkHandlerGeo';
import Color from '@util/Color';
import Translation from '@util/Translation';
+import messageHistory from './dialogs/messageHistory';
+import MenuComponent from './MenuComponent';
+import onLongPress from './util/LongPress';
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, useLastVersion: boolean = true) {
+ this.message = useLastVersion ? originalMessage.getLastVersion() : originalMessage;
+
+ let template = chatWindowMessageTemplate({
+ id: this.message.getCssId(),
+ direction: this.message.getDirectionString(),
+ });
+
+ this.element = $(template);
- constructor(private message: IMessage, private chatWindow: ChatWindow) {
this.generateElement();
this.registerHooks();
+
+ this.initMenu();
}
public getElement() {
@@ -36,35 +52,30 @@ 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() {
- let template = chatWindowMessageTemplate({
- id: this.message.getCssId(),
- direction: this.message.getDirectionString(),
- });
-
- this.element = $(template);
-
let bodyElement = $(await this.message.getProcessedBody());
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 +96,18 @@ export default class ChatWindowMessage {
this.element.addClass('jsxc-unread');
}
+ if (this.message.isReplacement()) {
+ this.element.addClass('jsxc-edited');
+ }
+
+ this.element.find('.jsxc-version').on('click', () => {
+ if (!this.element.hasClass('jsxc-edited')) {
+ return;
+ }
+
+ messageHistory(this.originalMessage, this.chatWindow);
+ });
+
if (this.message.getErrorMessage()) {
this.element.addClass('jsxc-error');
this.element.find('.jsxc-error-content').text(Translation.t(this.message.getErrorMessage()));
@@ -218,7 +241,29 @@ export default class ChatWindowMessage {
}
}
+ private initMenu() {
+ if (this.message.isSystem()) {
+ return;
+ }
+
+ const messageMenu = this.chatWindow.getAccount().getChatMessageMenu();
+ const menuType = this.message.isOutgoing() ? 'vertical-right' : 'vertical-left';
+ const menu = new MenuComponent('more', menuType, messageMenu, [this.chatWindow.getContact(), this.message]);
+
+ this.element.append(menu.getElement());
+
+ onLongPress(this.element, () => {
+ menu.toggle();
+ });
+ }
+
private registerHooks() {
+ this.message.registerHook('replacedBy', () => {
+ const chatWindowMessageReplacement = new ChatWindowMessage(this.originalMessage, this.chatWindow);
+
+ this.element.replaceWith(chatWindowMessageReplacement.getElement());
+ });
+
this.message.registerHook('encrypted', encrypted => {
if (encrypted) {
this.element.addClass('jsxc-encrypted');
diff --git a/src/ui/MenuComponent.ts b/src/ui/MenuComponent.ts
new file mode 100644
index 00000000..c3112ed0
--- /dev/null
+++ b/src/ui/MenuComponent.ts
@@ -0,0 +1,127 @@
+import Menu from '@src/Menu';
+
+const CLASSNAME_OPENED = 'jsxc-menu--opened';
+const menuTemplate = require('../../template/menu.hbs');
+
+export default class MenuComponent<Params extends unknown[]> {
+ private element: JQuery<HTMLElement>;
+
+ private timer: number;
+
+ constructor(
+ label: string | { text: string; icon: string },
+ type: 'pushup' | 'vertical-left' | 'vertical-right',
+ private menu: Menu<Params>,
+ private params: Params,
+ theme: 'dark' | 'light' = 'light'
+ ) {
+ this.element = $(
+ menuTemplate({
+ classes: `jsxc-menu--${type} jsxc-menu--${theme}`,
+ labelText: typeof label === 'object' ? label.text : '',
+ })
+ );
+
+ const icon = typeof label === 'string' ? label : label.icon;
+
+ if (icon) {
+ this.element.find('.jsxc-menu__button').prepend($('<i>').addClass(`jsxc-icon-${icon} jsxc-icon--center`));
+ }
+
+ this.registerHandlers();
+ }
+
+ public getElement(): JQuery<HTMLElement> {
+ return this.element;
+ }
+
+ public getButtonElement(): JQuery {
+ return this.element.find('.jsxc-menu__button');
+ }
+
+ public toggle(): void {
+ if (this.element.hasClass(CLASSNAME_OPENED)) {
+ this.closeMenu();
+ } else {
+ this.openMenu();
+ }
+ }
+
+ private addEntry(
+ label: string,
+ handler: (ev: JQuery.ClickEvent) => void,
+ icon?: string,
+ disabled?: boolean
+ ): JQuery {
+ let itemElement = $('<li>');
+
+ if (disabled) {
+ itemElement.addClass('jsxc-disabled');
+ }
+
+ itemElement.text(label);
+ itemElement.on('click', handler);
+
+ if (icon) {
+ itemElement.prepend($('<i>').addClass(`jsxc-icon-${icon} jsxc-icon--center`));
+ }
+
+ this.element.find('ul').append(itemElement);
+
+ return itemElement;
+ }
+
+ private clearEntries() {
+ this.element.find('ul').empty();
+ }
+
+ private registerHandlers() {
+ this.element.on('click', this.onClick);
+ this.element.on('mouseleave', this.onMouseLeave);
+ this.element.on('mouseenter', this.onMouseEnter);
+ }
+
+ private onClick = (ev: JQuery.ClickEvent) => {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ this.toggle();
+ };
+
+ private onMouseLeave = () => {
+ if (this.element.hasClass(CLASSNAME_OPENED)) {
+ this.timer = window.setTimeout(this.closeMenu, 2000);
+ }
+ };
+
+ private onMouseEnter = () => {
+ window.clearTimeout(this.timer);
+ };
+
+ private openMenu = () => {
+ this.clearEntries();
+
+ this.menu.getMenuItems(...this.params).map(({ label, handler, disabled, icon }) => {
+ this.addEntry(label, (ev: JQuery.ClickEvent) => handler(ev.originalEvent), icon, disabled);
+ });
+
+ $('body').off('click', null, this.closeMenu);
+
+ // hide other lists
+ $('body').trigger('click');
+
+ window.clearTimeout(this.timer);
+
+ this.element.addClass(CLASSNAME_OPENED);
+
+ $('body').on('click', this.closeMenu);
+ };
+
+ private closeMenu = () => {
+ this.element.removeClass(CLASSNAME_OPENED);
+
+ this.clearEntries();
+
+ $('body').off('click', null, this.closeMenu);
+ };
+}
diff --git a/src/ui/dialogs/messageHistory.ts b/src/ui/dialogs/messageHistory.ts
new file mode 100644
index 00000000..e2652e79
--- /dev/null
+++ b/src/ui/dialogs/messageHistory.ts
@@ -0,0 +1,22 @@
+import Dialog from '../Dialog';
+import { IMessage } from '@src/Message.interface';
+import ChatWindowMessage from '@ui/ChatWindowMessage';
+import ChatWindow from '@ui/ChatWindow';
+
+const messageHistoryTemplate = require('../../../template/messageHistory.hbs');
+
+export default function messageHistory(message: IMessage, chatWindow: ChatWindow) {
+ const content = messageHistoryTemplate({});
+
+ const dialog = new Dialog(content);
+ const dom = dialog.open();
+
+ while (message) {
+ const messageElement = new ChatWindowMessage(message, chatWindow, false);
+
+ dom.find('ul').append('<li>');
+ dom.find('ul > li:last-child').append(messageElement.getElement());
+
+ message = message.getReplacedBy();
+ }
+}
diff --git a/src/ui/util/LongPress.ts b/src/ui/util/LongPress.ts
new file mode 100644
index 00000000..166aa18d
--- /dev/null
+++ b/src/ui/util/LongPress.ts
@@ -0,0 +1,32 @@
+const onLongPress = (element: JQuery<HTMLElement>, handler: () => void): void => {
+ const duration = 1000;
+
+ let durationTimeout: number;
+ let isLongPress: boolean;
+
+ element.on('mousedown', ev => {
+ isLongPress = false;
+
+ durationTimeout = window.setTimeout(() => {
+ ev.stopPropagation();
+ ev.preventDefault();
+
+ isLongPress = true;
+
+ handler();
+ }, duration);
+ });
+
+ element.on('mouseup', () => {
+ window.clearTimeout(durationTimeout);
+ });
+
+ element.on('click', ev => {
+ if (isLongPress) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ });
+};
+
+export default onLongPress;
diff --git a/src/util/HookRepository.ts b/src/util/HookRepository.ts
index e9415970..965a79d8 100644
--- a/src/util/HookRepository.ts
+++ b/src/util/HookRepository.ts
@@ -1,7 +1,7 @@
-export default class HookRepository<T extends Function> {
+export default class HookRepository<Args extends any[] = any[]> {
private hooks = {};
- public registerHook(eventName: string, func: T) {
+ public registerHook(eventName: string, func: (...args: Args) => void) {
if (!this.hooks[eventName]) {
this.hooks[eventName] = [];
}
@@ -9,7 +9,7 @@ export default class HookRepository<T extends Function> {
this.hooks[eventName].push(func);
}
- public removeHook(eventName: string, func: T) {
+ public removeHook(eventName: string, func: (...args: Args) => void) {
let eventNameList = this.hooks[eventName] || [];
if (eventNameList.indexOf(func) > -1) {
@@ -21,7 +21,7 @@ export default class HookRepository<T extends Function> {
this.hooks[eventName] = eventNameList;
}
- public trigger(targetEventName: string, ...args) {
+ public trigger(targetEventName: string, ...args: Args) {
let hooks = this.hooks;
let eventNames = Object.keys(hooks);
diff --git a/src/util/Pipe.ts b/src/util/Pipe.ts
index 359bba6a..95dad395 100644
--- a/src/util/Pipe.ts
+++ b/src/util/Pipe.ts
@@ -1,14 +1,14 @@
+import Log from './Log';
+
const MAX_PRIORITY = 100;
const MIN_PRIORITY = 0;
-type Params = any[];
-
-export default class Pipe<params extends Params = any[]> {
- private pipe = [];
+export default class Pipe<Params extends any[] = any[]> {
+ private pipe: ((...args: Params) => Promise<Params> | Params)[][] = [];
constructor() {}
- public addProcessor(processor: (...args: params) => Promise<params> | params, priority: number = 50) {
+ public addProcessor(processor: (...args: Params) => Promise<Params> | Params, priority: number = 50) {
if (isNaN(priority) || priority < MIN_PRIORITY || priority > MAX_PRIORITY) {
throw new Error('Priority has to be between 0 and 100');
}
@@ -20,7 +20,7 @@ export default class Pipe<params extends Params = any[]> {
this.pipe[priority].push(processor);
}
- public run(...args: params): Promise<params> {
+ public run(...args: Params): Promise<Params> {
let chain = Promise.resolve(args);
this.pipe.forEach(processors => {
@@ -34,9 +34,17 @@ export default class Pipe<params extends Params = any[]> {
}
processors.forEach(processor => {
- chain = chain.then((args2: any[]) => {
- return processor.apply(this, args2);
- });
+ chain = chain
+ .then((args2: Params) => {
+ return processor.apply(this, args2);
+ })
+ .then(args3 => {
+ if (args.length !== args3.length) {
+ Log.warn('Bad processor detected', processor);
+ }
+
+ return args3;
+ });
});
});
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>
diff --git a/template/menu.hbs b/template/menu.hbs
new file mode 100644
index 00000000..b1335628
--- /dev/null
+++ b/template/menu.hbs
@@ -0,0 +1,7 @@
+<div class="jsxc-menu {{classes}}">
+ <a class="jsxc-menu__button jsxc-icon--clickable">{{label-text}}</a>
+ <div class="jsxc-menu__content">
+ <ul>
+ </ul>
+ </div>
+</div>
diff --git a/template/messageHistory.hbs b/template/messageHistory.hbs
new file mode 100644
index 00000000..dd465ebe
--- /dev/null
+++ b/template/messageHistory.hbs
@@ -0,0 +1,7 @@
+<h3>{{t "Message_history"}}</h3>
+
+<ul class="jsxc-message-history"></ul>
+
+<div class="col-sm-offset-4 col-sm-8">
+ <button class="jsxc-button jsxc-button--default jsxc-js-close" type="button">{{t "Close"}}</button>
+</div>