mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 04:04:16 +02:00
window-curvature: no bar flicker during a drag
The bar panel is no longer padded during a bar drag (its new size and the compensated origin reached the compositor at different times, so the bar flickered). SteamVR keeps sending the pressed bar's coordinates past its edge, so the drag simply uses them. `barDragRoom` is removed.
This commit is contained in:
1 parent
2feb3d405d
commit
817190facd
4 files changed
+17
-77
No files matched your search
@@ -219,7 +219,6 @@ without it.
|
|||||||
| `steamFrame.dashboard.windowCurvature.dragThreshold` | unsigned int (px) | `8` | Vertical travel before a press becomes a drag. |
|
| `steamFrame.dashboard.windowCurvature.dragThreshold` | unsigned int (px) | `8` | Vertical travel before a press becomes a drag. |
|
||||||
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | number (px) | `120` | Drag distance per 1.0 in the menu (6 px per 0.05 step). |
|
| `steamFrame.dashboard.windowCurvature.dragPixelsPerUnit` | number (px) | `120` | Drag distance per 1.0 in the menu (6 px per 0.05 step). |
|
||||||
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `60` | Drag distance per 1.0 on the bar button. |
|
| `steamFrame.dashboard.windowCurvature.barDragPixelsPerUnit` | number (px) | `60` | Drag distance per 1.0 on the bar button. |
|
||||||
| `steamFrame.dashboard.windowCurvature.barDragRoom` | unsigned int (px) | `160` | Room added above/below the bar while dragging; `0`: none. |
|
|
||||||
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging (steps, detents, edges); the dashboard's hover clicks are muted during a drag. |
|
| `steamFrame.dashboard.windowCurvature.haptics` | bool | `true` | Controller haptics while dragging (steps, detents, edges); the dashboard's hover clicks are muted during a drag. |
|
||||||
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move window controls between bar and three-dot menu, see [Window control bar](#window-control-bar-dashboardframecontrols). |
|
| `steamFrame.dashboard.frameControls.enable` | bool | `false` | Move window controls between bar and three-dot menu, see [Window control bar](#window-control-bar-dashboardframecontrols). |
|
||||||
| `steamFrame.dashboard.frameControls.longPressMs` | int, 300-10000 (ms) | `1500` | Long-press duration. |
|
| `steamFrame.dashboard.frameControls.longPressMs` | int, 300-10000 (ms) | `1500` | Long-press duration. |
|
||||||
|
|||||||
@@ -331,11 +331,6 @@
|
|||||||
"expects": ["properties.origin=", "properties.debug_name=this.props.debug_name", ".TopCenter:return{x:0,y:1}",
|
"expects": ["properties.origin=", "properties.debug_name=this.props.debug_name", ".TopCenter:return{x:0,y:1}",
|
||||||
"this.m_Rect=this.getCurrentRootElement().getBoundingClientRect()", "new ResizeObserver("]
|
"this.m_Rect=this.getCurrentRootElement().getBoundingClientRect()", "new ResizeObserver("]
|
||||||
},
|
},
|
||||||
"sgApp": {
|
|
||||||
"checkOnly": true,
|
|
||||||
"module": { "includes": ["forceLayoutUpdate(){", "s_bPanelsAreDirty"] },
|
|
||||||
"expects": ["forceLayoutUpdate=this.forceLayoutUpdate", "updateLayoutValues()"]
|
|
||||||
},
|
|
||||||
"hapticEffects": {
|
"hapticEffects": {
|
||||||
"checkOnly": true,
|
"checkOnly": true,
|
||||||
"module": { "includes": ["e[e.Snap=3]=\"Snap\",e[e.Sliding=4]=\"Sliding\",e[e.SlidingEdge=5]=\"SlidingEdge\""] }
|
"module": { "includes": ["e[e.Snap=3]=\"Snap\",e[e.Sliding=4]=\"Sliding\",e[e.SlidingEdge=5]=\"SlidingEdge\""] }
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ let
|
|||||||
in {
|
in {
|
||||||
imports = [
|
imports = [
|
||||||
./steam-ui-patches.nix ./steamvr-debugger.nix
|
./steam-ui-patches.nix ./steamvr-debugger.nix
|
||||||
|
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "barDragRoom" ]
|
||||||
|
"Not needed: SteamVR keeps sending the bar's coordinates past its edge during a drag.")
|
||||||
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "snap" ]
|
(lib.mkRemovedOptionModule [ "steamFrame" "dashboard" "windowCurvature" "snap" ]
|
||||||
"Snapping is a detent in drag pixels now: use steamFrame.dashboard.windowCurvature.snapPixels.")
|
"Snapping is a detent in drag pixels now: use steamFrame.dashboard.windowCurvature.snapPixels.")
|
||||||
];
|
];
|
||||||
@@ -61,14 +63,6 @@ in {
|
|||||||
barDragPixelsPerUnit = value 60 ''
|
barDragPixelsPerUnit = value 60 ''
|
||||||
Bar pixels per 1.0 of curvature on the bottom-bar button.
|
Bar pixels per 1.0 of curvature on the bottom-bar button.
|
||||||
'';
|
'';
|
||||||
barDragRoom = mkOption {
|
|
||||||
type = types.ints.unsigned;
|
|
||||||
default = 160;
|
|
||||||
description = ''
|
|
||||||
Transparent room (px) added above and below the bar while its button
|
|
||||||
is dragged, so the laser stays on the panel; 0 = none.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
haptics = mkOption {
|
haptics = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
@@ -105,7 +99,7 @@ in {
|
|||||||
patch = mkPatch {
|
patch = mkPatch {
|
||||||
name = "window-curvature";
|
name = "window-curvature";
|
||||||
src = ./window-curvature/patch.js;
|
src = ./window-curvature/patch.js;
|
||||||
opts = removeAttrs cfg [ "enable" "snap" ];
|
opts = removeAttrs cfg [ "enable" "snap" "barDragRoom" ];
|
||||||
};
|
};
|
||||||
unpatch = ./window-curvature/unpatch.js;
|
unpatch = ./window-curvature/unpatch.js;
|
||||||
} ];
|
} ];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
|
// Target: SteamVR dashboard (vrwebhelper, DevTools 127.0.0.1:8087, title
|
||||||
// "systemui"). mkPatch patch (see lib/default.nix); opts: { default, max,
|
// "systemui"). mkPatch patch (see lib/default.nix); opts: { default, max,
|
||||||
// step, snapPixels, snapPoints, dragThreshold, dragPixelsPerUnit,
|
// step, snapPixels, snapPoints, dragThreshold, dragPixelsPerUnit,
|
||||||
// barDragPixelsPerUnit, barDragRoom, haptics }.
|
// barDragPixelsPerUnit, haptics }.
|
||||||
//
|
//
|
||||||
// Stock curvature (frame.curvature, systemui's `curvature` component):
|
// Stock curvature (frame.curvature, systemui's `curvature` component):
|
||||||
// shouldCurve = m_bCurveOverride (set by ToggleCurvature(), cleared on dock
|
// shouldCurve = m_bCurveOverride (set by ToggleCurvature(), cleared on dock
|
||||||
@@ -29,12 +29,11 @@
|
|||||||
// opts.default in the world or on a hand, 1 when docked in the dashboard or
|
// opts.default in the world or on a hand, 1 when docked in the dashboard or
|
||||||
// theater (so the Steam window stays concentric with the dashboard bar).
|
// theater (so the Steam window stays concentric with the dashboard bar).
|
||||||
//
|
//
|
||||||
// Bar button: the bar panel (#legacy-frame-controls-<frameID>, origin
|
// Bar button: the bar panel (#legacy-frame-controls-<frameID>) is one button
|
||||||
// TopCenter) is one button high and the laser stops at the pressed panel's
|
// high, but while the trigger is held SteamVR keeps sending the pressed
|
||||||
// edge. During a bar drag the panel gets opts.barDragRoom px of transparent
|
// panel's coordinates past its edge, so the drag uses them as they come. The
|
||||||
// padding above and below (window.forceLayoutUpdate() re-measures) and its
|
// bar panel is never resized or moved (padding it made it flicker: size and
|
||||||
// origin is compensated in outgoing scene graphs so the bar stays put in VR.
|
// origin reach the compositor at different times).
|
||||||
// Laser events still mapped with the old layout are detected and shifted.
|
|
||||||
//
|
//
|
||||||
// Dragging: value = start value + drag distance / px per unit, rounded to
|
// Dragging: value = start value + drag distance / px per unit, rounded to
|
||||||
// step. Snap points are detents in drag distance: the value holds there for
|
// step. Snap points are detents in drag distance: the value holds there for
|
||||||
@@ -65,19 +64,19 @@
|
|||||||
// dragValue(v0, dy, ppu, cur).
|
// dragValue(v0, dy, ppu, cur).
|
||||||
((find, sigs, opts, hooks) => {
|
((find, sigs, opts, hooks) => {
|
||||||
const NAME = 'window-curvature';
|
const NAME = 'window-curvature';
|
||||||
const VERSION = 16;
|
const VERSION = 18;
|
||||||
const FLAT = 999; // origin distances >= this are "flat"
|
const FLAT = 999; // origin distances >= this are "flat"
|
||||||
const ICON_OFF = 40, ICON_ON = 39; // Toggle Curvature action icons (sigs.curvatureAction)
|
const ICON_OFF = 40, ICON_ON = 39; // Toggle Curvature action icons (sigs.curvatureAction)
|
||||||
const HAPTIC = { Snap: 3, Sliding: 4, SlidingEdge: 5 }; // EOverlayHapticEffect (sigs.hapticEffects)
|
const HAPTIC = { Snap: 3, Sliding: 4, SlidingEdge: 5 }; // EOverlayHapticEffect (sigs.hapticEffects)
|
||||||
|
|
||||||
const o = {
|
const o = {
|
||||||
default: 1, max: 3, step: 0.05, snapPixels: 24, snapPoints: [0, 1], dragThreshold: 8, dragPixelsPerUnit: 120,
|
default: 1, max: 3, step: 0.05, snapPixels: 24, snapPoints: [0, 1], dragThreshold: 8, dragPixelsPerUnit: 120,
|
||||||
barDragPixelsPerUnit: 60, barDragRoom: 160, haptics: true, ...opts,
|
barDragPixelsPerUnit: 60, haptics: true, ...opts,
|
||||||
};
|
};
|
||||||
if (!(o.max > 0 && o.step > 0 && o.step <= o.max && o.default >= 0 && o.default <= o.max && o.snapPixels >= 0 &&
|
if (!(o.max > 0 && o.step > 0 && o.step <= o.max && o.default >= 0 && o.default <= o.max && o.snapPixels >= 0 &&
|
||||||
Array.isArray(o.snapPoints) && o.snapPoints.every((p) => p >= 0 && p <= o.max) &&
|
Array.isArray(o.snapPoints) && o.snapPoints.every((p) => p >= 0 && p <= o.max) &&
|
||||||
o.dragThreshold >= 0 && o.dragPixelsPerUnit > 0 && o.barDragPixelsPerUnit > 0 &&
|
o.dragThreshold >= 0 && o.dragPixelsPerUnit > 0 && o.barDragPixelsPerUnit > 0 &&
|
||||||
Number.isInteger(o.barDragRoom) && o.barDragRoom >= 0 && typeof o.haptics === 'boolean'))
|
typeof o.haptics === 'boolean'))
|
||||||
return `invalid options ${JSON.stringify(opts)}`;
|
return `invalid options ${JSON.stringify(opts)}`;
|
||||||
const id = JSON.stringify([VERSION, o]);
|
const id = JSON.stringify([VERSION, o]);
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
logBuf.push({ t: new Date().toISOString().slice(11, 23), msg, ...(data ? { data } : {}) });
|
logBuf.push({ t: new Date().toISOString().slice(11, 23), msg, ...(data ? { data } : {}) });
|
||||||
if (logBuf.length > 200) logBuf.splice(0, logBuf.length - 200);
|
if (logBuf.length > 200) logBuf.splice(0, logBuf.length - 200);
|
||||||
};
|
};
|
||||||
const hits = { sends: 0, origins: 0, scaled: 0, roomOrigins: 0, haptics: 0 };
|
const hits = { sends: 0, origins: 0, scaled: 0, haptics: 0 };
|
||||||
|
|
||||||
// ---- values ------------------------------------------------------------------------
|
// ---- values ------------------------------------------------------------------------
|
||||||
const round = (v) => +(Math.round(v / o.step) * o.step).toFixed(6);
|
const round = (v) => +(Math.round(v / o.step) * o.step).toFixed(6);
|
||||||
@@ -173,20 +172,6 @@
|
|||||||
: o.snapPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
|
: o.snapPoints.includes(v) ? HAPTIC.Snap : HAPTIC.Sliding);
|
||||||
|
|
||||||
// ---- scene graph ---------------------------------------------------------------------
|
// ---- scene graph ---------------------------------------------------------------------
|
||||||
// Bar panels with drag room: frameID -> { node, pad, saved }.
|
|
||||||
const rooms = new Map();
|
|
||||||
// Keeps a padded bar panel's content where it was: the stock origin (x, y
|
|
||||||
// in -1..1 of the panel, y = 1 top) is re-expressed for the taller panel.
|
|
||||||
const fixOrigin = (p) => {
|
|
||||||
const m = /^legacy-frame-controls-(\d+)$/.exec(p.debug_name ?? '');
|
|
||||||
const r = m && rooms.get(Number(m[1]));
|
|
||||||
if (!r || !Array.isArray(p.origin) || p.origin.length < 2) return;
|
|
||||||
const h = r.node.getBoundingClientRect().height, inner = h - 2 * r.pad;
|
|
||||||
if (!(inner > 0)) return;
|
|
||||||
const fromTop = r.pad + ((1 - p.origin[1]) / 2) * inner;
|
|
||||||
p.origin = [p.origin[0], 1 - (2 * fromTop) / h, ...p.origin.slice(2)];
|
|
||||||
hits.roomOrigins++;
|
|
||||||
};
|
|
||||||
// Origin node ids may carry a "<prefix>::" (sub-scene); match the suffix.
|
// Origin node ids may carry a "<prefix>::" (sub-scene); match the suffix.
|
||||||
const rewrite = (sg) => {
|
const rewrite = (sg) => {
|
||||||
const byOrigin = new Map();
|
const byOrigin = new Map();
|
||||||
@@ -194,7 +179,7 @@
|
|||||||
const c = curvatureOf(f);
|
const c = curvatureOf(f);
|
||||||
if (c?.shouldCurve) byOrigin.set(c.curvatureTransformOriginID, f);
|
if (c?.shouldCurve) byOrigin.set(c.curvatureTransformOriginID, f);
|
||||||
}
|
}
|
||||||
if (!byOrigin.size && !rooms.size) return;
|
if (!byOrigin.size) return;
|
||||||
const walk = (n) => {
|
const walk = (n) => {
|
||||||
if (!n || typeof n !== 'object') return;
|
if (!n || typeof n !== 'object') return;
|
||||||
if (Array.isArray(n)) { n.forEach(walk); return; }
|
if (Array.isArray(n)) { n.forEach(walk); return; }
|
||||||
@@ -209,7 +194,6 @@
|
|||||||
hits.scaled++;
|
hits.scaled++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (rooms.size && n.type === 'panel' && p) fixOrigin(p);
|
|
||||||
if (n.children) walk(n.children);
|
if (n.children) walk(n.children);
|
||||||
};
|
};
|
||||||
walk(sg);
|
walk(sg);
|
||||||
@@ -230,12 +214,6 @@
|
|||||||
try { resendStock(); } catch (e) { log('resend failed', String(e)); }
|
try { resendStock(); } catch (e) { log('resend failed', String(e)); }
|
||||||
}, Math.max(0, lastResend + 40 - performance.now()));
|
}, Math.max(0, lastResend + 40 - performance.now()));
|
||||||
};
|
};
|
||||||
// Panel bounds are re-measured on the stock forced layout update (a
|
|
||||||
// padding change doesn't trigger the panels' ResizeObserver).
|
|
||||||
const relayout = () => {
|
|
||||||
try { window.forceLayoutUpdate?.(); } catch (e) { log('forceLayoutUpdate failed', String(e)); }
|
|
||||||
resend();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 0 = off, > 0 = on with that strength. On/off via stock ToggleCurvature so
|
// 0 = off, > 0 = on with that strength. On/off via stock ToggleCurvature so
|
||||||
// Steam's state and icon match. 0 forgets the stored value.
|
// Steam's state and icon match. 0 forgets the stored value.
|
||||||
@@ -364,7 +342,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
// re-bases (no jump). One press at a time, window capture listeners.
|
// re-bases (no jump). One press at a time, window capture listeners.
|
||||||
const stop = (e) => e.stopPropagation();
|
const stop = (e) => e.stopPropagation();
|
||||||
const STOPPED = ['click', 'dblclick', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'];
|
const STOPPED = ['click', 'dblclick', 'mouseup', 'pointerdown', 'pointerup', 'contextmenu'];
|
||||||
let press = null; // { c, y0, v0, last, moved, shift, stale }
|
let press = null; // { c, y0, v0, last, moved }
|
||||||
|
|
||||||
const emit = (c, type) => {
|
const emit = (c, type) => {
|
||||||
try {
|
try {
|
||||||
@@ -372,36 +350,13 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
} catch (e) { log(`${type} listener failed`, String(e)); }
|
} catch (e) { log(`${type} listener failed`, String(e)); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const addRoom = (p) => {
|
|
||||||
const node = p.c.panel;
|
|
||||||
if (!o.barDragRoom || !node?.isConnected || rooms.has(p.c.fid)) return;
|
|
||||||
const before = p.c.el.getBoundingClientRect().top;
|
|
||||||
rooms.set(p.c.fid, { node, pad: o.barDragRoom, saved: [node.style.paddingTop, node.style.paddingBottom] });
|
|
||||||
node.style.paddingTop = node.style.paddingBottom = `${o.barDragRoom}px`;
|
|
||||||
const shift = p.c.el.getBoundingClientRect().top - before;
|
|
||||||
p.shift = shift; p.stale = shift !== 0; p.last += shift; p.y0 += shift;
|
|
||||||
relayout();
|
|
||||||
};
|
|
||||||
const removeRoom = (fid) => {
|
|
||||||
const r = rooms.get(fid);
|
|
||||||
if (!r) return;
|
|
||||||
rooms.delete(fid);
|
|
||||||
[r.node.style.paddingTop, r.node.style.paddingBottom] = r.saved;
|
|
||||||
relayout();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMove = (e) => {
|
const onMove = (e) => {
|
||||||
const p = press;
|
const p = press;
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
const f = FS.GetFrame(p.c.fid);
|
const f = FS.GetFrame(p.c.fid);
|
||||||
if (!f) { endPress('frame gone'); return; }
|
if (!f) { endPress('frame gone'); return; }
|
||||||
let y = e.clientY;
|
const y = e.clientY;
|
||||||
// After the bar got its room, events the compositor still maps with the
|
|
||||||
// old layout are off by `shift`: take the reading closer to the last one.
|
|
||||||
if (p.stale) {
|
|
||||||
if (Math.abs(y + p.shift - p.last) < Math.abs(y - p.last)) y += p.shift;
|
|
||||||
else p.stale = false;
|
|
||||||
}
|
|
||||||
p.last = y;
|
p.last = y;
|
||||||
if (!p.moved) {
|
if (!p.moved) {
|
||||||
if (Math.abs(y - p.y0) < o.dragThreshold) return;
|
if (Math.abs(y - p.y0) < o.dragThreshold) return;
|
||||||
@@ -411,7 +366,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
log('drag', { frame: p.c.fid, where: p.c.where });
|
log('drag', { frame: p.c.fid, where: p.c.where });
|
||||||
emit(p.c, 'sfui-curv-dragstart');
|
emit(p.c, 'sfui-curv-dragstart');
|
||||||
if (press !== p) return; // a listener cancelled the press
|
if (press !== p) return; // a listener cancelled the press
|
||||||
if (p.c.where === 'bar') addRoom(p);
|
|
||||||
p.y0 = p.last;
|
p.y0 = p.last;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -433,7 +387,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
window.removeEventListener('mousemove', onMove, true);
|
window.removeEventListener('mousemove', onMove, true);
|
||||||
window.removeEventListener('mouseup', onUp, true);
|
window.removeEventListener('mouseup', onUp, true);
|
||||||
p.c.el?.classList.remove('sfui-dragging');
|
p.c.el?.classList.remove('sfui-dragging');
|
||||||
removeRoom(p.c.fid);
|
|
||||||
if (p.moved) emit(p.c, 'sfui-curv-dragend');
|
if (p.moved) emit(p.c, 'sfui-curv-dragend');
|
||||||
const f = FS.GetFrame(p.c.fid);
|
const f = FS.GetFrame(p.c.fid);
|
||||||
if (why === 'release') {
|
if (why === 'release') {
|
||||||
@@ -448,7 +401,7 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
const f = FS.GetFrame(c.fid);
|
const f = FS.GetFrame(c.fid);
|
||||||
if (e.button !== 0 || !f) return;
|
if (e.button !== 0 || !f) return;
|
||||||
endPress('new press');
|
endPress('new press');
|
||||||
press = { c, y0: e.clientY, last: e.clientY, v0: shown(f), moved: false, shift: 0, stale: false };
|
press = { c, y0: e.clientY, last: e.clientY, v0: shown(f), moved: false };
|
||||||
window.addEventListener('mousemove', onMove, true);
|
window.addEventListener('mousemove', onMove, true);
|
||||||
window.addEventListener('mouseup', onUp, true);
|
window.addEventListener('mouseup', onUp, true);
|
||||||
};
|
};
|
||||||
@@ -560,7 +513,6 @@ ${ROW}:last-child > .sfui-curv-ind { margin-bottom: -14px; }
|
|||||||
for (const d of disposers.splice(0)) d();
|
for (const d of disposers.splice(0)) d();
|
||||||
clearTimeout(syncTimer); clearTimeout(resendTimer);
|
clearTimeout(syncTimer); clearTimeout(resendTimer);
|
||||||
for (const key of [...controls.keys()]) removeControl(key);
|
for (const key of [...controls.keys()]) removeControl(key);
|
||||||
for (const fid of [...rooms.keys()]) removeRoom(fid);
|
|
||||||
document.getElementById(STYLE_ID)?.remove();
|
document.getElementById(STYLE_ID)?.remove();
|
||||||
hooks.remove(proto, 'SendMessage', NAME);
|
hooks.remove(proto, 'SendMessage', NAME);
|
||||||
if (window.__sfuiWindowCurvature === state) delete window.__sfuiWindowCurvature;
|
if (window.__sfuiWindowCurvature === state) delete window.__sfuiWindowCurvature;
|
||||||
|
|||||||
Reference in new issue
Block a user