refactor: new mobile layout

This commit is contained in:
Tristan Yang
2023-02-16 08:48:45 +08:00
parent 26e54e10fe
commit 0b07bbf440
33 changed files with 174 additions and 66 deletions
+2
View File
@@ -23,6 +23,7 @@ import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
import GoBackNav from "../GoBackNav";
type Props = {
cid?: number;
dropFiles?: File[];
@@ -133,6 +134,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
}
header={
<header className="h-14 px-5 flex items-center justify-center md:justify-between shadow-[inset_0_-1px_0_rgb(0_0_0_/_10%)]">
<GoBackNav />
<div className="flex items-center gap-1">
<ChannelIcon personal={!is_public} />
<span className="text-gray-800 dark:text-white">{name}</span>
+3 -1
View File
@@ -11,6 +11,7 @@ import LoadMore from "../LoadMore";
import { renderMessageFragment } from "../utils";
import useMessageFeed from "../../../common/hook/useMessageFeed";
import { useAppSelector } from "../../../app/store";
import GoBackNav from "../GoBackNav";
type Props = {
uid: number;
dropFiles?: File[];
@@ -64,7 +65,8 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
</ul>
}
header={
<header className="box-border h-14 px-5 flex items-center justify-between shadow-[inset_0_-1px_0_rgb(0_0_0_/_10%)]">
<header className="box-border h-14 px-5 flex items-center justify-center md:justify-between shadow-[inset_0_-1px_0_rgb(0_0_0_/_10%)]">
<GoBackNav />
<User interactive={false} uid={currUser.uid} />
</header>
}
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import IconArrow from '../../assets/icons/arrow.left.svg';
// type Props = {}
const GoBackNav = () => {
const navigate = useNavigate();
const handleBack = () => {
navigate(-1);
};
return (
<button className='p-2 absolute left-2 md:hidden' onClick={handleBack}>
<IconArrow className="dark:stroke-white w-6 h-6" />
</button>
);
};
export default GoBackNav;
+1 -1
View File
@@ -21,7 +21,7 @@ const GuestBlankPlaceholder = () => {
navigateTo("/login");
};
return (
<section className="flex flex-col items-center bg-transparent">
<section className="flex flex-col items-center">
<h2 className="text-3xl text-gray-600 dark:text-gray-50 font-bold text-center">{t("welcome", { name: serverName })}</h2>
<div className="flex flex-col gap-2">
<span className="text-gray-400 dark:text-gray-200 my-3 text-sm">{t("guest_login_tip")}</span>
@@ -7,6 +7,7 @@ import Layout from "../Layout";
import { renderMessageFragment } from "../utils";
import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store";
import GoBackNav from "../GoBackNav";
type Props = {
cid?: number;
@@ -41,6 +42,7 @@ export default function GuestChannelChat({ cid = 0 }: Props) {
context="channel"
header={
<header className="h-14 px-5 flex items-center justify-center md:justify-between shadow-[inset_0_-1px_0_rgb(0_0_0_/_10%)]">
<GoBackNav />
<div className="flex items-center gap-1">
<ChannelIcon personal={!is_public} />
<span className="text-gray-800 dark:text-white">{name}</span>
+10 -6
View File
@@ -2,6 +2,7 @@ import { FC } from "react";
import { useState, useEffect } from "react";
import Session from "./Session";
import { useAppSelector } from "../../../app/store";
import LoginTip from "../Layout/LoginTip";
export interface ChatSession {
key: string;
id: number;
@@ -40,12 +41,15 @@ const SessionList: FC<Props> = () => {
}, [channelIDs, channelMessage, readChannels, readUsers, loginUid, userMessage]);
return (
<ul className="flex flex-col gap-0.5 p-2 overflow-auto h-[calc(100vh_-_56px_-_38px)]">
{sessions.map((s) => {
const { key, id, mid = 0 } = s;
return <Session key={key} id={id} mid={mid} />;
})}
</ul>
<>
<ul className="flex flex-col gap-0.5 p-2 overflow-auto h-[calc(100vh_-_56px_-_38px)]">
{sessions.map((s) => {
const { key, id, mid = 0 } = s;
return <Session key={key} id={id} mid={mid} />;
})}
</ul>
<LoginTip placement="session" />
</>
);
};
export default SessionList;
+9 -4
View File
@@ -1,13 +1,16 @@
// import { useEffect } from "react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { resetAuthData } from "../../../app/slices/auth.data";
import Button from "../../../common/component/styled/Button";
import useLogout from "../../../common/hook/useLogout";
// type Props = {};
type Props = {
placement?: "session" | "chat"
};
const LoginTip = () => {
const LoginTip = ({ placement = "chat" }: Props) => {
const { t } = useTranslation("welcome");
const { t: ct } = useTranslation();
const dispatch = useDispatch();
@@ -20,8 +23,10 @@ const LoginTip = () => {
};
return (
<div className="flex items-center justify-between bg-slate-200/80 dark:bg-gray-800 rounded-lg w-full p-4">
<span className="text-xs md:text-base text-[#98a2b3] dark:text-gray-100">
<div className={clsx("flex items-center justify-between bg-slate-200/80 dark:bg-gray-800 rounded-lg w-full p-4 border border-solid border-gray-200 dark:border-gray-500",
placement == "session" && "w-[96%] md:hidden fixed bottom-2 left-1/2 -translate-x-1/2"
)}>
<span className="text-xs md:text-base text-gray-400 dark:text-gray-100">
<i className="mr-2 text-xs md:text-lg ">👋</i>
{t("sign_in_tip")}
</span>
+1 -1
View File
@@ -111,7 +111,7 @@ const Layout: FC<Props> = ({
<main className="h-full w-full flex items-start justify-between relative" ref={messagesContainer}>
<div className="rounded-br-2xl w-full flex flex-col h-[calc(100vh_-_56px_-_18px)]">
{children}
<div className={`p-4 pt-0 ${selects ? "selecting" : ""}`}>
<div className={`px-2 py-0 md:p-4 ${selects ? "selecting" : ""}`}>
{readonly ? (
<LoginTip />
) : reachLimit ? (
+9 -9
View File
@@ -1,5 +1,5 @@
import { memo, useState } from "react";
import { useParams } from "react-router-dom";
import { useMatch, useParams } from "react-router-dom";
import clsx from "clsx";
import BlankPlaceholder from "../../common/component/BlankPlaceholder";
@@ -13,9 +13,10 @@ import { useAppSelector } from "../../app/store";
import GuestBlankPlaceholder from "./GuestBlankPlaceholder";
import GuestChannelChat from "./GuestChannelChat";
import GuestSessionList from "./GuestSessionList";
import IconList from '../../assets/icons/list.svg';
function ChatPage() {
const isHomePath = useMatch(`/`);
const isChatHomePath = useMatch(`/chat`);
const [sessionListVisible, setSessionListVisible] = useState(false);
const [channelModalVisible, setChannelModalVisible] = useState(false);
const [usersModalVisible, setUsersModalVisible] = useState(false);
@@ -46,7 +47,7 @@ function ChatPage() {
};
// console.log("temp uid", tmpUid);
const placeholderVisible = channel_id == 0 && user_id == 0;
const isMainPath = isHomePath || isChatHomePath;
return (
<>
{channelModalVisible && (
@@ -55,17 +56,16 @@ function ChatPage() {
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
<div className={clsx(`flex h-full md:pt-2 md:pb-2.5 md:pr-1`, isGuest ? "guest-container md:px-1" : "md:pr-12")}>
{sessionListVisible && <div onClick={toggleSessionList} className="z-30 fixed top-0 left-4 w-screen h-screen bg-black/50 transition-all backdrop-blur-sm"></div>}
<div className={clsx("left-container flex-col md:rounded-l-2xl max-w-[250px] md:min-w-[268px] h-full shadow-[rgb(0_0_0_/_10%)_-1px_0px_0px_inset] fixed md:relative top-0 left-0 z-40 transition-all bg-white dark:!bg-[#1F2A37]", sessionListVisible ? "max-md:translate-x-0" : "max-md:-translate-x-full")}>
<div className={clsx("left-container pb-14 md:pb-0 flex-col md:rounded-l-2xl w-full md:max-w-[250px] md:min-w-[268px] h-full shadow-[rgb(0_0_0_/_10%)_-1px_0px_0px_inset] bg-white dark:!bg-gray-800",
isMainPath ? "flex" : "hidden md:flex"
)}>
<Server readonly={isGuest} />
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
{sessionListVisible ? null : <button className="absolute top-2 -right-[52px] z-50 p-2 bg-none md:hidden" onClick={toggleSessionList}>
<IconList className="dark:stroke-gray-300" />
</button>}
{isGuest && <footer className="text-xs text-gray-300 dark:text-gray-700 text-center">
{isGuest && <footer className="hidden md:block text-xs text-gray-300 dark:text-gray-700 text-center">
Host your own <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-600">voce.chat</a>
</footer>}
</div>
<div className={`right-container md:rounded-r-2xl w-full ${placeholderVisible ? "h-full flex-center" : ""} bg-white dark:!bg-[#384250]`}>
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
{placeholderVisible && (isGuest ? <GuestBlankPlaceholder /> : <BlankPlaceholder />)}
{channel_id !== 0 &&
(isGuest ? (
+1 -1
View File
@@ -118,7 +118,7 @@ function FavsPage() {
removeFavorite(id);
};
return (
<div className="h-screen flex bg-white mt-2 mr-6 mb-2.5 overflow-auto dark:bg-[#384250] rounded-2xl">
<div className="h-screen flex bg-white mt-2 mr-6 mb-2.5 overflow-auto dark:bg-gray-700 rounded-2xl">
<div className=" md:min-w-[268px] p-2 shadow-inner-[-1px_0px_0px_rgba(0,_0,_0,_0.1)]">
<ul className="flex flex-col gap-0.5">
{Filters.map(({ icon, title, filter: f }) => {
+67
View File
@@ -0,0 +1,67 @@
import clsx from 'clsx';
import React from 'react';
import { NavLink, useLocation, useMatch } from 'react-router-dom';
import { useAppSelector } from '../../app/store';
import ChatIcon from "../../assets/icons/chat.svg";
import UserIcon from "../../assets/icons/user.svg";
// type Props = {}
const MobileNavs = () => {
const isHomePath = useMatch(`/`);
const { pathname } = useLocation();
const isChatHomePath = useMatch(`/chat`);
const isDMChat = useMatch(`/chat/dm/:user_id`);
const isChannelChat = useMatch(`/chat/channel/:channel_id`);
const {
ui: {
rememberedNavs: { chat: chatPath, user: userPath }
}
} = useAppSelector((store) => {
return {
ui: store.ui,
loginUid: store.authData.user?.uid,
guest: store.authData.guest
};
});
const linkClass = `flex`;
const isChatPage = isHomePath || pathname.startsWith("/chat");
const isChattingPage = !!isDMChat || !!isChannelChat;
console.log("rrr", isDMChat, isChannelChat);
// 有点绕
const chatNav = isChatHomePath ? "/chat" : chatPath || "/chat";
const userNav = userPath || "/users";
return (
<ul className={clsx('flex justify-around py-2 fixed bottom-0 left-0 w-full bg-gray-100 dark:bg-gray-800 md:hidden', isChattingPage && "hidden")}>
<li>
<NavLink
className={() => `${linkClass}`}
to={chatNav}
>
{({ isActive }) => {
const active = isActive || isChatPage;
return <div className='flex flex-col gap-1 items-center'>
<ChatIcon className={!active ? "fill-gray-600" : "fill-gray-400"} />
<span className={clsx('text-xs', !active ? "text-gray-500" : "text-gray-400")}>Chats</span>
</div>;
}}
</NavLink>
</li>
<li>
<NavLink className={() => `${linkClass}`} to={userNav}>
{({ isActive: active }) => {
return <div className='flex flex-col gap-1 items-center'>
<UserIcon className={!active ? "fill-gray-600" : "fill-gray-400"} />
<span className={clsx('text-xs', !active ? "text-gray-500" : "text-gray-400")}>Contacts</span>
</div>;
}}
</NavLink>
</li>
</ul>
);
};
export default MobileNavs;
+7 -7
View File
@@ -14,7 +14,7 @@ import UserIcon from "../../assets/icons/user.svg";
import FavIcon from "../../assets/icons/bookmark.svg";
import FolderIcon from "../../assets/icons/folder.svg";
import { useAppSelector } from "../../app/store";
import { isMobile } from "../../common/utils";
import MobileNavs from "./MobileNavs";
function HomePage() {
@@ -49,14 +49,13 @@ function HomePage() {
const chatNav = isChatHomePath ? "/chat" : chatPath || "/chat";
const userNav = userPath || "/users";
const linkClass = `flex items-center gap-2.5 px-3 py-2 font-semibold text-sm text-gray-600 rounded-lg hover:bg-gray-800/10`;
const mobile = isMobile();
return (
<>
<Manifest />
{!guest && <Notification />}
<div className={`vocechat-container flex w-screen h-screen bg-[#e5e7eb] dark:bg-[#121926]`}>
{!guest && (
<div className={`h-full flex flex-col items-center relative w-16 bg-[#e5e7eb] dark:bg-[#121926] transition-all`}>
<div className={`hidden md:flex h-full flex-col items-center relative w-16 bg-[#e5e7eb] dark:bg-[#121926] transition-all`}>
{loginUid && <User uid={loginUid} />}
<nav className="flex flex-col gap-1 px-3 py-6">
<NavLink
@@ -64,28 +63,28 @@ function HomePage() {
to={chatNav}
>
{({ isActive }) => {
return <Tooltip disabled={mobile} tip={t("chat")}>
return <Tooltip tip={t("chat")}>
<ChatIcon className={(isActive || isChattingPage) ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400 hover:bg-primary-400' : ""}`} to={userNav}>
{({ isActive }) => {
return <Tooltip disabled={mobile} tip={t("members")}>
return <Tooltip tip={t("members")}>
<UserIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400 hover:bg-primary-400' : ""}`} to={"/favs"}>
{({ isActive }) => {
return <Tooltip disabled={mobile} tip={t("favs")}>
return <Tooltip tip={t("favs")}>
<FavIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400 hover:bg-primary-400' : ""}`} to={"/files"}>
{({ isActive }) => {
return <Tooltip disabled={mobile} tip={t("files")}>
return <Tooltip tip={t("files")}>
<FolderIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
@@ -98,6 +97,7 @@ function HomePage() {
<Outlet />
</div>
</div>
{!guest && <MobileNavs />}
</>
);
}
+1 -1
View File
@@ -118,7 +118,7 @@ const InvitePage: FC = () => {
if (!valid) return <>invite token expires or invalid</>;
return (
<div className="flex-center h-screen dark:bg-[#384250]">
<div className="flex-center h-screen dark:bg-gray-700">
<div className="py-8 px-10 shadow-md rounded-xl">
<div className="flex-center flex-col pb-6">
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="w-14 h-14 mb-7 rounded-full" />
+1 -1
View File
@@ -124,7 +124,7 @@ export default function LoginPage() {
return (
<div className="flex-center h-screen dark:bg-[#384250]">
<div className="flex-center h-screen dark:bg-gray-700">
<div className="py-8 px-10 shadow-md rounded-xl">
<div className="flex-center flex-col pb-6">
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="w-14 h-14 mb-3 md:mb-7 rounded-full" />
@@ -35,10 +35,10 @@ const UpdateFrontendURL = ({ refreshInviteLink }: Props) => {
}
}, [isSuccess]);
return (
<div className="absolute left-1/2 -translate-x-1/2 bottom-8 border-2 border-solid border-[#67E3F9] bg-[#F5FEFF] rounded-lg px-2 py-3 flex justify-start gap-4">
<div className="absolute left-1/2 -translate-x-1/2 bottom-8 border-2 border-solid border-primary-300 bg-primary-25 rounded-lg px-2 py-3 flex justify-start gap-4">
<InfoIcon />
<div className="flex flex-col items-start gap-2">
<span className="text-sm text-[#0E7090] mb-1">{t("update_domain_tip")}</span>
<span className="text-sm text-primary-700 mb-1">{t("update_domain_tip")}</span>
<div className="w-[400px] rounded flex gap-2">
<StyledInput type={"url"} className="!shadow-none !bg-transparent" placeholder="Frontend URL" value={frontUrl} onChange={handleUpdateUrl} />
<StyledButton disabled={!frontUrl || isLoading} onClick={updateFrontUrl} className="small ">
+1 -1
View File
@@ -2,7 +2,7 @@ import { Outlet } from "react-router-dom";
export default function RegContainer() {
return (
<div className="flex-center h-screen dark:bg-[#384250]">
<div className="flex-center h-screen dark:bg-gray-700">
<div className="py-8 px-10 shadow-md rounded-xl">
<Outlet />
</div>
+1 -1
View File
@@ -82,7 +82,7 @@ function ResourceManagement({ fileMessages }) {
}, [view, filter]);
return (
<div className="h-screen md:overflow-y-scroll flex flex-col items-start my-2 mr-6 rounded-2xl bg-white dark:bg-[#384250]">
<div className="h-screen md:overflow-y-scroll flex flex-col items-start my-2 mr-6 rounded-2xl bg-white dark:bg-gray-700">
<Search value={filter.name} updateSearchValue={handleUpdateSearch} />
<div className="flex justify-between w-full px-4 py-5">
<Filter filter={filter} updateFilter={updateFilter} />
@@ -76,7 +76,7 @@ const CreateAPIKeyModal = ({ closeModal, uid }: Props) => {
</div>
: <form ref={formRef} className="w-full flex flex-col gap-2 items-center" action="/">
<div className="flex flex-col gap-1 w-full">
<label htmlFor={"name"} className="text-sm text-[#6b7280]">Name</label>
<label htmlFor={"name"} className="text-sm text-gray-500">Name</label>
<Input name={"name"} required placeholder='Please input API Key name'></Input>
</div>
</form>}
+2 -2
View File
@@ -79,11 +79,11 @@ const CreateModal = ({ closeModal }: Props) => {
>
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
<div className="flex flex-col items-start gap-1 w-full">
<label htmlFor={"name"} className="text-sm text-[#6b7280]">Name</label>
<label htmlFor={"name"} className="text-sm text-gray-500">Name</label>
<Input name={"name"} required placeholder='Please input bot name'></Input>
</div>
<div className="flex flex-col items-start gap-1 w-full">
<label htmlFor={"webhook_url"} className="text-sm text-[#6b7280]">Webhook URL (Optional)</label>
<label htmlFor={"webhook_url"} className="text-sm text-gray-500">Webhook URL (Optional)</label>
<Input name={"webhook_url"} type="url" placeholder='Please input webhook url'></Input>
</div>
</form>
@@ -58,7 +58,7 @@ const WebhookModal = ({ uid, webhook, closeModal }: Props) => {
}
>
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
<label htmlFor={"webhook"} className="text-sm text-[#6b7280]">Webhook URL</label>
<label htmlFor={"webhook"} className="text-sm text-gray-500">Webhook URL</label>
<Input name={"webhook"} value={url} onChange={handleUrlChange} type="url"></Input>
</form>
</StyledModal>
+12 -6
View File
@@ -8,6 +8,7 @@ import Profile from "../../common/component/Profile";
import BlankPlaceholder from "../../common/component/BlankPlaceholder";
import useFilteredUsers from "../../common/hook/useFilteredUsers";
import clsx from "clsx";
function UsersPage() {
const dispatch = useDispatch();
@@ -23,12 +24,14 @@ function UsersPage() {
}, [pathname]);
if (!users) return null;
const isUserDetail = !!user_id;
return (
<div className="flex h-full pt-2 md:pr-12 pb-2.5">
<div className="rounded-l-2xl bg-white dark:bg-[#1F2A37] relative flex flex-col md:min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]">
<div className={clsx("flex h-full md:pt-2 md:pb-2.5 md:pr-12")}>
<div className={clsx("md:rounded-l-2xl bg-white dark:bg-gray-800 relative flex flex-col w-full md:w-auto md:min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]",
isUserDetail && "hidden"
)}>
<Search input={input} updateInput={updateInput} />
<div className="px-2 py-3 overflow-scroll">
<div className="px-2 pt-3 pb-20 md:py-3 overflow-scroll">
<nav className="flex flex-col md:gap-1">
{users.map(({ uid }) => {
return (
@@ -40,8 +43,11 @@ function UsersPage() {
</nav>
</div>
</div>
<div className={`rounded-r-2xl bg-white w-full flex justify-center items-start ${!user_id ? "h-full items-center" : ""} dark:bg-[#384250]`}>
{user_id ? <Profile uid={+user_id} /> : <BlankPlaceholder type="user" />}
<div className={clsx(`md:rounded-r-2xl bg-white w-full flex justify-center items-start dark:bg-gray-700`,
!user_id && "h-full items-center",
!isUserDetail && "hidden md:flex"
)}>
{isUserDetail ? <Profile uid={+user_id} /> : <BlankPlaceholder type="user" />}
</div>
</div>
);