1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-26 04:02:46 +01:00

banners/offline banner

This commit is contained in:
Puyodead1 2023-09-05 00:45:29 -04:00
parent 2f30c9bb1b
commit 698ac81ed4
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
8 changed files with 170 additions and 12 deletions

View File

@ -8,7 +8,9 @@ import RegistrationPage from "./pages/RegistrationPage";
import { reaction } from "mobx";
import Loader from "./components/Loader";
import OfflineBanner from "./components/banners/OfflineBanner";
import { UnauthenticatedGuard } from "./components/guards/UnauthenticatedGuard";
import { BannerContext } from "./contexts/BannerContext";
import useLogger from "./hooks/useLogger";
import AppPage from "./pages/AppPage";
import LogoutPage from "./pages/LogoutPage";
@ -18,6 +20,7 @@ import { Globals } from "./utils/Globals";
function App() {
const app = useAppStore();
const bannerContext = React.useContext(BannerContext);
const logger = useLogger("App");
const navigate = useNavigate();
@ -54,6 +57,15 @@ function App() {
return dispose;
}, []);
React.useEffect(() => {
if (!app.isNetworkConnected)
bannerContext.setContent({
forced: true,
element: <OfflineBanner />,
});
else bannerContext.close();
}, [app.isNetworkConnected]);
return (
<Loader>
<Routes>

68
src/components/Banner.tsx Normal file
View File

@ -0,0 +1,68 @@
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
import styled from "styled-components";
import { BannerContext } from "../contexts/BannerContext";
import Icon from "./Icon";
import IconButton from "./IconButton";
const Container = styled(motion.div)`
display: flex;
justify-content: center;
align-items: center;
`;
const CloseWrapper = styled(IconButton)`
position: absolute;
right: 1%;
`;
function Banner() {
const bannerContext = React.useContext(BannerContext);
return (
<AnimatePresence>
{bannerContext.content && (
<Container
variants={{
show: {
// slide down
y: 0,
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
hide: {
// slide up
y: "-100%",
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
}}
initial="hide"
animate="show"
exit="hide"
onAnimationComplete={() => {
console.log("animation complete");
}}
style={bannerContext.content.style}
>
{bannerContext.content.element}
{!bannerContext.content.forced && (
<CloseWrapper
onClick={() => {
bannerContext.close();
}}
>
<Icon icon="mdiClose" color="var(--text)" size="24px" />
</CloseWrapper>
)}
</Container>
)}
</AnimatePresence>
);
}
export default Banner;

View File

@ -0,0 +1,24 @@
import styled from "styled-components";
import Icon from "../Icon";
const Wrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;
const Text = styled.span`
padding: 10px;
color: var(--warning);
`;
function OfflineBanner() {
return (
<Wrapper>
<Text>You are offline</Text>
<Icon icon="mdiWifiStrengthOff" color="var(--warning)" size="24px" />
</Wrapper>
);
}
export default OfflineBanner;

View File

@ -0,0 +1,31 @@
// context to handle banner open/close state
import { MotionStyle } from "framer-motion";
import React from "react";
export interface BannerContent {
element: React.ReactNode;
style?: MotionStyle;
forced?: boolean;
}
export type BannerContextType = {
content?: BannerContent;
setContent: React.Dispatch<React.SetStateAction<BannerContent | undefined>>;
close: () => void;
};
// @ts-expect-error not specifying a default value here
export const BannerContext = React.createContext<BannerContextType>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const BannerContextProvider: React.FC<any> = ({ children }) => {
const [content, setContent] = React.useState<BannerContent>();
const close = () => {
// clear content
setContent(undefined);
};
return <BannerContext.Provider value={{ content, setContent, close }}>{children}</BannerContext.Provider>;
};

View File

@ -6,7 +6,6 @@ h5,
h6,
p,
span {
color: var(--text);
padding: 0;
margin: 0;
}

View File

@ -16,6 +16,7 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import ModalRenderer from "./components/modals/ModalRenderer";
import { BannerContextProvider } from "./contexts/BannerContext";
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
import Theme from "./contexts/Theme";
import "./index.css";
@ -25,7 +26,9 @@ root.render(
<BrowserRouter>
<ModalStack renderModals={ModalRenderer}>
<ContextMenuContextProvider>
<App />
<BannerContextProvider>
<App />
</BannerContextProvider>
</ContextMenuContextProvider>
<Theme />
</ModalStack>

View File

@ -2,22 +2,32 @@ import { observer } from "mobx-react-lite";
import React from "react";
import { useParams } from "react-router-dom";
import styled from "styled-components";
import Banner from "../../components/Banner";
import ChannelSidebar from "../../components/ChannelSidebar";
import Container from "../../components/Container";
import ContainerComponent from "../../components/Container";
import ContextMenu from "../../components/ContextMenu";
import GuildSidebar from "../../components/GuildSidebar";
import Chat from "../../components/messaging/Chat";
import { BannerContext } from "../../contexts/BannerContext";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { useAppStore } from "../../stores/AppStore";
const Wrapper = styled(Container)`
const Container = styled(ContainerComponent)`
display: flex;
flex-direction: column;
`;
const Wrapper = styled.div`
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
`;
function ChannelPage() {
const app = useAppStore();
const contextMenu = React.useContext(ContextMenuContext);
const bannerContext = React.useContext(BannerContext);
const { guildId, channelId } = useParams<{
guildId: string;
@ -27,12 +37,15 @@ function ChannelPage() {
const channel = guild?.channels.get(channelId!);
return (
<Wrapper>
{contextMenu.visible && <ContextMenu {...contextMenu} />}
<GuildSidebar guildId={guildId!} />
<ChannelSidebar channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
<Chat channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
</Wrapper>
<Container>
<Banner />
<Wrapper>
{contextMenu.visible && <ContextMenu {...contextMenu} />}
<GuildSidebar guildId={guildId!} />
<ChannelSidebar channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
<Chat channel={channel} guild={guild} channelId={channelId} guildId={guildId} />
</Wrapper>
</Container>
);
}

View File

@ -24,7 +24,7 @@ export default class AppStore {
// whether the app is still loading
@observable isAppLoading = true;
@observable isNetworkConnected = true; // TODO: Implement this
@observable isNetworkConnected = true;
@observable tokenLoaded = false;
@observable token: string | null = null;
@ -42,6 +42,9 @@ export default class AppStore {
constructor() {
makeAutoObservable(this);
window.addEventListener("online", () => this.setNetworkConnected(true));
window.addEventListener("offline", () => this.setNetworkConnected(false));
}
@action
@ -91,12 +94,17 @@ export default class AppStore {
secureLocalStorage.removeItem("token");
}
@action
setNetworkConnected(value: boolean) {
this.isNetworkConnected = value;
}
@computed
/**
* Whether the app is done loading and ready to be displayed
*/
get isReady() {
return !this.isAppLoading && this.isGatewayReady && this.isNetworkConnected;
return !this.isAppLoading && this.isGatewayReady /* && this.isNetworkConnected */;
}
}