diff options
-rw-r--r-- | FrontEnd/src/App.tsx | 2 | ||||
-rw-r--r-- | FrontEnd/src/components/alert/AlertHost.tsx | 126 | ||||
-rw-r--r-- | FrontEnd/src/components/alert/AlertService.ts | 111 | ||||
-rw-r--r-- | FrontEnd/src/components/alert/alert.css | 25 | ||||
-rw-r--r-- | FrontEnd/src/components/alert/index.ts | 8 | ||||
-rw-r--r-- | FrontEnd/src/pages/setting/index.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelineCard.tsx | 4 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx | 5 | ||||
-rw-r--r-- | FrontEnd/src/pages/timeline/TimelinePostView.tsx | 5 | ||||
-rw-r--r-- | FrontEnd/src/services/user.ts | 15 |
10 files changed, 190 insertions, 115 deletions
diff --git a/FrontEnd/src/App.tsx b/FrontEnd/src/App.tsx index b5b076ec..58463d08 100644 --- a/FrontEnd/src/App.tsx +++ b/FrontEnd/src/App.tsx @@ -10,7 +10,7 @@ import LoginPage from "./pages/login"; import RegisterPage from "./pages/register"; import TimelinePage from "./pages/timeline"; import LoadingPage from "./pages/loading"; -import AlertHost from "./components/alert/AlertHost"; +import { AlertHost } from "./components/alert"; export default function App() { return ( diff --git a/FrontEnd/src/components/alert/AlertHost.tsx b/FrontEnd/src/components/alert/AlertHost.tsx index b234ac03..23f62472 100644 --- a/FrontEnd/src/components/alert/AlertHost.tsx +++ b/FrontEnd/src/components/alert/AlertHost.tsx @@ -1,95 +1,56 @@ -import * as React from "react"; -import without from "lodash/without"; -import { useTranslation } from "react-i18next"; +import { useEffect, useState } from "react"; import classNames from "classnames"; -import { alertService, AlertInfoEx, AlertInfo } from "~src/services/alert"; -import { convertI18nText } from "~src/common"; - +import { ThemeColor, useC, Text } from "../common"; import IconButton from "../button/IconButton"; +import { alertService, AlertInfoWithId } from "./AlertService"; + import "./alert.css"; interface AutoCloseAlertProps { - alert: AlertInfo; - close: () => void; + color: ThemeColor; + message: Text; + onDismiss?: () => void; + onIn?: () => void; + onOut?: () => void; } -export const AutoCloseAlert: React.FC<AutoCloseAlertProps> = (props) => { - const { alert, close } = props; - const { dismissTime } = alert; - - const { t } = useTranslation(); - - const timerTag = React.useRef<number | null>(null); - const closeHandler = React.useRef<(() => void) | null>(null); - - React.useEffect(() => { - closeHandler.current = close; - }, [close]); - - React.useEffect(() => { - const tag = - dismissTime === "never" - ? null - : typeof dismissTime === "number" - ? window.setTimeout(() => closeHandler.current?.(), dismissTime) - : window.setTimeout(() => closeHandler.current?.(), 5000); - timerTag.current = tag; - return () => { - if (tag != null) { - window.clearTimeout(tag); - } - }; - }, [dismissTime]); - - const cancelTimer = (): void => { - const { current: tag } = timerTag; - if (tag != null) { - window.clearTimeout(tag); - } - }; +function Alert({ + color, + message, + onDismiss, + onIn, + onOut, +}: AutoCloseAlertProps) { + const c = useC(); return ( <div - className={classNames( - "m-3 cru-alert", - "cru-" + (alert.type ?? "primary") - )} - onClick={cancelTimer} + className={classNames("cru-alert", `cru-theme-${color}`)} + onPointerEnter={onIn} + onPointerLeave={onOut} > - <div className="cru-alert-content"> - {(() => { - const { message, customMessage } = alert; - if (customMessage != null) { - return customMessage; - } else { - return convertI18nText(message, t); - } - })()} - </div> - <div className="cru-alert-close-button-container"> - <IconButton - icon="x" - className="cru-alert-close-button" - onClick={close} - /> - </div> + <div className="cru-alert-message">{c(message)}</div> + <IconButton + icon="x" + color="danger" + className="cru-alert-close-button" + onClick={onDismiss} + /> </div> ); -}; +} -const AlertHost: React.FC = () => { - const [alerts, setAlerts] = React.useState<AlertInfoEx[]>([]); +export default function AlertHost() { + const [alerts, setAlerts] = useState<AlertInfoWithId[]>([]); - React.useEffect(() => { - const consume = (alert: AlertInfoEx): void => { - setAlerts((old) => [...old, alert]); - }; + useEffect(() => { + alertService.registerListener(setAlerts); - alertService.registerConsumer(consume); return () => { - alertService.unregisterConsumer(consume); + alertService.unregisterListener(setAlerts); + alert; }; }, []); @@ -97,17 +58,22 @@ const AlertHost: React.FC = () => { <div className="alert-container"> {alerts.map((alert) => { return ( - <AutoCloseAlert + <Alert key={alert.id} - alert={alert} - close={() => { - setAlerts((old) => without(old, alert)); + message={alert.message} + color={alert.color ?? "primary"} + onIn={() => { + alertService.clearDismissTimer(alert.id); + }} + onOut={() => { + alertService.resetDismissTimer(alert.id); + }} + onDismiss={() => { + alertService.dismiss(alert.id); }} /> ); })} </div> ); -}; - -export default AlertHost; +} diff --git a/FrontEnd/src/components/alert/AlertService.ts b/FrontEnd/src/components/alert/AlertService.ts new file mode 100644 index 00000000..b3565b8a --- /dev/null +++ b/FrontEnd/src/components/alert/AlertService.ts @@ -0,0 +1,111 @@ +import { ThemeColor, Text } from "../common"; + +export interface AlertInfo { + color?: ThemeColor; + message: Text; + dismissTime?: number | "never"; +} + +export interface AlertInfoWithId extends AlertInfo { + id: number; +} + +interface AlertServiceAlert extends AlertInfoWithId { + timerId: number | null; +} + +export type AlertsListener = (alerts: AlertInfoWithId[]) => void; + +export class AlertService { + private listeners: AlertsListener[] = []; + private alerts: AlertServiceAlert[] = []; + private currentId = 1; + + getAlert(alertId?: number | null | undefined): AlertServiceAlert | null { + for (const alert of this.alerts) { + if (alert.id === alertId) return alert; + } + return null; + } + + registerListener(listener: AlertsListener): void { + this.listeners.push(listener); + listener(this.alerts); + } + + unregisterListener(listener: AlertsListener): void { + this.listeners = this.listeners.filter((l) => l !== listener); + } + + notify() { + for (const listener of this.listeners) { + listener(this.alerts); + } + } + + push(alert: AlertInfo): void { + const newAlert: AlertServiceAlert = { + ...alert, + id: this.currentId++, + timerId: null, + }; + + this.alerts.push(newAlert); + this._resetDismissTimer(newAlert); + + this.notify(); + } + + private _dismiss(alert: AlertServiceAlert) { + if (alert.timerId != null) { + window.clearTimeout(alert.timerId); + } + this.alerts = this.alerts.filter((a) => a !== alert); + this.notify(); + } + + dismiss(alertId?: number | null | undefined) { + const alert = this.getAlert(alertId); + if (alert != null) { + this._dismiss(alert); + } + } + + private _clearDismissTimer(alert: AlertServiceAlert) { + if (alert.timerId != null) { + window.clearTimeout(alert.timerId); + alert.timerId = null; + } + } + + clearDismissTimer(alertId?: number | null | undefined) { + const alert = this.getAlert(alertId); + if (alert != null) { + this._clearDismissTimer(alert); + } + } + + private _resetDismissTimer( + alert: AlertServiceAlert, + dismissTime?: number | null | undefined, + ) { + this._clearDismissTimer(alert); + + const realDismissTime = dismissTime ?? alert.dismissTime; + + if (typeof realDismissTime === "number") { + alert.timerId = window.setTimeout(() => { + this._dismiss(alert); + }, realDismissTime); + } + } + + resetDismissTimer(alertId?: number | null | undefined) { + const alert = this.getAlert(alertId); + if (alert != null) { + this._resetDismissTimer(alert); + } + } +} + +export const alertService = new AlertService(); diff --git a/FrontEnd/src/components/alert/alert.css b/FrontEnd/src/components/alert/alert.css index 54c2b87f..063af933 100644 --- a/FrontEnd/src/components/alert/alert.css +++ b/FrontEnd/src/components/alert/alert.css @@ -5,29 +5,18 @@ .cru-alert {
border-radius: 5px;
- border: var(--cru-key-color) 1px solid;
- color: var(--cru-key-t-color);
- background-color: var(--cru-key-b1-color);
+ border: var(--cru-theme-color) 1px solid;
+ color: var(--cru-text-primary-color);
+ background-color: var(--cru-container-background-color);
- display: flex;
- overflow: hidden;
-}
-
-.cru-alert-content {
padding: 0.5em 2em;
-}
-.cru-alert-close-button-container {
- flex-shrink: 0;
- margin-left: auto;
- width: 2em;
- text-align: center;
display: flex;
align-items: center;
- justify-content: center;
- background-color: var(--cru-key-t-color);
}
+.cru-alert-message {}
+
.cru-alert-close-button {
- color: var(--cru-key-color);
-}
+ margin-left: auto;
+}
\ No newline at end of file diff --git a/FrontEnd/src/components/alert/index.ts b/FrontEnd/src/components/alert/index.ts new file mode 100644 index 00000000..1be0c2ec --- /dev/null +++ b/FrontEnd/src/components/alert/index.ts @@ -0,0 +1,8 @@ +import { alertService, AlertInfo } from "./AlertService"; +import { default as AlertHost } from "./AlertHost"; + +export { alertService, AlertHost }; + +export function pushAlert(alert: AlertInfo): void { + alertService.push(alert); +} diff --git a/FrontEnd/src/pages/setting/index.tsx b/FrontEnd/src/pages/setting/index.tsx index d2333134..25434339 100644 --- a/FrontEnd/src/pages/setting/index.tsx +++ b/FrontEnd/src/pages/setting/index.tsx @@ -10,10 +10,10 @@ import classNames from "classnames"; import { useUser, userService } from "~src/services/user"; import { getHttpUserClient } from "~src/http/user"; -import { pushAlert } from "~src/services/alert"; import { useC, Text } from "~src/common"; +import { pushAlert } from "~src/components/alert"; import { useDialog, DialogProvider, @@ -192,7 +192,7 @@ function RegisterCodeSettingItem() { onClick={(event) => { void navigator.clipboard.writeText(registerCode).then(() => { pushAlert({ - type: "create", + color: "create", message: "settings.myRegisterCodeCopied", }); }); diff --git a/FrontEnd/src/pages/timeline/TimelineCard.tsx b/FrontEnd/src/pages/timeline/TimelineCard.tsx index 44dfbf9e..1e0e9b75 100644 --- a/FrontEnd/src/pages/timeline/TimelineCard.tsx +++ b/FrontEnd/src/pages/timeline/TimelineCard.tsx @@ -2,11 +2,11 @@ import { useState } from "react"; import { HubConnectionState } from "@microsoft/signalr"; import { useUser } from "~src/services/user"; -import { pushAlert } from "~src/services/alert"; import { HttpTimelineInfo } from "~src/http/timeline"; import { getHttpBookmarkClient } from "~src/http/bookmark"; +import { pushAlert } from "~src/components/alert"; import { useMobile } from "~src/components/hooks"; import { Dialog, DialogProvider, useDialog } from "~src/components/dialog"; import UserAvatar from "~src/components/user/UserAvatar"; @@ -89,7 +89,7 @@ export default function TimelineCard(props: TimelinePageCardProps) { message: timeline.isBookmark ? "timeline.removeBookmarkFail" : "timeline.addBookmarkFail", - type: "danger", + color: "danger", }); }); }} diff --git a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx index 3bc4dab3..70925cd9 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx @@ -10,10 +10,9 @@ import { HttpTimelinePostPostRequestData, } from "~src/http/timeline"; -import { pushAlert } from "~src/services/alert"; - import base64 from "~src/utilities/base64"; +import { pushAlert } from "~src/components/alert"; import BlobImage from "~src/components/BlobImage"; import LoadingButton from "~src/components/button/LoadingButton"; import PopupMenu from "~src/components/menu/PopupMenu"; @@ -141,7 +140,7 @@ function TimelinePostEdit(props: TimelinePostEditProps) { const onPostError = (): void => { pushAlert({ - type: "danger", + color: "danger", message: "timeline.sendPostFailed", }); }; diff --git a/FrontEnd/src/pages/timeline/TimelinePostView.tsx b/FrontEnd/src/pages/timeline/TimelinePostView.tsx index 5de09b28..b09fe6f8 100644 --- a/FrontEnd/src/pages/timeline/TimelinePostView.tsx +++ b/FrontEnd/src/pages/timeline/TimelinePostView.tsx @@ -5,8 +5,7 @@ import { HttpTimelinePostInfo, } from "~src/http/timeline"; -import { pushAlert } from "~src/services/alert"; - +import { pushAlert } from "~src/components/alert"; import { useClickOutside } from "~src/components/hooks"; import UserAvatar from "~src/components/user/UserAvatar"; import { DialogProvider, useDialog } from "~src/components/dialog"; @@ -44,7 +43,7 @@ export default function TimelinePostView(props: TimelinePostViewProps) { .deletePost(post.timelineOwnerV2, post.timelineNameV2, post.id) .then(onDeleted, () => { pushAlert({ - type: "danger", + color: "danger", message: "timeline.deletePostFailed", }); }); diff --git a/FrontEnd/src/services/user.ts b/FrontEnd/src/services/user.ts index ddba4dab..5f682a36 100644 --- a/FrontEnd/src/services/user.ts +++ b/FrontEnd/src/services/user.ts @@ -8,14 +8,17 @@ import { setHttpToken, axios, HttpBadRequestError } from "~src/http/common"; import { getHttpTokenClient } from "~src/http/token"; import { getHttpUserClient, HttpUser, UserPermission } from "~src/http/user"; -import { pushAlert } from "./alert"; +import { pushAlert } from "~src/components/alert"; interface IAuthUser extends HttpUser { token: string; } export class AuthUser implements IAuthUser { - constructor(user: HttpUser, public token: string) { + constructor( + user: HttpUser, + public token: string, + ) { this.uniqueId = user.uniqueId; this.username = user.username; this.permissions = user.permissions; @@ -61,7 +64,7 @@ export class UserService { if (e.isAxiosError && e.response && e.response.status === 401) { this.userSubject.next(null); pushAlert({ - type: "danger", + color: "danger", message: "user.tokenInvalid", }); } else { @@ -97,11 +100,11 @@ export class UserService { localStorage.removeItem(USER_STORAGE_KEY); this.userSubject.next(null); pushAlert({ - type: "danger", + color: "danger", message: "user.tokenInvalid", }); } - } + }, ); } } @@ -118,7 +121,7 @@ export class UserService { async login( credentials: LoginCredentials, - rememberMe: boolean + rememberMe: boolean, ): Promise<void> { if (this.currentUser) { throw new UiLogicError("Already login."); |