aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FrontEnd/src/App.tsx2
-rw-r--r--FrontEnd/src/components/alert/AlertHost.tsx126
-rw-r--r--FrontEnd/src/components/alert/AlertService.ts111
-rw-r--r--FrontEnd/src/components/alert/alert.css25
-rw-r--r--FrontEnd/src/components/alert/index.ts8
-rw-r--r--FrontEnd/src/pages/setting/index.tsx4
-rw-r--r--FrontEnd/src/pages/timeline/TimelineCard.tsx4
-rw-r--r--FrontEnd/src/pages/timeline/TimelinePostCreateView.tsx5
-rw-r--r--FrontEnd/src/pages/timeline/TimelinePostView.tsx5
-rw-r--r--FrontEnd/src/services/user.ts15
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.");