Compare commits
No commits in common. "main" and "main" have entirely different histories.
@ -2,27 +2,29 @@ import { app } from "../app.js";
|
|||||||
|
|
||||||
class ChatInputComponent extends HTMLElement {
|
class ChatInputComponent extends HTMLElement {
|
||||||
autoCompletions = {
|
autoCompletions = {
|
||||||
"example 1": () => {
|
"example 1": () => {},
|
||||||
},
|
"example 2": () => {},
|
||||||
"example 2": () => {
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
hiddenCompletions = {
|
hiddenCompletions = {
|
||||||
"/starsRender": () => {
|
"/starsRender": () => {
|
||||||
app.rpc.starsRender(this.channelUid, this.value.replace("/starsRender ", ""))
|
app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ",""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
users = []
|
users = []
|
||||||
textarea = null
|
textarea = null
|
||||||
_value = ""
|
_value = ""
|
||||||
lastUpdateEvent = null
|
lastUpdateEvent = null
|
||||||
expiryTimer = null;
|
previousValue = ""
|
||||||
|
lastChange = null
|
||||||
|
changed = false
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.lastUpdateEvent = new Date();
|
this.lastUpdateEvent = new Date();
|
||||||
this.textarea = document.createElement("textarea");
|
this.textarea = document.createElement("textarea");
|
||||||
this.value = this.getAttribute("value") || "";
|
this.value = this.getAttribute("value") || "";
|
||||||
|
this.previousValue = this.value;
|
||||||
|
this.lastChange = new Date();
|
||||||
|
this.changed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
@ -30,27 +32,24 @@ class ChatInputComponent extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set value(value) {
|
set value(value) {
|
||||||
this._value = value;
|
this._value = value || "";
|
||||||
this.textarea.value = this._value;
|
this.textarea.value = this._value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get allAutoCompletions() {
|
get allAutoCompletions() {
|
||||||
return Object.assign({}, this.autoCompletions, this.hiddenCompletions)
|
return Object.assign({},this.autoCompletions,this.hiddenCompletions)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveAutoComplete() {
|
resolveAutoComplete() {
|
||||||
|
let count = 0;
|
||||||
let value = null;
|
let value = null;
|
||||||
|
|
||||||
for (const key of Object.keys(this.allAutoCompletions)) {
|
Object.keys(this.allAutoCompletions).forEach((key) => {
|
||||||
if (key.startsWith(this.value.split(" ", 1)[0])) {
|
if (key.startsWith(this.value.split(" ")[0])) {
|
||||||
if (value) {
|
count++;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
value = key;
|
value = key;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
if (count == 1) return value;
|
||||||
return value;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive() {
|
isActive() {
|
||||||
@ -59,103 +58,117 @@ class ChatInputComponent extends HTMLElement {
|
|||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.textarea.focus();
|
this.textarea.focus();
|
||||||
|
}
|
||||||
|
getAuthors(){
|
||||||
|
let authors = []
|
||||||
|
for (let i = 0; i < this.users.length; i++) {
|
||||||
|
authors.push(this.users[i].username)
|
||||||
|
authors.push(this.users[i].nick)
|
||||||
|
}
|
||||||
|
return authors
|
||||||
|
|
||||||
|
}
|
||||||
|
extractMentions(text) {
|
||||||
|
const regex = /@([a-zA-Z0-9_-]+)/g;
|
||||||
|
const mentions = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
mentions.push(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthors() {
|
return mentions;
|
||||||
return this.users.flatMap((user) => [user.username, user.nick])
|
}
|
||||||
}
|
matchMentionsToAuthors(mentions, authors) {
|
||||||
|
return mentions.map(mention => {
|
||||||
|
let closestAuthor = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
const lowerMention = mention.toLowerCase();
|
||||||
|
|
||||||
|
authors.forEach(author => {
|
||||||
|
const lowerAuthor = author.toLowerCase();
|
||||||
|
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
|
||||||
|
|
||||||
|
|
||||||
extractMentions(text) {
|
if(!this.isSubsequence(lowerMention,lowerAuthor)) {
|
||||||
return Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g), m => m[1]);
|
distance += 10
|
||||||
}
|
|
||||||
|
|
||||||
matchMentionsToAuthors(mentions, authors) {
|
|
||||||
return mentions.map(mention => {
|
|
||||||
let closestAuthor = null;
|
|
||||||
let minDistance = Infinity;
|
|
||||||
const lowerMention = mention.toLowerCase();
|
|
||||||
|
|
||||||
authors.forEach(author => {
|
|
||||||
const lowerAuthor = author.toLowerCase();
|
|
||||||
let distance = this.levenshteinDistance(lowerMention, lowerAuthor);
|
|
||||||
|
|
||||||
|
|
||||||
if (!this.isSubsequence(lowerMention, lowerAuthor)) {
|
|
||||||
distance += 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distance < minDistance) {
|
if (distance < minDistance) {
|
||||||
minDistance = distance;
|
minDistance = distance;
|
||||||
closestAuthor = author;
|
closestAuthor = author;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return { mention, closestAuthor, distance: minDistance };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return { mention, closestAuthor, distance: minDistance };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
levenshteinDistance(a, b) {
|
||||||
|
const matrix = [];
|
||||||
|
|
||||||
|
// Initialize the first row and column
|
||||||
|
for (let i = 0; i <= b.length; i++) {
|
||||||
|
matrix[i] = [i];
|
||||||
|
}
|
||||||
|
for (let j = 0; j <= a.length; j++) {
|
||||||
|
matrix[0][j] = j;
|
||||||
}
|
}
|
||||||
|
|
||||||
levenshteinDistance(a, b) {
|
// Fill in the matrix
|
||||||
const matrix = [];
|
for (let i = 1; i <= b.length; i++) {
|
||||||
|
for (let j = 1; j <= a.length; j++) {
|
||||||
// Initialize the first row and column
|
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
||||||
for (let i = 0; i <= b.length; i++) {
|
matrix[i][j] = matrix[i - 1][j - 1];
|
||||||
matrix[i] = [i];
|
} else {
|
||||||
}
|
matrix[i][j] = Math.min(
|
||||||
for (let j = 0; j <= a.length; j++) {
|
matrix[i - 1][j] + 1, // Deletion
|
||||||
matrix[0][j] = j;
|
matrix[i][j - 1] + 1, // Insertion
|
||||||
}
|
matrix[i - 1][j - 1] + 1 // Substitution
|
||||||
|
);
|
||||||
// Fill in the matrix
|
|
||||||
for (let i = 1; i <= b.length; i++) {
|
|
||||||
for (let j = 1; j <= a.length; j++) {
|
|
||||||
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
||||||
matrix[i][j] = matrix[i - 1][j - 1];
|
|
||||||
} else {
|
|
||||||
matrix[i][j] = Math.min(
|
|
||||||
matrix[i - 1][j] + 1, // Deletion
|
|
||||||
matrix[i][j - 1] + 1, // Insertion
|
|
||||||
matrix[i - 1][j - 1] + 1 // Substitution
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return matrix[b.length][a.length];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return matrix[b.length][a.length];
|
||||||
|
}
|
||||||
|
|
||||||
replaceMentionsWithAuthors(text) {
|
|
||||||
|
|
||||||
|
replaceMentionsWithAuthors(text) {
|
||||||
const authors = this.getAuthors();
|
const authors = this.getAuthors();
|
||||||
const mentions = this.extractMentions(text);
|
const mentions = this.extractMentions(text);
|
||||||
|
|
||||||
const matches = this.matchMentionsToAuthors(mentions, authors);
|
const matches = this.matchMentionsToAuthors(mentions, authors);
|
||||||
let updatedText = text;
|
let updatedText = text;
|
||||||
matches.forEach(({ mention, closestAuthor }) => {
|
matches.forEach(({ mention, closestAuthor }) => {
|
||||||
const mentionRegex = new RegExp(`@${mention}`, 'g');
|
const mentionRegex = new RegExp(`@${mention}`, 'g');
|
||||||
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
|
updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return updatedText;
|
||||||
|
}
|
||||||
|
|
||||||
return updatedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async connectedCallback() {
|
async connectedCallback() {
|
||||||
this.user = null
|
this.user = null
|
||||||
app.rpc.getUser(null).then((user) => {
|
app.rpc.getUser(null).then((user) => {
|
||||||
this.user = user
|
this.user=user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const me = this;
|
||||||
this.liveType = this.getAttribute("live-type") === "true";
|
this.liveType = this.getAttribute("live-type") === "true";
|
||||||
this.liveTypeInterval =
|
this.liveTypeInterval =
|
||||||
parseInt(this.getAttribute("live-type-interval")) || 6;
|
parseInt(this.getAttribute("live-type-interval")) || 6;
|
||||||
this.channelUid = this.getAttribute("channel");
|
this.channelUid = this.getAttribute("channel");
|
||||||
|
|
||||||
app.rpc.getRecentUsers(this.channelUid).then(users => {
|
app.rpc.getRecentUsers(this.channelUid).then(users=>{
|
||||||
this.users = users
|
this.users = users
|
||||||
})
|
})
|
||||||
this.messageUid = null;
|
this.messageUid = null;
|
||||||
|
|
||||||
this.classList.add("chat-input");
|
this.classList.add("chat-input");
|
||||||
|
|
||||||
@ -177,33 +190,18 @@ class ChatInputComponent extends HTMLElement {
|
|||||||
|
|
||||||
this.textarea.addEventListener("keyup", (e) => {
|
this.textarea.addEventListener("keyup", (e) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
this.value = "";
|
||||||
const message = this.replaceMentionsWithAuthors(this.value);
|
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ", 1)[0]];
|
|
||||||
if (autoCompletionHandler) {
|
|
||||||
autoCompletionHandler();
|
|
||||||
this.value = "";
|
|
||||||
e.target.value = "";
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.finalizeMessage()
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.value = e.target.value;
|
||||||
this.updateFromInput(e.target.value);
|
this.changed = true;
|
||||||
|
this.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.textarea.addEventListener("keydown", (e) => {
|
this.textarea.addEventListener("keydown", (e) => {
|
||||||
this.value = e.target.value;
|
this.value = e.target.value;
|
||||||
let autoCompletion = null;
|
let autoCompletion = null;
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
autoCompletion = this.resolveAutoComplete();
|
autoCompletion = this.resolveAutoComplete();
|
||||||
@ -215,94 +213,160 @@ class ChatInputComponent extends HTMLElement {
|
|||||||
}
|
}
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const message = me.replaceMentionsWithAuthors(this.value);
|
||||||
|
e.target.value = "";
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]];
|
||||||
|
if (autoCompletionHandler) {
|
||||||
|
autoCompletionHandler();
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
e.target.value = "";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateMessage()
|
||||||
|
app.rpc.finalizeMessage(this.messageUid)
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
this.messageUid = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.changeInterval = setInterval(() => {
|
||||||
|
if (!this.liveType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.value !== this.previousValue) {
|
||||||
|
if (
|
||||||
|
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||||
|
this.liveTypeInterval
|
||||||
|
) {
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
}
|
||||||
|
this.lastChange = new Date();
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
}, 300);
|
||||||
|
|
||||||
this.addEventListener("upload", (e) => {
|
this.addEventListener("upload", (e) => {
|
||||||
this.focus();
|
this.focus();
|
||||||
});
|
});
|
||||||
this.addEventListener("uploaded", function (e) {
|
this.addEventListener("uploaded", function (e) {
|
||||||
let message = e.detail.files.reduce((message, file) => {
|
let message = "";
|
||||||
return `${message}[${file.name}](/channel/attachment/${file.relative_url})`;
|
e.detail.files.forEach((file) => {
|
||||||
}, '');
|
message += `[${file.name}](/channel/attachment/${file.relative_url})`;
|
||||||
app.rpc.sendMessage(this.channelUid, message, true);
|
});
|
||||||
|
app.rpc.sendMessage(this.channelUid, message,true);
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(()=>{
|
||||||
this.focus();
|
this.focus();
|
||||||
}, 1000)
|
},1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
trackSecondsBetweenEvents(event1Time, event2Time) {
|
trackSecondsBetweenEvents(event1Time, event2Time) {
|
||||||
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
const millisecondsDifference = event2Time.getTime() - event1Time.getTime();
|
||||||
return millisecondsDifference / 1000;
|
return millisecondsDifference / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubsequence(s, t) {
|
isSubsequence(s, t) {
|
||||||
let i = 0, j = 0;
|
let i = 0, j = 0;
|
||||||
while (i < s.length && j < t.length) {
|
while (i < s.length && j < t.length) {
|
||||||
if (s[i] === t[j]) {
|
if (s[i] === t[j]) {
|
||||||
i++;
|
i++;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
}
|
}
|
||||||
j++;
|
return i === s.length;
|
||||||
}
|
}
|
||||||
return i === s.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
flagTyping() {
|
|
||||||
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) {
|
|
||||||
this.lastUpdateEvent = new Date();
|
|
||||||
app.rpc.set_typing(this.channelUid, this.user.color).catch(() => {
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalizeMessage() {
|
newMessage() {
|
||||||
if (!this.messageUid) {
|
if (!this.messageUid) {
|
||||||
if (this.value.trim() === "") {
|
this.messageUid = "?";
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(this.value), !this.liveType);
|
|
||||||
} else {
|
|
||||||
app.rpc.finalizeMessage(this.messageUid)
|
|
||||||
}
|
|
||||||
this.value = "";
|
|
||||||
this.messageUid = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFromInput(value) {
|
|
||||||
if (this.expiryTimer) {
|
|
||||||
clearTimeout(this.expiryTimer);
|
|
||||||
this.expiryTimer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.value = value;
|
this.value = this.replaceMentionsWithAuthors(this.value);
|
||||||
|
this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => {
|
||||||
this.flagTyping()
|
if (this.liveType) {
|
||||||
|
|
||||||
if (this.liveType && value[0] !== "/") {
|
|
||||||
this.expiryTimer = setTimeout(() => {
|
|
||||||
this.finalizeMessage()
|
|
||||||
}, this.liveTypeInterval * 1000);
|
|
||||||
|
|
||||||
if (this.messageUid === "?") {
|
|
||||||
} else if (this.messageUid) {
|
|
||||||
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
|
|
||||||
} else {
|
|
||||||
this.messageUid = "?"; // Indicate that a message is being sent
|
|
||||||
this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(value), !this.liveType).then((uid) => {
|
|
||||||
if (this.liveType) {
|
|
||||||
this.messageUid = uid;
|
this.messageUid = uid;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMessage() {
|
||||||
|
if (this.value[0] == "/") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.messageUid) {
|
||||||
|
this.newMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.messageUid === "?") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof app !== "undefined" &&
|
||||||
|
app.rpc &&
|
||||||
|
typeof app.rpc.updateMessageText === "function"
|
||||||
|
) {
|
||||||
|
app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus() {
|
||||||
|
if (this.liveType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) {
|
||||||
|
this.lastUpdateEvent = new Date();
|
||||||
|
if (
|
||||||
|
typeof app !== "undefined" &&
|
||||||
|
app.rpc &&
|
||||||
|
typeof app.rpc.set_typing === "function"
|
||||||
|
) {
|
||||||
|
app.rpc.set_typing(this.channelUid, this.user.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(channelUid, value, is_final) {
|
update() {
|
||||||
|
const expired =
|
||||||
|
this.trackSecondsBetweenEvents(this.lastChange, new Date()) >=
|
||||||
|
this.liveTypeInterval;
|
||||||
|
const changed = this.value !== this.previousValue;
|
||||||
|
|
||||||
|
if (changed || expired) {
|
||||||
|
this.lastChange = new Date();
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousValue = this.value;
|
||||||
|
|
||||||
|
if (this.liveType && expired) {
|
||||||
|
this.value = "";
|
||||||
|
this.previousValue = "";
|
||||||
|
this.messageUid = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
if (this.liveType) {
|
||||||
|
this.updateMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(channelUid, value,is_final) {
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await app.rpc.sendMessage(channelUid, value, is_final);
|
return await app.rpc.sendMessage(channelUid, value,is_final);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user