aboutsummaryrefslogtreecommitdiff
path: root/app/src/web/assets/js
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2019-02-05 22:13:41 -0500
committerGitHub <noreply@github.com>2019-02-05 22:13:41 -0500
commit330bf2857396b15752afadb030c981a1cb2848fe (patch)
treece224b4ce241ccc58af32614052b2a407f3e89c0 /app/src/web/assets/js
parentc484c2728c1688ed695205a420eba3f2b2ba179d (diff)
downloadfrost-330bf2857396b15752afadb030c981a1cb2848fe.tar.gz
frost-330bf2857396b15752afadb030c981a1cb2848fe.tar.bz2
frost-330bf2857396b15752afadb030c981a1cb2848fe.zip
Enhancement/js (#1344)
* Attempt to add package json * Add initial typescript components * Convert remaining files * Remove some null checks * Reorganize folders * Add missing js and tests for file paths * Delete unused babelrc * Remove unused packages
Diffstat (limited to 'app/src/web/assets/js')
-rw-r--r--app/src/web/assets/js/click_a.js55
-rw-r--r--app/src/web/assets/js/click_a.ts57
-rw-r--r--app/src/web/assets/js/click_debugger.js14
-rw-r--r--app/src/web/assets/js/click_debugger.ts15
-rw-r--r--app/src/web/assets/js/context_a.js68
-rw-r--r--app/src/web/assets/js/context_a.ts69
-rw-r--r--app/src/web/assets/js/document_watcher.js24
-rw-r--r--app/src/web/assets/js/document_watcher.ts27
-rw-r--r--app/src/web/assets/js/header_badges.js8
-rw-r--r--app/src/web/assets/js/header_badges.ts7
-rw-r--r--app/src/web/assets/js/header_hider.js12
-rw-r--r--app/src/web/assets/js/header_hider.ts17
-rw-r--r--app/src/web/assets/js/media.js43
-rw-r--r--app/src/web/assets/js/media.ts47
-rw-r--r--app/src/web/assets/js/menu.js57
-rw-r--r--app/src/web/assets/js/menu.ts59
-rw-r--r--app/src/web/assets/js/notif_msg.js26
-rw-r--r--app/src/web/assets/js/notif_msg.ts25
-rw-r--r--app/src/web/assets/js/textarea_listener.js31
-rw-r--r--app/src/web/assets/js/textarea_listener.ts31
20 files changed, 692 insertions, 0 deletions
diff --git a/app/src/web/assets/js/click_a.js b/app/src/web/assets/js/click_a.js
new file mode 100644
index 00000000..7faafc15
--- /dev/null
+++ b/app/src/web/assets/js/click_a.js
@@ -0,0 +1,55 @@
+"use strict";
+(function () {
+ var prevented = false;
+ var _frostAClick = function (e) {
+ // check for valid target
+ var target = e.target || e.currentTarget || e.srcElement;
+ if (!(target instanceof Element)) {
+ console.log("No element found");
+ return;
+ }
+ var element = target;
+ // Notifications are two layers under
+ for (var i = 0; i < 2; i++) {
+ if (element.tagName !== 'A') {
+ element = element.parentElement;
+ }
+ }
+ if (element.tagName === 'A') {
+ if (!prevented) {
+ var url = element.getAttribute('href');
+ if (!url || url === '#') {
+ return;
+ }
+ console.log("Click intercept " + url);
+ // If Frost is injected, check if loading the url through an overlay works
+ if (Frost.loadUrl(url)) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ else {
+ console.log("Click intercept prevented");
+ }
+ }
+ };
+ /*
+ * On top of the click event, we must stop it for long presses
+ * Since that will conflict with the context menu
+ * Note that we only override it on conditions where the context menu
+ * Will occur
+ */
+ var _frostPreventClick = function () {
+ console.log("Click _frostPrevented");
+ prevented = true;
+ };
+ document.addEventListener('click', _frostAClick, true);
+ var clickTimeout = undefined;
+ document.addEventListener('touchstart', function () {
+ clickTimeout = setTimeout(_frostPreventClick, 400);
+ }, true);
+ document.addEventListener('touchend', function () {
+ prevented = false;
+ clearTimeout(clickTimeout);
+ }, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/click_a.ts b/app/src/web/assets/js/click_a.ts
new file mode 100644
index 00000000..5023610e
--- /dev/null
+++ b/app/src/web/assets/js/click_a.ts
@@ -0,0 +1,57 @@
+(function () {
+ let prevented = false;
+
+ const _frostAClick = (e: Event) => {
+ // check for valid target
+ const target = e.target || e.currentTarget || e.srcElement;
+ if (!(target instanceof Element)) {
+ console.log("No element found");
+ return
+ }
+ let element: Element = target;
+ // Notifications are two layers under
+ for (let i = 0; i < 2; i++) {
+ if (element.tagName !== 'A') {
+ element = <Element>element.parentElement;
+ }
+ }
+ if (element.tagName === 'A') {
+ if (!prevented) {
+ const url = element.getAttribute('href');
+ if (!url || url === '#') {
+ return
+ }
+ console.log(`Click intercept ${url}`);
+ // If Frost is injected, check if loading the url through an overlay works
+ if (Frost.loadUrl(url)) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ } else {
+ console.log("Click intercept prevented")
+ }
+ }
+ };
+
+ /*
+ * On top of the click event, we must stop it for long presses
+ * Since that will conflict with the context menu
+ * Note that we only override it on conditions where the context menu
+ * Will occur
+ */
+ const _frostPreventClick = () => {
+ console.log("Click _frostPrevented");
+ prevented = true;
+ };
+
+ document.addEventListener('click', _frostAClick, true);
+ let clickTimeout: number | undefined = undefined;
+ document.addEventListener('touchstart', () => {
+ clickTimeout = setTimeout(_frostPreventClick, 400);
+ }, true);
+ document.addEventListener('touchend', () => {
+ prevented = false;
+ clearTimeout(clickTimeout)
+ }, true);
+}).call(undefined);
+
diff --git a/app/src/web/assets/js/click_debugger.js b/app/src/web/assets/js/click_debugger.js
new file mode 100644
index 00000000..aab4572d
--- /dev/null
+++ b/app/src/web/assets/js/click_debugger.js
@@ -0,0 +1,14 @@
+"use strict";
+// For desktop only
+(function () {
+ var _frostAContext = function (e) {
+ // Commonality; check for valid target
+ var element = e.target || e.currentTarget || e.srcElement;
+ if (!(element instanceof Element)) {
+ console.log("No element found");
+ return;
+ }
+ console.log("Clicked element " + element.tagName + " " + element.className);
+ };
+ document.addEventListener('contextmenu', _frostAContext, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/click_debugger.ts b/app/src/web/assets/js/click_debugger.ts
new file mode 100644
index 00000000..088271fa
--- /dev/null
+++ b/app/src/web/assets/js/click_debugger.ts
@@ -0,0 +1,15 @@
+// For desktop only
+
+(function () {
+ const _frostAContext = (e: Event) => {
+ // Commonality; check for valid target
+ const element = e.target || e.currentTarget || e.srcElement;
+ if (!(element instanceof Element)) {
+ console.log("No element found");
+ return
+ }
+ console.log(`Clicked element ${element.tagName} ${element.className}`);
+ };
+
+ document.addEventListener('contextmenu', _frostAContext, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/context_a.js b/app/src/web/assets/js/context_a.js
new file mode 100644
index 00000000..7e636cea
--- /dev/null
+++ b/app/src/web/assets/js/context_a.js
@@ -0,0 +1,68 @@
+"use strict";
+/**
+ * Context menu for links
+ * Largely mimics click_a.js
+ */
+(function () {
+ var longClick = false;
+ var _frostAContext = function (e) {
+ Frost.longClick(true);
+ longClick = true;
+ /*
+ * Commonality; check for valid target
+ */
+ var target = e.target || e.currentTarget || e.srcElement;
+ if (!(target instanceof Element)) {
+ console.log("No element found");
+ return;
+ }
+ var element = target;
+ // Notifications are two layers under
+ for (var i = 0; i < 2; i++) {
+ if (element.tagName != 'A') {
+ element = element.parentElement;
+ }
+ }
+ if (element.tagName == 'A') {
+ var url = element.getAttribute('href');
+ if (!url || url == '#') {
+ return;
+ }
+ var text = element.parentElement.innerText;
+ // Check if image item exists, first in children and then in parent
+ var image = element.querySelector("[style*=\"background-image: url(\"]");
+ if (!image) {
+ image = element.parentElement.querySelector("[style*=\"background-image: url(\"]");
+ }
+ if (image) {
+ var imageUrl = window.getComputedStyle(image, null).backgroundImage.trim().slice(4, -1);
+ console.log("Context image: " + imageUrl);
+ Frost.loadImage(imageUrl, text);
+ e.stopPropagation();
+ e.preventDefault();
+ return;
+ }
+ // Check if true img exists
+ var img = element.querySelector("img[src*=scontent]");
+ if (img instanceof HTMLMediaElement) {
+ var imgUrl = img.src;
+ console.log("Context img: " + imgUrl);
+ Frost.loadImage(imgUrl, text);
+ e.stopPropagation();
+ e.preventDefault();
+ return;
+ }
+ console.log("Context content " + url + " " + text);
+ Frost.contextMenu(url, text);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+ document.addEventListener('contextmenu', _frostAContext, true);
+ document.addEventListener('touchend', function () {
+ if (longClick) {
+ Frost.longClick(false);
+ longClick = false;
+ }
+ }, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/context_a.ts b/app/src/web/assets/js/context_a.ts
new file mode 100644
index 00000000..16ed33a9
--- /dev/null
+++ b/app/src/web/assets/js/context_a.ts
@@ -0,0 +1,69 @@
+/**
+ * Context menu for links
+ * Largely mimics click_a.js
+ */
+(function () {
+ let longClick = false;
+ const _frostAContext = (e: Event) => {
+ Frost.longClick(true);
+ longClick = true;
+
+ /*
+ * Commonality; check for valid target
+ */
+ const target = e.target || e.currentTarget || e.srcElement;
+ if (!(target instanceof Element)) {
+ console.log("No element found");
+ return
+ }
+ let element: Element = target;
+ // Notifications are two layers under
+ for (let i = 0; i < 2; i++) {
+ if (element.tagName != 'A') {
+ element = <Element>element.parentElement;
+ }
+ }
+ if (element.tagName == 'A') {
+ const url = element.getAttribute('href');
+ if (!url || url == '#') {
+ return
+ }
+ const text = (<HTMLElement>element.parentElement).innerText;
+ // Check if image item exists, first in children and then in parent
+ let image = element.querySelector("[style*=\"background-image: url(\"]");
+ if (!image) {
+ image = (<Element>element.parentElement).querySelector("[style*=\"background-image: url(\"]")
+ }
+ if (image) {
+ const imageUrl = (<String>window.getComputedStyle(image, null).backgroundImage).trim().slice(4, -1);
+ console.log(`Context image: ${imageUrl}`);
+ Frost.loadImage(imageUrl, text);
+ e.stopPropagation();
+ e.preventDefault();
+ return
+ }
+ // Check if true img exists
+ const img = element.querySelector("img[src*=scontent]");
+ if (img instanceof HTMLMediaElement) {
+ const imgUrl = img.src;
+ console.log(`Context img: ${imgUrl}`);
+ Frost.loadImage(imgUrl, text);
+ e.stopPropagation();
+ e.preventDefault();
+ return
+ }
+ console.log(`Context content ${url} ${text}`);
+ Frost.contextMenu(url, text);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+
+ document.addEventListener('contextmenu', _frostAContext, true);
+ document.addEventListener('touchend', () => {
+ if (longClick) {
+ Frost.longClick(false);
+ longClick = false
+ }
+ }, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/document_watcher.js b/app/src/web/assets/js/document_watcher.js
new file mode 100644
index 00000000..f3c4ab25
--- /dev/null
+++ b/app/src/web/assets/js/document_watcher.js
@@ -0,0 +1,24 @@
+"use strict";
+// Emit key once half the viewport is covered
+(function () {
+ var isReady = function () {
+ return document.body.scrollHeight > innerHeight + 100;
+ };
+ if (isReady()) {
+ console.log('Already ready');
+ Frost.isReady();
+ return;
+ }
+ console.log('Injected document watcher');
+ var observer = new MutationObserver(function () {
+ if (isReady()) {
+ observer.disconnect();
+ Frost.isReady();
+ console.log("Documented surpassed height in " + performance.now());
+ }
+ });
+ observer.observe(document, {
+ childList: true,
+ subtree: true
+ });
+}).call(undefined);
diff --git a/app/src/web/assets/js/document_watcher.ts b/app/src/web/assets/js/document_watcher.ts
new file mode 100644
index 00000000..e671149c
--- /dev/null
+++ b/app/src/web/assets/js/document_watcher.ts
@@ -0,0 +1,27 @@
+// Emit key once half the viewport is covered
+(function () {
+ const isReady = () => {
+ return document.body.scrollHeight > innerHeight + 100
+ };
+
+ if (isReady()) {
+ console.log('Already ready');
+ Frost.isReady();
+ return
+ }
+
+ console.log('Injected document watcher');
+
+ const observer = new MutationObserver(() => {
+ if (isReady()) {
+ observer.disconnect();
+ Frost.isReady();
+ console.log(`Documented surpassed height in ${performance.now()}`);
+ }
+ });
+
+ observer.observe(document, {
+ childList: true,
+ subtree: true
+ })
+}).call(undefined);
diff --git a/app/src/web/assets/js/header_badges.js b/app/src/web/assets/js/header_badges.js
new file mode 100644
index 00000000..daaf540a
--- /dev/null
+++ b/app/src/web/assets/js/header_badges.js
@@ -0,0 +1,8 @@
+"use strict";
+// Fetches the header contents if it exists
+(function () {
+ var header = document.getElementById('mJewelNav');
+ if (header) {
+ Frost.handleHeader(header.outerHTML);
+ }
+}).call(undefined);
diff --git a/app/src/web/assets/js/header_badges.ts b/app/src/web/assets/js/header_badges.ts
new file mode 100644
index 00000000..473749f2
--- /dev/null
+++ b/app/src/web/assets/js/header_badges.ts
@@ -0,0 +1,7 @@
+// Fetches the header contents if it exists
+(function() {
+ const header = document.getElementById('mJewelNav');
+ if (header) {
+ Frost.handleHeader(header.outerHTML);
+ }
+}).call(undefined);
diff --git a/app/src/web/assets/js/header_hider.js b/app/src/web/assets/js/header_hider.js
new file mode 100644
index 00000000..faa9f66d
--- /dev/null
+++ b/app/src/web/assets/js/header_hider.js
@@ -0,0 +1,12 @@
+"use strict";
+(function () {
+ var header = document.querySelector('#header');
+ if (!header) {
+ return;
+ }
+ var jewel = header.querySelector('#mJewelNav');
+ if (!jewel) {
+ return;
+ }
+ header.style.display = 'none';
+}).call(undefined);
diff --git a/app/src/web/assets/js/header_hider.ts b/app/src/web/assets/js/header_hider.ts
new file mode 100644
index 00000000..1a8f27f2
--- /dev/null
+++ b/app/src/web/assets/js/header_hider.ts
@@ -0,0 +1,17 @@
+(function () {
+ const header = document.querySelector('#header');
+
+ if (!header) {
+ return
+ }
+
+ const jewel = header.querySelector('#mJewelNav');
+
+ if (!jewel) {
+ return
+ }
+
+ (<HTMLElement>header).style.display = 'none'
+}).call(undefined);
+
+
diff --git a/app/src/web/assets/js/media.js b/app/src/web/assets/js/media.js
new file mode 100644
index 00000000..571168d6
--- /dev/null
+++ b/app/src/web/assets/js/media.js
@@ -0,0 +1,43 @@
+"use strict";
+// Handles media events
+(function () {
+ var _frostMediaClick = function (e) {
+ var target = e.target || e.srcElement;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+ var element = target;
+ var dataset = element.dataset;
+ if (!dataset || !dataset.sigil || dataset.sigil.toLowerCase().indexOf('inlinevideo') == -1) {
+ return;
+ }
+ var i = 0;
+ while (!element.hasAttribute('data-store')) {
+ if (++i > 2) {
+ return;
+ }
+ element = element.parentNode;
+ }
+ var store = element.dataset.store;
+ if (!store) {
+ return;
+ }
+ var dataStore;
+ try {
+ dataStore = JSON.parse(store);
+ }
+ catch (e) {
+ return;
+ }
+ var url = dataStore.src;
+ // !startsWith; see https://stackoverflow.com/a/36876507/4407321
+ if (!url || url.lastIndexOf('http', 0) !== 0) {
+ return;
+ }
+ console.log("Inline video " + url);
+ if (Frost.loadVideo(url, dataStore.animatedGifVideo || false)) {
+ e.stopPropagation();
+ }
+ };
+ document.addEventListener('click', _frostMediaClick, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/media.ts b/app/src/web/assets/js/media.ts
new file mode 100644
index 00000000..5b9b1a54
--- /dev/null
+++ b/app/src/web/assets/js/media.ts
@@ -0,0 +1,47 @@
+// Handles media events
+(function () {
+ const _frostMediaClick = (e: Event) => {
+ const target = e.target || e.srcElement;
+ if (!(target instanceof HTMLElement)) {
+ return
+ }
+ let element: HTMLElement = target;
+ const dataset = element.dataset;
+ if (!dataset || !dataset.sigil || dataset.sigil.toLowerCase().indexOf('inlinevideo') == -1) {
+ return
+ }
+ let i = 0;
+ while (!element.hasAttribute('data-store')) {
+ if (++i > 2) {
+ return
+ }
+ element = <HTMLElement>element.parentNode;
+ }
+ const store = element.dataset.store;
+ if (!store) {
+ return
+ }
+
+ let dataStore;
+
+ try {
+ dataStore = JSON.parse(store)
+ } catch (e) {
+ return
+ }
+
+ const url = dataStore.src;
+
+ // !startsWith; see https://stackoverflow.com/a/36876507/4407321
+ if (!url || url.lastIndexOf('http', 0) !== 0) {
+ return
+ }
+
+ console.log(`Inline video ${url}`);
+ if (Frost.loadVideo(url, dataStore.animatedGifVideo || false)) {
+ e.stopPropagation()
+ }
+ };
+
+ document.addEventListener('click', _frostMediaClick, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/menu.js b/app/src/web/assets/js/menu.js
new file mode 100644
index 00000000..c30e93cf
--- /dev/null
+++ b/app/src/web/assets/js/menu.js
@@ -0,0 +1,57 @@
+"use strict";
+// Click menu and move contents to main view
+(function () {
+ var viewport = document.querySelector("#viewport");
+ var root = document.querySelector("#root");
+ var bookmarkJewel = document.querySelector("#bookmarks_jewel");
+ if (!viewport || !root || !bookmarkJewel) {
+ console.log('Menu.js: main elements not found');
+ Frost.emit(0);
+ return;
+ }
+ var menuA = bookmarkJewel.querySelector("a");
+ if (!menuA) {
+ console.log('Menu.js: menu links not found');
+ Frost.emit(0);
+ return;
+ }
+ var jewel = document.querySelector('#mJewelNav');
+ if (!jewel) {
+ console.log('Menu.js: jewel is null');
+ return;
+ }
+ var y = new MutationObserver(function () {
+ viewport.removeAttribute('style');
+ root.removeAttribute('style');
+ });
+ y.observe(viewport, {
+ attributes: true
+ });
+ y.observe(root, {
+ attributes: true
+ });
+ var x = new MutationObserver(function () {
+ var menu = document.querySelector('.mSideMenu');
+ if (menu) {
+ x.disconnect();
+ console.log("Found side menu");
+ // Transfer elements
+ while (root.firstChild) {
+ root.removeChild(root.firstChild);
+ }
+ while (menu.childNodes.length) {
+ viewport.appendChild(menu.childNodes[0]);
+ }
+ Frost.emit(0);
+ setTimeout(function () {
+ y.disconnect();
+ console.log('Unhook styler');
+ }, 500);
+ }
+ });
+ x.observe(jewel, {
+ childList: true,
+ subtree: true
+ });
+ menuA.click();
+}).call(undefined);
diff --git a/app/src/web/assets/js/menu.ts b/app/src/web/assets/js/menu.ts
new file mode 100644
index 00000000..6f9dbf16
--- /dev/null
+++ b/app/src/web/assets/js/menu.ts
@@ -0,0 +1,59 @@
+// Click menu and move contents to main view
+(function () {
+ const viewport = document.querySelector("#viewport");
+ const root = document.querySelector("#root");
+ const bookmarkJewel = document.querySelector("#bookmarks_jewel");
+ if (!viewport || !root || !bookmarkJewel) {
+ console.log('Menu.js: main elements not found');
+ Frost.emit(0);
+ return
+ }
+ const menuA = bookmarkJewel.querySelector("a");
+ if (!menuA) {
+ console.log('Menu.js: menu links not found');
+ Frost.emit(0);
+ return
+ }
+ const jewel = document.querySelector('#mJewelNav');
+ if (!jewel) {
+ console.log('Menu.js: jewel is null');
+ return
+ }
+
+ const y = new MutationObserver(() => {
+ viewport.removeAttribute('style');
+ root.removeAttribute('style');
+ });
+
+ y.observe(viewport, {
+ attributes: true
+ });
+ y.observe(root, {
+ attributes: true
+ });
+
+ const x = new MutationObserver(() => {
+ const menu = document.querySelector('.mSideMenu');
+ if (menu) {
+ x.disconnect();
+ console.log("Found side menu");
+ // Transfer elements
+ while (root.firstChild) {
+ root.removeChild(root.firstChild);
+ }
+ while (menu.childNodes.length) {
+ viewport.appendChild(menu.childNodes[0]);
+ }
+ Frost.emit(0);
+ setTimeout(() => {
+ y.disconnect();
+ console.log('Unhook styler');
+ }, 500);
+ }
+ });
+ x.observe(jewel, {
+ childList: true,
+ subtree: true
+ });
+ menuA.click();
+}).call(undefined);
diff --git a/app/src/web/assets/js/notif_msg.js b/app/src/web/assets/js/notif_msg.js
new file mode 100644
index 00000000..20a89e88
--- /dev/null
+++ b/app/src/web/assets/js/notif_msg.js
@@ -0,0 +1,26 @@
+"use strict";
+// Binds callback to an invisible webview to take in the search events
+(function () {
+ var finished = false;
+ var x = new MutationObserver(function () {
+ var _f_thread = document.querySelector('#threadlist_rows');
+ if (!_f_thread) {
+ return;
+ }
+ console.log("Found message threads " + _f_thread.outerHTML);
+ Frost.handleHtml(_f_thread.outerHTML);
+ finished = true;
+ x.disconnect();
+ });
+ x.observe(document, {
+ childList: true,
+ subtree: true
+ });
+ setTimeout(function () {
+ if (!finished) {
+ finished = true;
+ console.log('Message thread timeout cancellation');
+ Frost.handleHtml("");
+ }
+ }, 20000);
+}).call(undefined);
diff --git a/app/src/web/assets/js/notif_msg.ts b/app/src/web/assets/js/notif_msg.ts
new file mode 100644
index 00000000..b7ce7a19
--- /dev/null
+++ b/app/src/web/assets/js/notif_msg.ts
@@ -0,0 +1,25 @@
+// Binds callback to an invisible webview to take in the search events
+(function () {
+ let finished = false;
+ const x = new MutationObserver(() => {
+ const _f_thread = document.querySelector('#threadlist_rows');
+ if (!_f_thread) {
+ return
+ }
+ console.log(`Found message threads ${_f_thread.outerHTML}`);
+ Frost.handleHtml(_f_thread.outerHTML);
+ finished = true;
+ x.disconnect();
+ });
+ x.observe(document, {
+ childList: true,
+ subtree: true
+ });
+ setTimeout(() => {
+ if (!finished) {
+ finished = true;
+ console.log('Message thread timeout cancellation');
+ Frost.handleHtml("")
+ }
+ }, 20000);
+}).call(undefined);
diff --git a/app/src/web/assets/js/textarea_listener.js b/app/src/web/assets/js/textarea_listener.js
new file mode 100644
index 00000000..9a8783c1
--- /dev/null
+++ b/app/src/web/assets/js/textarea_listener.js
@@ -0,0 +1,31 @@
+"use strict";
+/*
+ * focus listener for textareas
+ * since swipe to refresh is quite sensitive, we will disable it
+ * when we detect a user typing
+ * note that this extends passed having a keyboard opened,
+ * as a user may still be reviewing his/her post
+ * swiping should automatically be reset on refresh
+ */
+(function () {
+ var _frostFocus = function (e) {
+ var element = e.target || e.srcElement;
+ if (!(element instanceof Element)) {
+ return;
+ }
+ console.log("FrostJSI focus, " + element.tagName);
+ if (element.tagName == 'TEXTAREA') {
+ Frost.disableSwipeRefresh(true);
+ }
+ };
+ var _frostBlur = function (e) {
+ var element = e.target || e.srcElement;
+ if (!(element instanceof Element)) {
+ return;
+ }
+ console.log("FrostJSI blur, " + element.tagName);
+ Frost.disableSwipeRefresh(false);
+ };
+ document.addEventListener("focus", _frostFocus, true);
+ document.addEventListener("blur", _frostBlur, true);
+}).call(undefined);
diff --git a/app/src/web/assets/js/textarea_listener.ts b/app/src/web/assets/js/textarea_listener.ts
new file mode 100644
index 00000000..9d5fd388
--- /dev/null
+++ b/app/src/web/assets/js/textarea_listener.ts
@@ -0,0 +1,31 @@
+/*
+ * focus listener for textareas
+ * since swipe to refresh is quite sensitive, we will disable it
+ * when we detect a user typing
+ * note that this extends passed having a keyboard opened,
+ * as a user may still be reviewing his/her post
+ * swiping should automatically be reset on refresh
+ */
+(function () {
+ const _frostFocus = (e: Event) => {
+ const element = e.target || e.srcElement;
+ if (!(element instanceof Element)) {
+ return
+ }
+ console.log(`FrostJSI focus, ${element.tagName}`);
+ if (element.tagName == 'TEXTAREA') {
+ Frost.disableSwipeRefresh(true);
+ }
+ };
+
+ const _frostBlur = (e: Event) => {
+ const element = e.target || e.srcElement;
+ if (!(element instanceof Element)) {
+ return
+ }
+ console.log(`FrostJSI blur, ${element.tagName}`);
+ Frost.disableSwipeRefresh(false);
+ };
+ document.addEventListener("focus", _frostFocus, true);
+ document.addEventListener("blur", _frostBlur, true);
+}).call(undefined);