From 99e5fbe303614783148dd4e47b13405c606237a9 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Fri, 11 Nov 2022 12:30:21 -0300 Subject: [PATCH] FEATURE: Replyable chat push notifications (#18973) Allows quick inline replies in chat push notifications. This will allow users in compatible platforms (Windows 10+ / Chrome OS / Android N+) to reply directly from the notification UI. Probable follow ups include: - inline replies for posts - handling failure of reply - fallback to draft creation if business logic error - store and try again later if connectivity error - sent inline replies lack the in_reply_to param - i18n of inline reply action text and placeholder --- app/assets/javascripts/service-worker.js.erb | 67 +++++++++++++++--- .../push-notifications/inline_reply.png | Bin 0 -> 2600 bytes 2 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 public/images/push-notifications/inline_reply.png diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb index 8580e6e6fce..e54831fef04 100644 --- a/app/assets/javascripts/service-worker.js.erb +++ b/app/assets/javascripts/service-worker.js.erb @@ -8,6 +8,8 @@ workbox.setConfig({ }); var authUrls = ["auth", "session/sso_login", "session/sso"].map(path => `<%= Discourse.base_path %>/${path}`); +var chatRegex = /\/chat\/channel\/(\d+)\//; +var inlineReplyIcon = "<%= UrlHelper.absolute("/images/push-notifications/inline_reply.png") %>"; var cacheVersion = "1"; var discourseCacheName = "discourse-" + cacheVersion; @@ -134,6 +136,16 @@ function showNotification(title, body, icon, badge, tag, baseUrl, url) { tag: tag } + if (chatRegex.test(url)) { + notificationOptions['actions'] = [{ + action: "reply", + title: "Reply", + placeholder: "reply", + type: "text", + icon: inlineReplyIcon + }]; + } + return self.registration.showNotification(title, notificationOptions); } @@ -163,18 +175,51 @@ self.addEventListener('notificationclick', function(event) { var url = event.notification.data.url; var baseUrl = event.notification.data.baseUrl; - // This looks to see if the current window is already open and - // focuses if it is - event.waitUntil( - clients.matchAll({ type: "window" }) - .then(function(clientList) { - var reusedClientWindow = clientList.some(function(client) { - if (client.url === baseUrl + url && 'focus' in client) { + if (event.action === "reply") { + let csrf; + fetch("/session/csrf", { + credentials: "include", + headers: { + Accept: "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not OK"); + } + return response.json(); + }) + .then((data) => { + csrf = data.csrf; + + let chatTest = url.match(chatRegex); + if (chatTest.length > 0) { + let chatChannel = chatTest[1]; + + fetch(`${baseUrl}/chat/${chatChannel}.json`, { + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "X-CSRF-Token": csrf, + }, + body: `message=${event.reply}`, + method: "POST", + mode: "cors", + }); + } + }); + } else { + // This looks to see if the current window is already open and + // focuses if it is + event.waitUntil( + clients.matchAll({ type: "window" }).then(function (clientList) { + var reusedClientWindow = clientList.some(function (client) { + if (client.url === baseUrl + url && "focus" in client) { client.focus(); return true; } - if ('postMessage' in client && 'focus' in client) { + if ("postMessage" in client && "focus" in client) { client.focus(); client.postMessage({ url: url }); return true; @@ -182,9 +227,11 @@ self.addEventListener('notificationclick', function(event) { return false; }); - if (!reusedClientWindow && clients.openWindow) return clients.openWindow(baseUrl + url); + if (!reusedClientWindow && clients.openWindow) + return clients.openWindow(baseUrl + url); }) - ); + ); + } }); self.addEventListener('message', function(event) { diff --git a/public/images/push-notifications/inline_reply.png b/public/images/push-notifications/inline_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..bafc74baa3b815ee8deb839cbd3381024405f055 GIT binary patch literal 2600 zcmZWqc{r5q9-grbCWI(^)~v}+L^xv^G1=@L-ZTUR;!yX0(83QxVQiVD4;`W zfIu7~5C<)=F*i52!)7QJGC=nUl*1wv3k=W{4S@%6UteE9z;PlYBf&OkAA%ClXf!lV z3xt*cfdUGQ00hfSa@+{G&E8v97;D&CmtxkXkSMI)r@rk$rw1d)WHyVBa;A^k-A#J= zW|AC!cEh9S%ZaiNH){p1v@37Ty=P&4>Tk7j`JqnSH`k8ly(%d#@l0#H!oq4`!Abdy z+}FobOw~&pf6wv3MRCxb z!Y^08BQbT}HW}XuYP$a8xl+W-F`H2s4DmM_rEe25(U-UF%I(R~x#%U;gk*l2AT7;o zZ-GxJmbSv$p#EY+;VHouh3+?$YR95G(}&j5+}+tw{N1izqJHR=TOM zr0cBo71lL^^HC4RU-0$&*CN(ZY;R?;eIn`5WtG6!C=v2xKmNv!i=~#29M{=he`ztQ zY0|rhuE2f#g&SL|_lwO{LmpQmhuWCVnLDo{hvo%Ca`bh(TOHP=FI*-jX3gjuN!5x7 zJf6DVchtZLKcf^RZ*aAf5N~ghBxnbW1_p z=d~)_)REjm?aH)l!>MlV%1woyW*5hHE#@^IzGlVf^s5ApO>^#~_D*iLGBjMctuZm!oc($cD{(vzi&*3g>b5&7!i)KWg`_o=1ag^wuAZs$K9kC$*XnfMUHGVKw z>yzt*{^2r$U{h0sAn&}FqOw0;WbeT?uXZJ zk~r-=jg=mn%J6sh@>U3Z^l7!R)ZNdQ?VjHX$#?5OKHO74jF-p+oT?08yNJ@xs1Wvl z*HZf;JIWeIHZdaC^_0wD8L`C0hxv=y|E-F2- zkc+Pj`ADy9Z!f^I{uh@}R#owm)90I(^~;z_|RclwH#otb<7$p}gv%>S%CR8}?%txn^lb`j48aeZr;do2U2O52PN(>9b-39FB zbg2Dq(gtm$%CzWA)CCO$VGuOqm9HW z{~@F)l4j3-=+BI{F{4vf9=Ml+5VfDmHaDhBCeiV)$ZvPWzO|1Rn1-_j)rRHd2jn`o?drum3eD&1wm9a>h$@~W->WMN zOonR^n6Z~?l{=K@?tkog4bfD~8oqQt zv?-R7$u824S=S32a%`zqm~hR{9G6AN;iMvjQ(3n^KVHSLJSCO?W=Q6c7utS-%pKXo xUWlzP^u*v