feat: Introduce swipe navigation between new Chat and Sidebar components and establish initial multi-platform project structure.
All checks were successful
Build and Release / build-and-release (push) Successful in 10m7s
All checks were successful
Build and Release / build-and-release (push) Successful in 10m7s
This commit is contained in:
@@ -79,6 +79,31 @@ jobs:
|
|||||||
npx cap add android
|
npx cap add android
|
||||||
echo "sdk.dir=${ANDROID_SDK_ROOT}" > android/local.properties
|
echo "sdk.dir=${ANDROID_SDK_ROOT}" > android/local.properties
|
||||||
|
|
||||||
|
# Patch AndroidManifest.xml to add required permissions
|
||||||
|
node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const manifestPath = 'android/app/src/main/AndroidManifest.xml';
|
||||||
|
let manifest = fs.readFileSync(manifestPath, 'utf8');
|
||||||
|
|
||||||
|
const permissions = [
|
||||||
|
'<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />',
|
||||||
|
'<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />',
|
||||||
|
'<uses-permission android:name=\"android.permission.CAMERA\" />',
|
||||||
|
'<uses-permission android:name=\"android.permission.BLUETOOTH\" android:maxSdkVersion=\"30\" />',
|
||||||
|
'<uses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" />',
|
||||||
|
'<uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />'
|
||||||
|
];
|
||||||
|
|
||||||
|
const permBlock = permissions.join('\n ');
|
||||||
|
manifest = manifest.replace(
|
||||||
|
'</manifest>',
|
||||||
|
' ' + permBlock + '\n</manifest>'
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(manifestPath, manifest);
|
||||||
|
console.log('Patched AndroidManifest.xml with permissions');
|
||||||
|
"
|
||||||
|
|
||||||
# Append a merged android block with signingConfigs + buildTypes.release signing
|
# Append a merged android block with signingConfigs + buildTypes.release signing
|
||||||
# Gradle merges multiple android {} blocks, and within one block signingConfigs
|
# Gradle merges multiple android {} blocks, and within one block signingConfigs
|
||||||
# is evaluated before buildTypes, so the reference resolves correctly.
|
# is evaluated before buildTypes, so the reference resolves correctly.
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ apps/android/android
|
|||||||
|
|
||||||
# Legacy
|
# Legacy
|
||||||
discord-html-copy
|
discord-html-copy
|
||||||
|
|
||||||
|
todo
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/android",
|
"name": "@discord-clone/android",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.27",
|
"version": "1.0.28",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cap:sync": "npx cap sync",
|
"cap:sync": "npx cap sync",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/electron",
|
"name": "@discord-clone/electron",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.27",
|
"version": "1.0.28",
|
||||||
"description": "Discord Clone - Electron app",
|
"description": "Discord Clone - Electron app",
|
||||||
"author": "Moyettes",
|
"author": "Moyettes",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/web",
|
"name": "@discord-clone/web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.27",
|
"version": "1.0.28",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@discord-clone/shared",
|
"name": "@discord-clone/shared",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.27",
|
"version": "1.0.28",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/App.jsx",
|
"main": "src/App.jsx",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1385,7 +1385,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderServerView = () => (
|
const renderServerView = () => (
|
||||||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--bg-tertiary)', borderLeft: '1px solid var(--app-frame-border)', borderTop: '1px solid var(--app-frame-border)', borderRadius: '8px 0 0 0' }}>
|
<div className="channel-panel" style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', backgroundColor: 'var(--bg-tertiary)', borderLeft: '1px solid var(--app-frame-border)', borderTop: '1px solid var(--app-frame-border)', borderRadius: '8px 0 0 0' }}>
|
||||||
<div className="server-header" style={{ borderBottom: '1px solid var(--app-frame-border)' }}>
|
<div className="server-header" style={{ borderBottom: '1px solid var(--app-frame-border)' }}>
|
||||||
<span className="server-header-name" onClick={() => setIsServerSettingsOpen(true)}>{serverName}</span>
|
<span className="server-header-name" onClick={() => setIsServerSettingsOpen(true)}>{serverName}</span>
|
||||||
<button className="server-header-invite" onClick={handleCreateInvite} title="Invite People">
|
<button className="server-header-invite" onClick={handleCreateInvite} title="Invite People">
|
||||||
|
|||||||
233
packages/shared/src/hooks/useSwipeNavigation.js
Normal file
233
packages/shared/src/hooks/useSwipeNavigation.js
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
const EDGE_THRESHOLD = 30;
|
||||||
|
const SNAP_THRESHOLD = 0.4;
|
||||||
|
const VELOCITY_THRESHOLD = 0.5;
|
||||||
|
|
||||||
|
export function useSwipeNavigation({ enabled, canSwipeToChat }) {
|
||||||
|
const [activeView, setActiveView] = useState('sidebar');
|
||||||
|
const trayRef = useRef(null);
|
||||||
|
|
||||||
|
// Refs so touch handlers always read current values
|
||||||
|
const activeViewRef = useRef(activeView);
|
||||||
|
const canSwipeToChatRef = useRef(canSwipeToChat);
|
||||||
|
const screenWidthRef = useRef(window.innerWidth);
|
||||||
|
|
||||||
|
useEffect(() => { activeViewRef.current = activeView; }, [activeView]);
|
||||||
|
useEffect(() => { canSwipeToChatRef.current = canSwipeToChat; }, [canSwipeToChat]);
|
||||||
|
|
||||||
|
// Track swiping state for pointer-events disabling
|
||||||
|
const [isSwiping, setIsSwiping] = useState(false);
|
||||||
|
|
||||||
|
// Programmatic navigation with smooth transition
|
||||||
|
const goToChat = useCallback(() => {
|
||||||
|
const tray = trayRef.current;
|
||||||
|
if (tray) {
|
||||||
|
tray.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
tray.style.transform = `translateX(-${screenWidthRef.current}px)`;
|
||||||
|
}
|
||||||
|
setActiveView('chat');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const goToSidebar = useCallback(() => {
|
||||||
|
const tray = trayRef.current;
|
||||||
|
if (tray) {
|
||||||
|
tray.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
tray.style.transform = 'translateX(0px)';
|
||||||
|
}
|
||||||
|
setActiveView('sidebar');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const tray = trayRef.current;
|
||||||
|
const panel = tray?.querySelector('.mobile-swipe-panel');
|
||||||
|
screenWidthRef.current = panel?.offsetWidth || window.innerWidth;
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
const tray = trayRef.current;
|
||||||
|
if (!tray) return;
|
||||||
|
|
||||||
|
// Measure actual rendered panel width (CSS 100vw) instead of window.innerWidth
|
||||||
|
const panel = tray.querySelector('.mobile-swipe-panel');
|
||||||
|
if (panel) {
|
||||||
|
screenWidthRef.current = panel.offsetWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tracking = false;
|
||||||
|
let decided = false; // whether we've decided horizontal vs vertical
|
||||||
|
let startX = 0;
|
||||||
|
let startY = 0;
|
||||||
|
let startTime = 0;
|
||||||
|
let baseOffset = 0; // starting translateX when touch began
|
||||||
|
let lastX = 0;
|
||||||
|
let lastTime = 0;
|
||||||
|
|
||||||
|
function getCurrentTranslateX() {
|
||||||
|
const style = window.getComputedStyle(tray);
|
||||||
|
const matrix = style.transform;
|
||||||
|
if (!matrix || matrix === 'none') return 0;
|
||||||
|
// matrix(a, b, c, d, tx, ty)
|
||||||
|
const match = matrix.match(/matrix\(([^)]+)\)/);
|
||||||
|
if (match) {
|
||||||
|
const values = match[1].split(',').map(Number);
|
||||||
|
return values[4] || 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchStart(e) {
|
||||||
|
if (e.touches.length !== 1) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const sw = screenWidthRef.current;
|
||||||
|
const view = activeViewRef.current;
|
||||||
|
|
||||||
|
// Edge detection: only start from edges
|
||||||
|
const fromLeftEdge = touch.clientX <= EDGE_THRESHOLD;
|
||||||
|
const fromRightEdge = touch.clientX >= sw - EDGE_THRESHOLD;
|
||||||
|
|
||||||
|
if (view === 'chat' && fromLeftEdge) {
|
||||||
|
// Potential swipe to sidebar
|
||||||
|
} else if (view === 'sidebar' && fromRightEdge && canSwipeToChatRef.current) {
|
||||||
|
// Potential swipe to chat
|
||||||
|
} else {
|
||||||
|
return; // Not an edge touch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture current visual position (handles mid-animation interruption)
|
||||||
|
tray.style.transition = 'none';
|
||||||
|
baseOffset = getCurrentTranslateX();
|
||||||
|
tray.style.transform = `translateX(${baseOffset}px)`;
|
||||||
|
|
||||||
|
startX = touch.clientX;
|
||||||
|
startY = touch.clientY;
|
||||||
|
startTime = Date.now();
|
||||||
|
lastX = touch.clientX;
|
||||||
|
lastTime = startTime;
|
||||||
|
tracking = false;
|
||||||
|
decided = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchMove(e) {
|
||||||
|
if (e.touches.length !== 1) return;
|
||||||
|
if (decided && !tracking) return; // Decided vertical, ignore
|
||||||
|
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const deltaX = touch.clientX - startX;
|
||||||
|
const deltaY = touch.clientY - startY;
|
||||||
|
|
||||||
|
// If we haven't decided intent yet
|
||||||
|
if (!decided) {
|
||||||
|
const absDx = Math.abs(deltaX);
|
||||||
|
const absDy = Math.abs(deltaY);
|
||||||
|
// Need some movement to decide
|
||||||
|
if (absDx < 5 && absDy < 5) return;
|
||||||
|
|
||||||
|
decided = true;
|
||||||
|
if (absDx > absDy * 1.5) {
|
||||||
|
tracking = true;
|
||||||
|
setIsSwiping(true);
|
||||||
|
} else {
|
||||||
|
// Vertical — let it scroll
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tracking) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const sw = screenWidthRef.current;
|
||||||
|
const view = activeViewRef.current;
|
||||||
|
let newOffset = baseOffset + deltaX;
|
||||||
|
|
||||||
|
// Clamp: tray can only be between -sw (chat) and 0 (sidebar)
|
||||||
|
if (view === 'chat') {
|
||||||
|
// On chat, base is -sw. Allow swiping right (toward 0) but not past
|
||||||
|
newOffset = Math.max(-sw, Math.min(0, newOffset));
|
||||||
|
} else {
|
||||||
|
// On sidebar, base is 0. Allow swiping left (toward -sw) but not past
|
||||||
|
newOffset = Math.max(-sw, Math.min(0, newOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
tray.style.transform = `translateX(${newOffset}px)`;
|
||||||
|
lastX = touch.clientX;
|
||||||
|
lastTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd(e) {
|
||||||
|
if (!tracking) {
|
||||||
|
decided = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracking = false;
|
||||||
|
decided = false;
|
||||||
|
setIsSwiping(false);
|
||||||
|
|
||||||
|
const sw = screenWidthRef.current;
|
||||||
|
const view = activeViewRef.current;
|
||||||
|
const currentOffset = getCurrentTranslateX();
|
||||||
|
const elapsed = Date.now() - lastTime;
|
||||||
|
const velocity = elapsed > 0 ? Math.abs(lastX - startX) / (Date.now() - startTime) : 0;
|
||||||
|
|
||||||
|
let shouldTransition = false;
|
||||||
|
|
||||||
|
if (view === 'chat') {
|
||||||
|
// Swiping right to sidebar: progress is how far from -sw toward 0
|
||||||
|
const progress = (currentOffset - (-sw)) / sw;
|
||||||
|
shouldTransition = progress > SNAP_THRESHOLD || velocity > VELOCITY_THRESHOLD;
|
||||||
|
} else {
|
||||||
|
// Swiping left to chat: progress is how far from 0 toward -sw
|
||||||
|
const progress = Math.abs(currentOffset) / sw;
|
||||||
|
shouldTransition = progress > SNAP_THRESHOLD || velocity > VELOCITY_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
tray.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||||
|
|
||||||
|
if (shouldTransition) {
|
||||||
|
if (view === 'chat') {
|
||||||
|
tray.style.transform = 'translateX(0px)';
|
||||||
|
setActiveView('sidebar');
|
||||||
|
} else {
|
||||||
|
tray.style.transform = `translateX(-${sw}px)`;
|
||||||
|
setActiveView('chat');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Snap back
|
||||||
|
if (view === 'chat') {
|
||||||
|
tray.style.transform = `translateX(-${sw}px)`;
|
||||||
|
} else {
|
||||||
|
tray.style.transform = 'translateX(0px)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tray.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||||
|
tray.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||||
|
tray.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
tray.removeEventListener('touchstart', onTouchStart);
|
||||||
|
tray.removeEventListener('touchmove', onTouchMove);
|
||||||
|
tray.removeEventListener('touchend', onTouchEnd);
|
||||||
|
};
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
// Resting style: set initial position without transition on mount / view change
|
||||||
|
const trayStyle = useMemo(() => {
|
||||||
|
if (!enabled) return {};
|
||||||
|
return {
|
||||||
|
transform: activeView === 'chat' ? `translateX(-${screenWidthRef.current}px)` : 'translateX(0px)',
|
||||||
|
};
|
||||||
|
}, [enabled, activeView]);
|
||||||
|
|
||||||
|
return { activeView, goToChat, goToSidebar, trayRef, trayStyle, isSwiping };
|
||||||
|
}
|
||||||
@@ -701,7 +701,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.date-divider span {
|
.date-divider span {
|
||||||
background-color: var(--bg-primary);
|
/* background-color: var(--bg-primary); */
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -3288,12 +3288,40 @@ body {
|
|||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar fills entire screen on mobile */
|
/* Sliding tray for swipe navigation */
|
||||||
.is-mobile .sidebar {
|
.mobile-swipe-tray {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 200vw;
|
||||||
|
height: 100%;
|
||||||
|
touch-action: pan-y;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-swipe-panel {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
|
height: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
overscroll-behavior-x: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable pointer events on children during active swipe */
|
||||||
|
.mobile-swipe-tray.is-swiping .mobile-swipe-panel > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar fills its panel on mobile */
|
||||||
|
.is-mobile .sidebar {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide members list on mobile (also enforced in JS) */
|
/* Hide members list on mobile (also enforced in JS) */
|
||||||
@@ -3345,7 +3373,8 @@ body {
|
|||||||
|
|
||||||
/* Chat container takes full width */
|
/* Chat container takes full width */
|
||||||
.is-mobile .chat-container {
|
.is-mobile .chat-container {
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Channel topic - hide on very small screens (also hidden via JS) */
|
/* Channel topic - hide on very small screens (also hidden via JS) */
|
||||||
@@ -3355,7 +3384,8 @@ body {
|
|||||||
|
|
||||||
/* FriendsView takes full width */
|
/* FriendsView takes full width */
|
||||||
.is-mobile .friends-view {
|
.is-mobile .friends-view {
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search panel full-width on mobile */
|
/* Search panel full-width on mobile */
|
||||||
@@ -3364,6 +3394,12 @@ body {
|
|||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove border between server-list and channel-list on mobile */
|
||||||
|
.is-mobile .server-list {
|
||||||
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { PresenceProvider } from '../contexts/PresenceContext';
|
|||||||
import { getUserPref, setUserPref } from '../utils/userPreferences';
|
import { getUserPref, setUserPref } from '../utils/userPreferences';
|
||||||
import { usePlatform } from '../platform';
|
import { usePlatform } from '../platform';
|
||||||
import { useIsMobile } from '../hooks/useIsMobile';
|
import { useIsMobile } from '../hooks/useIsMobile';
|
||||||
|
import { useSwipeNavigation } from '../hooks/useSwipeNavigation';
|
||||||
import IncomingCallUI from '../components/IncomingCallUI';
|
import IncomingCallUI from '../components/IncomingCallUI';
|
||||||
import Avatar from '../components/Avatar';
|
import Avatar from '../components/Avatar';
|
||||||
import callRingSound from '../assets/sounds/default_call_sound.mp3';
|
import callRingSound from '../assets/sounds/default_call_sound.mp3';
|
||||||
@@ -36,7 +37,12 @@ const Chat = () => {
|
|||||||
const [activeDMChannel, setActiveDMChannel] = useState(null);
|
const [activeDMChannel, setActiveDMChannel] = useState(null);
|
||||||
const [showMembers, setShowMembers] = useState(true);
|
const [showMembers, setShowMembers] = useState(true);
|
||||||
const [showPinned, setShowPinned] = useState(false);
|
const [showPinned, setShowPinned] = useState(false);
|
||||||
const [mobileView, setMobileView] = useState('sidebar');
|
|
||||||
|
const { activeView: mobileView, goToChat, goToSidebar, trayRef, trayStyle, isSwiping } =
|
||||||
|
useSwipeNavigation({
|
||||||
|
enabled: isMobile,
|
||||||
|
canSwipeToChat: activeChannel !== null || activeDMChannel !== null || view === 'me'
|
||||||
|
});
|
||||||
|
|
||||||
// Jump-to-message state (for search result clicks)
|
// Jump-to-message state (for search result clicks)
|
||||||
const [jumpToMessageId, setJumpToMessageId] = useState(null);
|
const [jumpToMessageId, setJumpToMessageId] = useState(null);
|
||||||
@@ -190,22 +196,22 @@ const Chat = () => {
|
|||||||
|
|
||||||
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
|
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
|
||||||
setView('me');
|
setView('me');
|
||||||
if (isMobile) setMobileView('chat');
|
if (isMobile) goToChat();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error opening DM:', err);
|
console.error('Error opening DM:', err);
|
||||||
}
|
}
|
||||||
}, [convex, isMobile]);
|
}, [convex, isMobile, goToChat]);
|
||||||
|
|
||||||
const handleSelectChannel = useCallback((channelId) => {
|
const handleSelectChannel = useCallback((channelId) => {
|
||||||
setActiveChannel(channelId);
|
setActiveChannel(channelId);
|
||||||
setShowPinned(false);
|
setShowPinned(false);
|
||||||
if (isMobile) setMobileView('chat');
|
if (isMobile) goToChat();
|
||||||
}, [isMobile]);
|
}, [isMobile, goToChat]);
|
||||||
|
|
||||||
const handleMobileBack = useCallback(() => {
|
const handleMobileBack = useCallback(() => {
|
||||||
setMobileView('sidebar');
|
goToSidebar();
|
||||||
}, []);
|
}, [goToSidebar]);
|
||||||
|
|
||||||
const activeChannelObj = channels.find(c => c._id === activeChannel);
|
const activeChannelObj = channels.find(c => c._id === activeChannel);
|
||||||
const { watchingStreamOf } = useVoice();
|
const { watchingStreamOf } = useVoice();
|
||||||
@@ -686,13 +692,13 @@ const Chat = () => {
|
|||||||
|
|
||||||
const handleSetActiveDMChannel = useCallback((dm) => {
|
const handleSetActiveDMChannel = useCallback((dm) => {
|
||||||
setActiveDMChannel(dm);
|
setActiveDMChannel(dm);
|
||||||
if (isMobile && dm) setMobileView('chat');
|
if (isMobile && dm) goToChat();
|
||||||
}, [isMobile]);
|
}, [isMobile, goToChat]);
|
||||||
|
|
||||||
const handleViewChange = useCallback((newView) => {
|
const handleViewChange = useCallback((newView) => {
|
||||||
setView(newView);
|
setView(newView);
|
||||||
if (isMobile) setMobileView('sidebar');
|
if (isMobile) goToSidebar();
|
||||||
}, [isMobile]);
|
}, [isMobile, goToSidebar]);
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return (
|
return (
|
||||||
@@ -704,13 +710,7 @@ const Chat = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSidebar = !isMobile || mobileView === 'sidebar';
|
const sidebarElement = (
|
||||||
const showMainContent = !isMobile || mobileView === 'chat';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PresenceProvider userId={userId}>
|
|
||||||
<div className={`app-container${isMobile ? ' is-mobile' : ''}`}>
|
|
||||||
{showSidebar && (
|
|
||||||
<Sidebar
|
<Sidebar
|
||||||
channels={channels}
|
channels={channels}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
@@ -730,8 +730,30 @@ const Chat = () => {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onStartCallWithUser={handleStartCallWithUser}
|
onStartCallWithUser={handleStartCallWithUser}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PresenceProvider userId={userId}>
|
||||||
|
<div className={`app-container${isMobile ? ' is-mobile' : ''}`}>
|
||||||
|
{isMobile ? (
|
||||||
|
<div
|
||||||
|
ref={trayRef}
|
||||||
|
className={`mobile-swipe-tray${isSwiping ? ' is-swiping' : ''}`}
|
||||||
|
style={trayStyle}
|
||||||
|
>
|
||||||
|
<div className="mobile-swipe-panel">
|
||||||
|
{sidebarElement}
|
||||||
|
</div>
|
||||||
|
<div className="mobile-swipe-panel">
|
||||||
|
{renderMainContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{sidebarElement}
|
||||||
|
{renderMainContent()}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{showMainContent && renderMainContent()}
|
|
||||||
{showPiP && <FloatingStreamPiP onGoBackToStream={handleGoBackToStream} />}
|
{showPiP && <FloatingStreamPiP onGoBackToStream={handleGoBackToStream} />}
|
||||||
{showSearchDropdown && !isMobile && (
|
{showSearchDropdown && !isMobile && (
|
||||||
<SearchDropdown
|
<SearchDropdown
|
||||||
|
|||||||
@@ -370,3 +370,43 @@
|
|||||||
|
|
||||||
--text-feedback-warning: hsl(38.455, 100%, 43.137%);
|
--text-feedback-warning: hsl(38.455, 100%, 43.137%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MOBILE OVERRIDES – Dark theme only
|
||||||
|
Darker, more unified backgrounds when .is-mobile is active.
|
||||||
|
Override --bg-tertiary so inline styles using var(--bg-tertiary)
|
||||||
|
(e.g. .channel-list in Sidebar.jsx) also pick up the new color.
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Override the variable so all var(--bg-tertiary) usages resolve to #1C1D22 */
|
||||||
|
.theme-dark .is-mobile {
|
||||||
|
--bg-tertiary: #1C1D22;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Server list needs a different (darker) color than --bg-tertiary */
|
||||||
|
.theme-dark .is-mobile .server-list {
|
||||||
|
background-color: #141318;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove borders on channel panel and DM view (inline styles, needs !important) */
|
||||||
|
.theme-dark .is-mobile .channel-panel,
|
||||||
|
.theme-dark .is-mobile .channel-list {
|
||||||
|
border-left: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat area/header use var(--bg-primary), not --bg-tertiary, so override explicitly */
|
||||||
|
.theme-dark .is-mobile .chat-container,
|
||||||
|
.theme-dark .is-mobile .chat-area,
|
||||||
|
.theme-dark .is-mobile .chat-header,
|
||||||
|
.theme-dark .is-mobile .chat-input-form {
|
||||||
|
background-color: #1C1D22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .is-mobile .chat-header {
|
||||||
|
border-bottom: 1px solid var(--app-frame-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .is-mobile .chat-input-wrapper {
|
||||||
|
background-color: #26262E;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user