Merge branch 'testing' into wine-access
This commit is contained in:
@@ -1715,6 +1715,49 @@ class EventManager:
|
||||
|
||||
return False
|
||||
|
||||
def _event_refreshes_xterm_handoff(self, event: Atspi.Event) -> bool:
|
||||
"""Returns True when an event can represent a real foreground transition."""
|
||||
|
||||
if event.type.startswith("window:activate"):
|
||||
return True
|
||||
|
||||
if event.type.startswith("object:state-changed:focused"):
|
||||
return bool(event.detail1)
|
||||
|
||||
if event.type.startswith("object:state-changed:active") and event.detail1:
|
||||
return AXUtilities.is_frame(event.source) or AXUtilities.is_window(event.source)
|
||||
|
||||
return False
|
||||
|
||||
def _is_notification_event_during_xterm_handoff(self, event: Atspi.Event) -> bool:
|
||||
"""Returns True when an event should bypass XTerm handoff suppression."""
|
||||
|
||||
for obj in (event.source, event.any_data):
|
||||
if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
|
||||
continue
|
||||
try:
|
||||
if AXUtilities.is_notification(obj) or AXUtilities.is_alert(obj):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
notificationEventTypes = (
|
||||
"window:create",
|
||||
"object:property-change:accessible-name",
|
||||
"object:property-change:accessible-value",
|
||||
)
|
||||
if not event.type.startswith(notificationEventTypes):
|
||||
return False
|
||||
|
||||
app = AXObject.get_application(event.source)
|
||||
appName = (AXObject.get_name(app) or "").casefold()
|
||||
return appName in (
|
||||
"notification-daemon",
|
||||
"notify-osd",
|
||||
"xfce4-notifyd",
|
||||
"mate-notification-daemon",
|
||||
)
|
||||
|
||||
def _processObjectEvent(self, event: Atspi.Event) -> None:
|
||||
"""Handles all object events destined for scripts.
|
||||
|
||||
@@ -1728,6 +1771,21 @@ class EventManager:
|
||||
debug.printObjectEvent(debug.LEVEL_INFO, event, timestamp=True)
|
||||
eType = event.type
|
||||
|
||||
refreshXtermHandoff = self._event_refreshes_xterm_handoff(event)
|
||||
isNotification = self._is_notification_event_during_xterm_handoff(event)
|
||||
suppressForXterm = not isNotification and self._inputEventManager is not None and (
|
||||
self._inputEventManager.should_suppress_atspi_events_for_xterm(
|
||||
refresh=refreshXtermHandoff,
|
||||
)
|
||||
)
|
||||
if suppressForXterm:
|
||||
msg = (
|
||||
"EVENT MANAGER: Ignoring AT-SPI event while XTerm owns the "
|
||||
"foreground handoff."
|
||||
)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return
|
||||
|
||||
if eType.startswith("object:children-changed:remove") \
|
||||
and event.source == AXUtilities.get_desktop():
|
||||
cthulhu.cthulhuApp.scriptManager.reclaim_scripts()
|
||||
|
||||
@@ -120,6 +120,13 @@ class InputEventManager:
|
||||
self._secure_input_active: bool = False
|
||||
self._wnck = None
|
||||
self._did_attempt_wnck_load: bool = False
|
||||
self._x11Display = None
|
||||
self._x11Root = None
|
||||
self._netActiveWindowAtom = None
|
||||
self._x11AnyPropertyType = None
|
||||
self._didAttemptX11Display: bool = False
|
||||
self._lastEwmhActiveWindowId: Optional[int] = None
|
||||
self._lastEwmhXtermMatch: Optional[bool] = None
|
||||
self._scriptWithSuspendedGrabsForXterm = None
|
||||
self._xtermFocusScreen = None
|
||||
self._xtermFocusHandlerId: int = 0
|
||||
@@ -177,6 +184,9 @@ class InputEventManager:
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
device.add_key_watcher(self.process_keyboard_event)
|
||||
|
||||
if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11":
|
||||
self._initialize_xterm_handoff()
|
||||
|
||||
def stop_key_watcher(self) -> None:
|
||||
"""Stops the watcher for keyboard input events."""
|
||||
|
||||
@@ -201,6 +211,8 @@ class InputEventManager:
|
||||
self._device = None
|
||||
self._stop_xterm_focus_monitor()
|
||||
self._stop_xterm_recovery_timer()
|
||||
self._close_x11_display()
|
||||
self._didAttemptX11Display = False
|
||||
self._scriptWithSuspendedGrabsForXterm = None
|
||||
|
||||
def get_debug_snapshot(self) -> Dict[str, object]:
|
||||
@@ -215,6 +227,14 @@ class InputEventManager:
|
||||
"paused": self._paused,
|
||||
"secure_input_active": self._secure_input_active,
|
||||
"suspended_xterm_script_present": self._scriptWithSuspendedGrabsForXterm is not None,
|
||||
"xterm_focus_monitor_active": self._xtermFocusHandlerId != 0,
|
||||
"xterm_recovery_timer_active": self._xtermRecoverySourceId != 0,
|
||||
"last_ewmh_active_window_id": (
|
||||
hex(self._lastEwmhActiveWindowId)
|
||||
if self._lastEwmhActiveWindowId is not None
|
||||
else ""
|
||||
),
|
||||
"last_ewmh_xterm_match": self._lastEwmhXtermMatch,
|
||||
"last_input_event_present": self._last_input_event is not None,
|
||||
"last_non_modifier_key_event_present": self._last_non_modifier_key_event is not None,
|
||||
}
|
||||
@@ -524,8 +544,8 @@ class InputEventManager:
|
||||
|
||||
return self._wnck
|
||||
|
||||
def _get_active_x11_window(self) -> Optional[Any]:
|
||||
"""Returns the active X11 window if Wnck can provide it."""
|
||||
def _get_active_x11_window(self, ewmhWindowId: Optional[int] = None) -> Optional[Any]:
|
||||
"""Returns the EWMH active X11 window, using Wnck for its metadata."""
|
||||
|
||||
wnck = self._get_wnck()
|
||||
if wnck is None:
|
||||
@@ -537,12 +557,78 @@ class InputEventManager:
|
||||
return None
|
||||
self._ensure_xterm_focus_monitor(screen)
|
||||
screen.force_update()
|
||||
windowId = ewmhWindowId
|
||||
if windowId is None:
|
||||
windowId = self._get_ewmh_active_x11_window_id()
|
||||
if windowId is not None:
|
||||
window = wnck.Window.get(windowId)
|
||||
if window is None:
|
||||
tokens = [
|
||||
"INPUT EVENT MANAGER: Wnck has no window for EWMH active X11 id",
|
||||
hex(windowId),
|
||||
]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return window
|
||||
return screen.get_active_window()
|
||||
except Exception as error:
|
||||
msg = f"INPUT EVENT MANAGER: Could not obtain active X11 window: {error}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
return None
|
||||
|
||||
def _get_ewmh_active_x11_window_id(self) -> Optional[int]:
|
||||
"""Returns _NET_ACTIVE_WINDOW from the X11 root window when available."""
|
||||
|
||||
if not self._didAttemptX11Display:
|
||||
self._didAttemptX11Display = True
|
||||
try:
|
||||
from Xlib import X # pylint: disable=import-outside-toplevel
|
||||
from Xlib import display as xDisplay # pylint: disable=import-outside-toplevel
|
||||
|
||||
self._x11Display = xDisplay.Display()
|
||||
self._x11Root = self._x11Display.screen().root
|
||||
self._netActiveWindowAtom = self._x11Display.intern_atom("_NET_ACTIVE_WINDOW")
|
||||
self._x11AnyPropertyType = X.AnyPropertyType
|
||||
except Exception as error:
|
||||
msg = f"INPUT EVENT MANAGER: EWMH active-window lookup unavailable: {error}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
self._close_x11_display()
|
||||
|
||||
if self._x11Root is None or self._netActiveWindowAtom is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
propertyData = self._x11Root.get_full_property(
|
||||
self._netActiveWindowAtom,
|
||||
self._x11AnyPropertyType,
|
||||
)
|
||||
if propertyData is None or not propertyData.value:
|
||||
return None
|
||||
windowId = int(propertyData.value[0])
|
||||
return windowId if windowId > 0 else None
|
||||
except Exception as error:
|
||||
msg = f"INPUT EVENT MANAGER: Could not read EWMH active window: {error}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
return None
|
||||
|
||||
def _close_x11_display(self) -> None:
|
||||
"""Closes the optional X11 display used for EWMH focus lookup."""
|
||||
|
||||
x11Display = self._x11Display
|
||||
self._x11Display = None
|
||||
self._x11Root = None
|
||||
self._netActiveWindowAtom = None
|
||||
self._x11AnyPropertyType = None
|
||||
self._lastEwmhActiveWindowId = None
|
||||
self._lastEwmhXtermMatch = None
|
||||
if x11Display is None:
|
||||
return
|
||||
|
||||
try:
|
||||
x11Display.close()
|
||||
except Exception as error:
|
||||
msg = f"INPUT EVENT MANAGER: Could not close EWMH display: {error}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _get_active_x11_window_pid(self) -> int:
|
||||
"""Returns the PID of the active X11 window if Wnck can provide it."""
|
||||
|
||||
@@ -641,6 +727,16 @@ class InputEventManager:
|
||||
self._xtermFocusScreen = screen
|
||||
self._xtermFocusHandlerId = handlerId
|
||||
|
||||
def _initialize_xterm_handoff(self) -> None:
|
||||
"""Initializes X11 focus monitoring and suspends immediately for XTerm."""
|
||||
|
||||
match = self._active_x11_window_xterm_match()
|
||||
tokens = ["INPUT EVENT MANAGER: Initial XTerm handoff matcher returned", match]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
if match is True:
|
||||
self._suspend_key_grabs_for_xterm()
|
||||
self._ensure_xterm_recovery_timer()
|
||||
|
||||
def _stop_xterm_recovery_timer(self) -> None:
|
||||
"""Stops the fallback timer used to recover XTerm-suspended grabs."""
|
||||
|
||||
@@ -656,7 +752,7 @@ class InputEventManager:
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _ensure_xterm_recovery_timer(self) -> None:
|
||||
"""Starts fallback recovery when X11 focus notifications are insufficient."""
|
||||
"""Starts generic X11 polling when focus notifications are insufficient."""
|
||||
|
||||
if self._device is None or self._xtermRecoverySourceId:
|
||||
return
|
||||
@@ -667,52 +763,50 @@ class InputEventManager:
|
||||
)
|
||||
|
||||
def _poll_xterm_grab_recovery(self) -> bool:
|
||||
"""Checks whether XTerm-suspended grabs can now be restored."""
|
||||
"""Updates XTerm handoff state from the authoritative EWMH active window."""
|
||||
|
||||
if self._scriptWithSuspendedGrabsForXterm is None:
|
||||
if self._device is None:
|
||||
self._xtermRecoverySourceId = 0
|
||||
return False
|
||||
|
||||
match = self._active_x11_window_xterm_match()
|
||||
tokens = ["INPUT EVENT MANAGER: XTerm recovery matcher returned", match]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
if match is False:
|
||||
self._xtermRecoverySourceId = 0
|
||||
if match is True:
|
||||
self._suspend_key_grabs_for_xterm()
|
||||
elif match is False:
|
||||
self._restore_key_grabs_after_xterm()
|
||||
return False
|
||||
|
||||
keepPolling = not self._xtermFocusHandlerId or match is None
|
||||
if not keepPolling:
|
||||
self._xtermRecoverySourceId = 0
|
||||
return keepPolling
|
||||
return True
|
||||
|
||||
def _on_active_x11_window_changed(self, screen: Any, _previousWindow: Any) -> None:
|
||||
"""Restores suspended grabs as soon as X11 focus leaves XTerm."""
|
||||
"""Updates grab suspension as soon as X11 focus changes."""
|
||||
|
||||
if self._scriptWithSuspendedGrabsForXterm is None:
|
||||
return
|
||||
|
||||
try:
|
||||
window = screen.get_active_window()
|
||||
except Exception as error:
|
||||
msg = f"INPUT EVENT MANAGER: Could not check changed X11 focus: {error}"
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
self._ensure_xterm_recovery_timer()
|
||||
return
|
||||
|
||||
match = self._x11_window_xterm_match(window)
|
||||
del screen
|
||||
match = self._active_x11_window_xterm_match()
|
||||
tokens = ["INPUT EVENT MANAGER: XTerm focus-change matcher returned", match]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
if match is False:
|
||||
if match is True:
|
||||
self._suspend_key_grabs_for_xterm()
|
||||
elif match is False:
|
||||
self._restore_key_grabs_after_xterm()
|
||||
elif match is None:
|
||||
elif self._scriptWithSuspendedGrabsForXterm is not None:
|
||||
self._ensure_xterm_recovery_timer()
|
||||
|
||||
def _active_x11_window_xterm_match(self) -> Optional[bool]:
|
||||
"""Returns whether the active X11 window is XTerm, or None when unknown."""
|
||||
|
||||
window = self._get_active_x11_window()
|
||||
return self._x11_window_xterm_match(window)
|
||||
windowId = self._get_ewmh_active_x11_window_id()
|
||||
if (
|
||||
windowId is not None
|
||||
and windowId == self._lastEwmhActiveWindowId
|
||||
and self._lastEwmhXtermMatch is not None
|
||||
):
|
||||
return self._lastEwmhXtermMatch
|
||||
|
||||
window = self._get_active_x11_window(ewmhWindowId=windowId)
|
||||
match = self._x11_window_xterm_match(window)
|
||||
if windowId is not None and match is not None:
|
||||
self._lastEwmhActiveWindowId = windowId
|
||||
self._lastEwmhXtermMatch = match
|
||||
return match
|
||||
|
||||
def _x11_window_xterm_match(self, window: Any) -> Optional[bool]:
|
||||
"""Returns whether window is XTerm, or None when it cannot be identified."""
|
||||
@@ -775,6 +869,25 @@ class InputEventManager:
|
||||
|
||||
return self._active_x11_window_xterm_match() is True
|
||||
|
||||
def should_suppress_atspi_events_for_xterm(self, refresh: bool = False) -> bool:
|
||||
"""Returns True while the XTerm handoff should suppress AT-SPI events."""
|
||||
|
||||
suspendedScript = self._scriptWithSuspendedGrabsForXterm
|
||||
if suspendedScript is None:
|
||||
return False
|
||||
|
||||
if not refresh:
|
||||
return True
|
||||
|
||||
match = self._active_x11_window_xterm_match()
|
||||
tokens = ["INPUT EVENT MANAGER: XTerm AT-SPI suppression matcher returned", match]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
if match is False:
|
||||
self._restore_key_grabs_after_xterm()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _find_active_x11_atspi_window(self) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the focused AT-SPI window for the active X11 PID, if possible."""
|
||||
|
||||
@@ -941,8 +1054,7 @@ class InputEventManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
removeGrabs()
|
||||
self._scriptWithSuspendedGrabsForXterm = script
|
||||
if not self._xtermFocusHandlerId:
|
||||
self._ensure_xterm_recovery_timer()
|
||||
self._ensure_xterm_recovery_timer()
|
||||
|
||||
def _restore_key_grabs_after_xterm(self) -> None:
|
||||
"""Restores key grabs after leaving XTerm."""
|
||||
@@ -960,7 +1072,6 @@ class InputEventManager:
|
||||
]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
self._scriptWithSuspendedGrabsForXterm = None
|
||||
self._stop_xterm_recovery_timer()
|
||||
return
|
||||
|
||||
addGrabs = getattr(script, "addKeyGrabs", None)
|
||||
@@ -988,13 +1099,11 @@ class InputEventManager:
|
||||
)
|
||||
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||
self._scriptWithSuspendedGrabsForXterm = None
|
||||
self._stop_xterm_recovery_timer()
|
||||
return
|
||||
self._ensure_xterm_recovery_timer()
|
||||
return
|
||||
|
||||
self._scriptWithSuspendedGrabsForXterm = None
|
||||
self._stop_xterm_recovery_timer()
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
|
||||
@@ -171,12 +171,20 @@ class Script(web.Script):
|
||||
def onColumnReordered(self, event):
|
||||
"""Callback for object:column-reordered accessibility events."""
|
||||
|
||||
if super().onColumnReordered(event):
|
||||
return
|
||||
def handle_shared_web(routed_event: object) -> RouteResult:
|
||||
handled = web.Script.onColumnReordered(self, routed_event)
|
||||
return RouteResult.HANDLED if handled else RouteResult.CONTINUE
|
||||
|
||||
msg = "CHROMIUM: Passing along event to default script"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
default.Script.onColumnReordered(self, event)
|
||||
self._eventRouter.route_event(
|
||||
event,
|
||||
application_handler=None,
|
||||
toolkit_handler=None,
|
||||
web_handler=handle_shared_web,
|
||||
default_handler=lambda routed_event: default.Script.onColumnReordered(
|
||||
self,
|
||||
routed_event,
|
||||
),
|
||||
)
|
||||
|
||||
def onChildrenAdded(self, event):
|
||||
"""Callback for object:children-changed:add accessibility events."""
|
||||
@@ -256,12 +264,20 @@ class Script(web.Script):
|
||||
def onExpandedChanged(self, event):
|
||||
"""Callback for object:state-changed:expanded accessibility events."""
|
||||
|
||||
if super().onExpandedChanged(event):
|
||||
return
|
||||
def handle_shared_web(routed_event: object) -> RouteResult:
|
||||
handled = web.Script.onExpandedChanged(self, routed_event)
|
||||
return RouteResult.HANDLED if handled else RouteResult.CONTINUE
|
||||
|
||||
msg = "CHROMIUM: Passing along event to default script"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
default.Script.onExpandedChanged(self, event)
|
||||
self._eventRouter.route_event(
|
||||
event,
|
||||
application_handler=None,
|
||||
toolkit_handler=None,
|
||||
web_handler=handle_shared_web,
|
||||
default_handler=lambda routed_event: default.Script.onExpandedChanged(
|
||||
self,
|
||||
routed_event,
|
||||
),
|
||||
)
|
||||
|
||||
def onFocus(self, event):
|
||||
"""Callback for focus: accessibility events."""
|
||||
@@ -315,12 +331,20 @@ class Script(web.Script):
|
||||
def onRowReordered(self, event):
|
||||
"""Callback for object:row-reordered accessibility events."""
|
||||
|
||||
if super().onRowReordered(event):
|
||||
return
|
||||
def handle_shared_web(routed_event: object) -> RouteResult:
|
||||
handled = web.Script.onRowReordered(self, routed_event)
|
||||
return RouteResult.HANDLED if handled else RouteResult.CONTINUE
|
||||
|
||||
msg = "CHROMIUM: Passing along event to default script"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
default.Script.onRowReordered(self, event)
|
||||
self._eventRouter.route_event(
|
||||
event,
|
||||
application_handler=None,
|
||||
toolkit_handler=None,
|
||||
web_handler=handle_shared_web,
|
||||
default_handler=lambda routed_event: default.Script.onRowReordered(
|
||||
self,
|
||||
routed_event,
|
||||
),
|
||||
)
|
||||
|
||||
def onSelectedChanged(self, event):
|
||||
"""Callback for object:state-changed:selected accessibility events."""
|
||||
|
||||
@@ -144,12 +144,20 @@ class Script(web.Script):
|
||||
def onColumnReordered(self, event):
|
||||
"""Callback for object:column-reordered accessibility events."""
|
||||
|
||||
if super().onColumnReordered(event):
|
||||
return
|
||||
def handle_shared_web(routed_event: object) -> RouteResult:
|
||||
handled = web.Script.onColumnReordered(self, routed_event)
|
||||
return RouteResult.HANDLED if handled else RouteResult.CONTINUE
|
||||
|
||||
msg = "GECKO: Passing along event to default script"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
default.Script.onColumnReordered(self, event)
|
||||
self._eventRouter.route_event(
|
||||
event,
|
||||
application_handler=None,
|
||||
toolkit_handler=None,
|
||||
web_handler=handle_shared_web,
|
||||
default_handler=lambda routed_event: default.Script.onColumnReordered(
|
||||
self,
|
||||
routed_event,
|
||||
),
|
||||
)
|
||||
|
||||
def onChildrenAdded(self, event):
|
||||
"""Callback for object:children-changed:add accessibility events."""
|
||||
@@ -209,12 +217,20 @@ class Script(web.Script):
|
||||
def onExpandedChanged(self, event):
|
||||
"""Callback for object:state-changed:expanded accessibility events."""
|
||||
|
||||
if super().onExpandedChanged(event):
|
||||
return
|
||||
def handle_shared_web(routed_event: object) -> RouteResult:
|
||||
handled = web.Script.onExpandedChanged(self, routed_event)
|
||||
return RouteResult.HANDLED if handled else RouteResult.CONTINUE
|
||||
|
||||
msg = "GECKO: Passing along event to default script"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
default.Script.onExpandedChanged(self, event)
|
||||
self._eventRouter.route_event(
|
||||
event,
|
||||
application_handler=None,
|
||||
toolkit_handler=None,
|
||||
web_handler=handle_shared_web,
|
||||
default_handler=lambda routed_event: default.Script.onExpandedChanged(
|
||||
self,
|
||||
routed_event,
|
||||
),
|
||||
)
|
||||
|
||||
def onFocus(self, event):
|
||||
"""Callback for focus: accessibility events."""
|
||||
@@ -285,12 +301,20 @@ class Script(web.Script):
|
||||
def onRowReordered(self, event):
|
||||
"""Callback for object:row-reordered accessibility events."""
|
||||
|
||||
if super().onRowReordered(event):
|
||||
return
|
||||
def handle_shared_web(routed_event: object) -> RouteResult:
|
||||
handled = web.Script.onRowReordered(self, routed_event)
|
||||
return RouteResult.HANDLED if handled else RouteResult.CONTINUE
|
||||
|
||||
msg = "GECKO: Passing along event to default script"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
default.Script.onRowReordered(self, event)
|
||||
self._eventRouter.route_event(
|
||||
event,
|
||||
application_handler=None,
|
||||
toolkit_handler=None,
|
||||
web_handler=handle_shared_web,
|
||||
default_handler=lambda routed_event: default.Script.onRowReordered(
|
||||
self,
|
||||
routed_event,
|
||||
),
|
||||
)
|
||||
|
||||
def onSelectedChanged(self, event):
|
||||
"""Callback for object:state-changed:selected accessibility events."""
|
||||
|
||||
Reference in New Issue
Block a user