342 lines
9.9 KiB
JavaScript
342 lines
9.9 KiB
JavaScript
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);
|
|
});
|
|
|
|
}
|
|
},
|
|
};
|