Compare commits
10 commits
7385255124
...
5a351c526a
Author | SHA1 | Date | |
---|---|---|---|
5a351c526a | |||
b4eeabb366 | |||
b03bb69a0d | |||
d4e70db999 | |||
42a0bc4759 | |||
61cf759b23 | |||
12221172e9 | |||
33dad2051d | |||
241e25347f | |||
dcdd18fa89 |
133
i18n/ar.json
133
i18n/ar.json
|
@ -2,7 +2,7 @@
|
||||||
"common.permissions.canBasicAuth": "مصادقه الويب",
|
"common.permissions.canBasicAuth": "مصادقه الويب",
|
||||||
"common.tabs.TAB_FOCALBOARD": "اللوحات",
|
"common.tabs.TAB_FOCALBOARD": "اللوحات",
|
||||||
"common.tabs.TAB_MESSAGING": "القنوات",
|
"common.tabs.TAB_MESSAGING": "القنوات",
|
||||||
"common.tabs.TAB_PLAYBOOKS": "Playbooks",
|
"common.tabs.TAB_PLAYBOOKS": "خطط العمل",
|
||||||
"label.accept": "قبول",
|
"label.accept": "قبول",
|
||||||
"label.add": "أضف",
|
"label.add": "أضف",
|
||||||
"label.allow": "سماح",
|
"label.allow": "سماح",
|
||||||
|
@ -113,9 +113,13 @@
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.geolocation": "سيستخدم {appName} الموقع لإعداد منطقتك الزمنية. ويمكنك دائمًا تغيير ذلك لاحقًا في إعدادات جهاز الكمبيوتر الخاص بك.",
|
"main.permissionsManager.checkPermission.dialog.detail.geolocation": "سيستخدم {appName} الموقع لإعداد منطقتك الزمنية. ويمكنك دائمًا تغيير ذلك لاحقًا في إعدادات جهاز الكمبيوتر الخاص بك.",
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.media": "{appName} سوف يستخدم المايكروفون و الكاميرا من أجل الاتصالات و الملاحظات الصوتية، يمكنك تغيير هذا لاحقاً من خلال الإعدادات.",
|
"main.permissionsManager.checkPermission.dialog.detail.media": "{appName} سوف يستخدم المايكروفون و الكاميرا من أجل الاتصالات و الملاحظات الصوتية، يمكنك تغيير هذا لاحقاً من خلال الإعدادات.",
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.notifications": "{appName} سوف يرسل اشعارات للرسائل والاتصالات. يمكنك ضبط تفضيلات الاشعارات في الإعدادات.",
|
"main.permissionsManager.checkPermission.dialog.detail.notifications": "{appName} سوف يرسل اشعارات للرسائل والاتصالات. يمكنك ضبط تفضيلات الاشعارات في الإعدادات.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.openExternal": "سيفتح {appName} الرابط المطلوب في تطبيق خارجي. إذا كنت لا تثق بهذا الرابط أو لا تتعرف عليه، فانقر فوق \"رفض\". يمكنك دائمًا تغيير هذا لاحقًا في إعدادات الكمبيوتر.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.screenShare": "سيستخدم {appName} هذا الإذن لمشاركة شاشتك لإجراء المكالمات. يمكنك دائمًا تغيير هذا لاحقًا في إعدادات الكمبيوتر.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) يود الوصول الى موقعك.",
|
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) يود الوصول الى موقعك.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) يود الوصول الى الكاميرا والمايكروفون.",
|
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) يود الوصول الى الكاميرا والمايكروفون.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) يود إرسال الإشعارات.",
|
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) يود إرسال الإشعارات.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.openExternal": "يريد {appName} ({url}) الحصول على إذن لفتح عنوان URL التالي: {externalURL}",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.screenShare": "يرغب {appName} ({url}) في أن يتمكن من عرض شاشتك.",
|
||||||
"main.permissionsManager.checkPermission.dialog.title": "تم طلب الإذن",
|
"main.permissionsManager.checkPermission.dialog.title": "تم طلب الإذن",
|
||||||
"main.tray.tray.expired": "انتهت الجلسة: الرجاء تسجيل الدخول لمتابعة تلقي الإخطارات.",
|
"main.tray.tray.expired": "انتهت الجلسة: الرجاء تسجيل الدخول لمتابعة تلقي الإخطارات.",
|
||||||
"main.tray.tray.mention": "تم ذِكرك",
|
"main.tray.tray.mention": "تم ذِكرك",
|
||||||
|
@ -141,7 +145,11 @@
|
||||||
"renderer.components.configureServer.title": "لنتصل بالخادم",
|
"renderer.components.configureServer.title": "لنتصل بالخادم",
|
||||||
"renderer.components.configureServer.url.info": "رابط الخادم الذي يخص Mattermost",
|
"renderer.components.configureServer.url.info": "رابط الخادم الذي يخص Mattermost",
|
||||||
"renderer.components.configureServer.url.insecure": "الخادم URL غير امن. لإتصال امن, نأمل استخدام URL مع HTTPS protocol.",
|
"renderer.components.configureServer.url.insecure": "الخادم URL غير امن. لإتصال امن, نأمل استخدام URL مع HTTPS protocol.",
|
||||||
|
"renderer.components.configureServer.url.notMattermost": "لا يبدو أن عنوان URL للخادم المقدم يشير إلى خادم Mattermost صالح. يرجى التحقق من عنوان URL والتحقق من اتصالك.",
|
||||||
|
"renderer.components.configureServer.url.ok": "عنوان URL للخادم صالح. إصدار الخادم: {serverVersion}",
|
||||||
"renderer.components.configureServer.url.placeholder": "URL الخادم",
|
"renderer.components.configureServer.url.placeholder": "URL الخادم",
|
||||||
|
"renderer.components.configureServer.url.urlNotMatched": "لا يتطابق عنوان URL للخادم المقدم مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.configureServer.url.urlUpdated": "تم تحديث عنوان URL للخادم المقدم ليتوافق مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
"renderer.components.configureServer.url.validating": "جاري التحقق...",
|
"renderer.components.configureServer.url.validating": "جاري التحقق...",
|
||||||
"renderer.components.errorView.cannotConnectToAppName": "لا يمكن الاتصال بـ{appName}",
|
"renderer.components.errorView.cannotConnectToAppName": "لا يمكن الاتصال بـ{appName}",
|
||||||
"renderer.components.errorView.havingTroubleConnecting": "نواجه مشكلة في الاتصال بـ {appName} . سنستمر في محاولة إقامة اتصال.",
|
"renderer.components.errorView.havingTroubleConnecting": "نواجه مشكلة في الاتصال بـ {appName} . سنستمر في محاولة إقامة اتصال.",
|
||||||
|
@ -154,22 +162,143 @@
|
||||||
"renderer.components.mainPage.contextMenu.ariaLabel": "قائمة الخيارات",
|
"renderer.components.mainPage.contextMenu.ariaLabel": "قائمة الخيارات",
|
||||||
"renderer.components.mainPage.titleBar": "{appName}",
|
"renderer.components.mainPage.titleBar": "{appName}",
|
||||||
"renderer.components.newServerModal.error.nameRequired": "الاسم مطلوب.",
|
"renderer.components.newServerModal.error.nameRequired": "الاسم مطلوب.",
|
||||||
|
"renderer.components.newServerModal.error.serverUrlExists": "يوجد بالفعل خادم بنفس عنوان URL.",
|
||||||
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL ليس صحيحاً.",
|
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL ليس صحيحاً.",
|
||||||
"renderer.components.newServerModal.error.urlRequired": "لم يتم ادخال الـURL.",
|
"renderer.components.newServerModal.error.urlRequired": "لم يتم ادخال الـURL.",
|
||||||
|
"renderer.components.newServerModal.permissions.geolocation": "الموقع",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera": "المايكروفون والكاميرا",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsCameraPermissions": "تم تعطيل الكاميرا في إعدادات Windows. انقر <link>هنا</link> لفتح إعدادات الكاميرا.",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsMicrophoneaPermissions": "تم تعطيل الميكروفون في إعدادات Windows. انقر <link>هنا</link> لفتح إعدادات الميكروفون.",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications": "التنبيهات",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications.mac": "قد تحتاج أيضًا إلى تمكين الإشعارات في نظام التشغيل macOS لتطبيق Mattermost. انقر <link>هنا</link> لفتح تفضيلات النظام.",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications.windows": "قد تحتاج أيضًا إلى تمكين الإشعارات في Windows لـ Mattermost. انقر <link>هنا</link> لفتح إعدادات الإشعارات.",
|
||||||
|
"renderer.components.newServerModal.permissions.screenShare": "مشاركة الشاشة",
|
||||||
|
"renderer.components.newServerModal.permissions.title": "الصلاحيات",
|
||||||
"renderer.components.newServerModal.serverDisplayName": "اسم الخادم",
|
"renderer.components.newServerModal.serverDisplayName": "اسم الخادم",
|
||||||
"renderer.components.newServerModal.serverDisplayName.description": "اسم الخادم الى التطبيق.",
|
"renderer.components.newServerModal.serverDisplayName.description": "اسم الخادم الى التطبيق.",
|
||||||
"renderer.components.newServerModal.serverURL": "URL الخادم",
|
"renderer.components.newServerModal.serverURL": "URL الخادم",
|
||||||
|
"renderer.components.newServerModal.serverURL.description": "عنوان URL لخادم Mattermost الخاص بك. يجب أن يبدأ بـ http:// أو https://.",
|
||||||
|
"renderer.components.newServerModal.success.ok": "عنوان URL للخادم صالح. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.newServerModal.title.add": "اضافة خادم",
|
||||||
|
"renderer.components.newServerModal.title.edit": "تعديل خادم",
|
||||||
"renderer.components.newServerModal.validating": "جاري التحقق...",
|
"renderer.components.newServerModal.validating": "جاري التحقق...",
|
||||||
|
"renderer.components.newServerModal.warning.insecure": "من المحتمل أن يكون عنوان URL الخاص بخادمك غير آمن. للحصول على أفضل النتائج، استخدم عنوان URL مع بروتوكول HTTPS.",
|
||||||
|
"renderer.components.newServerModal.warning.notMattermost": "لا يبدو أن عنوان URL للخادم المقدم يشير إلى خادم Mattermost صالح. يرجى التحقق من عنوان URL والتحقق من اتصالك.",
|
||||||
|
"renderer.components.newServerModal.warning.urlNotMatched": "لا يتطابق عنوان URL للخادم مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.newServerModal.warning.urlUpdated": "تم تحديث عنوان URL للخادم المقدم ليتوافق مع عنوان URL للموقع الذي تم تكوينه على خادم Mattermost الخاص بك. إصدار الخادم: {serverVersion}",
|
||||||
|
"renderer.components.removeServerModal.body": "سيؤدي هذا إلى إزالة الخادم من تطبيق سطح المكتب الخاص بك ولكن لن يؤدي إلى حذف أي من بياناته - يمكنك إضافة الخادم مرة أخرى إلى التطبيق في أي وقت.",
|
||||||
|
"renderer.components.removeServerModal.confirm": "هل تريد تأكيد رغبتك في إزالة الخادم {serverName}؟",
|
||||||
|
"renderer.components.removeServerModal.title": "إزالة الخادم",
|
||||||
"renderer.components.saveButton.save": "حفظ",
|
"renderer.components.saveButton.save": "حفظ",
|
||||||
"renderer.components.saveButton.saving": "جاري الحفظ",
|
"renderer.components.saveButton.saving": "جاري الحفظ",
|
||||||
|
"renderer.components.serverDropdownButton.noServersConfigured": "لا توجد خوادم تم تكوينها",
|
||||||
"renderer.components.settingsPage.afterRestart": "يسري مفعول الإعداد بعد إعادة تشغيل التطبيق.",
|
"renderer.components.settingsPage.afterRestart": "يسري مفعول الإعداد بعد إعادة تشغيل التطبيق.",
|
||||||
"renderer.components.settingsPage.appLanguage": "ضبط لغة التطبيق (اختباري)",
|
"renderer.components.settingsPage.appLanguage": "ضبط لغة التطبيق (اختباري)",
|
||||||
|
"renderer.components.settingsPage.appLanguage.description": "يختار اللغة التي سيستخدمها تطبيق سطح المكتب لعناصر القائمة والنوافذ المنبثقة. لا يزال التطبيق في مرحلة الإصدار التجريبي، وقد تفتقر بعض اللغات إلى سلاسل الترجمة.",
|
||||||
|
"renderer.components.settingsPage.appLanguage.useSystemDefault": "استخدم النظام الافتراضي",
|
||||||
"renderer.components.settingsPage.appOptions": "خيارات التطبيق",
|
"renderer.components.settingsPage.appOptions": "خيارات التطبيق",
|
||||||
|
"renderer.components.settingsPage.bounceIcon": "ارتد أيقونة Dock",
|
||||||
|
"renderer.components.settingsPage.bounceIcon.description": "إذا تم تمكين هذا الخيار، يرتد رمز Dock مرة واحدة أو حتى يفتح المستخدم التطبيق عند تلقي إشعار جديد.",
|
||||||
"renderer.components.settingsPage.bounceIcon.once": "مرة",
|
"renderer.components.settingsPage.bounceIcon.once": "مرة",
|
||||||
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "حتى أفتح التطبيق",
|
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "حتى أفتح التطبيق",
|
||||||
"renderer.components.settingsPage.checkSpelling": "التحقق من الإملاء",
|
"renderer.components.settingsPage.checkSpelling": "التحقق من الإملاء",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.description": "قم بتمييز الكلمات المكتوبة بشكل خاطئ في رسائلك استنادًا إلى لغة نظامك أو تفضيلات اللغة.",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "استخدم عنوان URL للقاموس البديل",
|
||||||
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "اختر اللغة أو اللغات المرغوبة",
|
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "اختر اللغة أو اللغات المرغوبة",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.revertToDefault": "العودة إلى الوضع الافتراضي",
|
||||||
|
"renderer.components.settingsPage.checkSpelling.specifyURL": "حدد عنوان URL حيث يمكن استرداد تعريفات القاموس",
|
||||||
|
"renderer.components.settingsPage.downloadLocation": "مكان التنزيل",
|
||||||
|
"renderer.components.settingsPage.downloadLocation.description": "حدد المجلد الذي سيتم تنزيل الملفات فيه.",
|
||||||
|
"renderer.components.settingsPage.enableHardwareAcceleration": "استخدام تسريع أجهزة GPU",
|
||||||
|
"renderer.components.settingsPage.enableHardwareAcceleration.description": "إذا تم تمكينه، فسيتم عرض واجهة المستخدم {appName} بكفاءة أكبر ولكن قد يؤدي ذلك إلى انخفاض الاستقرار لبعض الأنظمة.",
|
||||||
|
"renderer.components.settingsPage.flashWindow": "اشعار بلون لأيقونة شريط المهام عند تلقي رسالة جديدة",
|
||||||
|
"renderer.components.settingsPage.flashWindow.description": "إذا تم تمكين هذا الخيار، فسوف يومض رمز شريط المهام لبضع ثوانٍ عند تلقي رسالة جديدة.",
|
||||||
|
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "قد لا تعمل هذه الوظيفة مع جميع مديري النوافذ في Linux.",
|
||||||
"renderer.components.settingsPage.flashWindow.description.note": "ملاحظة: ",
|
"renderer.components.settingsPage.flashWindow.description.note": "ملاحظة: ",
|
||||||
|
"renderer.components.settingsPage.fullscreen": "فتح التطبيق في وضع ملء الشاشة",
|
||||||
"renderer.components.settingsPage.fullscreen.description": "إذا تم التمكين، فسيتم فتح تطبيق {appName} دائماً في وضع ملء الشاشة",
|
"renderer.components.settingsPage.fullscreen.description": "إذا تم التمكين، فسيتم فتح تطبيق {appName} دائماً في وضع ملء الشاشة",
|
||||||
"renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall": "يتوفر إصدار جديد من تطبيق سطح المكتب لـ{appName} (الإصدار {version}) للتثبيت."
|
"renderer.components.settingsPage.header": "الاعدادات",
|
||||||
|
"renderer.components.settingsPage.launchAppMinimized": "تشغيل التطبيق المصغر",
|
||||||
|
"renderer.components.settingsPage.launchAppMinimized.description": "إذا تم تمكين هذا الخيار، سيبدأ التطبيق في system tray، ولن يعرض النافذة عند التشغيل.",
|
||||||
|
"renderer.components.settingsPage.loadingConfig": "جاري تحميل التكوين...",
|
||||||
|
"renderer.components.settingsPage.loggingLevel": "مستوى التسجيل",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.description": "يعد التسجيل مفيدًا للمطورين والدعم لعزل المشكلات التي قد تواجهها مع تطبيق سطح المكتب.",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.description.subtitle": "يؤدي زيادة مستوى السجل إلى زيادة استخدام مساحة القرص وقد يؤثر على الأداء. نوصي بزيادة مستوى السجل فقط إذا كنت تواجه مشكلات.",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.debug": "تصحيح الأخطاء (debug)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.error": "الأخطاء (الخطأ)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.info": "معلومات (معلومات)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.silly": "أفضل (سخيف)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.verbose": "مطوّل (مطوّل)",
|
||||||
|
"renderer.components.settingsPage.loggingLevel.level.warn": "الأخطاء والتحذيرات (التحذير)",
|
||||||
|
"renderer.components.settingsPage.minimizeToTray": "ترك التطبيق قيد التشغيل في منطقة الإشعارات عند إغلاق نافذة التطبيق",
|
||||||
|
"renderer.components.settingsPage.minimizeToTray.description": "إذا تم تمكين هذا الخيار، فسيظل التطبيق قيد التشغيل في منطقة الإعلام بعد إغلاق نافذة التطبيق.",
|
||||||
|
"renderer.components.settingsPage.saving.error": "لا يمكن حفظ التغييرات. يرجى المحاولة مرة أخرى.",
|
||||||
|
"renderer.components.settingsPage.showUnreadBadge": "إظهار الشارة الحمراء على أيقونة {taskbar} للإشارة إلى الرسائل غير المقروءة",
|
||||||
|
"renderer.components.settingsPage.showUnreadBadge.description": "بغض النظر عن هذا الإعداد، تتم الإشارة إلى الإشارات دائمًا بشارة حمراء وعدد العناصر على أيقونة {taskbar}.",
|
||||||
|
"renderer.components.settingsPage.startAppOnLogin": "ابدأ تشغيل التطبيق عند تسجيل الدخول",
|
||||||
|
"renderer.components.settingsPage.startAppOnLogin.description": "إذا تم تمكين هذا الخيار، فسيتم تشغيل التطبيق تلقائيًا عند تسجيل الدخول إلى جهازك.",
|
||||||
|
"renderer.components.settingsPage.trayIcon.color": "لون الأيقونة: ",
|
||||||
|
"renderer.components.settingsPage.trayIcon.show": "إظهار الرمز في منطقة الإشعارات",
|
||||||
|
"renderer.components.settingsPage.trayIcon.show.darwin": "إظهار أيقونة {appName} في شريط القائمة",
|
||||||
|
"renderer.components.settingsPage.trayIcon.theme.dark": "داكن",
|
||||||
|
"renderer.components.settingsPage.trayIcon.theme.light": "فاتح",
|
||||||
|
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "استخدم النظام الافتراضي",
|
||||||
|
"renderer.components.settingsPage.updates": "تحديثات",
|
||||||
|
"renderer.components.settingsPage.updates.automatic": "التحقق تلقائيًا من التحديثات",
|
||||||
|
"renderer.components.settingsPage.updates.automatic.description": "إذا تم تمكين هذا الخيار، فسيتم تنزيل التحديثات الخاصة بتطبيق سطح المكتب تلقائيًا وسيتم إعلامك عندما تصبح جاهزًا للتثبيت.",
|
||||||
|
"renderer.components.settingsPage.updates.checkNow": "التحقق من التحديثات الآن",
|
||||||
|
"renderer.components.showCertificateModal.algorithm": "خوارزمية",
|
||||||
|
"renderer.components.showCertificateModal.commonName": "الاسم الشائع",
|
||||||
|
"renderer.components.showCertificateModal.issuerName": "اسم المنشئ",
|
||||||
|
"renderer.components.showCertificateModal.noCertSelected": "لم يتم اختيار الشهادة",
|
||||||
|
"renderer.components.showCertificateModal.notValidAfter": "غير صالح بعد",
|
||||||
|
"renderer.components.showCertificateModal.notValidBefore": "غير صالح قبل",
|
||||||
|
"renderer.components.showCertificateModal.publicKeyInfo": "معلومات المفتاح العام",
|
||||||
|
"renderer.components.showCertificateModal.serialNumber": "الرقم التسلسلي",
|
||||||
|
"renderer.components.showCertificateModal.subjectName": "اسم العنوان",
|
||||||
|
"renderer.components.welcomeScreen.button.getStarted": "ابدأ هنا",
|
||||||
|
"renderer.components.welcomeScreen.slides.calls.subtitle": "عندما لا تكون الكتابة سريعة بما يكفي، يمكنك الانتقال بسلاسة من الدردشة إلى المكالمات الصوتية ومشاركة الشاشة دون الحاجة إلى تبديل الأدوات.",
|
||||||
|
"renderer.components.welcomeScreen.slides.calls.title": "ابدأ مكالمات آمنة على الفور",
|
||||||
|
"renderer.components.welcomeScreen.slides.collaborate.subtitle": "التواصل والتعاون بشكل فعال مع القنوات المستمرة ومشاركة الملفات ومقاطع التعليمات البرمجية وأتمتة سير العمل المصممة خصيصًا للفرق الفنية.",
|
||||||
|
"renderer.components.welcomeScreen.slides.collaborate.title": "التعاون في الوقت الحقيقي",
|
||||||
|
"renderer.components.welcomeScreen.slides.integrate.subtitle": "قم بتنفيذ سير العمل وأتمتته باستخدام تكاملات مرنة ومخصصة مع أدوات تقنية شائعة مثل GitHub وGitLab وServiceNow.",
|
||||||
|
"renderer.components.welcomeScreen.slides.integrate.title": "التكامل مع الأدوات التي تحبها",
|
||||||
|
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost عبارة عن منصة تعاون مفتوحة المصدر للأعمال المهمة. آمنة ومرنة ومتكاملة مع الأدوات التي تحبها.",
|
||||||
|
"renderer.components.welcomeScreen.slides.welcome.title": "مرحباً",
|
||||||
|
"renderer.downloadsDropdown.ClearAll": "مسح الكل",
|
||||||
|
"renderer.downloadsDropdown.Downloads": "التنزيلات",
|
||||||
|
"renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall": "يتوفر إصدار جديد من تطبيق سطح المكتب لـ{appName} (الإصدار {version}) للتثبيت.",
|
||||||
|
"renderer.downloadsDropdown.Update.DownloadUpdate": "تنزيل التحديث",
|
||||||
|
"renderer.downloadsDropdown.Update.MattermostVersionX": "{appName} الإصدار {version}",
|
||||||
|
"renderer.downloadsDropdown.Update.NewDesktopVersionAvailable": "إصدار سطح المكتب الجديد متاح",
|
||||||
|
"renderer.downloadsDropdown.Update.RestartAndUpdate": "إعادة التشغيل والتحديث",
|
||||||
|
"renderer.downloadsDropdown.remaining": "المتبقي",
|
||||||
|
"renderer.downloadsDropdownMenu.CancelDownload": "الغاء التنزيل",
|
||||||
|
"renderer.downloadsDropdownMenu.Clear": "مسح",
|
||||||
|
"renderer.downloadsDropdownMenu.Open": "فتح",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFileExplorer": "إظهار في مستكشف الملفات",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFileManager": "عرض في مدير الملفات",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFinder": "عرض في Finder",
|
||||||
|
"renderer.downloadsDropdownMenu.ShowInFolder": "إظهار في المجلد",
|
||||||
|
"renderer.dropdown.addAServer": "اضافة خادم",
|
||||||
|
"renderer.dropdown.servers": "الخوادم",
|
||||||
|
"renderer.modals.certificate.certificateModal.certInfoButton": "معلومات الشهادة",
|
||||||
|
"renderer.modals.certificate.certificateModal.issuer": "المنشئ",
|
||||||
|
"renderer.modals.certificate.certificateModal.noCertsAvailable": "لا يوجد شهادة متاحة",
|
||||||
|
"renderer.modals.certificate.certificateModal.serial": "تسلسل",
|
||||||
|
"renderer.modals.certificate.certificateModal.subject": "العنوان",
|
||||||
|
"renderer.modals.certificate.certificateModal.subtitle": "حدد شهادة للتحقق من هويتك {url}",
|
||||||
|
"renderer.modals.certificate.certificateModal.title": "اختيار شهادة",
|
||||||
|
"renderer.modals.login.loginModal.message.proxy": "يتطلب الوكيل {host}:{port} اسم مستخدم وكلمة مرور.",
|
||||||
|
"renderer.modals.login.loginModal.message.server": "يتطلب الخادم {url} اسم مستخدم وكلمة مرور.",
|
||||||
|
"renderer.modals.login.loginModal.password": "كلمة المرور",
|
||||||
|
"renderer.modals.login.loginModal.title": "المصادقة مطلوبة",
|
||||||
|
"renderer.modals.login.loginModal.username": "اسم المستخدم",
|
||||||
|
"renderer.modals.permission.permissionModal.body": "يتطلب الموقع الذي لم يتم تضمينه في تكوين خادم Mattermost الخاص بك الوصول إلى {permission}.",
|
||||||
|
"renderer.modals.permission.permissionModal.requestOriginatedFromOrigin": "نشأ هذا الطلب من <link>{origin}</link>",
|
||||||
|
"renderer.modals.permission.permissionModal.title": "{permission} مطلوب",
|
||||||
|
"renderer.modals.permission.permissionModal.unknownOrigin": "مصدر غير معروف",
|
||||||
|
"renderer.time.hours": "ساعات",
|
||||||
|
"renderer.time.mins": "دقائق",
|
||||||
|
"renderer.time.sec": "ثواني"
|
||||||
}
|
}
|
||||||
|
|
134
i18n/cs.json
134
i18n/cs.json
|
@ -19,8 +19,8 @@
|
||||||
"label.yes": "Ano",
|
"label.yes": "Ano",
|
||||||
"main.CriticalErrorHandler.uncaughtException.button.reopen": "Znovu otevřít",
|
"main.CriticalErrorHandler.uncaughtException.button.reopen": "Znovu otevřít",
|
||||||
"main.CriticalErrorHandler.uncaughtException.button.showDetails": "Zobrazit podrobnosti",
|
"main.CriticalErrorHandler.uncaughtException.button.showDetails": "Zobrazit podrobnosti",
|
||||||
"main.CriticalErrorHandler.uncaughtException.dialog.message": "Aplikace {appName} neočekávaně skončila. Kliknutím na \"{showDetails}\" se dozvíte více nebo \"{reopen}\" aplikaci znovu otevřete.\n\nVnitřní chyba: Vnitřní chyba: {err}",
|
"main.CriticalErrorHandler.uncaughtException.dialog.message": "Aplikace {appName} neočekávaně skončila. Kliknutím na \"{showDetails}\" se dozvíte více nebo \"{reopen}\" aplikaci znovu otevřete.\n\nVnitřní chyba: {err}",
|
||||||
"main.CriticalErrorHandler.unresponsive.dialog.message": "Okno již nereaguje.\nPočkáte, až bude okno opět reagovat?",
|
"main.CriticalErrorHandler.unresponsive.dialog.message": "Okno nereaguje.\nChcete počkat, až bude okno opět reagovat?",
|
||||||
"main.allowProtocolDialog.button.saveProtocolAsAllowed": "Ano (Uložit {protocol} jako povolený)",
|
"main.allowProtocolDialog.button.saveProtocolAsAllowed": "Ano (Uložit {protocol} jako povolený)",
|
||||||
"main.allowProtocolDialog.detail": "Požadovaný odkaz je {URL}. Chcete pokračovat?",
|
"main.allowProtocolDialog.detail": "Požadovaný odkaz je {URL}. Chcete pokračovat?",
|
||||||
"main.allowProtocolDialog.message": "Odkaz {protocol} vyžaduje externí aplikaci.",
|
"main.allowProtocolDialog.message": "Odkaz {protocol} vyžaduje externí aplikaci.",
|
||||||
|
@ -41,55 +41,55 @@
|
||||||
"main.app.utils.migrateMacAppStore.dialog.detail": "Zdá se, že již existuje konfigurace {appName}, chcete ji importovat? Budete vyzváni k výběru správného adresáře konfigurace.",
|
"main.app.utils.migrateMacAppStore.dialog.detail": "Zdá se, že již existuje konfigurace {appName}, chcete ji importovat? Budete vyzváni k výběru správného adresáře konfigurace.",
|
||||||
"main.app.utils.migrateMacAppStore.dialog.message": "Import stávající konfigurace",
|
"main.app.utils.migrateMacAppStore.dialog.message": "Import stávající konfigurace",
|
||||||
"main.autoUpdater.noUpdate.detail": "Používáte nejnovější verzi aplikace {appName} Desktop App (verze {version}). Jakmile bude k dispozici nová verze k instalaci, budete o tom informováni.",
|
"main.autoUpdater.noUpdate.detail": "Používáte nejnovější verzi aplikace {appName} Desktop App (verze {version}). Jakmile bude k dispozici nová verze k instalaci, budete o tom informováni.",
|
||||||
"main.autoUpdater.noUpdate.message": "Vaše verze je aktuální",
|
"main.autoUpdater.noUpdate.message": "Vše je aktuální",
|
||||||
"main.badge.noUnreads": "Nemáte žádné nepřečtené zprávy",
|
"main.badge.noUnreads": "Nemáte žádné nepřečtené zprávy",
|
||||||
"main.badge.sessionExpired": "Sezení vypršelo: Přihlaste se, abyste nadále dostávali upozornění.",
|
"main.badge.sessionExpired": "Sezení vypršelo: Přihlaste se, abyste nadále dostávali upozornění.",
|
||||||
"main.badge.unreadChannels": "Máte nepřečtené kanály",
|
"main.badge.unreadChannels": "Máte nepřečtené kanály",
|
||||||
"main.badge.unreadMentions": "Máte nepřečtené zmínky ({mentionCount})",
|
"main.badge.unreadMentions": "Máte nepřečtené zmínky ({mentionCount})",
|
||||||
"main.downloadsManager.resetDownloadsFolder": "Prosím resetujte složku, do které se budou stahovat soubory",
|
"main.downloadsManager.resetDownloadsFolder": "Prosím resetujte složku, do které se budou stahovat soubory",
|
||||||
"main.downloadsManager.specifyDownloadsFolder": "Zadejte složku, do které se budou stahovat soubory",
|
"main.downloadsManager.specifyDownloadsFolder": "Zadejte složku, do které se budou stahovat soubory",
|
||||||
"main.menus.app.edit": "Upravit",
|
"main.menus.app.edit": "a Upravit",
|
||||||
"main.menus.app.edit.copy": "Kopírovat",
|
"main.menus.app.edit.copy": "Kopírovat",
|
||||||
"main.menus.app.edit.cut": "Vyjmout",
|
"main.menus.app.edit.cut": "Vyjmout",
|
||||||
"main.menus.app.edit.paste": "Vložit",
|
"main.menus.app.edit.paste": "Vložit",
|
||||||
"main.menus.app.edit.pasteAndMatchStyle": "Vložit a přizpůsobit styl",
|
"main.menus.app.edit.pasteAndMatchStyle": "Vložit a přizpůsobit styl",
|
||||||
"main.menus.app.edit.redo": "Dopředu",
|
"main.menus.app.edit.redo": "Znovu provést",
|
||||||
"main.menus.app.edit.selectAll": "Vybrat vše",
|
"main.menus.app.edit.selectAll": "Vybrat vše",
|
||||||
"main.menus.app.edit.undo": "Zpět",
|
"main.menus.app.edit.undo": "Vrátit zpět",
|
||||||
"main.menus.app.file": "&Soubor",
|
"main.menus.app.file": "a Soubor",
|
||||||
"main.menus.app.file.about": "O {appName}",
|
"main.menus.app.file.about": "O {appName}",
|
||||||
"main.menus.app.file.exit": "Konec",
|
"main.menus.app.file.exit": "Ukončit",
|
||||||
"main.menus.app.file.hide": "Skrýt {appName}",
|
"main.menus.app.file.hide": "Skrýt {appName}",
|
||||||
"main.menus.app.file.hideOthers": "Skrýt ostatní",
|
"main.menus.app.file.hideOthers": "Skrýt ostatní",
|
||||||
"main.menus.app.file.preferences": "Předvolby...",
|
"main.menus.app.file.preferences": "Volby...",
|
||||||
"main.menus.app.file.quit": "Ukončit {appName}",
|
"main.menus.app.file.quit": "Ukončit {appName}",
|
||||||
"main.menus.app.file.settings": "Nastavení...",
|
"main.menus.app.file.settings": "Nastavení...",
|
||||||
"main.menus.app.file.signInToAnotherServer": "Přihlášení k jinému serveru",
|
"main.menus.app.file.signInToAnotherServer": "Přihlášení k jinému serveru",
|
||||||
"main.menus.app.file.unhide": "Zobrazit vše",
|
"main.menus.app.file.unhide": "Zobrazit vše",
|
||||||
"main.menus.app.help": "Ná&pověda",
|
"main.menus.app.help": "Ná&pověda",
|
||||||
"main.menus.app.help.RunDiagnostics": "Spustit diagnostiku",
|
"main.menus.app.help.RunDiagnostics": "Spustit diagnostiku",
|
||||||
"main.menus.app.help.ShowLogs": "Zobrazit logy",
|
"main.menus.app.help.ShowLogs": "Zobrazit záznamy",
|
||||||
"main.menus.app.help.checkForUpdates": "Zkontrolovat aktualizace",
|
"main.menus.app.help.checkForUpdates": "Zkontrolovat aktualizace",
|
||||||
"main.menus.app.help.commitString": " odeslat: {hashVersion}",
|
"main.menus.app.help.commitString": " odeslat: {hashVersion}",
|
||||||
"main.menus.app.help.downloadUpdate": "Aktualizace ke stažení",
|
"main.menus.app.help.downloadUpdate": "Stáhnout Aktualizaci",
|
||||||
"main.menus.app.help.learnMore": "Zjistit více...",
|
"main.menus.app.help.learnMore": "Zjistit více...",
|
||||||
"main.menus.app.help.restartAndUpdate": "Restart a aktualizace",
|
"main.menus.app.help.restartAndUpdate": "Restartovat a Aktualizovat",
|
||||||
"main.menus.app.help.versionString": "Verze {version}{commit}",
|
"main.menus.app.help.versionString": "Verze {version}{commit}",
|
||||||
"main.menus.app.history": "&Historie",
|
"main.menus.app.history": "&Historie",
|
||||||
"main.menus.app.history.back": "Zpět",
|
"main.menus.app.history.back": "Zpět",
|
||||||
"main.menus.app.history.forward": "Vpřed",
|
"main.menus.app.history.forward": "Vpřed",
|
||||||
"main.menus.app.view": "&Zobrazit",
|
"main.menus.app.view": "&Zobrazit",
|
||||||
"main.menus.app.view.actualSize": "Skutečná velikost",
|
"main.menus.app.view.actualSize": "Skutečná velikost",
|
||||||
"main.menus.app.view.clearCacheAndReload": "Vymazání mezipaměti a opětovné načtení",
|
"main.menus.app.view.clearCacheAndReload": "Vymazat mezipaměť a znovu načíst",
|
||||||
"main.menus.app.view.devToolsAppWrapper": "Vývojářské nástroje pro Application Wrapper",
|
"main.menus.app.view.devToolsAppWrapper": "Vývojářské nástroje pro Application Wrapper",
|
||||||
"main.menus.app.view.devToolsCurrentCallWidget": "Vývojářské nástroje pro Widget volání",
|
"main.menus.app.view.devToolsCurrentCallWidget": "Vývojářské nástroje pro Widget volání",
|
||||||
"main.menus.app.view.devToolsCurrentServer": "Vývojářské nástroje pro aktuální server",
|
"main.menus.app.view.devToolsCurrentServer": "Vývojářské nástroje pro aktuální server",
|
||||||
"main.menus.app.view.devToolsSubMenu": "Vývojářské nástroje",
|
"main.menus.app.view.devToolsSubMenu": "Vývojářské Nástroje",
|
||||||
"main.menus.app.view.downloads": "Stáhnout",
|
"main.menus.app.view.downloads": "Stažení",
|
||||||
"main.menus.app.view.find": "Najít..",
|
"main.menus.app.view.find": "Najít..",
|
||||||
"main.menus.app.view.fullscreen": "Přepnutí na celou obrazovku",
|
"main.menus.app.view.fullscreen": "Přepnout na režim celé obrazovky",
|
||||||
"main.menus.app.view.reload": "Načíst znovu",
|
"main.menus.app.view.reload": "Načíst znovu",
|
||||||
"main.menus.app.view.toggleDarkMode": "Přepínání tmavého režimu",
|
"main.menus.app.view.toggleDarkMode": "Zapnout tmavý mód",
|
||||||
"main.menus.app.view.zoomIn": "Přiblížit",
|
"main.menus.app.view.zoomIn": "Přiblížit",
|
||||||
"main.menus.app.view.zoomOut": "Oddálit",
|
"main.menus.app.view.zoomOut": "Oddálit",
|
||||||
"main.menus.app.window": "&Okno",
|
"main.menus.app.window": "&Okno",
|
||||||
|
@ -104,10 +104,10 @@
|
||||||
"main.menus.tray.preferences": "Předvolby...",
|
"main.menus.tray.preferences": "Předvolby...",
|
||||||
"main.menus.tray.settings": "Nastavení...",
|
"main.menus.tray.settings": "Nastavení...",
|
||||||
"main.notifications.download.complete.body": "Stahování dokončeno \n {fileName}",
|
"main.notifications.download.complete.body": "Stahování dokončeno \n {fileName}",
|
||||||
"main.notifications.download.complete.title": "Stažení kompletní",
|
"main.notifications.download.complete.title": "Stahování dokončeno",
|
||||||
"main.notifications.mention.title": "Někdo se o vás zmínil",
|
"main.notifications.mention.title": "Někdo se o vás zmínil",
|
||||||
"main.notifications.upgrade.newVersion.body": "Nová verze je dostupná ke stažení.",
|
"main.notifications.upgrade.newVersion.body": "Nová verze je dostupná ke stažení.",
|
||||||
"main.notifications.upgrade.newVersion.title": "K dispozici je nová verze pro stolní počítače",
|
"main.notifications.upgrade.newVersion.title": "K dispozici je nová verze pro Stolní počítače",
|
||||||
"main.notifications.upgrade.readyToInstall.body": "Nová verze pro stolní počítače je nyní připravena k instalaci.",
|
"main.notifications.upgrade.readyToInstall.body": "Nová verze pro stolní počítače je nyní připravena k instalaci.",
|
||||||
"main.notifications.upgrade.readyToInstall.title": "Kliknutím restartujete a nainstalujete aktualizaci",
|
"main.notifications.upgrade.readyToInstall.title": "Kliknutím restartujete a nainstalujete aktualizaci",
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.geolocation": "{appName} použije vaši polohu pro nastavení časové zóny. Toto nastavení můžete kdykoli později změnit v nastavení vašeho počítače.",
|
"main.permissionsManager.checkPermission.dialog.detail.geolocation": "{appName} použije vaši polohu pro nastavení časové zóny. Toto nastavení můžete kdykoli později změnit v nastavení vašeho počítače.",
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.screenShare": "{appName} použije toto oprávnění ke sdílení vaší obrazovky pro hovory. Toto můžete kdykoli později změnit v nastavení počítače.",
|
"main.permissionsManager.checkPermission.dialog.detail.screenShare": "{appName} použije toto oprávnění ke sdílení vaší obrazovky pro hovory. Toto můžete kdykoli později změnit v nastavení počítače.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) vyžaduje přístup k vaší poloze.",
|
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) vyžaduje přístup k vaší poloze.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) vyžaduje přístup k vašemu mikrofonu a kameře.",
|
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) vyžaduje přístup k vašemu mikrofonu a kameře.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) by vám chtěla zasílat notifikace.",
|
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) by vám chtěl zasílat notifikace.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.openExternal": "{appName} ({url}) by rád získal povolení k otevření následující adresy URL: {externalURL}",
|
"main.permissionsManager.checkPermission.dialog.message.openExternal": "{appName} ({url}) by rád získal povolení k otevření následující adresy URL: {externalURL}",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.screenShare": "{appName} ({url}) chce mít možnost zobrazit vaši obrazovku.",
|
"main.permissionsManager.checkPermission.dialog.message.screenShare": "{appName} ({url}) chce mít možnost zobrazit vaši obrazovku.",
|
||||||
"main.permissionsManager.checkPermission.dialog.title": "Vyžadováno povolení",
|
"main.permissionsManager.checkPermission.dialog.title": "Vyžadováno povolení",
|
||||||
|
@ -126,23 +126,23 @@
|
||||||
"main.tray.tray.unread": "Máte nepřečtené kanály",
|
"main.tray.tray.unread": "Máte nepřečtené kanály",
|
||||||
"main.views.viewManager.handleDeepLink.error.body": "V aplikaci není nakonfigurován žádný server, který by odpovídal požadované url adrese: {url}",
|
"main.views.viewManager.handleDeepLink.error.body": "V aplikaci není nakonfigurován žádný server, který by odpovídal požadované url adrese: {url}",
|
||||||
"main.views.viewManager.handleDeepLink.error.title": "Žádný odpovídající server",
|
"main.views.viewManager.handleDeepLink.error.title": "Žádný odpovídající server",
|
||||||
"main.windows.mainWindow.closeApp.dialog.checkboxLabel": "Už nezobrazovat",
|
"main.windows.mainWindow.closeApp.dialog.checkboxLabel": "Neptat se znovu",
|
||||||
"main.windows.mainWindow.closeApp.dialog.detail": "Nebudete již dostávat oznámení o zprávách. Pokud chcete nechat {appName} běžet v systémové liště, můžete to povolit v Nastavení.",
|
"main.windows.mainWindow.closeApp.dialog.detail": "Nebudete již dostávat oznámení o zprávách. Pokud chcete nechat {appName} běžet v systémové liště, můžete to povolit v Nastavení.",
|
||||||
"main.windows.mainWindow.closeApp.dialog.message": "Jste si jistý, že chcete skončit?",
|
"main.windows.mainWindow.closeApp.dialog.message": "Jste si jistý, že chcete odejít?",
|
||||||
"main.windows.mainWindow.closeApp.dialog.title": "Zavřít aplikaci",
|
"main.windows.mainWindow.closeApp.dialog.title": "Zavřít aplikaci",
|
||||||
"main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel": "Už nezobrazovat",
|
"main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel": "Již nezobrazovat",
|
||||||
"main.windows.mainWindow.minimizeToTray.dialog.message": "{appName} se bude nadále spouštět v systémové liště. Tuto funkci lze vypnout v Nastavení.",
|
"main.windows.mainWindow.minimizeToTray.dialog.message": "{appName} bude nadále běžet v systémové liště. Tuto funkci lze vypnout v Nastavení.",
|
||||||
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimalizovat do panelu",
|
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimalizovat do systémové lišty",
|
||||||
"renderer.components.autoSaveIndicator.saved": "Uloženo",
|
"renderer.components.autoSaveIndicator.saved": "Uloženo",
|
||||||
"renderer.components.autoSaveIndicator.saving": "Ukládání...",
|
"renderer.components.autoSaveIndicator.saving": "Ukládání...",
|
||||||
"renderer.components.configureServer.cardtitle": "Zadejte údaje o serveru",
|
"renderer.components.configureServer.cardtitle": "Zadejte podrobnosti o serveru",
|
||||||
"renderer.components.configureServer.connect.default": "Připojit",
|
"renderer.components.configureServer.connect.default": "Navázat spojení",
|
||||||
"renderer.components.configureServer.connect.override": "Přesto se připojit",
|
"renderer.components.configureServer.connect.override": "Přesto se připojit",
|
||||||
"renderer.components.configureServer.connect.saving": "Připojuji…",
|
"renderer.components.configureServer.connect.saving": "Připojuji…",
|
||||||
"renderer.components.configureServer.name.info": "Název, který se zobrazí v seznamu serverů",
|
"renderer.components.configureServer.name.info": "Název, který se zobrazí v seznamu serverů",
|
||||||
"renderer.components.configureServer.name.placeholder": "Zadejte zobrazovaný název",
|
"renderer.components.configureServer.name.placeholder": "Zobrazovací název serveru",
|
||||||
"renderer.components.configureServer.subtitle": "Nastavení prvního serveru pro připojení ke komunikačnímu uzlu vašeho týmu<br></br>",
|
"renderer.components.configureServer.subtitle": "Nastavte svůj první server pro připojení k<br></br>komunikačnímu centru vašeho týmu",
|
||||||
"renderer.components.configureServer.title": "Připojíme se k serveru",
|
"renderer.components.configureServer.title": "Připojme se k serveru",
|
||||||
"renderer.components.configureServer.url.info": "Adresa URL vašeho serveru Mattermost",
|
"renderer.components.configureServer.url.info": "Adresa URL vašeho serveru Mattermost",
|
||||||
"renderer.components.configureServer.url.insecure": "URL adresa vašeho serveru je potenciálně nezabezpečená. Z důvodu bezpečnosti nastavte URL adresu s HTTPS protokolem.",
|
"renderer.components.configureServer.url.insecure": "URL adresa vašeho serveru je potenciálně nezabezpečená. Z důvodu bezpečnosti nastavte URL adresu s HTTPS protokolem.",
|
||||||
"renderer.components.configureServer.url.notMattermost": "Zdá se, že zadaná URL adresa neodkazuje na platný server Mattermost. Ověřte prosím URL adresu a zkontrolujte připojení.",
|
"renderer.components.configureServer.url.notMattermost": "Zdá se, že zadaná URL adresa neodkazuje na platný server Mattermost. Ověřte prosím URL adresu a zkontrolujte připojení.",
|
||||||
|
@ -153,7 +153,7 @@
|
||||||
"renderer.components.configureServer.url.validating": "Ověřování...",
|
"renderer.components.configureServer.url.validating": "Ověřování...",
|
||||||
"renderer.components.errorView.cannotConnectToAppName": "Nelze se připojit k {appName}",
|
"renderer.components.errorView.cannotConnectToAppName": "Nelze se připojit k {appName}",
|
||||||
"renderer.components.errorView.havingTroubleConnecting": "Máme potíže s připojením k {appName}. Budeme pokračovat v pokusech o navázání spojení.",
|
"renderer.components.errorView.havingTroubleConnecting": "Máme potíže s připojením k {appName}. Budeme pokračovat v pokusech o navázání spojení.",
|
||||||
"renderer.components.errorView.refreshThenVerify": "Pokud obnovení této stránky (Ctrl+R nebo Command+R) nefunguje, ověřte si to:",
|
"renderer.components.errorView.refreshThenVerify": "Pokud obnovení této stránky (Ctrl+R nebo Command+R) nefunguje, ověřte si:",
|
||||||
"renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow": "Na adresu <link>{url}</link> se dostanete z okna prohlížeče.",
|
"renderer.components.errorView.troubleshooting.browserView.canReachFromBrowserWindow": "Na adresu <link>{url}</link> se dostanete z okna prohlížeče.",
|
||||||
"renderer.components.errorView.troubleshooting.computerIsConnected": "Váš počítač je připojen k internetu.",
|
"renderer.components.errorView.troubleshooting.computerIsConnected": "Váš počítač je připojen k internetu.",
|
||||||
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "Adresa URL {appName} <link>{url}</link> je správná",
|
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "Adresa URL {appName} <link>{url}</link> je správná",
|
||||||
|
@ -169,17 +169,17 @@
|
||||||
"renderer.components.newServerModal.permissions.microphoneAndCamera": "Mikrofon a kamera",
|
"renderer.components.newServerModal.permissions.microphoneAndCamera": "Mikrofon a kamera",
|
||||||
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsCameraPermissions": "Kamera není povolena v nastavení Windows. Klikněte <link>sem</link> pro otevření nastavení kamery.",
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsCameraPermissions": "Kamera není povolena v nastavení Windows. Klikněte <link>sem</link> pro otevření nastavení kamery.",
|
||||||
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsMicrophoneaPermissions": "Mikrofon není povolen v nastavení Windows. Klikněte <link>sem</link> pro otevření nastavení mikrofonu.",
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsMicrophoneaPermissions": "Mikrofon není povolen v nastavení Windows. Klikněte <link>sem</link> pro otevření nastavení mikrofonu.",
|
||||||
"renderer.components.newServerModal.permissions.notifications": "Notifikace",
|
"renderer.components.newServerModal.permissions.notifications": "Oznámení",
|
||||||
"renderer.components.newServerModal.permissions.notifications.mac": "Je třeba povolit notifikace pro Mattermost také v macOS. Klikněte <link>sem</link> pro otevření Systémových nastavení.",
|
"renderer.components.newServerModal.permissions.notifications.mac": "Je třeba povolit notifikace pro Mattermost také v macOS. Klikněte <link>sem</link> pro otevření Systémových nastavení.",
|
||||||
"renderer.components.newServerModal.permissions.notifications.windows": "Je třeba povolit notifikace pro Mattermost také ve Windows. Klikněte <link>sem</link> pro otevření nastavení systému.",
|
"renderer.components.newServerModal.permissions.notifications.windows": "Je třeba povolit notifikace pro Mattermost také ve Windows. Klikněte <link>sem</link> pro otevření nastavení systému.",
|
||||||
"renderer.components.newServerModal.permissions.screenShare": "Sdílení obrazovky",
|
"renderer.components.newServerModal.permissions.screenShare": "Sdílení obrazovky",
|
||||||
"renderer.components.newServerModal.permissions.title": "Oprávnění",
|
"renderer.components.newServerModal.permissions.title": "Povolení",
|
||||||
"renderer.components.newServerModal.serverDisplayName": "Zobrazovaný název serveru",
|
"renderer.components.newServerModal.serverDisplayName": "Zobrazovaný název serveru",
|
||||||
"renderer.components.newServerModal.serverDisplayName.description": "Název serveru zobrazený na panelu karet aplikace pro stolní počítače.",
|
"renderer.components.newServerModal.serverDisplayName.description": "Název serveru zobrazený na panelu karet aplikace pro stolní počítače.",
|
||||||
"renderer.components.newServerModal.serverURL": "Adresa URL serveru",
|
"renderer.components.newServerModal.serverURL": "Adresa URL serveru",
|
||||||
"renderer.components.newServerModal.serverURL.description": "Adresa URL vašeho serveru Mattermost. Musí začínat http:// nebo https://.",
|
"renderer.components.newServerModal.serverURL.description": "Adresa URL vašeho serveru Mattermost. Musí začínat http:// nebo https://.",
|
||||||
"renderer.components.newServerModal.success.ok": "Adresa URL serveru je platná. Verze serveru: {serverVersion}",
|
"renderer.components.newServerModal.success.ok": "Adresa URL serveru je platná. Verze serveru: {serverVersion}",
|
||||||
"renderer.components.newServerModal.title.add": "Přidat server",
|
"renderer.components.newServerModal.title.add": "Přidat Server",
|
||||||
"renderer.components.newServerModal.title.edit": "Upravit server",
|
"renderer.components.newServerModal.title.edit": "Upravit server",
|
||||||
"renderer.components.newServerModal.validating": "Ověřování...",
|
"renderer.components.newServerModal.validating": "Ověřování...",
|
||||||
"renderer.components.newServerModal.warning.insecure": "Adresa URL vašeho serveru je potenciálně nezabezpečená. Nejlepších výsledků dosáhnete, když použijete adresu URL s protokolem HTTPS.",
|
"renderer.components.newServerModal.warning.insecure": "Adresa URL vašeho serveru je potenciálně nezabezpečená. Nejlepších výsledků dosáhnete, když použijete adresu URL s protokolem HTTPS.",
|
||||||
|
@ -198,9 +198,9 @@
|
||||||
"renderer.components.newTeamModal.serverURL.description": "Adresa URL vašeho serveru Mattermost. Musí začínat http:// nebo https://.",
|
"renderer.components.newTeamModal.serverURL.description": "Adresa URL vašeho serveru Mattermost. Musí začínat http:// nebo https://.",
|
||||||
"renderer.components.newTeamModal.title.add": "Přidat server",
|
"renderer.components.newTeamModal.title.add": "Přidat server",
|
||||||
"renderer.components.newTeamModal.title.edit": "Upravit server",
|
"renderer.components.newTeamModal.title.edit": "Upravit server",
|
||||||
"renderer.components.removeServerModal.body": "Toto server odstraní z aplikace pro počítače, ale neodstraní se žádná jeho data - server můžete do aplikace kdykoli přidat zpět.",
|
"renderer.components.removeServerModal.body": "Toto odstraní server z aplikace pro počítače, ale neodstraní se žádná jeho data - server můžete do aplikace kdykoli přidat zpět.",
|
||||||
"renderer.components.removeServerModal.confirm": "Potvrzujete, že si přejete odebrat server {serverName}?",
|
"renderer.components.removeServerModal.confirm": "Potvrďte, že si přejete odstranit server {serverName}?",
|
||||||
"renderer.components.removeServerModal.title": "Odebrání serveru",
|
"renderer.components.removeServerModal.title": "Odebrat Server",
|
||||||
"renderer.components.saveButton.save": "Uložit",
|
"renderer.components.saveButton.save": "Uložit",
|
||||||
"renderer.components.saveButton.saving": "Ukládám",
|
"renderer.components.saveButton.saving": "Ukládám",
|
||||||
"renderer.components.serverDropdownButton.noServersConfigured": "Nejsou nakonfigurovány žádné servery",
|
"renderer.components.serverDropdownButton.noServersConfigured": "Nejsou nakonfigurovány žádné servery",
|
||||||
|
@ -210,7 +210,7 @@
|
||||||
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Použít výchozí nastavení systému",
|
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Použít výchozí nastavení systému",
|
||||||
"renderer.components.settingsPage.appOptions": "Možnosti aplikace",
|
"renderer.components.settingsPage.appOptions": "Možnosti aplikace",
|
||||||
"renderer.components.settingsPage.bounceIcon": "Skákající ikona v Docku",
|
"renderer.components.settingsPage.bounceIcon": "Skákající ikona v Docku",
|
||||||
"renderer.components.settingsPage.bounceIcon.description": "Pokud je tato funkce povolena, ikona Docku se při přijetí nového oznámení jednou odrazí nebo dokud uživatel aplikaci neotevře.",
|
"renderer.components.settingsPage.bounceIcon.description": "Pokud je povoleno, ikona na Docku se při přijetí nové notifikace jednou nebo opakovaně pohne, dokud uživatel aplikaci neotevře.",
|
||||||
"renderer.components.settingsPage.bounceIcon.once": "jednou",
|
"renderer.components.settingsPage.bounceIcon.once": "jednou",
|
||||||
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "dokud aplikaci neotevřu",
|
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "dokud aplikaci neotevřu",
|
||||||
"renderer.components.settingsPage.checkSpelling": "Kontrola pravopisu",
|
"renderer.components.settingsPage.checkSpelling": "Kontrola pravopisu",
|
||||||
|
@ -224,13 +224,13 @@
|
||||||
"renderer.components.settingsPage.enableHardwareAcceleration": "Použití hardwarové akcelerace GPU",
|
"renderer.components.settingsPage.enableHardwareAcceleration": "Použití hardwarové akcelerace GPU",
|
||||||
"renderer.components.settingsPage.enableHardwareAcceleration.description": "Pokud je povoleno, uživatelské rozhraní aplikace {appName} se vykresluje efektivněji, ale u některých systémů může vést ke snížení stability.",
|
"renderer.components.settingsPage.enableHardwareAcceleration.description": "Pokud je povoleno, uživatelské rozhraní aplikace {appName} se vykresluje efektivněji, ale u některých systémů může vést ke snížení stability.",
|
||||||
"renderer.components.settingsPage.flashWindow": "Blikání ikony na hlavním panelu při přijetí nové zprávy",
|
"renderer.components.settingsPage.flashWindow": "Blikání ikony na hlavním panelu při přijetí nové zprávy",
|
||||||
"renderer.components.settingsPage.flashWindow.description": "Pokud je tato funkce povolena, ikona na hlavním panelu při přijetí nové zprávy na několik sekund zabliká.",
|
"renderer.components.settingsPage.flashWindow.description": "Pokud je povoleno, ikona na hlavním panelu se na několik sekund rozbliká, když je přijata nová zpráva.",
|
||||||
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "Tato funkce nemusí fungovat ve všech správcích oken systému Linux.",
|
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "Tato funkce nemusí fungovat ve všech správcích oken systému Linux.",
|
||||||
"renderer.components.settingsPage.flashWindow.description.note": "POZNÁMKA: ",
|
"renderer.components.settingsPage.flashWindow.description.note": "POZNÁMKA: ",
|
||||||
"renderer.components.settingsPage.fullscreen": "Otevření aplikace na celou obrazovku",
|
"renderer.components.settingsPage.fullscreen": "Otevřít aplikaci v režimu celé obrazovky",
|
||||||
"renderer.components.settingsPage.fullscreen.description": "Pokud je povoleno, aplikace {appName} se vždy otevře na celé obrazovce",
|
"renderer.components.settingsPage.fullscreen.description": "Pokud je povoleno, aplikace {appName} se vždy otevře v režimu celé obrazovky",
|
||||||
"renderer.components.settingsPage.header": "Nastavení",
|
"renderer.components.settingsPage.header": "Nastavení",
|
||||||
"renderer.components.settingsPage.launchAppMinimized": "Spuštění aplikace minimalizované",
|
"renderer.components.settingsPage.launchAppMinimized": "Spustit aplikaci minimalizovanou",
|
||||||
"renderer.components.settingsPage.launchAppMinimized.description": "Pokud je tato možnost povolena, aplikace se spustí v systémové liště a při spuštění se nezobrazí okno.",
|
"renderer.components.settingsPage.launchAppMinimized.description": "Pokud je tato možnost povolena, aplikace se spustí v systémové liště a při spuštění se nezobrazí okno.",
|
||||||
"renderer.components.settingsPage.loadingConfig": "Načítání konfigurace...",
|
"renderer.components.settingsPage.loadingConfig": "Načítání konfigurace...",
|
||||||
"renderer.components.settingsPage.loggingLevel": "Úroveň protokolování",
|
"renderer.components.settingsPage.loggingLevel": "Úroveň protokolování",
|
||||||
|
@ -239,16 +239,16 @@
|
||||||
"renderer.components.settingsPage.loggingLevel.level.debug": "Ladění (debug)",
|
"renderer.components.settingsPage.loggingLevel.level.debug": "Ladění (debug)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.error": "Chyby (error)",
|
"renderer.components.settingsPage.loggingLevel.level.error": "Chyby (error)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.info": "Informace (info)",
|
"renderer.components.settingsPage.loggingLevel.level.info": "Informace (info)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.silly": "Finest (hloupé)",
|
"renderer.components.settingsPage.loggingLevel.level.silly": "Nejpodrobnější (ne vážné)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.verbose": "Verbose (slovní)",
|
"renderer.components.settingsPage.loggingLevel.level.verbose": "Podrobný (podrobný)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.warn": "Chyby a varování (warn)",
|
"renderer.components.settingsPage.loggingLevel.level.warn": "Chyby a varování (varovat)",
|
||||||
"renderer.components.settingsPage.minimizeToTray": "Ponechání spuštěné aplikace v oznamovací oblasti po zavření okna aplikace",
|
"renderer.components.settingsPage.minimizeToTray": "Ponechání spuštěné aplikace v oznamovací oblasti po zavření okna aplikace",
|
||||||
"renderer.components.settingsPage.minimizeToTray.description": "Pokud je tato možnost povolena, aplikace zůstane spuštěna v oznamovací oblasti i po zavření okna aplikace.",
|
"renderer.components.settingsPage.minimizeToTray.description": "Pokud je tato možnost povolena, aplikace zůstane spuštěna v oznamovací oblasti i po zavření okna aplikace.",
|
||||||
"renderer.components.settingsPage.saving.error": "Nelze uložit změny. Zkuste to prosím znovu.",
|
"renderer.components.settingsPage.saving.error": "Nelze uložit změny. Zkuste to prosím znovu.",
|
||||||
"renderer.components.settingsPage.showUnreadBadge": "Zobrazení červeného odznaku na ikoně {taskbar} pro označení nepřečtených zpráv",
|
"renderer.components.settingsPage.showUnreadBadge": "Zobrazení červeného odznaku na ikoně {taskbar} pro označení nepřečtených zpráv",
|
||||||
"renderer.components.settingsPage.showUnreadBadge.description": "Bez ohledu na toto nastavení jsou zmínky vždy označeny červeným odznakem a počtem položek na ikoně {taskbar}.",
|
"renderer.components.settingsPage.showUnreadBadge.description": "Bez ohledu na toto nastavení jsou zmínky vždy označeny červeným odznakem a počtem položek na ikoně {taskbar}.",
|
||||||
"renderer.components.settingsPage.startAppOnLogin": "Spuštění aplikace po přihlášení",
|
"renderer.components.settingsPage.startAppOnLogin": "Spustit aplikaci při přihlášení",
|
||||||
"renderer.components.settingsPage.startAppOnLogin.description": "Pokud je tato možnost povolena, aplikace se spustí automaticky po přihlášení k počítači.",
|
"renderer.components.settingsPage.startAppOnLogin.description": "Pokud je povoleno, aplikace se automaticky spustí, když se přihlásíte ke svému počítači.",
|
||||||
"renderer.components.settingsPage.trayIcon.color": "Barva ikony: ",
|
"renderer.components.settingsPage.trayIcon.color": "Barva ikony: ",
|
||||||
"renderer.components.settingsPage.trayIcon.show": "Zobrazení ikony v oznamovací oblasti",
|
"renderer.components.settingsPage.trayIcon.show": "Zobrazení ikony v oznamovací oblasti",
|
||||||
"renderer.components.settingsPage.trayIcon.show.darwin": "Zobrazení ikony {appName} na panelu nabídek",
|
"renderer.components.settingsPage.trayIcon.show.darwin": "Zobrazení ikony {appName} na panelu nabídek",
|
||||||
|
@ -256,16 +256,16 @@
|
||||||
"renderer.components.settingsPage.trayIcon.theme.dark": "Tmavý",
|
"renderer.components.settingsPage.trayIcon.theme.dark": "Tmavý",
|
||||||
"renderer.components.settingsPage.trayIcon.theme.light": "Světlý",
|
"renderer.components.settingsPage.trayIcon.theme.light": "Světlý",
|
||||||
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "Použít výchozí nastavení systému",
|
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "Použít výchozí nastavení systému",
|
||||||
"renderer.components.settingsPage.updates": "Aktualizovat",
|
"renderer.components.settingsPage.updates": "Aktualizace",
|
||||||
"renderer.components.settingsPage.updates.automatic": "Automatická kontrola aktualizací",
|
"renderer.components.settingsPage.updates.automatic": "Automaticky kontrolovat aktualizace",
|
||||||
"renderer.components.settingsPage.updates.automatic.description": "Pokud je tato možnost povolena, aktualizace aplikace pro počítače se stahují automaticky a po dokončení instalace budete upozorněni.",
|
"renderer.components.settingsPage.updates.automatic.description": "Pokud je povoleno, aktualizace Desktopové aplikace se stáhnou automaticky a budete upozorněni, když budou připraveny k instalaci.",
|
||||||
"renderer.components.settingsPage.updates.checkNow": "Zkontrolovat aktualizace",
|
"renderer.components.settingsPage.updates.checkNow": "Zkontrolovat Aktualizace Nyní",
|
||||||
"renderer.components.showCertificateModal.algorithm": "Algoritmus",
|
"renderer.components.showCertificateModal.algorithm": "Algoritmus",
|
||||||
"renderer.components.showCertificateModal.commonName": "Společný název",
|
"renderer.components.showCertificateModal.commonName": "Společný název",
|
||||||
"renderer.components.showCertificateModal.issuerName": "Název emitenta",
|
"renderer.components.showCertificateModal.issuerName": "Název Vydavatele",
|
||||||
"renderer.components.showCertificateModal.noCertSelected": "Žádný vybraný certifikát",
|
"renderer.components.showCertificateModal.noCertSelected": "Žádný vybraný certifikát",
|
||||||
"renderer.components.showCertificateModal.notValidAfter": "Neplatí po",
|
"renderer.components.showCertificateModal.notValidAfter": "Neplatné po",
|
||||||
"renderer.components.showCertificateModal.notValidBefore": "Neplatí před",
|
"renderer.components.showCertificateModal.notValidBefore": "Neplatné před",
|
||||||
"renderer.components.showCertificateModal.publicKeyInfo": "Informace o veřejném klíči",
|
"renderer.components.showCertificateModal.publicKeyInfo": "Informace o veřejném klíči",
|
||||||
"renderer.components.showCertificateModal.serialNumber": "Sériové číslo",
|
"renderer.components.showCertificateModal.serialNumber": "Sériové číslo",
|
||||||
"renderer.components.showCertificateModal.subjectName": "Jméno subjektu",
|
"renderer.components.showCertificateModal.subjectName": "Jméno subjektu",
|
||||||
|
@ -277,21 +277,21 @@
|
||||||
"renderer.components.welcomeScreen.slides.calls.title": "Začněte zabezpečené hovory okamžitě",
|
"renderer.components.welcomeScreen.slides.calls.title": "Začněte zabezpečené hovory okamžitě",
|
||||||
"renderer.components.welcomeScreen.slides.channels.subtitle": "Veškerá komunikace vašeho týmu na jednom místě.<br></br>Bezpečná spolupráce, vytvořená pro vývojáře.",
|
"renderer.components.welcomeScreen.slides.channels.subtitle": "Veškerá komunikace vašeho týmu na jednom místě.<br></br>Bezpečná spolupráce, vytvořená pro vývojáře.",
|
||||||
"renderer.components.welcomeScreen.slides.channels.title": "Kanály",
|
"renderer.components.welcomeScreen.slides.channels.title": "Kanály",
|
||||||
"renderer.components.welcomeScreen.slides.collaborate.subtitle": "Spolupracujte efektivně s robustními channely, sdílením souborů, kódu a automatizacemi procesů vytvořenými právě pro technické teamy.",
|
"renderer.components.welcomeScreen.slides.collaborate.subtitle": "Efektivně spolupracujte s pomocí trvalých kanálů, sdílení souborů a kódových úryvků a automatizace pracovních postupů navržené speciálně pro technické týmy.",
|
||||||
"renderer.components.welcomeScreen.slides.collaborate.title": "Spolupracujte v reálném čase",
|
"renderer.components.welcomeScreen.slides.collaborate.title": "Spolupracujte živě",
|
||||||
"renderer.components.welcomeScreen.slides.integrate.subtitle": "Provádějte a automatizujte pracovní postupy pomocí flexibilních, vlastních integrací s oblíbenými technickými nástroji, jako jsou GitHub, GitLab a ServiceNow.",
|
"renderer.components.welcomeScreen.slides.integrate.subtitle": "Provádějte a automatizujte pracovní postupy s flexibilními, vlastními integracemi s oblíbenými technickými nástroji, jako jsou GitHub, GitLab a ServiceNow.",
|
||||||
"renderer.components.welcomeScreen.slides.integrate.title": "Integrujte s nástroji, které máte rádi",
|
"renderer.components.welcomeScreen.slides.integrate.title": "Integrujte se s nástroji, které máte rádi",
|
||||||
"renderer.components.welcomeScreen.slides.palybooks.subtitle": "Díky kontrolním seznamům, automatizacím a integracím nástrojů, které podporují pracovní postupy vašeho týmu, můžete postupovat rychleji a dělat méně chyb.",
|
"renderer.components.welcomeScreen.slides.palybooks.subtitle": "Díky kontrolním seznamům, automatizacím a integracím nástrojů, které podporují pracovní postupy vašeho týmu, můžete postupovat rychleji a dělat méně chyb.",
|
||||||
"renderer.components.welcomeScreen.slides.playbooks.title": "Scénáře",
|
"renderer.components.welcomeScreen.slides.playbooks.title": "Scénáře",
|
||||||
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost je platforma pro spolupráci s otevřeným zdrojovým kódem pro kriticky důležitou práci. Bezpečné, flexibilní a integrované s nástroji, které máte rádi.",
|
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost je open source platforma pro spolupráci při kritických úkolech. Bezpečná, flexibilní a integrována s nástroji, které máte rádi.",
|
||||||
"renderer.components.welcomeScreen.slides.welcome.title": "Vítejte na",
|
"renderer.components.welcomeScreen.slides.welcome.title": "Vítejte",
|
||||||
"renderer.downloadsDropdown.ClearAll": "Vymazat vše",
|
"renderer.downloadsDropdown.ClearAll": "Vymazat vše",
|
||||||
"renderer.downloadsDropdown.Downloads": "Stáhnout",
|
"renderer.downloadsDropdown.Downloads": "Stažení",
|
||||||
"renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall": "K dispozici je nová instalace verze aplikace {appName} pro stolní počítače (verze {version}).",
|
"renderer.downloadsDropdown.Update.ANewVersionIsAvailableToInstall": "Nová verze Desktopové aplikace {appName} (verze {version}) je k dispozici k instalaci.",
|
||||||
"renderer.downloadsDropdown.Update.DownloadUpdate": "Aktualizace ke stažení",
|
"renderer.downloadsDropdown.Update.DownloadUpdate": "Stáhnout Aktualizaci",
|
||||||
"renderer.downloadsDropdown.Update.MattermostVersionX": "{appName} verze {version}",
|
"renderer.downloadsDropdown.Update.MattermostVersionX": "{appName} verze {version}",
|
||||||
"renderer.downloadsDropdown.Update.NewDesktopVersionAvailable": "K dispozici je nová verze pro stolní počítače",
|
"renderer.downloadsDropdown.Update.NewDesktopVersionAvailable": "K dispozici je nová verze pro stolní počítače",
|
||||||
"renderer.downloadsDropdown.Update.RestartAndUpdate": "Restart a aktualizace",
|
"renderer.downloadsDropdown.Update.RestartAndUpdate": "Restartovat a aktualizovat",
|
||||||
"renderer.downloadsDropdown.remaining": "zbývající",
|
"renderer.downloadsDropdown.remaining": "zbývající",
|
||||||
"renderer.downloadsDropdownMenu.CancelDownload": "Zrušit stahování",
|
"renderer.downloadsDropdownMenu.CancelDownload": "Zrušit stahování",
|
||||||
"renderer.downloadsDropdownMenu.Clear": "Vyčistit",
|
"renderer.downloadsDropdownMenu.Clear": "Vyčistit",
|
||||||
|
@ -312,9 +312,9 @@
|
||||||
"renderer.modals.login.loginModal.message.proxy": "Proxy server {host}:{port} vyžaduje uživatelské jméno a heslo.",
|
"renderer.modals.login.loginModal.message.proxy": "Proxy server {host}:{port} vyžaduje uživatelské jméno a heslo.",
|
||||||
"renderer.modals.login.loginModal.message.server": "Server {url} vyžaduje uživatelské jméno a heslo.",
|
"renderer.modals.login.loginModal.message.server": "Server {url} vyžaduje uživatelské jméno a heslo.",
|
||||||
"renderer.modals.login.loginModal.password": "Heslo",
|
"renderer.modals.login.loginModal.password": "Heslo",
|
||||||
"renderer.modals.login.loginModal.title": "Požadované ověření",
|
"renderer.modals.login.loginModal.title": "Požadováno Ověření",
|
||||||
"renderer.modals.login.loginModal.username": "Uživatelské jméno",
|
"renderer.modals.login.loginModal.username": "Přihlašovací Jméno",
|
||||||
"renderer.modals.permission.permissionModal.body": "Web, který není zahrnut v konfiguraci serveru Mattermost, vyžaduje přístup pro {permission}.",
|
"renderer.modals.permission.permissionModal.body": "Web, který není zahrnut v konfiguraci serveru Mattermost, požaduje přístup pro {permission}.",
|
||||||
"renderer.modals.permission.permissionModal.requestOriginatedFromOrigin": "Tento požadavek pochází z adresy <link>{origin}.</link>",
|
"renderer.modals.permission.permissionModal.requestOriginatedFromOrigin": "Tento požadavek pochází z adresy <link>{origin}.</link>",
|
||||||
"renderer.modals.permission.permissionModal.title": "{permission} Požadováno",
|
"renderer.modals.permission.permissionModal.title": "{permission} Požadováno",
|
||||||
"renderer.modals.permission.permissionModal.unknownOrigin": "neznámý původ",
|
"renderer.modals.permission.permissionModal.unknownOrigin": "neznámý původ",
|
||||||
|
|
|
@ -81,6 +81,12 @@
|
||||||
"main.menus.app.view": "&View",
|
"main.menus.app.view": "&View",
|
||||||
"main.menus.app.view.actualSize": "Actual Size",
|
"main.menus.app.view.actualSize": "Actual Size",
|
||||||
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
|
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
|
||||||
|
"main.menus.app.view.developerModeBrowserOnly": "Browser Only Mode",
|
||||||
|
"main.menus.app.view.developerModeDisableContextMenu": "Disable Context Menu",
|
||||||
|
"main.menus.app.view.developerModeDisableNotificationStorage": "Disable Notification Storage",
|
||||||
|
"main.menus.app.view.developerModeDisableUserActivityMonitor": "Disable User Activity Monitor",
|
||||||
|
"main.menus.app.view.developerModeForceLegacyAPI": "Force Legacy API",
|
||||||
|
"main.menus.app.view.developerModeForceNewAPI": "Force New API",
|
||||||
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
|
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
|
||||||
"main.menus.app.view.devToolsCurrentCallWidget": "Developer Tools for Call Widget",
|
"main.menus.app.view.devToolsCurrentCallWidget": "Developer Tools for Call Widget",
|
||||||
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
|
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
|
||||||
|
@ -151,6 +157,7 @@
|
||||||
"renderer.components.configureServer.url.urlNotMatched": "The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
"renderer.components.configureServer.url.urlNotMatched": "The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
||||||
"renderer.components.configureServer.url.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
"renderer.components.configureServer.url.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
||||||
"renderer.components.configureServer.url.validating": "Validating...",
|
"renderer.components.configureServer.url.validating": "Validating...",
|
||||||
|
"renderer.components.developerModeIndicator.tooltip": "Developer mode is enabled. You should only have this enabled if a Mattermost developer has instructed you to.",
|
||||||
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
|
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
|
||||||
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
|
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
|
||||||
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
|
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
|
||||||
|
|
19
i18n/tr.json
19
i18n/tr.json
|
@ -122,10 +122,12 @@
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.media": "{appName} aramalar ve sesli iletiler için mikrofon ve kamerayı kullanacak. Bunu daha sonra istediğiniz zaman bilgisayar ayarlarından değiştirebilirsiniz.",
|
"main.permissionsManager.checkPermission.dialog.detail.media": "{appName} aramalar ve sesli iletiler için mikrofon ve kamerayı kullanacak. Bunu daha sonra istediğiniz zaman bilgisayar ayarlarından değiştirebilirsiniz.",
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.notifications": "{appName} size iletiler ve aramalar için bildirimler gönderecek. Bildirim tercihlerinizi Ayarlar bölümünden yapılandırabilirsiniz.",
|
"main.permissionsManager.checkPermission.dialog.detail.notifications": "{appName} size iletiler ve aramalar için bildirimler gönderecek. Bildirim tercihlerinizi Ayarlar bölümünden yapılandırabilirsiniz.",
|
||||||
"main.permissionsManager.checkPermission.dialog.detail.openExternal": "{appName} istenen bağlantıyı bir dış uygulamada açacak. Bu bağlantıya güvenmiyorsanız veya tanımıyorsanız, Reddet üzerine tıklayın. Bu davranışı daha sonra bilgisayarınızın ayarlarından değiştirebilirsiniz.",
|
"main.permissionsManager.checkPermission.dialog.detail.openExternal": "{appName} istenen bağlantıyı bir dış uygulamada açacak. Bu bağlantıya güvenmiyorsanız veya tanımıyorsanız, Reddet üzerine tıklayın. Bu davranışı daha sonra bilgisayarınızın ayarlarından değiştirebilirsiniz.",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.detail.screenShare": "{appName} bu izni ekranınızı aramalarda paylaşmak için kullanır. Bunu istediğiniz zaman bilgisayarınızın ayarlarından değiştirebilirsiniz.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) konumunuza erişmek istiyor.",
|
"main.permissionsManager.checkPermission.dialog.message.geolocation": "{appName} ({url}) konumunuza erişmek istiyor.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) mikrofon ve kameraya erişmek istiyor.",
|
"main.permissionsManager.checkPermission.dialog.message.media": "{appName} ({url}) mikrofon ve kameraya erişmek istiyor.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) size bildirimler göndermek istiyor.",
|
"main.permissionsManager.checkPermission.dialog.message.notifications": "{appName} ({url}) size bildirimler göndermek istiyor.",
|
||||||
"main.permissionsManager.checkPermission.dialog.message.openExternal": "{appName} ({url}) şu adresi açmak için izninizi istiyor: {externalURL}",
|
"main.permissionsManager.checkPermission.dialog.message.openExternal": "{appName} ({url}) şu adresi açmak için izninizi istiyor: {externalURL}",
|
||||||
|
"main.permissionsManager.checkPermission.dialog.message.screenShare": "{appName} ({url}) ekranınızı görüntüleme izni istiyor.",
|
||||||
"main.permissionsManager.checkPermission.dialog.title": "İzin İstendi",
|
"main.permissionsManager.checkPermission.dialog.title": "İzin İstendi",
|
||||||
"main.tray.tray.expired": "Oturumun süresi dolmuş: Lütfen bildirimleri almayı sürdürmek için oturum açın.",
|
"main.tray.tray.expired": "Oturumun süresi dolmuş: Lütfen bildirimleri almayı sürdürmek için oturum açın.",
|
||||||
"main.tray.tray.mention": "Anıldınız",
|
"main.tray.tray.mention": "Anıldınız",
|
||||||
|
@ -176,6 +178,15 @@
|
||||||
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Adresin biçimi doğru değil.",
|
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Adresin biçimi doğru değil.",
|
||||||
"renderer.components.newServerModal.error.urlNeedsHttp": "Adres http:// ya da https:// ile başlamalıdır.",
|
"renderer.components.newServerModal.error.urlNeedsHttp": "Adres http:// ya da https:// ile başlamalıdır.",
|
||||||
"renderer.components.newServerModal.error.urlRequired": "Adresin yazılması zorunludur.",
|
"renderer.components.newServerModal.error.urlRequired": "Adresin yazılması zorunludur.",
|
||||||
|
"renderer.components.newServerModal.permissions.geolocation": "Konum",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera": "Mikrofon ve kamera",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsCameraPermissions": "Kamera Windows ayarlarından kapatılmış. Kamera ayarlarını açmak için <link>buraya tıklayın</link>.",
|
||||||
|
"renderer.components.newServerModal.permissions.microphoneAndCamera.windowsMicrophoneaPermissions": "Mikrofon Windows ayarlarından kapatılmış. Mikrofon ayarlarını açmak için <link>buraya tıklayın</link>.",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications": "Bildirimler",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications.mac": "Ayrıca macOS üzerinde Mattermost bildirimlerini açmanız gerekebilir. Sistem Ayarlarını açmak için <link>buraya tıklayın</link>.",
|
||||||
|
"renderer.components.newServerModal.permissions.notifications.windows": "Ayrıca Windows üzerinde Mattermost bildirimlerini açmanız gerekebilir. Bildirim ayarlarını açmak için <link>buraya tıklayın</link>.",
|
||||||
|
"renderer.components.newServerModal.permissions.screenShare": "Ekran paylaşımı",
|
||||||
|
"renderer.components.newServerModal.permissions.title": "İzinler",
|
||||||
"renderer.components.newServerModal.serverDisplayName": "Sunucunun görüntülenecek adı",
|
"renderer.components.newServerModal.serverDisplayName": "Sunucunun görüntülenecek adı",
|
||||||
"renderer.components.newServerModal.serverDisplayName.description": "Sunucunun masaüstü uygulama sekmesi çubuğunda görüntülenecek adı.",
|
"renderer.components.newServerModal.serverDisplayName.description": "Sunucunun masaüstü uygulama sekmesi çubuğunda görüntülenecek adı.",
|
||||||
"renderer.components.newServerModal.serverURL": "Sunucu adresi",
|
"renderer.components.newServerModal.serverURL": "Sunucu adresi",
|
||||||
|
@ -262,11 +273,17 @@
|
||||||
"renderer.components.welcomeScreen.button.getStarted": "Başlayalım",
|
"renderer.components.welcomeScreen.button.getStarted": "Başlayalım",
|
||||||
"renderer.components.welcomeScreen.slides.boards.subtitle": "Dijital operasyonlar için oluşturulmuş bir proje ve görev yönetimi çözümü ile hep zamanında sonuç alın.",
|
"renderer.components.welcomeScreen.slides.boards.subtitle": "Dijital operasyonlar için oluşturulmuş bir proje ve görev yönetimi çözümü ile hep zamanında sonuç alın.",
|
||||||
"renderer.components.welcomeScreen.slides.boards.title": "Panolar",
|
"renderer.components.welcomeScreen.slides.boards.title": "Panolar",
|
||||||
|
"renderer.components.welcomeScreen.slides.calls.subtitle": "Yeterince hızlı yazmadığınızda, araçlar arasında geçiş yapmadan görüşmeden sesli aramaya ve ekran paylaşımına sorunsuz bir şekilde geçin.",
|
||||||
|
"renderer.components.welcomeScreen.slides.calls.title": "Anında güvenli görüşmeler başlatın",
|
||||||
"renderer.components.welcomeScreen.slides.channels.subtitle": "Takımınızın tüm iletişimi tek bir yerden sağlayın.<br></br>Güvenli işbirliği, geliştiriciler için hazırlandı.",
|
"renderer.components.welcomeScreen.slides.channels.subtitle": "Takımınızın tüm iletişimi tek bir yerden sağlayın.<br></br>Güvenli işbirliği, geliştiriciler için hazırlandı.",
|
||||||
"renderer.components.welcomeScreen.slides.channels.title": "Kanallar",
|
"renderer.components.welcomeScreen.slides.channels.title": "Kanallar",
|
||||||
|
"renderer.components.welcomeScreen.slides.collaborate.subtitle": "Teknik ekipler için özel olarak tasarlanmış kalıcı kanallar, dosya ve kod parçası paylaşımı ve iş akışı otomasyonu ile etkili bir şekilde işbirliği yapın.",
|
||||||
|
"renderer.components.welcomeScreen.slides.collaborate.title": "Gerçek zamanlı iş birliği yapın",
|
||||||
|
"renderer.components.welcomeScreen.slides.integrate.subtitle": "GitHub, GitLab ve ServiceNow gibi yaygın kullanılan teknik araçlarla esnek ve özel bütünleştirmeler yaparak iş akışlarını yürütün ve otomatikleştirin.",
|
||||||
|
"renderer.components.welcomeScreen.slides.integrate.title": "Sevdiğiniz araçlarla bütünleştirin",
|
||||||
"renderer.components.welcomeScreen.slides.palybooks.subtitle": "Takımınızın iş akışlarını kolaylaştıran kontrol listeleri, otomasyonlar ve araç bütünleştirmeleri ile daha hızlı çalışın ve daha az hata yapın.",
|
"renderer.components.welcomeScreen.slides.palybooks.subtitle": "Takımınızın iş akışlarını kolaylaştıran kontrol listeleri, otomasyonlar ve araç bütünleştirmeleri ile daha hızlı çalışın ve daha az hata yapın.",
|
||||||
"renderer.components.welcomeScreen.slides.playbooks.title": "Senaryolar",
|
"renderer.components.welcomeScreen.slides.playbooks.title": "Senaryolar",
|
||||||
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost, geliştiricilerin işbirliği yapmasını sağlayan açık kaynaklı bir platformdur. Güvenli, esnek ve sevdiğiniz araçlarla bütünleşik.",
|
"renderer.components.welcomeScreen.slides.welcome.subtitle": "Mattermost, kritik görev işlerinin yapılmasını sağlayan açık kaynaklı bir iş birliği platformudur. Güvenlidir, esnektir ve sevdiğiniz araçlarla birlikte çalışır.",
|
||||||
"renderer.components.welcomeScreen.slides.welcome.title": "Hoş geldiniz",
|
"renderer.components.welcomeScreen.slides.welcome.title": "Hoş geldiniz",
|
||||||
"renderer.downloadsDropdown.ClearAll": "Tümünü temizle",
|
"renderer.downloadsDropdown.ClearAll": "Tümünü temizle",
|
||||||
"renderer.downloadsDropdown.Downloads": "İndirmeler",
|
"renderer.downloadsDropdown.Downloads": "İndirmeler",
|
||||||
|
|
|
@ -193,3 +193,7 @@ export const GET_MEDIA_ACCESS_STATUS = 'get-media-access-status';
|
||||||
export const LEGACY_OFF = 'legacy-off';
|
export const LEGACY_OFF = 'legacy-off';
|
||||||
|
|
||||||
export const GET_NONCE = 'get-nonce';
|
export const GET_NONCE = 'get-nonce';
|
||||||
|
|
||||||
|
export const DEVELOPER_MODE_UPDATED = 'developer-mode-updated';
|
||||||
|
export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
|
||||||
|
export const GET_DEVELOPER_MODE_SETTING = 'get-developer-mode-setting';
|
||||||
|
|
|
@ -21,12 +21,12 @@ import type {BuildConfig} from 'types/config';
|
||||||
* @prop {[]} allowedProtocols - Defines which protocols should be automatically allowed
|
* @prop {[]} allowedProtocols - Defines which protocols should be automatically allowed
|
||||||
*/
|
*/
|
||||||
const buildConfig: BuildConfig = {
|
const buildConfig: BuildConfig = {
|
||||||
defaultServers: [/*
|
defaultServers: [
|
||||||
{
|
{
|
||||||
name: 'example',
|
name: 'buds',
|
||||||
url: 'https://example.com'
|
url: 'https://chat.peanutsmediaserver.com/'
|
||||||
}
|
}
|
||||||
*/],
|
],
|
||||||
helpLink: 'https://docs.mattermost.com/messaging/managing-desktop-app-servers.html',
|
helpLink: 'https://docs.mattermost.com/messaging/managing-desktop-app-servers.html',
|
||||||
enableServerManagement: true,
|
enableServerManagement: true,
|
||||||
enableAutoUpdater: true,
|
enableAutoUpdater: true,
|
||||||
|
|
|
@ -82,6 +82,7 @@ export class UserActivityMonitor extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
stopMonitoring() {
|
stopMonitoring() {
|
||||||
clearInterval(this.systemIdleTimeIntervalID);
|
clearInterval(this.systemIdleTimeIntervalID);
|
||||||
|
this.systemIdleTimeIntervalID = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
TOGGLE_SECURE_INPUT,
|
TOGGLE_SECURE_INPUT,
|
||||||
GET_APP_INFO,
|
GET_APP_INFO,
|
||||||
SHOW_SETTINGS_WINDOW,
|
SHOW_SETTINGS_WINDOW,
|
||||||
|
DEVELOPER_MODE_UPDATED,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -43,6 +44,7 @@ import {setupBadge} from 'main/badge';
|
||||||
import CertificateManager from 'main/certificateManager';
|
import CertificateManager from 'main/certificateManager';
|
||||||
import {configPath, updatePaths} from 'main/constants';
|
import {configPath, updatePaths} from 'main/constants';
|
||||||
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
import i18nManager from 'main/i18nManager';
|
import i18nManager from 'main/i18nManager';
|
||||||
import NonceManager from 'main/nonceManager';
|
import NonceManager from 'main/nonceManager';
|
||||||
|
@ -405,14 +407,16 @@ async function initializeAfterAppReady() {
|
||||||
// Call this to initiate a permissions check for DND state
|
// Call this to initiate a permissions check for DND state
|
||||||
getDoNotDisturb();
|
getDoNotDisturb();
|
||||||
|
|
||||||
// listen for status updates and pass on to renderer
|
DeveloperMode.switchOff('disableUserActivityMonitor', () => {
|
||||||
UserActivityMonitor.on('status', (status) => {
|
// listen for status updates and pass on to renderer
|
||||||
log.debug('UserActivityMonitor.on(status)', status);
|
UserActivityMonitor.on('status', onUserActivityStatus);
|
||||||
ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status.userIsActive, status.idleTime, status.isSystemEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// start monitoring user activity (needs to be started after the app is ready)
|
// start monitoring user activity (needs to be started after the app is ready)
|
||||||
UserActivityMonitor.startMonitoring();
|
UserActivityMonitor.startMonitoring();
|
||||||
|
}, () => {
|
||||||
|
UserActivityMonitor.off('status', onUserActivityStatus);
|
||||||
|
UserActivityMonitor.stopMonitoring();
|
||||||
|
});
|
||||||
|
|
||||||
if (shouldShowTrayIcon()) {
|
if (shouldShowTrayIcon()) {
|
||||||
Tray.init(Config.trayIconTheme);
|
Tray.init(Config.trayIconTheme);
|
||||||
|
@ -430,6 +434,7 @@ async function initializeAfterAppReady() {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateMenuEvent();
|
handleUpdateMenuEvent();
|
||||||
|
DeveloperMode.on(DEVELOPER_MODE_UPDATED, handleUpdateMenuEvent);
|
||||||
|
|
||||||
ipcMain.emit('update-dict');
|
ipcMain.emit('update-dict');
|
||||||
|
|
||||||
|
@ -445,6 +450,15 @@ async function initializeAfterAppReady() {
|
||||||
handleMainWindowIsShown();
|
handleMainWindowIsShown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUserActivityStatus(status: {
|
||||||
|
userIsActive: boolean;
|
||||||
|
idleTime: number;
|
||||||
|
isSystemEvent: boolean;
|
||||||
|
}) {
|
||||||
|
log.debug('UserActivityMonitor.on(status)', status);
|
||||||
|
ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status.userIsActive, status.idleTime, status.isSystemEvent);
|
||||||
|
}
|
||||||
|
|
||||||
function handleStartDownload() {
|
function handleStartDownload() {
|
||||||
if (updateManager) {
|
if (updateManager) {
|
||||||
updateManager.handleDownload();
|
updateManager.handleDownload();
|
||||||
|
|
|
@ -20,6 +20,7 @@ export let boundsInfoPath = '';
|
||||||
export let migrationInfoPath = '';
|
export let migrationInfoPath = '';
|
||||||
export let downloadsJson = '';
|
export let downloadsJson = '';
|
||||||
export let permissionsJson = '';
|
export let permissionsJson = '';
|
||||||
|
export let developerModeJson = '';
|
||||||
|
|
||||||
export function updatePaths(emit = false) {
|
export function updatePaths(emit = false) {
|
||||||
userDataPath = app.getPath('userData');
|
userDataPath = app.getPath('userData');
|
||||||
|
@ -33,6 +34,7 @@ export function updatePaths(emit = false) {
|
||||||
migrationInfoPath = path.resolve(userDataPath, 'migration-info.json');
|
migrationInfoPath = path.resolve(userDataPath, 'migration-info.json');
|
||||||
downloadsJson = path.resolve(userDataPath, 'downloads.json');
|
downloadsJson = path.resolve(userDataPath, 'downloads.json');
|
||||||
permissionsJson = path.resolve(userDataPath, 'permissions.json');
|
permissionsJson = path.resolve(userDataPath, 'permissions.json');
|
||||||
|
developerModeJson = path.resolve(userDataPath, 'developerMode.json');
|
||||||
|
|
||||||
if (emit) {
|
if (emit) {
|
||||||
ipcMain.emit(UPDATE_PATHS);
|
ipcMain.emit(UPDATE_PATHS);
|
||||||
|
|
34
src/main/developerMode.test.js
Normal file
34
src/main/developerMode.test.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DeveloperMode} from './developerMode';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
readFileSync: jest.fn(),
|
||||||
|
writeFile: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
handle: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/developerMode', () => {
|
||||||
|
it('should toggle values correctly', () => {
|
||||||
|
const developerMode = new DeveloperMode('file.json');
|
||||||
|
|
||||||
|
// Should be false unless developer mode is enabled
|
||||||
|
developerMode.toggle('setting1');
|
||||||
|
expect(developerMode.get('setting1')).toBe(false);
|
||||||
|
|
||||||
|
developerMode.enabled = () => true;
|
||||||
|
|
||||||
|
developerMode.toggle('setting1');
|
||||||
|
expect(developerMode.get('setting1')).toBe(true);
|
||||||
|
|
||||||
|
developerMode.toggle('setting1');
|
||||||
|
expect(developerMode.get('setting1')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
68
src/main/developerMode.ts
Normal file
68
src/main/developerMode.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {ipcMain} from 'electron';
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
import {DEVELOPER_MODE_UPDATED, IS_DEVELOPER_MODE_ENABLED, UPDATE_PATHS, GET_DEVELOPER_MODE_SETTING} from 'common/communication';
|
||||||
|
import JsonFileManager from 'common/JsonFileManager';
|
||||||
|
import {developerModeJson} from 'main/constants';
|
||||||
|
|
||||||
|
import type {DeveloperSettings} from 'types/settings';
|
||||||
|
|
||||||
|
export class DeveloperMode extends EventEmitter {
|
||||||
|
private json: JsonFileManager<DeveloperSettings>;
|
||||||
|
|
||||||
|
constructor(file: string) {
|
||||||
|
super();
|
||||||
|
this.json = new JsonFileManager(file);
|
||||||
|
|
||||||
|
ipcMain.handle(IS_DEVELOPER_MODE_ENABLED, this.enabled);
|
||||||
|
ipcMain.handle(GET_DEVELOPER_MODE_SETTING, (_, setting) => this.get(setting));
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = () => process.env.MM_DESKTOP_DEVELOPER_MODE === 'true';
|
||||||
|
|
||||||
|
toggle = (setting: keyof DeveloperSettings) => {
|
||||||
|
if (!this.enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.json.setValue(setting, !this.json.getValue(setting));
|
||||||
|
this.emit(DEVELOPER_MODE_UPDATED, {[setting]: this.json.getValue(setting)});
|
||||||
|
};
|
||||||
|
|
||||||
|
get = (setting: keyof DeveloperSettings) => {
|
||||||
|
if (!this.enabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.json.getValue(setting);
|
||||||
|
};
|
||||||
|
|
||||||
|
switchOff = (
|
||||||
|
setting: keyof DeveloperSettings,
|
||||||
|
onStart: () => void,
|
||||||
|
onStop: () => void,
|
||||||
|
) => {
|
||||||
|
if (!this.get(setting)) {
|
||||||
|
onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on(DEVELOPER_MODE_UPDATED, (settings: DeveloperSettings) => {
|
||||||
|
if (typeof settings[setting] !== 'undefined') {
|
||||||
|
if (settings[setting]) {
|
||||||
|
onStop();
|
||||||
|
} else {
|
||||||
|
onStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let developerMode = new DeveloperMode(developerModeJson);
|
||||||
|
ipcMain.on(UPDATE_PATHS, () => {
|
||||||
|
developerMode = new DeveloperMode(developerModeJson);
|
||||||
|
});
|
||||||
|
export default developerMode;
|
|
@ -635,7 +635,15 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
|
||||||
|
|
||||||
let thumbnailData;
|
let thumbnailData;
|
||||||
if (state === 'completed' && item.getMimeType().toLowerCase().startsWith('image/')) {
|
if (state === 'completed' && item.getMimeType().toLowerCase().startsWith('image/')) {
|
||||||
thumbnailData = (await nativeImage.createThumbnailFromPath(overridePath ?? item.getSavePath(), {height: 32, width: 32})).toDataURL();
|
// Linux doesn't support the thumbnail creation so we have to use the base function
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
// We also will cap this at 1MB so as to not inflate the memory usage of the downloads dropdown
|
||||||
|
if (item.getReceivedBytes() < 1000000) {
|
||||||
|
thumbnailData = (await nativeImage.createFromPath(overridePath ?? item.getSavePath())).toDataURL();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
thumbnailData = (await nativeImage.createThumbnailFromPath(overridePath ?? item.getSavePath(), {height: 32, width: 32})).toDataURL();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {MenuItemConstructorOptions, MenuItem, WebContents} from 'electron';
|
import type {MenuItemConstructorOptions, MenuItem, BrowserWindow} from 'electron';
|
||||||
import {app, ipcMain, Menu, session, shell, clipboard} from 'electron';
|
import {app, ipcMain, Menu, session, shell, clipboard} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import {t} from 'common/utils/util';
|
||||||
import {getViewDisplayName} from 'common/views/View';
|
import {getViewDisplayName} from 'common/views/View';
|
||||||
import type {ViewType} from 'common/views/View';
|
import type {ViewType} from 'common/views/View';
|
||||||
import type {UpdateManager} from 'main/autoUpdater';
|
import type {UpdateManager} from 'main/autoUpdater';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import Diagnostics from 'main/diagnostics';
|
import Diagnostics from 'main/diagnostics';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
@ -139,7 +140,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
const devToolsSubMenu = [
|
const devToolsSubMenu: Electron.MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
|
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
|
||||||
accelerator: (() => {
|
accelerator: (() => {
|
||||||
|
@ -148,13 +149,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
}
|
}
|
||||||
return 'Ctrl+Shift+I';
|
return 'Ctrl+Shift+I';
|
||||||
})(),
|
})(),
|
||||||
click(item: Electron.MenuItem, focusedWindow?: WebContents) {
|
click(item: Electron.MenuItem, focusedWindow?: BrowserWindow) {
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
|
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
|
||||||
if (focusedWindow.isDevToolsOpened()) {
|
if (focusedWindow.webContents.isDevToolsOpened()) {
|
||||||
focusedWindow.closeDevTools();
|
focusedWindow.webContents.closeDevTools();
|
||||||
} else {
|
} else {
|
||||||
focusedWindow.openDevTools({mode: 'detach'});
|
focusedWindow.webContents.openDevTools({mode: 'detach'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -176,6 +177,60 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DeveloperMode.enabled()) {
|
||||||
|
devToolsSubMenu.push(...[
|
||||||
|
separatorItem,
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeBrowserOnly', 'Browser Only Mode'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('browserOnly'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('browserOnly');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeDisableNotificationStorage', 'Disable Notification Storage'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('disableNotificationStorage'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('disableNotificationStorage');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeDisableUserActivityMonitor', 'Disable User Activity Monitor'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('disableUserActivityMonitor'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('disableUserActivityMonitor');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeDisableContextMenu', 'Disable Context Menu'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('disableContextMenu'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('disableContextMenu');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeForceLegacyAPI', 'Force Legacy API'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('forceLegacyAPI'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('forceLegacyAPI');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeForceNewAPI', 'Force New API'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('forceNewAPI'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('forceNewAPI');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
const viewSubMenu = [{
|
const viewSubMenu = [{
|
||||||
label: localizeMessage('main.menus.app.view.find', 'Find..'),
|
label: localizeMessage('main.menus.app.view.find', 'Find..'),
|
||||||
accelerator: 'CmdOrCtrl+F',
|
accelerator: 'CmdOrCtrl+F',
|
||||||
|
|
|
@ -106,7 +106,9 @@ jest.mock('../windows/mainWindow', () => ({
|
||||||
show: jest.fn(),
|
show: jest.fn(),
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/developerMode', () => ({
|
||||||
|
switchOff: (_: string, onStart: () => void) => onStart(),
|
||||||
|
}));
|
||||||
jest.mock('main/i18nManager', () => ({
|
jest.mock('main/i18nManager', () => ({
|
||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state
|
||||||
import {PLAY_SOUND, NOTIFICATION_CLICKED, BROWSER_HISTORY_PUSH, OPEN_NOTIFICATION_PREFERENCES} from 'common/communication';
|
import {PLAY_SOUND, NOTIFICATION_CLICKED, BROWSER_HISTORY_PUSH, OPEN_NOTIFICATION_PREFERENCES} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
|
|
||||||
import getLinuxDoNotDisturb from './dnd-linux';
|
import getLinuxDoNotDisturb from './dnd-linux';
|
||||||
import getWindowsDoNotDisturb from './dnd-windows';
|
import getWindowsDoNotDisturb from './dnd-windows';
|
||||||
|
@ -22,13 +23,23 @@ import MainWindow from '../windows/mainWindow';
|
||||||
const log = new Logger('Notifications');
|
const log = new Logger('Notifications');
|
||||||
|
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
private mentionsPerChannel: Map<string, Mention> = new Map();
|
private mentionsPerChannel?: Map<string, Mention>;
|
||||||
private allActiveNotifications: Map<string, Notification> = new Map();
|
private allActiveNotifications?: Map<string, Notification>;
|
||||||
private upgradeNotification?: NewVersionNotification;
|
private upgradeNotification?: NewVersionNotification;
|
||||||
private restartToUpgradeNotification?: UpgradeNotification;
|
private restartToUpgradeNotification?: UpgradeNotification;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
ipcMain.on(OPEN_NOTIFICATION_PREFERENCES, this.openNotificationPreferences);
|
ipcMain.on(OPEN_NOTIFICATION_PREFERENCES, this.openNotificationPreferences);
|
||||||
|
|
||||||
|
DeveloperMode.switchOff('disableNotificationStorage', () => {
|
||||||
|
this.mentionsPerChannel = new Map();
|
||||||
|
this.allActiveNotifications = new Map();
|
||||||
|
}, () => {
|
||||||
|
this.mentionsPerChannel?.clear();
|
||||||
|
delete this.mentionsPerChannel;
|
||||||
|
this.allActiveNotifications?.clear();
|
||||||
|
delete this.allActiveNotifications;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async displayMention(title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, soundName: string) {
|
public async displayMention(title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, soundName: string) {
|
||||||
|
@ -68,12 +79,12 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mention = new Mention(options, channelId, teamId);
|
const mention = new Mention(options, channelId, teamId);
|
||||||
this.allActiveNotifications.set(mention.uId, mention);
|
this.allActiveNotifications?.set(mention.uId, mention);
|
||||||
|
|
||||||
mention.on('click', () => {
|
mention.on('click', () => {
|
||||||
log.debug('notification click', serverName, mention.uId);
|
log.debug('notification click', serverName, mention.uId);
|
||||||
|
|
||||||
this.allActiveNotifications.delete(mention.uId);
|
this.allActiveNotifications?.delete(mention.uId);
|
||||||
|
|
||||||
// Show the window after navigation has finished to avoid the focus handler
|
// Show the window after navigation has finished to avoid the focus handler
|
||||||
// being called before the current channel has updated
|
// being called before the current channel has updated
|
||||||
|
@ -87,7 +98,7 @@ class NotificationManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
mention.on('close', () => {
|
mention.on('close', () => {
|
||||||
this.allActiveNotifications.delete(mention.uId);
|
this.allActiveNotifications?.delete(mention.uId);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -107,12 +118,12 @@ class NotificationManager {
|
||||||
// On Windows, manually dismiss notifications from the same channel and only show the latest one
|
// On Windows, manually dismiss notifications from the same channel and only show the latest one
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const mentionKey = `${mention.teamId}:${mention.channelId}`;
|
const mentionKey = `${mention.teamId}:${mention.channelId}`;
|
||||||
if (this.mentionsPerChannel.has(mentionKey)) {
|
if (this.mentionsPerChannel?.has(mentionKey)) {
|
||||||
log.debug(`close ${mentionKey}`);
|
log.debug(`close ${mentionKey}`);
|
||||||
this.mentionsPerChannel.get(mentionKey)?.close();
|
this.mentionsPerChannel?.get(mentionKey)?.close();
|
||||||
this.mentionsPerChannel.delete(mentionKey);
|
this.mentionsPerChannel?.delete(mentionKey);
|
||||||
}
|
}
|
||||||
this.mentionsPerChannel.set(mentionKey, mention);
|
this.mentionsPerChannel?.set(mentionKey, mention);
|
||||||
}
|
}
|
||||||
const notificationSound = mention.getNotificationSound();
|
const notificationSound = mention.getNotificationSound();
|
||||||
if (notificationSound) {
|
if (notificationSound) {
|
||||||
|
@ -127,7 +138,7 @@ class NotificationManager {
|
||||||
|
|
||||||
mention.on('failed', (_, error) => {
|
mention.on('failed', (_, error) => {
|
||||||
failed = true;
|
failed = true;
|
||||||
this.allActiveNotifications.delete(mention.uId);
|
this.allActiveNotifications?.delete(mention.uId);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
// Special case for Windows - means that notifications are disabled at the OS level
|
// Special case for Windows - means that notifications are disabled at the OS level
|
||||||
|
@ -156,7 +167,7 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = new DownloadNotification(fileName, serverName);
|
const download = new DownloadNotification(fileName, serverName);
|
||||||
this.allActiveNotifications.set(download.uId, download);
|
this.allActiveNotifications?.set(download.uId, download);
|
||||||
|
|
||||||
download.on('show', () => {
|
download.on('show', () => {
|
||||||
flashFrame(true);
|
flashFrame(true);
|
||||||
|
@ -164,15 +175,15 @@ class NotificationManager {
|
||||||
|
|
||||||
download.on('click', () => {
|
download.on('click', () => {
|
||||||
shell.showItemInFolder(path.normalize());
|
shell.showItemInFolder(path.normalize());
|
||||||
this.allActiveNotifications.delete(download.uId);
|
this.allActiveNotifications?.delete(download.uId);
|
||||||
});
|
});
|
||||||
|
|
||||||
download.on('close', () => {
|
download.on('close', () => {
|
||||||
this.allActiveNotifications.delete(download.uId);
|
this.allActiveNotifications?.delete(download.uId);
|
||||||
});
|
});
|
||||||
|
|
||||||
download.on('failed', () => {
|
download.on('failed', () => {
|
||||||
this.allActiveNotifications.delete(download.uId);
|
this.allActiveNotifications?.delete(download.uId);
|
||||||
});
|
});
|
||||||
download.show();
|
download.show();
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,89 +43,99 @@ import {
|
||||||
UNREADS_AND_MENTIONS,
|
UNREADS_AND_MENTIONS,
|
||||||
LEGACY_OFF,
|
LEGACY_OFF,
|
||||||
TAB_LOGIN_CHANGED,
|
TAB_LOGIN_CHANGED,
|
||||||
|
GET_DEVELOPER_MODE_SETTING,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
import type {ExternalAPI} from 'types/externalAPI';
|
import type {ExternalAPI} from 'types/externalAPI';
|
||||||
|
|
||||||
const createListener: ExternalAPI['createListener'] = (channel: string, listener: (...args: never[]) => void) => {
|
let legacyEnabled = false;
|
||||||
const listenerWithEvent = (_: IpcRendererEvent, ...args: unknown[]) =>
|
let legacyOff: () => void;
|
||||||
listener(...args as never[]);
|
|
||||||
ipcRenderer.on(channel, listenerWithEvent);
|
ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceLegacyAPI').then((force) => {
|
||||||
return () => {
|
if (force) {
|
||||||
ipcRenderer.off(channel, listenerWithEvent);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createListener: ExternalAPI['createListener'] = (channel: string, listener: (...args: never[]) => void) => {
|
||||||
|
const listenerWithEvent = (_: IpcRendererEvent, ...args: unknown[]) =>
|
||||||
|
listener(...args as never[]);
|
||||||
|
ipcRenderer.on(channel, listenerWithEvent);
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.off(channel, listenerWithEvent);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const desktopAPI: DesktopAPI = {
|
const desktopAPI: DesktopAPI = {
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
isDev: () => ipcRenderer.invoke(GET_IS_DEV_MODE),
|
isDev: () => ipcRenderer.invoke(GET_IS_DEV_MODE),
|
||||||
getAppInfo: () => {
|
getAppInfo: () => {
|
||||||
// Using this signal as the sign to disable the legacy code, since it is run before the app is rendered
|
// Using this signal as the sign to disable the legacy code, since it is run before the app is rendered
|
||||||
if (legacyEnabled) {
|
if (legacyEnabled) {
|
||||||
legacyOff();
|
legacyOff?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipcRenderer.invoke(GET_APP_INFO);
|
return ipcRenderer.invoke(GET_APP_INFO);
|
||||||
},
|
},
|
||||||
reactAppInitialized: () => ipcRenderer.send(REACT_APP_INITIALIZED),
|
reactAppInitialized: () => ipcRenderer.send(REACT_APP_INITIALIZED),
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
setSessionExpired: (isExpired) => ipcRenderer.send(SESSION_EXPIRED, isExpired),
|
setSessionExpired: (isExpired) => ipcRenderer.send(SESSION_EXPIRED, isExpired),
|
||||||
onUserActivityUpdate: (listener) => createListener(USER_ACTIVITY_UPDATE, listener),
|
onUserActivityUpdate: (listener) => createListener(USER_ACTIVITY_UPDATE, listener),
|
||||||
|
|
||||||
onLogin: () => ipcRenderer.send(TAB_LOGIN_CHANGED, true),
|
onLogin: () => ipcRenderer.send(TAB_LOGIN_CHANGED, true),
|
||||||
onLogout: () => ipcRenderer.send(TAB_LOGIN_CHANGED, false),
|
onLogout: () => ipcRenderer.send(TAB_LOGIN_CHANGED, false),
|
||||||
|
|
||||||
// Unreads/mentions/notifications
|
// Unreads/mentions/notifications
|
||||||
sendNotification: (title, body, channelId, teamId, url, silent, soundName) =>
|
sendNotification: (title, body, channelId, teamId, url, silent, soundName) =>
|
||||||
ipcRenderer.invoke(NOTIFY_MENTION, title, body, channelId, teamId, url, silent, soundName),
|
ipcRenderer.invoke(NOTIFY_MENTION, title, body, channelId, teamId, url, silent, soundName),
|
||||||
onNotificationClicked: (listener) => createListener(NOTIFICATION_CLICKED, listener),
|
onNotificationClicked: (listener) => createListener(NOTIFICATION_CLICKED, listener),
|
||||||
setUnreadsAndMentions: (isUnread, mentionCount) => ipcRenderer.send(UNREADS_AND_MENTIONS, isUnread, mentionCount),
|
setUnreadsAndMentions: (isUnread, mentionCount) => ipcRenderer.send(UNREADS_AND_MENTIONS, isUnread, mentionCount),
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
requestBrowserHistoryStatus: () => ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS),
|
requestBrowserHistoryStatus: () => ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS),
|
||||||
onBrowserHistoryStatusUpdated: (listener) => createListener(BROWSER_HISTORY_STATUS_UPDATED, listener),
|
onBrowserHistoryStatusUpdated: (listener) => createListener(BROWSER_HISTORY_STATUS_UPDATED, listener),
|
||||||
onBrowserHistoryPush: (listener) => createListener(BROWSER_HISTORY_PUSH, listener),
|
onBrowserHistoryPush: (listener) => createListener(BROWSER_HISTORY_PUSH, listener),
|
||||||
sendBrowserHistoryPush: (path) => ipcRenderer.send(BROWSER_HISTORY_PUSH, path),
|
sendBrowserHistoryPush: (path) => ipcRenderer.send(BROWSER_HISTORY_PUSH, path),
|
||||||
|
|
||||||
// Calls
|
// Calls
|
||||||
joinCall: (opts) => ipcRenderer.invoke(CALLS_JOIN_CALL, opts),
|
joinCall: (opts) => ipcRenderer.invoke(CALLS_JOIN_CALL, opts),
|
||||||
leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL),
|
leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL),
|
||||||
|
|
||||||
callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID),
|
callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID),
|
||||||
resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height),
|
resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height),
|
||||||
|
|
||||||
sendCallsError: (err, callID, errMsg) => ipcRenderer.send(CALLS_ERROR, err, callID, errMsg),
|
sendCallsError: (err, callID, errMsg) => ipcRenderer.send(CALLS_ERROR, err, callID, errMsg),
|
||||||
onCallsError: (listener) => createListener(CALLS_ERROR, listener),
|
onCallsError: (listener) => createListener(CALLS_ERROR, listener),
|
||||||
|
|
||||||
getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts),
|
getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts),
|
||||||
openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST),
|
openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST),
|
||||||
onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener),
|
onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener),
|
||||||
|
|
||||||
shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio),
|
shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio),
|
||||||
onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener),
|
onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener),
|
||||||
|
|
||||||
sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId),
|
sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId),
|
||||||
onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener),
|
onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener),
|
||||||
|
|
||||||
openLinkFromCalls: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url),
|
openLinkFromCalls: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url),
|
||||||
|
|
||||||
focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS),
|
focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS),
|
||||||
|
|
||||||
openThreadForCalls: (threadID) => ipcRenderer.send(CALLS_WIDGET_OPEN_THREAD, threadID),
|
openThreadForCalls: (threadID) => ipcRenderer.send(CALLS_WIDGET_OPEN_THREAD, threadID),
|
||||||
onOpenThreadForCalls: (listener) => createListener(CALLS_WIDGET_OPEN_THREAD, listener),
|
onOpenThreadForCalls: (listener) => createListener(CALLS_WIDGET_OPEN_THREAD, listener),
|
||||||
|
|
||||||
openStopRecordingModal: (channelID) => ipcRenderer.send(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, channelID),
|
openStopRecordingModal: (channelID) => ipcRenderer.send(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, channelID),
|
||||||
onOpenStopRecordingModal: (listener) => createListener(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, listener),
|
onOpenStopRecordingModal: (listener) => createListener(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, listener),
|
||||||
|
|
||||||
openCallsUserSettings: () => ipcRenderer.send(CALLS_WIDGET_OPEN_USER_SETTINGS),
|
openCallsUserSettings: () => ipcRenderer.send(CALLS_WIDGET_OPEN_USER_SETTINGS),
|
||||||
onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener),
|
onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener),
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
};
|
};
|
||||||
contextBridge.exposeInMainWorld('desktopAPI', desktopAPI);
|
contextBridge.exposeInMainWorld('desktopAPI', desktopAPI);
|
||||||
|
});
|
||||||
|
|
||||||
// Specific info for the testing environment
|
// Specific info for the testing environment
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
@ -181,312 +191,318 @@ setInterval(() => {
|
||||||
webFrame.clearCache();
|
webFrame.clearCache();
|
||||||
}, CLEAR_CACHE_INTERVAL);
|
}, CLEAR_CACHE_INTERVAL);
|
||||||
|
|
||||||
/****************************************************************************
|
ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceNewAPI').then((force) => {
|
||||||
* LEGACY CODE BELOW
|
if (force) {
|
||||||
* All of this code is deprecated and should be removed eventually
|
|
||||||
* Current it is there to support older versions of the web app
|
|
||||||
****************************************************************************
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy helper functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
const onLoad = () => {
|
|
||||||
if (document.getElementById('root') === null) {
|
|
||||||
console.warn('The guest is not assumed as mattermost-webapp');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
watchReactAppUntilInitialized(() => {
|
|
||||||
console.warn('Legacy preload initialized');
|
|
||||||
ipcRenderer.send(REACT_APP_INITIALIZED);
|
|
||||||
ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStorageChanged = (e: StorageEvent) => {
|
/****************************************************************************
|
||||||
if (e.key === '__login__' && e.storageArea === localStorage && e.newValue) {
|
* LEGACY CODE BELOW
|
||||||
ipcRenderer.send(APP_LOGGED_IN);
|
* All of this code is deprecated and should be removed eventually
|
||||||
}
|
* Current it is there to support older versions of the web app
|
||||||
if (e.key === '__logout__' && e.storageArea === localStorage && e.newValue) {
|
****************************************************************************
|
||||||
ipcRenderer.send(APP_LOGGED_OUT);
|
*/
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isReactAppInitialized = () => {
|
/**
|
||||||
const initializedRoot =
|
* Legacy helper functions
|
||||||
document.querySelector('#root.channel-view') || // React 16 webapp
|
*/
|
||||||
document.querySelector('#root .signup-team__container') || // React 16 login
|
|
||||||
document.querySelector('div[data-reactroot]'); // Older React apps
|
|
||||||
if (initializedRoot === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return initializedRoot.children.length !== 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const watchReactAppUntilInitialized = (callback: () => void) => {
|
const onLoad = () => {
|
||||||
let count = 0;
|
if (document.getElementById('root') === null) {
|
||||||
const interval = 500;
|
console.warn('The guest is not assumed as mattermost-webapp');
|
||||||
const timeout = 30000;
|
return;
|
||||||
const timer = setInterval(() => {
|
|
||||||
count += interval;
|
|
||||||
if (isReactAppInitialized() || count >= timeout) { // assumed as webapp has been initialized.
|
|
||||||
clearTimeout(timer);
|
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
}, interval);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkUnread = () => {
|
|
||||||
if (isReactAppInitialized()) {
|
|
||||||
findUnread();
|
|
||||||
} else {
|
|
||||||
watchReactAppUntilInitialized(() => {
|
watchReactAppUntilInitialized(() => {
|
||||||
|
console.warn('Legacy preload initialized');
|
||||||
|
ipcRenderer.send(REACT_APP_INITIALIZED);
|
||||||
|
ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStorageChanged = (e: StorageEvent) => {
|
||||||
|
if (e.key === '__login__' && e.storageArea === localStorage && e.newValue) {
|
||||||
|
ipcRenderer.send(APP_LOGGED_IN);
|
||||||
|
}
|
||||||
|
if (e.key === '__logout__' && e.storageArea === localStorage && e.newValue) {
|
||||||
|
ipcRenderer.send(APP_LOGGED_OUT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isReactAppInitialized = () => {
|
||||||
|
const initializedRoot =
|
||||||
|
document.querySelector('#root.channel-view') || // React 16 webapp
|
||||||
|
document.querySelector('#root .signup-team__container') || // React 16 login
|
||||||
|
document.querySelector('div[data-reactroot]'); // Older React apps
|
||||||
|
if (initializedRoot === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return initializedRoot.children.length !== 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const watchReactAppUntilInitialized = (callback: () => void) => {
|
||||||
|
let count = 0;
|
||||||
|
const interval = 500;
|
||||||
|
const timeout = 30000;
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
count += interval;
|
||||||
|
if (isReactAppInitialized() || count >= timeout) { // assumed as webapp has been initialized.
|
||||||
|
clearTimeout(timer);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkUnread = () => {
|
||||||
|
if (isReactAppInitialized()) {
|
||||||
findUnread();
|
findUnread();
|
||||||
});
|
} else {
|
||||||
}
|
watchReactAppUntilInitialized(() => {
|
||||||
};
|
findUnread();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findUnread = () => {
|
const findUnread = () => {
|
||||||
const classes = ['team-container unread', 'SidebarChannel unread', 'sidebar-item unread-title'];
|
const classes = ['team-container unread', 'SidebarChannel unread', 'sidebar-item unread-title'];
|
||||||
const isUnread = classes.some((classPair) => {
|
const isUnread = classes.some((classPair) => {
|
||||||
const result = document.getElementsByClassName(classPair);
|
const result = document.getElementsByClassName(classPair);
|
||||||
return result && result.length > 0;
|
return result && result.length > 0;
|
||||||
|
});
|
||||||
|
ipcRenderer.send(UNREAD_RESULT, isUnread);
|
||||||
|
};
|
||||||
|
|
||||||
|
let sessionExpired: boolean;
|
||||||
|
const getUnreadCount = () => {
|
||||||
|
// LHS not found => Log out => Count should be 0, but session may be expired.
|
||||||
|
let isExpired;
|
||||||
|
if (document.getElementById('sidebar-left') === null) {
|
||||||
|
const extraParam = (new URLSearchParams(window.location.search)).get('extra');
|
||||||
|
isExpired = extraParam === 'expired';
|
||||||
|
} else {
|
||||||
|
isExpired = false;
|
||||||
|
}
|
||||||
|
if (isExpired !== sessionExpired) {
|
||||||
|
sessionExpired = isExpired;
|
||||||
|
ipcRenderer.send(SESSION_EXPIRED, sessionExpired);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy message passing code - can be running alongside the new API stuff
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Disabling no-explicit-any for this legacy code
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
window.addEventListener('message', ({origin, data = {}}: {origin?: string; data?: {type?: string; message?: any}} = {}) => {
|
||||||
|
const {type, message = {}} = data;
|
||||||
|
if (origin !== window.location.origin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'webapp-ready':
|
||||||
|
case 'get-app-version': {
|
||||||
|
// register with the webapp to enable custom integration functionality
|
||||||
|
ipcRenderer.invoke(GET_APP_INFO).then((info) => {
|
||||||
|
console.log(`registering ${info.name} v${info.version} with the server`);
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: 'register-desktop',
|
||||||
|
message: info,
|
||||||
|
},
|
||||||
|
window.location.origin || '*',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'dispatch-notification': {
|
||||||
|
const {title, body, channel, teamId, url, silent, data: messageData} = message;
|
||||||
|
channels.set(channel.id, channel);
|
||||||
|
ipcRenderer.invoke(NOTIFY_MENTION, title, body, channel.id, teamId, url, silent, messageData.soundName);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BROWSER_HISTORY_PUSH: {
|
||||||
|
const {path} = message as {path: string};
|
||||||
|
ipcRenderer.send(BROWSER_HISTORY_PUSH, path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'history-button': {
|
||||||
|
ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_LINK_CLICK: {
|
||||||
|
ipcRenderer.send(CALLS_LINK_CLICK, message.link);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GET_DESKTOP_SOURCES: {
|
||||||
|
ipcRenderer.invoke(GET_DESKTOP_SOURCES, message).then(sendDesktopSourcesResult);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_WIDGET_SHARE_SCREEN: {
|
||||||
|
ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, message.sourceID, message.withAudio);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_JOIN_CALL: {
|
||||||
|
ipcRenderer.invoke(CALLS_JOIN_CALL, message).then(sendCallsJoinedCall);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_JOINED_CALL: {
|
||||||
|
ipcRenderer.send(CALLS_JOINED_CALL, message.callID, message.sessionID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_JOIN_REQUEST: {
|
||||||
|
ipcRenderer.send(CALLS_JOIN_REQUEST, message.callID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_WIDGET_RESIZE: {
|
||||||
|
ipcRenderer.send(CALLS_WIDGET_RESIZE, message.width, message.height);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_ERROR: {
|
||||||
|
ipcRenderer.send(CALLS_ERROR, message.err, message.callID, message.errMsg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CALLS_WIDGET_CHANNEL_LINK_CLICK:
|
||||||
|
case CALLS_LEAVE_CALL:
|
||||||
|
case DESKTOP_SOURCES_MODAL_REQUEST:
|
||||||
|
case CALLS_POPOUT_FOCUS: {
|
||||||
|
ipcRenderer.send(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ipcRenderer.send(UNREAD_RESULT, isUnread);
|
|
||||||
};
|
|
||||||
|
|
||||||
let sessionExpired: boolean;
|
// Legacy support to hold the full channel object so that it can be used for the click event
|
||||||
const getUnreadCount = () => {
|
const channels: Map<string, {id: string}> = new Map();
|
||||||
// LHS not found => Log out => Count should be 0, but session may be expired.
|
ipcRenderer.on(NOTIFICATION_CLICKED, (event, channelId, teamId, url) => {
|
||||||
let isExpired;
|
const channel = channels.get(channelId) ?? {id: channelId};
|
||||||
if (document.getElementById('sidebar-left') === null) {
|
channels.delete(channelId);
|
||||||
const extraParam = (new URLSearchParams(window.location.search)).get('extra');
|
window.postMessage(
|
||||||
isExpired = extraParam === 'expired';
|
{
|
||||||
} else {
|
type: NOTIFICATION_CLICKED,
|
||||||
isExpired = false;
|
message: {
|
||||||
}
|
channel,
|
||||||
if (isExpired !== sessionExpired) {
|
teamId,
|
||||||
sessionExpired = isExpired;
|
url,
|
||||||
ipcRenderer.send(SESSION_EXPIRED, sessionExpired);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy message passing code - can be running alongside the new API stuff
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Disabling no-explicit-any for this legacy code
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
window.addEventListener('message', ({origin, data = {}}: {origin?: string; data?: {type?: string; message?: any}} = {}) => {
|
|
||||||
const {type, message = {}} = data;
|
|
||||||
if (origin !== window.location.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (type) {
|
|
||||||
case 'webapp-ready':
|
|
||||||
case 'get-app-version': {
|
|
||||||
// register with the webapp to enable custom integration functionality
|
|
||||||
ipcRenderer.invoke(GET_APP_INFO).then((info) => {
|
|
||||||
console.log(`registering ${info.name} v${info.version} with the server`);
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
type: 'register-desktop',
|
|
||||||
message: info,
|
|
||||||
},
|
},
|
||||||
window.location.origin || '*',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'dispatch-notification': {
|
|
||||||
const {title, body, channel, teamId, url, silent, data: messageData} = message;
|
|
||||||
channels.set(channel.id, channel);
|
|
||||||
ipcRenderer.invoke(NOTIFY_MENTION, title, body, channel.id, teamId, url, silent, messageData.soundName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case BROWSER_HISTORY_PUSH: {
|
|
||||||
const {path} = message as {path: string};
|
|
||||||
ipcRenderer.send(BROWSER_HISTORY_PUSH, path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'history-button': {
|
|
||||||
ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_LINK_CLICK: {
|
|
||||||
ipcRenderer.send(CALLS_LINK_CLICK, message.link);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GET_DESKTOP_SOURCES: {
|
|
||||||
ipcRenderer.invoke(GET_DESKTOP_SOURCES, message).then(sendDesktopSourcesResult);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_WIDGET_SHARE_SCREEN: {
|
|
||||||
ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, message.sourceID, message.withAudio);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_JOIN_CALL: {
|
|
||||||
ipcRenderer.invoke(CALLS_JOIN_CALL, message).then(sendCallsJoinedCall);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_JOINED_CALL: {
|
|
||||||
ipcRenderer.send(CALLS_JOINED_CALL, message.callID, message.sessionID);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_JOIN_REQUEST: {
|
|
||||||
ipcRenderer.send(CALLS_JOIN_REQUEST, message.callID);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_WIDGET_RESIZE: {
|
|
||||||
ipcRenderer.send(CALLS_WIDGET_RESIZE, message.width, message.height);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_ERROR: {
|
|
||||||
ipcRenderer.send(CALLS_ERROR, message.err, message.callID, message.errMsg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case CALLS_WIDGET_CHANNEL_LINK_CLICK:
|
|
||||||
case CALLS_LEAVE_CALL:
|
|
||||||
case DESKTOP_SOURCES_MODAL_REQUEST:
|
|
||||||
case CALLS_POPOUT_FOCUS: {
|
|
||||||
ipcRenderer.send(type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Legacy support to hold the full channel object so that it can be used for the click event
|
|
||||||
const channels: Map<string, {id: string}> = new Map();
|
|
||||||
ipcRenderer.on(NOTIFICATION_CLICKED, (event, channelId, teamId, url) => {
|
|
||||||
const channel = channels.get(channelId) ?? {id: channelId};
|
|
||||||
channels.delete(channelId);
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
type: NOTIFICATION_CLICKED,
|
|
||||||
message: {
|
|
||||||
channel,
|
|
||||||
teamId,
|
|
||||||
url,
|
|
||||||
},
|
},
|
||||||
},
|
window.location.origin,
|
||||||
window.location.origin,
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => {
|
ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
type: 'browser-history-push-return',
|
type: 'browser-history-push-return',
|
||||||
message: {
|
message: {
|
||||||
pathName,
|
pathName,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
window.location.origin,
|
||||||
window.location.origin,
|
);
|
||||||
);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const sendHistoryButtonReturn = (status: {canGoBack: boolean; canGoForward: boolean}) => {
|
const sendHistoryButtonReturn = (status: {canGoBack: boolean; canGoForward: boolean}) => {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
type: 'history-button-return',
|
type: 'history-button-return',
|
||||||
message: {
|
message: {
|
||||||
enableBack: status.canGoBack,
|
enableBack: status.canGoBack,
|
||||||
enableForward: status.canGoForward,
|
enableForward: status.canGoForward,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
window.location.origin,
|
||||||
window.location.origin,
|
);
|
||||||
);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
ipcRenderer.on(BROWSER_HISTORY_STATUS_UPDATED, (event, canGoBack, canGoForward) => sendHistoryButtonReturn({canGoBack, canGoForward}));
|
ipcRenderer.on(BROWSER_HISTORY_STATUS_UPDATED, (event, canGoBack, canGoForward) => sendHistoryButtonReturn({canGoBack, canGoForward}));
|
||||||
|
|
||||||
const sendDesktopSourcesResult = (sources: Array<{
|
const sendDesktopSourcesResult = (sources: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
thumbnailURL: string;
|
thumbnailURL: string;
|
||||||
}>) => {
|
}>) => {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
type: DESKTOP_SOURCES_RESULT,
|
type: DESKTOP_SOURCES_RESULT,
|
||||||
message: sources,
|
message: sources,
|
||||||
},
|
},
|
||||||
window.location.origin,
|
window.location.origin,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendCallsJoinedCall = (message: {callID: string; sessionID: string}) => {
|
const sendCallsJoinedCall = (message: {callID: string; sessionID: string}) => {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
type: CALLS_JOINED_CALL,
|
type: CALLS_JOINED_CALL,
|
||||||
message,
|
message,
|
||||||
},
|
},
|
||||||
window.location.origin,
|
window.location.origin,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ipcRenderer.on(CALLS_JOIN_REQUEST, (_, callID) => {
|
ipcRenderer.on(CALLS_JOIN_REQUEST, (_, callID) => {
|
||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
type: CALLS_JOIN_REQUEST,
|
type: CALLS_JOIN_REQUEST,
|
||||||
message: {callID},
|
message: {callID},
|
||||||
},
|
},
|
||||||
window.location.origin,
|
window.location.origin,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(DESKTOP_SOURCES_MODAL_REQUEST, () => {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: DESKTOP_SOURCES_MODAL_REQUEST,
|
||||||
|
},
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(CALLS_WIDGET_SHARE_SCREEN, (_, sourceID, withAudio) => {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: CALLS_WIDGET_SHARE_SCREEN,
|
||||||
|
message: {sourceID, withAudio},
|
||||||
|
},
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(CALLS_ERROR, (_, err, callID, errMsg) => {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: CALLS_ERROR,
|
||||||
|
message: {err, callID, errMsg},
|
||||||
|
},
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// push user activity updates to the webapp
|
||||||
|
ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, userIsActive, isSystemEvent) => {
|
||||||
|
if (window.location.origin !== 'null') {
|
||||||
|
window.postMessage({type: USER_ACTIVITY_UPDATE, message: {userIsActive, manual: isSystemEvent}}, window.location.origin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy functionality that needs to be disabled with the new API
|
||||||
|
*/
|
||||||
|
|
||||||
|
legacyEnabled = true;
|
||||||
|
ipcRenderer.on(IS_UNREAD, checkUnread);
|
||||||
|
const unreadInterval = setInterval(getUnreadCount, 1000);
|
||||||
|
window.addEventListener('storage', onStorageChanged);
|
||||||
|
window.addEventListener('load', onLoad);
|
||||||
|
|
||||||
|
legacyOff = () => {
|
||||||
|
ipcRenderer.send(LEGACY_OFF);
|
||||||
|
ipcRenderer.off(IS_UNREAD, checkUnread);
|
||||||
|
clearInterval(unreadInterval);
|
||||||
|
window.removeEventListener('storage', onStorageChanged);
|
||||||
|
window.removeEventListener('load', onLoad);
|
||||||
|
|
||||||
|
legacyEnabled = false;
|
||||||
|
console.log('New API preload initialized');
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on(DESKTOP_SOURCES_MODAL_REQUEST, () => {
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
type: DESKTOP_SOURCES_MODAL_REQUEST,
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on(CALLS_WIDGET_SHARE_SCREEN, (_, sourceID, withAudio) => {
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
type: CALLS_WIDGET_SHARE_SCREEN,
|
|
||||||
message: {sourceID, withAudio},
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcRenderer.on(CALLS_ERROR, (_, err, callID, errMsg) => {
|
|
||||||
window.postMessage(
|
|
||||||
{
|
|
||||||
type: CALLS_ERROR,
|
|
||||||
message: {err, callID, errMsg},
|
|
||||||
},
|
|
||||||
window.location.origin,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// push user activity updates to the webapp
|
|
||||||
ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, userIsActive, isSystemEvent) => {
|
|
||||||
if (window.location.origin !== 'null') {
|
|
||||||
window.postMessage({type: USER_ACTIVITY_UPDATE, message: {userIsActive, manual: isSystemEvent}}, window.location.origin);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy functionality that needs to be disabled with the new API
|
|
||||||
*/
|
|
||||||
|
|
||||||
let legacyEnabled = true;
|
|
||||||
ipcRenderer.on(IS_UNREAD, checkUnread);
|
|
||||||
const unreadInterval = setInterval(getUnreadCount, 1000);
|
|
||||||
window.addEventListener('storage', onStorageChanged);
|
|
||||||
window.addEventListener('load', onLoad);
|
|
||||||
|
|
||||||
function legacyOff() {
|
|
||||||
ipcRenderer.send(LEGACY_OFF);
|
|
||||||
ipcRenderer.off(IS_UNREAD, checkUnread);
|
|
||||||
clearInterval(unreadInterval);
|
|
||||||
window.removeEventListener('storage', onStorageChanged);
|
|
||||||
window.removeEventListener('load', onLoad);
|
|
||||||
|
|
||||||
legacyEnabled = false;
|
|
||||||
console.log('New API preload initialized');
|
|
||||||
}
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ import {
|
||||||
GET_MEDIA_ACCESS_STATUS,
|
GET_MEDIA_ACCESS_STATUS,
|
||||||
VIEW_FINISHED_RESIZING,
|
VIEW_FINISHED_RESIZING,
|
||||||
GET_NONCE,
|
GET_NONCE,
|
||||||
|
IS_DEVELOPER_MODE_ENABLED,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
console.log('Preload initialized');
|
console.log('Preload initialized');
|
||||||
|
@ -126,6 +127,7 @@ contextBridge.exposeInMainWorld('desktop', {
|
||||||
checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES),
|
checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES),
|
||||||
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
|
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
|
||||||
getNonce: () => ipcRenderer.invoke(GET_NONCE),
|
getNonce: () => ipcRenderer.invoke(GET_NONCE),
|
||||||
|
isDeveloperModeEnabled: () => ipcRenderer.invoke(IS_DEVELOPER_MODE_ENABLED),
|
||||||
|
|
||||||
updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
|
updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
|
||||||
updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder),
|
updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder),
|
||||||
|
|
|
@ -88,12 +88,16 @@ export function getLocalPreload(file: string) {
|
||||||
return path.join(app.getAppPath(), file);
|
return path.join(app.getAppPath(), file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function composeUserAgent() {
|
export function composeUserAgent(browserMode?: boolean) {
|
||||||
const baseUserAgent = app.userAgentFallback.split(' ');
|
const baseUserAgent = app.userAgentFallback.split(' ');
|
||||||
|
|
||||||
// filter out the Mattermost tag that gets added earlier on
|
// filter out the Mattermost tag that gets added earlier on
|
||||||
const filteredUserAgent = baseUserAgent.filter((ua) => !ua.startsWith('Mattermost'));
|
const filteredUserAgent = baseUserAgent.filter((ua) => !ua.startsWith('Mattermost'));
|
||||||
|
|
||||||
|
if (browserMode) {
|
||||||
|
return filteredUserAgent.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()}`;
|
return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,9 @@ jest.mock('../utils', () => ({
|
||||||
composeUserAgent: () => 'Mattermost/5.0.0',
|
composeUserAgent: () => 'Mattermost/5.0.0',
|
||||||
shouldHaveBackBar: jest.fn(),
|
shouldHaveBackBar: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/developerMode', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
|
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
|
||||||
const view = new MessagingView(server, true);
|
const view = new MessagingView(server, true);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import ServerManager from 'common/servers/serverManager';
|
||||||
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
||||||
import {isInternalURL, parseURL} from 'common/utils/url';
|
import {isInternalURL, parseURL} from 'common/utils/url';
|
||||||
import type {MattermostView} from 'common/views/View';
|
import type {MattermostView} from 'common/views/View';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import {getServerAPI} from 'main/server/serverAPI';
|
import {getServerAPI} from 'main/server/serverAPI';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
private atRoot: boolean;
|
private atRoot: boolean;
|
||||||
private options: BrowserViewConstructorOptions;
|
private options: BrowserViewConstructorOptions;
|
||||||
private removeLoading?: NodeJS.Timeout;
|
private removeLoading?: NodeJS.Timeout;
|
||||||
private contextMenu: ContextMenu;
|
private contextMenu?: ContextMenu;
|
||||||
private status?: Status;
|
private status?: Status;
|
||||||
private retryLoad?: NodeJS.Timeout;
|
private retryLoad?: NodeJS.Timeout;
|
||||||
private maxRetries: number;
|
private maxRetries: number;
|
||||||
|
@ -65,7 +66,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
const preload = getLocalPreload('externalAPI.js');
|
const preload = getLocalPreload('externalAPI.js');
|
||||||
this.options = Object.assign({}, options);
|
this.options = Object.assign({}, options);
|
||||||
this.options.webPreferences = {
|
this.options.webPreferences = {
|
||||||
preload,
|
preload: DeveloperMode.get('browserOnly') ? undefined : preload,
|
||||||
additionalArguments: [
|
additionalArguments: [
|
||||||
`version=${app.getVersion()}`,
|
`version=${app.getVersion()}`,
|
||||||
`appName=${app.name}`,
|
`appName=${app.name}`,
|
||||||
|
@ -99,7 +100,10 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
|
|
||||||
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
|
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
|
||||||
|
|
||||||
this.contextMenu = new ContextMenu({}, this.browserView);
|
if (!DeveloperMode.get('disableContextMenu')) {
|
||||||
|
this.contextMenu = new ContextMenu({}, this.browserView);
|
||||||
|
}
|
||||||
|
|
||||||
this.maxRetries = MAX_SERVER_RETRIES;
|
this.maxRetries = MAX_SERVER_RETRIES;
|
||||||
|
|
||||||
this.altPressStatus = false;
|
this.altPressStatus = false;
|
||||||
|
@ -192,7 +196,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
loadURL = this.view.url.toString();
|
loadURL = this.view.url.toString();
|
||||||
}
|
}
|
||||||
this.log.verbose(`Loading ${loadURL}`);
|
this.log.verbose(`Loading ${loadURL}`);
|
||||||
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
|
||||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||||
if (err.code && err.code.startsWith('ERR_CERT')) {
|
if (err.code && err.code.startsWith('ERR_CERT')) {
|
||||||
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
|
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
|
||||||
|
@ -427,7 +431,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
if (!this.browserView || !this.browserView.webContents) {
|
if (!this.browserView || !this.browserView.webContents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
|
||||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||||
if (this.maxRetries-- > 0) {
|
if (this.maxRetries-- > 0) {
|
||||||
this.loadRetry(loadURL, err);
|
this.loadRetry(loadURL, err);
|
||||||
|
|
159
src/main/views/pluginsPopUps.test.js
Normal file
159
src/main/views/pluginsPopUps.test.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {shell} from 'electron';
|
||||||
|
|
||||||
|
import ServerViewState from 'app/serverViewState';
|
||||||
|
import {parseURL} from 'common/utils/url';
|
||||||
|
import ViewManager from 'main/views/viewManager';
|
||||||
|
|
||||||
|
import PluginsPopUpsManager from './pluginsPopUps';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
shell: {
|
||||||
|
openExternal: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/views/viewManager', () => ({
|
||||||
|
getView: jest.fn(),
|
||||||
|
getViewByWebContentsId: jest.fn(),
|
||||||
|
handleDeepLink: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/serverViewState', () => ({
|
||||||
|
switchServer: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
focus: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockContextMenuReload = jest.fn();
|
||||||
|
const mockContextMenuDispose = jest.fn();
|
||||||
|
jest.mock('../contextMenu', () => {
|
||||||
|
return jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
reload: mockContextMenuReload,
|
||||||
|
dispose: mockContextMenuDispose,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PluginsPopUpsManager', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generateHandleCreateWindow', () => {
|
||||||
|
const handlers = {};
|
||||||
|
const win = {
|
||||||
|
webContents: {
|
||||||
|
id: 45,
|
||||||
|
on: jest.fn((ev, handler) => {
|
||||||
|
handlers[ev] = handler;
|
||||||
|
}),
|
||||||
|
once: jest.fn((ev, handler) => {
|
||||||
|
handlers[ev] = handler;
|
||||||
|
}),
|
||||||
|
setWindowOpenHandler: jest.fn((handler) => {
|
||||||
|
handlers['window-open'] = handler;
|
||||||
|
}),
|
||||||
|
removeAllListeners: jest.fn(),
|
||||||
|
},
|
||||||
|
once: jest.fn((ev, handler) => {
|
||||||
|
handlers[ev] = handler;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const details = {
|
||||||
|
url: 'about:blank',
|
||||||
|
};
|
||||||
|
PluginsPopUpsManager.generateHandleCreateWindow(1)(win, details);
|
||||||
|
|
||||||
|
expect(win.webContents.on).toHaveBeenNthCalledWith(1, 'will-redirect', handlers['will-redirect']);
|
||||||
|
expect(win.webContents.on).toHaveBeenNthCalledWith(2, 'will-navigate', handlers['will-navigate']);
|
||||||
|
expect(win.webContents.on).toHaveBeenNthCalledWith(3, 'did-start-navigation', handlers['did-start-navigation']);
|
||||||
|
expect(win.webContents.once).toHaveBeenCalledWith('render-process-gone', handlers['render-process-gone']);
|
||||||
|
expect(win.webContents.setWindowOpenHandler).toHaveBeenCalledWith(handlers['window-open']);
|
||||||
|
|
||||||
|
expect(win.once).toHaveBeenCalledWith('closed', handlers.closed);
|
||||||
|
|
||||||
|
expect(mockContextMenuReload).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Verify the popout has been added to the map
|
||||||
|
expect(PluginsPopUpsManager.popups).toHaveProperty('45', {parentId: 1, win});
|
||||||
|
|
||||||
|
// Verify redirects are disabled
|
||||||
|
const redirectEv = {
|
||||||
|
preventDefault: jest.fn(),
|
||||||
|
};
|
||||||
|
handlers['will-redirect'](redirectEv);
|
||||||
|
expect(redirectEv.preventDefault).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify navigations are only allowed to the same url
|
||||||
|
const navigateEv = {
|
||||||
|
preventDefault: jest.fn(),
|
||||||
|
url: 'about:blank',
|
||||||
|
};
|
||||||
|
handlers['will-navigate'](navigateEv);
|
||||||
|
expect(navigateEv.preventDefault).not.toHaveBeenCalled();
|
||||||
|
navigateEv.url = 'http://localhost:8065';
|
||||||
|
handlers['will-navigate'](navigateEv);
|
||||||
|
expect(navigateEv.preventDefault).toHaveBeenCalled();
|
||||||
|
|
||||||
|
navigateEv.preventDefault = jest.fn();
|
||||||
|
navigateEv.url = 'about:blank';
|
||||||
|
handlers['did-start-navigation'](navigateEv);
|
||||||
|
expect(navigateEv.preventDefault).not.toHaveBeenCalled();
|
||||||
|
navigateEv.url = 'http://localhost:8065';
|
||||||
|
handlers['did-start-navigation'](navigateEv);
|
||||||
|
expect(navigateEv.preventDefault).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify opening new windows is not allowed
|
||||||
|
expect(handlers['window-open']({url: ''})).toEqual({action: 'deny'});
|
||||||
|
expect(shell.openExternal).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify internal link is routed in main view
|
||||||
|
ViewManager.getViewByWebContentsId.mockReturnValue({name: 'parent', webContentsId: 1, view: {server: {url: parseURL('http://localhost:8065'), id: 4545}}});
|
||||||
|
expect(handlers['window-open']({url: 'http://localhost:8065/team/channel'})).toEqual({action: 'deny'});
|
||||||
|
expect(shell.openExternal).not.toHaveBeenCalled();
|
||||||
|
expect(ServerViewState.switchServer).toHaveBeenCalledWith(4545);
|
||||||
|
expect(ViewManager.handleDeepLink).toHaveBeenCalledWith(parseURL('http://localhost:8065/team/channel'));
|
||||||
|
|
||||||
|
// Verify opening external links is allowed through browser
|
||||||
|
expect(handlers['window-open']({url: 'https://www.example.com'})).toEqual({action: 'deny'});
|
||||||
|
expect(shell.openExternal).toHaveBeenCalledWith('https://www.example.com');
|
||||||
|
|
||||||
|
// Simulate render process gone
|
||||||
|
handlers['render-process-gone'](null, {reason: 'oom'});
|
||||||
|
expect(win.webContents.removeAllListeners).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Throw case
|
||||||
|
win.webContents.removeAllListeners = jest.fn(() => {
|
||||||
|
throw new Error('failed');
|
||||||
|
});
|
||||||
|
handlers['render-process-gone'](null, {reason: 'clean-exit'});
|
||||||
|
expect(win.webContents.removeAllListeners).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Close
|
||||||
|
handlers.closed();
|
||||||
|
expect(mockContextMenuDispose).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Verify the popout reference has been deleted
|
||||||
|
expect(PluginsPopUpsManager.popups).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handleNewWindow', () => {
|
||||||
|
// Anything but about:blank should not be allowed
|
||||||
|
expect(PluginsPopUpsManager.handleNewWindow(45, {url: ''})).toEqual({action: 'deny'});
|
||||||
|
expect(PluginsPopUpsManager.handleNewWindow(45, {url: 'http://localhost:8065'})).toEqual({action: 'deny'});
|
||||||
|
|
||||||
|
// We should deny also if the parent view doesn't exist
|
||||||
|
expect(PluginsPopUpsManager.handleNewWindow(45, {url: 'about:blank'})).toEqual({action: 'deny'});
|
||||||
|
|
||||||
|
// Finally, we allow if URL is `about:blank` and a parent view exists
|
||||||
|
ViewManager.getViewByWebContentsId.mockReturnValue({name: 'parent', webContentsId: 1});
|
||||||
|
expect(PluginsPopUpsManager.handleNewWindow(45, {url: 'about:blank'})).toEqual({action: 'allow'});
|
||||||
|
});
|
||||||
|
});
|
137
src/main/views/pluginsPopUps.ts
Normal file
137
src/main/views/pluginsPopUps.ts
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import type {
|
||||||
|
BrowserWindow,
|
||||||
|
Event,
|
||||||
|
WebContentsWillNavigateEventParams,
|
||||||
|
WebContentsWillRedirectEventParams,
|
||||||
|
WebContentsDidStartNavigationEventParams,
|
||||||
|
} from 'electron';
|
||||||
|
import {shell} from 'electron';
|
||||||
|
|
||||||
|
import ServerViewState from 'app/serverViewState';
|
||||||
|
import {Logger} from 'common/log';
|
||||||
|
import {
|
||||||
|
isTeamUrl,
|
||||||
|
parseURL,
|
||||||
|
} from 'common/utils/url';
|
||||||
|
import ContextMenu from 'main/contextMenu';
|
||||||
|
import ViewManager from 'main/views/viewManager';
|
||||||
|
import {generateHandleConsoleMessage} from 'main/views/webContentEventsCommon';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
|
const log = new Logger('PluginsPopUpsManager');
|
||||||
|
|
||||||
|
type PluginPopUp = {
|
||||||
|
parentId: number;
|
||||||
|
win: BrowserWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginsPopUpsManager {
|
||||||
|
popups: Record<number, PluginPopUp>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.popups = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHandleCreateWindow = (parentId: number) => (win: BrowserWindow, details: Electron.DidCreateWindowDetails) => {
|
||||||
|
const webContentsId = win.webContents.id;
|
||||||
|
|
||||||
|
log.debug('created popup window', details.url, webContentsId);
|
||||||
|
this.popups[webContentsId] = {
|
||||||
|
parentId,
|
||||||
|
win,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We take a conservative approach for the time being and disallow most events coming from popups:
|
||||||
|
// - Redirects
|
||||||
|
// - Navigation
|
||||||
|
// - Opening new windows
|
||||||
|
win.webContents.on('will-redirect', (ev: Event<WebContentsWillRedirectEventParams>) => {
|
||||||
|
log.warn(`prevented popup window from redirecting to: ${ev.url}`);
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
win.webContents.on('will-navigate', (ev: Event<WebContentsWillNavigateEventParams>) => {
|
||||||
|
if (ev.url === details.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(`prevented popup window from navigating to: ${ev.url}`);
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
win.webContents.on('did-start-navigation', (ev: Event<WebContentsDidStartNavigationEventParams>) => {
|
||||||
|
if (ev.url === details.url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(`prevented popup window from navigating to: ${ev.url}`);
|
||||||
|
ev.preventDefault();
|
||||||
|
});
|
||||||
|
win.webContents.setWindowOpenHandler(({url}): {action: 'deny'} => {
|
||||||
|
const parsedURL = parseURL(url);
|
||||||
|
if (!parsedURL) {
|
||||||
|
log.warn(`Ignoring non-url ${url}`);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverView = ViewManager.getViewByWebContentsId(parentId)?.view;
|
||||||
|
|
||||||
|
// We allow internal (i.e., same server) links to be routed as expected.
|
||||||
|
if (serverView && parsedURL && isTeamUrl(serverView.server.url, parsedURL, true)) {
|
||||||
|
ServerViewState.switchServer(serverView.server.id);
|
||||||
|
MainWindow.get()?.focus();
|
||||||
|
ViewManager.handleDeepLink(parsedURL);
|
||||||
|
} else {
|
||||||
|
// We allow to open external links through browser.
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(`prevented popup window from opening window to ${url}`);
|
||||||
|
|
||||||
|
return {action: 'deny'};
|
||||||
|
});
|
||||||
|
|
||||||
|
win.webContents.on('console-message', generateHandleConsoleMessage(log));
|
||||||
|
|
||||||
|
const contextMenu = new ContextMenu({}, win);
|
||||||
|
contextMenu.reload();
|
||||||
|
|
||||||
|
win.once('closed', () => {
|
||||||
|
log.debug('removing popup window', details.url, webContentsId);
|
||||||
|
Reflect.deleteProperty(this.popups, webContentsId);
|
||||||
|
contextMenu.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
win.webContents.once('render-process-gone', (_, details) => {
|
||||||
|
if (details.reason !== 'clean-exit') {
|
||||||
|
log.error('Renderer process for a webcontent is no longer available:', details.reason);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
win.webContents.removeAllListeners();
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`Error while trying to detach listeners, this might be ok if the process crashed: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public handleNewWindow(parentId: number, details: Electron.HandlerDetails): {action: 'deny' | 'allow'} {
|
||||||
|
// Making extra explicit what we allow. This should already be enforced on
|
||||||
|
// the calling side.
|
||||||
|
if (details.url !== 'about:blank') {
|
||||||
|
log.warn(`prevented new window creation: ${details.url}`);
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the parent view exists.
|
||||||
|
const parentView = ViewManager.getViewByWebContentsId(parentId);
|
||||||
|
if (!parentView) {
|
||||||
|
log.warn('handleNewWindow: parent view not found');
|
||||||
|
return {action: 'deny'};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {action: 'allow'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginsPopUpsManager = new PluginsPopUpsManager();
|
||||||
|
export default pluginsPopUpsManager;
|
|
@ -34,6 +34,7 @@ import {
|
||||||
LEGACY_OFF,
|
LEGACY_OFF,
|
||||||
UNREADS_AND_MENTIONS,
|
UNREADS_AND_MENTIONS,
|
||||||
TAB_LOGIN_CHANGED,
|
TAB_LOGIN_CHANGED,
|
||||||
|
DEVELOPER_MODE_UPDATED,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -45,10 +46,13 @@ import Utils from 'common/utils/util';
|
||||||
import type {MattermostView} from 'common/views/View';
|
import type {MattermostView} from 'common/views/View';
|
||||||
import {TAB_MESSAGING} from 'common/views/View';
|
import {TAB_MESSAGING} from 'common/views/View';
|
||||||
import {flushCookiesStore} from 'main/app/utils';
|
import {flushCookiesStore} from 'main/app/utils';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
import PermissionsManager from 'main/permissionsManager';
|
import PermissionsManager from 'main/permissionsManager';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
|
import type {DeveloperSettings} from 'types/settings';
|
||||||
|
|
||||||
import LoadingScreen from './loadingScreen';
|
import LoadingScreen from './loadingScreen';
|
||||||
import {MattermostBrowserView} from './MattermostBrowserView';
|
import {MattermostBrowserView} from './MattermostBrowserView';
|
||||||
import modalManager from './modalManager';
|
import modalManager from './modalManager';
|
||||||
|
@ -91,6 +95,7 @@ export class ViewManager {
|
||||||
ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId));
|
ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId));
|
||||||
|
|
||||||
ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration);
|
ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration);
|
||||||
|
DeveloperMode.on(DEVELOPER_MODE_UPDATED, this.handleDeveloperModeUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private init = () => {
|
private init = () => {
|
||||||
|
@ -99,6 +104,17 @@ export class ViewManager {
|
||||||
this.showInitial();
|
this.showInitial();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleDeveloperModeUpdated = (json: DeveloperSettings) => {
|
||||||
|
log.debug('handleDeveloperModeUpdated', json);
|
||||||
|
|
||||||
|
if (['browserOnly', 'disableContextMenu', 'forceLegacyAPI', 'forceNewAPI'].some((key) => Object.hasOwn(json, key))) {
|
||||||
|
this.views.forEach((view) => view.destroy());
|
||||||
|
this.views = new Map();
|
||||||
|
this.closedViews = new Map();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getView = (viewId: string) => {
|
getView = (viewId: string) => {
|
||||||
return this.views.get(viewId);
|
return this.views.get(viewId);
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,9 @@ import {getLevel} from 'common/log';
|
||||||
import ContextMenu from 'main/contextMenu';
|
import ContextMenu from 'main/contextMenu';
|
||||||
import ViewManager from 'main/views/viewManager';
|
import ViewManager from 'main/views/viewManager';
|
||||||
|
|
||||||
|
import PluginsPopUpsManager from './pluginsPopUps';
|
||||||
import {WebContentsEventManager} from './webContentEvents';
|
import {WebContentsEventManager} from './webContentEvents';
|
||||||
|
import {generateHandleConsoleMessage} from './webContentEventsCommon';
|
||||||
|
|
||||||
import allowProtocolDialog from '../allowProtocolDialog';
|
import allowProtocolDialog from '../allowProtocolDialog';
|
||||||
|
|
||||||
|
@ -31,6 +33,11 @@ jest.mock('main/views/viewManager', () => ({
|
||||||
getViewByWebContentsId: jest.fn(),
|
getViewByWebContentsId: jest.fn(),
|
||||||
handleDeepLink: jest.fn(),
|
handleDeepLink: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/views/pluginsPopUps', () => ({
|
||||||
|
handleNewWindow: jest.fn(() => ({action: 'allow'})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
composeUserAgent: jest.fn(),
|
composeUserAgent: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
@ -179,6 +186,11 @@ describe('main/views/webContentsEvents', () => {
|
||||||
expect(newWindow({url: 'devtools://aaaaaa.com'})).toStrictEqual({action: 'allow'});
|
expect(newWindow({url: 'devtools://aaaaaa.com'})).toStrictEqual({action: 'allow'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should defer about:blank to PluginsPopUpsManager', () => {
|
||||||
|
expect(newWindow({url: 'about:blank'})).toStrictEqual({action: 'allow'});
|
||||||
|
expect(PluginsPopUpsManager.handleNewWindow).toHaveBeenCalledWith(1, {url: 'about:blank'});
|
||||||
|
});
|
||||||
|
|
||||||
it('should open invalid URIs in browser', () => {
|
it('should open invalid URIs in browser', () => {
|
||||||
expect(newWindow({url: 'https://google.com/?^'})).toStrictEqual({action: 'deny'});
|
expect(newWindow({url: 'https://google.com/?^'})).toStrictEqual({action: 'deny'});
|
||||||
expect(shell.openExternal).toBeCalledWith('https://google.com/?^');
|
expect(shell.openExternal).toBeCalledWith('https://google.com/?^');
|
||||||
|
@ -249,7 +261,7 @@ describe('main/views/webContentsEvents', () => {
|
||||||
withPrefix: jest.fn().mockReturnThis(),
|
withPrefix: jest.fn().mockReturnThis(),
|
||||||
};
|
};
|
||||||
webContentsEventManager.log = jest.fn().mockReturnValue(logObject);
|
webContentsEventManager.log = jest.fn().mockReturnValue(logObject);
|
||||||
const consoleMessage = webContentsEventManager.generateHandleConsoleMessage();
|
const consoleMessage = generateHandleConsoleMessage(logObject);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
getLevel.mockReset();
|
getLevel.mockReset();
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import type {WebContents, Event} from 'electron';
|
import type {WebContents, Event} from 'electron';
|
||||||
import {BrowserWindow, shell} from 'electron';
|
import {BrowserWindow, shell} from 'electron';
|
||||||
|
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger, getLevel} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
import ServerManager from 'common/servers/serverManager';
|
import ServerManager from 'common/servers/serverManager';
|
||||||
import {
|
import {
|
||||||
isAdminUrl,
|
isAdminUrl,
|
||||||
|
@ -28,10 +26,13 @@ import {
|
||||||
} from 'common/utils/url';
|
} from 'common/utils/url';
|
||||||
import {flushCookiesStore} from 'main/app/utils';
|
import {flushCookiesStore} from 'main/app/utils';
|
||||||
import ContextMenu from 'main/contextMenu';
|
import ContextMenu from 'main/contextMenu';
|
||||||
|
import PluginsPopUpsManager from 'main/views/pluginsPopUps';
|
||||||
import ViewManager from 'main/views/viewManager';
|
import ViewManager from 'main/views/viewManager';
|
||||||
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
|
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
|
import {generateHandleConsoleMessage} from './webContentEventsCommon';
|
||||||
|
|
||||||
import {protocols} from '../../../electron-builder.json';
|
import {protocols} from '../../../electron-builder.json';
|
||||||
import allowProtocolDialog from '../allowProtocolDialog';
|
import allowProtocolDialog from '../allowProtocolDialog';
|
||||||
import {composeUserAgent} from '../utils';
|
import {composeUserAgent} from '../utils';
|
||||||
|
@ -40,13 +41,6 @@ type CustomLogin = {
|
||||||
inProgress: boolean;
|
inProgress: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ConsoleMessageLevel {
|
|
||||||
Verbose,
|
|
||||||
Info,
|
|
||||||
Warning,
|
|
||||||
Error
|
|
||||||
}
|
|
||||||
|
|
||||||
const log = new Logger('WebContentsEventManager');
|
const log = new Logger('WebContentsEventManager');
|
||||||
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
||||||
|
|
||||||
|
@ -169,6 +163,11 @@ export class WebContentsEventManager {
|
||||||
return {action: 'allow'};
|
return {action: 'allow'};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow plugins to open blank popup windows.
|
||||||
|
if (parsedURL.toString() === 'about:blank') {
|
||||||
|
return PluginsPopUpsManager.handleNewWindow(webContentsId, details);
|
||||||
|
}
|
||||||
|
|
||||||
// Check for custom protocol
|
// Check for custom protocol
|
||||||
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:' && parsedURL.protocol !== `${scheme}:`) {
|
if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:' && parsedURL.protocol !== `${scheme}:`) {
|
||||||
allowProtocolDialog.handleDialogEvent(parsedURL.protocol, details.url);
|
allowProtocolDialog.handleDialogEvent(parsedURL.protocol, details.url);
|
||||||
|
@ -296,27 +295,6 @@ export class WebContentsEventManager {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private generateHandleConsoleMessage = (webContentsId: number) => (_: Event, level: number, message: string, line: number, sourceId: string) => {
|
|
||||||
const wcLog = this.log(webContentsId).withPrefix('renderer');
|
|
||||||
let logFn = wcLog.debug;
|
|
||||||
switch (level) {
|
|
||||||
case ConsoleMessageLevel.Error:
|
|
||||||
logFn = wcLog.error;
|
|
||||||
break;
|
|
||||||
case ConsoleMessageLevel.Warning:
|
|
||||||
logFn = wcLog.warn;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include line entries if we're debugging
|
|
||||||
const entries = [message];
|
|
||||||
if (['debug', 'silly'].includes(getLevel())) {
|
|
||||||
entries.push(`(${path.basename(sourceId)}:${line})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
logFn(...entries);
|
|
||||||
};
|
|
||||||
|
|
||||||
removeWebContentsListeners = (id: number) => {
|
removeWebContentsListeners = (id: number) => {
|
||||||
if (this.listeners[id]) {
|
if (this.listeners[id]) {
|
||||||
this.listeners[id]();
|
this.listeners[id]();
|
||||||
|
@ -352,7 +330,11 @@ export class WebContentsEventManager {
|
||||||
const newWindow = this.generateNewWindowListener(contents.id, spellcheck);
|
const newWindow = this.generateNewWindowListener(contents.id, spellcheck);
|
||||||
contents.setWindowOpenHandler(newWindow);
|
contents.setWindowOpenHandler(newWindow);
|
||||||
|
|
||||||
const consoleMessage = this.generateHandleConsoleMessage(contents.id);
|
// Defer handling of new popup windows to PluginsPopUpsManager. These still need to be
|
||||||
|
// previously allowed from generateNewWindowListener through PluginsPopUpsManager.handleNewWindow.
|
||||||
|
contents.on('did-create-window', PluginsPopUpsManager.generateHandleCreateWindow(contents.id));
|
||||||
|
|
||||||
|
const consoleMessage = generateHandleConsoleMessage(this.log(contents.id));
|
||||||
contents.on('console-message', consoleMessage);
|
contents.on('console-message', consoleMessage);
|
||||||
|
|
||||||
addListeners?.(contents);
|
addListeners?.(contents);
|
||||||
|
|
36
src/main/views/webContentEventsCommon.ts
Normal file
36
src/main/views/webContentEventsCommon.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import type {Event} from 'electron';
|
||||||
|
|
||||||
|
import type {Logger} from 'common/log';
|
||||||
|
import {getLevel} from 'common/log';
|
||||||
|
|
||||||
|
enum ConsoleMessageLevel {
|
||||||
|
Verbose,
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateHandleConsoleMessage = (log: Logger) => (_: Event, level: number, message: string, line: number, sourceId: string) => {
|
||||||
|
const wcLog = log.withPrefix('renderer');
|
||||||
|
let logFn = wcLog.debug;
|
||||||
|
switch (level) {
|
||||||
|
case ConsoleMessageLevel.Error:
|
||||||
|
logFn = wcLog.error;
|
||||||
|
break;
|
||||||
|
case ConsoleMessageLevel.Warning:
|
||||||
|
logFn = wcLog.warn;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include line entries if we're debugging
|
||||||
|
const entries = [message];
|
||||||
|
if (['debug', 'silly'].includes(getLevel())) {
|
||||||
|
entries.push(`(${path.basename(sourceId)}:${line})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logFn(...entries);
|
||||||
|
};
|
|
@ -556,7 +556,6 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
|
|
||||||
describe('handleGetDesktopSources', () => {
|
describe('handleGetDesktopSources', () => {
|
||||||
const callsWidgetWindow = new CallsWidgetWindow();
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
callsWidgetWindow.options = {callID: 'callID'};
|
|
||||||
callsWidgetWindow.win = {
|
callsWidgetWindow.win = {
|
||||||
webContents: {
|
webContents: {
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
|
@ -618,6 +617,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(true));
|
PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(true));
|
||||||
ViewManager.getViewByWebContentsId.mockImplementation((id) => [...views.values()].find((view) => view.webContentsId === id));
|
ViewManager.getViewByWebContentsId.mockImplementation((id) => [...views.values()].find((view) => view.webContentsId === id));
|
||||||
callsWidgetWindow.mainView = views.get('server-1_view-1');
|
callsWidgetWindow.mainView = views.get('server-1_view-1');
|
||||||
|
callsWidgetWindow.options = {callID: 'callID'};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -625,6 +625,34 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
callsWidgetWindow.missingScreensharePermissions = undefined;
|
callsWidgetWindow.missingScreensharePermissions = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should send sources back - uninitialized', async () => {
|
||||||
|
callsWidgetWindow.mainView = undefined;
|
||||||
|
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'screen0',
|
||||||
|
thumbnail: {
|
||||||
|
toDataURL: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'window0',
|
||||||
|
thumbnail: {
|
||||||
|
toDataURL: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sources = await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
||||||
|
expect(sources).toEqual([
|
||||||
|
{
|
||||||
|
id: 'screen0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'window0',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should send sources back', async () => {
|
it('should send sources back', async () => {
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
||||||
{
|
{
|
||||||
|
@ -652,15 +680,28 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send error with no sources', async () => {
|
it('should throw and send error with no sources', async () => {
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]);
|
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]);
|
||||||
await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
|
||||||
|
await expect(callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null)).rejects.toThrow('permissions denied');
|
||||||
|
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
||||||
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send error with no permissions', async () => {
|
it('should throw but not send calls error when uninitialized', async () => {
|
||||||
|
callsWidgetWindow.options = undefined;
|
||||||
|
|
||||||
|
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]);
|
||||||
|
|
||||||
|
await expect(callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null)).rejects.toThrow('permissions denied');
|
||||||
|
|
||||||
|
expect(callsWidgetWindow.win.webContents.send).not.toHaveBeenCalled();
|
||||||
|
expect(views.get('server-1_view-1').sendToRenderer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw and send error with no permissions', async () => {
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
||||||
{
|
{
|
||||||
id: 'screen0',
|
id: 'screen0',
|
||||||
|
@ -671,7 +712,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
]);
|
]);
|
||||||
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
||||||
|
|
||||||
await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
await expect(callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null)).rejects.toThrow('permissions denied');
|
||||||
|
|
||||||
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
|
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
||||||
|
@ -680,6 +721,27 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw but not send error with no permissions when uninitialized', async () => {
|
||||||
|
callsWidgetWindow.options = undefined;
|
||||||
|
|
||||||
|
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'screen0',
|
||||||
|
thumbnail: {
|
||||||
|
toDataURL: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
||||||
|
|
||||||
|
await expect(callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null)).rejects.toThrow('permissions denied');
|
||||||
|
|
||||||
|
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
|
||||||
|
|
||||||
|
expect(callsWidgetWindow.win.webContents.send).not.toHaveBeenCalled();
|
||||||
|
expect(views.get('server-1_view-1').sendToRenderer).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('macos - no permissions', async () => {
|
it('macos - no permissions', async () => {
|
||||||
const originalPlatform = process.platform;
|
const originalPlatform = process.platform;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
|
@ -696,7 +758,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
]);
|
]);
|
||||||
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
||||||
|
|
||||||
await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
await expect(callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null)).rejects.toThrow('permissions denied');
|
||||||
|
|
||||||
expect(callsWidgetWindow.missingScreensharePermissions).toBe(true);
|
expect(callsWidgetWindow.missingScreensharePermissions).toBe(true);
|
||||||
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1);
|
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1);
|
||||||
|
@ -704,7 +766,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
||||||
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', 'screen-permissions', 'callID');
|
||||||
|
|
||||||
await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
await expect(callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null)).rejects.toThrow('permissions denied');
|
||||||
|
|
||||||
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2);
|
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2);
|
||||||
expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(1);
|
expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(1);
|
||||||
|
|
|
@ -378,15 +378,15 @@ export class CallsWidgetWindow {
|
||||||
private handleGetDesktopSources = async (event: IpcMainInvokeEvent, opts: Electron.SourcesOptions) => {
|
private handleGetDesktopSources = async (event: IpcMainInvokeEvent, opts: Electron.SourcesOptions) => {
|
||||||
log.debug('handleGetDesktopSources', opts);
|
log.debug('handleGetDesktopSources', opts);
|
||||||
|
|
||||||
if (event.sender.id !== this.mainView?.webContentsId) {
|
// For Calls we make an extra check to ensure the event is coming from the expected window (main view).
|
||||||
log.warn('handleGetDesktopSources', 'Blocked on wrong webContentsId');
|
// Otherwise we want to allow for other plugins to ask for screen sharing sources.
|
||||||
return [];
|
if (this.mainView && event.sender.id !== this.mainView.webContentsId) {
|
||||||
|
throw new Error('handleGetDesktopSources: blocked on wrong webContentsId');
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = ViewManager.getViewByWebContentsId(event.sender.id);
|
const view = ViewManager.getViewByWebContentsId(event.sender.id);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
log.error('handleGetDesktopSources: view not found');
|
throw new Error('handleGetDesktopSources: view not found');
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') {
|
if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') {
|
||||||
|
@ -407,8 +407,7 @@ export class CallsWidgetWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await PermissionsManager.doPermissionRequest(view.webContentsId, 'screenShare', {requestingUrl: view.view.server.url.toString(), isMainFrame: false})) {
|
if (!await PermissionsManager.doPermissionRequest(view.webContentsId, 'screenShare', {requestingUrl: view.view.server.url.toString(), isMainFrame: false})) {
|
||||||
log.warn('screen share permissions disallowed', view.webContentsId, view.view.server.url.toString());
|
throw new Error('permissions denied');
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenPermissionsErrArgs = ['screen-permissions', this.callID];
|
const screenPermissionsErrArgs = ['screen-permissions', this.callID];
|
||||||
|
@ -425,10 +424,7 @@ export class CallsWidgetWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasScreenPermissions || !sources.length) {
|
if (!hasScreenPermissions || !sources.length) {
|
||||||
log.info('missing screen permissions');
|
throw new Error('handleGetDesktopSources: permissions denied');
|
||||||
view.sendToRenderer(CALLS_ERROR, ...screenPermissionsErrArgs);
|
|
||||||
this.win?.webContents.send(CALLS_ERROR, ...screenPermissionsErrArgs);
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = sources.map((source) => {
|
const message = sources.map((source) => {
|
||||||
|
@ -441,12 +437,14 @@ export class CallsWidgetWindow {
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
log.error('desktopCapturer.getSources failed', err);
|
// Only send calls error if this window has been initialized (i.e. we are in a call).
|
||||||
|
// The rest of the logic is shared so that other plugins can request screen sources.
|
||||||
|
if (this.callID) {
|
||||||
|
view.sendToRenderer(CALLS_ERROR, ...screenPermissionsErrArgs);
|
||||||
|
this.win?.webContents.send(CALLS_ERROR, ...screenPermissionsErrArgs);
|
||||||
|
}
|
||||||
|
|
||||||
view.sendToRenderer(CALLS_ERROR, ...screenPermissionsErrArgs);
|
throw new Error(`handleGetDesktopSources: desktopCapturer.getSources failed: ${err}`);
|
||||||
this.win?.webContents.send(CALLS_ERROR, ...screenPermissionsErrArgs);
|
|
||||||
|
|
||||||
return [];
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
36
src/renderer/components/DeveloperModeIndicator.tsx
Normal file
36
src/renderer/components/DeveloperModeIndicator.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import 'renderer/css/components/DeveloperModeIndicator.scss';
|
||||||
|
|
||||||
|
export default function DeveloperModeIndicator({developerMode, darkMode}: {developerMode: boolean; darkMode: boolean}) {
|
||||||
|
if (!developerMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement='left'
|
||||||
|
overlay={
|
||||||
|
<Tooltip id='DeveloperModeIndicator__tooltip'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='renderer.components.developerModeIndicator.tooltip'
|
||||||
|
defaultMessage='Developer mode is enabled. You should only have this enabled if a Mattermost developer has instructed you to.'
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={classNames('DeveloperModeIndicator', {darkMode})}>
|
||||||
|
<i className='icon-flask-outline'/>
|
||||||
|
<span className='DeveloperModeIndicator__badge'/>
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {injectIntl} from 'react-intl';
|
||||||
import type {UniqueView, UniqueServer} from 'types/config';
|
import type {UniqueView, UniqueServer} from 'types/config';
|
||||||
import type {DownloadedItems} from 'types/downloads';
|
import type {DownloadedItems} from 'types/downloads';
|
||||||
|
|
||||||
|
import DeveloperModeIndicator from './DeveloperModeIndicator';
|
||||||
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
|
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
|
||||||
import ErrorView from './ErrorView';
|
import ErrorView from './ErrorView';
|
||||||
import ExtraBar from './ExtraBar';
|
import ExtraBar from './ExtraBar';
|
||||||
|
@ -55,6 +56,7 @@ type State = {
|
||||||
showDownloadsBadge: boolean;
|
showDownloadsBadge: boolean;
|
||||||
hasDownloads: boolean;
|
hasDownloads: boolean;
|
||||||
threeDotsIsFocused: boolean;
|
threeDotsIsFocused: boolean;
|
||||||
|
developerMode: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabViewStatus = {
|
type TabViewStatus = {
|
||||||
|
@ -88,6 +90,7 @@ class MainPage extends React.PureComponent<Props, State> {
|
||||||
showDownloadsBadge: false,
|
showDownloadsBadge: false,
|
||||||
hasDownloads: false,
|
hasDownloads: false,
|
||||||
threeDotsIsFocused: false,
|
threeDotsIsFocused: false,
|
||||||
|
developerMode: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,6 +266,10 @@ class MainPage extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('click', this.handleCloseDropdowns);
|
window.addEventListener('click', this.handleCloseDropdowns);
|
||||||
|
|
||||||
|
window.desktop.isDeveloperModeEnabled().then((developerMode) => {
|
||||||
|
this.setState({developerMode});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -471,6 +478,10 @@ class MainPage extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tabsRow}
|
{tabsRow}
|
||||||
|
<DeveloperModeIndicator
|
||||||
|
darkMode={this.props.darkMode}
|
||||||
|
developerMode={this.state.developerMode}
|
||||||
|
/>
|
||||||
{downloadsDropdownButton}
|
{downloadsDropdownButton}
|
||||||
{window.process.platform !== 'darwin' && this.state.fullScreen &&
|
{window.process.platform !== 'darwin' && this.state.fullScreen &&
|
||||||
<span className='title-bar-btns'>
|
<span className='title-bar-btns'>
|
||||||
|
|
54
src/renderer/css/components/DeveloperModeIndicator.scss
Normal file
54
src/renderer/css/components/DeveloperModeIndicator.scss
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
.DeveloperModeIndicator {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px;
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: rgba(63, 67, 80, 0.56);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
i {
|
||||||
|
color: rgba(63, 67, 80, 0.78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.DeveloperModeIndicator__badge {
|
||||||
|
background: rgba(210, 75, 78, 1);
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.darkMode {
|
||||||
|
i {
|
||||||
|
color: rgba(221, 223, 228, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
i {
|
||||||
|
color: rgba(221, 223, 228, 0.78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#DeveloperModeIndicator__tooltip {
|
||||||
|
> .tooltip-inner {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,3 +8,12 @@ export type SaveQueueItem = {
|
||||||
key: keyof CombinedConfig;
|
key: keyof CombinedConfig;
|
||||||
data: CombinedConfig[keyof CombinedConfig];
|
data: CombinedConfig[keyof CombinedConfig];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeveloperSettings = {
|
||||||
|
browserOnly?: boolean;
|
||||||
|
disableNotificationStorage?: boolean;
|
||||||
|
disableUserActivityMonitor?: boolean;
|
||||||
|
disableContextMenu?: boolean;
|
||||||
|
forceLegacyAPI?: boolean;
|
||||||
|
forceNewAPI?: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -45,6 +45,7 @@ declare global {
|
||||||
checkForUpdates: () => void;
|
checkForUpdates: () => void;
|
||||||
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
||||||
getNonce: () => Promise<string | undefined>;
|
getNonce: () => Promise<string | undefined>;
|
||||||
|
isDeveloperModeEnabled: () => Promise<boolean>;
|
||||||
|
|
||||||
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
||||||
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
||||||
|
|
Loading…
Reference in a new issue