Skip to content

Commit a71c456

Browse files
fix: improvements and bug fixes in hover metadata feature (#677)
* fix: word breaking issue * fix: logic working on non-eng text and multi phrase words * chore: removing extra code * revert: to old working code * chore: sort target words * chore: wip hover reach element code * feat: basic tooltip rendering using react componnet * style: hover ui change * fix: google UI break * chore: search working on multi-phrase words * fix: delete and update metadata * revert: old logic to locate slurs * fix: fetching metadata from json object * chore: hover UI component changes * style: hover ui element * style: font change in hover ui * chore: add min-width to hover box, add vertical gap in categories * style: font change in hover ui * chore: manifest version update --------- Co-authored-by: maanasb01 <maanasb01@gmail.com>
1 parent 5a5c40f commit a71c456

File tree

5 files changed

+209
-181
lines changed

5 files changed

+209
-181
lines changed

browser-extension/plugin/manifest.firefox.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 2,
33
"name": "uli",
44
"description": "Moderate your Twitter Feed",
5-
"version": "0.2.1",
5+
"version": "0.2.2",
66
"author": "tattlemade|cis",
77
"content_security_policy": "default-src 'none'; connect-src https://uli-community.tattle.co.in/ https://uli-media.tattle.co.in/; font-src https://fonts.gstatic.com; object-src 'none'; script-src 'self' ; style-src https://fonts.googleapis.com 'self' 'unsafe-inline'; img-src https://uli-media.tattle.co.in/; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; report-uri 'none';",
88
"permissions": [

browser-extension/plugin/manifest.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "uli",
44
"description": "Moderate your Twitter Feed",
5-
"version": "0.2.1",
5+
"version": "0.2.2",
66
"author": "tattlemade|cis",
77
"content_security_policy": {
88
"extension_pages": "default-src 'none'; connect-src https://uli-community.tattle.co.in/ https://uli-media.tattle.co.in/; font-src https://fonts.gstatic.com; object-src 'none'; script-src 'self'; style-src https://fonts.googleapis.com 'self' 'unsafe-inline'; img-src https://uli-media.tattle.co.in/; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; report-uri 'none';"
@@ -23,4 +23,4 @@
2323
"default_popup": "options.html"
2424
},
2525
"icons": { "16": "icon16.png", "48": "icon32.png" }
26-
}
26+
}

browser-extension/plugin/src/slur-store.js

+42-9
Original file line numberDiff line numberDiff line change
@@ -172,27 +172,60 @@ export async function convertSlurMetadataFromDBtoJSON() {
172172
}
173173
}
174174

