diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 7cf73370b..fe7e5d776 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -13,11 +13,12 @@ dependencies { implementation project(':capacitor-clipboard') implementation project(':capacitor-device') implementation project(':capacitor-filesystem') + implementation project(':capacitor-keyboard') implementation project(':capacitor-screen-orientation') implementation project(':capacitor-splash-screen') - } + if (hasProperty('postBuildExtras')) { postBuildExtras() } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 9dd1d9112..7ca103cf7 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -14,6 +14,9 @@ project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/d include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') +include ':capacitor-keyboard' +project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') + include ':capacitor-screen-orientation' project(':capacitor-screen-orientation').projectDir = new File('../node_modules/@capacitor/screen-orientation/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 388474730..9876b9ace 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -15,6 +15,7 @@ def capacitor_pods pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' + pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' pod 'CapacitorScreenOrientation', :path => '../../node_modules/@capacitor/screen-orientation' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' end diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index 5d67c18b0..d926ffb28 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -10,9 +10,11 @@ PODS: - Capacitor - CapacitorFilesystem (6.0.2): - Capacitor + - CapacitorKeyboard (6.0.3): + - Capacitor - CapacitorScreenOrientation (6.0.3): - Capacitor - - CapacitorSplashScreen (6.0.3): + - CapacitorSplashScreen (6.0.2): - Capacitor DEPENDENCIES: @@ -22,6 +24,7 @@ DEPENDENCIES: - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorDevice (from `../../node_modules/@capacitor/device`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" + - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)" - "CapacitorScreenOrientation (from `../../node_modules/@capacitor/screen-orientation`)" - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" @@ -38,6 +41,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/device" CapacitorFilesystem: :path: "../../node_modules/@capacitor/filesystem" + CapacitorKeyboard: + :path: "../../node_modules/@capacitor/keyboard" CapacitorScreenOrientation: :path: "../../node_modules/@capacitor/screen-orientation" CapacitorSplashScreen: @@ -50,9 +55,10 @@ SPEC CHECKSUMS: CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9 CapacitorDevice: 1a215717f0b5061503b21a03508b0ec458a57d78 CapacitorFilesystem: c832a3f6d4870c3872688e782ae8e33665e6ecbf + CapacitorKeyboard: 460c6f9ec5e52c84f2742d5ce2e67bbc7ab0ebb0 CapacitorScreenOrientation: 3bb823f5d265190301cdc5d58a568a287d98972a - CapacitorSplashScreen: 68893659d77b5f82d753b3a70475082845e3039c + CapacitorSplashScreen: 250df9ef8014fac5c7c1fd231f0f8b1d8f0b5624 -PODFILE CHECKSUM: 80366870d5c5081f271e0ddeab86b283217ebd9d +PODFILE CHECKSUM: 0bfaa008b5f31bb57606a8c6259197a6af507ba4 -COCOAPODS: 1.16.1 +COCOAPODS: 1.16.2 diff --git a/package.json b/package.json index 99a349d8f..9b4209a8e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@capacitor/device": "^6.0.2", "@capacitor/filesystem": "^6.0.1", "@capacitor/ios": "^6.2.0", + "@capacitor/keyboard": "^6.0.3", "@capacitor/screen-orientation": "^6.0.3", "@capacitor/splash-screen": "^6.0.2", "@dicebear/collection": "^9.0.1", diff --git a/src/lib/components/ui/ContextMenu.svelte b/src/lib/components/ui/ContextMenu.svelte index 2c6d4e833..998aafebd 100644 --- a/src/lib/components/ui/ContextMenu.svelte +++ b/src/lib/components/ui/ContextMenu.svelte @@ -9,12 +9,14 @@ import { clickoutside } from "@svelte-put/clickoutside" import { Appearance } from "$lib/enums" import type { ContextItem } from "$lib/types" - import { createEventDispatcher, tick } from "svelte" + import { createEventDispatcher, onDestroy, onMount, tick } from "svelte" import { log } from "$lib/utils/Logger" let visible: boolean = false let coords: [number, number] = [0, 0] let context: HTMLElement + let touchTimeout: number | undefined + let slotContainer: HTMLElement export let items: ContextItem[] = [] export let hook: string = "" @@ -25,38 +27,89 @@ close_context = undefined } - function calculatePos(evt: MouseEvent): [number, number] { - if (context === undefined) return [evt.clientX, evt.clientY] + function calculatePos(evt: MouseEvent | TouchEvent): [number, number] { + if (!context) { + if (evt instanceof MouseEvent) { + return [evt.clientX, evt.clientY] + } else if (evt instanceof TouchEvent) { + const touch = evt.touches[0] + return [touch.clientX, touch.clientY] + } + return [0, 0] + } + const { width, height } = context.getBoundingClientRect() - let offsetX = evt.pageX - let offsetY = evt.pageY - let screenWidth = evt.view!.innerWidth - let screenHeight = evt.view!.innerHeight - let overFlowX = screenWidth < width + offsetX - let overFlowY = screenHeight < height + offsetY - let topX = overFlowX ? Math.max(5, screenWidth - width - 5) : Math.max(5, offsetX) - if (screenHeight - offsetY < height + 30) { - let adjustedY = offsetY - height - let topY = Math.max(5, adjustedY) - return [topX, topY] + + let offsetX: number, offsetY: number, screenWidth: number, screenHeight: number + + if (evt instanceof MouseEvent) { + offsetX = evt.pageX + offsetY = evt.pageY + screenWidth = evt.view!.innerWidth + screenHeight = evt.view!.innerHeight + } else if (evt instanceof TouchEvent) { + const touch = evt.touches[0] + const targetElement = touch.target as HTMLElement + + const doc = targetElement.ownerDocument! + const win = doc.defaultView! + + offsetX = touch.pageX + offsetY = touch.pageY + screenWidth = win.innerWidth + screenHeight = win.innerHeight } else { - let topY = Math.max(5, overFlowY ? offsetY - height : offsetY) - return [topX, topY] + return [0, 0] } + + // Calculate overflow + const overFlowX = screenWidth < width + offsetX + const overFlowY = screenHeight < height + offsetY + + // Adjust X position + const topX = overFlowX ? Math.max(5, screenWidth - width - 5) : Math.max(5, offsetX) + + // Adjust Y position + const topY = screenHeight - offsetY < height + 30 ? Math.max(5, offsetY - height) : Math.max(5, overFlowY ? offsetY - height : offsetY) + + return [topX, topY] } - async function openContext(evt: MouseEvent) { + async function openContext(evt: MouseEvent | TouchEvent) { if (close_context !== undefined) { close_context() } close_context = () => (visible = false) + evt.preventDefault() visible = true - coords = [evt.clientX, evt.clientY] + + if (evt instanceof MouseEvent) { + coords = [evt.clientX, evt.clientY] + } else if (evt instanceof TouchEvent) { + const touch = evt.touches[0] + coords = [touch.clientX, touch.clientY] + } + await tick() coords = calculatePos(evt) } + function handleTouchStart(evt: TouchEvent) { + document.body.style.userSelect = "none" + + touchTimeout = window.setTimeout(() => { + openContext(evt) + }, 350) + } + + function handleTouchEnd() { + if (touchTimeout !== undefined) { + clearTimeout(touchTimeout) + touchTimeout = undefined + } + } + function handleItemClick(e: MouseEvent, item: ContextItem) { e.stopPropagation() log.info(`Clicked ${item.text}`) @@ -66,9 +119,22 @@ }) onClose(customEvent) } + + onMount(() => { + // Add event listeners for mobile + slotContainer.addEventListener("touchstart", handleTouchStart) + slotContainer.addEventListener("touchend", handleTouchEnd) + }) + + onDestroy(() => { + slotContainer.removeEventListener("touchstart", handleTouchStart) + slotContainer.removeEventListener("touchend", handleTouchEnd) + }) - +
+ +
{#if visible}
diff --git a/src/lib/components/ui/InstallBanner.svelte b/src/lib/components/ui/InstallBanner.svelte index 8a2861baa..752abf388 100644 --- a/src/lib/components/ui/InstallBanner.svelte +++ b/src/lib/components/ui/InstallBanner.svelte @@ -107,6 +107,7 @@ text={platformButtonProps[platform].text} on:click={() => { window.open(platformButtonProps[platform].download) + closeBanner() }}> diff --git a/src/lib/layouts/BottomNavBarMobile.svelte b/src/lib/layouts/BottomNavBarMobile.svelte new file mode 100644 index 000000000..b74b60e9b --- /dev/null +++ b/src/lib/layouts/BottomNavBarMobile.svelte @@ -0,0 +1,154 @@ + + +
+ + + + diff --git a/src/lib/layouts/Sidebar.svelte b/src/lib/layouts/Sidebar.svelte index 486799ee8..6b781a2c1 100644 --- a/src/lib/layouts/Sidebar.svelte +++ b/src/lib/layouts/Sidebar.svelte @@ -14,6 +14,7 @@ import { Slimbar } from "." import WidgetBar from "$lib/components/widgets/WidgetBar.svelte" import { SettingsStore, type ISettingsState } from "$lib/state" + import { isAndroidOriOS } from "$lib/utils/Mobile" export let activeRoute: Route = Route.Chat export let open: boolean = true @@ -81,7 +82,9 @@
- goto(e.detail)} /> + {#if !isAndroidOriOS()} + goto(e.detail)} /> + {/if}
{/if} diff --git a/src/lib/state/ui/index.ts b/src/lib/state/ui/index.ts index 598e9cb8d..aaf0b463e 100644 --- a/src/lib/state/ui/index.ts +++ b/src/lib/state/ui/index.ts @@ -4,6 +4,7 @@ import { createPersistentState } from ".." import { EmojiFont, Font, Identicon, Route } from "$lib/enums" import { Store as MainStore } from "../Store" import { page } from "$app/stores" +import { checkMobile } from "$lib/utils/Mobile" export interface IUIState { color: Writable @@ -36,7 +37,7 @@ class Store { emojiFont: createPersistentState("uplink.ui.emojiFont", EmojiFont.Fluent), theme: createPersistentState("uplink.ui.theme", "default"), cssOverride: createPersistentState("uplink.ui.cssOverride", ""), - sidebarOpen: createPersistentState("uplink.ui.sidebarOpen", true), + sidebarOpen: createPersistentState("uplink.ui.sidebarOpen", !checkMobile()), chats: createPersistentState("uplink.ui.chats", [], { deserializer: (c: Chat[]) => { // The typing indicator is read as an {}. Init it properly here diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 50a99fa0c..b0267420e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -27,7 +27,8 @@ import InstallBanner from "$lib/components/ui/InstallBanner.svelte" import Market from "$lib/components/market/Market.svelte" import { swipe } from "$lib/components/ui/Swipe" - import { fetchDeviceInfo } from "$lib/utils/Mobile" + import { ScreenOrientation } from "@capacitor/screen-orientation" + import { fetchDeviceInfo, isAndroidOriOS } from "$lib/utils/Mobile" log.debug("Initializing app, layout routes page.") @@ -265,8 +266,21 @@ isLocaleSet = true } + const lockOrientation = async () => { + try { + await ScreenOrientation.lock({ orientation: "portrait" }) + log.info("Screen orientation locked to portrait.") + } catch (error) { + log.error("Failed to lock screen orientation:", error) + } + } + onMount(async () => { await fetchDeviceInfo() + if (await isAndroidOriOS()) { + lockOrientation() + } + await checkIfUserIsLogged($page.route.id) await initializeLocale() buildStyle() diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index b79fa4aef..4a7728c1e 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -49,13 +49,15 @@ import { debounce, getTimeAgo } from "$lib/utils/Functions" import Controls from "$lib/layouts/Controls.svelte" import { tempCDN } from "$lib/utils/CommonVariables" - import { checkMobile } from "$lib/utils/Mobile" + import { checkMobile, isAndroidOriOS } from "$lib/utils/Mobile" import BrowseFiles from "../files/BrowseFiles.svelte" import AttachmentRenderer from "$lib/components/messaging/AttachmentRenderer.svelte" import ShareFile from "$lib/components/files/ShareFile.svelte" import { StateEffect } from "@codemirror/state" import { ToastMessage } from "$lib/state/ui/toast" import AddMembers from "$lib/components/group/AddMembers.svelte" + import { routes } from "$lib/defaults/routes" + import BottomNavBarMobile from "$lib/layouts/BottomNavBarMobile.svelte" let loading = false let contentAsideOpen = false @@ -721,6 +723,9 @@ {/each} + {#if isAndroidOriOS()} + goto(e.detail)} /> + {/if}
@@ -1061,7 +1066,7 @@ }) }} /> - {#if $activeChat.users.length > 0} + {#if $activeChat.users.length > 0 && (!isAndroidOriOS() || (isAndroidOriOS() && get(UIStore.state.sidebarOpen) === false))} {/if}
+{#if isAndroidOriOS() && $activeChat.users.length === 0} + goto(e.detail)} /> +{/if}