From 17c7b6a1bfda195fca032efce6a6f6b67534af12 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 8 Jun 2022 08:37:19 -0400 Subject: [PATCH] Add reactive tray icon for when update is available (#5567) --- Makefile | 1 + build/generate-tray-icons.ts | 138 ++++++++++++++---- ...{trayIconDarkTemplate.png => trayIcon.png} | Bin ...conDarkTemplate@2x.png => trayIcon@2x.png} | Bin build/tray/trayIcon@3x.png | Bin 0 -> 1039 bytes build/tray/trayIcon@4x.png | Bin 0 -> 1368 bytes build/tray/trayIconTemplate@3x.png | Bin 0 -> 1031 bytes build/tray/trayIconTemplate@4x.png | Bin 0 -> 1371 bytes build/tray/trayIconUpdateAvailable.png | Bin 0 -> 518 bytes build/tray/trayIconUpdateAvailable@2x.png | Bin 0 -> 1232 bytes build/tray/trayIconUpdateAvailable@3x.png | Bin 0 -> 1995 bytes build/tray/trayIconUpdateAvailable@4x.png | Bin 0 -> 3035 bytes .../tray/trayIconUpdateAvailableTemplate.png | Bin 0 -> 466 bytes .../trayIconUpdateAvailableTemplate@2x.png | Bin 0 -> 1048 bytes .../trayIconUpdateAvailableTemplate@3x.png | Bin 0 -> 1658 bytes .../trayIconUpdateAvailableTemplate@4x.png | Bin 0 -> 2439 bytes .../installing-update-using-tray.test.ts.snap | 20 +-- .../installing-update-using-tray.test.ts | 41 +++++- .../application-update/update-channels.ts | 1 + .../sync-box/sync-box-injection-token.ts | 17 ++- .../electron-tray/electron-tray.injectable.ts | 92 +++--------- .../tray/menu-icon/reactive.injectable.ts | 37 +++++ .../menu-icon/start-reactivity.injectable.ts | 28 ++++ .../menu-icon/stop-reactivity.injectable.ts | 25 ++++ .../reactive-tray-menu-items/converters.ts | 40 +++++ .../reactive-tray-menu-items.injectable.ts | 16 +- src/main/tray/tray-icon-path.injectable.ts | 25 +++- src/main/tray/tray-menu-items.injectable.ts | 3 +- src/main/tray/tray.ts | 99 ------------- src/renderer/components/icon/icon.tsx | 2 + src/renderer/components/icon/notice.svg | 30 ++++ .../test-utils/get-application-builder.tsx | 25 +++- 32 files changed, 399 insertions(+), 241 deletions(-) rename build/tray/{trayIconDarkTemplate.png => trayIcon.png} (100%) rename build/tray/{trayIconDarkTemplate@2x.png => trayIcon@2x.png} (100%) create mode 100644 build/tray/trayIcon@3x.png create mode 100644 build/tray/trayIcon@4x.png create mode 100644 build/tray/trayIconTemplate@3x.png create mode 100644 build/tray/trayIconTemplate@4x.png create mode 100644 build/tray/trayIconUpdateAvailable.png create mode 100644 build/tray/trayIconUpdateAvailable@2x.png create mode 100644 build/tray/trayIconUpdateAvailable@3x.png create mode 100644 build/tray/trayIconUpdateAvailable@4x.png create mode 100644 build/tray/trayIconUpdateAvailableTemplate.png create mode 100644 build/tray/trayIconUpdateAvailableTemplate@2x.png create mode 100644 build/tray/trayIconUpdateAvailableTemplate@3x.png create mode 100644 build/tray/trayIconUpdateAvailableTemplate@4x.png create mode 100644 src/main/tray/menu-icon/reactive.injectable.ts create mode 100644 src/main/tray/menu-icon/start-reactivity.injectable.ts create mode 100644 src/main/tray/menu-icon/stop-reactivity.injectable.ts create mode 100644 src/main/tray/reactive-tray-menu-items/converters.ts delete mode 100644 src/main/tray/tray.ts create mode 100644 src/renderer/components/icon/notice.svg diff --git a/Makefile b/Makefile index 48ce768766..9dfb2cf512 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,7 @@ integration: build build: node_modules binaries/client yarn run npm:fix-build-version $(MAKE) build-extensions -B + yarn run build:tray-icons yarn run compile ifeq "$(DETECTED_OS)" "Windows" # https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish diff --git a/build/generate-tray-icons.ts b/build/generate-tray-icons.ts index a7ab3bd48b..ed90d27832 100644 --- a/build/generate-tray-icons.ts +++ b/build/generate-tray-icons.ts @@ -3,8 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { readFileSync } from "fs"; -import { ensureDirSync } from "fs-extra"; +import { ensureDir, readFile } from "fs-extra"; import { JSDOM } from "jsdom"; import path from "path"; import sharp from "sharp"; @@ -12,39 +11,120 @@ import sharp from "sharp"; const size = Number(process.env.OUTPUT_SIZE || "16"); const outputFolder = process.env.OUTPUT_DIR || "./build/tray"; const inputFile = process.env.INPUT_SVG_PATH || "./src/renderer/components/icon/logo-lens.svg"; +const noticeFile = process.env.NOTICE_SVG_PATH || "./src/renderer/components/icon/notice.svg"; -const svgData = readFileSync(inputFile, { encoding: "utf-8" }); -const svgDom = new JSDOM(`${svgData}`); -const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; +async function ensureOutputFoler() { + await ensureDir(outputFolder); +} -svgRoot.innerHTML += ``; -const lightTemplate = svgRoot.outerHTML; +function getSvgStyling(colouring: "dark" | "light"): string { + return ` + + `; +} -svgRoot.innerHTML += ``; +type TargetSystems = "macos" | "windows-or-linux"; -const darkTemplate = svgRoot.outerHTML; +async function getBaseIconImage(system: TargetSystems) { + const svgData = await readFile(inputFile, { encoding: "utf-8" }); + const dom = new JSDOM(`${svgData}`); + const root = dom.window.document.body.getElementsByTagName("svg")[0]; -console.log("Generating tray icon pngs"); + root.innerHTML += getSvgStyling(system === "macos" ? "light" : "dark"); -ensureDirSync(outputFolder); + return Buffer.from(root.outerHTML); +} -Promise.all([ - sharp(Buffer.from(lightTemplate)) +async function generateImage(image: Buffer, size: number, namePrefix: string) { + sharp(image) .resize({ width: size, height: size }) .png() - .toFile(path.join(outputFolder, "trayIconDarkTemplate.png")), - sharp(Buffer.from(lightTemplate)) - .resize({ width: size*2, height: size*2 }) - .png() - .toFile(path.join(outputFolder, "trayIconDarkTemplate@2x.png")), - sharp(Buffer.from(darkTemplate)) - .resize({ width: size, height: size }) - .png() - .toFile(path.join(outputFolder, "trayIconTemplate.png")), - sharp(Buffer.from(darkTemplate)) - .resize({ width: size*2, height: size*2 }) - .png() - .toFile(path.join(outputFolder, "trayIconTemplate@2x.png")), -]) - .then((resolutions) => console.log(`Generated ${resolutions.length} images`)) - .catch(console.error); + .toFile(path.join(outputFolder, `${namePrefix}.png`)); +} + +async function generateImages(image: Buffer, size: number, name: string) { + await Promise.all([ + generateImage(image, size, name), + generateImage(image, size*2, `${name}@2x`), + generateImage(image, size*3, `${name}@3x`), + generateImage(image, size*4, `${name}@4x`), + ]); +} + +async function generateUpdateAvailableImages(baseImage: Buffer, system: TargetSystems) { + const noticeIconImage = await getNoticeIconImage(system); + const circleBuffer = await sharp(Buffer.from(` + + + + `)) + .toBuffer(); + + return sharp(baseImage) + .resize({ width: 128, height: 128 }) + .composite([ + { + input: circleBuffer, + top: 64, + left: 64, + blend: "dest-out", + }, + { + input: ( + await sharp(noticeIconImage) + .resize({ + width: 60, + height: 60, + }) + .toBuffer() + ), + top: 66, + left: 66, + }, + ]) + .toBuffer(); +} + +async function getNoticeIconImage(system: TargetSystems) { + const svgData = await readFile(noticeFile, { encoding: "utf-8" }); + const root = new JSDOM(svgData).window.document.getElementsByTagName("svg")[0]; + + root.innerHTML += getSvgStyling(system === "macos" ? "light" : "dark"); + + return Buffer.from(root.outerHTML); +} + +async function generateTrayIcons() { + try { + console.log("Generating tray icon pngs"); + await ensureOutputFoler(); + + const baseIconTemplateImage = await getBaseIconImage("macos"); + const updateAvailableTemplateImage = await generateUpdateAvailableImages(baseIconTemplateImage, "macos"); + const baseIconImage = await getBaseIconImage("windows-or-linux"); + const updateAvailableImage = await generateUpdateAvailableImages(baseIconImage, "windows-or-linux"); + + await Promise.all([ + // Templates are for macOS only + generateImages(baseIconTemplateImage, size, "trayIconTemplate"), + generateImages(updateAvailableTemplateImage, size, "trayIconUpdateAvailableTemplate"), + + // Non-templates are for windows and linux + generateImages(baseIconImage, size, "trayIcon"), + generateImages(updateAvailableImage, size, "trayIconUpdateAvailable"), + ]); + + console.log("Generated all images"); + } catch (error) { + console.error(error); + } +} + +generateTrayIcons(); diff --git a/build/tray/trayIconDarkTemplate.png b/build/tray/trayIcon.png similarity index 100% rename from build/tray/trayIconDarkTemplate.png rename to build/tray/trayIcon.png diff --git a/build/tray/trayIconDarkTemplate@2x.png b/build/tray/trayIcon@2x.png similarity index 100% rename from build/tray/trayIconDarkTemplate@2x.png rename to build/tray/trayIcon@2x.png diff --git a/build/tray/trayIcon@3x.png b/build/tray/trayIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c706ec9b3a962c77c1878322552b556818da5ab0 GIT binary patch literal 1039 zcmV+q1n~QbP)x@e@Zjli3z%(iU+zg8V5X)_w<{cfh1r(k;c`y@3w_qF92cRlIz;@x^1UTpE{}veJOTGwrr~{N68`Tq!o4)`$3jZ>~@JPmS5pYmAzltGXoiEy!9K1=&-e zomg)NOvuC4BT2voM*(kuZo*GBD!HG}e1e}jT4fqn>@oaL>$<+FS29xMr~@Yq!UE!BMzl9*owd)^CASOG^`4>P%M+d z)&>TtYw#@ki&!5aU`-kV6gEZ(C^XV=uz-pP0q=oc5h7*>-gkNIOYC3j+C`iWxaS-h zU>NZjEoKwR&GnM*&xXZdc z6tVjPA1!IPXO5m+qB`6289MfGX)BLqvE6Ujp2%WrPZqj^x{(5Tv#lJT!~Q!kTG;mo zJ}21SRD%br0PQ>@1p%_HYz|y?@VO-W2{qA6u$hvTyk)TE3$v~CDO1Xf>EJW5cEE|e zcJ8Fdc_EM_Isnfj#FvY7TjVT240qL$kPjNPmB3E?QDwZ7XRK+{LBwQB>a)FwBZcn( z&pz9h0P08lp(4iWNklH}HT+Hzj>A!;6GRNv$38Zw30-$#Kb<%{&eu7aI)F>UI$>9! z5A;HDR3(vA$s0l@7@J+KJbhP_yFlVKImhQIifE==L3IQ*OMKonAahj%i~ia{cQ&Y> z5jYV3qSNiB23=KSfSxj__OMj{z~k2#utLvJM*yw=HP1|A!Vf8$X2Bv~XiES9002ov JPDHLkV1hy3(C+{M literal 0 HcmV?d00001 diff --git a/build/tray/trayIcon@4x.png b/build/tray/trayIcon@4x.png new file mode 100644 index 0000000000000000000000000000000000000000..22b1c50c2855ca1f2f4d1a134d05f783e51f6525 GIT binary patch literal 1368 zcmV-e1*iInP)$gE z;D;g$!muPvGYi5GRwhKCNe@dgL$CxDOEPtmi0DDkYe0LKslCl{%3o~mHtwv}J@=fw z*Ew?s=J)ZQeg1o|b|t&2K%hzd zHp$Qd_b7d?iO_r3JtQmR`vEI~iQ@Mr;2i_wKat-qaJ_-XKSmUo220o&+aq)d9s}#Nau;s>39)>AvM#IfEg1#x2sA=-UK`~wCW`}Bt-F94z$4Rb9U&X>@=Y#4%OTTf4Vbr$V_udLJ4zzsz^ zU?;ZBv~)n#vRc?Dunk-O1Wq(|z?nHWYqi)`)G{bpZ0P_$&ZmfHlP3H;_PC7lMG?l1 zQg)l}yl_VmfzWh{VM}Bvyxw5jE_onnbbwo{20Tt&I}rFN!U5fqx{bOuuZ~PRl-=g# zfyVDGZQ(IF_T&01MskDb6sLoBo&KxcrrFp(|y|)3(Y}(j!>&mV#?%V87p-A1D{5# zR%ZWxLVKcFnOs$6s4G4UWlbXR{|d2UlCqVy-SALx!uo*0X-DYIzC6~7AArM64Nlpn z=1ok_Q*;(!&zz(TsD!MuGoIPGO!Lv={&Nbd279s;2kgg{xcW6VjmgS=8;lKipVIiG zC!ms`oL}RKe(X|LqWySd<5#)hr$>xes>9Za!L^Du0}QLdTsht)NuvRr<y}?Hfe;sKv-^BQDW4}WyNxfDc+b& zQd=&q6sEkI@sdtTeV?;UUfbqRh>%mg_B0 zy0scl>8*EFE;;xMn5Ni^n`5!VAFSBnJ`@>bvg&ZThl5MuwC)#9mij z11@^3DbAn2m=YL(keQBURBhUeEQeyoGAa(Ty@9gQl6+U$Pv~X%pac0f$+<(Y){q8+ zfe)4eT#a*Fn5B!Cez|K=h&+=Q!W6bx2CzFWfbj|J16}TD9n`)Uomy>-6W0;yGQsrq zHYOavT*db&;G3Zg56jS#t0FiW;XC%QX)Dj9(LJi^o=c;fwpfS`>Rtlm^|o?O2L0c_ z3`Kto@FPa=z8pNHMOgv?sBNX#1Gg<~ZfN~PeT3{=nOS;477KD*|%DIQb;xLSAnx4+N%Pjy$oAWit~>4W-Yt5yiwVg|Uu( zc4R03>WJS9v*aWq)53=}yYm72=p+eavV8V2E!Zks#1s|<;%1|X&|AZs7#GA%4{MlBMK|B z4}B;aAdD=+Y=-EAjSzt)4VKYBPzm;EAT29Gq=BI6LGxs3qvrFOCl;&=hxPWo_nf`g zx$jN7IHl33w+_=^A|nj5PdbnlY~dN6`k@ z3s~x;c#Y08bBq8!3XJ;qx^p4$hqLl;2Ije<$veLSvs$hE1I~(PdTtzH=IK!d zo4vbDW!j-t2DqyN13VO%=f09WdV%g%7+|#C!Szl%ZMcToP*z}}thz;}h-Aj!B#8GZ+hGS80H11LpM zdxGx>eD1We?oL(ybfe%MficSx1Y92Acb((b5;`|l`EkHchS|Ts{$`x`?Egdv)(;$H zc#W^Xw^$eJEG@0WQ2cR=TOtHo6yWo)v+^~PRay#D)YYuV@I*6yXZ7KW3iM0hs+#N& zL^Yyg${66SCd@tvoUj*^5azPLoU1BSUg*`L%1%q ziCD%n61PtZJzwPHA1J}_-T;p!4aQ4S!{XK!DPOyJM8~?gx+Fs})}aNjvQ2?2bYkQc znnLqZ4!^99%V}|aVBBrAIl6U;#8~x=ChqL_0XMqsa7x0m`V!d5@H;#A{31F43lsO2 zq`@tEwGLoI=_2Tg+>%9TL1@A!5}E#~Nzy#zfSgJ^rZ(|NW9hNbWbZjJUW`8;t-t`1 z!XJch92jp^XUG$KRuCbI<24BDotUqV-1CiagR=pC3r*7RIDz=~o{($x3rG3-t2V&5 z6{M%nsoDVRotK%E!@bd4&m2nV4#p&Pd>+wUK%X9G0T3QN)f55^;D;0vEU0<}2>ymky6HoCD49}eB%*>g4=AKHe zQd@vxcdM6xarKsZviPM>YdKdXIKPM0XX;P?m45YsI$`Kq=9%X$8h-z~K(;8dC5ab66m7i*Md%A8;+o2PU$_jwH?j^^H2K4yzZ` zhrp+RTB&V0g}H3d3v~uqQ||*eqT|_*yXu`9q|s7djHH=diDc@z^t&2CX`fY>N`kH= zM=-6B1TXuEP)1M z)S`XmC_1ASR7hY>P$F&GSWrZSL_ZrBK}8f~Aw^MXlOkg#B5E^@&lAs`=e^%M=esj~ z^uo(M_x!y7Kj&0c3y{p%N3y?|6D}ipl@Nf&6@a|n51=p4^#q*Hq-Ja)V)hWp2>^#k z{tDnU$&L~Fr6i~WJ7yF ztufz>kC8l{m|Oxd17KETlKKj!0Ic?zJ!$_R0EQFIzE(@1@SVT+B|fi zX>9PE)~D0?i5yeGuZy|H>0g2Ml?jBVwE)H3VUkOI=3pV%Y})r$5!iFCK!@QHe_sXg zxX1U`TmhLrG!K+T=)e@6hZEB*C~U!OiQrY0W`D#Re*|#3=iadpp;gxK12eyj5YU1;z@JlZnZg=WiobkQqnw_>@jnt-AOb0TY@Q5EZ|K32XMYnu0LFGB}o+%+PA^vO|P$Xu?aZg`k=M< zxd6ym9fQq-X#j&c28I&zGS%+(o+lH5r;^*Yk=#Xcr!~|6_BI3%V_Ey=S^#IkMDlBy zFNBlw2oxMQWi)SwlMU~8KjbnmB`#rz#^(ebY}IxcR{h3F<1hwcA?Dl%g(Ix0Tn zKEKa|GxL?U*tEx`5U*tc22;!IrIg0}+xC0h3$j(N0C2_uu5q@W_6pWE=67(=zMj`i zqM^8{sNsnFDW|0F1A@&(nkp@S0B|IA+N}uk6?fEKq;F_8b4ax6JPQC z>Y~AnJK%C~wiU2Z3q1)y>02%KAdQ%~#!s`){p`vvia=G_-T|O)>o&3cJOp6KjO*M7 zFzK>g$6d9^!!*~3{AiKnACvq|4^vx0jYYLx%3{r-H3X5p0~Yl)r uX|DT(3QVM!aIJm6)u{L3nsmN-nEVT;)K@lMg+J2(0000JJP)-e2Dd!KV= z92fkYJ$wKA|G#x#>)$hIP#=k8F#yE%IV9WT{+W)mM!JcEZx<%w-e{6DNG>P2mgExv zHj-TD_htYXn?VDp6M(ICR)G7+n@uOVmE^}H|6+4`u?#dz);?p6q{Z6YT#C&Bta8wq zP6FrwaDrsF4FFyC0Dv(#PBgcKTO#QKG=Ko9fa7mEV+Qb$gDSLW{=rjp+_e?- zCdmtZ&fq>Xk?cDpe-X)_6!>YuAp=(iAahl^;{p5~CMS%>+}}xF;}{Ai!t5H7Minue z_3hgtrOPK%amk;ko4=d<@yF9 zZ@gMea|Fpo%(Frhp~>Bfv6ihhojYf@`wKnt^7F}=KJxoz4jrohJgNN|zu%oqt%_>C z6oCW49Z|;AOetksTvEkc0G(;A%ru$|-Dj0z&Kfu<9P0uhfdw-65|aNUO~ghMB_!YEpt~hm zwLEaDuIdpDeo~hc#BnWDg@AH~4F(Rm3gexP0y-pQp*GeXM)C&^Jk4QiOeKAz2Qo(7uI4aFWV!R28-4H8Vw zvU_u8)W)Ylr5j9`7LES%0MKQfn|E(r5*Vzb({aq-vyfkt%tHdMk!Ug##(2d0hHapj zR5VEAB@9QDQz|;W* zwLIbG73Isd8b&QCm_u#pM`E#Lv;)B1;tP?u!d&7vgYP6csX?Z-XGJ9|w8Z_cC>RGk zwI`F&zs+ov9!T>id#P;W%Jgh?y~1%-rC4;LuQ?)Xpxqm1>iK62rjjewu80!ukkJlqQ}cl^ zLvpIDH#z4OVRXCCv4l3MEWIxlbvHV^%?z-lt+a7{Z4#i)8@VECsPQr{8m2bMCuPn& zv+7`LcZDfcf|ri1P_3+KpOs>6F9vYhM20)#mRrsZ4fT})Jx)K;;%PL}yp?hUcpk}H zrEIPkM7tDu|GK%YNTwEZ_T*68sB2OtrbaB6#Cw(fYi0K34Hv%CHDhYJ~(n6$lM4tgzB6G&Y*ZS1CMH^@Z+fp$-x>ojjcwFsc zJAi#rmKb^9SdqW-H>Dh(EB>(~?%BT)G$Tt?Q{1^*A^-=&Y@{-6je$NgGS=JE_-Bue z9!BkYnV;rO6ae$xL@YprmW=G$;=DbJ!mihz{h|`k{Ta0nbuLvaJuG=y9%0B#y8GOF&i!wJmF%25>}jpN=Cx0!GiETe4ghTv0A>J~31Bk)*KIOxGWzbEC7 zpdT6E{LBI|`2Q>b#{-xfZ8iY^twg+9f<57Y{*BEjobrhP-VWe10KNlY6M#Da`~$$f z0QLdc17ItFTL4@GU?qUl3Pd0`^{o43L3~2KKOexw05$^nFGd3rpi5BUsELv|JrT%H zq>a24!1ZSKP(sGc>`?#*0XzoaP;O@C&qt#FrwW8!2jE6Cd$p`LnZ@MsLcEh$aY?YY z!og@Jts+JyPCT734u1u3T+BN&PX0|nMUMF;fY<3>vc5TL1hB&k@zfUr_?{fIpX5P5 z?u2kgI&+5!0p1g9RLDpBZFF=FaZVw8*UVhURC{ve>tjibv4RPOp9RYFeOCtF5<;wa1P5^Ls z^gks1_|exUtjR=}7AK%=e@WeM#10O~W||_d1kjWHmgpAhm(|)wkx#mV;`6icDIED8 zb2%O4Cxn{RxN`vPj7?D{+YrFg*n0pxOJ(28UMixI6=@FaLcJDiMAt{kZYZrapa}PKY~vwP5zH7&cEEB8Z>xgDeVlif=MF;Z-tGw~5($V>&sH z?eA2|7fhA|$Nv++f;7h&iC#8M0%TqDh8)9mp`a~qDxHKVr7bds2rLx>WXFgE?F_4P>)C^D`&Bpa~K}2+HIZ*+`LiV?Z8heR9E@)t9N0{p&Ll z`V=wPGk}eIL?^Lg79S6Nx5=6|PKdQDU0`N6^eIEc4{<8b4Nadf{-7FXf%1K-CBW6u>4veWZ1=~!aL?C>}tPS+DPvxR1+rtDBQp@RG=ksEvv z!2JLoIEu}4j}8~=9y-bU`&D$JvTLGTej*elTYsYJcv$Iq?4F&djfx@ zHqXrF`EyKdegtwyyh0q2ho>{aF-#GOe34LRkqbf$Jb3z(47n?i?2$ihbt&|t)w%%^ zj-1HbYS?@b7f`d`o_u+Ko-O^C(H(7oI0@u^pE?ds@Ua{p&u&21kuRRN&lw&x6$LZ9 zM7V%Tbu)n9XQT%K{q-2XA#{k#?*nEwEj=K*>66@RN8;smickvzP|5k^Oi=QqUk2cn zDUmvm0LdhK3jAom_qk4oY-nbiQViY%vO>H=9cEnY#S(8ieuu`6dnxDxGz5U zZs>5}rsAybxGUAURXh-jOi7Vrlt3is`RQ@?)YX*;WZ=0w|mPFMRz*T%b--D9=f$4y}rqp()&D zW*0?T??a-WnA_aiqXg~n!neo?PRG<}k-{%K4D|t$N7yBnnR)$jw;*;v*t!1%4*z#R zzTX`l?(-z%cDPwx;YrN_j;I3$0n6CcIzyD6R-vl$n`nQ+egL<|e!WKdmH2#X{8t)t znVESYbX$DCQXQe0DII<9bY0GHJ6+SdIl6I>A0P;;zu<{i2VSah*bz}?B~~>B9RF_x z%XTrAxr_A?3AzHjOkLqlOyb&hY@ufa9a9m|Fj>;$6seCOE;&!0w?$a_Wi;8OO-0rj zX|68;cu^?Is&<&|ZSiy4md7SUOZ}+r-zO(eSf(a1#<*xV?|oSGgduzm}2mj=rE|z*&2iZOrtFo z{o`e9gxTCLrmn|>tV!VJy8uPs0>GjW0XkA^O2rYfnkNiOcXOtLyv}!1+8n)5ghSJ_ zpX4ynaq}y39J5ZdP6Y>xjgF9mCxH5#5l;U4*k~13%ju1Te}R+EkWC#6;BWHZ`VumN z5Lw!7I$@=n$-)?x{oFMt_?pL-s6`@pNm=dZbW7C-lN(B#uDJ+=d)pOBg>*ZG7R%>8 zziy2FZf4!YK-@l}-uiBqo}RfLY`oR$ROX)ZRiU-*S=GvocjIf zxZl`I040KZNuhCb)wV4W>Qd(#=er5qE@Q5)L#C;BS|{vnZm1G$LWENPc&ZYta+iIeq9B7erYk2yiIB~nmd|AV-DdVd zxnkp@vcE2olJt~cssE-oGrKhB9E6)|%^>O$GzEotoKQ~#6>FF&-XI8(02j(emZaNg zJ&q7HyX_Xap}33K8AbqAR`U`OsNFL+bc+}9zZ2mlX)`B=|Bi?UPL2X9 zxo78)V(Lt}ljN|iCT8fm!Z9Z#CsM2m*1o_Nc+&$bNM4+5`PVQ;3F|w#v zjtC^+9{@blbwU!I<=m*c#RY=&X!7KCh9+NBXaCJ?QPdH4%FvH+PG0i!X=b)E{QOO3 z_U9;I?h9l+MRkXndBtX3=mFm9ToCoZZpy3yxCr=q3+fJNtbn8+k}d9`x_EN4<>l$) zYhIpK+wP9b*}=}6B7oW(ZF~6kYr?~uQ<~$byieE}0IA;B==J%~(Au~7!n8+nBw&0V d#*M{I{vUZmV1!A2grWcd002ovPDHLkV1gt4!e;;g literal 0 HcmV?d00001 diff --git a/build/tray/trayIconUpdateAvailableTemplate.png b/build/tray/trayIconUpdateAvailableTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..72fd9a8cf740647d0286c60b8f55b9b9b4e6559c GIT binary patch literal 466 zcmV;@0WJQCP)>PB zCTK4n)&!*~N^`W$GM9FJioy8ps<89#LEDa-(g*eRaz2hXxEi0KSVdhFTv&B5%Agc( zWpK76(PzBEUF^Z`_!Rhyrm{PtWrT~wMXCb3aDSA*qS%7}U$4`4q@O@(p#T5?07*qo IM6N<$f}63~3;+NC literal 0 HcmV?d00001 diff --git a/build/tray/trayIconUpdateAvailableTemplate@2x.png b/build/tray/trayIconUpdateAvailableTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eed819c648afa788c749415177243700692de1b4 GIT binary patch literal 1048 zcmV+z1n2vSP)4LA)v z9hA6ivL;Q2jFAi3DK&T>y38cRuD1=%m4+g3!;w#O**qL@ZSX5 z!hIDfk?Hr1aM6qe76Tf6R=>M-<`vOwe>$8WSgH~zvCioNw=@cO+AvUi?*yDo6e|S2 z7=q~{O=3Hu4y)2%06UCe-u3dpYfG4BuOdlVJ!IS47V7>M} z2?AV*WxJQ%Z3&u^Vx@2*F)nj2JgR2m@pd7{wBHc+-3sdu249!y4|!-tHvX3Z&{87V zjP|;Cc7zLdhOqSj9|HZd%)Z|*gXZmZQGtOF*R;mZ1ip*FWgZSJXwgGCB~%Pbyc3#J zb5cq*VHh=sSm)s+Fs?hlb>VjST22W3dtUK>yi6l;+LsmP!406^r`%tOz*s)+g*_7B zU9Xe^&jWu8ugbX5aIcr+m}IkBALTmUq%M!{j}U1cD$`Ga&w#x$;QOG+s4Gxk!Kiio zszeApp`HPbhB!yGcf(;-%A`uU)-O0Erg3ei=t(rIMD*OL3Bq60_?=f~;b18U2arbvYgq5m&)8d{$3F$$_$h#RmY!7qE?;j)IGH!QGZtKtCGD= zooRDMG$L>1%=A)ypydb_n(>u`BZ%ciQi-7T-U{a5n$+!{>eu?}akw=y%dbxgSXmfr;kFWAE! SrIGaj0000FP)*c9wRU(KFCbrsCV!fNF2sS3Q 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 FV1mw_YMKB5 literal 0 HcmV?d00001 diff --git a/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap b/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap index 32e6cb1cb1..5c5ebac6e4 100644 --- a/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap +++ b/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap @@ -44,7 +44,7 @@ exports[`installing update using tray when started when user checks for updates > { let applicationBuilder: ApplicationBuilder; let checkForPlatformUpdatesMock: AsyncFnMock; let downloadPlatformUpdateMock: AsyncFnMock; let showApplicationWindowMock: jest.Mock; + let trayIconPaths: TrayIconPaths; beforeEach(() => { applicationBuilder = getApplicationBuilder(); @@ -44,6 +47,7 @@ describe("installing update using tray", () => { mainDi.override(electronUpdaterIsActiveInjectable, () => true); mainDi.override(publishIsConfiguredInjectable, () => true); + trayIconPaths = mainDi.inject(trayIconPathsInjectable); }); }); @@ -58,22 +62,29 @@ describe("installing update using tray", () => { expect(rendered.baseElement).toMatchSnapshot(); }); + it("should use the normal tray icon", () => { + expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal); + }); + it("user cannot install update yet", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); describe("when user checks for updates using tray", () => { let processCheckingForUpdatesPromise: Promise; beforeEach(async () => { - processCheckingForUpdatesPromise = - applicationBuilder.tray.click("check-for-updates"); + processCheckingForUpdatesPromise = applicationBuilder.tray.click("check-for-updates"); }); it("does not show application window yet", () => { expect(showApplicationWindowMock).not.toHaveBeenCalled(); }); + it("should still use the normal tray icon", () => { + expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal); + }); + it("user cannot check for updates again", () => { expect( applicationBuilder.tray.get("check-for-updates")?.enabled.get(), @@ -87,7 +98,7 @@ describe("installing update using tray", () => { }); it("user cannot install update yet", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); it("renders", () => { @@ -107,8 +118,12 @@ describe("installing update using tray", () => { expect(showApplicationWindowMock).toHaveBeenCalled(); }); + it("should still use the normal tray icon", () => { + expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal); + }); + it("user cannot install update", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); it("user can check for updates again", () => { @@ -142,6 +157,10 @@ describe("installing update using tray", () => { expect(showApplicationWindowMock).toHaveBeenCalled(); }); + it("should use the update available icon", () => { + expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.updateAvailable); + }); + it("user cannot check for updates again yet", () => { expect( applicationBuilder.tray.get("check-for-updates")?.enabled.get(), @@ -167,7 +186,7 @@ describe("installing update using tray", () => { }); it("user still cannot install update", () => { - expect(applicationBuilder.tray.get("install-update")).toBeUndefined(); + expect(applicationBuilder.tray.get("install-update")).toBeNull(); }); it("renders", () => { @@ -182,7 +201,11 @@ describe("installing update using tray", () => { it("user cannot install update", () => { expect( applicationBuilder.tray.get("install-update"), - ).toBeUndefined(); + ).toBeNull(); + }); + + it("should revert to use the normal tray icon", () => { + expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.normal); }); it("user can check for updates again", () => { @@ -213,6 +236,10 @@ describe("installing update using tray", () => { ).toBe("Install update some-version"); }); + it("should use the update available icon", () => { + expect(applicationBuilder.tray.getIconPath()).toBe(trayIconPaths.updateAvailable); + }); + it("user can check for updates again", () => { expect( applicationBuilder.tray.get("check-for-updates")?.enabled.get(), diff --git a/src/common/application-update/update-channels.ts b/src/common/application-update/update-channels.ts index c5f7b4b8c1..dff1e5879e 100644 --- a/src/common/application-update/update-channels.ts +++ b/src/common/application-update/update-channels.ts @@ -3,6 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ + export type UpdateChannelId = "alpha" | "beta" | "latest"; const latestChannel: UpdateChannel = { diff --git a/src/common/utils/sync-box/sync-box-injection-token.ts b/src/common/utils/sync-box/sync-box-injection-token.ts index d35c7d5367..76ba0679f3 100644 --- a/src/common/utils/sync-box/sync-box-injection-token.ts +++ b/src/common/utils/sync-box/sync-box-injection-token.ts @@ -4,12 +4,21 @@ */ import { getInjectionToken } from "@ogre-tools/injectable"; import type { IComputedValue } from "mobx"; -import type { JsonValue } from "type-fest"; -export interface SyncBox { +type AsJson = T extends string | number | boolean | null + ? T + : T extends Function + ? never + : T extends Array + ? AsJson[] + : T extends object + ? { [K in keyof T]: AsJson } + : never; + +export interface SyncBox { id: string; - value: IComputedValue; - set: (value: TValue) => void; + value: IComputedValue>; + set: (value: AsJson) => void; } export const syncBoxInjectionToken = getInjectionToken>({ diff --git a/src/main/tray/electron-tray/electron-tray.injectable.ts b/src/main/tray/electron-tray/electron-tray.injectable.ts index 409e7abf3f..a96104f047 100644 --- a/src/main/tray/electron-tray/electron-tray.injectable.ts +++ b/src/main/tray/electron-tray/electron-tray.injectable.ts @@ -5,31 +5,37 @@ import { getInjectable } from "@ogre-tools/injectable"; import { Menu, Tray } from "electron"; import packageJsonInjectable from "../../../common/vars/package-json.injectable"; -import logger from "../../logger"; -import { TRAY_LOG_PREFIX } from "../tray"; import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; -import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token"; -import { pipeline } from "@ogre-tools/fp"; -import { isEmpty, map, filter } from "lodash/fp"; import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; import loggerInjectable from "../../../common/logger.injectable"; -import trayIconPathInjectable from "../tray-icon-path.injectable"; +import trayIconPathsInjectable from "../tray-icon-path.injectable"; +import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token"; +import { convertToElectronMenuTemplate } from "../reactive-tray-menu-items/converters"; + +const TRAY_LOG_PREFIX = "[TRAY]"; + +export interface ElectronTray { + start(): void; + stop(): void; + setMenuItems(menuItems: TrayMenuItem[]): void; + setIconPath(iconPath: string): void; +} const electronTrayInjectable = getInjectable({ id: "electron-tray", - instantiate: (di) => { + instantiate: (di): ElectronTray => { const packageJson = di.inject(packageJsonInjectable); const showApplicationWindow = di.inject(showApplicationWindowInjectable); const isWindows = di.inject(isWindowsInjectable); const logger = di.inject(loggerInjectable); - const trayIconPath = di.inject(trayIconPathInjectable); + const trayIconPaths = di.inject(trayIconPathsInjectable); let tray: Tray; return { start: () => { - tray = new Tray(trayIconPath); + tray = new Tray(trayIconPaths.normal); tray.setToolTip(packageJson.description); tray.setIgnoreDoubleClickEvents(true); @@ -41,21 +47,17 @@ const electronTrayInjectable = getInjectable({ }); } }, - stop: () => { tray.destroy(); }, + setMenuItems: (menuItems) => { + const template = convertToElectronMenuTemplate(menuItems); + const menu = Menu.buildFromTemplate(template); - setMenuItems: (items: TrayMenuItem[]) => { - pipeline( - items, - convertToElectronMenuTemplate, - Menu.buildFromTemplate, - - (template) => { - tray.setContextMenu(template); - }, - ); + tray.setContextMenu(menu); + }, + setIconPath: (iconPath) => { + tray.setImage(iconPath); }, }; }, @@ -64,53 +66,3 @@ const electronTrayInjectable = getInjectable({ }); export default electronTrayInjectable; - -const convertToElectronMenuTemplate = (trayMenuItems: TrayMenuItem[]) => { - const _toTrayMenuOptions = (parentId: string | null) => - pipeline( - trayMenuItems, - - filter((item) => item.parentId === parentId), - - map( - (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => { - if (trayMenuItem.separator) { - return { id: trayMenuItem.id, type: "separator" }; - } - - const childItems = _toTrayMenuOptions(trayMenuItem.id); - - return { - id: trayMenuItem.id, - label: trayMenuItem.label?.get(), - enabled: trayMenuItem.enabled.get(), - toolTip: trayMenuItem.tooltip, - - ...(isEmpty(childItems) - ? { - type: "normal", - submenu: _toTrayMenuOptions(trayMenuItem.id), - - click: () => { - try { - trayMenuItem.click?.(); - } catch (error) { - logger.error( - `${TRAY_LOG_PREFIX}: clicking item "${trayMenuItem.id} failed."`, - { error }, - ); - } - }, - } - : { - type: "submenu", - submenu: _toTrayMenuOptions(trayMenuItem.id), - }), - - }; - }, - ), - ); - - return _toTrayMenuOptions(null); -}; diff --git a/src/main/tray/menu-icon/reactive.injectable.ts b/src/main/tray/menu-icon/reactive.injectable.ts new file mode 100644 index 0000000000..42622ff2a8 --- /dev/null +++ b/src/main/tray/menu-icon/reactive.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { reaction } from "mobx"; +import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable"; +import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; +import trayIconPathsInjectable from "../tray-icon-path.injectable"; + +const reactiveTrayMenuIconInjectable = getInjectable({ + id: "reactive-tray-menu-icon", + instantiate: (di) => { + const discoveredUpdateVersion = di.inject(discoveredUpdateVersionInjectable); + const electronTray = di.inject(electronTrayInjectable); + const trayIconPaths = di.inject(trayIconPathsInjectable); + + return getStartableStoppable("reactive-tray-menu-icon", () => ( + reaction( + () => discoveredUpdateVersion.value.get(), + updateVersion => { + if (updateVersion) { + electronTray.setIconPath(trayIconPaths.updateAvailable); + } else { + electronTray.setIconPath(trayIconPaths.normal); + } + }, + { + fireImmediately: true, + }, + ) + )); + }, +}); + +export default reactiveTrayMenuIconInjectable; diff --git a/src/main/tray/menu-icon/start-reactivity.injectable.ts b/src/main/tray/menu-icon/start-reactivity.injectable.ts new file mode 100644 index 0000000000..373c3cf8fb --- /dev/null +++ b/src/main/tray/menu-icon/start-reactivity.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import startTrayInjectable from "../electron-tray/start-tray.injectable"; +import reactiveTrayMenuIconInjectable from "./reactive.injectable"; + +const startReactiveTrayMenuIconInjectable = getInjectable({ + id: "start-reactive-tray-menu-icon", + + instantiate: (di) => { + const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable); + + return { + run: async () => { + await reactiveTrayMenuIcon.start(); + }, + + runAfter: di.inject(startTrayInjectable), + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startReactiveTrayMenuIconInjectable; diff --git a/src/main/tray/menu-icon/stop-reactivity.injectable.ts b/src/main/tray/menu-icon/stop-reactivity.injectable.ts new file mode 100644 index 0000000000..4b60aaaa54 --- /dev/null +++ b/src/main/tray/menu-icon/stop-reactivity.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; +import reactiveTrayMenuIconInjectable from "./reactive.injectable"; + +const stopReactiveTrayMenuIconInjectable = getInjectable({ + id: "stop-reactive-tray-menu-icon", + + instantiate: (di) => { + const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable); + + return { + run: async () => { + await reactiveTrayMenuIcon.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopReactiveTrayMenuIconInjectable; diff --git a/src/main/tray/reactive-tray-menu-items/converters.ts b/src/main/tray/reactive-tray-menu-items/converters.ts new file mode 100644 index 0000000000..42add7481e --- /dev/null +++ b/src/main/tray/reactive-tray-menu-items/converters.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token"; + +export function convertToElectronMenuTemplate(trayMenuItems: TrayMenuItem[]): Electron.MenuItemConstructorOptions[] { + const toTrayMenuOptions = (parentId: string | null) => ( + trayMenuItems + .filter((item) => item.parentId === parentId) + .map((trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => { + if (trayMenuItem.separator) { + return { id: trayMenuItem.id, type: "separator" }; + } + + const childItems = toTrayMenuOptions(trayMenuItem.id); + + return { + id: trayMenuItem.id, + label: trayMenuItem.label?.get(), + enabled: trayMenuItem.enabled.get(), + toolTip: trayMenuItem.tooltip, + + ...(childItems.length === 0 + ? { + type: "normal", + submenu: toTrayMenuOptions(trayMenuItem.id), + click: trayMenuItem.click, + } + : { + type: "submenu", + submenu: toTrayMenuOptions(trayMenuItem.id), + }), + + }; + }) + ); + + return toTrayMenuOptions(null); +} diff --git a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts index b11654393a..22c3d29399 100644 --- a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts +++ b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts @@ -4,9 +4,9 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; -import { autorun } from "mobx"; -import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable"; +import { reaction } from "mobx"; import electronTrayInjectable from "../electron-tray/electron-tray.injectable"; +import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable"; const reactiveTrayMenuItemsInjectable = getInjectable({ id: "reactive-tray-menu-items", @@ -15,9 +15,15 @@ const reactiveTrayMenuItemsInjectable = getInjectable({ const electronTray = di.inject(electronTrayInjectable); const trayMenuItems = di.inject(trayMenuItemsInjectable); - return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => { - electronTray.setMenuItems(trayMenuItems.get()); - })); + return getStartableStoppable("reactive-tray-menu-items", () => ( + reaction( + () => trayMenuItems.get(), + electronTray.setMenuItems, + { + fireImmediately: true, + }, + ) + )); }, }); diff --git a/src/main/tray/tray-icon-path.injectable.ts b/src/main/tray/tray-icon-path.injectable.ts index 1eb4d13118..df83a2e31c 100644 --- a/src/main/tray/tray-icon-path.injectable.ts +++ b/src/main/tray/tray-icon-path.injectable.ts @@ -6,21 +6,32 @@ import { getInjectable } from "@ogre-tools/injectable"; import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable"; import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; +import isMacInjectable from "../../common/vars/is-mac.injectable"; -const trayIconPathInjectable = getInjectable({ - id: "tray-icon-path", +export interface TrayIconPaths { + normal: string; + updateAvailable: string; +} - instantiate: (di) => { +const trayIconPathsInjectable = getInjectable({ + id: "tray-icon-paths", + + instantiate: (di): TrayIconPaths => { const getAbsolutePath = di.inject(getAbsolutePathInjectable); const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); const isDevelopment = di.inject(isDevelopmentInjectable); - - return getAbsolutePath( + const isMac = di.inject(isMacInjectable); + const baseIconDirectory = getAbsolutePath( staticFilesDirectory, isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras - "trayIconTemplate.png", ); + const fileSuffix = isMac ? "Template.png" : ".png"; + + return { + normal: getAbsolutePath(baseIconDirectory, `trayIcon${fileSuffix}`), + updateAvailable: getAbsolutePath(baseIconDirectory, `trayIconUpdateAvailable${fileSuffix}`), + }; }, }); -export default trayIconPathInjectable; +export default trayIconPathsInjectable; diff --git a/src/main/tray/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-items.injectable.ts index 8ee9d25e5e..c008c123e0 100644 --- a/src/main/tray/tray-menu-items.injectable.ts +++ b/src/main/tray/tray-menu-items.injectable.ts @@ -12,8 +12,7 @@ const trayItemsInjectable = getInjectable({ instantiate: (di) => { const extensions = di.inject(mainExtensionsInjectable); - return computed(() => - extensions.get().flatMap(extension => extension.trayMenus)); + return computed(() => extensions.get().flatMap(extension => extension.trayMenus)); }, }); diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts deleted file mode 100644 index 4d7e39c344..0000000000 --- a/src/main/tray/tray.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import packageInfo from "../../../package.json"; -import { Menu, Tray } from "electron"; -import type { IComputedValue } from "mobx"; -import { autorun } from "mobx"; -import logger from "../logger"; -import { isWindows } from "../../common/vars"; -import type { Disposer } from "../../common/utils"; -import { disposer } from "../../common/utils"; -import type { TrayMenuItem } from "./tray-menu-item/tray-menu-item-injection-token"; -import { pipeline } from "@ogre-tools/fp"; -import { filter, isEmpty, map } from "lodash/fp"; - -export const TRAY_LOG_PREFIX = "[TRAY]"; - -// note: instance of Tray should be saved somewhere, otherwise it disappears -export let tray: Tray | null = null; - -export function initTray( - trayMenuItems: IComputedValue, - showApplicationWindow: () => Promise, - trayIconPath: string, -): Disposer { - tray = new Tray(trayIconPath); - tray.setToolTip(packageInfo.description); - tray.setIgnoreDoubleClickEvents(true); - - if (isWindows) { - tray.on("click", () => { - showApplicationWindow() - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); - }); - } - - return disposer( - autorun(() => { - try { - const options = toTrayMenuOptions(trayMenuItems.get()); - - const menu = Menu.buildFromTemplate(options); - - tray?.setContextMenu(menu); - } catch (error) { - logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error }); - } - }), - () => { - tray?.destroy(); - tray = null; - }, - ); -} - -const toTrayMenuOptions = (trayMenuItems: TrayMenuItem[]) => { - const _toTrayMenuOptions = (parentId: string | null) => - pipeline( - trayMenuItems, - - filter((item) => item.parentId === parentId), - - map( - (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => { - if (trayMenuItem.separator) { - return { id: trayMenuItem.id, type: "separator" }; - } - - const childItems = _toTrayMenuOptions(trayMenuItem.id); - - return { - id: trayMenuItem.id, - label: trayMenuItem.label?.get(), - enabled: trayMenuItem.enabled.get(), - toolTip: trayMenuItem.tooltip, - - ...(isEmpty(childItems) - ? { - type: "normal", - submenu: _toTrayMenuOptions(trayMenuItem.id), - - click: () => { - trayMenuItem.click?.(); - }, - } - : { - type: "submenu", - submenu: _toTrayMenuOptions(trayMenuItem.id), - }), - }; - }, - ), - ); - - return _toTrayMenuOptions(null); -}; - diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index 084b75d804..7655194886 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -29,6 +29,7 @@ import Spinner from "./spinner.svg"; import Ssh from "./ssh.svg"; import Storage from "./storage.svg"; import Terminal from "./terminal.svg"; +import Notice from "./notice.svg"; import User from "./user.svg"; import Users from "./users.svg"; import Wheel from "./wheel.svg"; @@ -58,6 +59,7 @@ const localSvgIcons = new Map([ ["ssh", Ssh], ["storage", Storage], ["terminal", Terminal], + ["notice", Notice], ["user", User], ["users", Users], ["wheel", Wheel], diff --git a/src/renderer/components/icon/notice.svg b/src/renderer/components/icon/notice.svg new file mode 100644 index 0000000000..2774b185f6 --- /dev/null +++ b/src/renderer/components/icon/notice.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index c1df2c5998..a817d21b71 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -24,12 +24,12 @@ import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; import { pipeline } from "@ogre-tools/fp"; -import { flatMap, compact, join, get, filter, find, map, matches } from "lodash/fp"; +import { flatMap, compact, join, get, filter, map, matches, find } from "lodash/fp"; import preferenceNavigationItemsInjectable from "../+preferences/preferences-navigation/preference-navigation-items.injectable"; import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable"; import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable"; -import type { MenuItem, MenuItemConstructorOptions } from "electron"; +import type { MenuItemConstructorOptions, MenuItem } from "electron"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable"; @@ -43,7 +43,6 @@ import { flushPromises } from "../../../common/test-utils/flush-promises"; import type { NamespaceStore } from "../+namespaces/store"; import namespaceStoreInjectable from "../+namespaces/store.injectable"; import historyInjectable from "../../navigation/history.injectable"; -import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token"; import electronTrayInjectable from "../../../main/tray/electron-tray/electron-tray.injectable"; import applicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/application-window.injectable"; import { Notifications } from "../notifications/notifications"; @@ -51,6 +50,8 @@ import broadcastThatRootFrameIsRenderedInjectable from "../../frames/root-frame/ import { getDiForUnitTesting as getRendererDi } from "../../getDiForUnitTesting"; import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTesting"; import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels"; +import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token"; +import trayIconPathsInjectable from "../../../main/tray/tray-icon-path.injectable"; type Callback = (dis: DiContainers) => void | Promise; @@ -65,7 +66,8 @@ export interface ApplicationBuilder { tray: { click: (id: string) => Promise; - get: (id: string) => TrayMenuItem | undefined; + get: (id: string) => TrayMenuItem | null; + getIconPath: () => string; }; applicationMenu: { @@ -166,15 +168,22 @@ export const getApplicationBuilder = () => { computed(() => []), ); + const iconPaths = mainDi.inject(trayIconPathsInjectable); + let trayMenuItemsStateFake: TrayMenuItem[]; + let trayMenuIconPath: string; mainDi.override(electronTrayInjectable, () => ({ - start: () => {}, + start: () => { + trayMenuIconPath = iconPaths.normal; + }, stop: () => {}, - setMenuItems: (items) => { trayMenuItemsStateFake = items; }, + setIconPath: (path) => { + trayMenuIconPath = path; + }, })); let allowedResourcesState: IObservableArray; @@ -222,9 +231,9 @@ export const getApplicationBuilder = () => { tray: { get: (id: string) => { - return trayMenuItemsStateFake.find(matches({ id })); + return trayMenuItemsStateFake.find(matches({ id })) ?? null; }, - + getIconPath: () => trayMenuIconPath, click: async (id: string) => { const menuItem = pipeline( trayMenuItemsStateFake,