Split up HintActions from HintManager
This commit is contained in:
parent
d8521f43ee
commit
e2ae133757
@ -47,9 +47,9 @@ Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg',
|
||||
'userscript', 'spawn'])
|
||||
|
||||
|
||||
class WordHintingError(Exception):
|
||||
class HintingError(Exception):
|
||||
|
||||
"""Exception raised on errors during word hinting."""
|
||||
"""Exception raised on errors during hinting."""
|
||||
|
||||
|
||||
def on_mode_entered(mode, win_id):
|
||||
@ -58,6 +58,32 @@ def on_mode_entered(mode, win_id):
|
||||
modeman.maybe_leave(win_id, usertypes.KeyMode.hint, 'insert mode')
|
||||
|
||||
|
||||
def _resolve_url(elem, baseurl):
|
||||
"""Resolve a URL and check if we want to keep it.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to get the URL of.
|
||||
baseurl: The baseurl of the current tab.
|
||||
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
for attr in ['href', 'src']:
|
||||
if attr in elem:
|
||||
text = elem[attr].strip()
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
url = QUrl(text)
|
||||
if not url.isValid():
|
||||
return None
|
||||
if url.isRelative():
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
||||
class HintContext:
|
||||
|
||||
"""Context namespace used for hinting.
|
||||
@ -105,6 +131,199 @@ class HintContext:
|
||||
return args
|
||||
|
||||
|
||||
class HintActions(QObject):
|
||||
|
||||
"""Actions which can be done after selecting a hint.
|
||||
|
||||
Signals:
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The ClickTarget to use.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
"""
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
|
||||
def click(self, elem, context):
|
||||
"""Click an element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to click.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
target_mapping = {
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.current: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
Target.hover: usertypes.ClickTarget.normal,
|
||||
}
|
||||
if config.get('tabs', 'background-tabs'):
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab
|
||||
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
# corner of the rectangle, this will help if part of the <a> element
|
||||
# is hidden behind other elements
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1005
|
||||
rect = elem.rect_on_view()
|
||||
if rect.width() > rect.height():
|
||||
rect.setWidth(rect.height())
|
||||
else:
|
||||
rect.setHeight(rect.width())
|
||||
pos = rect.center()
|
||||
|
||||
action = "Hovering" if context.target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at position {}".format(
|
||||
action, elem.debug_text(), pos))
|
||||
|
||||
self.start_hinting.emit(target_mapping[context.target])
|
||||
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
|
||||
Target.window]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if context.target != Target.hover:
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
|
||||
if context.target in [Target.normal, Target.current]:
|
||||
# Set the pre-jump mark ', so we can jump back here after following
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.set_mark("'")
|
||||
|
||||
if context.target == Target.current:
|
||||
elem.remove_blank_target()
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
elem.frame().page().triggerAction,
|
||||
QWebPage.MoveToEndOfDocument))
|
||||
QTimer.singleShot(0, self.stop_hinting.emit)
|
||||
|
||||
def yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
sel = (context.target == Target.yank_primary and
|
||||
utils.supports_selection())
|
||||
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
utils.set_clipboard(urlstr, selection=sel)
|
||||
|
||||
msg = "Yanked URL to {}: {}".format(
|
||||
"primary selection" if sel else "clipboard",
|
||||
urlstr)
|
||||
message.info(self._win_id, msg)
|
||||
|
||||
def run_cmd(self, url, context):
|
||||
"""Run the command based on a hint URL.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded)
|
||||
args = context.get_args(urlstr)
|
||||
commandrunner = runners.CommandRunner(self._win_id)
|
||||
commandrunner.run_safely(' '.join(args))
|
||||
|
||||
def preset_cmd_text(self, url, context):
|
||||
"""Preset a commandline text based on a hint URL.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toDisplayString(QUrl.FullyEncoded)
|
||||
args = context.get_args(urlstr)
|
||||
text = ' '.join(args)
|
||||
if text[0] not in modeparsers.STARTCHARS:
|
||||
message.error(self._win_id,
|
||||
"Invalid command text '{}'.".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.set_cmd_text(self._win_id, text)
|
||||
|
||||
def download(self, elem, context):
|
||||
"""Download a hint URL.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to download.
|
||||
_context: The HintContext to use.
|
||||
"""
|
||||
url = _resolve_url(elem, context.baseurl)
|
||||
if url is None:
|
||||
raise HintingError
|
||||
if context.rapid:
|
||||
prompt = False
|
||||
else:
|
||||
prompt = None
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
download_manager.get(url, page=elem.frame().page(),
|
||||
prompt_download_directory=prompt)
|
||||
|
||||
def call_userscript(self, elem, context):
|
||||
"""Call a userscript from a hint.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to use in the userscript.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
cmd = context.args[0]
|
||||
args = context.args[1:]
|
||||
env = {
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
'QUTE_SELECTED_HTML': elem.outer_xml(),
|
||||
}
|
||||
url = _resolve_url(elem, context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
|
||||
try:
|
||||
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
|
||||
env=env)
|
||||
except userscripts.UnsupportedError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
|
||||
def spawn(self, url, context):
|
||||
"""Spawn a simple command from a hint.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
args = context.get_args(urlstr)
|
||||
commandrunner = runners.CommandRunner(self._win_id)
|
||||
commandrunner.run_safely('spawn ' + ' '.join(args))
|
||||
|
||||
|
||||
class HintManager(QObject):
|
||||
|
||||
"""Manage drawing hints over links or other elements.
|
||||
@ -119,11 +338,7 @@ class HintManager(QObject):
|
||||
_filterstr: Used to save the filter string for restoring in rapid mode.
|
||||
|
||||
Signals:
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The ClickTarget to use.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
See HintActions
|
||||
"""
|
||||
|
||||
HINT_TEXTS = {
|
||||
@ -155,6 +370,12 @@ class HintManager(QObject):
|
||||
self._context = None
|
||||
self._filterstr = None
|
||||
self._word_hinter = WordHinter()
|
||||
|
||||
self._actions = HintActions(win_id)
|
||||
self._actions.start_hinting.connect(self.start_hinting)
|
||||
self._actions.stop_hinting.connect(self.stop_hinting)
|
||||
self._actions.mouse_event.connect(self.mouse_event)
|
||||
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
@ -203,7 +424,7 @@ class HintManager(QObject):
|
||||
if hint_mode == 'word':
|
||||
try:
|
||||
return self._word_hinter.hint(elems)
|
||||
except WordHintingError as e:
|
||||
except HintingError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
# falls back on letter hints
|
||||
if hint_mode == 'number':
|
||||
@ -405,204 +626,6 @@ class HintManager(QObject):
|
||||
message.error(self._win_id, "No suitable link found for this element.",
|
||||
immediately=True)
|
||||
|
||||
def _click(self, elem, context):
|
||||
"""Click an element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to click.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
target_mapping = {
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.current: usertypes.ClickTarget.normal,
|
||||
Target.tab_fg: usertypes.ClickTarget.tab,
|
||||
Target.tab_bg: usertypes.ClickTarget.tab_bg,
|
||||
Target.window: usertypes.ClickTarget.window,
|
||||
Target.hover: usertypes.ClickTarget.normal,
|
||||
}
|
||||
if config.get('tabs', 'background-tabs'):
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab
|
||||
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
# corner of the rectangle, this will help if part of the <a> element
|
||||
# is hidden behind other elements
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1005
|
||||
rect = elem.rect_on_view()
|
||||
if rect.width() > rect.height():
|
||||
rect.setWidth(rect.height())
|
||||
else:
|
||||
rect.setHeight(rect.width())
|
||||
pos = rect.center()
|
||||
|
||||
action = "Hovering" if context.target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at position {}".format(
|
||||
action, elem.debug_text(), pos))
|
||||
|
||||
self.start_hinting.emit(target_mapping[context.target])
|
||||
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
|
||||
Target.window]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if context.target != Target.hover:
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
|
||||
if context.target in [Target.normal, Target.current]:
|
||||
# Set the pre-jump mark ', so we can jump back here after following
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.set_mark("'")
|
||||
|
||||
if context.target == Target.current:
|
||||
elem.remove_blank_target()
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
elem.frame().page().triggerAction,
|
||||
QWebPage.MoveToEndOfDocument))
|
||||
QTimer.singleShot(0, self.stop_hinting.emit)
|
||||
|
||||
def _yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
sel = (context.target == Target.yank_primary and
|
||||
utils.supports_selection())
|
||||
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
utils.set_clipboard(urlstr, selection=sel)
|
||||
|
||||
msg = "Yanked URL to {}: {}".format(
|
||||
"primary selection" if sel else "clipboard",
|
||||
urlstr)
|
||||
message.info(self._win_id, msg)
|
||||
|
||||
def _run_cmd(self, url, context):
|
||||
"""Run the command based on a hint URL.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded)
|
||||
args = context.get_args(urlstr)
|
||||
commandrunner = runners.CommandRunner(self._win_id)
|
||||
commandrunner.run_safely(' '.join(args))
|
||||
|
||||
def _preset_cmd_text(self, url, context):
|
||||
"""Preset a commandline text based on a hint URL.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toDisplayString(QUrl.FullyEncoded)
|
||||
args = context.get_args(urlstr)
|
||||
text = ' '.join(args)
|
||||
if text[0] not in modeparsers.STARTCHARS:
|
||||
message.error(self._win_id,
|
||||
"Invalid command text '{}'.".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.set_cmd_text(self._win_id, text)
|
||||
|
||||
def _download(self, elem, context):
|
||||
"""Download a hint URL.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to download.
|
||||
_context: The HintContext to use.
|
||||
"""
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is None:
|
||||
self._show_url_error()
|
||||
return
|
||||
if context.rapid:
|
||||
prompt = False
|
||||
else:
|
||||
prompt = None
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
download_manager.get(url, page=elem.frame().page(),
|
||||
prompt_download_directory=prompt)
|
||||
|
||||
def _call_userscript(self, elem, context):
|
||||
"""Call a userscript from a hint.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to use in the userscript.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
cmd = context.args[0]
|
||||
args = context.args[1:]
|
||||
env = {
|
||||
'QUTE_MODE': 'hints',
|
||||
'QUTE_SELECTED_TEXT': str(elem),
|
||||
'QUTE_SELECTED_HTML': elem.outer_xml(),
|
||||
}
|
||||
url = self._resolve_url(elem, context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
|
||||
try:
|
||||
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
|
||||
env=env)
|
||||
except userscripts.UnsupportedError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
|
||||
def _spawn(self, url, context):
|
||||
"""Spawn a simple command from a hint.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
args = context.get_args(urlstr)
|
||||
commandrunner = runners.CommandRunner(self._win_id)
|
||||
commandrunner.run_safely('spawn ' + ' '.join(args))
|
||||
|
||||
def _resolve_url(self, elem, baseurl):
|
||||
"""Resolve a URL and check if we want to keep it.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to get the URL of.
|
||||
baseurl: The baseurl of the current tab.
|
||||
|
||||
Return:
|
||||
A QUrl with the absolute URL, or None.
|
||||
"""
|
||||
for attr in ['href', 'src']:
|
||||
if attr in elem:
|
||||
text = elem[attr].strip()
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
url = QUrl(text)
|
||||
if not url.isValid():
|
||||
return None
|
||||
if url.isRelative():
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
def _check_args(self, target, *args):
|
||||
"""Check the arguments passed to start() and raise if they're wrong.
|
||||
|
||||
@ -684,7 +707,7 @@ class HintManager(QObject):
|
||||
if elem is None:
|
||||
message.error(self._win_id, "No {} links found!".format(word))
|
||||
return
|
||||
url = self._resolve_url(elem, baseurl)
|
||||
url = _resolve_url(elem, baseurl)
|
||||
if url is None:
|
||||
message.error(self._win_id, "No {} links found!".format(word))
|
||||
return
|
||||
@ -955,38 +978,41 @@ class HintManager(QObject):
|
||||
self.handle_partial_key(keystr)
|
||||
self._context.to_follow = keystr
|
||||
return
|
||||
|
||||
# Handlers which take a QWebElement
|
||||
elem_handlers = {
|
||||
Target.normal: self._click,
|
||||
Target.current: self._click,
|
||||
Target.tab: self._click,
|
||||
Target.tab_fg: self._click,
|
||||
Target.tab_bg: self._click,
|
||||
Target.window: self._click,
|
||||
Target.hover: self._click,
|
||||
Target.normal: self._actions.click,
|
||||
Target.current: self._actions.click,
|
||||
Target.tab: self._actions.click,
|
||||
Target.tab_fg: self._actions.click,
|
||||
Target.tab_bg: self._actions.click,
|
||||
Target.window: self._actions.click,
|
||||
Target.hover: self._actions.click,
|
||||
# _download needs a QWebElement to get the frame.
|
||||
Target.download: self._download,
|
||||
Target.userscript: self._call_userscript,
|
||||
Target.download: self._actions.download,
|
||||
Target.userscript: self._actions.call_userscript,
|
||||
}
|
||||
# Handlers which take a QUrl
|
||||
url_handlers = {
|
||||
Target.yank: self._yank,
|
||||
Target.yank_primary: self._yank,
|
||||
Target.run: self._run_cmd,
|
||||
Target.fill: self._preset_cmd_text,
|
||||
Target.spawn: self._spawn,
|
||||
Target.yank: self._actions.yank,
|
||||
Target.yank_primary: self._actions.yank,
|
||||
Target.run: self._actions.run_cmd,
|
||||
Target.fill: self._actions.preset_cmd_text,
|
||||
Target.spawn: self._actions.spawn,
|
||||
}
|
||||
elem = self._context.elems[keystr].elem
|
||||
|
||||
if elem.frame() is None:
|
||||
message.error(self._win_id,
|
||||
"This element has no webframe.",
|
||||
immediately=True)
|
||||
return
|
||||
|
||||
if self._context.target in elem_handlers:
|
||||
handler = functools.partial(elem_handlers[self._context.target],
|
||||
elem, self._context)
|
||||
elif self._context.target in url_handlers:
|
||||
url = self._resolve_url(elem, self._context.baseurl)
|
||||
url = _resolve_url(elem, self._context.baseurl)
|
||||
if url is None:
|
||||
self._show_url_error()
|
||||
return
|
||||
@ -994,6 +1020,7 @@ class HintManager(QObject):
|
||||
url, self._context)
|
||||
else:
|
||||
raise ValueError("No suitable handler found!")
|
||||
|
||||
if not self._context.rapid:
|
||||
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'followed')
|
||||
@ -1003,7 +1030,11 @@ class HintManager(QObject):
|
||||
# Undo keystring highlighting
|
||||
for string, elem in self._context.elems.items():
|
||||
elem.label.set_inner_xml(string)
|
||||
handler()
|
||||
|
||||
try:
|
||||
handler()
|
||||
except HintingError:
|
||||
self._show_url_error()
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
||||
modes=[usertypes.KeyMode.hint])
|
||||
@ -1086,7 +1117,7 @@ class WordHinter:
|
||||
self.words.update(hints)
|
||||
except IOError as e:
|
||||
error = "Word hints requires reading the file at {}: {}"
|
||||
raise WordHintingError(error.format(dictionary, str(e)))
|
||||
raise HintingError(error.format(dictionary, str(e)))
|
||||
|
||||
def extract_tag_words(self, elem):
|
||||
"""Extract tag words form the given element."""
|
||||
@ -1155,7 +1186,7 @@ class WordHinter:
|
||||
for elem in elems:
|
||||
hint = self.new_hint_for(elem, used_hints, words)
|
||||
if not hint:
|
||||
raise WordHintingError("Not enough words in the dictionary.")
|
||||
raise HintingError("Not enough words in the dictionary.")
|
||||
used_hints.add(hint)
|
||||
hints.append(hint)
|
||||
return hints
|
||||
|
Loading…
Reference in New Issue
Block a user