-
Notifications
You must be signed in to change notification settings - Fork 22
Test Suite Fixes and UI Enhancements #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1437dd0
a49bbf2
c275d8e
0815793
dbfa73c
af30cd2
b9f8197
7057d56
98b7cff
0e26bde
12cf7cc
f014dec
03c19d2
08aafd6
baec510
de28e5c
bbebb23
a53bb9e
deb3cc4
4faafe8
e843a7d
0f1b541
46b4823
6047cbb
5ed823b
4913213
035e3dd
25a8574
f61af0c
aa47c9f
7376cf4
8703825
8b0ecf3
302105f
d1dd981
bfc2b80
d70cbac
ecb5422
205b493
d135111
12a4e8f
8479576
95fa443
e7bcd09
2410bc2
20bc2d1
429f9f2
ba83f8b
c6d2b5c
fb584e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # Android Keyboard Resize Fix | ||
|
|
||
| ## Problem | ||
| On Android devices using the Tauri app, when clicking on the channel input box, the keyboard opens but covers the bottom of the screen (including the input box). The viewport only properly resizes after navigating away from the app and returning. | ||
|
|
||
| ## Root Cause | ||
| The issue was caused by improper Android keyboard handling configuration: | ||
| 1. Missing `android:windowSoftInputMode` in the AndroidManifest.xml | ||
| 2. No viewport resize handling for keyboard events | ||
| 3. Lack of proper mobile CSS for keyboard state transitions | ||
|
|
||
| ## Solution Implemented | ||
|
|
||
| ### 1. AndroidManifest.xml Configuration | ||
| **File:** `src-tauri/gen/android/app/src/main/AndroidManifest.xml` | ||
| - Added `android:windowSoftInputMode="adjustResize"` to the MainActivity declaration | ||
| - This tells Android to resize the viewport when the keyboard appears instead of covering content | ||
|
|
||
| ### 2. Enhanced HTML Viewport Settings | ||
| **File:** `index.html` | ||
| - Updated viewport meta tag to include `viewport-fit=cover, user-scalable=no` | ||
| - Provides better mobile viewport handling | ||
|
|
||
| ### 3. Native Android Keyboard Detection | ||
| **File:** `src-tauri/gen/android/app/src/main/java/com/obsidianirc/dev/MainActivity.kt` | ||
| - Added `setupKeyboardDetection()` method that monitors layout changes | ||
| - Detects keyboard open/close events and dispatches JavaScript events | ||
| - Provides immediate feedback to the web view when keyboard state changes | ||
|
|
||
| ### 4. JavaScript Keyboard Handling Hook | ||
| **File:** `src/hooks/useKeyboardResize.ts` (NEW) | ||
| - Created a React hook that handles keyboard visibility events | ||
| - Listens for both Visual Viewport API changes and native Android events | ||
| - Updates CSS custom properties to track keyboard height | ||
| - Triggers layout recalculations when keyboard state changes | ||
|
|
||
| ### 5. Mobile-Optimized CSS | ||
| **File:** `src/index.css` | ||
| - Added `--keyboard-height` CSS custom property | ||
| - Added mobile-specific CSS rules for keyboard handling | ||
| - Ensures proper viewport adjustments with smooth transitions | ||
| - Fixed viewport on mobile devices to prevent layout shifts | ||
|
|
||
| ### 6. App Integration | ||
| **File:** `src/App.tsx` | ||
| - Integrated the `useKeyboardResize` hook into the main App component | ||
| - Ensures keyboard handling is active throughout the application lifecycle | ||
|
|
||
| ## Technical Details | ||
|
|
||
| ### Android Window Soft Input Modes | ||
| - `adjustResize`: Resizes the window to make room for the keyboard | ||
| - This is preferred over `adjustPan` which just shifts content up | ||
|
|
||
| ### Visual Viewport API | ||
| - Modern browsers provide this API to detect viewport changes | ||
| - Especially useful for keyboard events on mobile devices | ||
| - Fallback handling for older browsers included | ||
|
|
||
| ### CSS Custom Properties | ||
| - `--keyboard-height` tracks the current keyboard height | ||
| - Allows responsive layout adjustments based on keyboard state | ||
| - Smooth transitions prevent jarring layout changes | ||
|
|
||
| ## Expected Behavior After Fix | ||
| 1. User taps on the channel input box | ||
| 2. Keyboard opens immediately | ||
| 3. Viewport resizes instantly to accommodate keyboard | ||
| 4. Input box remains visible above the keyboard | ||
| 5. No need to navigate away and back to see proper layout | ||
|
|
||
| ## Testing Considerations | ||
| - Test on various Android devices and screen sizes | ||
| - Verify both portrait and landscape orientations | ||
| - Ensure keyboard animations are smooth | ||
| - Check that all input fields throughout the app behave consistently | ||
|
|
||
| ## Browser Compatibility | ||
| - Modern Android browsers with Visual Viewport API support | ||
| - Fallback handling for older browsers | ||
| - iOS support included for future compatibility |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |||||
|
|
||||||
| <head> | ||||||
| <meta charset="UTF-8" /> | ||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no" /> | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-enable pinch zoom for accessibility Line 6 adds - <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||
| <link rel="icon" type="image/x-icon" href="/images/obsidian.png"> | ||||||
| </head> | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||
| import type * as React from "react"; | ||||||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||||||
| import { useEffect, useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||
| FaChevronDown, | ||||||||||||||||||||||||||||||
| FaChevronLeft, | ||||||||||||||||||||||||||||||
|
|
@@ -13,6 +13,7 @@ import { | |||||||||||||||||||||||||||||
| FaVolumeUp, | ||||||||||||||||||||||||||||||
| } from "react-icons/fa"; | ||||||||||||||||||||||||||||||
| import { useMediaQuery } from "../../hooks/useMediaQuery"; | ||||||||||||||||||||||||||||||
| import ircClient from "../../lib/ircClient"; | ||||||||||||||||||||||||||||||
| import useStore from "../../store"; | ||||||||||||||||||||||||||||||
| import TouchableContextMenu from "../mobile/TouchableContextMenu"; | ||||||||||||||||||||||||||||||
| import AddPrivateChatModal from "../ui/AddPrivateChatModal"; | ||||||||||||||||||||||||||||||
|
|
@@ -22,27 +23,80 @@ export const ChannelList: React.FC<{ | |||||||||||||||||||||||||||||
| }> = ({ onToggle }: { onToggle: () => void }) => { | ||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||
| servers, | ||||||||||||||||||||||||||||||
| currentUser: globalCurrentUser, | ||||||||||||||||||||||||||||||
| ui: { selectedServerId, selectedChannelId, selectedPrivateChatId }, | ||||||||||||||||||||||||||||||
| selectChannel, | ||||||||||||||||||||||||||||||
| selectPrivateChat, | ||||||||||||||||||||||||||||||
| joinChannel, | ||||||||||||||||||||||||||||||
| leaveChannel, | ||||||||||||||||||||||||||||||
| deletePrivateChat, | ||||||||||||||||||||||||||||||
| toggleUserProfileModal, | ||||||||||||||||||||||||||||||
| currentUser, | ||||||||||||||||||||||||||||||
| setMobileViewActiveColumn, | ||||||||||||||||||||||||||||||
| } = useStore(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get the current user for the selected server from the store data (includes metadata) | ||||||||||||||||||||||||||||||
| const currentUser = useMemo(() => { | ||||||||||||||||||||||||||||||
| if (!selectedServerId) return null; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Get the current user's username from IRCClient | ||||||||||||||||||||||||||||||
| const ircCurrentUser = ircClient.getCurrentUser(selectedServerId); | ||||||||||||||||||||||||||||||
| if (!ircCurrentUser) return null; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // First, check if we have a global current user with metadata for this username | ||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||
| globalCurrentUser && | ||||||||||||||||||||||||||||||
| globalCurrentUser.username === ircCurrentUser.username | ||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||
| return globalCurrentUser; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Find the current user in the server's channel data to get metadata | ||||||||||||||||||||||||||||||
| const selectedServer = servers.find((s) => s.id === selectedServerId); | ||||||||||||||||||||||||||||||
| if (!selectedServer) return ircCurrentUser; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Look for the user in any channel to get their metadata | ||||||||||||||||||||||||||||||
| for (const channel of selectedServer.channels) { | ||||||||||||||||||||||||||||||
| const userWithMetadata = channel.users.find( | ||||||||||||||||||||||||||||||
| (u) => u.username === ircCurrentUser.username, | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| if (userWithMetadata) { | ||||||||||||||||||||||||||||||
| return userWithMetadata; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If not found in channels, return the basic IRC user | ||||||||||||||||||||||||||||||
| return ircCurrentUser; | ||||||||||||||||||||||||||||||
| }, [selectedServerId, servers, globalCurrentUser]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const [isTextChannelsOpen, setIsTextChannelsOpen] = useState(true); | ||||||||||||||||||||||||||||||
| const [isVoiceChannelsOpen, setIsVoiceChannelsOpen] = useState(true); | ||||||||||||||||||||||||||||||
| const [isPrivateChatsOpen, setIsPrivateChatsOpen] = useState(true); | ||||||||||||||||||||||||||||||
| const [newChannelName, setNewChannelName] = useState(""); | ||||||||||||||||||||||||||||||
| const [isAddPrivateChatModalOpen, setIsAddPrivateChatModalOpen] = | ||||||||||||||||||||||||||||||
| useState(false); | ||||||||||||||||||||||||||||||
| const [avatarLoadFailed, setAvatarLoadFailed] = useState(false); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const selectedServer = servers.find( | ||||||||||||||||||||||||||||||
| (server) => server.id === selectedServerId, | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Reset avatar load failed state when user or server changes | ||||||||||||||||||||||||||||||
| // biome-ignore lint/correctness/useExhaustiveDependencies: We want to reset when user/server changes | ||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| setAvatarLoadFailed(false); | ||||||||||||||||||||||||||||||
| }, [currentUser?.username, selectedServerId]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reset avatar failure when avatar URL changes. Once - }, [currentUser?.username, selectedServerId]);
+ }, [
+ currentUser?.username,
+ currentUser?.metadata?.avatar?.value,
+ selectedServerId,
+ ]);π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||
| // Get user status based on server connection and away status | ||||||||||||||||||||||||||||||
| const userStatus = useMemo(() => { | ||||||||||||||||||||||||||||||
| if (!selectedServer || !selectedServer.isConnected) { | ||||||||||||||||||||||||||||||
| return "offline"; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if (selectedServer.isAway) { | ||||||||||||||||||||||||||||||
| return "away"; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return "online"; | ||||||||||||||||||||||||||||||
| }, [selectedServer]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const handleAddChannel = () => { | ||||||||||||||||||||||||||||||
| if (selectedServerId && newChannelName.trim()) { | ||||||||||||||||||||||||||||||
| const channelName = newChannelName.trim().startsWith("#") | ||||||||||||||||||||||||||||||
|
|
@@ -62,21 +116,29 @@ export const ChannelList: React.FC<{ | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const isNarrowView = useMediaQuery(); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const handleCollapseClick = () => { | ||||||||||||||||||||||||||||||
| if (isNarrowView) { | ||||||||||||||||||||||||||||||
| // On mobile, navigate to chat view | ||||||||||||||||||||||||||||||
| setMobileViewActiveColumn("chatView"); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| // On desktop, toggle the channel list | ||||||||||||||||||||||||||||||
| onToggle(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <div className="h-full flex flex-col text-discord-channels-default"> | ||||||||||||||||||||||||||||||
| {/* Server header */} | ||||||||||||||||||||||||||||||
| <div className="px-4 h-12 shadow-md flex items-center justify-between border-b border-discord-dark-400"> | ||||||||||||||||||||||||||||||
| <h1 className="font-bold text-white truncate"> | ||||||||||||||||||||||||||||||
| {selectedServer?.name || "Home"} | ||||||||||||||||||||||||||||||
| </h1> | ||||||||||||||||||||||||||||||
| {!isNarrowView && ( | ||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||
| onClick={onToggle} | ||||||||||||||||||||||||||||||
| className="text-discord-channels-default hover:text-white" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <FaChevronLeft /> | ||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||
| onClick={handleCollapseClick} | ||||||||||||||||||||||||||||||
| className="text-discord-channels-default hover:text-white" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <FaChevronLeft /> | ||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Channel list */} | ||||||||||||||||||||||||||||||
|
|
@@ -364,20 +426,13 @@ export const ChannelList: React.FC<{ | |||||||||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||
| <div className="relative"> | ||||||||||||||||||||||||||||||
| <div className="w-8 h-8 rounded-full bg-discord-dark-100 flex items-center justify-center"> | ||||||||||||||||||||||||||||||
| {currentUser?.metadata?.avatar?.value ? ( | ||||||||||||||||||||||||||||||
| {currentUser?.metadata?.avatar?.value && !avatarLoadFailed ? ( | ||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||
| src={currentUser.metadata.avatar.value} | ||||||||||||||||||||||||||||||
| alt={currentUser.username} | ||||||||||||||||||||||||||||||
| className="w-8 h-8 rounded-full object-cover" | ||||||||||||||||||||||||||||||
| onError={(e) => { | ||||||||||||||||||||||||||||||
| // Fallback to initial if image fails to load | ||||||||||||||||||||||||||||||
| e.currentTarget.style.display = "none"; | ||||||||||||||||||||||||||||||
| const parent = e.currentTarget.parentElement; | ||||||||||||||||||||||||||||||
| if (parent && currentUser?.username) { | ||||||||||||||||||||||||||||||
| parent.textContent = currentUser.username | ||||||||||||||||||||||||||||||
| .charAt(0) | ||||||||||||||||||||||||||||||
| .toUpperCase(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| onError={() => { | ||||||||||||||||||||||||||||||
| setAvatarLoadFailed(true); | ||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
|
|
@@ -387,21 +442,19 @@ export const ChannelList: React.FC<{ | |||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||
| className={`absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-discord-dark-400 ${currentUser?.status === "online" ? "bg-discord-green" : currentUser?.status === "idle" ? "bg-discord-yellow" : currentUser?.status === "dnd" ? "bg-discord-red" : "bg-discord-dark-500"}`} | ||||||||||||||||||||||||||||||
| className={`absolute -bottom-1 -right-1 w-3 h-3 rounded-full border-2 border-discord-dark-400 ${userStatus === "online" ? "bg-discord-green" : userStatus === "away" ? "bg-discord-yellow" : "bg-discord-dark-500"}`} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||
| <div className="text-white font-medium text-sm"> | ||||||||||||||||||||||||||||||
| {currentUser?.username || "User"} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div className="text-xs text-discord-channels-default"> | ||||||||||||||||||||||||||||||
| {currentUser?.status === "online" | ||||||||||||||||||||||||||||||
| {userStatus === "online" | ||||||||||||||||||||||||||||||
| ? "Online" | ||||||||||||||||||||||||||||||
| : currentUser?.status === "idle" | ||||||||||||||||||||||||||||||
| ? "Idle" | ||||||||||||||||||||||||||||||
| : currentUser?.status === "dnd" | ||||||||||||||||||||||||||||||
| ? "Do Not Disturb" | ||||||||||||||||||||||||||||||
| : "Offline"} | ||||||||||||||||||||||||||||||
| : userStatus === "away" | ||||||||||||||||||||||||||||||
| ? selectedServer?.awayMessage || "Away" | ||||||||||||||||||||||||||||||
| : "Offline"} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File not needed?