mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: SFTP 支持不显示隐藏文件
This commit is contained in:
@@ -8,6 +8,8 @@ object Icons {
|
|||||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||||
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
val close by lazy { DynamicIcon("icons/close.svg", "icons/close_dark.svg") }
|
||||||
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
val searchHistory by lazy { DynamicIcon("icons/searchHistory.svg", "icons/searchHistory_dark.svg") }
|
||||||
|
val eye by lazy { DynamicIcon("icons/eye.svg", "icons/eye_dark.svg") }
|
||||||
|
val eyeClose by lazy { DynamicIcon("icons/eyeClose.svg", "icons/eyeClose_dark.svg") }
|
||||||
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
val matchCase by lazy { DynamicIcon("icons/matchCase.svg", "icons/matchCase_dark.svg") }
|
||||||
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
val regex by lazy { DynamicIcon("icons/regex.svg", "icons/regex_dark.svg") }
|
||||||
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
val vcs by lazy { DynamicIcon("icons/vcs.svg", "icons/vcs_dark.svg") }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
|||||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||||
import com.formdev.flatlaf.icons.FlatFileViewDirectoryIcon
|
import com.formdev.flatlaf.icons.FlatFileViewDirectoryIcon
|
||||||
import com.formdev.flatlaf.icons.FlatFileViewFileIcon
|
import com.formdev.flatlaf.icons.FlatFileViewFileIcon
|
||||||
|
import com.formdev.flatlaf.ui.FlatTableUI
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
@@ -42,8 +43,7 @@ class FileSystemPanel(
|
|||||||
private val fileSystem: FileSystem,
|
private val fileSystem: FileSystem,
|
||||||
private val transportManager: TransportManager,
|
private val transportManager: TransportManager,
|
||||||
private val host: Host
|
private val host: Host
|
||||||
) : JPanel(BorderLayout()), Disposable,
|
) : JPanel(BorderLayout()), Disposable, FileSystemTransportListener.Provider {
|
||||||
FileSystemTransportListener.Provider {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(FileSystemPanel::class.java)
|
private val log = LoggerFactory.getLogger(FileSystemPanel::class.java)
|
||||||
@@ -58,6 +58,10 @@ class FileSystemPanel(
|
|||||||
private val loadingPanel = LoadingPanel()
|
private val loadingPanel = LoadingPanel()
|
||||||
private val bookmarkBtn = BookmarkButton()
|
private val bookmarkBtn = BookmarkButton()
|
||||||
private val homeBtn = JButton(Icons.homeFolder)
|
private val homeBtn = JButton(Icons.homeFolder)
|
||||||
|
private val showHiddenFilesBtn = JButton(Icons.eyeClose)
|
||||||
|
private val properties get() = Database.getDatabase().properties
|
||||||
|
private var isShowHiddenFiles = false
|
||||||
|
private val showHiddenFilesKey by lazy { "termora.transport.host.${host.id}.show-hidden-files" }
|
||||||
|
|
||||||
val workdir get() = tableModel.workdir
|
val workdir get() = tableModel.workdir
|
||||||
|
|
||||||
@@ -72,6 +76,8 @@ class FileSystemPanel(
|
|||||||
bookmarkBtn.name = "Host.${host.id}.Bookmarks"
|
bookmarkBtn.name = "Host.${host.id}.Bookmarks"
|
||||||
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString())
|
bookmarkBtn.isBookmark = bookmarkBtn.getBookmarks().contains(workdir.toString())
|
||||||
|
|
||||||
|
table.setUI(FlatTableUI())
|
||||||
|
table.rowHeight = UIManager.getInt("Table.rowHeight")
|
||||||
table.autoResizeMode = JTable.AUTO_RESIZE_OFF
|
table.autoResizeMode = JTable.AUTO_RESIZE_OFF
|
||||||
table.fillsViewportHeight = true
|
table.fillsViewportHeight = true
|
||||||
table.putClientProperty(
|
table.putClientProperty(
|
||||||
@@ -121,6 +127,16 @@ class FileSystemPanel(
|
|||||||
})
|
})
|
||||||
|
|
||||||
parentBtn.toolTipText = I18n.getString("termora.transport.parent-folder")
|
parentBtn.toolTipText = I18n.getString("termora.transport.parent-folder")
|
||||||
|
showHiddenFilesBtn.toolTipText = I18n.getString("termora.transport.show-hidden-files")
|
||||||
|
|
||||||
|
if (properties.getString(showHiddenFilesKey, "true").toBoolean()) {
|
||||||
|
showHiddenFilesBtn.icon = Icons.eye
|
||||||
|
tableModel.isShowHiddenFiles = true
|
||||||
|
} else {
|
||||||
|
showHiddenFilesBtn.icon = Icons.eyeClose
|
||||||
|
properties.putString(showHiddenFilesKey, "true")
|
||||||
|
tableModel.isShowHiddenFiles = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val toolbar = FlatToolBar()
|
val toolbar = FlatToolBar()
|
||||||
@@ -128,6 +144,7 @@ class FileSystemPanel(
|
|||||||
toolbar.add(Box.createHorizontalStrut(2))
|
toolbar.add(Box.createHorizontalStrut(2))
|
||||||
toolbar.add(workdirTextField)
|
toolbar.add(workdirTextField)
|
||||||
toolbar.add(bookmarkBtn)
|
toolbar.add(bookmarkBtn)
|
||||||
|
toolbar.add(showHiddenFilesBtn)
|
||||||
toolbar.add(parentBtn)
|
toolbar.add(parentBtn)
|
||||||
toolbar.add(JButton(Icons.refresh).apply {
|
toolbar.add(JButton(Icons.refresh).apply {
|
||||||
addActionListener { reload() }
|
addActionListener { reload() }
|
||||||
@@ -290,6 +307,21 @@ class FileSystemPanel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showHiddenFilesBtn.addActionListener {
|
||||||
|
val showHiddenFiles = tableModel.isShowHiddenFiles
|
||||||
|
tableModel.isShowHiddenFiles = !showHiddenFiles
|
||||||
|
if (tableModel.isShowHiddenFiles) {
|
||||||
|
showHiddenFilesBtn.icon = Icons.eye
|
||||||
|
} else {
|
||||||
|
showHiddenFilesBtn.icon = Icons.eyeClose
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Disposer.register(this, object : Disposable {
|
||||||
|
override fun dispose() {
|
||||||
|
properties.putString(showHiddenFilesKey, "${tableModel.isShowHiddenFiles}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,13 +45,18 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
private val propertyChangeListeners = mutableListOf<PropertyChangeListener>()
|
private val propertyChangeListeners = mutableListOf<PropertyChangeListener>()
|
||||||
|
|
||||||
val isLocalFileSystem by lazy { FileSystems.getDefault() == fileSystem }
|
val isLocalFileSystem by lazy { FileSystems.getDefault() == fileSystem }
|
||||||
|
var isShowHiddenFiles = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
fireTableDataChanged()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getRowCount(): Int {
|
override fun getRowCount(): Int {
|
||||||
return files?.size ?: 0
|
return getShownFiles().size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValueAt(row: Int, column: Int): Any {
|
override fun getValueAt(row: Int, column: Int): Any {
|
||||||
val path = files?.get(row) ?: return StringUtils.EMPTY
|
val path = getShownFiles()[row]
|
||||||
|
|
||||||
if (path.fileName == ".." && column != 0) {
|
if (path.fileName == ".." && column != 0) {
|
||||||
return StringUtils.EMPTY
|
return StringUtils.EMPTY
|
||||||
@@ -98,7 +103,7 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCacheablePath(index: Int): CacheablePath {
|
fun getCacheablePath(index: Int): CacheablePath {
|
||||||
return files?.get(index) ?: throw IndexOutOfBoundsException()
|
return getShownFiles()[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isCellEditable(row: Int, column: Int): Boolean {
|
override fun isCellEditable(row: Int, column: Int): Boolean {
|
||||||
@@ -106,7 +111,8 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun removeRow(row: Int) {
|
override fun removeRow(row: Int) {
|
||||||
files?.removeAt(row) ?: return
|
val e = getShownFiles()[row]
|
||||||
|
files?.removeIf { it == e }
|
||||||
fireTableRowsDeleted(row, row)
|
fireTableRowsDeleted(row, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +161,19 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
propertyChangeListeners.add(propertyChangeListener)
|
propertyChangeListeners.add(propertyChangeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getShownFiles(): List<CacheablePath> {
|
||||||
|
if (isShowHiddenFiles) {
|
||||||
|
return files ?: emptyList()
|
||||||
|
}
|
||||||
|
return files?.filter { !it.isHidden } ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
open class CacheablePath(val path: Path) {
|
open class CacheablePath(val path: Path) {
|
||||||
val fileName by lazy { path.fileName.toString() }
|
val fileName by lazy { path.fileName.toString() }
|
||||||
val extension by lazy { path.extension }
|
val extension by lazy { path.extension }
|
||||||
|
|
||||||
open val isDirectory by lazy { path.isDirectory() }
|
open val isDirectory by lazy { path.isDirectory() }
|
||||||
|
open val isHidden by lazy { fileName != ".." && path.isHidden() }
|
||||||
open val fileSize by lazy { path.fileSize() }
|
open val fileSize by lazy { path.fileSize() }
|
||||||
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
|
open val lastModifiedTime by lazy { Files.getLastModifiedTime(path).toMillis() }
|
||||||
open val owner by lazy { path.getOwner().toString() }
|
open val owner by lazy { path.getOwner().toString() }
|
||||||
@@ -216,6 +230,9 @@ class FileSystemTableModel(private val fileSystem: FileSystem) : DefaultTableMod
|
|||||||
override val isDirectory: Boolean
|
override val isDirectory: Boolean
|
||||||
get() = attributes.isDirectory
|
get() = attributes.isDirectory
|
||||||
|
|
||||||
|
override val isHidden: Boolean
|
||||||
|
get() = fileName != ".." && fileName.startsWith(".")
|
||||||
|
|
||||||
override val fileSize: Long
|
override val fileSize: Long
|
||||||
get() = attributes.size
|
get() = attributes.size
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ termora.tools.multiple=Send commands to multiple sessions
|
|||||||
# Transport
|
# Transport
|
||||||
termora.transport.local=Local
|
termora.transport.local=Local
|
||||||
termora.transport.parent-folder=Parent Folder
|
termora.transport.parent-folder=Parent Folder
|
||||||
|
termora.transport.show-hidden-files=Show hidden files
|
||||||
termora.transport.file-already-exists=The file {0} already exists
|
termora.transport.file-already-exists=The file {0} already exists
|
||||||
|
|
||||||
termora.transport.bookmarks=Bookmarks Manager
|
termora.transport.bookmarks=Bookmarks Manager
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ termora.macro.run=运行
|
|||||||
# Transport
|
# Transport
|
||||||
termora.transport.local=本机
|
termora.transport.local=本机
|
||||||
termora.transport.parent-folder=父文件夹
|
termora.transport.parent-folder=父文件夹
|
||||||
|
termora.transport.show-hidden-files=显示隐藏文件
|
||||||
termora.transport.file-already-exists=文件 {0} 已存在
|
termora.transport.file-already-exists=文件 {0} 已存在
|
||||||
|
|
||||||
termora.transport.bookmarks=书签管理
|
termora.transport.bookmarks=书签管理
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ termora.macro.run=運行
|
|||||||
# Transport
|
# Transport
|
||||||
termora.transport.local=本機
|
termora.transport.local=本機
|
||||||
termora.transport.parent-folder=父資料夾
|
termora.transport.parent-folder=父資料夾
|
||||||
|
termora.transport.show-hidden-files=顯示隱藏文件
|
||||||
termora.transport.file-already-exists=檔案 {0} 已存在
|
termora.transport.file-already-exists=檔案 {0} 已存在
|
||||||
|
|
||||||
termora.transport.bookmarks=書籤管理
|
termora.transport.bookmarks=書籤管理
|
||||||
|
|||||||
7
src/main/resources/icons/eye.svg
Normal file
7
src/main/resources/icons/eye.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg t="1736928517310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="877"
|
||||||
|
width="16" height="16">
|
||||||
|
<path d="M1001.472 482.64533333C893.61066667 255.43111111 730.56711111 141.08444445 512 141.08444445c-218.68088889 0-381.61066667 114.34666667-489.472 341.67466666-8.76088889 18.432-8.76088889 40.04977778 0 58.59555556C130.38933333 768.56888889 293.43288889 882.91555555 512 882.91555555c218.68088889 0 381.61066667-114.34666667 489.472-341.67466666 8.76088889-18.432 8.76088889-39.82222222 0-58.59555556zM512 800.99555555c-183.52355555 0-317.89511111-93.07022222-412.672-288.99555555C194.10488889 316.07466667 328.47644445 223.00444445 512 223.00444445c183.52355555 0 317.89511111 93.07022222 412.672 288.99555555C830.00888889 707.92533333 695.63733333 800.99555555 512 800.99555555z"
|
||||||
|
p-id="878" fill="#6C707E"></path>
|
||||||
|
<path d="M507.73333333 324.26666667c-103.68 0-187.73333333 84.05333333-187.73333333 187.73333333s84.05333333 187.73333333 187.73333333 187.73333333 187.73333333-84.05333333 187.73333334-187.73333333-84.05333333-187.73333333-187.73333334-187.73333333z m0 307.2c-66.02666667 0-119.46666667-53.44-119.46666666-119.46666667s53.44-119.46666667 119.46666666-119.46666667 119.46666667 53.44 119.46666667 119.46666667-53.44 119.46666667-119.46666667 119.46666667z"
|
||||||
|
p-id="879" fill="#6C707E"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/main/resources/icons/eyeClose.svg
Normal file
1
src/main/resources/icons/eyeClose.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1736928586708" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="907" width="16" height="16"><path d="M1001.58577778 482.87288889l-0.11377778-0.11377778-0.11377778-0.11377778c-41.41511111-87.26755555-91.02222222-157.80977778-148.70755555-211.62666666L794.96533333 328.81777778c49.72088889 45.73866667 92.72888889 106.60977778 129.82044445 183.06844444C830.00888889 707.92533333 695.63733333 800.99555555 512 800.99555555c-58.368 0-111.84355555-9.44355555-160.65422222-28.55822222l-62.23644445 62.23644445C355.66933333 866.75911111 429.85244445 882.91555555 512 882.91555555c218.68088889 0 381.61066667-114.34666667 489.472-341.67466666 8.76088889-18.432 8.76088889-39.82222222 0.11377778-58.368zM928.768 104.90311111l-48.24177778-48.24177778c-3.52711111-3.52711111-9.32977778-3.52711111-12.85688889 0L734.77688889 189.44C668.33066667 157.24088889 594.14755555 141.08444445 512 141.08444445c-218.68088889 0-381.61066667 114.34666667-489.472 341.67466666v0.11377778c-8.76088889 18.432-8.76088889 40.04977778 0 58.59555556 41.41511111 87.26755555 91.02222222 157.80977778 148.70755555 211.74044444L56.66133333 867.55555555c-3.52711111 3.52711111-3.52711111 9.32977778 0 12.8568889l48.24177778 48.24177777c3.52711111 3.52711111 9.32977778 3.52711111 12.85688889 0l811.008-811.008c3.52711111-3.41333333 3.52711111-9.216 0-12.74311111zM383.31733333 540.89955555c-2.16177778-9.32977778-3.29955555-19.00088889-3.29955555-28.89955555 0-70.42844445 57.00266667-127.43111111 127.43111111-127.43111111 9.89866667 0 19.68355555 1.13777778 28.89955556 3.29955556L383.31733333 540.89955555z m209.92-209.92C567.18222222 318.69155555 538.16888889 311.75111111 507.44888889 311.75111111c-110.592 0-200.24888889 89.65688889-200.24888889 200.24888889 0 30.72 6.94044445 59.73333333 19.22844445 85.78844445L229.03466667 695.18222222c-49.72088889-45.73866667-92.72888889-106.60977778-129.82044445-183.06844444C194.10488889 316.07466667 328.47644445 223.00444445 512 223.00444445c58.368 0 111.84355555 9.44355555 160.65422222 28.55822222l-79.41688889 79.41688888z" p-id="908" fill="#6C707E"></path><path d="M507.44888889 639.43111111c-7.28177778 0-14.44977778-0.56888889-21.39022222-1.82044444l-58.14044445 58.14044444c24.34844445 10.58133333 51.31377778 16.384 79.53066667 16.384 110.592 0 200.24888889-89.65688889 200.24888889-200.24888889 0-28.21688889-5.80266667-55.18222222-16.384-79.53066667l-58.14044445 58.14044445c1.13777778 6.94044445 1.82044445 14.10844445 1.82044445 21.39022222C634.88 582.42844445 577.87733333 639.43111111 507.44888889 639.43111111z" p-id="909" fill="#6C707E"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
7
src/main/resources/icons/eyeClose_dark.svg
Normal file
7
src/main/resources/icons/eyeClose_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg t="1736928517310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="877"
|
||||||
|
width="16" height="16">
|
||||||
|
<path d="M1001.472 482.64533333C893.61066667 255.43111111 730.56711111 141.08444445 512 141.08444445c-218.68088889 0-381.61066667 114.34666667-489.472 341.67466666-8.76088889 18.432-8.76088889 40.04977778 0 58.59555556C130.38933333 768.56888889 293.43288889 882.91555555 512 882.91555555c218.68088889 0 381.61066667-114.34666667 489.472-341.67466666 8.76088889-18.432 8.76088889-39.82222222 0-58.59555556zM512 800.99555555c-183.52355555 0-317.89511111-93.07022222-412.672-288.99555555C194.10488889 316.07466667 328.47644445 223.00444445 512 223.00444445c183.52355555 0 317.89511111 93.07022222 412.672 288.99555555C830.00888889 707.92533333 695.63733333 800.99555555 512 800.99555555z"
|
||||||
|
p-id="878" fill="#CED0D6"></path>
|
||||||
|
<path d="M507.73333333 324.26666667c-103.68 0-187.73333333 84.05333333-187.73333333 187.73333333s84.05333333 187.73333333 187.73333333 187.73333333 187.73333333-84.05333333 187.73333334-187.73333333-84.05333333-187.73333333-187.73333334-187.73333333z m0 307.2c-66.02666667 0-119.46666667-53.44-119.46666666-119.46666667s53.44-119.46666667 119.46666666-119.46666667 119.46666667 53.44 119.46666667 119.46666667-53.44 119.46666667-119.46666667 119.46666667z"
|
||||||
|
p-id="879" fill="#CED0D6"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
7
src/main/resources/icons/eye_dark.svg
Normal file
7
src/main/resources/icons/eye_dark.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg t="1736928517310" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="877"
|
||||||
|
width="16" height="16">
|
||||||
|
<path d="M1001.472 482.64533333C893.61066667 255.43111111 730.56711111 141.08444445 512 141.08444445c-218.68088889 0-381.61066667 114.34666667-489.472 341.67466666-8.76088889 18.432-8.76088889 40.04977778 0 58.59555556C130.38933333 768.56888889 293.43288889 882.91555555 512 882.91555555c218.68088889 0 381.61066667-114.34666667 489.472-341.67466666 8.76088889-18.432 8.76088889-39.82222222 0-58.59555556zM512 800.99555555c-183.52355555 0-317.89511111-93.07022222-412.672-288.99555555C194.10488889 316.07466667 328.47644445 223.00444445 512 223.00444445c183.52355555 0 317.89511111 93.07022222 412.672 288.99555555C830.00888889 707.92533333 695.63733333 800.99555555 512 800.99555555z"
|
||||||
|
p-id="878" fill="#CED0D6"></path>
|
||||||
|
<path d="M507.73333333 324.26666667c-103.68 0-187.73333333 84.05333333-187.73333333 187.73333333s84.05333333 187.73333333 187.73333333 187.73333333 187.73333333-84.05333333 187.73333334-187.73333333-84.05333333-187.73333333-187.73333334-187.73333333z m0 307.2c-66.02666667 0-119.46666667-53.44-119.46666666-119.46666667s53.44-119.46666667 119.46666666-119.46666667 119.46666667 53.44 119.46666667 119.46666667-53.44 119.46666667-119.46666667 119.46666667z"
|
||||||
|
p-id="879" fill="#CED0D6"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user