import QuickReactions from "./QuickReactions.vue"; import * as linkify from 'linkifyjs'; import linkifyHtml from 'linkify-html'; import utils from "../../plugins/utils"; import Hammer from "hammerjs"; linkify.options.defaults.className = "link"; linkify.options.defaults.target = { url: "_blank" }; export default { components: { QuickReactions, }, props: { room: { type: Object, default: function () { return null; }, }, originalEvent: { type: Object, default: function () { return {}; }, }, nextEvent: { type: Object, default: function () { return null; }, }, timelineSet: { type: Object, default: function () { return null; }, }, componentFn: { type: Function, default: function () { return () => {}; }, }, }, data() { return { event: {}, thread: null, utils, mc: null, mcCustom: null }; }, beforeUnmount() { this.thread = null; }, watch: { originalEvent: { immediate: true, handler(originalEvent, ignoredOldValue) { this.event = originalEvent; // Check not null and not {} if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) { this.originalEvent.getDecryptionPromise().then(() => { this.event = originalEvent; }); } }, }, thread: { handler(newValue, oldValue) { if (oldValue) { oldValue.off("Relations.add", this.onAddRelation); } if (newValue) { newValue.on("Relations.add", this.onAddRelation); this.processThread(); } }, immediate: true, }, }, computed: { /** * * @returns true if event is non-null and contains data */ validEvent() { return this.event && Object.keys(this.event).length !== 0; }, /** * If this is a thread event, we return the root here, so all reactions will land on the root event. */ eventForReactions() { if (this.event.parentThread) { return this.event.parentThread; } return this.event; }, incoming() { return this.event && this.event.getSender() != this.$matrix.currentUserId; }, /** * Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages) */ showSenderAndTime() { if (!this.isPinned && this.nextEvent && this.nextEvent.getSender() == this.event.getSender()) { const ts1 = this.nextEvent.event.origin_server_ts; const ts2 = this.event.event.origin_server_ts; return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes } return true; }, inReplyToSender() { const originalEvent = this.validEvent && this.event.replyEvent; if (originalEvent) { const sender = this.eventSenderDisplayName(originalEvent); if (originalEvent.isThreadRoot || originalEvent.isMxThread) { return sender || this.$t("message.someone"); } else { return this.$t("message.user_said", { user: sender || this.$t("message.someone") }); } } return null; }, inReplyToEvent() { return this.validEvent && this.event.replyEvent; }, inReplyToText() { const relatesTo = this.event.getWireContent()["m.relates_to"]; if (relatesTo && relatesTo["m.in_reply_to"]) { if (this.inReplyToEvent && (this.inReplyToEvent.isThreadRoot || this.inReplyToEvent.isMxThread)) { const children = this.timelineSet.relations .getAllChildEventsForEvent(this.inReplyToEvent.getId()) .filter((e) => utils.downloadableTypes().includes(e.getContent().msgtype)); return this.$t("message.sent_media", { count: children.length }); } const content = this.event.getContent(); if ("body" in content) { const lines = content.body.split("\n").reverse() || []; while (lines.length && !lines[0].startsWith("> ")) lines.shift(); // Reply fallback has a blank line after it, so remove it to prevent leading newline if (lines[0] === "") lines.shift(); const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, ""); if (text) { return text; } } if (this.inReplyToEvent) { var c = this.inReplyToEvent.getContent(); return c.body; } // We don't have the original text (at the moment at least) return this.$t("fallbacks.original_text"); } return null; }, messageText() { const relatesTo = this.event.getWireContent()["m.relates_to"]; if (relatesTo && relatesTo["m.in_reply_to"]) { const content = this.event.getContent(); if ("body" in content) { // Remove the new text and strip "> " from the old original text const lines = content.body.split("\n"); while (lines.length && lines[0].startsWith("> ")) lines.shift(); // Reply fallback has a blank line after it, so remove it to prevent leading newline if (lines[0] === "") lines.shift(); return lines.join("\n"); } } return this.event.getContent().body; }, isPinned() { return this.event.parentThread ? this.event.parentThread.isPinned : this.event.isPinned; }, /** * Classes to set for the message. Currently only for "messageIn" */ messageClasses() { if (this.incoming) { return { messageIn: true, "from-admin": this.senderIsAdminOrModerator(this.event), "pinned": this.isPinned }; } else { return { messageOut: true, "pinned": this.isPinned }; } }, userAvatar() { if (!this.$matrix.userAvatar) { return null; } return this.$matrix.matrixClient.mxcUrlToHttp(this.$matrix.userAvatar, 80, 80, "scale", true, undefined, this.$matrix.useAuthedMedia); }, userAvatarLetter() { if (!this.$matrix.currentUser) { return null; } return (this.$matrix.currentUserDisplayName || this.$matrix.currentUserId.substring(1)) .substring(0, 1) .toUpperCase(); }, }, methods: { onAddRelation() { console.error("onAddRelation"); this.processThread(); }, ownAvatarClicked() { this.$emit("own-avatar-clicked", { event: this.event }); }, otherAvatarClicked(avatarRef) { this.$emit("other-avatar-clicked", { event: this.event, anchor: avatarRef }); }, showContextMenu(buttonRef) { this.$emit("context-menu", { event: this.event, anchor: buttonRef }); }, /** * Get a display name given an event. */ eventSenderDisplayName(event) { if (event.getSender() == this.$matrix.currentUserId) { return this.$t("message.you"); } if (this.room) { const member = this.room.getMember(event.getSender()); if (member) { return member.name; } } return event.getContent().displayname || event.getSender(); }, /** * In the case where the state_key points out a userId for an operation (e.g. membership events) * return the display name of the affected user. * @param event * @returns */ eventStateKeyDisplayName(event) { if (event.getStateKey() == this.$matrix.currentUserId) { return this.$t("message.you"); } if (this.room) { const member = this.room.getMember(event.getStateKey()); if (member) { return member.name; } } return event.getStateKey(); }, messageEventAvatar(event) { if (this.room) { const member = this.room.getMember(event.getSender()); if (member) { return member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true, false, this.$matrix.useAuthedMedia); } } return null; }, /** * Return true if the event sender has a powel level > 0, e.g. is moderator or admin of some sort. */ senderIsAdminOrModerator(event) { if (this.room) { const member = this.room.getMember(event.getSender()); if (member) { return member.powerLevel > 0; } } return false; }, redactedBySomeoneElse(event) { if (!event.isRedacted()) return false; const redactionEvent = event.getUnsigned().redacted_because; if (redactionEvent) { return redactionEvent.sender !== this.$matrix.currentUserId; } return false; }, formatTimeAgo(time) { const date = new Date(); date.setTime(time); var ti = Math.abs(new Date().getTime() - date.getTime()); ti = ti / 1000; // Convert to seconds let s = ""; if (ti < 60) { s = this.$t("global.time.recently"); } else if (ti < 3600 && Math.round(ti / 60) < 60) { s = this.$t("global.time.minutes", Math.round(ti / 60)); } else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) { s = this.$t("global.time.hours", Math.round(ti / 60 / 60)); } else { s = this.$t("global.time.days", Math.round(ti / 60 / 60 / 24)); } return utils.toLocalNumbers(s); }, linkify(text) { return linkifyHtml(text); }, /** * Override this to handle updates to (the) message thread. */ processThread() {}, initMsgHammerJs(element) { this.mc = new Hammer(element); this.mcCustom = new Hammer.Manager(element); this.mcCustom.add(new Hammer.Tap({ event: 'doubletap', taps: 2 })); this.mcCustom.on("doubletap", (evt) => { var { top, left } = evt.target.getBoundingClientRect(); var position = { top: `${top}px`, left: `${left}px`}; this.$emit("addQuickHeartReaction", { position }); }); this.mc.on("press", () => { this.showContextMenu(this.$refs.opbutton); }); } }, };