-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
executable file
·202 lines (162 loc) · 6.06 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
/**
* Returns a promise that resolves with the element matching the given selector
* when it becomes available in the DOM.
*
* @param {string} selector - The CSS selector to match the desired element.
* @param {HTMLElement} parentElement - The parent element to search within.
* @returns {Promise<Element>} A promise that resolves with the element when it exists in the DOM.
*/
function getElementWhenExists(selector, parentElement = document) {
return new Promise((resolve) => {
if (parentElement.querySelector(selector)) {
return resolve(parentElement.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (parentElement.querySelector(selector)) {
resolve(parentElement.querySelector(selector));
observer.disconnect();
}
});
observer.observe(
parentElement === document ? document.body : parentElement,
{
subtree: true,
childList: true,
}
);
});
}
/**
* Waits for an element to disappear from the DOM.
*
* @param {string} selector - The CSS selector of the element to wait for.
* @returns {Promise<void>} A promise that resolves when the element has disappeared.
*/
function waitForElementToDisappear(selector) {
return new Promise((resolve) => {
if (!document.querySelector(selector)) {
return resolve();
}
const observer = new MutationObserver(() => {
if (!document.querySelector(selector)) {
resolve();
observer.disconnect();
}
});
observer.observe(document.body, {
subtree: true,
childList: true,
});
});
}
/**
* Parses a string and returns a Date object representing the date and time.
*
* @param {string} string - The string to parse.
* @returns {Date|undefined} - The parsed Date object, or undefined if the string does not match the expected format.
*/
function getDateFromString(string) {
// This regex matches date-time strings in the following formats:
// [10:30 AM, 4/15/2023]
// [2:45 p.m., 12/31/2022]
// [14:00, 1/1/2024]
const dateTimeRegex =
/\[(\d{1,2}:\d{2})(?:\s*(a\.?\s*m\.?|p\.?\s*m\.?))?, (\d{1,2}\/\d{1,2}\/\d{4})\]/i;
const matches = dateTimeRegex.exec(string.replace(/ /g, " "));
if (!matches || matches.length < 3) return;
const timePart = matches[1].trim();
const ampm = matches[2]
? matches[2].replace(/\.\s?/g, "").toLowerCase()
: null;
const userLanguage = chrome.i18n.getUILanguage();
const dateParts = new Intl.DateTimeFormat(userLanguage).formatToParts();
const day = dateParts.find(part => part.type === 'day').value;
const month = dateParts.find(part => part.type === 'month').value;
const year = dateParts.find(part => part.type === 'year').value;
let [hours, minutes] = timePart.split(":").map(Number);
if (ampm === "pm" && hours < 12) {
hours += 12;
} else if (ampm === "am" && hours === 12) {
hours = 0;
}
return new Date(year, month - 1, day, hours, minutes);
}
/**
* Checks if a message bubble is editable based on certain conditions.
*
* @param {HTMLElement} messageBubble - The message bubble element to check.
* @returns {boolean} - Returns true if the message bubble is editable, false otherwise.
*/
const isMessageEditable = (messageBubble) => {
const messageContainer = messageBubble.querySelector(
".copyable-text[data-pre-plain-text]"
);
if (!messageContainer) return false;
const isPoll = Boolean(
messageBubble.querySelector(
`[aria-label*='${chrome.i18n.getMessage("poll")}' i]`
)
);
const wasSent = Boolean(
messageBubble.querySelector("[data-icon='msg-check']")
);
const wasForwarded = Boolean(
messageBubble.querySelector("[data-icon='forwarded']")
);
const wasDelivered = Boolean(
messageBubble.querySelector("[data-icon='msg-dblcheck']")
);
if ((!wasSent && !wasDelivered) || wasForwarded || isPoll) return false;
const messagePrePlainText = messageContainer.dataset.prePlainText;
const messageDateTime = getDateFromString(messagePrePlainText);
const currentDateTime = new Date();
const elapsedTime = currentDateTime - messageDateTime;
const isRecent = elapsedTime <= 15 * 60 * 1000; // Fifteen minutes in milliseconds.
if (!isRecent) {
alert(chrome.i18n.getMessage("editTimeLimitErrorMessage"));
return false;
}
return true;
};
/**
* Handles the key up event. If the ArrowUp key is pressed, the popup to edit last sent message is shown.
* If the last sent message is not editable, an alert is shown.
*
* @param {Event} event - The key up event object.
* @returns {Promise<void>} - A promise that resolves when the function completes.
*/
const handleKeyUp = async (event) => {
if (event.key !== "ArrowUp") return false;
const messageInputField = document.querySelector("#main [role='textbox']");
const isMessageInputFieldEventTarget = messageInputField === event.target;
const isMessageInputFieldFocused =
messageInputField === document.activeElement;
const isMessageInputFieldEmpty = messageInputField.innerText.trim() === "";
if (
!isMessageInputFieldEventTarget ||
!isMessageInputFieldFocused ||
!isMessageInputFieldEmpty
)
return;
const lastSentMessage = [...document.querySelectorAll(".message-out")].at(-1);
const messageBubble = lastSentMessage.querySelector(
`[aria-label='${chrome.i18n.getMessage("you")}:' i]`
)?.parentElement;
if (!isMessageEditable(messageBubble)) return;
messageBubble.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
const contextMenuButton = await getElementWhenExists(
`[aria-label='${chrome.i18n.getMessage("contextMenu")}' i]`,
messageBubble
);
contextMenuButton.click();
const editButton = await getElementWhenExists(
`[aria-label='${chrome.i18n.getMessage("edit")}' i]`
);
editButton.click();
const editTextField = await getElementWhenExists("[role='textbox']");
editTextField.focus();
await waitForElementToDisappear("[data-animate-modal-popup='true']");
lastSentMessage.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
messageInputField.focus();
};
document.addEventListener("keyup", handleKeyUp);