diff --git a/apps/reply/ChangeLog b/apps/reply/ChangeLog
new file mode 100644
index 000000000..f3c7b0d2c
--- /dev/null
+++ b/apps/reply/ChangeLog
@@ -0,0 +1 @@
+0.01: New Library!
\ No newline at end of file
diff --git a/apps/reply/README.md b/apps/reply/README.md
new file mode 100644
index 000000000..dc874d183
--- /dev/null
+++ b/apps/reply/README.md
@@ -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.
\ No newline at end of file
diff --git a/apps/reply/app.png b/apps/reply/app.png
new file mode 100644
index 000000000..bef8338cf
Binary files /dev/null and b/apps/reply/app.png differ
diff --git a/apps/reply/interface.html b/apps/reply/interface.html
new file mode 100644
index 000000000..2034a1195
--- /dev/null
+++ b/apps/reply/interface.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading
+
Syncing custom replies with your watch
+
+
+
+
+
+
+
+
No custom replies
+
Use the field above to add a custom reply
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/reply/lib.js b/apps/reply/lib.js
new file mode 100644
index 000000000..7bd4780a5
--- /dev/null
+++ b/apps/reply/lib.js
@@ -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);
+ });
+};
diff --git a/apps/reply/metadata.json b/apps/reply/metadata.json
new file mode 100644
index 000000000..34843edd4
--- /dev/null
+++ b/apps/reply/metadata.json
@@ -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"}]
+}
\ No newline at end of file