mirror of
https://github.com/discourse/discourse.git
synced 2026-08-07 03:35:32 -05:00
DEV: Deprecate Site.mobileView/desktopView during initialization (#34122)
This commit introduces deprecation warnings for accessing Site.mobileView or Site.desktopView during application initialization to prevent layout-related errors and improve code reliability. The changes include: * Added deprecation warnings for Site.mobileView and Site.desktopView access during the initialization phase. * Updated multiple plugins and components to avoid these deprecated calls during startup. * Refactored initialization logic across discourse-ai, discourse-chat, discourse-calendar, discourse-reactions, discourse-assign, discourse-subscriptions, and discourse-user-notes plugins * Improved error prevention by discouraging early access to view-dependent properties before the application is fully initialized * Enhanced code maintainability by establishing clearer boundaries between initialization and runtime phases This deprecation helps prevent subtle bugs that can occur when components try to determine the view type before the application context is properly established, leading to more robust plugin initialization patterns.
This commit is contained in:
@@ -12,7 +12,7 @@ export default class AdminEmbeddingIndexController extends Controller {
|
||||
@alias("adminEmbedding.embedding") embedding;
|
||||
|
||||
get showEmbeddingCode() {
|
||||
return !this.site.isMobileDevice;
|
||||
return this.site.desktopView;
|
||||
}
|
||||
|
||||
@discourseComputed("embedding.base_url")
|
||||
|
||||
@@ -231,7 +231,7 @@ export default class BookmarkMenu extends Component {
|
||||
}
|
||||
|
||||
async _openBookmarkModal() {
|
||||
this.dMenu.close();
|
||||
await this.dMenu.close();
|
||||
|
||||
try {
|
||||
const closeData = await this.modal.show(BookmarkModal, {
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class DModal extends Component {
|
||||
});
|
||||
|
||||
setupModalBody = modifierFn((el) => {
|
||||
if (!this.site.mobileView) {
|
||||
if (this.site.desktopView) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const BOOKMARK_BINDINGS = {
|
||||
export default class BookmarkModal extends Component {
|
||||
@service dialog;
|
||||
@service currentUser;
|
||||
@service site;
|
||||
@service capabilities;
|
||||
@service bookmarkApi;
|
||||
|
||||
@tracked postDetectedLocalDate = null;
|
||||
@@ -149,7 +149,7 @@ export default class BookmarkModal extends Component {
|
||||
@action
|
||||
didInsert() {
|
||||
discourseLater(() => {
|
||||
if (this.site.isMobileDevice) {
|
||||
if (this.capabilities.isMobileDevice) {
|
||||
document.getElementById("bookmark-name").blur();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -251,7 +251,7 @@ export default class FeatureTopic extends Component {
|
||||
<p>
|
||||
{{i18n "topic.feature_topic.pin_note"}}
|
||||
</p>
|
||||
{{#if this.site.isMobileDevice}}
|
||||
{{#if this.site.mobileView}}
|
||||
<p>
|
||||
{{htmlSafe this.pinMessage}}
|
||||
</p>
|
||||
@@ -325,7 +325,7 @@ export default class FeatureTopic extends Component {
|
||||
<p>
|
||||
{{i18n "topic.feature_topic.global_pin_note"}}
|
||||
</p>
|
||||
{{#if this.site.isMobileDevice}}
|
||||
{{#if this.site.mobileView}}
|
||||
<p>
|
||||
{{i18n "topic.feature_topic.pin_globally"}}
|
||||
</p>
|
||||
|
||||
@@ -8,6 +8,7 @@ import highlightSyntax from "discourse/lib/highlight-syntax";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class FullscreenCode extends Component {
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service session;
|
||||
|
||||
@@ -23,6 +24,7 @@ export default class FullscreenCode extends Component {
|
||||
highlightSyntax(modalElement, this.siteSettings, this.session);
|
||||
|
||||
this.codeBlockButtons = new CodeblockButtons({
|
||||
site: this.site,
|
||||
showFullscreen: false,
|
||||
showCopy: true,
|
||||
});
|
||||
|
||||
@@ -277,7 +277,7 @@ export default class PostTextSelection extends Component {
|
||||
get shouldRenderUnder() {
|
||||
const { isIOS, isAndroid, isOpera, isFirefox, touch } = this.capabilities;
|
||||
return (
|
||||
this.site.isMobileDevice ||
|
||||
this.capabilities.isMobileDevice ||
|
||||
isIOS ||
|
||||
isAndroid ||
|
||||
isOpera ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { service } from "@ember/service";
|
||||
import { and, or } from "truth-helpers";
|
||||
import GroupLink from "discourse/components/group-link";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import PostMetaDataPosterNameIcon from "discourse/components/post/meta-data/poster-name/icon";
|
||||
import UserBadge from "discourse/components/user-badge";
|
||||
import UserLink from "discourse/components/user-link";
|
||||
import UserStatusMessage from "discourse/components/user-status-message";
|
||||
@@ -19,6 +20,7 @@ import { formatUsername } from "discourse/lib/utilities";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class PostMetaDataPosterName extends Component {
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service userStatus;
|
||||
|
||||
@@ -92,6 +94,14 @@ export default class PostMetaDataPosterName extends Component {
|
||||
return this.userStatus.isEnabled && this.user.status;
|
||||
}
|
||||
|
||||
get shouldDisplayIconsBefore() {
|
||||
return this.site.mobileView;
|
||||
}
|
||||
|
||||
get shouldDisplayIconsAfter() {
|
||||
return !this.shouldDisplayIconsBefore;
|
||||
}
|
||||
|
||||
@bind
|
||||
withBadgeDescription(badge) {
|
||||
// Alter the badge description to show that the badge was granted for this post.
|
||||
@@ -133,6 +143,9 @@ export default class PostMetaDataPosterName extends Component {
|
||||
@name="post-meta-data-poster-name"
|
||||
@outletArgs={{lazyHash post=@post user=this.user}}
|
||||
>
|
||||
{{#if this.shouldDisplayIconsBefore}}
|
||||
<PostMetaDataPosterNameIcons @post={{@post}} />
|
||||
{{/if}}
|
||||
<span
|
||||
class={{concatClass
|
||||
"first"
|
||||
@@ -237,8 +250,33 @@ export default class PostMetaDataPosterName extends Component {
|
||||
</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if this.shouldDisplayIconsAfter}}
|
||||
<PostMetaDataPosterNameIcons @post={{@post}} />
|
||||
{{/if}}
|
||||
</PluginOutlet>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
class PostMetaDataPosterNameIcons extends Component {
|
||||
get definitions() {
|
||||
return applyValueTransformer("poster-name-icons", [], {
|
||||
post: this.args.post,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#each this.definitions as |definition|}}
|
||||
<PostMetaDataPosterNameIcon
|
||||
@className={{definition.className}}
|
||||
@emoji={{definition.emoji}}
|
||||
@emojiTitle={{definition.emojiTitle}}
|
||||
@icon={{definition.icon}}
|
||||
@text={{definition.text}}
|
||||
@title={{definition.title}}
|
||||
@url={{definition.url}}
|
||||
/>
|
||||
{{/each}}
|
||||
</template>
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class TopicMapSummary extends Component {
|
||||
this.args.topic.posts_count >= MIN_POSTS_COUNT &&
|
||||
this.args.topicDetails.participants?.length >=
|
||||
MIN_USERS_COUNT_FOR_AVATARS &&
|
||||
!this.site.mobileView
|
||||
this.site.desktopView
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ let _codeblockButtons = [];
|
||||
|
||||
export default {
|
||||
initialize(owner) {
|
||||
const site = owner.lookup("service:site");
|
||||
const siteSettings = owner.lookup("service:site-settings");
|
||||
|
||||
withPluginApi((api) => {
|
||||
@@ -25,6 +26,7 @@ export default {
|
||||
|
||||
const post = helper.getModel();
|
||||
const cb = new CodeblockButtons({
|
||||
site,
|
||||
showFullscreen: true,
|
||||
showCopy: true,
|
||||
});
|
||||
|
||||
@@ -203,11 +203,7 @@ export default {
|
||||
|
||||
table.parentNode.insertBefore(buttonWrapper, table);
|
||||
|
||||
if (!isOverflown(table.parentNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (site.isMobileDevice) {
|
||||
if (site.mobileView || !isOverflown(table.parentNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -18,14 +18,15 @@ import { currentThemeId } from "discourse/lib/theme-selector";
|
||||
import Notification from "discourse/models/notification";
|
||||
|
||||
class SubscribeUserNotificationsInit {
|
||||
@service appEvents;
|
||||
@service capabilities;
|
||||
@service currentUser;
|
||||
@service messageBus;
|
||||
@service pmTopicTrackingState;
|
||||
@service store;
|
||||
@service appEvents;
|
||||
@service siteSettings;
|
||||
@service site;
|
||||
@service router;
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@service store;
|
||||
|
||||
constructor(owner) {
|
||||
setOwner(this, owner);
|
||||
@@ -272,7 +273,7 @@ class SubscribeUserNotificationsInit {
|
||||
|
||||
@bind
|
||||
onAlert(data) {
|
||||
if (this.site.desktopView) {
|
||||
if (!this.capabilities.isMobileDevice) {
|
||||
return onDesktopNotification(
|
||||
data,
|
||||
this.siteSettings,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { bind } from "discourse/lib/decorators";
|
||||
import { getOwnerWithFallback } from "discourse/lib/get-owner";
|
||||
import { iconHTML } from "discourse/lib/icon-library";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
import Mobile from "discourse/lib/mobile";
|
||||
import { clipboardCopy } from "discourse/lib/utilities";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@@ -32,9 +31,13 @@ import { i18n } from "discourse-i18n";
|
||||
// Make sure to run .cleanup() on the instance once you are done to
|
||||
// remove click events.
|
||||
export default class CodeblockButtons {
|
||||
#site;
|
||||
|
||||
constructor(opts = {}) {
|
||||
this._codeblockButtonClickHandlers = {};
|
||||
this._fadeCopyCodeblocksRunners = {};
|
||||
this.#site = opts.site;
|
||||
|
||||
opts = Object.assign(
|
||||
{
|
||||
showFullscreen: true,
|
||||
@@ -121,7 +124,7 @@ export default class CodeblockButtons {
|
||||
}px`;
|
||||
}
|
||||
|
||||
if (this.showFullscreen && !Mobile.isMobileDevice) {
|
||||
if (this.#site?.desktopView && this.showFullscreen) {
|
||||
const fullscreenButton = document.createElement("button");
|
||||
fullscreenButton.classList.add(
|
||||
"btn",
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
import { isTesting } from "discourse/lib/environment";
|
||||
import { getOwnerWithFallback } from "discourse/lib/get-owner";
|
||||
|
||||
let mobileForced = false;
|
||||
|
||||
// An object that is responsible for logic related to mobile devices.
|
||||
const Mobile = {
|
||||
isMobileDevice: false,
|
||||
mobileView: false,
|
||||
|
||||
init() {
|
||||
const documentClassList = document.documentElement.classList;
|
||||
this.isMobileDevice =
|
||||
mobileForced || documentClassList.contains("mobile-device");
|
||||
this.mobileView = mobileForced || documentClassList.contains("mobile-view");
|
||||
|
||||
if (isTesting() || mobileForced) {
|
||||
@@ -33,6 +32,20 @@ const Mobile = {
|
||||
}
|
||||
},
|
||||
|
||||
get mobileForced() {
|
||||
return mobileForced;
|
||||
},
|
||||
|
||||
get isMobileDevice() {
|
||||
deprecated(
|
||||
"`Mobile.isMobileDevice` is deprecated. Use `capabilities.isMobileDevice` instead.",
|
||||
{ id: "discourse.site.is-mobile-device", since: "3.5.0.beta9-dev" }
|
||||
);
|
||||
|
||||
return getOwnerWithFallback(this).lookup("service:capabilities")
|
||||
.isMobileDevice;
|
||||
},
|
||||
|
||||
maybeReload() {
|
||||
if (localStorage.mobileView) {
|
||||
let savedValue = localStorage.mobileView === "true";
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
export const PLUGIN_API_VERSION = "2.1.1";
|
||||
|
||||
import Component from "@glimmer/component";
|
||||
import $ from "jquery";
|
||||
import { h } from "virtual-dom";
|
||||
import { addAboutPageActivity } from "discourse/components/about-page";
|
||||
@@ -27,7 +26,6 @@ import { headerIconsDAG } from "discourse/components/header/icons";
|
||||
import { registeredTabs } from "discourse/components/more-topics";
|
||||
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
||||
import { addPluginOutletDecorator } from "discourse/components/plugin-connector";
|
||||
import PostMetaDataPosterNameIcon from "discourse/components/post/meta-data/poster-name/icon";
|
||||
import { addGroupPostSmallActionCode } from "discourse/components/post/small-action";
|
||||
import {
|
||||
addPluginReviewableParam,
|
||||
@@ -700,49 +698,28 @@ class PluginApi {
|
||||
* ```
|
||||
**/
|
||||
addPosterIcons(cb) {
|
||||
const site = this._lookupContainer("service:site");
|
||||
const loc = site && site.mobileView ? "before" : "after";
|
||||
this.registerValueTransformer(
|
||||
"poster-name-icons",
|
||||
({ value, context: { post } }) => {
|
||||
// `cb` is called with the post's user custom fields and post attributes
|
||||
// and should return an array of icon definitions.
|
||||
const definitions = makeArray(cb(post.user_custom_fields || {}, post));
|
||||
|
||||
const IconsComponent = class extends Component {
|
||||
get definitions() {
|
||||
return makeArray(
|
||||
cb(
|
||||
this.args.outletArgs.post.user_custom_fields || {},
|
||||
this.args.outletArgs.post
|
||||
)
|
||||
);
|
||||
return makeArray(value).concat(definitions).filter(Boolean);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#each this.definitions as |definition|}}
|
||||
<PostMetaDataPosterNameIcon
|
||||
@className={{definition.className}}
|
||||
@emoji={{definition.emoji}}
|
||||
@emojiTitle={{definition.emojiTitle}}
|
||||
@icon={{definition.icon}}
|
||||
@text={{definition.text}}
|
||||
@title={{definition.title}}
|
||||
@url={{definition.url}}
|
||||
/>
|
||||
{{/each}}
|
||||
</template>
|
||||
};
|
||||
|
||||
if (loc === "after") {
|
||||
this.renderAfterWrapperOutlet(
|
||||
"post-meta-data-poster-name",
|
||||
IconsComponent
|
||||
);
|
||||
} else {
|
||||
this.renderBeforeWrapperOutlet(
|
||||
"post-meta-data-poster-name",
|
||||
IconsComponent
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// TODO (glimmer-post-stream): remove the fallback when removing the legacy post stream code
|
||||
withSilencedDeprecations(POST_STREAM_DEPRECATION_OPTIONS.id, () => {
|
||||
decorateWidget(`poster-name:${loc}`, (dec) => {
|
||||
const decoratorFor = (view) => (dec) => {
|
||||
const currentView = this.container.lookup("service:site").mobileView
|
||||
? "mobile"
|
||||
: "desktop";
|
||||
|
||||
if (view !== currentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attrs = dec.attrs;
|
||||
let results = cb(attrs.userCustomFields || {}, attrs);
|
||||
|
||||
@@ -788,7 +765,10 @@ class PluginApi {
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
decorateWidget(`poster-name:before`, decoratorFor("mobile"));
|
||||
decorateWidget(`poster-name:after`, decoratorFor("desktop"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2150,13 +2130,16 @@ class PluginApi {
|
||||
*
|
||||
* ```
|
||||
* const IconWithDropdown = <template>
|
||||
* <DMenu @icon="foo" title={{i18n "title"}}>
|
||||
* <:content as |args|>
|
||||
* dropdown content here
|
||||
* <DButton @action={{args.close}} @icon="bar" />
|
||||
* </:content>
|
||||
* </DMenu>
|
||||
* </template>;
|
||||
*
|
||||
<DMenu @icon="foo" title={{i18n "title"}}>
|
||||
*
|
||||
<:content as |args|>
|
||||
* dropdown content here
|
||||
*
|
||||
<DButton @action={{args.close}} @icon="bar" />
|
||||
* </:content>
|
||||
* </DMenu>
|
||||
* </template>;
|
||||
*
|
||||
* api.headerIcons.add("icon-name", IconWithDropdown, { before: "search" })
|
||||
* ```
|
||||
|
||||
@@ -25,11 +25,11 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
||||
"composer-service-cannot-submit-post",
|
||||
"composer-toggles-class",
|
||||
"create-topic-label",
|
||||
"flag-button-render-decision",
|
||||
"flag-button-dynamic-class",
|
||||
"flag-button-disabled-state",
|
||||
"flag-description",
|
||||
"flag-button-dynamic-class",
|
||||
"flag-button-render-decision",
|
||||
"flag-custom-placeholder",
|
||||
"flag-description",
|
||||
"flag-formatted-name",
|
||||
"hamburger-dropdown-click-outside-exceptions",
|
||||
"header-notifications-avatar-size",
|
||||
@@ -65,8 +65,9 @@ export const VALUE_TRANSFORMERS = Object.freeze([
|
||||
"post-small-action-class",
|
||||
"post-small-action-custom-component",
|
||||
"post-small-action-icon",
|
||||
"poster-name-user-title",
|
||||
"poster-name-class",
|
||||
"poster-name-icons",
|
||||
"poster-name-user-title",
|
||||
"quote-params",
|
||||
"small-user-attrs",
|
||||
"tag-separator",
|
||||
|
||||
@@ -484,7 +484,7 @@ export default class UppyComposerUpload {
|
||||
optionsResolverFn({
|
||||
composerModel: this.composerModel,
|
||||
capabilities: this.capabilities,
|
||||
isMobileDevice: this.site.isMobileDevice,
|
||||
isMobileDevice: this.capabilities.isMobileDevice,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import discourseComputed from "discourse/lib/decorators";
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
import deprecated, { withSilencedDeprecations } from "discourse/lib/deprecated";
|
||||
import { isRailsTesting, isTesting } from "discourse/lib/environment";
|
||||
import { getOwnerWithFallback } from "discourse/lib/get-owner";
|
||||
import Mobile from "discourse/lib/mobile";
|
||||
@@ -92,6 +92,7 @@ export default class Site extends RestModel {
|
||||
@sort("categories", "topicCountDesc") categoriesByCount;
|
||||
|
||||
#glimmerPostStreamEnabled;
|
||||
#siteInitialized = false;
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
@@ -107,8 +108,37 @@ export default class Site extends RestModel {
|
||||
|
||||
@dependentKeyCompat
|
||||
get mobileView() {
|
||||
this.#siteInitialized ||= getOwnerWithFallback(this).lookup(
|
||||
"-application-instance:main"
|
||||
)?._booted;
|
||||
|
||||
if (!this.#siteInitialized) {
|
||||
if (isTesting() || isRailsTesting()) {
|
||||
throw new Error(
|
||||
"Accessing `site.mobileView` or `site.desktopView` during the site initialization phase. " +
|
||||
"Move these checks to a component, transformer, or API callback that executes during page rendering."
|
||||
);
|
||||
}
|
||||
|
||||
deprecated(
|
||||
"Accessing `site.mobileView` or `site.desktopView` during the site initialization phase is deprecated. " +
|
||||
"In future updates, the mobile mode will be determined by the viewport size and as consequence using " +
|
||||
"these values during initialization can lead to errors and inconsistencies when the browser window is " +
|
||||
"resized. Please move these checks to a component, transformer, or API callback that executes during page" +
|
||||
" rendering.",
|
||||
{
|
||||
since: "3.5.0.beta9-dev",
|
||||
id: "discourse.static-viewport-initialization",
|
||||
url: "https://meta.discourse.org/t/367810",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.siteSettings.viewport_based_mobile_mode) {
|
||||
return !this.capabilities.viewport.sm;
|
||||
return withSilencedDeprecations(
|
||||
"discourse.static-viewport-initialization",
|
||||
() => !this.capabilities.viewport.sm
|
||||
);
|
||||
} else {
|
||||
return Mobile.mobileView;
|
||||
}
|
||||
@@ -116,6 +146,16 @@ export default class Site extends RestModel {
|
||||
|
||||
@dependentKeyCompat
|
||||
get isMobileDevice() {
|
||||
deprecated(
|
||||
"Site.isMobileDevice is deprecated. Use `site.mobileView` and `site.desktopView` instead for " +
|
||||
"viewport-based values or `capabilities.isMobileDevice` for user-agent based detection.",
|
||||
{
|
||||
id: "discourse.site.is-mobile-device",
|
||||
since: "3.5.0.beta9-dev",
|
||||
url: "https://meta.discourse.org/t/367810",
|
||||
}
|
||||
);
|
||||
|
||||
return this.mobileView;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export default class SwipeModifier extends Modifier {
|
||||
lockBody,
|
||||
}
|
||||
) {
|
||||
if (enabled === false || !this.site.mobileView) {
|
||||
if (enabled === false || this.site.desktopView) {
|
||||
this.enabled = enabled;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
import { isRailsTesting, isTesting } from "discourse/lib/environment";
|
||||
import { getOwnerWithFallback } from "discourse/lib/get-owner";
|
||||
import Mobile from "discourse/lib/mobile";
|
||||
import TrackedMediaQuery from "discourse/lib/tracked-media-query";
|
||||
|
||||
const APPLE_NAVIGATOR_PLATFORMS = /iPhone|iPod|iPad|Macintosh|MacIntel/;
|
||||
@@ -5,17 +9,10 @@ const APPLE_USER_AGENT_DATA_PLATFORM = /macOS/;
|
||||
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
// Values match those in viewport.scss
|
||||
const breakpointQueries = {
|
||||
sm: new TrackedMediaQuery("(min-width: 40rem)"),
|
||||
md: new TrackedMediaQuery("(min-width: 48rem)"),
|
||||
lg: new TrackedMediaQuery("(min-width: 64rem)"),
|
||||
xl: new TrackedMediaQuery("(min-width: 80rem)"),
|
||||
"2xl": new TrackedMediaQuery("(min-width: 96rem)"),
|
||||
};
|
||||
|
||||
const anyPointerCourseQuery = new TrackedMediaQuery("(any-pointer: coarse)");
|
||||
|
||||
let siteInitialized = false;
|
||||
|
||||
class Capabilities {
|
||||
isAndroid = ua.includes("Android");
|
||||
isWinphone = ua.includes("Windows Phone");
|
||||
@@ -48,23 +45,69 @@ class Capabilities {
|
||||
window.location.search.includes("discourse_app=1");
|
||||
isAppWebview = window.ReactNativeWebView !== undefined;
|
||||
|
||||
viewport = {
|
||||
get sm() {
|
||||
return breakpointQueries.sm.matches;
|
||||
},
|
||||
get md() {
|
||||
return breakpointQueries.md.matches;
|
||||
},
|
||||
get lg() {
|
||||
return breakpointQueries.lg.matches;
|
||||
},
|
||||
get xl() {
|
||||
return breakpointQueries.xl.matches;
|
||||
},
|
||||
get "2xl"() {
|
||||
return breakpointQueries["2xl"].matches;
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Defines the responsive viewport breakpoints and their media queries.
|
||||
* Reduces the breakpoint entries into viewport properties that can be accessed
|
||||
* to check if each breakpoint matches the current viewport size.
|
||||
*
|
||||
* @type {Object.<string, boolean>}
|
||||
* @property {boolean} sm - True if viewport width is at least 40rem
|
||||
* @property {boolean} md - True if viewport width is at least 48rem
|
||||
* @property {boolean} lg - True if viewport width is at least 64rem
|
||||
* @property {boolean} xl - True if viewport width is at least 80rem
|
||||
* @property {boolean} 2xl - True if viewport width is at least 96rem
|
||||
* @throws {Error} If accessed during initialization in test environment
|
||||
* @deprecated Using viewport properties during initialization is forbidden
|
||||
*/
|
||||
viewport = Array.from(
|
||||
// Values match those in viewport.scss
|
||||
Object.entries({
|
||||
sm: new TrackedMediaQuery("(min-width: 40rem)"),
|
||||
md: new TrackedMediaQuery("(min-width: 48rem)"),
|
||||
lg: new TrackedMediaQuery("(min-width: 64rem)"),
|
||||
xl: new TrackedMediaQuery("(min-width: 80rem)"),
|
||||
"2xl": new TrackedMediaQuery("(min-width: 96rem)"),
|
||||
})
|
||||
).reduce((obj, [key, breakpointQuery]) => {
|
||||
Object.defineProperty(obj, key, {
|
||||
get() {
|
||||
siteInitialized ||= getOwnerWithFallback(this).lookup(
|
||||
"-application-instance:main"
|
||||
)?._booted;
|
||||
|
||||
if (!siteInitialized) {
|
||||
if (isTesting() || isRailsTesting()) {
|
||||
throw new Error(
|
||||
`Accessing \`capabilities.viewport.${key}\` during the site initialization phase. Move these checks ` +
|
||||
`to a component, transformer, or API callback that executes during page rendering.`
|
||||
);
|
||||
}
|
||||
|
||||
deprecated(
|
||||
`Accessing \`capabilities.viewport.${key}\` during the site initialization phase is not recommended. ` +
|
||||
`Using these values during initialization can lead to errors and inconsistencies when the browser ` +
|
||||
`window is resized. Please move these checks to a component, transformer, or API callback that ` +
|
||||
`executes during page rendering.`,
|
||||
{
|
||||
id: "discourse.static-viewport-initialization",
|
||||
url: "https://meta.discourse.org/t/367810",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return breakpointQuery.matches;
|
||||
},
|
||||
});
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
#isMobileDevice =
|
||||
Mobile.mobileForced || (ua.includes("Mobile") && !ua.includes("iPad"));
|
||||
|
||||
get isMobileDevice() {
|
||||
return this.#isMobileDevice;
|
||||
}
|
||||
|
||||
get touch() {
|
||||
return anyPointerCourseQuery.matches;
|
||||
|
||||
@@ -105,11 +105,6 @@ export default class ComposerService extends Service {
|
||||
@service store;
|
||||
@service toasts;
|
||||
|
||||
@tracked
|
||||
showPreview = this.site.mobileView
|
||||
? false
|
||||
: (this.keyValueStore.get("composer.showPreview") || "true") === "true";
|
||||
|
||||
@tracked allowPreview = false;
|
||||
@tracked selectedTranslationLocale = null;
|
||||
checkedMessages = false;
|
||||
@@ -136,6 +131,21 @@ export default class ComposerService extends Service {
|
||||
@and("model.creatingTopic", "isStaffUser") canUnlistTopic;
|
||||
@or("replyingToWhisper", "model.whisper") isWhispering;
|
||||
|
||||
@tracked _showPreview;
|
||||
|
||||
get showPreview() {
|
||||
return (
|
||||
this._showPreview ??
|
||||
(this.site.mobileView
|
||||
? false
|
||||
: (this.keyValueStore.get("composer.showPreview") || "true") === "true")
|
||||
);
|
||||
}
|
||||
|
||||
set showPreview(value) {
|
||||
this._showPreview = value;
|
||||
}
|
||||
|
||||
get topicController() {
|
||||
return getOwner(this).lookup("controller:topic");
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export default class SelectKitCollection extends Component {
|
||||
@service site;
|
||||
|
||||
bodyScrollLock = modifier((element) => {
|
||||
if (!this.site.mobileView) {
|
||||
if (this.site.desktopView) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MobileDetection
|
||||
# if the criteria for mobile_device? changes, update the code for `mobileDevice` in
|
||||
# `javascripts/discourse/app/lib/mobile.js`
|
||||
def self.mobile_device?(user_agent)
|
||||
user_agent =~ /Mobile/ && !(user_agent =~ /iPad/)
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@ import ChatComposerUpload from "discourse/plugins/chat/discourse/components/chat
|
||||
|
||||
@classNames("chat-composer-uploads")
|
||||
export default class ChatComposerUploads extends Component {
|
||||
@service capabilities;
|
||||
@service mediaOptimizationWorker;
|
||||
|
||||
uppyUpload = new UppyUpload(getOwner(this), {
|
||||
@@ -27,7 +28,7 @@ export default class ChatComposerUploads extends Component {
|
||||
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
|
||||
optimizeFn: (data, opts) =>
|
||||
this.mediaOptimizationWorker.optimizeImage(data, opts),
|
||||
runParallel: !this.site.isMobileDevice,
|
||||
runParallel: !this.capabilities.isMobileDevice,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -66,9 +66,7 @@ class ChatSetupInit {
|
||||
class: "chat-emoji-btn",
|
||||
icon: "face-smile",
|
||||
position: "dropdown",
|
||||
get displayed() {
|
||||
return owner.lookup("service:site").mobileView;
|
||||
},
|
||||
displayed: () => owner.lookup("service:site").mobileView,
|
||||
action(context) {
|
||||
const didSelectEmoji = (emoji) => {
|
||||
const composer = owner.lookup(`service:chat-${context}-composer`);
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
import { isTesting } from "discourse/lib/environment";
|
||||
|
||||
export default class ChatNotificationManager extends Service {
|
||||
@service capabilities;
|
||||
@service chat;
|
||||
@service currentUser;
|
||||
@service appEvents;
|
||||
@service site;
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
@@ -54,6 +54,8 @@ export default class ChatNotificationManager extends Service {
|
||||
}
|
||||
|
||||
get #shouldRun() {
|
||||
return this.site.desktopView && this.chat.userCanChat && !isTesting();
|
||||
return (
|
||||
!this.capabilities.isMobileDevice && this.chat.userCanChat && !isTesting()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const message = {
|
||||
|
||||
acceptance("Discourse Chat - Channel Reactions", function (needs) {
|
||||
needs.user({ has_chat_enabled: true });
|
||||
needs.settings({ chat_enabled: true });
|
||||
needs.settings({ chat_enabled: true, enable_emoji: true });
|
||||
|
||||
needs.hooks.beforeEach(function () {
|
||||
pretender.get("/chat/api/me/channels", () =>
|
||||
|
||||
@@ -19,7 +19,11 @@ const GROUP_NAME = "group1";
|
||||
|
||||
acceptance("Discourse Chat - Composer", function (needs) {
|
||||
needs.user({ has_chat_enabled: true });
|
||||
needs.settings({ chat_enabled: true, enable_rich_text_paste: true });
|
||||
needs.settings({
|
||||
chat_enabled: true,
|
||||
enable_rich_text_paste: true,
|
||||
enable_emoji: true,
|
||||
});
|
||||
needs.pretender((server, helper) => {
|
||||
baseChatPretenders(server, helper);
|
||||
chatChannelPretender(server, helper);
|
||||
@@ -70,7 +74,10 @@ acceptance("Discourse Chat - Composer", function (needs) {
|
||||
let sendAttempt = 0;
|
||||
acceptance("Discourse Chat - Composer - unreliable network", function (needs) {
|
||||
needs.user({ id: 1, has_chat_enabled: true });
|
||||
needs.settings({ chat_enabled: true });
|
||||
needs.settings({
|
||||
chat_enabled: true,
|
||||
enable_emoji: true,
|
||||
});
|
||||
needs.pretender((server, helper) => {
|
||||
chatChannelPretender(server, helper);
|
||||
server.get("/chat/:id/messages.json", () =>
|
||||
|
||||
@@ -12,6 +12,7 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
|
||||
|
||||
needs.settings({
|
||||
chat_enabled: true,
|
||||
enable_emoji: true,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ acceptance(
|
||||
needs.settings({
|
||||
chat_enabled: true,
|
||||
navigation_menu: "legacy",
|
||||
enable_emoji: true,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
|
||||
@@ -23,7 +23,10 @@ acceptance("Chat | Hashtag CSS Generator", function (needs) {
|
||||
name: "category3",
|
||||
};
|
||||
|
||||
needs.settings({ chat_enabled: true });
|
||||
needs.settings({
|
||||
chat_enabled: true,
|
||||
enable_emoji: true,
|
||||
});
|
||||
needs.user({
|
||||
has_chat_enabled: true,
|
||||
});
|
||||
|
||||
+2
-1
@@ -36,6 +36,7 @@ import AiPersonaLlmSelector from "discourse/plugins/discourse-ai/discourse/compo
|
||||
|
||||
export default class AiBotConversations extends Component {
|
||||
@service aiBotConversationsHiddenSubmit;
|
||||
@service capabilities;
|
||||
@service mediaOptimizationWorker;
|
||||
@service site;
|
||||
@service siteSettings;
|
||||
@@ -81,7 +82,7 @@ export default class AiBotConversations extends Component {
|
||||
this.uppyUpload.uppyWrapper.useUploadPlugin(UppyMediaOptimization, {
|
||||
optimizeFn: (data, opts) =>
|
||||
this.mediaOptimizationWorker.optimizeImage(data, opts),
|
||||
runParallel: !this.site.isMobileDevice,
|
||||
runParallel: !this.capabilities.isMobileDevice,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+19
-8
@@ -1,10 +1,10 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { apiInitializer } from "discourse/lib/api";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import AiTopicGist from "../components/ai-topic-gist";
|
||||
|
||||
export default apiInitializer((api) => {
|
||||
const site = api.container.lookup("service:site");
|
||||
const settings = api.container.lookup("service:site-settings");
|
||||
const MAX_ALLOWED_GISTS_REGENERATE = 30;
|
||||
|
||||
@@ -87,15 +87,26 @@ export default apiInitializer((api) => {
|
||||
});
|
||||
|
||||
if (settings.discourse_ai_enabled && settings.ai_summarization_enabled) {
|
||||
const gistTemplate = <template>
|
||||
<AiTopicGist @topic={{@outletArgs.topic}} />
|
||||
</template>;
|
||||
const OUTLETS = {
|
||||
mobile: "topic-list-before-category",
|
||||
desktop: "topic-list-topic-cell-link-bottom-line__before",
|
||||
};
|
||||
|
||||
const outlet = site.mobileView
|
||||
? "topic-list-before-category"
|
||||
: "topic-list-topic-cell-link-bottom-line__before";
|
||||
function renderGistInOutlet(outletName, shouldRenderFn) {
|
||||
api.renderInOutlet(
|
||||
outletName,
|
||||
class extends Component {
|
||||
static shouldRender(args, context) {
|
||||
return shouldRenderFn(context);
|
||||
}
|
||||
|
||||
api.renderInOutlet(outlet, gistTemplate);
|
||||
<template><AiTopicGist @topic={{@topic}} /></template>
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
renderGistInOutlet(OUTLETS.mobile, (context) => context.site.mobileView);
|
||||
renderGistInOutlet(OUTLETS.desktop, (context) => context.site.desktopView);
|
||||
|
||||
api.addTopicAdminMenuButton((topic) => {
|
||||
if (!settings.ai_summary_gists_enabled) {
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ export default {
|
||||
displayed() {
|
||||
return (
|
||||
this.currentUser?.can_assign &&
|
||||
!this.site.mobileView &&
|
||||
this.site.desktopView &&
|
||||
(this.topic.isAssigned() || this.topic.hasAssignedPosts())
|
||||
);
|
||||
},
|
||||
|
||||
+5
-9
@@ -37,9 +37,6 @@ function initializeDiscourseCalendar(api) {
|
||||
let _topicController;
|
||||
const outletName = siteSettings.calendar_categories_outlet;
|
||||
|
||||
const site = api.container.lookup("service:site");
|
||||
const isMobileView = site && site.mobileView;
|
||||
|
||||
const router = api.container.lookup("service:router");
|
||||
|
||||
let selector = `.${outletName}-outlet`;
|
||||
@@ -346,6 +343,8 @@ function initializeDiscourseCalendar(api) {
|
||||
}
|
||||
|
||||
function _buildCalendar($calendar, timeZone) {
|
||||
const isMobileView = api.container.lookup("service:site")?.mobileView;
|
||||
|
||||
let $calendarTitle = document.querySelector(
|
||||
".discourse-calendar-header > .discourse-calendar-title"
|
||||
);
|
||||
@@ -519,6 +518,7 @@ function initializeDiscourseCalendar(api) {
|
||||
calendar.setOption("eventClick", ({ event, jsEvent }) => {
|
||||
destroyPopover();
|
||||
const { htmlContent, postNumber, postUrl } = event.extendedProps;
|
||||
const isMobileView = api.container.lookup("service:site")?.mobileView;
|
||||
|
||||
if (postUrl) {
|
||||
DiscourseURL.routeTo(postUrl);
|
||||
@@ -636,14 +636,10 @@ function initializeDiscourseCalendar(api) {
|
||||
const event = _buildEvent(eventData);
|
||||
event.classNames = ["grouped-event"];
|
||||
|
||||
if (users.length > 2) {
|
||||
event.title = `(${users.length}) ${localEventNames[0]}`;
|
||||
} else if (users.length === 1) {
|
||||
if (users.length === 1) {
|
||||
event.title = users[0].username;
|
||||
} else {
|
||||
event.title = isMobileView
|
||||
? `(${users.length}) ${localEventNames[0]}`
|
||||
: `(${users.length}) ` + users.map((u) => u.username).join(", ");
|
||||
event.title = `(${users.length}) ${localEventNames[0]}`;
|
||||
}
|
||||
|
||||
if (localEventNames.length > 1) {
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export default class DiscourseReactionsCounter extends Component {
|
||||
|
||||
this.args.cancelCollapse();
|
||||
|
||||
if (!this.capabilities.touch || !this.site.mobileView) {
|
||||
if (!this.capabilities.touch || this.site.desktopView) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export default class ReactionsReactionButton extends Component {
|
||||
this.args.cancelCollapse();
|
||||
|
||||
const currentUserReaction = this.args.post.current_user_reaction;
|
||||
if (!this.capabilities.touch || !this.site.mobileView) {
|
||||
if (!this.capabilities.touch || this.site.desktopView) {
|
||||
this.args.toggleFromButton({
|
||||
reaction: currentUserReaction
|
||||
? currentUserReaction.id
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ export default class CampaignBanner extends Component {
|
||||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
if (this.isSidebar && this.shouldShow && !this.site.mobileView) {
|
||||
if (this.isSidebar && this.shouldShow && this.site.desktopView) {
|
||||
document.body.classList.add(SIDEBAR_BODY_CLASS);
|
||||
} else {
|
||||
document.body.classList.remove(SIDEBAR_BODY_CLASS);
|
||||
|
||||
+19
-5
@@ -115,9 +115,6 @@ function customizeWidgetPost(api) {
|
||||
updatePostUserNotesCount(this.model, count);
|
||||
});
|
||||
|
||||
const mobileView = api.container.lookup("service:site").mobileView;
|
||||
const loc = mobileView ? "before" : "after";
|
||||
|
||||
// Helper to attach notes icon if user has notes
|
||||
const attachUserNotesIconIfPresent = (dec) => {
|
||||
const post = dec.getModel();
|
||||
@@ -127,8 +124,25 @@ function customizeWidgetPost(api) {
|
||||
};
|
||||
|
||||
// Add notes icon to poster name
|
||||
api.decorateWidget(`poster-name:${loc}`, (dec) => {
|
||||
if (dec.widget.settings.hideNotes) {
|
||||
|
||||
// place the icon before the poster name on mobile
|
||||
api.decorateWidget(`poster-name:before`, (dec) => {
|
||||
if (
|
||||
api.container.lookup("service:site").desktopView ||
|
||||
dec.widget.settings.hideNotes
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return attachUserNotesIconIfPresent(dec);
|
||||
});
|
||||
|
||||
// place the icon after the poster name on desktop
|
||||
api.decorateWidget(`poster-name:after`, (dec) => {
|
||||
if (
|
||||
api.container.lookup("service:site").mobileView ||
|
||||
dec.widget.settings.hideNotes
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user