mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
Merge remote-tracking branch 'origin/qm-newmsg' into ls-mesh
This commit is contained in:
commit
f3d244e90c
6 changed files with 224 additions and 22 deletions
22
gui/main.js
22
gui/main.js
|
@ -100,7 +100,8 @@ const configDefaultSettings =
|
|||
"max_retry_attempts" : 5, \
|
||||
"enable_auto_retry" : "False", \
|
||||
"tx_delay" : 0, \
|
||||
"auto_start": 0 \
|
||||
"auto_start": 0, \
|
||||
"notification": 1 \
|
||||
}';
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
|
@ -410,6 +411,11 @@ ipcMain.on("request-show-chat-window", () => {
|
|||
chat.show();
|
||||
});
|
||||
|
||||
ipcMain.on("request-clear-chat-connected", () => {
|
||||
//Clear chat window's connected with text
|
||||
chat.webContents.send("action-clear-reception-status");
|
||||
});
|
||||
|
||||
// UPDATE TNC CONNECTION
|
||||
ipcMain.on("request-update-tnc-ip", (event, data) => {
|
||||
win.webContents.send("action-update-tnc-ip", data);
|
||||
|
@ -479,6 +485,20 @@ ipcMain.on("request-update-transmission-status", (event, arg) => {
|
|||
|
||||
ipcMain.on("request-update-reception-status", (event, arg) => {
|
||||
win.webContents.send("action-update-reception-status", arg);
|
||||
chat.webContents.send("action-update-reception-status", arg);
|
||||
|
||||
});
|
||||
|
||||
//Called by main to query chat if there are new messages
|
||||
ipcMain.on("request-update-unread-messages",() => {
|
||||
//mainLog.info("Got request to check if chat has new messages")
|
||||
chat.webContents.send("action-update-unread-messages");
|
||||
|
||||
});
|
||||
//Called by chat to notify main if there are new messages
|
||||
ipcMain.on("request-update-unread-messages-main", (event,arg) => {
|
||||
win.webContents.send("action-update-unread-messages-main",arg);
|
||||
//mainLog.info("Received reply from chat and ?new messages = " +arg);
|
||||
});
|
||||
|
||||
ipcMain.on("request-open-tnc-log", () => {
|
||||
|
|
|
@ -661,8 +661,25 @@ ipcRenderer.on("return-select-user-image", (event, arg) => {
|
|||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on("action-update-reception-status", (event, arg) => {
|
||||
var data = arg["data"][0];
|
||||
|
||||
document.getElementById("txtConnectedWithChat").textContent = data.dxcallsign;
|
||||
});
|
||||
ipcRenderer.on("action-clear-reception-status", (event) => {
|
||||
//Clear connected with textbox
|
||||
let cwc = document.getElementById("txtConnectedWithChat");
|
||||
if (cwc.textContent != "------") {
|
||||
cwc.textContent = "------";
|
||||
//console.log("Reseting connected with");
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on("action-update-transmission-status", (event, arg) => {
|
||||
var data = arg["data"][0];
|
||||
|
||||
document.getElementById("txtConnectedWithChat").textContent = data.dxcallsign;
|
||||
|
||||
console.log(data.status);
|
||||
if (data.uuid !== "no-uuid") {
|
||||
db.get(data.uuid, {
|
||||
|
@ -768,6 +785,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
obj.new = 1;
|
||||
console.log(obj);
|
||||
add_obj_to_database(obj);
|
||||
update_chat_obj_by_uuid(obj.uuid);
|
||||
|
@ -796,6 +814,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
obj.new = 1;
|
||||
|
||||
add_obj_to_database(obj);
|
||||
update_chat_obj_by_uuid(obj.uuid);
|
||||
|
@ -815,7 +834,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new = 1;
|
||||
add_obj_to_database(obj);
|
||||
update_chat_obj_by_uuid(obj.uuid);
|
||||
|
||||
|
@ -834,7 +853,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new = 1;
|
||||
add_obj_to_database(obj);
|
||||
update_chat_obj_by_uuid(obj.uuid);
|
||||
|
||||
|
@ -862,6 +881,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filetype = splitted_data[7];
|
||||
//obj.file = btoa(splitted_data[8]);
|
||||
obj.file = FD.btoa_FD(splitted_data[8]);
|
||||
obj.new=1;
|
||||
} else if (splitted_data[1] == "req" && splitted_data[2] == "0") {
|
||||
obj.uuid = uuidv4().toString();
|
||||
obj.timestamp = Math.floor(Date.now() / 1000);
|
||||
|
@ -874,7 +894,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new=1;
|
||||
if (config.enable_request_profile == "True") {
|
||||
sendUserData(item.dxcallsign);
|
||||
}
|
||||
|
@ -890,7 +910,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new=1;
|
||||
if (config.enable_request_shared_folder == "True") {
|
||||
sendSharedFolderList(item.dxcallsign);
|
||||
}
|
||||
|
@ -911,7 +931,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new=1;
|
||||
if (config.enable_request_shared_folder == "True") {
|
||||
sendSharedFolderFile(item.dxcallsign, name);
|
||||
}
|
||||
|
@ -927,7 +947,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new=1;
|
||||
console.log(splitted_data);
|
||||
let userData = new Object();
|
||||
userData.user_info_image = splitted_data[2];
|
||||
|
@ -956,7 +976,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = "null";
|
||||
obj.filetype = "null";
|
||||
obj.file = "null";
|
||||
|
||||
obj.new=1;
|
||||
console.log(splitted_data);
|
||||
|
||||
let userData = new Object();
|
||||
|
@ -984,6 +1004,7 @@ ipcRenderer.on("action-new-msg-received", (event, arg) => {
|
|||
obj.filename = sharedFileInfo[0];
|
||||
obj.filetype = "application/octet-stream";
|
||||
obj.file = FD.btoa_FD(sharedFileInfo[1]);
|
||||
obj.new=1;
|
||||
} else {
|
||||
console.log("no rule matched for handling received data!");
|
||||
}
|
||||
|
@ -1023,7 +1044,7 @@ update_chat = function (obj) {
|
|||
}
|
||||
|
||||
// add percent and bytes per minute if not existing
|
||||
console.log(obj.percent)
|
||||
//console.log(obj.percent)
|
||||
if (typeof obj.percent == "undefined") {
|
||||
obj.percent = 0;
|
||||
obj.bytesperminute = 0;
|
||||
|
@ -1043,13 +1064,16 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
|
||||
}
|
||||
}
|
||||
if (typeof obj.new == "undefined"){
|
||||
obj.new=0;
|
||||
}
|
||||
|
||||
if (typeof config.max_retry_attempts == "undefined") {
|
||||
var max_retry_attempts = 3;
|
||||
} else {
|
||||
var max_retry_attempts = parseInt(config.max_retry_attempts);
|
||||
}
|
||||
console.log(obj.msg);
|
||||
//console.log(obj.msg);
|
||||
// define shortmessage
|
||||
if (obj.msg == "null" || obj.msg == "NULL") {
|
||||
var shortmsg = obj.type;
|
||||
|
@ -1184,7 +1208,7 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
|
||||
</div>
|
||||
|
||||
<span style="font-size:1.2rem;"><strong>${dxcallsign}</strong></span>
|
||||
<span style="font-size:1.2rem;" ><strong id="chat-${dxcallsign}-list-displaydxcall">${dxcallsign}</strong></span>
|
||||
<span class="badge bg-secondary text-white p-1 h-100" id="chat-${dxcallsign}-list-dxgrid"><small>${dxgrid}</small></span>
|
||||
<span style="font-size:0.8rem;" id="chat-${dxcallsign}-list-time">${timestampHours}</span>
|
||||
<span class="position-absolute m-2 bottom-0 end-0" style="font-size:0.8rem;" id="chat-${dxcallsign}-list-shortmsg">${shortmsg}</span>
|
||||
|
@ -1214,6 +1238,11 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
.addEventListener("click", function () {
|
||||
//document.getElementById('chatModuleDxCall').value = dxcallsign;
|
||||
selected_callsign = dxcallsign;
|
||||
//Reset unread messages and new message indicator
|
||||
let clear = selected_callsign;
|
||||
clearUnreadMessages(clear);
|
||||
document.getElementById(`chat-${selected_callsign}-list-displaydxcall`).textContent=selected_callsign;
|
||||
|
||||
setTimeout(scrollMessagesToBottom, 200);
|
||||
|
||||
//get user information
|
||||
|
@ -1234,6 +1263,7 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
// short message
|
||||
document.getElementById("chat-" + dxcallsign + "-list-shortmsg").innerHTML =
|
||||
shortmsg;
|
||||
if (obj.new==1) document.getElementById(`chat-${obj.dxcallsign}-list-displaydxcall`).textContent="*" +obj.dxcallsign;
|
||||
}
|
||||
// APPEND MESSAGES TO CALLSIGN
|
||||
|
||||
|
@ -1243,7 +1273,10 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
if (config.enable_auto_retry.toUpperCase() == "TRUE") {
|
||||
checkForWaitingMessages(obj.dxcallsign);
|
||||
}
|
||||
|
||||
if (obj.new == 1)
|
||||
{
|
||||
showOsPopUp("Ping from " + obj.dxcallsign,"You've been ping'd!");
|
||||
}
|
||||
var new_message = `
|
||||
<div class="m-auto mt-1 p-0 w-50 rounded bg-secondary bg-gradient" id="msg-${obj._id}">
|
||||
<p class="text-small text-white mb-0 text-break" style="font-size: 0.7rem;"><i class="m-3 bi bi-arrow-left-right"></i>snr: ${obj.snr} - ${timestamp} </p>
|
||||
|
@ -1294,6 +1327,11 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
var message_html = obj.msg.replaceAll(/\n/g, "<br>");
|
||||
|
||||
if (obj.type == "received") {
|
||||
if (obj.new == 1)
|
||||
{
|
||||
showOsPopUp("Message received from " + obj.dxcallsign,obj.msg);
|
||||
}
|
||||
|
||||
var new_message = `
|
||||
<div class="d-flex align-items-center" style="margin-left: auto;"> <!-- max-width: 75%; -->
|
||||
|
||||
|
@ -1384,7 +1422,7 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
}
|
||||
|
||||
if (obj.type == "transmit") {
|
||||
console.log(obj);
|
||||
//console.log(obj);
|
||||
//console.log('msg-' + obj._id + '-status')
|
||||
|
||||
if (obj.status == "failed") {
|
||||
|
@ -1457,11 +1495,11 @@ var TimeDifference = (new Date().getTime()/1000) - obj.timestamp
|
|||
|
||||
/* UPDATE EXISTING ELEMENTS */
|
||||
} else if (document.getElementById("msg-" + obj._id)) {
|
||||
console.log("element already exists......");
|
||||
console.log(obj);
|
||||
//console.log("element already exists......");
|
||||
//console.log(obj);
|
||||
|
||||
console.log(obj.status)
|
||||
console.log(obj.attempt)
|
||||
// console.log(obj.status)
|
||||
// console.log(obj.attempt)
|
||||
|
||||
|
||||
|
||||
|
@ -1844,6 +1882,7 @@ add_obj_to_database = function (obj) {
|
|||
status: obj.status,
|
||||
snr: obj.snr,
|
||||
attempt: obj.attempt,
|
||||
new: obj.new,
|
||||
_attachments: {
|
||||
[obj.filename]: {
|
||||
content_type: obj.filetype,
|
||||
|
@ -2093,6 +2132,7 @@ function createChatIndex() {
|
|||
"attempt",
|
||||
"bytesperminute",
|
||||
"_attachments",
|
||||
"new",
|
||||
],
|
||||
},
|
||||
})
|
||||
|
@ -2232,9 +2272,9 @@ function getSetUserSharedFolder(selected_callsign) {
|
|||
.then(function (data) {
|
||||
console.log(data);
|
||||
|
||||
console.log(data.user_shared_folder);
|
||||
|
||||
if (typeof data.user_shared_folder !== "undefined") {
|
||||
console.log(data.user_shared_folder);
|
||||
// shared folder table
|
||||
var icons = [
|
||||
"aac",
|
||||
|
@ -2620,6 +2660,12 @@ ipcRenderer.on("update-config", (event, data) => {
|
|||
config = data;
|
||||
});
|
||||
|
||||
ipcRenderer.on("action-update-unread-messages", (event) => {
|
||||
checkForNewMessages().then(function(count) {
|
||||
ipcRenderer.send("request-update-unread-messages-main",count);
|
||||
});
|
||||
});
|
||||
|
||||
// https://stackoverflow.com/a/18650828
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (!+bytes) return "0 Bytes";
|
||||
|
@ -2749,3 +2795,56 @@ function checkForWaitingMessages(dxcall) {
|
|||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForNewMessages()
|
||||
{
|
||||
var newmsgs;
|
||||
await db.find({
|
||||
selector: {
|
||||
new: {$eq: 1},
|
||||
}, limit:1,
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.docs.length >0)
|
||||
newmsgs=true;
|
||||
else
|
||||
newmsgs=false;
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
return newmsgs;
|
||||
}
|
||||
|
||||
function clearUnreadMessages(dxcall) {
|
||||
//console.log(dxcall);
|
||||
db.find({
|
||||
selector: {
|
||||
dxcallsign: dxcall,
|
||||
new: {$eq: 1},
|
||||
}
|
||||
})
|
||||
.then(function (result) {
|
||||
//console.log(result);
|
||||
//console.log ("New messages count to clear for " + dxcall + ": " + result.docs.length)
|
||||
result.docs.forEach(function (item) {
|
||||
db.upsert(item._id, function (doc) {
|
||||
doc.new=0;
|
||||
//console.log("Clearing new on _id " + item._id);
|
||||
return doc;
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
//Have the operating system show a notification popup
|
||||
function showOsPopUp(title, message)
|
||||
{
|
||||
if (config.notification == 0) return;
|
||||
const NOTIFICATION_TITLE = title;
|
||||
const NOTIFICATION_BODY = message;
|
||||
new Notification(NOTIFICATION_TITLE, { body: NOTIFICATION_BODY });
|
||||
}
|
|
@ -390,6 +390,12 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||
document.getElementById("AutoStartSwitch").checked = false;
|
||||
}
|
||||
|
||||
if (config.notification == 1) {
|
||||
document.getElementById("NotificationSwitch").checked = true;
|
||||
} else {
|
||||
document.getElementById("NotificationSwitch").checked = false;
|
||||
}
|
||||
|
||||
// theme selector
|
||||
changeGuiDesign(config.theme);
|
||||
|
||||
|
@ -1213,6 +1219,17 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||
FD.saveConfig(config, configPath);
|
||||
});
|
||||
|
||||
//Handle change of Notification settings
|
||||
document.getElementById("NotificationSwitch").addEventListener("click", () => {
|
||||
if (document.getElementById("NotificationSwitch").checked == true) {
|
||||
config.notification = 1;
|
||||
} else {
|
||||
config.notification = 0;
|
||||
}
|
||||
//fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
FD.saveConfig(config, configPath);
|
||||
});
|
||||
|
||||
// enable fsk Switch clicked
|
||||
document.getElementById("fskModeSwitch").addEventListener("click", () => {
|
||||
if (document.getElementById("fskModeSwitch").checked == true) {
|
||||
|
@ -1371,7 +1388,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
|||
|
||||
// Discord Link clicked
|
||||
document.getElementById("discordUrl").addEventListener("click", () => {
|
||||
shell.openExternal("https://discord.gg/jnADeDtxUF");
|
||||
shell.openExternal("https://discord.freedata.app/");
|
||||
});
|
||||
|
||||
// startTNC button clicked
|
||||
|
@ -1938,7 +1955,16 @@ function signal_quality_perc_quad(rssi, perfect_rssi = 10, worst_rssi = -150) {
|
|||
}
|
||||
|
||||
var lastHeard = "";
|
||||
var checkForNewMessageWait=85;
|
||||
|
||||
ipcRenderer.on("action-update-tnc-state", (event, arg) => {
|
||||
//check for new messages
|
||||
if (checkForNewMessageWait >= 100){
|
||||
//This is very expensive
|
||||
ipcRenderer.send("request-update-unread-messages");
|
||||
checkForNewMessageWait=-1;
|
||||
}
|
||||
checkForNewMessageWait++;
|
||||
// update FFT
|
||||
if (typeof arg.fft !== "undefined") {
|
||||
// FIXME: WE need to fix this when disabled waterfall chart
|
||||
|
@ -2258,6 +2284,7 @@ ipcRenderer.on("action-update-tnc-state", (event, arg) => {
|
|||
"bi bi-chat-fill text-success me-1";
|
||||
} else {
|
||||
document.getElementById("spnConnectedWith").className = "bi bi-chat-fill";
|
||||
ipcRenderer.send("request-clear-chat-connected");
|
||||
}
|
||||
|
||||
// HAMLIB STATUS
|
||||
|
@ -2894,6 +2921,22 @@ ipcRenderer.on("run-tnc-command-fec-iswriting", (event) => {
|
|||
sock.sendFecIsWriting(config.mycall);
|
||||
});
|
||||
|
||||
//Change background color of RF Chat button if new messages are available
|
||||
ipcRenderer.on("action-update-unread-messages-main", (event,data) => {
|
||||
//Do something
|
||||
if (data == true)
|
||||
{
|
||||
document.getElementById("openRFChat").classList.add("btn-warning")
|
||||
document.getElementById("openRFChat").classList.remove("btn-secondary")
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById("openRFChat").classList.remove("btn-warning")
|
||||
document.getElementById("openRFChat").classList.add("btn-secondary")
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ipcRenderer.on("run-tnc-command", (event, arg) => {
|
||||
|
||||
if (arg.command == "enable_mesh") {
|
||||
|
@ -3090,7 +3133,7 @@ ipcRenderer.on("action-show-cq-toast-received", (event, data) => {
|
|||
let dxcallsign = data["data"][0]["dxcallsign"];
|
||||
let dxgrid = data["data"][0]["dxgrid"];
|
||||
let content = `cq from <strong>${dxcallsign}</strong> (${dxgrid})`;
|
||||
|
||||
showOsPopUp("CQ from " + dxcallsign,"Say hello!");
|
||||
displayToast(
|
||||
(type = "success"),
|
||||
(icon = "bi-broadcast"),
|
||||
|
@ -3778,3 +3821,12 @@ function autostart_tnc() {
|
|||
document.getElementById("startTNC").click();
|
||||
}
|
||||
}
|
||||
|
||||
//Have the operating system show a notification popup
|
||||
function showOsPopUp(title, message)
|
||||
{
|
||||
if (config.notification == 0) return;
|
||||
const NOTIFICATION_TITLE = title;
|
||||
const NOTIFICATION_BODY = message;
|
||||
new Notification(NOTIFICATION_TITLE, { body: NOTIFICATION_BODY });
|
||||
}
|
|
@ -264,7 +264,18 @@
|
|||
Help
|
||||
</button>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<span
|
||||
class="input-group-text ms-2"
|
||||
id="txtConnectedWithChat"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
title="Connected with"
|
||||
>------</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@
|
|||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="false"
|
||||
title="Open the HF chat module. This is currently just a test and not finished, yet!"
|
||||
title="Open the chat window. The background will change to yellow if a new message is available."
|
||||
>
|
||||
<i class="bi bi-chat-left-text-fill me-2"></i>
|
||||
<strong>RF Chat</strong>
|
||||
|
@ -2911,6 +2911,23 @@
|
|||
<option value="alpha">alpha</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50"
|
||||
>Enable notifications</label
|
||||
>
|
||||
<label class="input-group-text w-50">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="NotificationSwitch"
|
||||
/>
|
||||
<label class="form-check-label" for="NotificationSwitch"
|
||||
>Show system pop-ups</label
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50"
|
||||
>Auto-start TNC/rigctld</label
|
||||
|
|
|
@ -76,6 +76,9 @@ def fetch_audio_devices(input_devices, output_devices):
|
|||
# Use a try/except block because Windows doesn't have an audio device range
|
||||
try:
|
||||
name = device["name"]
|
||||
# Ignore some Flex Radio devices to make device selection simpler
|
||||
if name.startswith("DAX RESERVED") or name.startswith("DAX IQ"):
|
||||
continue
|
||||
|
||||
max_output_channels = device["max_output_channels"]
|
||||
max_input_channels = device["max_input_channels"]
|
||||
|
|
Loading…
Reference in a new issue