diff --git a/app/assets/javascripts/discourse/app/components/d-document.js b/app/assets/javascripts/discourse/app/components/d-document.js
index 9a476f7b6ee..ff62bd4811d 100644
--- a/app/assets/javascripts/discourse/app/components/d-document.js
+++ b/app/assets/javascripts/discourse/app/components/d-document.js
@@ -1,6 +1,7 @@
import Component from "@ember/component";
import { service } from "@ember/service";
import { setLogoffCallback } from "discourse/lib/ajax";
+import { clearAllBodyScrollLocks } from "discourse/lib/body-scroll-lock";
import logout from "discourse/lib/logout";
import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
@@ -59,6 +60,9 @@ export default Component.extend({
@bind
_focusChanged() {
+ // changing app while keyboard is up could cause the keyboard to not collapse and not release lock
+ clearAllBodyScrollLocks();
+
if (document.visibilityState === "hidden") {
if (this.session.hasFocus) {
this.documentTitle.setFocus(false);
diff --git a/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs b/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs
new file mode 100644
index 00000000000..4c88f270c87
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/d-virtual-height.gjs
@@ -0,0 +1,138 @@
+import Component from "@glimmer/component";
+import { cancel, scheduleOnce } from "@ember/runloop";
+import { service } from "@ember/service";
+import { clearAllBodyScrollLocks } from "discourse/lib/body-scroll-lock";
+import isZoomed from "discourse/lib/zoom-check";
+import discourseDebounce from "discourse-common/lib/debounce";
+import { bind } from "discourse-common/utils/decorators";
+
+const KEYBOARD_DETECT_THRESHOLD = 150;
+
+export default class DVirtualHeight extends Component {
+ @service site;
+ @service capabilities;
+ @service appEvents;
+
+ constructor() {
+ super(...arguments);
+
+ if (!window.visualViewport) {
+ return;
+ }
+
+ if (!this.capabilities.isIpadOS && this.site.desktopView) {
+ return;
+ }
+
+ // TODO: Handle device rotation
+ this.windowInnerHeight = window.innerHeight;
+
+ scheduleOnce("afterRender", this, this.debouncedOnViewportResize);
+
+ window.visualViewport.addEventListener(
+ "resize",
+ this.debouncedOnViewportResize
+ );
+ if ("virtualKeyboard" in navigator) {
+ navigator.virtualKeyboard.overlaysContent = true;
+ navigator.virtualKeyboard.addEventListener(
+ "geometrychange",
+ this.debouncedOnViewportResize
+ );
+ }
+ }
+
+ willDestroy() {
+ super.willDestroy(...arguments);
+
+ cancel(this.debouncedHandler);
+
+ window.visualViewport.removeEventListener(
+ "resize",
+ this.debouncedOnViewportResize
+ );
+ if ("virtualKeyboard" in navigator) {
+ navigator.virtualKeyboard.overlaysContent = false;
+ navigator.virtualKeyboard.removeEventListener(
+ "geometrychange",
+ this.debouncedOnViewportResize
+ );
+ }
+ }
+
+ setVH() {
+ if (isZoomed()) {
+ return;
+ }
+
+ let height;
+ if ("virtualKeyboard" in navigator) {
+ height =
+ window.visualViewport.height -
+ navigator.virtualKeyboard.boundingRect.height;
+ } else {
+ const activeWindow = window.visualViewport || window;
+ height = activeWindow?.height || window.innerHeight;
+ }
+
+ const newVh = height * 0.01;
+ if (this.lastVh === newVh) {
+ return;
+ }
+
+ document.documentElement.style.setProperty("--composer-vh", `${newVh}px`);
+ this.lastVh = newVh;
+ }
+
+ @bind
+ debouncedOnViewportResize() {
+ this.debouncedHandler = discourseDebounce(this, this.onViewportResize, 50);
+ }
+
+ @bind
+ onViewportResize() {
+ this.setVH();
+
+ let keyboardVisible = false;
+ if ("virtualKeyboard" in navigator) {
+ if (navigator.virtualKeyboard.boundingRect.height > 0) {
+ keyboardVisible = true;
+ }
+ } else if (this.capabilities.isFirefox && this.capabilities.isAndroid) {
+ if (
+ Math.abs(
+ this.windowInnerHeight -
+ Math.min(window.innerHeight, window.visualViewport.height)
+ ) > KEYBOARD_DETECT_THRESHOLD
+ ) {
+ keyboardVisible = true;
+ }
+ } else {
+ let viewportWindowDiff =
+ this.windowInnerHeight - window.visualViewport.height;
+ const IPAD_HARDWARE_KEYBOARD_TOOLBAR_HEIGHT = 71.5;
+ if (viewportWindowDiff > IPAD_HARDWARE_KEYBOARD_TOOLBAR_HEIGHT) {
+ keyboardVisible = true;
+ }
+
+ // adds bottom padding when using a hardware keyboard and the accessory bar is visible
+ // accessory bar height is 55px, using 75 allows a small buffer
+ if (this.capabilities.isIpadOS) {
+ document.documentElement.style.setProperty(
+ "--composer-ipad-padding",
+ `${viewportWindowDiff < 75 ? viewportWindowDiff : 0}px`
+ );
+ }
+ }
+
+ this.appEvents.trigger("keyboard-visibility-change", keyboardVisible);
+
+ keyboardVisible
+ ? document.documentElement.classList.add("keyboard-visible")
+ : document.documentElement.classList.remove("keyboard-visible");
+
+ if (!keyboardVisible) {
+ clearAllBodyScrollLocks();
+ }
+ }
+}
diff --git a/app/assets/javascripts/discourse/app/instance-initializers/mobile-keyboard.js b/app/assets/javascripts/discourse/app/instance-initializers/mobile-keyboard.js
deleted file mode 100644
index 08efdd7b7c8..00000000000
--- a/app/assets/javascripts/discourse/app/instance-initializers/mobile-keyboard.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import { bind } from "discourse-common/utils/decorators";
-
-export default {
- after: "mobile",
-
- initialize(owner) {
- const site = owner.lookup("service:site");
- this.capabilities = owner.lookup("service:capabilities");
- this.appEvents = owner.lookup("service:app-events");
-
- if (!this.capabilities.isIpadOS && site.desktopView) {
- return;
- }
-
- if (!window.visualViewport) {
- return;
- }
-
- // TODO: Handle device rotation?
- this.windowInnerHeight = window.innerHeight;
-
- this.onViewportResize();
- window.visualViewport.addEventListener("resize", this.onViewportResize);
- if ("virtualKeyboard" in navigator) {
- navigator.virtualKeyboard.overlaysContent = true;
- navigator.virtualKeyboard.addEventListener(
- "geometrychange",
- this.onViewportResize
- );
- }
- },
-
- teardown() {
- window.visualViewport.removeEventListener("resize", this.onViewportResize);
- if ("virtualKeyboard" in navigator) {
- navigator.virtualKeyboard.overlaysContent = false;
- navigator.virtualKeyboard.removeEventListener(
- "geometrychange",
- this.onViewportResize
- );
- }
- },
-
- @bind
- onViewportResize() {
- const composerVH = window.visualViewport.height * 0.01,
- doc = document.documentElement,
- KEYBOARD_DETECT_THRESHOLD = 150;
-
- doc.style.setProperty("--composer-vh", `${composerVH}px`);
-
- let keyboardVisible = false;
- if ("virtualKeyboard" in navigator) {
- if (navigator.virtualKeyboard.boundingRect.height > 0) {
- keyboardVisible = true;
- }
- } else if (this.capabilities.isFirefox && this.capabilities.isAndroid) {
- if (
- Math.abs(
- this.windowInnerHeight -
- Math.min(window.innerHeight, window.visualViewport.height)
- ) > KEYBOARD_DETECT_THRESHOLD
- ) {
- keyboardVisible = true;
- }
- } else {
- let viewportWindowDiff =
- this.windowInnerHeight - window.visualViewport.height;
- const IPAD_HARDWARE_KEYBOARD_TOOLBAR_HEIGHT = 71.5;
- if (viewportWindowDiff > IPAD_HARDWARE_KEYBOARD_TOOLBAR_HEIGHT) {
- keyboardVisible = true;
- }
-
- // adds bottom padding when using a hardware keyboard and the accessory bar is visible
- // accessory bar height is 55px, using 75 allows a small buffer
- if (this.capabilities.isIpadOS) {
- doc.style.setProperty(
- "--composer-ipad-padding",
- `${viewportWindowDiff < 75 ? viewportWindowDiff : 0}px`
- );
- }
- }
-
- this.appEvents.trigger("keyboard-visibility-change", keyboardVisible);
-
- keyboardVisible
- ? doc.classList.add("keyboard-visible")
- : doc.classList.remove("keyboard-visible");
- },
-};
diff --git a/app/assets/javascripts/discourse/app/lib/body-scroll-lock.js b/app/assets/javascripts/discourse/app/lib/body-scroll-lock.js
index 8051cd0308e..4bf305e084d 100644
--- a/app/assets/javascripts/discourse/app/lib/body-scroll-lock.js
+++ b/app/assets/javascripts/discourse/app/lib/body-scroll-lock.js
@@ -156,17 +156,40 @@ const isTargetElementTotallyScrolled = (targetElement) =>
? targetElement.scrollHeight - targetElement.scrollTop <=
targetElement.clientHeight
: false;
-const handleScroll = (event, targetElement) => {
+const handleScroll = (event, targetElement, options = {}) => {
const clientY = event.targetTouches[0].clientY - initialClientY;
if (allowTouchMove(event.target)) {
return false;
}
- if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {
- return preventDefault(event);
- }
- if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {
- return preventDefault(event);
+
+ const { reverse } = options;
+ const atStart = targetElement.scrollTop === 0;
+ const atEnd = isTargetElementTotallyScrolled(targetElement);
+
+ // Adjust the conditions based on the 'reverse' option
+ if (reverse) {
+ // For 'column-reverse', scrolling "up" means moving towards the end of the content,
+ // and scrolling "down" means moving towards the start.
+ if (atEnd && clientY > 0) {
+ // At the end and attempting to scroll towards the start (down in a reversed setup)
+ return preventDefault(event);
+ }
+ if (atStart && clientY < 0) {
+ // At the start and attempting to scroll away from the start (up in a reversed setup)
+ return preventDefault(event);
+ }
+ } else {
+ // Normal scrolling (not reversed)
+ if (atStart && clientY > 0) {
+ // At the start and attempting to scroll towards the start (traditional setup)
+ return preventDefault(event);
+ }
+ if (atEnd && clientY < 0) {
+ // At the end and attempting to scroll away from the start (traditional setup)
+ return preventDefault(event);
+ }
}
+
event.stopPropagation();
return true;
};
@@ -204,7 +227,7 @@ const disableBodyScroll = (targetElement, options) => {
};
targetElement.ontouchmove = (event) => {
if (event.targetTouches.length === 1) {
- handleScroll(event, targetElement);
+ handleScroll(event, targetElement, options);
}
};
if (!documentListenerAdded) {
diff --git a/plugins/chat/assets/javascripts/discourse/lib/zoom-check.js b/app/assets/javascripts/discourse/app/lib/zoom-check.js
similarity index 100%
rename from plugins/chat/assets/javascripts/discourse/lib/zoom-check.js
rename to app/assets/javascripts/discourse/app/lib/zoom-check.js
diff --git a/app/assets/javascripts/discourse/app/templates/application.hbs b/app/assets/javascripts/discourse/app/templates/application.hbs
index 9af9116828f..1c6666b8708 100644
--- a/app/assets/javascripts/discourse/app/templates/application.hbs
+++ b/app/assets/javascripts/discourse/app/templates/application.hbs
@@ -1,4 +1,5 @@
+
{{i18n "skip_to_main_content"}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs
index 30ef22e8cde..b76d932f470 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel.gjs
@@ -27,7 +27,6 @@ import {
PAST,
READ_INTERVAL_MS,
} from "discourse/plugins/chat/discourse/lib/chat-constants";
-import { bodyScrollFix } from "discourse/plugins/chat/discourse/lib/chat-ios-hacks";
import ChatMessagesLoader from "discourse/plugins/chat/discourse/lib/chat-messages-loader";
import {
checkMessageBottomVisibility,
@@ -184,7 +183,6 @@ export default class ChatChannel extends Component {
onPresenceChangeCallback(present) {
if (present) {
this.debouncedUpdateLastReadMessage();
- bodyScrollFix({ delayed: true });
}
}
@@ -463,7 +461,6 @@ export default class ChatChannel extends Component {
return;
}
- bodyScrollFix();
DatesSeparatorsPositioner.apply(this.scrollable);
this.needsArrow =
@@ -765,6 +762,7 @@ export default class ChatChannel extends Component {
@channel={{@channel}}
@uploadDropZone={{this.uploadDropZone}}
@onSendMessage={{this.onSendMessage}}
+ @scrollable={{this.scrollable}}
/>
{{/if}}
{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
index 4e5be6499f2..6e5ed5de928 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
@@ -141,6 +141,7 @@ export default class ChatComposer extends Component {
@action
setup() {
+ this.composer.scrollable = this.args.scrollable;
this.appEvents.on("chat:modify-selection", this, "modifySelection");
this.appEvents.on(
"chat:open-insert-link-modal",
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs
index 3de3629f629..bb7d0196186 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-message.gjs
@@ -16,6 +16,7 @@ import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import optionalService from "discourse/lib/optional-service";
import { updateUserStatusOnMention } from "discourse/lib/update-user-status-on-mention";
+import isZoomed from "discourse/lib/zoom-check";
import discourseDebounce from "discourse-common/lib/debounce";
import discourseLater from "discourse-common/lib/later";
import { bind } from "discourse-common/utils/decorators";
@@ -30,7 +31,6 @@ import ChatMessageSeparator from "discourse/plugins/chat/discourse/components/ch
import ChatMessageText from "discourse/plugins/chat/discourse/components/chat-message-text";
import ChatMessageThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator";
import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor";
-import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
import ChatOnLongPress from "discourse/plugins/chat/discourse/modifiers/chat/on-long-press";
let _chatMessageDecorators = [];
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-thread.gjs b/plugins/chat/assets/javascripts/discourse/components/chat-thread.gjs
index c0d53095fa4..ee9df33f77b 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-thread.gjs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread.gjs
@@ -19,10 +19,7 @@ import {
PAST,
READ_INTERVAL_MS,
} from "discourse/plugins/chat/discourse/lib/chat-constants";
-import {
- bodyScrollFix,
- stackingContextFix,
-} from "discourse/plugins/chat/discourse/lib/chat-ios-hacks";
+import { stackingContextFix } from "discourse/plugins/chat/discourse/lib/chat-ios-hacks";
import ChatMessagesLoader from "discourse/plugins/chat/discourse/lib/chat-messages-loader";
import DatesSeparatorsPositioner from "discourse/plugins/chat/discourse/lib/dates-separators-positioner";
import { extractCurrentTopicInfo } from "discourse/plugins/chat/discourse/lib/extract-current-topic-info";
@@ -119,7 +116,6 @@ export default class ChatThread extends Component {
return;
}
- bodyScrollFix();
DatesSeparatorsPositioner.apply(this.scrollable);
this.needsArrow =
@@ -578,6 +574,7 @@ export default class ChatThread extends Component {
@thread={{@thread}}
@onSendMessage={{this.onSendMessage}}
@uploadDropZone={{this.uploadDropZone}}
+ @scrollable={{this.scrollable}}
/>
{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-vh.js b/plugins/chat/assets/javascripts/discourse/components/chat-vh.js
deleted file mode 100644
index 620f78f989d..00000000000
--- a/plugins/chat/assets/javascripts/discourse/components/chat-vh.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import Component from "@ember/component";
-import { service } from "@ember/service";
-import { bind } from "discourse-common/utils/decorators";
-import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
-
-const CSS_VAR = "--chat-vh";
-let lastVH;
-
-export default class ChatVh extends Component {
- @service capabilities;
-
- tagName = "";
-
- didInsertElement() {
- super.didInsertElement(...arguments);
-
- if ("virtualKeyboard" in navigator) {
- navigator.virtualKeyboard.overlaysContent = true;
- navigator.virtualKeyboard.addEventListener("geometrychange", this.setVH);
- }
-
- this.activeWindow = window.visualViewport || window;
- this.activeWindow.addEventListener("resize", this.setVH);
- this.setVH();
- }
-
- willDestroyElement() {
- super.willDestroyElement(...arguments);
-
- this.activeWindow?.removeEventListener("resize", this.setVH);
- lastVH = null;
-
- if ("virtualKeyboard" in navigator) {
- navigator.virtualKeyboard.removeEventListener(
- "geometrychange",
- this.setVH
- );
- }
- }
-
- @bind
- setVH() {
- if (isZoomed()) {
- return;
- }
-
- let height;
- if ("virtualKeyboard" in navigator) {
- height =
- window.visualViewport.height -
- navigator.virtualKeyboard.boundingRect.height;
- } else {
- height = this.activeWindow?.height || window.innerHeight;
- }
-
- const vh = height * 0.01;
-
- if (lastVH === vh) {
- return;
- }
- lastVH = vh;
-
- document.documentElement.style.setProperty(CSS_VAR, `${vh}px`);
- }
-
- #blurActiveElement() {
- if (document.activeElement?.blur) {
- document.activeElement.blur();
- }
- }
-}
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-ios-hacks.js b/plugins/chat/assets/javascripts/discourse/lib/chat-ios-hacks.js
index 90036d4f6c6..5e2fd1a9218 100644
--- a/plugins/chat/assets/javascripts/discourse/lib/chat-ios-hacks.js
+++ b/plugins/chat/assets/javascripts/discourse/lib/chat-ios-hacks.js
@@ -1,7 +1,6 @@
import { next, schedule } from "@ember/runloop";
import { capabilities } from "discourse/services/capabilities";
import discourseLater from "discourse-common/lib/later";
-import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
// since -webkit-overflow-scrolling: touch can't be used anymore to disable momentum scrolling
// we use different hacks to work around this
@@ -33,21 +32,3 @@ export function stackingContextFix(scrollable, callback) {
});
}
}
-
-export function bodyScrollFix(options = {}) {
- // when keyboard is visible this will ensure body
- // doesn’t scroll out of viewport
- if (
- capabilities.isIOS &&
- document.documentElement.classList.contains("keyboard-visible") &&
- !isZoomed()
- ) {
- if (options.delayed) {
- setTimeout(() => {
- document.documentElement.scrollTo(0, 0);
- }, 200);
- } else {
- document.documentElement.scrollTo(0, 0);
- }
- }
-}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js
index 2057acee108..89ff9389244 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js
@@ -1,6 +1,8 @@
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
+import { schedule } from "@ember/runloop";
import Service, { service } from "@ember/service";
+import { disableBodyScroll } from "discourse/lib/body-scroll-lock";
export default class ChatChannelComposer extends Service {
@service chat;
@@ -9,12 +11,31 @@ export default class ChatChannelComposer extends Service {
@service router;
@service("chat-thread-composer") threadComposer;
@service loadingSlider;
+ @service capabilities;
+ @service appEvents;
@tracked textarea;
+ @tracked scrollable;
+
+ init() {
+ super.init(...arguments);
+ this.appEvents.on("discourse:focus-changed", this, this.blur);
+ }
+
+ willDestroy() {
+ super.willDestroy(...arguments);
+ this.appEvents.off("discourse:focus-changed", this, this.blur);
+ }
@action
focus(options = {}) {
this.textarea?.focus(options);
+
+ schedule("afterRender", () => {
+ if (this.capabilities.isIOS && !this.capabilities.isIpadOS) {
+ disableBodyScroll(this.scrollable, { reverse: true });
+ }
+ });
}
@action
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-thread-composer.js b/plugins/chat/assets/javascripts/discourse/services/chat-thread-composer.js
index 03ee77a23f0..7eff7352dea 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-thread-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-thread-composer.js
@@ -1,15 +1,36 @@
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
+import { schedule } from "@ember/runloop";
import Service, { service } from "@ember/service";
+import { disableBodyScroll } from "discourse/lib/body-scroll-lock";
export default class ChatThreadComposer extends Service {
@service chat;
+ @service capabilities;
+ @service appEvents;
@tracked textarea;
+ @tracked scrollable;
+
+ init() {
+ super.init(...arguments);
+ this.appEvents.on("discourse:focus-changed", this, this.blur);
+ }
+
+ willDestroy() {
+ super.willDestroy(...arguments);
+ this.appEvents.off("discourse:focus-changed", this, this.blur);
+ }
@action
focus(options = {}) {
this.textarea?.focus(options);
+
+ schedule("afterRender", () => {
+ if (this.capabilities.isIOS && !this.capabilities.isIpadOS) {
+ disableBodyScroll(this.scrollable, { reverse: true });
+ }
+ });
}
@action
diff --git a/plugins/chat/assets/javascripts/discourse/templates/chat.hbs b/plugins/chat/assets/javascripts/discourse/templates/chat.hbs
index f8132b4803f..292f53880b8 100644
--- a/plugins/chat/assets/javascripts/discourse/templates/chat.hbs
+++ b/plugins/chat/assets/javascripts/discourse/templates/chat.hbs
@@ -1,7 +1,5 @@
-
-
{{#if this.chat.sidebarActive}}