175+
export async function deleteSlurMetadataEntries(entriesToDelete) {
176+
if (!Array.isArray(entriesToDelete) || entriesToDelete.length === 0) {
177+
console.warn("No valid entries provided for deletion.");
178+
return;
179+
}
180+
181+
try {
182+
for (const entry of entriesToDelete) {
183+
await db.words_metadata
184+
.filter(dbEntry =>
185+
dbEntry.label === entry.label &&
186+
dbEntry.level_of_severity === entry.level_of_severity &&
187+
dbEntry.meaning === entry.meaning &&
188+
JSON.stringify(dbEntry.categories) === JSON.stringify(entry.categories) &&
189+
dbEntry.language === entry.language &&
190+
dbEntry.timestamp === entry.timestamp
191+
)
192+
.delete();
193+
}
194+
console.log(`${entriesToDelete.length} metadata entries deleted.`);
195+
} catch (error) {
196+
console.error('Error deleting slur metadata:', error);
197+
}
198+
}
199+
175200
export async function fetchPublicSlursMetadata() {
176201
try {
177202
// Fetch slurs from the backend
178203
const publicSlursMetadata = await getPublicSlursMetadata();
179204
// Fetch existing metadata from the indexed database
180205
const existingMetadata = await getAllSlurMetadata();
181-
// Convert existing metadata objects to JSON strings for comparison
206+
// Convert existing metadata to a Set of JSON strings for exact comparison
207+
const publicMetadataSet = new Set(publicSlursMetadata.map(meta => JSON.stringify(meta)));
182208
const existingMetadataSet = new Set(existingMetadata.map(meta => JSON.stringify(meta)));
183-
// Filter out metadata entries that already exist
209+
// Identify metadata that needs to be added (exists in fetched data but not in DB)
184210
const newMetadata = publicSlursMetadata.filter(meta =>
185211
!existingMetadataSet.has(JSON.stringify(meta))
186212
);
187-
if (newMetadata.length === 0) {
188-
console.log("All public slurs metadata already exist in the database. Skipping addition.");
189-
return;
213+
// Identify metadata that needs to be removed (exists in DB but not in fetched data)
214+
const outdatedMetadata = existingMetadata.filter(meta =>
215+
!publicMetadataSet.has(JSON.stringify(meta))
216+
);
217+
// Add new metadata
218+
if (newMetadata.length > 0) {
219+
await bulkAddSlurMetadata(newMetadata);
220+
console.log(`${newMetadata.length} new slur metadata entries added.`);
221+
}
222+
// Delete outdated metadata
223+
if (outdatedMetadata.length > 0) {
224+
await deleteSlurMetadataEntries(outdatedMetadata);
225+
console.log(`${outdatedMetadata.length} outdated slur metadata entries removed.`);
190226
}
191-
192-
await bulkAddSlurMetadata(newMetadata);
193-
console.log(`${newMetadata.length} new slur metadata entries added.`);
194227
} catch (error) {
195-
console.error('Error fetching and adding public slurs metadata:', error);
228+
console.error('Error fetching and updating public slurs metadata:', error);
196229
}
197230
}
198231

browser-extension/plugin/src/transform-general.js

+65-169
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { replaceSlur } from './slur-replace';
22
import { log } from './logger';
33
import repository from './repository';
44
const { getPreferenceData } = repository;
5+
import { createRoot } from 'react-dom/client';
6+
import HoverSlurMetadata from './ui-components/atoms/HoverSlurMetadata';
57

68
// Traverse dom nodes to find leaf node that are text nodes and process
79
function bft(node) {
@@ -49,19 +51,6 @@ function setCaretPosition(element, offset) {
4951
sel.addRange(range);
5052
}
5153

52-
const processNewlyAddedNodesGeneral2 = function (firstBody, jsonData) {
53-
let targetWords = [];
54-
jsonData.forEach((slur) => {
55-
const slurWord = Object.keys(slur)[0];
56-
targetWords.push(slurWord);
57-
// targetWords.push(slurWord.charAt(0).toUpperCase() + slurWord.slice(1));
58-
});
59-
let uliStore = [];
60-
getAllTextNodes(firstBody, uliStore);
61-
abc = locateSlur(uliStore, targetWords);
62-
addMetaData(targetWords, jsonData);
63-
};
64-
6554
const processNewlyAddedNodesGeneral = function (firstBody) {
6655
log('processing new nodes');
6756
const config = { attributes: true, childList: true, subtree: true };
@@ -85,6 +74,16 @@ const processNewlyAddedNodesGeneral = function (firstBody) {
8574
observer.observe(firstBody, config);
8675
};
8776

77+
const processNewlyAddedNodesGeneral2 = function (firstBody, jsonData) {
78+
let targetWords = jsonData.map(slur => Object.keys(slur)[0]);
79+
targetWords.sort((a, b) => b.length - a.length);
80+
81+
let uliStore = [];
82+
getAllTextNodes(firstBody, uliStore);
83+
locateSlur(uliStore, targetWords);
84+
addMetaData(targetWords, jsonData);
85+
};
86+
8887
function checkFalseTextNode(text, actualLengthOfText) {
8988
let totalNewlineAndWhitespaces = 0;
9089
for (let i = 0; i < text.length; i++) {
@@ -95,7 +94,6 @@ function checkFalseTextNode(text, actualLengthOfText) {
9594
return totalNewlineAndWhitespaces === actualLengthOfText;
9695
}
9796

98-
// Function to recursively get all text nodes for a given node
9997
function getAllTextNodes(node, uliStore) {
10098
if (node.nodeType === 3) {
10199
if (!checkFalseTextNode(node.data, node.length)) {
@@ -125,188 +123,86 @@ function findPositions(word, text) {
125123
}
126124

127125
function locateSlur(uliStore, targetWords) {
128-
let n = uliStore.length;
129-
130-
for (let i = 0; i < n; i++) {
131-
let store = uliStore[i];
132-
let parentNode = store.parent;
126+
uliStore.forEach(store => {
133127
let textnode = store.node;
134-
let text = store.node.textContent;
135128
let tempParent = document.createElement('span');
136-
tempParent.textContent = text;
137-
let slurs = [];
129+
tempParent.textContent = textnode.textContent;
138130
let slurPresentInTempParent = false;
139-
targetWords.forEach((targetWord) => {
140-
let slurWord = targetWord;
141-
let pos = findPositions(slurWord, text);
142-
if (Object.keys(pos).length !== 0) {
143-
slurs.push(pos);
144-
}
145-
146-
if (tempParent.innerHTML.includes(targetWord)) {
147-
const className = `icon-container-${targetWord}`;
148-
const slurClass = `slur-container-${targetWord}`;
149-
150-
// if (!tempParent.innerHTML.includes(`class="${slurClass}"`)) {
151-
const parts = tempParent.innerHTML.split(targetWord);
152-
const replacedHTML = parts.join(
153-
`<span class="${slurClass}"><span class="slur">${targetWord}</span></span>`
154-
);
155-
tempParent.innerHTML = replacedHTML;
131+
targetWords.forEach(targetWord => {
132+
const sanitizedTargetWord = targetWord.replace(/\s+/g, '-');
133+
const slurClass = `slur-container-${sanitizedTargetWord}`;
134+
135+
const escapedTargetWord = targetWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
136+
// regex for multi-word and single-word phrases
137+
const regex = new RegExp(`(^|\\s|[.,!?])(${escapedTargetWord})(?=\\s|$|[.,!?])`, 'giu');
138+
139+
if (regex.test(tempParent.textContent)) {
140+
tempParent.innerHTML = tempParent.innerHTML.replace(regex, (match, prefix, word) => {
141+
return `${prefix}<span class="${slurClass}"><span class="slur">${word}</span></span>`;
142+
});
156143
slurPresentInTempParent = true;
157-
// }
158144
}
159145
});
160-
uliStore[i].nodeToParent = tempParent;
161-
uliStore[i].slurs = slurs;
162146

163-
//O(1) complexity
164147
if (slurPresentInTempParent) {
165148
textnode.replaceWith(tempParent);
166149
}
167-
168-
// console.log("TEMPParent: ",tempParent)
169-
}
170-
return uliStore;
150+
});
171151
}
172152

173153
function addMetaData(targetWords, jsonData) {
174154
targetWords.forEach((targetWord) => {
175-
const className = `slur-container-${targetWord}`;
155+
const sanitizedTargetWord = targetWord.replace(/\s+/g, '-');
156+
const className = `slur-container-${sanitizedTargetWord}`;
176157
const elements = Array.from(document.querySelectorAll(`.${className}`));
177-
// console.log("ELEMENTS are: ",elements)
178-
elements.forEach((element) => {
179-
// console.log("ELements InnerHTML:",element.innerHTML)
180158

181-
// element.innerHTML = element.innerHTML.replace(/<img[^>]*>/g, '')
182-
183-
let sup = document.createElement('span');
184-
let img = document.createElement('img');
185-
img.style.height = '0.6em';
186-
img.style.width = '0.6em';
187-
img.style.border = '1px solid black';
188-
img.style.cursor = 'pointer';
189-
img.style.marginBottom = '0.4em';
190-
191-
img.src =
192-
'https://raw.githubusercontent.com/tattle-made/Uli/main/uli-website/src/images/favicon-32x32.png';
193-
img.alt = 'altText';
194-
195-
let span = document.createElement('span');
196-
span.style.display = 'none';
197-
span.style.position = 'absolute';
198-
span.style.marginLeft = '2px';
199-
span.style.marginTop = '2px';
200-
span.style.backgroundColor = 'antiquewhite';
201-
span.style.border = '1px solid black';
202-
span.style.borderRadius = '12px';
203-
span.style.padding = '2px 6px';
204-
span.style.width = '16rem';
205-
span.style.textAlign = 'justify';
206-
span.style.fontWeight = 'lighter';
207-
span.style.color = 'black';
208-
span.style.zIndex = '1000000000'; // This ensures it appears above other elements
209-
span.style.fontSize = '14px';
210-
span.style.textDecoration = 'none';
211-
span.style.fontStyle = 'normal';
212-
213-
span.innerHTML = `${targetWord} is an offensive word`;
214-
215-
jsonData.forEach((slur) => {
216-
const slurWord = Object.keys(slur)[0];
217-
if (slurWord.toLowerCase() === targetWord.toLowerCase()) {
218-
const slurDetails = slur[slurWord];
219-
let levelOfSeverity = slurDetails['Level of Severity'];
220-
let casual = slurDetails['Casual'];
221-
let approapriated = slurDetails['Appropriated'];
222-
let reason =
223-
slurDetails[
224-
'If, Appropriated, Is it by Community or Others?'
225-
];
226-
let problematic = slurDetails['What Makes it Problematic?'];
227-
let categories = slurDetails['Categories'];
228-
let htmlContent = ``;
229-
if (levelOfSeverity) {
230-
htmlContent += `<p><span class="label"><b>Level of Severity:</b></span> ${levelOfSeverity}</p>`;
231-
}
232-
if (casual) {
233-
htmlContent += `<p><span class="label"><b>Casual:</b></span> ${casual}</p>`;
234-
}
235-
if (approapriated) {
236-
htmlContent += `<p><span class="label"><b>Appropriated:</b></span> ${approapriated}</p>`;
237-
}
238-
if (reason) {
239-
htmlContent += `<p><span class="label"><b>If, Appropriated, Is it by Community or Others?:</b></span> ${reason}</p>`;
240-
}
241-
if (problematic) {
242-
htmlContent += `<p><span class="label"><b>What Makes it Problematic?:</b></span> ${problematic}</p>`;
243-
}
244-
if (categories.length > 0) {
245-
htmlContent += `<p><span class="label"><b>Categories:</b></span> ${slurDetails[
246-
'Categories'
247-
].join(', ')}</p>`;
248-
}
249-
span.innerHTML = htmlContent;
250-
}
251-
});
252-
253-
254-
255-
// sup.appendChild(span)
256-
257-
// element.append(sup)
258-
// element.append(img)
259-
// let sups = element.children[0]
260-
// let spans = element.children[0].children[1]
261-
// // const svgs = element.children[0].children[0];
262-
// const svgs = element.children[element.children.length-1];
263-
// svgs.addEventListener('mouseover', function () {
264-
// sups.children[0].style.display = "inline-block"
265-
// });
159+
elements.forEach((element) => {
160+
const slur = element.querySelector('.slur');
161+
if (!slur) return;
266162

267-
// svgs.addEventListener('mouseout', function () {
268-
// sups.children[0].style.display = "none"
269-
// });
163+
// Add hover styles
164+
slur.style.backgroundColor = '#ffde2155';
165+
slur.style.boxShadow = '0px 0px 5px #ffde21';
166+
slur.style.cursor = 'pointer';
270167

271-
sup.appendChild(span);
168+
// Create a single tooltip container and React root if not already created
169+
let tooltipContainer = document.getElementById('slur-tooltip-container');
170+
if (!tooltipContainer) {
171+
tooltipContainer = document.createElement('div');
172+
tooltipContainer.id = 'slur-tooltip-container';
173+
tooltipContainer.style.position = 'absolute';
174+
tooltipContainer.style.zIndex = '1000000000';
175+
document.body.appendChild(tooltipContainer);
176+
tooltipContainer.root = createRoot(tooltipContainer);
177+
}
272178

273-
// console.log("Element first child",element.children[0])
274-
// console.log("Element last child",element.children[element.children.length-1])
275-
// console.log("SUP: ",sup)
276-
// console.log("ELEMENT IS: ",element)
277-
// console.log("ELEMENT INNERHTML: ",element.innerHTML)
179+
// Find the slur details from jsonData
180+
const matchedSlur = jsonData.find(slur =>
181+
Object.keys(slur)[0].toLowerCase() === targetWord.toLowerCase()
182+
);
183+
const slurDetails = matchedSlur ? matchedSlur[Object.keys(matchedSlur)[0]] : {};
278184

279-
element.append(span);
185+
const handleMouseOver = () => {
186+
const rect = slur.getBoundingClientRect();
187+
tooltipContainer.style.top = `${rect.bottom + window.scrollY}px`;
188+
tooltipContainer.style.left = `${rect.left + window.scrollX}px`;
280189

281-
// console.log("ELEMENT AFTER IS: ",element)
282-
// element.append(img)
283-
let slur = element.children[0];
284-
slur.style.backgroundColor = '#ffde2155';
285-
slur.style.boxShadow = '0px 0px 5px #ffde21';
286-
slur.style.cursor = 'pointer';
190+
tooltipContainer.root.render(
191+
<HoverSlurMetadata slurDetails={slurDetails} />
192+
);
193+
};
287194

288-
let metabox = element.children[1];
289-
// console.log("METABOX IS: ",metabox)
290-
let spans = element.children[0].children[1];
291-
// const svgs = element.children[0].children[0];
292-
const svgs = element.children[element.children.length - 1];
293-
slur.addEventListener('mouseover', function () {
294-
metabox.style.display = 'inline-block';
295-
});
195+
const handleMouseOut = () => {
196+
tooltipContainer.root.render(null);
197+
};
296198

297-
slur.addEventListener('mouseout', function () {
298-
metabox.style.display = 'none';
299-
});
199+
slur.addEventListener('mouseover', handleMouseOver);
200+
slur.addEventListener('mouseout', handleMouseOut);
300201
});
301202
});
302203
}
303204

304205
export default {
305206
processNewlyAddedNodesGeneral,
306207
processNewlyAddedNodesGeneral2,
307-
addMetaData,
308-
locateSlur,
309-
findPositions,
310-
getAllTextNodes,
311-
checkFalseTextNode
312-
};
208+
};

0 commit comments

Comments
 (0)