mirror of
https://github.com/frappe/books.git
synced 2024-11-14 01:14:03 +00:00
Merge pull request #331 from 18alantom/auto-updation
feat: notification based updation
This commit is contained in:
commit
da9c677fc2
@ -88,15 +88,6 @@ module.exports = {
|
|||||||
'Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.'
|
'Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.'
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldname: 'autoUpdate',
|
|
||||||
label: 'Auto Update',
|
|
||||||
fieldtype: 'Check',
|
|
||||||
default: 1,
|
|
||||||
description: t(
|
|
||||||
'Automatically checks for updates and download them if available. The update will be applied after you restart the app.'
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
fieldname: 'autoReportErrors',
|
fieldname: 'autoReportErrors',
|
||||||
label: 'Hide & Auto Report Errors',
|
label: 'Hide & Auto Report Errors',
|
||||||
@ -112,7 +103,6 @@ module.exports = {
|
|||||||
'locale',
|
'locale',
|
||||||
'displayPrecision',
|
'displayPrecision',
|
||||||
'hideGetStarted',
|
'hideGetStarted',
|
||||||
'autoUpdate',
|
|
||||||
'autoReportErrors',
|
'autoReportErrors',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -39,7 +39,7 @@ import {
|
|||||||
postSetup,
|
postSetup,
|
||||||
purgeCache,
|
purgeCache,
|
||||||
} from '@/initialization';
|
} from '@/initialization';
|
||||||
import { routeTo } from './utils';
|
import { checkForUpdates, routeTo } from './utils';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { showErrorDialog } from './errorHandling';
|
import { showErrorDialog } from './errorHandling';
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ export default {
|
|||||||
this.activeScreen = 'SetupWizard';
|
this.activeScreen = 'SetupWizard';
|
||||||
} else {
|
} else {
|
||||||
this.activeScreen = 'Desk';
|
this.activeScreen = 'Desk';
|
||||||
this.checkForUpdates();
|
checkForUpdates(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resetRoute) {
|
if (!resetRoute) {
|
||||||
@ -122,9 +122,6 @@ export default {
|
|||||||
routeTo('/get-started');
|
routeTo('/get-started');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkForUpdates() {
|
|
||||||
frappe.events.trigger('check-for-updates');
|
|
||||||
},
|
|
||||||
changeDbFile() {
|
changeDbFile() {
|
||||||
config.set('lastSelectedFilePath', null);
|
config.set('lastSelectedFilePath', null);
|
||||||
purgeCache(true);
|
purgeCache(true);
|
||||||
|
@ -16,7 +16,7 @@ import fs from 'fs/promises';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||||
import { sendError } from './contactMothership';
|
import { sendError } from './contactMothership';
|
||||||
import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
|
import { IPC_ACTIONS, IPC_CHANNELS, IPC_MESSAGES } from './messages';
|
||||||
import saveHtmlAsPdf from './saveHtmlAsPdf';
|
import saveHtmlAsPdf from './saveHtmlAsPdf';
|
||||||
|
|
||||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
@ -37,6 +37,10 @@ protocol.registerSchemesAsPrivileged([
|
|||||||
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (isDevelopment) {
|
||||||
|
autoUpdater.logger = console;
|
||||||
|
}
|
||||||
|
|
||||||
Store.initRenderer();
|
Store.initRenderer();
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
@ -104,7 +108,7 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on('did-finish-load', () => {
|
mainWindow.webContents.on('did-finish-load', () => {
|
||||||
mainWindow.webContents.send('store-on-window', {
|
mainWindow.webContents.send(IPC_CHANNELS.STORE_ON_WINDOW, {
|
||||||
appVersion: app.getVersion(),
|
appVersion: app.getVersion(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -114,13 +118,6 @@ function createWindow() {
|
|||||||
* Register ipcMain message handlers
|
* Register ipcMain message handlers
|
||||||
* ---------------------------------*/
|
* ---------------------------------*/
|
||||||
|
|
||||||
ipcMain.on(IPC_MESSAGES.CHECK_FOR_UPDATES, () => {
|
|
||||||
if (!isDevelopment && !checkedForUpdate) {
|
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
|
||||||
checkedForUpdate = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on(IPC_MESSAGES.OPEN_MENU, (event) => {
|
ipcMain.on(IPC_MESSAGES.OPEN_MENU, (event) => {
|
||||||
const window = event.sender.getOwnerBrowserWindow();
|
const window = event.sender.getOwnerBrowserWindow();
|
||||||
const menu = Menu.getApplicationMenu();
|
const menu = Menu.getApplicationMenu();
|
||||||
@ -154,6 +151,14 @@ ipcMain.on(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, (event, filePath) => {
|
|||||||
return shell.showItemInFolder(filePath);
|
return shell.showItemInFolder(filePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on(IPC_MESSAGES.DOWNLOAD_UPDATE, (event) => {
|
||||||
|
autoUpdater.downloadUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on(IPC_MESSAGES.INSTALL_UPDATE, (event) => {
|
||||||
|
autoUpdater.quitAndInstall(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
/* ----------------------------------
|
/* ----------------------------------
|
||||||
* Register ipcMain function handlers
|
* Register ipcMain function handlers
|
||||||
* ----------------------------------*/
|
* ----------------------------------*/
|
||||||
@ -207,6 +212,45 @@ ipcMain.handle(IPC_ACTIONS.SEND_ERROR, (event, bodyJson) => {
|
|||||||
sendError(bodyJson);
|
sendError(bodyJson);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_ACTIONS.CHECK_FOR_UPDATES, (event, force) => {
|
||||||
|
if (!isDevelopment && !checkedForUpdate) {
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
checkedForUpdate = true;
|
||||||
|
} else if (force) {
|
||||||
|
autoUpdater.checkForUpdates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ------------------------------
|
||||||
|
* Register autoUpdater events lis
|
||||||
|
* ------------------------------*/
|
||||||
|
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
if (!checkedForUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainWindow.webContents.send(IPC_CHANNELS.CHECKING_FOR_UPDATE);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
mainWindow.webContents.send(IPC_CHANNELS.UPDATE_AVAILABLE, info.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-not-available', () => {
|
||||||
|
mainWindow.webContents.send(IPC_CHANNELS.UPDATE_NOT_AVAILABLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', () => {
|
||||||
|
mainWindow.webContents.send(IPC_CHANNELS.UPDATE_DOWNLOADED);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('error', (error) => {
|
||||||
|
mainWindow.webContents.send(IPC_CHANNELS.UPDATE_ERROR, error);
|
||||||
|
});
|
||||||
|
|
||||||
/* ------------------------------
|
/* ------------------------------
|
||||||
* Register app lifecycle methods
|
* Register app lifecycle methods
|
||||||
* ------------------------------*/
|
* ------------------------------*/
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
mb-3
|
mb-3
|
||||||
w-80
|
w-80
|
||||||
"
|
"
|
||||||
:class="bgColor + (action ? ' cursor-pointer' : '')"
|
:class="bgColor + (actionText ? ' cursor-pointer' : '')"
|
||||||
style="transition: opacity 150ms ease-in"
|
style="transition: opacity 150ms ease-in"
|
||||||
:style="{ opacity }"
|
:style="{ opacity }"
|
||||||
@click="action"
|
@click="action"
|
||||||
@ -37,9 +37,9 @@ export default {
|
|||||||
return { opacity: 0, show: true };
|
return { opacity: 0, show: true };
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
message: String,
|
message: { type: String, required: true },
|
||||||
action: Function,
|
action: { type: Function, default: () => {} },
|
||||||
actionText: String,
|
actionText: { type: String, default: '' },
|
||||||
type: { type: String, default: 'info' },
|
type: { type: String, default: 'info' },
|
||||||
duration: { type: Number, default: 5000 },
|
duration: { type: Number, default: 5000 },
|
||||||
},
|
},
|
||||||
|
@ -58,6 +58,16 @@ function getToastProps(errorLogObj: ErrorLog, cb?: Function) {
|
|||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getErrorLogObject(error: Error, more: object = {}): ErrorLog {
|
||||||
|
const { name, stack, message } = error;
|
||||||
|
const errorLogObj = { name, stack, message, more };
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
frappe.errorLog.push(errorLogObj);
|
||||||
|
|
||||||
|
return errorLogObj;
|
||||||
|
}
|
||||||
|
|
||||||
export function handleError(
|
export function handleError(
|
||||||
shouldLog: boolean,
|
shouldLog: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
@ -72,11 +82,7 @@ export function handleError(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, stack, message } = error;
|
const errorLogObj = getErrorLogObject(error, more);
|
||||||
const errorLogObj: ErrorLog = { name, stack, message, more };
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
frappe.errorLog.push(errorLogObj);
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (frappe.SystemSettings?.autoReportErrors) {
|
if (frappe.SystemSettings?.autoReportErrors) {
|
||||||
|
86
src/main.js
86
src/main.js
@ -5,10 +5,10 @@ import models from '../models';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import FeatherIcon from './components/FeatherIcon';
|
import FeatherIcon from './components/FeatherIcon';
|
||||||
import { getErrorHandled, handleError } from './errorHandling';
|
import { getErrorHandled, handleError } from './errorHandling';
|
||||||
import { IPC_MESSAGES } from './messages';
|
import { IPC_CHANNELS, IPC_MESSAGES } from './messages';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
import { outsideClickDirective } from './ui';
|
import { outsideClickDirective } from './ui';
|
||||||
import { stringifyCircular } from './utils';
|
import { showToast, stringifyCircular } from './utils';
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
frappe.isServer = true;
|
frappe.isServer = true;
|
||||||
@ -23,19 +23,10 @@ import { stringifyCircular } from './utils';
|
|||||||
ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.events.on('check-for-updates', () => {
|
|
||||||
let { autoUpdate } = frappe.SystemSettings;
|
|
||||||
if (autoUpdate == null || autoUpdate === 1) {
|
|
||||||
ipcRenderer.send(IPC_MESSAGES.CHECK_FOR_UPDATES);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.frappe = frappe;
|
window.frappe = frappe;
|
||||||
window.frappe.store = {};
|
window.frappe.store = {};
|
||||||
|
|
||||||
ipcRenderer.on('store-on-window', (event, message) => {
|
registerIpcRendererListeners();
|
||||||
Object.assign(window.frappe.store, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
Vue.component('feather-icon', FeatherIcon);
|
Vue.component('feather-icon', FeatherIcon);
|
||||||
@ -60,17 +51,19 @@ import { stringifyCircular } from './utils';
|
|||||||
});
|
});
|
||||||
|
|
||||||
Vue.config.errorHandler = (err, vm, info) => {
|
Vue.config.errorHandler = (err, vm, info) => {
|
||||||
const { fullPath, params } = vm.$route;
|
const more = {
|
||||||
const data = stringifyCircular(vm.$data, true, true);
|
|
||||||
const props = stringifyCircular(vm.$props, true, true);
|
|
||||||
|
|
||||||
handleError(false, err, {
|
|
||||||
fullPath,
|
|
||||||
params: stringifyCircular(params),
|
|
||||||
data,
|
|
||||||
props,
|
|
||||||
info,
|
info,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (vm) {
|
||||||
|
const { fullPath, params } = vm.$route;
|
||||||
|
more.fullPath = fullPath;
|
||||||
|
more.params = stringifyCircular(params ?? {});
|
||||||
|
more.data = stringifyCircular(vm.$data ?? {}, true, true);
|
||||||
|
more.props = stringifyCircular(vm.$props ?? {}, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleError(false, err, more);
|
||||||
console.error(err, vm, info);
|
console.error(err, vm, info);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,3 +90,52 @@ import { stringifyCircular } from './utils';
|
|||||||
template: '<App/>',
|
template: '<App/>',
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function registerIpcRendererListeners() {
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.STORE_ON_WINDOW, (event, message) => {
|
||||||
|
Object.assign(window.frappe.store, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.CHECKING_FOR_UPDATE, (_) => {
|
||||||
|
showToast({ message: frappe.t`Checking for updates` });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.UPDATE_AVAILABLE, (_, version) => {
|
||||||
|
const message = version
|
||||||
|
? frappe.t`Version ${version} available`
|
||||||
|
: frappe.t`New version available`;
|
||||||
|
const action = () => {
|
||||||
|
ipcRenderer.send(IPC_MESSAGES.DOWNLOAD_UPDATE);
|
||||||
|
};
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
message,
|
||||||
|
action,
|
||||||
|
actionText: frappe.t`Download Update`,
|
||||||
|
duration: 10_000,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.UPDATE_NOT_AVAILABLE, (_) => {
|
||||||
|
showToast({ message: frappe.t`No updates available` });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.UPDATE_DOWNLOADED, (_) => {
|
||||||
|
const action = () => {
|
||||||
|
ipcRenderer.send(IPC_MESSAGES.INSTALL_UPDATE);
|
||||||
|
};
|
||||||
|
showToast({
|
||||||
|
message: frappe.t`Update downloaded`,
|
||||||
|
action,
|
||||||
|
actionText: frappe.t`Install Update`,
|
||||||
|
duration: 10_000,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(IPC_CHANNELS.UPDATE_ERROR, (_, error) => {
|
||||||
|
error.name = 'Updation Error';
|
||||||
|
handleError(true, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
|
// ipcRenderer.send(...)
|
||||||
export const IPC_MESSAGES = {
|
export const IPC_MESSAGES = {
|
||||||
OPEN_MENU: 'open-menu',
|
OPEN_MENU: 'open-menu',
|
||||||
OPEN_SETTINGS: 'open-settings',
|
OPEN_SETTINGS: 'open-settings',
|
||||||
OPEN_EXTERNAL: 'open-external',
|
OPEN_EXTERNAL: 'open-external',
|
||||||
SHOW_ITEM_IN_FOLDER: 'show-item-in-folder',
|
SHOW_ITEM_IN_FOLDER: 'show-item-in-folder',
|
||||||
CHECK_FOR_UPDATES: 'check-for-updates',
|
|
||||||
RELOAD_MAIN_WINDOW: 'reload-main-window',
|
RELOAD_MAIN_WINDOW: 'reload-main-window',
|
||||||
RESIZE_MAIN_WINDOW: 'resize-main-window',
|
RESIZE_MAIN_WINDOW: 'resize-main-window',
|
||||||
CLOSE_CURRENT_WINDOW: 'close-current-window',
|
CLOSE_CURRENT_WINDOW: 'close-current-window',
|
||||||
MINIMIZE_CURRENT_WINDOW: 'minimize-current-window',
|
MINIMIZE_CURRENT_WINDOW: 'minimize-current-window',
|
||||||
|
DOWNLOAD_UPDATE: 'download-update',
|
||||||
|
INSTALL_UPDATE: 'install-update',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ipcRenderer.invoke(...)
|
||||||
export const IPC_ACTIONS = {
|
export const IPC_ACTIONS = {
|
||||||
TOGGLE_MAXIMIZE_CURRENT_WINDOW: 'toggle-maximize-current-window',
|
TOGGLE_MAXIMIZE_CURRENT_WINDOW: 'toggle-maximize-current-window',
|
||||||
GET_OPEN_FILEPATH: 'open-dialog',
|
GET_OPEN_FILEPATH: 'open-dialog',
|
||||||
@ -20,6 +23,17 @@ export const IPC_ACTIONS = {
|
|||||||
SAVE_DATA: 'save-data',
|
SAVE_DATA: 'save-data',
|
||||||
SHOW_ERROR: 'show-error',
|
SHOW_ERROR: 'show-error',
|
||||||
SEND_ERROR: 'send-error',
|
SEND_ERROR: 'send-error',
|
||||||
|
CHECK_FOR_UPDATES: 'check-for-updates',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ipcMain.send(...)
|
||||||
|
export const IPC_CHANNELS = {
|
||||||
|
STORE_ON_WINDOW: 'store-on-window',
|
||||||
|
CHECKING_FOR_UPDATE: 'checking-for-update',
|
||||||
|
UPDATE_AVAILABLE: 'update-available',
|
||||||
|
UPDATE_NOT_AVAILABLE: 'update-not-available',
|
||||||
|
UPDATE_DOWNLOADED: 'update-downloaded',
|
||||||
|
UPDATE_ERROR: 'update-error',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DB_CONN_FAILURE = {
|
export const DB_CONN_FAILURE = {
|
||||||
|
@ -8,12 +8,21 @@
|
|||||||
:emit-change="true"
|
:emit-change="true"
|
||||||
@change="forwardChangeEvent"
|
@change="forwardChangeEvent"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-row justify-end my-4">
|
||||||
|
<button
|
||||||
|
class="text-gray-900 text-sm hover:bg-gray-100 rounded-md px-4 py-1.5"
|
||||||
|
@click="checkForUpdates(true)"
|
||||||
|
>
|
||||||
|
Check for Updates
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import frappe from 'frappe';
|
import frappe from 'frappe';
|
||||||
import TwoColumnForm from '@/components/TwoColumnForm';
|
import TwoColumnForm from '@/components/TwoColumnForm';
|
||||||
|
import { checkForUpdates } from '@/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TabSystem',
|
name: 'TabSystem',
|
||||||
@ -36,6 +45,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkForUpdates,
|
||||||
forwardChangeEvent(...args) {
|
forwardChangeEvent(...args) {
|
||||||
this.$emit('change', ...args);
|
this.$emit('change', ...args);
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,7 @@ import Avatar from '@/components/Avatar';
|
|||||||
import Toast from '@/components/Toast';
|
import Toast from '@/components/Toast';
|
||||||
import router from '@/router';
|
import router from '@/router';
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import frappe from 'frappe';
|
import frappe, { t } from 'frappe';
|
||||||
import { t } from 'frappe';
|
|
||||||
import { isPesa } from 'frappe/utils';
|
import { isPesa } from 'frappe/utils';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
@ -414,3 +413,7 @@ export function stringifyCircular(
|
|||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkForUpdates(force = false) {
|
||||||
|
ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES, force);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user