mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-30 16:32:50 -05:00
feat: Pinnable DMS coreApps w/Color options
This commit is contained in:
@@ -76,8 +76,6 @@ FocusScope {
|
|||||||
function showContextMenu(item, x, y, fromKeyboard) {
|
function showContextMenu(item, x, y, fromKeyboard) {
|
||||||
if (!item)
|
if (!item)
|
||||||
return;
|
return;
|
||||||
if (item.isCore)
|
|
||||||
return;
|
|
||||||
if (!contextMenu.hasContextMenuActions(item))
|
if (!contextMenu.hasContextMenuActions(item))
|
||||||
return;
|
return;
|
||||||
contextMenu.show(x, y, item, fromKeyboard);
|
contextMenu.show(x, y, item, fromKeyboard);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Popup {
|
|||||||
function hasContextMenuActions(spotlightItem) {
|
function hasContextMenuActions(spotlightItem) {
|
||||||
if (!spotlightItem)
|
if (!spotlightItem)
|
||||||
return false;
|
return false;
|
||||||
if (spotlightItem.type === "app" && !spotlightItem.isCore)
|
if (spotlightItem.type === "app")
|
||||||
return true;
|
return true;
|
||||||
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
|
||||||
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
|
||||||
@@ -34,9 +34,16 @@ Popup {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var desktopEntry: item?.data ?? null
|
readonly property bool isCoreApp: item?.type === "app" && item?.isCore
|
||||||
readonly property string appId: desktopEntry?.id || desktopEntry?.execString || ""
|
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
|
||||||
readonly property bool isPinned: SessionData.isPinnedApp(appId)
|
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
|
||||||
|
readonly property string appId: {
|
||||||
|
if (isCoreApp) {
|
||||||
|
return item?.id || coreAppData?.builtInPluginId || "";
|
||||||
|
}
|
||||||
|
return desktopEntry?.id || desktopEntry?.execString || "";
|
||||||
|
}
|
||||||
|
readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
|
||||||
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
|
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
|
||||||
readonly property bool isPluginItem: item?.type === "plugin"
|
readonly property bool isPluginItem: item?.type === "plugin"
|
||||||
|
|
||||||
@@ -82,15 +89,14 @@ Popup {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!desktopEntry)
|
if (item?.type === "app") {
|
||||||
return items;
|
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
type: "item",
|
type: "item",
|
||||||
icon: isPinned ? "keep_off" : "push_pin",
|
icon: isPinned ? "keep_off" : "push_pin",
|
||||||
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
|
||||||
action: togglePin
|
action: togglePin
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isRegularApp) {
|
if (isRegularApp) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -200,6 +206,14 @@ Popup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function launchApp() {
|
function launchApp() {
|
||||||
|
if (isCoreApp) {
|
||||||
|
if (!coreAppData)
|
||||||
|
return;
|
||||||
|
AppSearchService.executeCoreApp(coreAppData);
|
||||||
|
controller?.itemExecuted();
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!desktopEntry)
|
if (!desktopEntry)
|
||||||
return;
|
return;
|
||||||
SessionService.launchDesktopEntry(desktopEntry);
|
SessionService.launchDesktopEntry(desktopEntry);
|
||||||
|
|||||||
@@ -29,12 +29,29 @@ Item {
|
|||||||
property bool showTooltip: mouseArea.containsMouse && !dragging
|
property bool showTooltip: mouseArea.containsMouse && !dragging
|
||||||
property var cachedDesktopEntry: null
|
property var cachedDesktopEntry: null
|
||||||
property real actualIconSize: 40
|
property real actualIconSize: 40
|
||||||
|
readonly property string coreIconColorOverride: SettingsData.dockLauncherLogoColorOverride
|
||||||
|
readonly property bool coreIconHasCustomColor: coreIconColorOverride !== "" && coreIconColorOverride !== "primary" && coreIconColorOverride !== "surface"
|
||||||
|
readonly property color effectiveCoreIconColor: {
|
||||||
|
if (coreIconColorOverride === "primary")
|
||||||
|
return Theme.primary;
|
||||||
|
if (coreIconColorOverride === "surface")
|
||||||
|
return Theme.surfaceText;
|
||||||
|
if (coreIconColorOverride !== "")
|
||||||
|
return coreIconColorOverride;
|
||||||
|
return Theme.surfaceText;
|
||||||
|
}
|
||||||
|
readonly property real effectiveCoreIconBrightness: coreIconHasCustomColor ? SettingsData.dockLauncherLogoBrightness : 0.0
|
||||||
|
readonly property real effectiveCoreIconContrast: coreIconHasCustomColor ? SettingsData.dockLauncherLogoContrast : 0.0
|
||||||
|
|
||||||
function updateDesktopEntry() {
|
function updateDesktopEntry() {
|
||||||
if (!appData || appData.appId === "__SEPARATOR__") {
|
if (!appData || appData.appId === "__SEPARATOR__") {
|
||||||
cachedDesktopEntry = null;
|
cachedDesktopEntry = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (appData.isCoreApp) {
|
||||||
|
cachedDesktopEntry = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const moddedId = Paths.moddedAppId(appData.appId);
|
const moddedId = Paths.moddedAppId(appData.appId);
|
||||||
cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||||
}
|
}
|
||||||
@@ -85,7 +102,12 @@ Item {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
let appName;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
appName = appData.coreAppData.name || appData.appId;
|
||||||
|
} else {
|
||||||
|
appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
||||||
|
}
|
||||||
|
|
||||||
if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
|
if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
|
||||||
const title = appData.type === "window" ? windowTitle : appData.windowTitle;
|
const title = appData.type === "window" ? windowTitle : appData.windowTitle;
|
||||||
@@ -227,6 +249,10 @@ Item {
|
|||||||
case "pinned":
|
case "pinned":
|
||||||
if (!appData.appId)
|
if (!appData.appId)
|
||||||
return;
|
return;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
AppSearchService.executeCoreApp(appData.coreAppData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pinnedEntry = cachedDesktopEntry;
|
const pinnedEntry = cachedDesktopEntry;
|
||||||
if (pinnedEntry) {
|
if (pinnedEntry) {
|
||||||
AppUsageHistoryData.addAppUsage({
|
AppUsageHistoryData.addAppUsage({
|
||||||
@@ -248,6 +274,10 @@ Item {
|
|||||||
if (appData.windowCount === 0) {
|
if (appData.windowCount === 0) {
|
||||||
if (!appData.appId)
|
if (!appData.appId)
|
||||||
return;
|
return;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
AppSearchService.executeCoreApp(appData.coreAppData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const groupedEntry = cachedDesktopEntry;
|
const groupedEntry = cachedDesktopEntry;
|
||||||
if (groupedEntry) {
|
if (groupedEntry) {
|
||||||
AppUsageHistoryData.addAppUsage({
|
AppUsageHistoryData.addAppUsage({
|
||||||
@@ -374,6 +404,19 @@ Item {
|
|||||||
z: -1
|
z: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
id: coreIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
iconSize: actualIconSize
|
||||||
|
iconValue: appData && appData.isCoreApp && appData.coreAppData ? (appData.coreAppData.icon || "") : ""
|
||||||
|
colorOverride: effectiveCoreIconColor
|
||||||
|
brightnessOverride: effectiveCoreIconBrightness
|
||||||
|
contrastOverride: effectiveCoreIconContrast
|
||||||
|
fallbackText: "?"
|
||||||
|
visible: iconValue !== ""
|
||||||
|
}
|
||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
id: iconImg
|
id: iconImg
|
||||||
|
|
||||||
@@ -383,12 +426,15 @@ Item {
|
|||||||
if (!appData || appData.appId === "__SEPARATOR__") {
|
if (!appData || appData.appId === "__SEPARATOR__") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
return Paths.getAppIcon(appData.appId, cachedDesktopEntry);
|
return Paths.getAppIcon(appData.appId, cachedDesktopEntry);
|
||||||
}
|
}
|
||||||
mipmap: true
|
mipmap: true
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready && !coreIcon.visible
|
||||||
layer.enabled: appData && appData.appId === "org.quickshell"
|
layer.enabled: appData && appData.appId === "org.quickshell"
|
||||||
layer.smooth: true
|
layer.smooth: true
|
||||||
layer.mipmap: true
|
layer.mipmap: true
|
||||||
@@ -403,7 +449,7 @@ Item {
|
|||||||
width: actualIconSize
|
width: actualIconSize
|
||||||
height: actualIconSize
|
height: actualIconSize
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: iconImg.status !== Image.Ready && appData && appData.appId && !Paths.isSteamApp(appData.appId)
|
visible: !coreIcon.visible && iconImg.status !== Image.Ready && appData && appData.appId && !Paths.isSteamApp(appData.appId)
|
||||||
color: Theme.surfaceLight
|
color: Theme.surfaceLight
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 1
|
border.width: 1
|
||||||
@@ -416,7 +462,12 @@ Item {
|
|||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
let appName;
|
||||||
|
if (appData.isCoreApp && appData.coreAppData) {
|
||||||
|
appName = appData.coreAppData.name || appData.appId;
|
||||||
|
} else {
|
||||||
|
appName = Paths.getAppName(appData.appId, cachedDesktopEntry);
|
||||||
|
}
|
||||||
return appName.charAt(0).toUpperCase();
|
return appName.charAt(0).toUpperCase();
|
||||||
}
|
}
|
||||||
font.pixelSize: Math.max(8, parent.width * 0.35)
|
font.pixelSize: Math.max(8, parent.width * 0.35)
|
||||||
@@ -430,7 +481,7 @@ Item {
|
|||||||
size: actualIconSize
|
size: actualIconSize
|
||||||
name: "sports_esports"
|
name: "sports_esports"
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
visible: iconImg.status !== Image.Ready && appData && appData.appId && Paths.isSteamApp(appData.appId)
|
visible: !coreIcon.visible && iconImg.status !== Image.Ready && appData && appData.appId && Paths.isSteamApp(appData.appId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
|
|||||||
@@ -91,6 +91,34 @@ Item {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCoreAppData(appId) {
|
||||||
|
if (typeof AppSearchService === "undefined")
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const coreApps = AppSearchService.coreApps || [];
|
||||||
|
for (let i = 0; i < coreApps.length; i++) {
|
||||||
|
const app = coreApps[i];
|
||||||
|
if (app.builtInPluginId === appId) {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoreAppDataByTitle(windowTitle) {
|
||||||
|
if (typeof AppSearchService === "undefined" || !windowTitle)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const coreApps = AppSearchService.coreApps || [];
|
||||||
|
for (let i = 0; i < coreApps.length; i++) {
|
||||||
|
const app = coreApps[i];
|
||||||
|
if (app.name === windowTitle) {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function insertLauncher(targetArray) {
|
function insertLauncher(targetArray) {
|
||||||
if (!SettingsData.dockLauncherEnabled)
|
if (!SettingsData.dockLauncherEnabled)
|
||||||
return;
|
return;
|
||||||
@@ -119,21 +147,35 @@ Item {
|
|||||||
|
|
||||||
pinnedApps.forEach(rawAppId => {
|
pinnedApps.forEach(rawAppId => {
|
||||||
const appId = Paths.moddedAppId(rawAppId);
|
const appId = Paths.moddedAppId(rawAppId);
|
||||||
|
const coreAppData = getCoreAppData(appId);
|
||||||
appGroups.set(appId, {
|
appGroups.set(appId, {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
windows: []
|
windows: [],
|
||||||
|
isCoreApp: coreAppData !== null,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
sortedToplevels.forEach((toplevel, index) => {
|
sortedToplevels.forEach((toplevel, index) => {
|
||||||
const rawAppId = toplevel.appId || "unknown";
|
const rawAppId = toplevel.appId || "unknown";
|
||||||
const appId = Paths.moddedAppId(rawAppId);
|
let appId = Paths.moddedAppId(rawAppId);
|
||||||
|
|
||||||
|
let coreAppData = null;
|
||||||
|
if (rawAppId === "org.quickshell") {
|
||||||
|
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||||
|
if (coreAppData) {
|
||||||
|
appId = coreAppData.builtInPluginId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!appGroups.has(appId)) {
|
if (!appGroups.has(appId)) {
|
||||||
appGroups.set(appId, {
|
appGroups.set(appId, {
|
||||||
appId: appId,
|
appId: appId,
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
windows: []
|
windows: [],
|
||||||
|
isCoreApp: coreAppData !== null,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +199,9 @@ Item {
|
|||||||
isPinned: group.isPinned,
|
isPinned: group.isPinned,
|
||||||
isRunning: group.windows.length > 0,
|
isRunning: group.windows.length > 0,
|
||||||
windowCount: group.windows.length,
|
windowCount: group.windows.length,
|
||||||
allWindows: group.windows
|
allWindows: group.windows,
|
||||||
|
isCoreApp: group.isCoreApp || false,
|
||||||
|
coreAppData: group.coreAppData || null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (group.isPinned) {
|
if (group.isPinned) {
|
||||||
@@ -187,13 +231,16 @@ Item {
|
|||||||
} else {
|
} else {
|
||||||
pinnedApps.forEach(rawAppId => {
|
pinnedApps.forEach(rawAppId => {
|
||||||
const appId = Paths.moddedAppId(rawAppId);
|
const appId = Paths.moddedAppId(rawAppId);
|
||||||
|
const coreAppData = getCoreAppData(appId);
|
||||||
items.push({
|
items.push({
|
||||||
uniqueKey: "pinned_" + appId,
|
uniqueKey: "pinned_" + appId,
|
||||||
type: "pinned",
|
type: "pinned",
|
||||||
appId: appId,
|
appId: appId,
|
||||||
toplevel: null,
|
toplevel: null,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
isRunning: false
|
isRunning: false,
|
||||||
|
isCoreApp: coreAppData !== null,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,13 +271,31 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawAppId = toplevel.appId || "unknown";
|
||||||
|
const moddedAppId = Paths.moddedAppId(rawAppId);
|
||||||
|
|
||||||
|
// Check if this is a core app window (e.g., Settings modal with appId "org.quickshell")
|
||||||
|
let coreAppData = null;
|
||||||
|
let isCoreApp = false;
|
||||||
|
if (rawAppId === "org.quickshell") {
|
||||||
|
coreAppData = getCoreAppDataByTitle(toplevel.title);
|
||||||
|
if (coreAppData) {
|
||||||
|
isCoreApp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalAppId = isCoreApp ? coreAppData.builtInPluginId : moddedAppId;
|
||||||
|
const isPinned = pinnedApps.indexOf(finalAppId) !== -1;
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
uniqueKey: uniqueKey,
|
uniqueKey: uniqueKey,
|
||||||
type: "window",
|
type: "window",
|
||||||
appId: Paths.moddedAppId(toplevel.appId),
|
appId: finalAppId,
|
||||||
toplevel: toplevel,
|
toplevel: toplevel,
|
||||||
isPinned: false,
|
isPinned: isPinned,
|
||||||
isRunning: true
|
isRunning: true,
|
||||||
|
isCoreApp: isCoreApp,
|
||||||
|
coreAppData: coreAppData
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ Item {
|
|||||||
required property int iconSize
|
required property int iconSize
|
||||||
property string fallbackText: "A"
|
property string fallbackText: "A"
|
||||||
property color iconColor: Theme.surfaceText
|
property color iconColor: Theme.surfaceText
|
||||||
|
property color colorOverride: "transparent"
|
||||||
|
property real brightnessOverride: 0.0
|
||||||
|
property real contrastOverride: 0.0
|
||||||
|
property real saturationOverride: 0.0
|
||||||
property color fallbackBackgroundColor: Theme.surfaceLight
|
property color fallbackBackgroundColor: Theme.surfaceLight
|
||||||
property color fallbackTextColor: Theme.primary
|
property color fallbackTextColor: Theme.primary
|
||||||
property real materialIconSizeAdjustment: Theme.spacingM
|
property real materialIconSizeAdjustment: Theme.spacingM
|
||||||
@@ -27,6 +31,7 @@ Item {
|
|||||||
readonly property bool isSvgCorner: iconValue.startsWith("svg+corner:")
|
readonly property bool isSvgCorner: iconValue.startsWith("svg+corner:")
|
||||||
readonly property bool isSvg: !isSvgCorner && iconValue.startsWith("svg:")
|
readonly property bool isSvg: !isSvgCorner && iconValue.startsWith("svg:")
|
||||||
readonly property bool isImage: iconValue.startsWith("image:")
|
readonly property bool isImage: iconValue.startsWith("image:")
|
||||||
|
readonly property bool hasColorOverride: colorOverride.a > 0
|
||||||
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
||||||
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
||||||
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
|
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
|
||||||
@@ -48,7 +53,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: root.materialName
|
name: root.materialName
|
||||||
size: root.iconSize - root.materialIconSizeAdjustment
|
size: root.iconSize - root.materialIconSizeAdjustment
|
||||||
color: root.iconColor
|
color: root.hasColorOverride ? root.colorOverride : root.iconColor
|
||||||
visible: root.isMaterial
|
visible: root.isMaterial
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +61,7 @@ Item {
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: root.unicodeChar
|
text: root.unicodeChar
|
||||||
font.pixelSize: root.iconSize * root.unicodeIconScale
|
font.pixelSize: root.iconSize * root.unicodeIconScale
|
||||||
color: root.iconColor
|
color: root.hasColorOverride ? root.colorOverride : root.iconColor
|
||||||
visible: root.isUnicode
|
visible: root.isUnicode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +70,10 @@ Item {
|
|||||||
source: root.svgSource
|
source: root.svgSource
|
||||||
size: root.iconSize
|
size: root.iconSize
|
||||||
cornerIcon: root.svgCornerIcon
|
cornerIcon: root.svgCornerIcon
|
||||||
|
colorOverride: root.colorOverride
|
||||||
|
brightnessOverride: root.brightnessOverride
|
||||||
|
contrastOverride: root.contrastOverride
|
||||||
|
saturationOverride: root.saturationOverride
|
||||||
visible: root.isSvg || root.isSvgCorner
|
visible: root.isSvg || root.isSvgCorner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user