Improve mobile support

This commit is contained in:
2025-10-10 13:15:33 +03:00
Unverified
parent 334b7c0f3a
commit f472f3882a
7 changed files with 92 additions and 105 deletions
+7 -28
View File
@@ -1,10 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ElectronTitleBar } from "./Electron"; import { ElectronTitleBar } from "./Electron";
import { useAppState } from "./pages/chat/state"; import { useAppState } from "./pages/chat/state";
import { useEffect, useState, lazy, Suspense } from "react"; import { useEffect, useState, lazy } from "react";
import { isElectron } from "./core/electron/electron";
import { MINIMUM_WIDTH } from "./core/config";
import useWindowSize from "./core/hooks/useWindowSize";
import ProtectedRoute from "./pages/ProtectedRoute"; import ProtectedRoute from "./pages/ProtectedRoute";
import NotFoundPage from "./pages/not-found/NotFoundPage"; import NotFoundPage from "./pages/not-found/NotFoundPage";
import DownloadAppPage from "./pages/download-app/DownloadAppPage"; import DownloadAppPage from "./pages/download-app/DownloadAppPage";
@@ -17,7 +14,6 @@ const ChatPage = lazy(() => import("./pages/chat/ui/ChatPage"));
export default function App() { export default function App() {
const { restoreUserFromStorage } = useAppState(); const { restoreUserFromStorage } = useAppState();
const { width } = useWindowSize();
const [authReady, setAuthReady] = useState(false); const [authReady, setAuthReady] = useState(false);
// Restore user from localStorage on app initialization // Restore user from localStorage on app initialization
@@ -29,28 +25,13 @@ export default function App() {
return authReady && ( return authReady && (
<BrowserRouter> <BrowserRouter>
{!isElectron && width < MINIMUM_WIDTH && <Navigate to="/download-app" replace />}
<ElectronTitleBar /> <ElectronTitleBar />
<div id="main-wrapper"> <div id="main-wrapper">
<Routes> <Routes>
<Route path="/" element={ <Route path="/" element={<HomePage />} />
<Suspense> <Route path="/login" element={<LoginPage />} />
<HomePage /> <Route path="/register" element={<RegisterPage />} />
</Suspense> <Route path="/download-app" element={<DownloadAppPage />} />
} />
<Route path="/login" element={
<Suspense>
<LoginPage />
</Suspense>
} />
<Route path="/register" element={
<Suspense>
<RegisterPage />
</Suspense>
} />
<Route path="/download-app" element={
<DownloadAppPage />
} />
<Route path="/"> <Route path="/">
<Route path="chat" element={ <Route path="chat" element={
<ProtectedRoute> <ProtectedRoute>
@@ -58,9 +39,7 @@ export default function App() {
</ProtectedRoute> </ProtectedRoute>
} /> } />
</Route> </Route>
<Route path="*" element={ <Route path="*" element={<NotFoundPage />} />
<NotFoundPage />
} />
</Routes> </Routes>
</div> </div>
</BrowserRouter> </BrowserRouter>
@@ -0,0 +1,13 @@
import { Navigate } from "react-router-dom";
import { MINIMUM_WIDTH } from "../config";
import useWindowSize from "./useWindowSize";
export default function useDownloadAppScreen() {
const { width } = useWindowSize();
const isMobile = width < MINIMUM_WIDTH;
return {
isMobile,
navigate: isMobile ? <Navigate to="/download-app" replace /> : null
};
}
+3
View File
@@ -12,11 +12,14 @@ import { initialize, isSupported, startElectronReceiver, subscribe } from "@/cor
import { isElectron } from "@/core/electron/electron"; import { isElectron } from "@/core/electron/electron";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./auth.scss"; import "./auth.scss";
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
export default function LoginPage() { export default function LoginPage() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]); const [alerts, updateAlerts] = useImmer<Alert[]>([]);
const setUser = useAppState(state => state.setUser); const setUser = useAppState(state => state.setUser);
const navigate = useNavigate(); const navigate = useNavigate();
const { navigate: navigateDownloadApp } = useDownloadAppScreen();
if (navigateDownloadApp) return navigateDownloadApp;
function showAlert(type: AlertType, message: string) { function showAlert(type: AlertType, message: string) {
updateAlerts((alerts) => { alerts.push({type: type, message: message}) }); updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
+3
View File
@@ -10,11 +10,14 @@ import { MaterialTextField } from "@/core/components/TextField";
import { ensureKeysOnLogin } from "@/core/api/authApi"; import { ensureKeysOnLogin } from "@/core/api/authApi";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./auth.scss"; import "./auth.scss";
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
export default function RegisterPage() { export default function RegisterPage() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]); const [alerts, updateAlerts] = useImmer<Alert[]>([]);
const setUser = useAppState(state => state.setUser); const setUser = useAppState(state => state.setUser);
const navigate = useNavigate(); const navigate = useNavigate();
const { navigate: navigateDownloadApp } = useDownloadAppScreen();
if (navigateDownloadApp) return navigateDownloadApp;
function showAlert(type: AlertType, message: string) { function showAlert(type: AlertType, message: string) {
updateAlerts((alerts) => { alerts.push({type: type, message: message}) }); updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
@@ -84,7 +84,6 @@
} }
.context-menu { .context-menu {
position: fixed;
background: $color-dark-surface; background: $color-dark-surface;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
@@ -269,24 +268,3 @@
transform: rotate(90deg); transform: rotate(90deg);
} }
} }
// Mobile responsive
@media (max-width: 768px) {
.reaction-bar {
padding: 6px;
}
.reaction-emoji-button,
.reaction-expand-button {
width: 28px;
height: 28px;
}
.reaction-emoji-button {
font-size: 16px;
}
.reaction-expand-button .material-symbols {
font-size: 16px;
}
}
+4
View File
@@ -1,8 +1,12 @@
import { LeftPanel } from "./left/LeftPanel"; import { LeftPanel } from "./left/LeftPanel";
import { RightPanel } from "./right/RightPanel"; import { RightPanel } from "./right/RightPanel";
import "@/pages/chat/css/chat.scss"; import "@/pages/chat/css/chat.scss";
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
export default function ChatPage() { export default function ChatPage() {
const { navigate: navigateDownloadApp } = useDownloadAppScreen();
if (navigateDownloadApp) return navigateDownloadApp;
return ( return (
<div id="chat-interface"> <div id="chat-interface">
<div className="all-container"> <div className="all-container">
+62 -55
View File
@@ -1,7 +1,7 @@
import { Navigate, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAppState } from "@/pages/chat/state"; import { useAppState } from "@/pages/chat/state";
import { isElectron } from "@/core/electron/electron";
import "./home.scss"; import "./home.scss";
import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
function GitHubLink({ children }: { children: React.ReactNode }) { function GitHubLink({ children }: { children: React.ReactNode }) {
return ( return (
@@ -18,18 +18,24 @@ function SupportLink({ children }: { children: React.ReactNode }) {
export default function HomePage() { export default function HomePage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAppState(); const { user } = useAppState();
const { isMobile } = useDownloadAppScreen();
const isLoggedIn = user.authToken && user.currentUser;
const handleGetStarted = () => { function handleGetStarted() {
if (user.authToken && user.currentUser) { if (isMobile) {
navigate("/download-app");
} else if (isLoggedIn) {
navigate("/chat"); navigate("/chat");
} else { } else {
navigate("/login"); navigate("/login");
} }
} }
if (isElectron) { const openBtn = (
return <Navigate to="/chat" />; <mdui-button variant="filled" onClick={handleGetStarted}>
} {isMobile ? "Скачать приложение" : isLoggedIn ? "Перейти в чат" : "Войти"}
</mdui-button>
);
return ( return (
<div className="homepage"> <div className="homepage">
@@ -48,15 +54,7 @@ export default function HomePage() {
<mdui-button variant="text">Поддержка</mdui-button> <mdui-button variant="text">Поддержка</mdui-button>
</SupportLink> </SupportLink>
{user.authToken ? ( {openBtn}
<mdui-button variant="filled" onClick={() => navigate("/chat")}>
Перейти в чат
</mdui-button>
) : (
<mdui-button variant="filled" onClick={() => navigate("/login")}>
Войти
</mdui-button>
)}
</nav> </nav>
</div> </div>
</div> </div>
@@ -74,18 +72,13 @@ export default function HomePage() {
поддержкой файлов и уведомлений. Создан для тех, кто ценит приватность и свободу. поддержкой файлов и уведомлений. Создан для тех, кто ценит приватность и свободу.
</p> </p>
<div className="hero-actions"> <div className="hero-actions">
<mdui-button {openBtn}
variant="filled" {!isMobile && <mdui-button
onClick={handleGetStarted}
>
{user.authToken ? "Перейти в чат" : "Начать общение"}
</mdui-button>
<mdui-button
variant="outlined" variant="outlined"
onClick={() => navigate("/register")} onClick={() => navigate("/register")}
> >
Зарегистрироваться Зарегистрироваться
</mdui-button> </mdui-button>}
</div> </div>
</div> </div>
<div className="hero-visual"> <div className="hero-visual">
@@ -129,7 +122,7 @@ export default function HomePage() {
<div className="features-grid"> <div className="features-grid">
<div className="feature-card"> <div className="feature-card">
<div className="feature-icon"> <div className="feature-icon">
<mdui-icon name="security"></mdui-icon> <mdui-icon name="security" />
</div> </div>
<h4>End-to-End Шифрование</h4> <h4>End-to-End Шифрование</h4>
<p> <p>
@@ -140,7 +133,7 @@ export default function HomePage() {
<div className="feature-card"> <div className="feature-card">
<div className="feature-icon"> <div className="feature-icon">
<mdui-icon name="code"></mdui-icon> <mdui-icon name="code" />
</div> </div>
<h4>100% открытый код</h4> <h4>100% открытый код</h4>
<p> <p>
@@ -151,7 +144,7 @@ export default function HomePage() {
<div className="feature-card"> <div className="feature-card">
<div className="feature-icon"> <div className="feature-icon">
<mdui-icon name="attach_file"></mdui-icon> <mdui-icon name="attach_file" />
</div> </div>
<h4>Обмен Файлами</h4> <h4>Обмен Файлами</h4>
<p> <p>
@@ -162,7 +155,7 @@ export default function HomePage() {
<div className="feature-card"> <div className="feature-card">
<div className="feature-icon"> <div className="feature-icon">
<mdui-icon name="notifications"></mdui-icon> <mdui-icon name="notifications" />
</div> </div>
<h4>Уведомления</h4> <h4>Уведомления</h4>
<p> <p>
@@ -173,7 +166,7 @@ export default function HomePage() {
<div className="feature-card"> <div className="feature-card">
<div className="feature-icon"> <div className="feature-icon">
<mdui-icon name="edit"></mdui-icon> <mdui-icon name="edit" />
</div> </div>
<h4>Редактирование</h4> <h4>Редактирование</h4>
<p> <p>
@@ -184,7 +177,7 @@ export default function HomePage() {
<div className="feature-card"> <div className="feature-card">
<div className="feature-icon"> <div className="feature-icon">
<mdui-icon name="computer"></mdui-icon> <mdui-icon name="computer" />
</div> </div>
<h4>Кроссплатформенность</h4> <h4>Кроссплатформенность</h4>
<p> <p>
@@ -205,20 +198,28 @@ export default function HomePage() {
уведомлений и автономной работы. уведомлений и автономной работы.
</p> </p>
<div className="download-buttons"> <div className="download-buttons">
<a {!isMobile ? (
href="https://github.com/Toolbox-io/FromChat/actions/workflows/build.yml" <>
target="_blank" <a
rel="noopener noreferrer" href="https://github.com/Toolbox-io/FromChat/actions/workflows/build.yml"
> target="_blank"
<mdui-button variant="filled"> rel="noopener noreferrer"
<mdui-icon name="download" slot="icon"></mdui-icon> >
Скачать для ПК <mdui-button variant="filled">
<mdui-icon name="download" slot="icon" />
Скачать для ПК
</mdui-button>
</a>
<mdui-button variant="outlined" onClick={() => navigate("/login")}>
<mdui-icon name="language" slot="icon" />
Веб-версия
</mdui-button>
</>
) : (
<mdui-button variant="filled" onClick={() => navigate("/download-app")}>
Скачать приложение
</mdui-button> </mdui-button>
</a> )}
<mdui-button variant="outlined" onClick={() => navigate("/login")}>
<mdui-icon name="language" slot="icon"></mdui-icon>
Веб-версия
</mdui-button>
</div> </div>
</div> </div>
</div> </div>
@@ -232,18 +233,24 @@ export default function HomePage() {
Присоединяйтесь к FromChat и общайтесь безопасно с друзьями и коллегами. Присоединяйтесь к FromChat и общайтесь безопасно с друзьями и коллегами.
</p> </p>
<div className="cta-actions"> <div className="cta-actions">
<mdui-button {isMobile ? (
variant="filled" <mdui-button variant="filled" onClick={() => navigate("/download-app")}>
onClick={() => navigate("/register")} Скачать приложение
> </mdui-button>
Создать аккаунт ) : (
</mdui-button> <>
<mdui-button <mdui-button
variant="outlined" variant="filled"
onClick={() => navigate("/login")} onClick={() => navigate("/register")}>
> Создать аккаунт
Войти </mdui-button>
</mdui-button> <mdui-button
variant="outlined"
onClick={() => navigate("/login")}>
Войти
</mdui-button>
</>
)}
</div> </div>
</div> </div>
</div> </div>