Add new module "reply" for canned responses

Adds a new module that enables replying to messages
pull/3473/head
Brian Whelan 2024-06-21 18:44:44 +01:00 committed by GitHub
parent 722c289351
commit 6ee5b73052
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 240 additions and 0 deletions

1
apps/reply/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New Library!

23
apps/reply/README.md Normal file
View File

@ -0,0 +1,23 @@
# Canned Replies Library
A library that handles replying to messages received from Gadgetbridge/Messages apps.
## Replying to a message
The user can define a set of canned responses via the customise page after installing the app, or alternatively if they have a keyboard installed, they can type a response back. The requesting app will receive either an object containing the full reply for GadgetBridge, or a string with the response from the user, depending on how they wish to handle the response.
## Integrating in your app
To use this in your app, simply call
```js
require("reply").reply(/*options*/{...}).then(result => ...);
```
The ```options``` object can contain the following:
- ```msg```: A message object containing a field ```id```, the ID to respond to. If this is included in options, the result of the promise will be an object as follows: ```{t: "notify", id: msg.id, n: "REPLY", msg: "USER REPLY"}```. If not included, the result of the promise will be an object, ```{msg: "USER REPLY"}```
- ```shouldReply```: Whether or not the library should send the response over Bluetooth with ```Bluetooth.println(...```. Useful if the calling app wants to handle the response a different way. Default is true.
- ```title```: The title to show at the top of the menu. Defaults to ```"Reply with:"```.
- ```fileOverride```: An override file to read canned responses from, which is an array of objects each with a ```text``` property. Default is ```replies.json```. Useful for apps which might want to make use of custom canned responses.
## Known Issues
Emojis are currently not supported.

BIN
apps/reply/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

125
apps/reply/interface.html Normal file
View File

@ -0,0 +1,125 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
</head>
<body>
<form id="replyForm">
<label class="label">New Custom Reply:</label>
<input class="form-input" id="msg" type="text" required></input>
<input class="btn btn-primary" type="submit" value="Add">
<br>
<br>
</form>
<button class="btn btn-primary" onclick="updateDevice()">Update Device</button>
<div>
<div id="loading">
<div class="empty">
<div class="empty-icon">
<div class="loading loading-lg"></div>
</div>
<p class="empty-title h5">Loading</p>
<p class="empty-subtitle">Syncing custom replies with your watch</p>
</div>
</div>
<div id="empty" class="d-hide">
<div class="empty">
<div class="empty-icon">
<i class="icon icon-more-horiz"></i>
</div>
<p class="empty-title h5">No custom replies</p>
<p class="empty-subtitle">Use the field above to add a custom reply</p>
</div>
</div>
<table id="replyList" class="table table-striped table-hover"></table>
</div>
<script src="../../core/lib/customize.js"></script>
<script>
const form = document.querySelector("#replyForm");
form.addEventListener("submit", (event) => {
event.preventDefault();
saveReply();
});
var replies = [];
var fetching = true; // Bit of a hack
var el = document.getElementById('items');
function onInit(device) {
fetching = true;
Util.readStorageJSON("replies.json", (arr) => {
if (arr) {
replies = replies.concat(arr);
fetching = false;
renderReplyList();
}
});
}
function toggleVisibility() {
let empty = document.getElementById("empty");
let loading = document.getElementById("loading");
if (replies.length == 0) {
empty.setAttribute("class", "d-block");
loading.setAttribute("class", "d-hide");
}
else if (fetching) {
loading.setAttribute("class", "d-block");
empty.setAttribute("class", "d-hide");
}
else {
loading.setAttribute("class", "d-hide");
empty.setAttribute("class", "d-hide");
}
}
function renderReplyList() {
toggleVisibility();
let table = document.getElementById("replyList");
table.innerHTML = "";
if (!fetching) {
for (var i = 0; i < replies.length; i++) {
var reply = replies[i];
var li = document.createElement("tr");
var label = document.createElement("td");
label.innerHTML = reply.text + " ";
var deleteButton = document.createElement("button");
deleteButton.innerText = "Delete";
deleteButton.setAttribute("onclick", `deleteReply(${i})`);
deleteButton.setAttribute("class", "btn btn-error");
li.appendChild(label);
li.appendChild(deleteButton);
table.appendChild(li);
}
}
}
function deleteReply(index) {
replies.splice(index, 1);
renderReplyList(replies);
}
function saveReply() {
var reply = {};
reply.text = document.getElementById('msg').value;
replies.push(reply);
return updateDevice();
}
function updateDevice() {
fetching = true;
renderReplyList();
Util.writeStorage("replies.json", JSON.stringify(replies), () => {
fetching = false;
renderReplyList();
return true;
});
fetching = false;
return false;
}
</script>
</body>
</html>

75
apps/reply/lib.js Normal file
View File

@ -0,0 +1,75 @@
exports.reply = function (options) {
var keyboard = "textinput";
try {
keyboard = require(keyboard);
} catch (e) {
keyboard = null;
}
function constructReply(msg, replyText, resolve) {
var responseMessage = {msg: replyText};
if (msg.id) {
responseMessage = { t: "notify", id: msg.id, n: "REPLY", msg: replyText };
}
E.showMenu();
layout.setUI();
layout.render();
if (options.sendReply == null || options.sendReply) {
Bluetooth.println(JSON.stringify(result));
}
resolve(responseMessage);
}
return new Promise((resolve, reject) => {
var menu = {
"": {
title: options.title || /*LANG*/ "Reply with:",
back: function () {
E.showMenu();
layout.setUI();
layout.render();
reject("User pressed back");
},
}, // options
/*LANG*/ "Compose": function () {
keyboard.input().then((result) => {
constructReply(options.msg ?? {}, result, resolve);
});
},
};
var replies =
require("Storage").readJSON(
options.fileOverride || "replies.json",
true
) || {};
replies.forEach((reply) => {
menu = Object.defineProperty(menu, reply.text, {
value: () => constructReply(options.msg ?? {}, reply.text, resolve),
});
});
if (!keyboard) delete menu[/*LANG*/ "Compose"];
if (replies.length == 0) {
if (!keyboard) {
E.showPrompt(
/*LANG*/ "Please install a keyboard app, or set a custom reply via the app loader!",
{
buttons: { Ok: true },
remove: function () {
layout.setUI();
layout.render();
reject(
"Please install a keyboard app, or set a custom reply via the app loader!"
);
},
}
);
} else {
keyboard.input().then((result) => {
constructReply(options.msg.id, result, resolve);
});
}
}
E.showMenu(menu);
});
};

16
apps/reply/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{ "id": "reply",
"name": "Reply Library",
"version": "0.01",
"description": "A library for replying to text messages via predefined responses or keyboard",
"icon": "app.png",
"type": "module",
"provides_modules" : ["reply"],
"tags": "",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"reply","url":"lib.js"}
],
"data": [{"name":"replies.json"}]
}