DEV: Make capabilities available outside of application instance (#22516)
Browser capabilities are inherently unconnected to the lifecycle of our app. Making them formally available outside of the service means that they can safely be used in non-app-linked functions without needing risky hacks like `helperContext()` or `discourse-common/lib/get-owner`. One example of where the old hacks were problematic is the `translateModKey()` utility function. This is called in the root of the `discourse/components/modal/keyboard-shortcuts-help` es6 module. If anything (e.g. a theme/plugin) caused that es6 module to be `require()`d before the application was booted, a fatal error would occur. Following this commit, `translateModKey()` can safely import and access `capabilities` without needing to worry about the app lifecycle. The only potential downside to this approach is that the capabilities data now persists across tests. If any tests need to 'stub' capabilities, they will need to revert their changes at the end of the test (e.g. by using Sinon to stub a property). This commit also updates some legacy references from `capabilities:main` to `service:capabilities`.
This commit is contained in:
parent
2fde58def4
commit
fb9948c79c
|
@ -13,7 +13,7 @@ import domUtils from "discourse-common/utils/dom-utils";
|
||||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { headerOffset } from "discourse/lib/offset-calculator";
|
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||||
import { helperContext } from "discourse-common/lib/helpers";
|
import { capabilities } from "discourse/services/capabilities";
|
||||||
|
|
||||||
let extraKeyboardShortcutsHelp = {};
|
let extraKeyboardShortcutsHelp = {};
|
||||||
function addExtraKeyboardShortcutHelp(help) {
|
function addExtraKeyboardShortcutHelp(help) {
|
||||||
|
@ -876,13 +876,13 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
webviewKeyboardBack() {
|
webviewKeyboardBack() {
|
||||||
if (helperContext().capabilities.isAppWebview) {
|
if (capabilities.isAppWebview) {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
webviewKeyboardForward() {
|
webviewKeyboardForward() {
|
||||||
if (helperContext().capabilities.isAppWebview) {
|
if (capabilities.isAppWebview) {
|
||||||
window.history.forward();
|
window.history.forward();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,10 +2,10 @@ import getURL from "discourse-common/lib/get-url";
|
||||||
import Handlebars from "handlebars";
|
import Handlebars from "handlebars";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { escape } from "pretty-text/sanitizer";
|
import { escape } from "pretty-text/sanitizer";
|
||||||
import { helperContext } from "discourse-common/lib/helpers";
|
|
||||||
import toMarkdown from "discourse/lib/to-markdown";
|
import toMarkdown from "discourse/lib/to-markdown";
|
||||||
import deprecated from "discourse-common/lib/deprecated";
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
import * as AvatarUtils from "discourse-common/lib/avatar-utils";
|
import * as AvatarUtils from "discourse-common/lib/avatar-utils";
|
||||||
|
import { capabilities } from "discourse/services/capabilities";
|
||||||
|
|
||||||
let _defaultHomepage;
|
let _defaultHomepage;
|
||||||
|
|
||||||
|
@ -268,7 +268,7 @@ export function determinePostReplaceSelection({
|
||||||
export function isAppleDevice() {
|
export function isAppleDevice() {
|
||||||
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
|
// IE has no DOMNodeInserted so can not get this hack despite saying it is like iPhone
|
||||||
// This will apply hack on all iDevices
|
// This will apply hack on all iDevices
|
||||||
let caps = helperContext().capabilities;
|
let caps = capabilities;
|
||||||
return caps.isIOS && !window.navigator.userAgent.match(/Trident/g);
|
return caps.isIOS && !window.navigator.userAgent.match(/Trident/g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,7 +451,7 @@ export function modKeysPressed(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function translateModKey(string) {
|
export function translateModKey(string) {
|
||||||
const { isApple } = helperContext().capabilities;
|
const { isApple } = capabilities;
|
||||||
// Apple device users are used to glyphs for shortcut keys
|
// Apple device users are used to glyphs for shortcut keys
|
||||||
if (isApple) {
|
if (isApple) {
|
||||||
string = string
|
string = string
|
||||||
|
|
|
@ -1,47 +1,54 @@
|
||||||
import Service from "@ember/service";
|
|
||||||
|
|
||||||
const APPLE_NAVIGATOR_PLATFORMS = /iPhone|iPod|iPad|Macintosh|MacIntel/;
|
const APPLE_NAVIGATOR_PLATFORMS = /iPhone|iPod|iPad|Macintosh|MacIntel/;
|
||||||
const APPLE_USER_AGENT_DATA_PLATFORM = /macOS/;
|
const APPLE_USER_AGENT_DATA_PLATFORM = /macOS/;
|
||||||
|
|
||||||
// Lets us know about browser's capabilities
|
function calculateCapabilities() {
|
||||||
export default class Capabilities extends Service {
|
const capabilities = {};
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
|
|
||||||
const ua = navigator.userAgent;
|
const ua = navigator.userAgent;
|
||||||
|
|
||||||
this.touch = navigator.maxTouchPoints > 1 || "ontouchstart" in window;
|
capabilities.touch = navigator.maxTouchPoints > 1 || "ontouchstart" in window;
|
||||||
|
|
||||||
this.isAndroid = ua.includes("Android");
|
capabilities.isAndroid = ua.includes("Android");
|
||||||
this.isWinphone = ua.includes("Windows Phone");
|
capabilities.isWinphone = ua.includes("Windows Phone");
|
||||||
this.isIpadOS =
|
capabilities.isIpadOS =
|
||||||
ua.includes("Mac OS") && !/iPhone|iPod/.test(ua) && this.touch;
|
ua.includes("Mac OS") && !/iPhone|iPod/.test(ua) && capabilities.touch;
|
||||||
this.isIOS =
|
capabilities.isIOS =
|
||||||
(/iPhone|iPod/.test(navigator.userAgent) || this.isIpadOS) &&
|
(/iPhone|iPod/.test(navigator.userAgent) || capabilities.isIpadOS) &&
|
||||||
!window.MSStream;
|
!window.MSStream;
|
||||||
this.isApple =
|
capabilities.isApple =
|
||||||
APPLE_NAVIGATOR_PLATFORMS.test(navigator.platform) ||
|
APPLE_NAVIGATOR_PLATFORMS.test(navigator.platform) ||
|
||||||
(navigator.userAgentData &&
|
(navigator.userAgentData &&
|
||||||
APPLE_USER_AGENT_DATA_PLATFORM.test(navigator.userAgentData.platform));
|
APPLE_USER_AGENT_DATA_PLATFORM.test(navigator.userAgentData.platform));
|
||||||
|
|
||||||
this.isOpera = !!window.opera || ua.includes(" OPR/");
|
capabilities.isOpera = !!window.opera || ua.includes(" OPR/");
|
||||||
this.isFirefox = ua.includes("Firefox");
|
capabilities.isFirefox = ua.includes("Firefox");
|
||||||
this.isChrome = !!window.chrome && !this.isOpera;
|
capabilities.isChrome = !!window.chrome && !capabilities.isOpera;
|
||||||
this.isSafari =
|
capabilities.isSafari =
|
||||||
/Constructor/.test(window.HTMLElement) ||
|
/Constructor/.test(window.HTMLElement) ||
|
||||||
window.safari?.pushNotification?.toString() ===
|
window.safari?.pushNotification?.toString() ===
|
||||||
"[object SafariRemoteNotification]";
|
"[object SafariRemoteNotification]";
|
||||||
|
|
||||||
this.hasContactPicker =
|
capabilities.hasContactPicker =
|
||||||
"contacts" in navigator && "ContactsManager" in window;
|
"contacts" in navigator && "ContactsManager" in window;
|
||||||
this.canVibrate = "vibrate" in navigator;
|
capabilities.canVibrate = "vibrate" in navigator;
|
||||||
this.isPwa =
|
capabilities.isPwa =
|
||||||
window.matchMedia("(display-mode: standalone)").matches ||
|
window.matchMedia("(display-mode: standalone)").matches ||
|
||||||
window.navigator.standalone ||
|
window.navigator.standalone ||
|
||||||
document.referrer.includes("android-app://");
|
document.referrer.includes("android-app://");
|
||||||
this.isiOSPWA = this.isPwa && this.isIOS;
|
capabilities.isiOSPWA = capabilities.isPwa && capabilities.isIOS;
|
||||||
this.wasLaunchedFromDiscourseHub =
|
capabilities.wasLaunchedFromDiscourseHub =
|
||||||
window.location.search.includes("discourse_app=1");
|
window.location.search.includes("discourse_app=1");
|
||||||
this.isAppWebview = window.ReactNativeWebView !== undefined;
|
capabilities.isAppWebview = window.ReactNativeWebView !== undefined;
|
||||||
|
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const capabilities = calculateCapabilities();
|
||||||
|
|
||||||
|
export default class CapabilitiesService {
|
||||||
|
static isServiceFactory = true;
|
||||||
|
|
||||||
|
static create() {
|
||||||
|
return capabilities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@ export default class ComposerController extends Controller {
|
||||||
@service site;
|
@service site;
|
||||||
@service store;
|
@service store;
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
@service capabilities;
|
||||||
|
|
||||||
checkedMessages = false;
|
checkedMessages = false;
|
||||||
messageCount = null;
|
messageCount = null;
|
||||||
|
@ -128,10 +129,6 @@ export default class ComposerController extends Controller {
|
||||||
return getOwner(this).lookup("controller:topic");
|
return getOwner(this).lookup("controller:topic");
|
||||||
}
|
}
|
||||||
|
|
||||||
get capabilities() {
|
|
||||||
return getOwner(this).lookup("capabilities:main");
|
|
||||||
}
|
|
||||||
|
|
||||||
@on("init")
|
@on("init")
|
||||||
_setupPreview() {
|
_setupPreview() {
|
||||||
const val = this.site.mobileView
|
const val = this.site.mobileView
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
query,
|
query,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
import Sinon from "sinon";
|
||||||
|
|
||||||
acceptance(
|
acceptance(
|
||||||
"Sidebar - Logged on user - Legacy navigation menu enabled",
|
"Sidebar - Logged on user - Legacy navigation menu enabled",
|
||||||
|
@ -162,7 +163,7 @@ acceptance(
|
||||||
|
|
||||||
test("button to toggle between mobile and desktop view on touch devices ", async function (assert) {
|
test("button to toggle between mobile and desktop view on touch devices ", async function (assert) {
|
||||||
const capabilities = this.container.lookup("service:capabilities");
|
const capabilities = this.container.lookup("service:capabilities");
|
||||||
capabilities.touch = true;
|
Sinon.stub(capabilities, "touch").value(true);
|
||||||
|
|
||||||
await visit("/");
|
await visit("/");
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { clipboardCopy } from "discourse/lib/utilities";
|
||||||
import ChatMessageReaction, {
|
import ChatMessageReaction, {
|
||||||
REACTIONS,
|
REACTIONS,
|
||||||
} from "discourse/plugins/chat/discourse/models/chat-message-reaction";
|
} from "discourse/plugins/chat/discourse/models/chat-message-reaction";
|
||||||
import { getOwner, setOwner } from "@ember/application";
|
import { setOwner } from "@ember/application";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||||
|
@ -42,6 +42,7 @@ export default class ChatMessageInteractor {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service site;
|
@service site;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service capabilities;
|
||||||
|
|
||||||
@tracked message = null;
|
@tracked message = null;
|
||||||
@tracked context = null;
|
@tracked context = null;
|
||||||
|
@ -56,10 +57,6 @@ export default class ChatMessageInteractor {
|
||||||
this.cachedFavoritesReactions = this.chatEmojiReactionStore.favorites;
|
this.cachedFavoritesReactions = this.chatEmojiReactionStore.favorites;
|
||||||
}
|
}
|
||||||
|
|
||||||
get capabilities() {
|
|
||||||
return getOwner(this).lookup("capabilities:main");
|
|
||||||
}
|
|
||||||
|
|
||||||
get pane() {
|
get pane() {
|
||||||
return this.context === MESSAGE_CONTEXT_THREAD
|
return this.context === MESSAGE_CONTEXT_THREAD
|
||||||
? this.chatThreadPane
|
? this.chatThreadPane
|
||||||
|
|
Loading…
Reference in New Issue