diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_01.png b/desktop/public/app_icons_with_badge/app_icon_badge_01.png new file mode 100644 index 0000000000..5304235582 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_01.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_02.png b/desktop/public/app_icons_with_badge/app_icon_badge_02.png new file mode 100644 index 0000000000..4fc56cc3ae Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_02.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_03.png b/desktop/public/app_icons_with_badge/app_icon_badge_03.png new file mode 100644 index 0000000000..aca09d640e Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_03.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_04.png b/desktop/public/app_icons_with_badge/app_icon_badge_04.png new file mode 100644 index 0000000000..bb93313b3f Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_04.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_05.png b/desktop/public/app_icons_with_badge/app_icon_badge_05.png new file mode 100644 index 0000000000..ca6ce09bc2 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_05.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_06.png b/desktop/public/app_icons_with_badge/app_icon_badge_06.png new file mode 100644 index 0000000000..6905501f9a Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_06.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_07.png b/desktop/public/app_icons_with_badge/app_icon_badge_07.png new file mode 100644 index 0000000000..048811ef29 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_07.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_08.png b/desktop/public/app_icons_with_badge/app_icon_badge_08.png new file mode 100644 index 0000000000..b83f6d7d23 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_08.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_09.png b/desktop/public/app_icons_with_badge/app_icon_badge_09.png new file mode 100644 index 0000000000..2cadfd2b26 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_09.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_10.png b/desktop/public/app_icons_with_badge/app_icon_badge_10.png new file mode 100644 index 0000000000..e8fd39a95e Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_10.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_11.png b/desktop/public/app_icons_with_badge/app_icon_badge_11.png new file mode 100644 index 0000000000..d8e83ffe04 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_11.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_12.png b/desktop/public/app_icons_with_badge/app_icon_badge_12.png new file mode 100644 index 0000000000..8c5f930dd7 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_12.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_13.png b/desktop/public/app_icons_with_badge/app_icon_badge_13.png new file mode 100644 index 0000000000..bd6c69d088 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_13.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_14.png b/desktop/public/app_icons_with_badge/app_icon_badge_14.png new file mode 100644 index 0000000000..d3bab24989 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_14.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_15.png b/desktop/public/app_icons_with_badge/app_icon_badge_15.png new file mode 100644 index 0000000000..620bcbf1ec Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_15.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_16.png b/desktop/public/app_icons_with_badge/app_icon_badge_16.png new file mode 100644 index 0000000000..5de3e8620d Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_16.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_17.png b/desktop/public/app_icons_with_badge/app_icon_badge_17.png new file mode 100644 index 0000000000..a20e8a6a73 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_17.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_18.png b/desktop/public/app_icons_with_badge/app_icon_badge_18.png new file mode 100644 index 0000000000..5038b245a4 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_18.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_19.png b/desktop/public/app_icons_with_badge/app_icon_badge_19.png new file mode 100644 index 0000000000..ac921ddc30 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_19.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_20.png b/desktop/public/app_icons_with_badge/app_icon_badge_20.png new file mode 100644 index 0000000000..29f9a8d3e3 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_20.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_21.png b/desktop/public/app_icons_with_badge/app_icon_badge_21.png new file mode 100644 index 0000000000..27ea0faa95 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_21.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_22.png b/desktop/public/app_icons_with_badge/app_icon_badge_22.png new file mode 100644 index 0000000000..e763b08fe4 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_22.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_23.png b/desktop/public/app_icons_with_badge/app_icon_badge_23.png new file mode 100644 index 0000000000..ad41a91f1e Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_23.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_24.png b/desktop/public/app_icons_with_badge/app_icon_badge_24.png new file mode 100644 index 0000000000..84bde07a25 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_24.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_25.png b/desktop/public/app_icons_with_badge/app_icon_badge_25.png new file mode 100644 index 0000000000..b129002486 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_25.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_26.png b/desktop/public/app_icons_with_badge/app_icon_badge_26.png new file mode 100644 index 0000000000..93d96fb115 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_26.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_27.png b/desktop/public/app_icons_with_badge/app_icon_badge_27.png new file mode 100644 index 0000000000..26b44a8340 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_27.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_28.png b/desktop/public/app_icons_with_badge/app_icon_badge_28.png new file mode 100644 index 0000000000..a80446a556 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_28.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_29.png b/desktop/public/app_icons_with_badge/app_icon_badge_29.png new file mode 100644 index 0000000000..9bcd29bfd7 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_29.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_30.png b/desktop/public/app_icons_with_badge/app_icon_badge_30.png new file mode 100644 index 0000000000..217b853c6f Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_30.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_31.png b/desktop/public/app_icons_with_badge/app_icon_badge_31.png new file mode 100644 index 0000000000..38e6717279 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_31.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_32.png b/desktop/public/app_icons_with_badge/app_icon_badge_32.png new file mode 100644 index 0000000000..f01b90a1f2 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_32.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_33.png b/desktop/public/app_icons_with_badge/app_icon_badge_33.png new file mode 100644 index 0000000000..def34ddb7c Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_33.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_34.png b/desktop/public/app_icons_with_badge/app_icon_badge_34.png new file mode 100644 index 0000000000..4da76a59aa Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_34.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_35.png b/desktop/public/app_icons_with_badge/app_icon_badge_35.png new file mode 100644 index 0000000000..7c2cb7ae81 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_35.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_36.png b/desktop/public/app_icons_with_badge/app_icon_badge_36.png new file mode 100644 index 0000000000..35193e4f21 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_36.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_37.png b/desktop/public/app_icons_with_badge/app_icon_badge_37.png new file mode 100644 index 0000000000..cd01ef71be Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_37.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_38.png b/desktop/public/app_icons_with_badge/app_icon_badge_38.png new file mode 100644 index 0000000000..30f5dab0fa Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_38.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_39.png b/desktop/public/app_icons_with_badge/app_icon_badge_39.png new file mode 100644 index 0000000000..50810be147 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_39.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_40.png b/desktop/public/app_icons_with_badge/app_icon_badge_40.png new file mode 100644 index 0000000000..627a8f022a Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_40.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_41.png b/desktop/public/app_icons_with_badge/app_icon_badge_41.png new file mode 100644 index 0000000000..e46c922aff Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_41.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_42.png b/desktop/public/app_icons_with_badge/app_icon_badge_42.png new file mode 100644 index 0000000000..34c163ad70 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_42.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_43.png b/desktop/public/app_icons_with_badge/app_icon_badge_43.png new file mode 100644 index 0000000000..22aa9514c3 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_43.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_44.png b/desktop/public/app_icons_with_badge/app_icon_badge_44.png new file mode 100644 index 0000000000..41fee189d9 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_44.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_45.png b/desktop/public/app_icons_with_badge/app_icon_badge_45.png new file mode 100644 index 0000000000..0c1629fb65 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_45.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_46.png b/desktop/public/app_icons_with_badge/app_icon_badge_46.png new file mode 100644 index 0000000000..d283a9f799 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_46.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_47.png b/desktop/public/app_icons_with_badge/app_icon_badge_47.png new file mode 100644 index 0000000000..2bf653c004 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_47.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_48.png b/desktop/public/app_icons_with_badge/app_icon_badge_48.png new file mode 100644 index 0000000000..2d56a00e05 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_48.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_49.png b/desktop/public/app_icons_with_badge/app_icon_badge_49.png new file mode 100644 index 0000000000..bdb1641d25 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_49.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_50.png b/desktop/public/app_icons_with_badge/app_icon_badge_50.png new file mode 100644 index 0000000000..b171b97b03 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_50.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_51.png b/desktop/public/app_icons_with_badge/app_icon_badge_51.png new file mode 100644 index 0000000000..e92dd23f05 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_51.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_52.png b/desktop/public/app_icons_with_badge/app_icon_badge_52.png new file mode 100644 index 0000000000..1a9ca26dfe Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_52.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_53.png b/desktop/public/app_icons_with_badge/app_icon_badge_53.png new file mode 100644 index 0000000000..ec98cb477a Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_53.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_54.png b/desktop/public/app_icons_with_badge/app_icon_badge_54.png new file mode 100644 index 0000000000..c6092d5c37 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_54.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_55.png b/desktop/public/app_icons_with_badge/app_icon_badge_55.png new file mode 100644 index 0000000000..b562011b52 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_55.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_56.png b/desktop/public/app_icons_with_badge/app_icon_badge_56.png new file mode 100644 index 0000000000..e2e14dfdef Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_56.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_57.png b/desktop/public/app_icons_with_badge/app_icon_badge_57.png new file mode 100644 index 0000000000..ca14e061d6 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_57.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_58.png b/desktop/public/app_icons_with_badge/app_icon_badge_58.png new file mode 100644 index 0000000000..6ba64128eb Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_58.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_59.png b/desktop/public/app_icons_with_badge/app_icon_badge_59.png new file mode 100644 index 0000000000..0bc442c77d Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_59.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_60.png b/desktop/public/app_icons_with_badge/app_icon_badge_60.png new file mode 100644 index 0000000000..88ab1af0bb Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_60.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_61.png b/desktop/public/app_icons_with_badge/app_icon_badge_61.png new file mode 100644 index 0000000000..791c9dcf19 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_61.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_62.png b/desktop/public/app_icons_with_badge/app_icon_badge_62.png new file mode 100644 index 0000000000..d6b6a8da86 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_62.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_63.png b/desktop/public/app_icons_with_badge/app_icon_badge_63.png new file mode 100644 index 0000000000..24ac975b10 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_63.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_64.png b/desktop/public/app_icons_with_badge/app_icon_badge_64.png new file mode 100644 index 0000000000..c167cf7565 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_64.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_65.png b/desktop/public/app_icons_with_badge/app_icon_badge_65.png new file mode 100644 index 0000000000..fd439b653a Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_65.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_66.png b/desktop/public/app_icons_with_badge/app_icon_badge_66.png new file mode 100644 index 0000000000..a4b2512136 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_66.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_67.png b/desktop/public/app_icons_with_badge/app_icon_badge_67.png new file mode 100644 index 0000000000..4651eefa38 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_67.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_68.png b/desktop/public/app_icons_with_badge/app_icon_badge_68.png new file mode 100644 index 0000000000..99a61edd9c Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_68.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_69.png b/desktop/public/app_icons_with_badge/app_icon_badge_69.png new file mode 100644 index 0000000000..36ed9430b9 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_69.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_70.png b/desktop/public/app_icons_with_badge/app_icon_badge_70.png new file mode 100644 index 0000000000..8e9400f270 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_70.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_71.png b/desktop/public/app_icons_with_badge/app_icon_badge_71.png new file mode 100644 index 0000000000..bab31d71ee Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_71.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_72.png b/desktop/public/app_icons_with_badge/app_icon_badge_72.png new file mode 100644 index 0000000000..3db1ca0d9b Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_72.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_73.png b/desktop/public/app_icons_with_badge/app_icon_badge_73.png new file mode 100644 index 0000000000..7ba41d01b7 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_73.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_74.png b/desktop/public/app_icons_with_badge/app_icon_badge_74.png new file mode 100644 index 0000000000..7090412d73 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_74.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_75.png b/desktop/public/app_icons_with_badge/app_icon_badge_75.png new file mode 100644 index 0000000000..820a09d44b Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_75.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_76.png b/desktop/public/app_icons_with_badge/app_icon_badge_76.png new file mode 100644 index 0000000000..0cc6f5e080 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_76.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_77.png b/desktop/public/app_icons_with_badge/app_icon_badge_77.png new file mode 100644 index 0000000000..649dfe1e23 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_77.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_78.png b/desktop/public/app_icons_with_badge/app_icon_badge_78.png new file mode 100644 index 0000000000..89b117d45d Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_78.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_79.png b/desktop/public/app_icons_with_badge/app_icon_badge_79.png new file mode 100644 index 0000000000..f4c2e33c21 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_79.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_80.png b/desktop/public/app_icons_with_badge/app_icon_badge_80.png new file mode 100644 index 0000000000..1742c7200f Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_80.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_81.png b/desktop/public/app_icons_with_badge/app_icon_badge_81.png new file mode 100644 index 0000000000..2d1c3fecf8 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_81.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_82.png b/desktop/public/app_icons_with_badge/app_icon_badge_82.png new file mode 100644 index 0000000000..d1e5ca7087 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_82.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_83.png b/desktop/public/app_icons_with_badge/app_icon_badge_83.png new file mode 100644 index 0000000000..565a3661f6 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_83.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_84.png b/desktop/public/app_icons_with_badge/app_icon_badge_84.png new file mode 100644 index 0000000000..862d01e0b2 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_84.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_85.png b/desktop/public/app_icons_with_badge/app_icon_badge_85.png new file mode 100644 index 0000000000..7b8aaa596e Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_85.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_86.png b/desktop/public/app_icons_with_badge/app_icon_badge_86.png new file mode 100644 index 0000000000..92e1cae958 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_86.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_87.png b/desktop/public/app_icons_with_badge/app_icon_badge_87.png new file mode 100644 index 0000000000..886dec1ff9 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_87.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_88.png b/desktop/public/app_icons_with_badge/app_icon_badge_88.png new file mode 100644 index 0000000000..6028a3d19c Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_88.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_89.png b/desktop/public/app_icons_with_badge/app_icon_badge_89.png new file mode 100644 index 0000000000..5590f988a8 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_89.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_90.png b/desktop/public/app_icons_with_badge/app_icon_badge_90.png new file mode 100644 index 0000000000..3a4332d354 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_90.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_91.png b/desktop/public/app_icons_with_badge/app_icon_badge_91.png new file mode 100644 index 0000000000..3c73d40173 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_91.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_92.png b/desktop/public/app_icons_with_badge/app_icon_badge_92.png new file mode 100644 index 0000000000..6c90a210ae Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_92.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_93.png b/desktop/public/app_icons_with_badge/app_icon_badge_93.png new file mode 100644 index 0000000000..17104540ad Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_93.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_94.png b/desktop/public/app_icons_with_badge/app_icon_badge_94.png new file mode 100644 index 0000000000..0bfb379a53 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_94.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_95.png b/desktop/public/app_icons_with_badge/app_icon_badge_95.png new file mode 100644 index 0000000000..183206617c Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_95.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_96.png b/desktop/public/app_icons_with_badge/app_icon_badge_96.png new file mode 100644 index 0000000000..5d8ac8b121 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_96.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_97.png b/desktop/public/app_icons_with_badge/app_icon_badge_97.png new file mode 100644 index 0000000000..6e101b679a Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_97.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_98.png b/desktop/public/app_icons_with_badge/app_icon_badge_98.png new file mode 100644 index 0000000000..15c6cef52d Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_98.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_99.png b/desktop/public/app_icons_with_badge/app_icon_badge_99.png new file mode 100644 index 0000000000..b6b1b669f3 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_99.png differ diff --git a/desktop/public/app_icons_with_badge/app_icon_badge_99plus.png b/desktop/public/app_icons_with_badge/app_icon_badge_99plus.png new file mode 100644 index 0000000000..f2a2742984 Binary files /dev/null and b/desktop/public/app_icons_with_badge/app_icon_badge_99plus.png differ diff --git a/desktop/src/__test__/main/selfCheckingNode.test.ts b/desktop/src/__test__/main/selfCheckingNode.test.ts index b21a301256..e118730320 100644 --- a/desktop/src/__test__/main/selfCheckingNode.test.ts +++ b/desktop/src/__test__/main/selfCheckingNode.test.ts @@ -14,9 +14,8 @@ // describe('node environment self checking', () => { - test('check Jest\'s node environment', () => { - const isNodeAvailable = typeof process !== 'undefined'; - expect(isNodeAvailable).toBe(true); - }) - + test('check Jest\'s node environment', () => { + const isNodeAvailable = typeof process !== 'undefined' + expect(isNodeAvailable).toBe(true) + }) }) diff --git a/desktop/src/__test__/main/settings.test.ts b/desktop/src/__test__/main/settings.test.ts new file mode 100644 index 0000000000..3db1f225b9 --- /dev/null +++ b/desktop/src/__test__/main/settings.test.ts @@ -0,0 +1,198 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Settings } from '../../main/settings' +import { PackedConfig } from '../../main/config' + +const createMockStore = (): { get: jest.Mock, set: jest.Mock } => { + const store: Record = {} + return { + get: jest.fn((key: string) => store[key]), + set: jest.fn((key: string, value: any) => { store[key] = value }) + } +} + +jest.mock('electron-store', () => { + return jest.fn() +}) + +describe('Settings', () => { + let originalEnvironment: NodeJS.ProcessEnv + let stubStoreInstance: ReturnType + + beforeEach(() => { + originalEnvironment = process.env + process.env = { ...originalEnvironment } + stubStoreInstance = createMockStore() + jest.clearAllMocks() + }) + + afterEach(() => { + process.env = originalEnvironment + jest.clearAllMocks() + }) + + describe('readServerUrl', () => { + test('isDev is true', () => { + const systemUnderTest = new Settings(stubStoreInstance as any, true) + + const actualUrl = systemUnderTest.readServerUrl() + + expect(actualUrl).toBe('http://huly.local:8087') + }) + + test('isDev is true and FRONT_URL is set', () => { + process.env.FRONT_URL = 'http://custom-dev.local:3000' + const systemUnderTest = new Settings(stubStoreInstance as any, true) + + const actualUrl = systemUnderTest.readServerUrl() + + expect(actualUrl).toBe('http://custom-dev.local:3000') + }) + + test('isDev is false and server is in store', () => { + const expectedUrl = 'https://stored.server.com' + stubStoreInstance.get.mockReturnValue(expectedUrl) + const systemUnderTest = new Settings(stubStoreInstance as any, false) + + const actualUrl = systemUnderTest.readServerUrl() + + expect(actualUrl).toBe(expectedUrl) + }) + + test('store is empty and packed config exists', () => { + stubStoreInstance.get.mockReturnValue(undefined) + const expectedUrl = 'https://packed.server.com' + const packedConfig: PackedConfig = { server: expectedUrl } + const systemUnderTest = new Settings(stubStoreInstance as any, false, packedConfig) + + const actualUrl = systemUnderTest.readServerUrl() + + expect(actualUrl).toBe(expectedUrl) + }) + + test('store and packed config are empty', () => { + stubStoreInstance.get.mockReturnValue(undefined) + const expectedUrl = 'https://env.server.com' + process.env.FRONT_URL = expectedUrl + const systemUnderTest = new Settings(stubStoreInstance as any, false) + + const actualUrl = systemUnderTest.readServerUrl() + + expect(actualUrl).toBe(expectedUrl) + }) + + test('all options are unavailable', () => { + stubStoreInstance.get.mockReturnValue(undefined) + delete process.env.FRONT_URL + const systemUnderTest = new Settings(stubStoreInstance as any, false) + + const result = systemUnderTest.readServerUrl() + + expect(result).toBe('https://huly.app') + }) + + test('all (store, packed config, environment) options are available', () => { + const expectedUrl = 'https://store.priority.com' + stubStoreInstance.get.mockReturnValue(expectedUrl) + process.env.FRONT_URL = 'https://env.server.com' + const packedConfig: PackedConfig = { server: 'https://packed.server.com' } + const systemUnderTest = new Settings(stubStoreInstance as any, false, packedConfig) + + const actualUrl = systemUnderTest.readServerUrl() + + expect(actualUrl).toBe(expectedUrl) + }) + }) + + describe('read/write', () => { + let systemUnderTest: Settings + beforeEach(() => { + systemUnderTest = new Settings(stubStoreInstance as any, false) + }) + + describe('isMinimizeToTrayEnabled', () => { + test.each([ + { storedValue: true, expected: true, description: 'stored value is true' }, + { storedValue: false, expected: false, description: 'stored value is false' } + ])('$description', ({ storedValue, expected }) => { + stubStoreInstance.get.mockReturnValue(storedValue) + + const actualValue = systemUnderTest.isMinimizeToTrayEnabled() + + expect(actualValue).toBe(expected) + }) + + test.each([ + { storedValue: undefined, description: 'no value is stored (undefined)' }, + { storedValue: null, description: 'stored value is null' } + ])('$description', ({ storedValue }) => { + stubStoreInstance.get.mockReturnValue(storedValue) + + const actualValue = systemUnderTest.isMinimizeToTrayEnabled() + + expect(actualValue).toBe(false) + }) + }) + + describe('setMinimizeToTrayEnabled', () => { + test.each([ + { value: true, description: 'stored value is true' }, + { value: false, description: 'stored value is false' } + ])('$description', ({ value }) => { + systemUnderTest.setMinimizeToTrayEnabled(value) + + expect(systemUnderTest.isMinimizeToTrayEnabled()).toBe(value) + }) + }) + + describe('setServerUrl', () => { + test('write than read', () => { + const expectedUrl = 'https://new.server.com' + + systemUnderTest.setServerUrl(expectedUrl) + + expect(systemUnderTest.readServerUrl()).toBe(expectedUrl) + }) + }) + + describe('getWindowBounds', () => { + test('read', () => { + const expectedValue = { x: 100, y: 300, width: 500, height: 700 } + stubStoreInstance.get.mockReturnValue(expectedValue) + + const actulValue = systemUnderTest.getWindowBounds() + + expect(actulValue).toEqual(expectedValue) + }) + + test('no bounds are stored', () => { + stubStoreInstance.get.mockReturnValue(undefined) + + expect(systemUnderTest.getWindowBounds()).toBeUndefined() + }) + }) + + describe('setWindowBounds', () => { + test('write than read', () => { + const expectedValue = { x: 100, y: 200, width: 800, height: 600 } + + systemUnderTest.setWindowBounds(expectedValue) + + expect(systemUnderTest.getWindowBounds()).toBe(expectedValue) + }) + }) + }) +}) diff --git a/desktop/src/__test__/main/trayUtils.test.ts b/desktop/src/__test__/main/trayUtils.test.ts new file mode 100644 index 0000000000..3b1af9fe58 --- /dev/null +++ b/desktop/src/__test__/main/trayUtils.test.ts @@ -0,0 +1,67 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { getBadgeIconInfo } from '../../main/trayUtils' + +describe('getBadgeIconInfo', () => { + const baseTitle = 'Huly' + + it('count of 0', () => { + const result = getBadgeIconInfo(0, baseTitle) + expect(result).toEqual({ + fileName: '', + tooltip: baseTitle + }) + }) + + it('negative count', () => { + const result = getBadgeIconInfo(-5, baseTitle) + expect(result).toEqual({ + fileName: '', + tooltip: baseTitle + }) + }) + + it.each([1, 2, 3, 4, 5, 6, 7, 8, 9])('count of %i', (count: number) => { + const result = getBadgeIconInfo(count, baseTitle) + expect(result).toEqual({ + fileName: `app_icon_badge_0${count}.png`, + tooltip: `${baseTitle}: ${count} unread` + }) + }) + + describe('badge count 10-98', () => { + const testCases: number[] = [] + for (let i = 10; i <= 98; i++) { + testCases.push(i) + } + + it.each(testCases)('count of %i', (count: number) => { + const result = getBadgeIconInfo(count, baseTitle) + expect(result).toEqual({ + fileName: `app_icon_badge_${count}.png`, + tooltip: `${baseTitle}: ${count} unread` + }) + }) + }) + + it.each([99, 100, 200, 301])('count of %i', (count: number) => { + const result = getBadgeIconInfo(count, baseTitle) + expect(result).toEqual({ + fileName: 'app_icon_badge_99plus.png', + tooltip: `${baseTitle}: ${count} unread` + }) + }) +}) diff --git a/desktop/src/main/config.ts b/desktop/src/main/config.ts index bd14c20136..ea6d211224 100644 --- a/desktop/src/main/config.ts +++ b/desktop/src/main/config.ts @@ -15,12 +15,12 @@ import * as path from 'path' import * as fs from 'fs' -interface PackedConfig { +export interface PackedConfig { server?: string updatesChannelKey?: string } -const configPath = path.join(process.resourcesPath, 'config/config.json') +const configPath = path.join(process.resourcesPath, 'config', 'config.json') export function readPackedConfig (): PackedConfig | undefined { if (fs.existsSync(configPath)) { diff --git a/desktop/src/main/customMenu.ts b/desktop/src/main/customMenu.ts index 915dea39ae..84b20eac2c 100644 --- a/desktop/src/main/customMenu.ts +++ b/desktop/src/main/customMenu.ts @@ -13,10 +13,11 @@ // limitations under the License. // -import { app, BrowserWindow } from 'electron' +import { BrowserWindow } from 'electron' import { MenuBarAction, CommandLogout, CommandSelectWorkspace, CommandOpenSettings } from '../ui/types' +import { TrayController } from './tray' -export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, action: MenuBarAction): void { +export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, action: MenuBarAction, tray: TrayController | undefined): void { if (mainWindow == null) { return } @@ -42,7 +43,7 @@ export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, ac mainWindow.webContents.send(CommandLogout) break case 'exit': - app.quit() + mainWindow.close() break case 'undo': mainWindow.webContents.undo() @@ -86,6 +87,13 @@ export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, ac case 'toggle-fullscreen': mainWindow.setFullScreen(!mainWindow.isFullScreen()) break + case 'toggle-minimize-to-tray': { + if (tray != null) { + const newSetting = tray.toggleMinimizeToTray() + mainWindow.webContents.send('minimize-to-tray-setting-changed', newSetting) + } + break + } default: console.log('unknown menu action:', action) } diff --git a/desktop/src/main/path.ts b/desktop/src/main/path.ts new file mode 100644 index 0000000000..5de505a45f --- /dev/null +++ b/desktop/src/main/path.ts @@ -0,0 +1,21 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { app } from 'electron' +import path from 'path' + +export function getFileInPublicBundledFolder (fileName: string): string { + return path.join(app.getAppPath(), 'dist', 'ui', 'public', fileName) +} diff --git a/desktop/src/main/settings.ts b/desktop/src/main/settings.ts new file mode 100644 index 0000000000..1996b23141 --- /dev/null +++ b/desktop/src/main/settings.ts @@ -0,0 +1,65 @@ +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Store from 'electron-store' +import { PackedConfig } from './config' + +export class Settings { + private static readonly SETTINGS_KEY_SERVER = 'server' + private static readonly SETTINGS_KEY_MINIMIZE_TO_TRAY = 'minimize-to-tray' + private static readonly SETTINGS_KEY_WINDOW_BOUNDS = 'windowBounds' + + private readonly store: Store + private readonly isDev: boolean + private readonly packedConfig?: PackedConfig + + constructor (store: Store, isDev: boolean, packedConfig?: PackedConfig) { + this.store = store + this.isDev = isDev + this.packedConfig = packedConfig + } + + readServerUrl (): string { + if (this.isDev) { + return process.env.FRONT_URL ?? 'http://huly.local:8087' + } + return ( + (this.store as any).get(Settings.SETTINGS_KEY_SERVER) as string ?? + this.packedConfig?.server ?? + process.env.FRONT_URL ?? + 'https://huly.app' + ) + } + + isMinimizeToTrayEnabled (): boolean { + return (this.store as any).get(Settings.SETTINGS_KEY_MINIMIZE_TO_TRAY) as boolean ?? false + } + + setMinimizeToTrayEnabled (enabled: boolean): void { + (this.store as any).set(Settings.SETTINGS_KEY_MINIMIZE_TO_TRAY, enabled) + } + + setServerUrl (serverUrl: string): void { + (this.store as any).set(Settings.SETTINGS_KEY_SERVER, serverUrl) + } + + getWindowBounds (): any { + return (this.store as any).get(Settings.SETTINGS_KEY_WINDOW_BOUNDS) + } + + setWindowBounds (bounds: any): void { + (this.store as any).set(Settings.SETTINGS_KEY_WINDOW_BOUNDS, bounds) + } +} diff --git a/desktop/src/main/start.ts b/desktop/src/main/start.ts index 021f96f71f..d915e0d9e5 100644 --- a/desktop/src/main/start.ts +++ b/desktop/src/main/start.ts @@ -14,7 +14,7 @@ // import { config as dotenvConfig } from 'dotenv' -import { BrowserWindow, CookiesSetDetails, Notification, app, desktopCapturer, dialog, ipcMain, nativeImage, session, shell, systemPreferences, nativeTheme } from 'electron' +import { Event, BrowserWindow, CookiesSetDetails, Notification, app, desktopCapturer, dialog, ipcMain, nativeImage, session, shell, systemPreferences, nativeTheme } from 'electron' import contextMenu from 'electron-context-menu' import log from 'electron-log' import Store from 'electron-store' @@ -32,8 +32,12 @@ import { generateId } from '@hcengineering/core' import { DownloadItem } from '@hcengineering/desktop-downloads' import { rebuildJumpList, setupWindowsSpecific } from './windowsSpecificSetup' import { readPackedConfig } from './config' +import { Settings } from './settings' +import { TrayController } from './tray' +import { getFileInPublicBundledFolder } from './path' let mainWindow: BrowserWindow | undefined +let isQuiting = false let winBadge: any const isMac = process.platform === 'darwin' @@ -41,7 +45,7 @@ const isWindows = process.platform === 'win32' const isDev = process.env.NODE_ENV === 'development' const sessionPartition = !isDev ? 'persist:huly' : 'persist:huly_dev' -const iconKey = path.join(app.getAppPath(), 'dist', 'ui', 'public', isWindows ? 'AppIcon.ico' : 'AppIcon.png') +const iconKey = getFileInPublicBundledFolder(isWindows ? 'AppIcon.ico' : 'AppIcon.png') const preloadScriptPath = path.join(app.getAppPath(), 'dist', 'main', 'preload.js') const defaultWidth = 1440 @@ -62,24 +66,16 @@ const containerPagePath = path.join('dist', 'ui', containerPageFileName) // Note: using electron-store here as local storage is not available in the main process // before the window is created -const settings = new Store() -const oldFront = readServerUrl() +const settings = new Settings(new Store(), isDev, packedConfig) +const oldFront = settings.readServerUrl() if (options.server !== undefined) { - ;(settings as any).set('server', options.server) + settings.setServerUrl(options.server) } -const FRONT_URL = readServerUrl() +const FRONT_URL = settings.readServerUrl() const serverChanged = oldFront !== FRONT_URL -function readServerUrl (): string { - if (isDev) { - return process.env.FRONT_URL ?? 'http://huly.local:8087' - } - - return ((settings as any).get('server') as string) ?? packedConfig?.server ?? process.env.FRONT_URL ?? 'https://huly.app' -} - // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (require('electron-squirrel-startup') === true) { app.quit() @@ -229,8 +225,7 @@ function handleWillDownload (window: BrowserWindow): void { } const createWindow = async (): Promise => { - // Restore window position if available - const restoredBounds: any = settings.get('windowBounds') + const restoredBounds: any = settings.getWindowBounds() const windowOptions: Electron.BrowserWindowConstructorOptions = { width: restoredBounds?.width ?? defaultWidth, height: restoredBounds?.height ?? defaultHeight, @@ -243,7 +238,6 @@ const createWindow = async (): Promise => { devTools: true, sandbox: false, nodeIntegration: true, - // backgroundThrottling: false, partition: sessionPartition, preload: preloadScriptPath } @@ -251,7 +245,6 @@ const createWindow = async (): Promise => { setupWindowTitleBar(windowOptions) mainWindow = new BrowserWindow(windowOptions) app.dock?.setIcon(nativeImage.createFromPath(iconKey)) - // await mainWindow.webContents.openDevTools() if (isDev) { mainWindow.webContents.openDevTools() } @@ -264,11 +257,27 @@ const createWindow = async (): Promise => { // All other urls will be blocked. hookOpenWindow(mainWindow) - // Save window position on close - mainWindow.on('close', () => { + function minimizeToTrayIsOn (): boolean { + return settings.isMinimizeToTrayEnabled() && trayController != null + } + + mainWindow.on('close', (event: Event) => { const bounds = mainWindow?.getBounds() - if (bounds !== undefined) { - settings.set('windowBounds', bounds) + if (bounds != null) { + settings.setWindowBounds(bounds) + } + + if (minimizeToTrayIsOn()) { + if (!isQuiting) { + event.preventDefault() + mainWindow?.hide() + return false + } + } + + if (isMac) { + event.preventDefault() + mainWindow?.hide() } }) @@ -284,6 +293,12 @@ const createWindow = async (): Promise => { sendWindowMaximizedMessage(true) }) + mainWindow.on('minimize', () => { + if (minimizeToTrayIsOn()) { + mainWindow?.hide() + } + }) + mainWindow.on('unmaximize', () => { sendWindowMaximizedMessage(false) }) @@ -298,14 +313,7 @@ const createWindow = async (): Promise => { } }) - if (isMac) { - mainWindow.on('close', (event) => { - // Prevent the default behavior (which would quit the app) - event.preventDefault() - // Hide the window - mainWindow?.hide() - }) - } else if (isWindows) { + if (isWindows) { winBadge = new WinBadge(mainWindow, { font: '14px arial' }) @@ -326,6 +334,17 @@ function activateWindow (): void { } } +function quitApplication (): void { + isQuiting = true + app.quit() +} + +let trayController: TrayController | undefined + +void app.whenReady().then(() => { + trayController = new TrayController(settings, activateWindow, quitApplication) +}) + if (isWindows) { setupWindowsSpecific(activateWindow, sendCommand) } else { @@ -345,6 +364,7 @@ ipcMain.on('set-badge', (_event: any, badge: number) => { if (isWindows && winBadge !== undefined) { winBadge.update(badge) } + trayController?.updateTrayBadge(badge) }) ipcMain.on('dock-bounce', (_event: any) => { @@ -455,7 +475,7 @@ ipcMain.handle('get-is-os-using-dark-theme', () => { }) ipcMain.handle('menu-action', async (_event: any, action: MenuBarAction) => { - dispatchMenuBarAction(mainWindow, action) + dispatchMenuBarAction(mainWindow, action, trayController) }) if (isWindows) { @@ -483,6 +503,10 @@ ipcMain.handle('get-screen-sources', () => { }) }) +ipcMain.handle('get-minimize-to-tray-enabled', () => { + return settings.isMinimizeToTrayEnabled() +}) + async function onReady (): Promise { await createWindow() @@ -509,12 +533,17 @@ if (isMac) { }) } app.on('before-quit', () => { + isQuiting = true + if (mainWindow !== undefined && !mainWindow.isDestroyed()) { const bounds = mainWindow.getBounds() - settings.set('windowBounds', bounds) + settings.setWindowBounds(bounds) } + // Note: in case the app is exited by auto-updater all windows will be destroyed at this point - if (mainWindow === undefined || mainWindow.isDestroyed()) return + if (mainWindow === undefined || mainWindow.isDestroyed()) { + return + } if (isMac) { mainWindow?.removeAllListeners('close') diff --git a/desktop/src/main/tray.ts b/desktop/src/main/tray.ts new file mode 100644 index 0000000000..bd95493767 --- /dev/null +++ b/desktop/src/main/tray.ts @@ -0,0 +1,134 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { Menu, NativeImage, nativeImage, Tray } from 'electron' +import { WindowAction } from '../ui/types' +import { Settings } from './settings' +import { getFileInPublicBundledFolder } from './path' +import { getBadgeIconInfo } from './trayUtils' +import path from 'path' + +export class TrayController { + private tray: Tray | undefined = undefined + private readonly defaultIcon: NativeImage + private readonly notificationIcon: NativeImage + private badgeCount: number = 0 + + constructor ( + private readonly settings: Settings, + private readonly activateWindow: WindowAction, + private readonly quitApplication: WindowAction) { + this.defaultIcon = nativeImage.createFromPath(getFileInPublicBundledFolder('AppIcon.ico')) + this.defaultIcon.setTemplateImage(true) + + this.notificationIcon = nativeImage.createFromPath(getFileInPublicBundledFolder('AppIconNotification.png')) + this.notificationIcon.setTemplateImage(true) + + if (this.settings.isMinimizeToTrayEnabled()) { + this.instantiateTray() + } + } + + public updateTrayBadge (count: number): void { + const countChanged = this.badgeCount !== count + this.badgeCount = count + if (!countChanged) { + return + } + + this.evaluateIcon(count) + } + + public setMinimizeToTrayEnabled (enabled: boolean): void { + this.settings.setMinimizeToTrayEnabled(enabled) + if (enabled) { + if (this.tray == null) { + this.instantiateTray() + } + } else { + if (this.tray != null) { + this.tray.destroy() + this.tray = undefined + } + } + } + + public toggleMinimizeToTray (): boolean { + const currentSetting = this.settings.isMinimizeToTrayEnabled() + const newSetting = !currentSetting + this.setMinimizeToTrayEnabled(newSetting) + return newSetting + } + + private instantiateTray (): void { + this.tray = createTray( + () => { this.activateWindow() }, + () => { this.quitApplication() }, + this.defaultIcon + ) + this.evaluateIcon(this.badgeCount) + } + + private evaluateIcon (badgeCount: number): void { + if (this.tray == null) { + return + } + + const iconInfo = getBadgeIconInfo(badgeCount, BASE_TITLE) + if (badgeCount > 0) { + const iconFilePath = getFileInPublicBundledFolder(path.join('app_icons_with_badge', iconInfo.fileName)) + const icon = nativeImage.createFromPath(iconFilePath) + this.tray.setImage(icon) + } else { + this.tray.setImage(this.defaultIcon) + } + + this.tray.setToolTip(iconInfo.tooltip) + } +} + +const BASE_TITLE = 'Huly' + +function createTray (activateWindow: WindowAction, quitApplication: WindowAction, icon: NativeImage): Tray { + const tray = new Tray(icon) + + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Show', + click: () => { + activateWindow() + } + }, + { + label: 'Exit', + click: () => { + quitApplication() + } + } + ]) + + tray.setContextMenu(contextMenu) + tray.setToolTip(BASE_TITLE) + + tray.on('double-click', () => { + activateWindow() + }) + + tray.on('click', (): void => { + activateWindow() + }) + + return tray +} diff --git a/desktop/src/main/trayUtils.ts b/desktop/src/main/trayUtils.ts new file mode 100644 index 0000000000..2868038e2e --- /dev/null +++ b/desktop/src/main/trayUtils.ts @@ -0,0 +1,47 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const BADGE_ICON_COMMON_PREFIX = 'app_icon_badge_' +const BADGE_ICON_COMMON_EXTENSION = '.png' + +export interface BadgeIconInfo { + fileName: string + tooltip: string +} + +export function getBadgeIconInfo (badgeCount: number, baseTitle: string): BadgeIconInfo { + if (badgeCount <= 0) { + return { + fileName: '', + tooltip: baseTitle + } + } + + if (badgeCount >= 99) { + return { + fileName: `${BADGE_ICON_COMMON_PREFIX}99plus${BADGE_ICON_COMMON_EXTENSION}`, + tooltip: `${baseTitle}: ${badgeCount} unread` + } + } + + const fileName = badgeCount <= 9 + ? `${BADGE_ICON_COMMON_PREFIX}0${badgeCount}${BADGE_ICON_COMMON_EXTENSION}` + : `${BADGE_ICON_COMMON_PREFIX}${badgeCount}${BADGE_ICON_COMMON_EXTENSION}` + + return { + fileName, + tooltip: `${baseTitle}: ${badgeCount} unread` + } +} diff --git a/desktop/src/main/windowsSpecificSetup.ts b/desktop/src/main/windowsSpecificSetup.ts index feb8393425..0b40204b84 100644 --- a/desktop/src/main/windowsSpecificSetup.ts +++ b/desktop/src/main/windowsSpecificSetup.ts @@ -14,106 +14,103 @@ // import { app, JumpListItem, JumpListCategory } from 'electron' -import { Command, CommandOpenSettings, CommandOpenInbox, CommandOpenApplication, JumpListSpares } from '../ui/types' +import { Command, CommandOpenSettings, CommandOpenInbox, CommandOpenApplication, JumpListSpares, WindowAction, SendCommandDelegate } from '../ui/types' import * as path from 'path' const JUMP_COMMANDS = { INBOX: '--jump-to-inbox', - SETTINGS: '--jump-to-settings', + SETTINGS: '--jump-to-settings' } as const type JumpCommand = typeof JUMP_COMMANDS[keyof typeof JUMP_COMMANDS] const JUMP_TO_APP_COMMAND_PREFIX = '--jump-to-app=' -function getIconPath(iconName: string): string { - return path.join(process.resourcesPath, 'icons', iconName) +function getIconPath (iconName: string): string { + return path.join(process.resourcesPath, 'icons', iconName) } -function addFixedFunctionJumpListItems(spares: JumpListSpares, tasks: JumpListItem[]): void { +function addFixedFunctionJumpListItems (spares: JumpListSpares, tasks: JumpListItem[]): void { + tasks.push({ + type: 'task', + program: process.execPath, + args: JUMP_COMMANDS.SETTINGS, + iconPath: getIconPath('SettingsIcon.ico'), + title: spares.settingsLabel, + iconIndex: 0 + }) + tasks.push({ + type: 'task', + program: process.execPath, + args: JUMP_COMMANDS.INBOX, + iconPath: getIconPath('InboxIcon.ico'), + title: spares.inboxLabel, + iconIndex: 0 + }) +} + +export function rebuildJumpList (spares: JumpListSpares): void { + const tasks: JumpListItem[] = [] + + addFixedFunctionJumpListItems(spares, tasks) + + if (spares.applications.length > 0) { tasks.push({ - type: 'task', - program: process.execPath, - args: JUMP_COMMANDS.SETTINGS, - iconPath: getIconPath('SettingsIcon.ico'), - title: spares.settingsLabel, - iconIndex: 0, + type: 'separator' }) + } + + for (const application of spares.applications) { + const iconFileName = application.id.replaceAll(':', '_') + '.ico' + const cliArguments = `${JUMP_TO_APP_COMMAND_PREFIX}${application.alias}` tasks.push({ - type: 'task', - program: process.execPath, - args: JUMP_COMMANDS.INBOX, - iconPath: getIconPath('InboxIcon.ico'), - title: spares.inboxLabel, - iconIndex: 0, + type: 'task', + program: process.execPath, + args: cliArguments, + iconPath: getIconPath(iconFileName), + title: application.title, + iconIndex: 0 }) + } + + const category: JumpListCategory = { + type: 'tasks', + items: tasks + } + + app.setJumpList([category]) } -export function rebuildJumpList(spares: JumpListSpares): void { - const tasks: JumpListItem[] = []; +export function setupWindowsSpecific (activateWindow: WindowAction, sendCommand: SendCommandDelegate): void { + app.setAppUserModelId(app.getName()) - addFixedFunctionJumpListItems(spares, tasks) - - if (spares.applications.length > 0) { - tasks.push({ - type: 'separator' - }) - } - - for (const application of spares.applications) { - const iconFileName = application.id.replaceAll(":", "_") + '.ico' - const cliArguments = `${JUMP_TO_APP_COMMAND_PREFIX}${application.alias}` - tasks.push({ - type: 'task', - program: process.execPath, - args: cliArguments, - iconPath: getIconPath(iconFileName), - title: application.title, - iconIndex: 0, - }) - } - - const category: JumpListCategory = { - type: 'tasks', - items: tasks, - } - - app.setJumpList([category]) -} - -export function setupWindowsSpecific(activateWindow: () => void, sendCommand: (cmd: Command, ...args: any[]) => void): void { - - app.setAppUserModelId(app.getName()) - - app.on('second-instance', (_event: any, commandLine: any, _workingDirectory: any) => { - const commandArgument = commandLine[1] as string - if (typeof commandArgument === 'string' && commandArgument.startsWith(JUMP_TO_APP_COMMAND_PREFIX)) { - const applicationId = commandArgument.replace(JUMP_TO_APP_COMMAND_PREFIX, '') - sendCommand(CommandOpenApplication, [applicationId]) - - } else { - const jumpCommandCode = commandArgument as JumpCommand - let command: Command | undefined - switch (jumpCommandCode) { - case JUMP_COMMANDS.INBOX: - command = CommandOpenInbox - break; - case JUMP_COMMANDS.SETTINGS: - command = CommandOpenSettings - break; - default: { - // compile-time check: if jumpCommand is not a known value, this line errors - const _exhaustive: never = jumpCommandCode - return; // or handle error - } - } - - if (command) { - sendCommand(command) - } + app.on('second-instance', (_event: any, commandLine: any, _workingDirectory: any) => { + const commandArgument = commandLine[1] as string + if (typeof commandArgument === 'string' && commandArgument.startsWith(JUMP_TO_APP_COMMAND_PREFIX)) { + const applicationId = commandArgument.replace(JUMP_TO_APP_COMMAND_PREFIX, '') + sendCommand(CommandOpenApplication, [applicationId]) + } else { + const jumpCommandCode = commandArgument as JumpCommand + let command: Command | undefined + switch (jumpCommandCode) { + case JUMP_COMMANDS.INBOX: + command = CommandOpenInbox + break + case JUMP_COMMANDS.SETTINGS: + command = CommandOpenSettings + break + default: { + // compile-time check: if jumpCommand is not a known value, this line errors + const _exhaustive: never = jumpCommandCode + return // or handle error } + } - activateWindow() - }) - -} \ No newline at end of file + if (command !== undefined) { + sendCommand(command) + } + } + + activateWindow() + }) +} diff --git a/desktop/src/ui/index.ts b/desktop/src/ui/index.ts index 9a51e98867..04cba0fc72 100644 --- a/desktop/src/ui/index.ts +++ b/desktop/src/ui/index.ts @@ -46,18 +46,18 @@ window.addEventListener('DOMContentLoaded', () => { if (currentOsIsWindows()) { const titleBarRoot = document.getElementById('desktop-app-titlebar-root') if (titleBarRoot != null) { - const menuBar = setupTitleBarMenu(ipcMain, titleBarRoot) + void setupTitleBarMenu(ipcMain, titleBarRoot).then((menuBar) => { + themeStore.subscribe((themeOptions) => { + if (themeOptions != null) { + menuBar.setTheme(themeOptions.variant) + } + }) - themeStore.subscribe((themeOptions) => { - if (themeOptions != null) { - menuBar.setTheme(themeOptions.variant) - } - }) - - void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => { - menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light) - }).catch(() => { - menuBar.setTheme(ThemeVariant.Light) // fallback + void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => { + menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light) + }).catch(() => { + menuBar.setTheme(ThemeVariant.Light) // fallback + }) }) } } diff --git a/desktop/src/ui/preload.ts b/desktop/src/ui/preload.ts index d5ea5f61af..052b84ba92 100644 --- a/desktop/src/ui/preload.ts +++ b/desktop/src/ui/preload.ts @@ -181,6 +181,13 @@ const expose: IPCMainExposed = { cancelBackup: () => { ipcRenderer.send('cancel-backup') }, startBackup: (token, endpoint, wsIds) => { ipcRenderer.send('start-backup', token, endpoint, wsIds) }, - rebuildJumpList: (spares: JumpListSpares) => { ipcRenderer.send('rebuild-user-jump-list', spares) } + rebuildJumpList: (spares: JumpListSpares) => { ipcRenderer.send('rebuild-user-jump-list', spares) }, + + isMinimizeToTrayEnabled: async () => { + return await ipcRenderer.invoke('get-minimize-to-tray-enabled') + }, + onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => { + ipcRenderer.on('minimize-to-tray-setting-changed', (_event, enabled: boolean) => { callback(enabled) }) + } } contextBridge.exposeInMainWorld('electron', expose) diff --git a/desktop/src/ui/titleBarMenu.ts b/desktop/src/ui/titleBarMenu.ts index f0c5c1987f..6121d38224 100644 --- a/desktop/src/ui/titleBarMenu.ts +++ b/desktop/src/ui/titleBarMenu.ts @@ -18,9 +18,14 @@ import { isMenuBarAction } from './typesUtils' import { TitleBarMenuState } from './titleBarMenuState' import { ThemeVariant, type ThemeVariantType } from '@hcengineering/theme' -export function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): MenuBar { +const ToggleMinimizeToTrayAction: MenuBarAction = 'toggle-minimize-to-tray' + +const LabelMinimizeToTrayEnabled = '☑ Minimize to tray' +const LabelMinimizeToTrayDisabled = '☐ Minimize to tray' + +export async function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): Promise { const themeManager = new ThemeManager(ThemeVariant.Light) - const menuManager = new MenuBarManager(root) + const menuManager = new MenuBarManager(root, await ipcMain.isMinimizeToTrayEnabled()) const menuBar = menuManager.getView() @@ -35,6 +40,13 @@ export function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): menuManager.attachEventListeners(ipcMain) + ipcMain.onMinimizeToTraySettingChanged((enabled) => { + const toggle = root.querySelector(`[data-action="${ToggleMinimizeToTrayAction}"]`) + if (toggle != null) { + toggle.textContent = enabled ? LabelMinimizeToTrayEnabled : LabelMinimizeToTrayDisabled + } + }) + ipcMain.onWindowStateChange((_event, state) => { const maximizeButton = root.querySelector('#maximize-button') if (maximizeButton != null) { @@ -58,7 +70,7 @@ export class MenuBar { } } -export function buildHulyApplicationMenu (): HTMLElement { +export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean): HTMLElement { const menuBuilder = new MenuBuilder() const MenuFileIndex = 0 @@ -93,6 +105,11 @@ export function buildHulyApplicationMenu (): HTMLElement { .addSeparator(MenuViewIndex) .addMenuItem(MenuViewIndex, 'Toggle Fullscreen', 'toggle-fullscreen', 'F11', 'l') + const MenuWindowIndex = 3 + const ToggleMinimizeToTrayLabel = minimizeToTrayEnabled ? LabelMinimizeToTrayEnabled : LabelMinimizeToTrayDisabled + menuBuilder.addTopLevelMenu('Window', 'w') + .addMenuItem(MenuWindowIndex, ToggleMinimizeToTrayLabel, 'toggle-minimize-to-tray', undefined, 'm') + return menuBuilder.build() } @@ -140,7 +157,7 @@ export class MenuBuilder { label: string, action: MenuBarAction, shortcut?: string, - acceleratorChar: string | null = null + acceleratorChar: string | undefined = undefined ): this { if (topLevelMenuIndex >= 0 && topLevelMenuIndex < this.menus.length) { const item: MenuItem = { @@ -281,7 +298,7 @@ class MenuBarManager { private readonly StateStyleKeyboardSelected = 'desktop-app-keyboard-selected' private readonly StateStyleAltModeActive = 'desktop-app-alt-active' - constructor (private readonly root: HTMLElement) { + constructor (private readonly root: HTMLElement, minimizeToTrayEnabled: boolean) { this.state = new TitleBarMenuState( () => this.topLevelMenus().length, (topLevelMenuIndex: number) => { @@ -290,7 +307,7 @@ class MenuBarManager { } ) - this.view = buildHulyApplicationMenu() + this.view = buildHulyApplicationMenu(minimizeToTrayEnabled) } public getView (): HTMLElement { diff --git a/desktop/src/ui/titleBarMenuState.ts b/desktop/src/ui/titleBarMenuState.ts index ada16d9fdc..dd358c994a 100644 --- a/desktop/src/ui/titleBarMenuState.ts +++ b/desktop/src/ui/titleBarMenuState.ts @@ -14,113 +14,113 @@ // export class TitleBarMenuState { - private altModeActive: boolean = false - private topLevelMenuExpanded: boolean = false - private focusedTopLevelMenuIndex: number | null = null - private focusedChildMenuIndex: number | null = null + private altModeActive: boolean = false + private topLevelMenuExpanded: boolean = false + private focusedTopLevelMenuIndex: number | null = null + private focusedChildMenuIndex: number | null = null - private readonly topLevelMenuCount: () => number; - private readonly menuItemsCount: (topLevelMenuIndex: number) => number; + private readonly topLevelMenuCount: () => number + private readonly menuItemsCount: (topLevelMenuIndex: number) => number - public constructor( - topLevelMenuCount: () => number, - menuItemsCount: (topLevelMenuIndex: number) => number - ) { - this.topLevelMenuCount = topLevelMenuCount - this.menuItemsCount = menuItemsCount + public constructor ( + topLevelMenuCount: () => number, + menuItemsCount: (topLevelMenuIndex: number) => number + ) { + this.topLevelMenuCount = topLevelMenuCount + this.menuItemsCount = menuItemsCount + } + + public focusChildMenu (): void { + if (this.focusedTopLevelMenuIndex != null && this.topLevelMenuExpanded) { + this.focusedChildMenuIndex = 0 + } + } + + public expandTopLevelMenu (topLevelMenuIndex: number): void { + if (topLevelMenuIndex < 0 || topLevelMenuIndex >= this.topLevelMenuCount()) { + return } - public focusChildMenu() { - if (null != this.focusedTopLevelMenuIndex && this.topLevelMenuExpanded) { - this.focusedChildMenuIndex = 0 - } + if (this.focusedTopLevelMenuIndex === topLevelMenuIndex && this.topLevelMenuExpanded) { + this.closeAll() + } else { + this.focusedTopLevelMenuIndex = topLevelMenuIndex + this.topLevelMenuExpanded = true + } + } + + public exitAltMode (): void { + this.altModeActive = false + } + + public enterAltMode (topLevelMenuIndex: number | null): void { + this.altModeActive = true + + if (topLevelMenuIndex == null || topLevelMenuIndex < 0 || topLevelMenuIndex >= this.topLevelMenuCount()) { + return } - public expandTopLevelMenu(topLevelMenuIndex: number): void { - if (0 > topLevelMenuIndex || topLevelMenuIndex >= this.topLevelMenuCount()) { - return - } - - if (this.focusedTopLevelMenuIndex === topLevelMenuIndex && this.topLevelMenuExpanded) { - this.closeAll() - } else { - this.focusedTopLevelMenuIndex = topLevelMenuIndex - this.topLevelMenuExpanded = true - } + this.focusedTopLevelMenuIndex = topLevelMenuIndex + } + + public closeAll (): void { + this.altModeActive = false + this.topLevelMenuExpanded = false + this.focusedTopLevelMenuIndex = null + this.focusedChildMenuIndex = null + } + + public defocus (): void { + if (this.topLevelMenuExpanded) { + this.topLevelMenuExpanded = false + this.focusedChildMenuIndex = null + } else { + this.closeAll() + } + } + + public moveFocusHorizontal (increment: number): void { + if (Math.abs(increment) !== 1 || this.focusedTopLevelMenuIndex == null) { + return } - public exitAltMode(): void { - this.altModeActive = false + const topLevelMenuCount = this.topLevelMenuCount() + this.focusedTopLevelMenuIndex = (this.focusedTopLevelMenuIndex + topLevelMenuCount + increment) % topLevelMenuCount + + if (this.topLevelMenuExpanded) { + this.focusedChildMenuIndex = 0 + } + } + + public moveFocusVertical (increment: number): void { + if (Math.abs(increment) !== 1 || this.focusedTopLevelMenuIndex == null) { + return } - public enterAltMode(topLevelMenuIndex: number | null): void { - this.altModeActive = true - - if (null == topLevelMenuIndex || 0 > topLevelMenuIndex || topLevelMenuIndex >= this.topLevelMenuCount()) { - return - } - - this.focusedTopLevelMenuIndex = topLevelMenuIndex + const menuItemsCount = this.menuItemsCount(this.focusedTopLevelMenuIndex) + + this.topLevelMenuExpanded = true + + if (menuItemsCount === 0) { + return } - public closeAll(): void { - this.altModeActive = false - this.topLevelMenuExpanded = false - this.focusedTopLevelMenuIndex = null - this.focusedChildMenuIndex = null - } + this.focusedChildMenuIndex = ((this.focusedChildMenuIndex ?? -1) + menuItemsCount + increment) % menuItemsCount + } - public defocus(): void { - if (this.topLevelMenuExpanded) { - this.topLevelMenuExpanded = false - this.focusedChildMenuIndex = null - } else { - this.closeAll(); - } - } + public get isAltModeActive (): boolean { + return this.altModeActive + } - public moveFocusHorizontal(increment: number): void { - if (Math.abs(increment) != 1 || null == this.focusedTopLevelMenuIndex) { - return; - } - - const topLevelMenuCount = this.topLevelMenuCount() - this.focusedTopLevelMenuIndex = (this.focusedTopLevelMenuIndex + topLevelMenuCount + increment) % topLevelMenuCount - - if (this.topLevelMenuExpanded) { - this.focusedChildMenuIndex = 0 - } - } + public get isTopLevelMenuExpanded (): boolean { + return this.topLevelMenuExpanded + } - public moveFocusVertical(increment: number): void { - if (Math.abs(increment) != 1 || null == this.focusedTopLevelMenuIndex) { - return; - } + public get FocusedTopLevelMenuIndex (): number | null { + return this.focusedTopLevelMenuIndex + } - const menuItemsCount = this.menuItemsCount(this.focusedTopLevelMenuIndex) - - this.topLevelMenuExpanded = true - - if (menuItemsCount === 0) { - return; - } - - this.focusedChildMenuIndex = ((this.focusedChildMenuIndex ?? -1) + menuItemsCount + increment) % menuItemsCount - } - - public get isAltModeActive(): boolean { - return this.altModeActive - } - - public get isTopLevelMenuExpanded(): boolean { - return this.topLevelMenuExpanded - } - - public get FocusedTopLevelMenuIndex(): number | null { - return this.focusedTopLevelMenuIndex - } - - public get FocusedChildMenuIndex(): number | null { - return this.focusedChildMenuIndex - } -} \ No newline at end of file + public get FocusedChildMenuIndex (): number | null { + return this.focusedChildMenuIndex + } +} diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts index 1543e00dbd..85596757de 100644 --- a/desktop/src/ui/types.ts +++ b/desktop/src/ui/types.ts @@ -104,8 +104,26 @@ export interface NotificationParams { objectClass?: Ref> } -export const MenuBarActions = ['settings', 'select-workspace', 'logout', 'exit', 'undo', 'redo', 'cut', 'copy', 'paste', 'delete', 'select-all', 'reload', 'force-reload', 'toggle-devtools', - 'zoom-in', 'zoom-out', 'restore-size', 'toggle-fullscreen'] as const +export const MenuBarActions = [ + 'settings', + 'select-workspace', + 'logout', + 'exit', + 'undo', + 'redo', + 'cut', + 'copy', + 'paste', + 'delete', + 'select-all', + 'reload', + 'force-reload', + 'toggle-devtools', + 'zoom-in', + 'zoom-out', + 'restore-size', + 'toggle-fullscreen', + 'toggle-minimize-to-tray'] as const export type MenuBarAction = typeof MenuBarActions[number] @@ -151,4 +169,10 @@ export interface IPCMainExposed { executeMenuBarAction: (action: MenuBarAction) => void rebuildJumpList: (spares: JumpListSpares) => void + + isMinimizeToTrayEnabled: () => Promise + onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => void } + +export type SendCommandDelegate = (cmd: Command, ...args: any[]) => void +export type WindowAction = () => void diff --git a/desktop/start-dev.ps1 b/desktop/start-dev.ps1 index 94bbd625f5..aad3d20939 100644 --- a/desktop/start-dev.ps1 +++ b/desktop/start-dev.ps1 @@ -1,11 +1,9 @@ -$MODEL_VERSION = node ../common/scripts/show_version.js | Out-String -$MODEL_VERSION = $MODEL_VERSION.Trim() - -$VERSION = node ../common/scripts/show_tag.js | Out-String -$VERSION = $VERSION.Trim() - -# Set environment variable -$env:NODE_ENV = "development" - -# Run Electron -electron --no-sandbox ./ \ No newline at end of file +$MODEL_VERSION = node ../common/scripts/show_version.js | Out-String +$MODEL_VERSION = $MODEL_VERSION.Trim() + +$VERSION = node ../common/scripts/show_tag.js | Out-String +$VERSION = $VERSION.Trim() + +$env:NODE_ENV = "development" + +electron --no-sandbox ./