Ô sửa inline trùng khít chữ gốc (đúng font/cỡ/vị trí — WYSIWYG như Foxit)
Build Windows portable / build (push) Canceled after 0s
Build Windows portable / build (push) Canceled after 0s
Ba lỗi làm chữ trong ô sửa khác hẳn chữ gốc: - Hồi quy: line-height từng dòng tính bằng khoảng hở giữa 2 bbox (~0/âm, kẹp 8px) thay vì khoảng cách baseline → các dòng dồn cục lên đầu ô. Fix: advance = hiệu mép dưới 2 bbox liền kề. - Sai font: family PostScript CamelCase (TimesNewRoman, SegoeUI) không khớp tên font Windows → CSS rơi về serif/sans mặc định. Fix: cssFontStack tách CamelCase + bảng alias (Helvetica→Arial...). - Sai cỡ với file engine báo cỡ lệch: thêm fitEditLinesToPdf — vòng ĐO-HIỆU CHỈNH sau khi mount: đo chiều cao mực thật của đúng chuỗi đó (canvas actualBoundingBox ở cỡ tham chiếu 100px, né hinting) so với bbox dòng PDF rồi tự sửa cỡ (dead-band 12% giữ ổn định file chuẩn); đo vị trí render thật (Range + font/ink ascent) rồi bù margin/indent từng dòng cho trùng mép mực — khối căn giữa cũng bù ngang qua text-indent. Kèm: giữ thụt lề từng dòng; đúp từ viewer thường đặt con trỏ đúng điểm đúp (chờ img.decode trước khi đo). Kiểm chứng bằng harness canvas + Edge headless: 3 kịch bản (cỡ báo sai 1/2, cỡ đúng, căn giữa) đều trùng khít ≤1.3px. Chi tiết: docs/12-phase4-summary.md (Vá vòng 3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+140
-22
@@ -2142,7 +2142,15 @@ async function loadEditPage() {
|
|||||||
const o = pickTextAt(state.editObjects, pending.x, pending.y);
|
const o = pickTextAt(state.editObjects, pending.x, pending.y);
|
||||||
if (o) {
|
if (o) {
|
||||||
selectEditObject(o.index);
|
selectEditObject(o.index);
|
||||||
startTextEdit(o);
|
// Chờ ảnh trang decode xong để mọi phép đo theo ảnh (ô sửa, con trỏ) đúng.
|
||||||
|
const img = $("editImg");
|
||||||
|
if (img.decode) { try { await img.decode(); } catch (_) {} }
|
||||||
|
// Toạ độ chuột giả lập từ điểm PDF đã đúp → con trỏ đặt đúng chỗ đó.
|
||||||
|
const ir = img.getBoundingClientRect();
|
||||||
|
startTextEdit(o, {
|
||||||
|
clientX: ir.left + pending.x * state.editScale,
|
||||||
|
clientY: ir.top + (p.heightPt - pending.y) * state.editScale,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2333,15 +2341,39 @@ function editRedo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CSS font-family xấp xỉ cho family PDF (để ô sửa hiển thị đúng dáng chữ).
|
// CSS font-family xấp xỉ cho family PDF (để ô sửa hiển thị đúng dáng chữ).
|
||||||
|
// Tên family từ engine là dạng PostScript CamelCase KHÔNG dấu cách
|
||||||
|
// ("TimesNewRoman", "SegoeUI", "OpenSans") — không khớp tên font cài trên máy
|
||||||
|
// nên CSS rơi về serif/sans-serif mặc định (ô sửa sai font). Map alias phổ biến
|
||||||
|
// + tự tách CamelCase thành tên có dấu cách.
|
||||||
|
const FONT_CSS_ALIASES = {
|
||||||
|
helvetica: "Arial",
|
||||||
|
helveticaneue: "Arial",
|
||||||
|
arial: "Arial",
|
||||||
|
arialmt: "Arial",
|
||||||
|
times: "Times New Roman",
|
||||||
|
timesroman: "Times New Roman",
|
||||||
|
timesnewroman: "Times New Roman",
|
||||||
|
timesnewromanps: "Times New Roman",
|
||||||
|
courier: "Courier New",
|
||||||
|
couriernew: "Courier New",
|
||||||
|
couriernewps: "Courier New",
|
||||||
|
segoeui: "Segoe UI",
|
||||||
|
};
|
||||||
function cssFontStack(family) {
|
function cssFontStack(family) {
|
||||||
if (!family) return "sans-serif";
|
if (!family) return "sans-serif";
|
||||||
const k = family.toLowerCase();
|
const k = family.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||||
const generic = /times|georgia|garamond|palatino|cambria|book|serif/.test(k)
|
const generic = /times|georgia|garamond|palatino|cambria|book|serif/.test(k)
|
||||||
? "serif"
|
? "serif"
|
||||||
: /courier|consolas|mono/.test(k)
|
: /courier|consolas|mono/.test(k)
|
||||||
? "monospace"
|
? "monospace"
|
||||||
: "sans-serif";
|
: "sans-serif";
|
||||||
return `"${family}", ${generic}`;
|
const parts = [`"${family}"`];
|
||||||
|
const spaced = family.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
||||||
|
if (spaced !== family) parts.push(`"${spaced}"`);
|
||||||
|
const alias = FONT_CSS_ALIASES[k];
|
||||||
|
if (alias && alias !== family && alias !== spaced) parts.push(`"${alias}"`);
|
||||||
|
parts.push(generic);
|
||||||
|
return parts.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ghép nội dung 1 dòng từ các run: chèn khoảng trắng khi 2 run hở nhau rõ.
|
// Ghép nội dung 1 dòng từ các run: chèn khoảng trắng khi 2 run hở nhau rõ.
|
||||||
@@ -2502,12 +2534,6 @@ function startBlockTextEdit(o, lines, ev) {
|
|||||||
lines.every((l) => Math.abs((l.rect.left + l.rect.right) / 2 - bc) <= tol) &&
|
lines.every((l) => Math.abs((l.rect.left + l.rect.right) / 2 - bc) <= tol) &&
|
||||||
lines.some((l) => l.rect.left - union.left > tol);
|
lines.some((l) => l.rect.left - union.left > tol);
|
||||||
|
|
||||||
const advPt =
|
|
||||||
lines.length > 1
|
|
||||||
? Math.abs(lines[0].rect.bottom - lines[1].rect.bottom)
|
|
||||||
: (o.font_size || 12) * 1.25;
|
|
||||||
const advPx = Math.max(8, advPt * s);
|
|
||||||
|
|
||||||
const original = lines.map((l) => composeLineText(l.runs)).join("\n");
|
const original = lines.map((l) => composeLineText(l.runs)).join("\n");
|
||||||
|
|
||||||
const ce = document.createElement("div");
|
const ce = document.createElement("div");
|
||||||
@@ -2528,25 +2554,24 @@ function startBlockTextEdit(o, lines, ev) {
|
|||||||
leftPt = union.left;
|
leftPt = union.left;
|
||||||
widthPt = Math.min(bw * 1.35, p.widthPt - union.left - 8);
|
widthPt = Math.min(bw * 1.35, p.widthPt - union.left - 8);
|
||||||
}
|
}
|
||||||
// Bù lệch dọc: line-box của trình duyệt căn giữa glyph trong line-height,
|
// Advance TỪNG DÒNG = khoảng cách baseline dòng này → dòng dưới (xấp xỉ
|
||||||
// còn bbox PDF ôm sát glyph → đẩy khung lên (advPx − cỡ chữ)/2 cho trùng.
|
// bằng hiệu mép DƯỚI 2 bbox — bbox PDF ôm sát glyph nên bottom bám baseline).
|
||||||
const fs0 = (lines[0].fs || o.font_size || 12) * s;
|
// KHÔNG dùng khoảng hở giữa 2 bbox (bottom trên − top dưới ≈ 0/âm) — chính
|
||||||
const topPx = Math.max(0, (p.heightPt - union.top) * s - Math.max(0, (advPx - fs0) / 2));
|
// là bug làm các dòng dồn cục 8px.
|
||||||
// Tính advance cho TỪNG DÒNG dựa trên vị trí gốc trong PDF để giữ layout.
|
|
||||||
const perLineAdvances = [];
|
const perLineAdvances = [];
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
let linAdvPx;
|
let advPt;
|
||||||
if (i < lines.length - 1) {
|
if (i < lines.length - 1) {
|
||||||
// Advance = khoảng cách giữa dòng này và dòng tiếp theo (giữ nguyên từ PDF).
|
advPt = lines[i].rect.bottom - lines[i + 1].rect.bottom;
|
||||||
const gap = lines[i].rect.bottom - lines[i + 1].rect.top;
|
|
||||||
linAdvPx = Math.max(8, gap * s);
|
|
||||||
} else {
|
|
||||||
// Dòng cuối: dùng cỡ chữ × 1.25.
|
|
||||||
linAdvPx = Math.max(8, (lines[i].fs || 12) * s * 1.25);
|
|
||||||
}
|
}
|
||||||
perLineAdvances.push(linAdvPx);
|
if (!(advPt > 0)) advPt = (lines[i].fs || 12) * 1.25;
|
||||||
|
perLineAdvances.push(Math.max(8, advPt * s));
|
||||||
}
|
}
|
||||||
const totalHeightPx = perLineAdvances.reduce((a, b) => a + b, 0);
|
const totalHeightPx = perLineAdvances.reduce((a, b) => a + b, 0);
|
||||||
|
// Bù lệch dọc: line-box của trình duyệt căn giữa glyph trong line-height,
|
||||||
|
// còn bbox PDF ôm sát glyph → đẩy khung lên (line-height − cỡ chữ)/2 cho trùng.
|
||||||
|
const fs0 = (lines[0].fs || o.font_size || 12) * s;
|
||||||
|
const topPx = Math.max(0, (p.heightPt - union.top) * s - Math.max(0, (perLineAdvances[0] - fs0) / 2));
|
||||||
|
|
||||||
Object.assign(ce.style, {
|
Object.assign(ce.style, {
|
||||||
left: leftPt * s + "px",
|
left: leftPt * s + "px",
|
||||||
@@ -2571,11 +2596,19 @@ function startBlockTextEdit(o, lines, ev) {
|
|||||||
div.style.fontWeight = (rep.font_bold != null ? rep.font_bold : o.font_bold) ? "bold" : "normal";
|
div.style.fontWeight = (rep.font_bold != null ? rep.font_bold : o.font_bold) ? "bold" : "normal";
|
||||||
div.style.fontStyle = (rep.font_italic != null ? rep.font_italic : o.font_italic) ? "italic" : "normal";
|
div.style.fontStyle = (rep.font_italic != null ? rep.font_italic : o.font_italic) ? "italic" : "normal";
|
||||||
if (rep.color || o.color) div.style.color = rgbCss(rep.color || o.color);
|
if (rep.color || o.color) div.style.color = rgbCss(rep.color || o.color);
|
||||||
|
// Giữ thụt lề từng dòng so với mép khối (dòng đầu đoạn thụt vào…).
|
||||||
|
if (!centered) {
|
||||||
|
const indent = (l.rect.left - leftPt) * s;
|
||||||
|
if (indent > 0.5) div.style.textIndent = indent + "px";
|
||||||
|
}
|
||||||
ce.appendChild(div);
|
ce.appendChild(div);
|
||||||
}
|
}
|
||||||
ov.appendChild(ce);
|
ov.appendChild(ce);
|
||||||
// WebView không hỗ trợ plaintext-only (hiếm) → contenteditable thường.
|
// WebView không hỗ trợ plaintext-only (hiếm) → contenteditable thường.
|
||||||
if (!ce.isContentEditable) ce.contentEditable = "true";
|
if (!ce.isContentEditable) ce.contentEditable = "true";
|
||||||
|
// ĐO chữ vừa render rồi HIỆU CHỈNH cỡ/vị trí từng dòng cho TRÙNG bbox gốc —
|
||||||
|
// phải chạy TRƯỚC focus/đặt con trỏ để caretRangeFromPoint trúng layout cuối.
|
||||||
|
fitEditLinesToPdf(ce, lines, s, p);
|
||||||
ce.focus();
|
ce.focus();
|
||||||
|
|
||||||
// Đặt con trỏ đúng chỗ vừa đúp (như Foxit); không có toạ độ → đầu đoạn.
|
// Đặt con trỏ đúng chỗ vừa đúp (như Foxit); không có toạ độ → đầu đoạn.
|
||||||
@@ -2643,6 +2676,91 @@ function startBlockTextEdit(o, lines, ev) {
|
|||||||
ce.addEventListener("blur", () => commit(true));
|
ce.addEventListener("blur", () => commit(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ĐO chữ đã render trong ô sửa rồi HIỆU CHỈNH cho trùng khít chữ gốc — vòng
|
||||||
|
// đo-bù nên đúng với MỌI loại file, không phụ thuộc engine trả cỡ chuẩn hay
|
||||||
|
// công thức line-height của trình duyệt:
|
||||||
|
// (1) CỠ CHỮ: chiều cao MỰC của đúng chuỗi đó (canvas actualBoundingBox) phải
|
||||||
|
// bằng chiều cao bbox dòng trong PDF (cùng là ink-bbox, gồm cả dấu tiếng
|
||||||
|
// Việt) — engine trả cỡ lệch (matrix scale lạ) hay font thay thế lệch
|
||||||
|
// metric đều tự sửa; lệch <12% coi như khác biệt metric, giữ nguyên.
|
||||||
|
// (2) VỊ TRÍ: quy cả 2 phía về MÉP MỰC TRÊN của dòng (DOM: đỉnh em-box của
|
||||||
|
// Range + (fontAscent − inkAscent); PDF: rect.top) rồi bù margin-top /
|
||||||
|
// text-indent từng dòng. Dòng đầu bù vào chính khung (giữ nền trắng che
|
||||||
|
// kín chữ cũ), các dòng sau bù margin (đo tuần tự nên tự cộng dồn đúng).
|
||||||
|
function fitEditLinesToPdf(ce, lines, s, page) {
|
||||||
|
const img = $("editImg");
|
||||||
|
if (!img) return;
|
||||||
|
const canvas = (fitEditLinesToPdf._c = fitEditLinesToPdf._c || document.createElement("canvas"));
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const divs = Array.from(ce.children);
|
||||||
|
const centered = ce.style.textAlign === "center";
|
||||||
|
|
||||||
|
const fontOf = (div, fsOverride) => {
|
||||||
|
const fs = parseFloat(div.style.fontSize) || 12;
|
||||||
|
const style = div.style.fontStyle === "italic" ? "italic " : "";
|
||||||
|
const weight = div.style.fontWeight === "bold" ? "bold " : "";
|
||||||
|
return { fs, css: `${style}${weight}${fsOverride || fs}px ${div.style.fontFamily || "sans-serif"}` };
|
||||||
|
};
|
||||||
|
|
||||||
|
// (1) Cỡ chữ theo chiều cao mực thật. Đo ở cỡ THAM CHIẾU lớn (100px) rồi
|
||||||
|
// suy tuyến tính — đo trực tiếp ở cỡ nhỏ bị hinting làm tròn lệch tới ~5%.
|
||||||
|
const REF = 100;
|
||||||
|
for (let i = 0; i < divs.length && i < lines.length; i++) {
|
||||||
|
const text = (divs[i].textContent || "").trim();
|
||||||
|
if (!text) continue;
|
||||||
|
const f = fontOf(divs[i], REF);
|
||||||
|
ctx.font = f.css;
|
||||||
|
const m = ctx.measureText(text);
|
||||||
|
const inkPerPx = ((m.actualBoundingBoxAscent || 0) + (m.actualBoundingBoxDescent || 0)) / REF;
|
||||||
|
const targetPx = (lines[i].rect.top - lines[i].rect.bottom) * s;
|
||||||
|
if (inkPerPx > 0.01 && targetPx > 1) {
|
||||||
|
const wantFs = targetPx / inkPerPx;
|
||||||
|
const ratio = wantFs / f.fs;
|
||||||
|
if (ratio < 0.88 || ratio > 1.12) {
|
||||||
|
divs[i].style.fontSize = f.fs * Math.max(0.3, Math.min(3, ratio)) + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) Vị trí từng dòng (đo lại sau khi đã chỉnh cỡ).
|
||||||
|
const imgR = img.getBoundingClientRect();
|
||||||
|
for (let i = 0; i < divs.length && i < lines.length; i++) {
|
||||||
|
const r = firstTextRect(divs[i]);
|
||||||
|
if (!r) continue;
|
||||||
|
const text = (divs[i].textContent || "").trim();
|
||||||
|
const f = fontOf(divs[i]);
|
||||||
|
ctx.font = f.css;
|
||||||
|
const m = ctx.measureText(text);
|
||||||
|
const fontAscent = m.fontBoundingBoxAscent || f.fs * 0.8;
|
||||||
|
const inkAscent = m.actualBoundingBoxAscent || fontAscent;
|
||||||
|
const curInkTop = r.top + (fontAscent - inkAscent);
|
||||||
|
const dy = imgR.top + (page.heightPt - lines[i].rect.top) * s - curInkTop;
|
||||||
|
if (Math.abs(dy) > 0.5) {
|
||||||
|
if (i === 0) ce.style.top = parseFloat(ce.style.top) + dy + "px";
|
||||||
|
else divs[i].style.marginTop = (parseFloat(divs[i].style.marginTop) || 0) + dy + "px";
|
||||||
|
}
|
||||||
|
// Ngang: khối căn trái bù qua ce.left (dòng đầu) / text-indent (dòng sau);
|
||||||
|
// khối căn giữa bù mọi dòng qua text-indent (indent cộng thêm vào offset
|
||||||
|
// căn giữa nên vẫn dịch được từng dòng mà không phá cơ chế center).
|
||||||
|
const dx = imgR.left + lines[i].rect.left * s - r.left;
|
||||||
|
if (Math.abs(dx) > 0.5) {
|
||||||
|
if (!centered && i === 0) ce.style.left = parseFloat(ce.style.left) + dx + "px";
|
||||||
|
else divs[i].style.textIndent = (parseFloat(divs[i].style.textIndent) || 0) + dx + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bbox render thật của text trong div — đo bằng Range (chỉ tính chữ, không
|
||||||
|
// tính line-box nên không dính line-height); null nếu dòng rỗng.
|
||||||
|
function firstTextRect(div) {
|
||||||
|
const tn = div.firstChild;
|
||||||
|
if (!tn || tn.nodeType !== Node.TEXT_NODE) return null;
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(tn);
|
||||||
|
const rects = range.getClientRects();
|
||||||
|
return rects.length ? rects[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
// Đổi thuộc tính chữ cho CẢ DÒNG đang chọn: setText từng run (giữ nội dung
|
// Đổi thuộc tính chữ cho CẢ DÒNG đang chọn: setText từng run (giữ nội dung
|
||||||
// từng run), `part` chỉ chứa field muốn đổi — field khác null = GIỮ NGUYÊN.
|
// từng run), `part` chỉ chứa field muốn đổi — field khác null = GIỮ NGUYÊN.
|
||||||
function applyTextPropToSelected(part) {
|
function applyTextPropToSelected(part) {
|
||||||
|
|||||||
@@ -89,6 +89,35 @@ Ba lỗi user báo sau khi thử bản build 6 trên chính file Word-export:
|
|||||||
render so khớp bản gốc. 2 test hồi quy mới (`reflow_keeps_per_line_font_sizes`,
|
render so khớp bản gốc. 2 test hồi quy mới (`reflow_keeps_per_line_font_sizes`,
|
||||||
`reflow_hard_line_grows_without_rewrap`) — 21 test edit.
|
`reflow_hard_line_grows_without_rewrap`) — 21 test edit.
|
||||||
|
|
||||||
|
### Vá vòng 3 — ô sửa trùng khít chữ gốc bằng ĐO–HIỆU CHỈNH (fix hồi quy vòng 2)
|
||||||
|
User báo (kèm ảnh): ô sửa hiện chữ SAI font + SAI cỡ + SAI vị trí. Ba nguyên nhân:
|
||||||
|
- **Hồi quy vòng 2**: `perLineAdvances` tính line-height mỗi dòng bằng KHOẢNG HỞ
|
||||||
|
giữa 2 bbox (`bottom trên − top dưới` ≈ 0/âm → kẹp 8px) thay vì khoảng cách
|
||||||
|
baseline → các dòng dồn cục. Fix: advance = hiệu mép DƯỚI 2 bbox liền kề
|
||||||
|
(bbox PDFium ôm sát glyph nên bottom bám baseline).
|
||||||
|
- **Sai font**: engine trả family PostScript CamelCase ("TimesNewRoman",
|
||||||
|
"SegoeUI") không khớp tên font cài trên Windows → CSS rơi về serif/sans mặc
|
||||||
|
định. Fix: `cssFontStack` tách CamelCase thành tên có dấu cách + bảng alias
|
||||||
|
(Helvetica→Arial, TimesNewRomanPS→Times New Roman…).
|
||||||
|
- **Sai cỡ với file lạ**: cỡ px suy mở từ `font_size` engine, không tự kiểm.
|
||||||
|
Fix: **`fitEditLinesToPdf` — vòng đo–hiệu chỉnh sau khi mount ô sửa**:
|
||||||
|
(1) CỠ: chiều cao MỰC của đúng chuỗi đó (canvas `measureText` actualBoundingBox,
|
||||||
|
đo ở cỡ tham chiếu 100px để né sai số hinting ~5% ở cỡ nhỏ) phải bằng chiều
|
||||||
|
cao bbox dòng PDF (cùng là ink-bbox, PDFium bounds = union glyph box) — engine
|
||||||
|
báo cỡ lệch bao nhiêu cũng tự sửa về đúng, lệch <12% coi là khác metric font
|
||||||
|
→ giữ cỡ engine (ổn định file chuẩn); (2) VỊ TRÍ: quy 2 phía về MÉP MỰC TRÊN
|
||||||
|
(DOM: Range rect + (fontAscent − inkAscent) từ canvas; PDF: rect.top) rồi bù
|
||||||
|
`margin-top`/`text-indent` từng dòng (dòng đầu bù vào khung để nền trắng che
|
||||||
|
kín); khối căn giữa cũng bù ngang qua text-indent. Thêm: giữ thụt lề từng
|
||||||
|
dòng; đúp từ viewer thường nay đặt con trỏ đúng điểm đúp (toạ độ giả lập từ
|
||||||
|
điểm PDF, chờ `img.decode()` trước khi đo).
|
||||||
|
- **Kiểm chứng**: harness HTML mô phỏng trang bằng canvas (chữ gốc đỏ, bbox mực
|
||||||
|
thật làm ground truth) + Edge headless chụp 3 kịch bản: (A) engine báo cỡ sai
|
||||||
|
MỘT NỬA + family CamelCase → tự sửa về đúng, lệch ≤0.4px dọc / 0px ngang;
|
||||||
|
(B) engine báo đúng → dead-band giữ nguyên, trùng khít 100%; (C) khối căn
|
||||||
|
giữa Times → lệch ngang ≤1.3px. (Không unit-test tự động được vì cần layout
|
||||||
|
engine thật; harness lưu ở scratchpad phiên làm việc.)
|
||||||
|
|
||||||
### Giới hạn ghi nhận (v1, sẽ nâng ở vòng sau)
|
### Giới hạn ghi nhận (v1, sẽ nâng ở vòng sau)
|
||||||
- Đoạn justify (giãn đều 2 lề) reflow về căn trái; chưa kerning; khối text
|
- Đoạn justify (giãn đều 2 lề) reflow về căn trái; chưa kerning; khối text
|
||||||
XOAY chưa reflow theo hướng xoay (dòng mới đặt theo trục ngang).
|
XOAY chưa reflow theo hướng xoay (dòng mới đặt theo trục ngang).
|
||||||
|
|||||||
Reference in New Issue
Block a user