Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1437dd0
feat: make messages wrap instead of widening the page
ValwareIRC Sep 29, 2025
a49bbf2
fix: ensure long URLs wrap properly
ValwareIRC Sep 29, 2025
c275d8e
fix: truncate long URLs to 60 chars for display
ValwareIRC Sep 29, 2025
0815793
style: use template literal for URL truncation
ValwareIRC Sep 29, 2025
dbfa73c
feat: Implement comprehensive IRCv3 client features
ValwareIRC Sep 30, 2025
af30cd2
feat: Implement comprehensive IRCv3 client features and add complete …
ValwareIRC Sep 30, 2025
b9f8197
Linting
ValwareIRC Sep 30, 2025
7057d56
Merge main branch updates into IRCv3 implementation
ValwareIRC Sep 30, 2025
98b7cff
Protocol fixes
ValwareIRC Sep 30, 2025
0e26bde
Linting
ValwareIRC Sep 30, 2025
12cf7cc
Persist metadata
ValwareIRC Sep 30, 2025
f014dec
Other parsing fixes
ValwareIRC Sep 30, 2025
03c19d2
update connection test
ValwareIRC Sep 30, 2025
08aafd6
Biome
ValwareIRC Sep 30, 2025
baec510
Actually fix text
ValwareIRC Sep 30, 2025
de28e5c
Perfect account-registration
ValwareIRC Sep 30, 2025
bbebb23
Properly deal with CAPs
ValwareIRC Sep 30, 2025
a53bb9e
More fixes
ValwareIRC Sep 30, 2025
deb3cc4
Put the bin buttons back aaa
ValwareIRC Sep 30, 2025
4faafe8
Make redaction properly work
ValwareIRC Sep 30, 2025
e843a7d
=]
ValwareIRC Sep 30, 2025
0f1b541
Maybe fix metadata not working on some servers
ValwareIRC Sep 30, 2025
46b4823
Maybe fix metadata not working on some servers
ValwareIRC Sep 30, 2025
6047cbb
Merge branch 'main' into ircv3
ValwareIRC Oct 1, 2025
5ed823b
Fix test suite and enhance components
ValwareIRC Oct 1, 2025
4913213
fix: Remove explicit any type in UserSettings test
ValwareIRC Oct 1, 2025
035e3dd
style: Improve code formatting in store and tests
ValwareIRC Oct 1, 2025
25a8574
Fix TypeScript compilation errors and test isolation issues
ValwareIRC Oct 1, 2025
f61af0c
Use alt nick if nick taken
ValwareIRC Oct 1, 2025
aa47c9f
Rely on 001 to know our nick at connect time
ValwareIRC Oct 1, 2025
7376cf4
Rely on 001 to know our nick at connect time
ValwareIRC Oct 1, 2025
8703825
Better self-nick tracking
ValwareIRC Oct 1, 2025
8b0ecf3
Various improvement
ValwareIRC Oct 1, 2025
302105f
fix broken statusprefix parsing
ValwareIRC Oct 1, 2025
d1dd981
Fix Android keyboard covering input box
ValwareIRC Oct 2, 2025
bfc2b80
...
ValwareIRC Oct 2, 2025
d70cbac
Fix mobile dark grey overlay issue
ValwareIRC Oct 2, 2025
ecb5422
Fix mobile dark grey overlay issue
ValwareIRC Oct 2, 2025
205b493
Add draft/multline and improve other batching
ValwareIRC Oct 2, 2025
d135111
Try out znc playback
ValwareIRC Oct 2, 2025
12a4e8f
Merge branch 'main' of https://github.com/obsidianirc/obsidianirc int…
ValwareIRC Oct 2, 2025
8479576
Update src/store/index.ts
ValwareIRC Oct 2, 2025
95fa443
Bump biome apparently O.o
ValwareIRC Oct 2, 2025
e7bcd09
Fix mobile display issues
ValwareIRC Oct 4, 2025
2410bc2
Fix mobile display issues
ValwareIRC Oct 4, 2025
20bc2d1
Bump biome apparently O.o
ValwareIRC Oct 4, 2025
429f9f2
More fixes regarding metadata syncing sessions over shared sessions l…
ValwareIRC Oct 4, 2025
ba83f8b
Update tests
ValwareIRC Oct 4, 2025
c6d2b5c
Modify input placeholder on mobiles
ValwareIRC Oct 4, 2025
fb584e1
Delete server info when deleting a server
ValwareIRC Oct 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions ANDROID_KEYBOARD_FIX.md
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File not needed?

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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Re-enable pinch zoom for accessibility

Line 6 adds user-scalable=no, which blocks pinch-zoom and violates mobile accessibility expectations (WCAG 2.1 1.4.4). Please drop that flag while keeping the other viewport settings.

