diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 9d2157b807..aed1666ac9 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -86,6 +86,7 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue' import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import SplashScreen from '@/components/ui/SplashScreen.vue' import WindowControls from '@/components/ui/WindowControls.vue' +import { useIntercomPositioning } from '@/composables/intercom-positioning' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { config } from '@/config' import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js' @@ -125,6 +126,17 @@ import { AppNotificationManager } from './providers/app-notifications' import { AppPopupNotificationManager } from './providers/app-popup-notifications' const themeStore = useTheming() +const router = useRouter() +const route = useRoute() +const intercomBubblePositioning = useIntercomPositioning({ route, themeStore }) +const { + sidebarToggled, + forceSidebar, + sidebarVisible, + intercomBubblePosition, + updateIntercomBubbleStyles, + clearIntercomBubbleStyles, +} = intercomBubblePositioning const notificationManager = new AppNotificationManager() provideNotificationManager(notificationManager) @@ -158,6 +170,7 @@ provideModrinthClient(tauriApiClient) providePageContext({ hierarchicalSidebarAvailable: ref(true), showAds: ref(false), + ...intercomBubblePositioning.pageContext, featureFlags: { serverRamAsBytesAlwaysOn: computed(() => themeStore.getFeatureFlag('server_ram_as_bytes_always_on'), @@ -242,6 +255,7 @@ onUnmounted(async () => { document.querySelector('body').removeEventListener('click', handleClick) document.querySelector('body').removeEventListener('auxclick', handleAuxClick) shutdownHostingIntercom() + clearIntercomBubbleStyles() await unlistenUpdateDownload?.() }) @@ -423,9 +437,6 @@ const handleClose = async () => { await getCurrentWindow().close() } -const router = useRouter() -const route = useRoute() - const loading = setupLoadingStateProvider() loading.setEnabled(false) let initialLoadToken = loading.begin() @@ -643,22 +654,10 @@ const hasPlus = computed( (credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG, ) -const sidebarToggled = ref(true) - -themeStore.$subscribe(() => { - sidebarToggled.value = !themeStore.toggleSidebar -}) - -const forceSidebar = computed( - () => route.path.startsWith('/browse') || route.path.startsWith('/project'), -) -const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value) const showAd = computed( () => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined, ) const hostingRouteActive = computed(() => route.path.startsWith('/hosting')) -const INTERCOM_DEFAULT_PADDING = 20 -const INTERCOM_APP_SIDEBAR_WIDTH = 300 let intercomBooting = false let intercomBooted = false @@ -706,10 +705,8 @@ async function bootIntercom() { intercom_user_jwt: token, session_duration: 1000 * 60 * 60 * 24, alignment: 'right', - horizontal_padding: sidebarVisible.value - ? INTERCOM_APP_SIDEBAR_WIDTH + INTERCOM_DEFAULT_PADDING - : INTERCOM_DEFAULT_PADDING, - vertical_padding: INTERCOM_DEFAULT_PADDING, + horizontal_padding: intercomBubblePosition.value.horizontalPadding, + vertical_padding: intercomBubblePosition.value.verticalPadding, }) intercomBooted = true } catch (error) { @@ -727,14 +724,13 @@ function shutdownHostingIntercom() { } watch( - sidebarVisible, - (visible) => { + intercomBubblePosition, + (position) => { + updateIntercomBubbleStyles(position) if (intercomBooted) { window.Intercom?.('update', { - horizontal_padding: visible - ? INTERCOM_APP_SIDEBAR_WIDTH + INTERCOM_DEFAULT_PADDING - : INTERCOM_DEFAULT_PADDING, - vertical_padding: INTERCOM_DEFAULT_PADDING, + horizontal_padding: position.horizontalPadding, + vertical_padding: position.verticalPadding, }) } }, @@ -1772,6 +1768,14 @@ provideAppUpdateDownloadProgress(appUpdateDownload) --os-handle-bg-active: var(--color-scrollbar) !important; } +.intercom-lightweight-app-launcher, +.intercom-launcher-frame, +iframe[name='intercom-launcher-frame'] { + right: var(--app-support-launcher-right, 20px) !important; + bottom: var(--app-support-launcher-bottom, 20px) !important; + z-index: 9 !important; +} + .mac { .app-grid-statusbar { padding-left: 5rem; diff --git a/apps/app-frontend/src/composables/intercom-positioning.ts b/apps/app-frontend/src/composables/intercom-positioning.ts new file mode 100644 index 0000000000..a188d26b5d --- /dev/null +++ b/apps/app-frontend/src/composables/intercom-positioning.ts @@ -0,0 +1,100 @@ +import { computed, onUnmounted, ref } from 'vue' +import type { RouteLocationNormalizedLoaded } from 'vue-router' + +interface ThemeStore { + toggleSidebar: boolean + $subscribe: (callback: () => void) => () => void +} + +interface IntercomBubblePosition { + horizontalPadding: number + verticalPadding: number +} + +const APP_LEFT_NAV_WIDTH = '4rem' +const APP_SIDEBAR_WIDTH = 300 +const INTERCOM_BUBBLE_DEFAULT_PADDING = 20 +const INTERCOM_BUBBLE_WIDTH = 72 +const INTERCOM_BUBBLE_RIGHT_VAR = '--app-support-launcher-right' +const INTERCOM_BUBBLE_BOTTOM_VAR = '--app-support-launcher-bottom' + +export function useIntercomPositioning({ + route, + themeStore, +}: { + route: RouteLocationNormalizedLoaded + themeStore: ThemeStore +}) { + const sidebarToggled = ref(true) + const unsubscribeSidebarToggle = themeStore.$subscribe(() => { + sidebarToggled.value = !themeStore.toggleSidebar + }) + + onUnmounted(unsubscribeSidebarToggle) + + const forceSidebar = computed( + () => route.path.startsWith('/browse') || route.path.startsWith('/project'), + ) + const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value) + const intercomBubbleHorizontalPadding = computed(() => + sidebarVisible.value + ? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING + : INTERCOM_BUBBLE_DEFAULT_PADDING, + ) + const intercomBubbleVerticalClearance = ref(null) + const intercomBubblePosition = computed(() => ({ + horizontalPadding: intercomBubbleHorizontalPadding.value, + verticalPadding: intercomBubbleVerticalClearance.value ?? INTERCOM_BUBBLE_DEFAULT_PADDING, + })) + const intercomBubbleClearanceRequests = new Map() + + function requestIntercomBubbleVerticalClearance(id: symbol, clearance: number | null) { + if (clearance === null) { + intercomBubbleClearanceRequests.delete(id) + } else { + intercomBubbleClearanceRequests.set(id, clearance) + } + + intercomBubbleVerticalClearance.value = + intercomBubbleClearanceRequests.size > 0 + ? Math.max(...intercomBubbleClearanceRequests.values()) + : null + } + + function updateIntercomBubbleStyles({ + horizontalPadding, + verticalPadding, + }: IntercomBubblePosition) { + if (typeof document === 'undefined') return + + document.documentElement.style.setProperty(INTERCOM_BUBBLE_RIGHT_VAR, `${horizontalPadding}px`) + document.documentElement.style.setProperty(INTERCOM_BUBBLE_BOTTOM_VAR, `${verticalPadding}px`) + } + + function clearIntercomBubbleStyles() { + if (typeof document === 'undefined') return + + document.documentElement.style.removeProperty(INTERCOM_BUBBLE_RIGHT_VAR) + document.documentElement.style.removeProperty(INTERCOM_BUBBLE_BOTTOM_VAR) + } + + return { + sidebarToggled, + forceSidebar, + sidebarVisible, + intercomBubblePosition, + updateIntercomBubbleStyles, + clearIntercomBubbleStyles, + pageContext: { + floatingActionBarOffsets: { + left: ref(APP_LEFT_NAV_WIDTH), + right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')), + }, + intercomBubble: { + width: ref(INTERCOM_BUBBLE_WIDTH), + horizontalPadding: intercomBubbleHorizontalPadding, + requestVerticalClearance: requestIntercomBubbleVerticalClearance, + }, + }, + } +} diff --git a/packages/ui/src/components/base/FloatingActionBar.vue b/packages/ui/src/components/base/FloatingActionBar.vue index fd46ec68da..c099be7394 100644 --- a/packages/ui/src/components/base/FloatingActionBar.vue +++ b/packages/ui/src/components/base/FloatingActionBar.vue @@ -1,7 +1,8 @@ @@ -68,8 +166,9 @@ onUnmounted(() => {
{