-  <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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<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" />
πŸ€– Prompt for AI Agents
In index.html around line 6, the viewport meta includes user-scalable=no which
disables pinch-zoom and breaks mobile accessibility; remove the user-scalable=no
token while preserving the other attributes (width=device-width,
initial-scale=1.0, viewport-fit=cover) so pinch-zoom is allowed and the
remaining viewport behavior is unchanged.

<link rel="icon" type="image/x-icon" href="/images/obsidian.png">
</head>

Expand Down
3 changes: 2 additions & 1 deletion src-tauri/gen/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,43 @@ package com.obsidianirc.dev

import android.webkit.WebView
import android.annotation.SuppressLint
import android.view.ViewTreeObserver
import android.view.View
import android.graphics.Rect


class MainActivity : TauriActivity() {
private lateinit var wv: WebView
private var isKeyboardOpen = false

override fun onWebViewCreate(webView: WebView) {
wv = webView
setupKeyboardDetection()
}

private fun setupKeyboardDetection() {
val rootView = findViewById<View>(android.R.id.content)
val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
rootView.getWindowVisibleDisplayFrame(rect)
val screenHeight = rootView.rootView.height
val keypadHeight = screenHeight - rect.bottom

if (keypadHeight > screenHeight * 0.15) { // keyboard is opened
if (!isKeyboardOpen) {
isKeyboardOpen = true
// Force immediate layout adjustment
wv.evaluateJavascript("window.dispatchEvent(new Event('keyboardDidShow'));", null)
}
} else { // keyboard is closed
if (isKeyboardOpen) {
isKeyboardOpen = false
wv.evaluateJavascript("window.dispatchEvent(new Event('keyboardDidHide'));", null)
}
}
}

rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
}

@SuppressLint("MissingSuperCall", "SetTextI18n")
Expand Down
5 changes: 5 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AddServerModal from "./components/ui/AddServerModal";
import ChannelListModal from "./components/ui/ChannelListModal";
import ChannelRenameModal from "./components/ui/ChannelRenameModal";
import UserSettings from "./components/ui/UserSettings";
import { useKeyboardResize } from "./hooks/useKeyboardResize";
import ircClient from "./lib/ircClient";
import useStore, { loadSavedServers } from "./store";

Expand Down Expand Up @@ -77,6 +78,10 @@ const App: React.FC = () => {
joinChannel,
connectToSavedServers,
} = useStore();

// Initialize keyboard resize handling for mobile platforms
useKeyboardResize();

// askPermissions();
useEffect(() => {
initializeEnvSettings(toggleAddServerModal, joinChannel);
Expand Down
13 changes: 11 additions & 2 deletions src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,15 @@ export const AppLayout: React.FC = () => {
return (
<>
{__HIDE_SERVER_LIST__ ? null : (
<div className="server-list flex-shrink-0 w-[72px] h-full bg-discord-dark-300 z-30">
<div
className={`server-list flex-shrink-0 h-full bg-discord-dark-300 z-30 ${
isNarrowView && mobileViewActiveColumn === "serverList"
? "w-[72px]"
: isNarrowView
? "w-0"
: "w-[72px]"
}`}
>
<ServerList />
</div>
)}
Expand Down Expand Up @@ -161,7 +169,8 @@ export const AppLayout: React.FC = () => {
}, [isTooNarrowForMemberList, toggleMemberList, isNarrowView]);

const getLayoutColumn = (column: layoutColumn) => {
if (isNarrowView && column !== mobileViewActiveColumn) return;
// On mobile, only show the active column
if (isNarrowView && column !== mobileViewActiveColumn) return null;
return getLayoutColumnElement(column);
};

Expand Down
107 changes: 80 additions & 27 deletions src/components/layout/ChannelList.tsx
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,
Expand All @@ -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";
Expand All @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset avatar failure when avatar URL changes.

Once avatarLoadFailed flips to true, we never retry unless the username or server changes. If the same user gets a new avatar (or metadata arrives late), we keep showing the fallback forever. Include the avatar metadata value in the dependencies so the reset runs when the avatar URL updates.

-  }, [currentUser?.username, selectedServerId]);
+  }, [
+    currentUser?.username,
+    currentUser?.metadata?.avatar?.value,
+    selectedServerId,
+  ]);
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 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]);
// 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,
currentUser?.metadata?.avatar?.value,
selectedServerId,
]);
πŸ€– Prompt for AI Agents
In src/components/layout/ChannelList.tsx around lines 83 to 88, the useEffect
resets avatarLoadFailed only when username or selectedServerId changes, so if
the user's avatar URL/metadata updates we never retry; update the dependency
array to include the avatar metadata (e.g., currentUser?.avatarUrl or
currentUser?.avatar) so setAvatarLoadFailed(false) runs when the avatar URL
changes, and keep or adjust the biome-ignore lint comment accordingly to allow
this dependency addition.

// 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("#")
Expand All @@ -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 */}
Expand Down Expand Up @@ -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);
}}
/>
) : (
Expand All @@ -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>
Expand Down
Loading