mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
@ -169,6 +169,24 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
|
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
|
||||||
}
|
}
|
||||||
if user == nil {
|
if user == nil {
|
||||||
|
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
|
||||||
|
Name: SystemSettingAllowSignUpName.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allowSignUpSettingValue := false
|
||||||
|
if allowSignUpSetting != nil {
|
||||||
|
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowSignUpSettingValue {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
userCreate := &store.User{
|
userCreate := &store.User{
|
||||||
Username: userInfo.Identifier,
|
Username: userInfo.Identifier,
|
||||||
// The new signup user should be normal user by default.
|
// The new signup user should be normal user by default.
|
||||||
|
@ -656,7 +656,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc
|
|||||||
return fmt.Errorf("Failed to find SystemSettingStorageServiceIDName: %s", err)
|
return fmt.Errorf("Failed to find SystemSettingStorageServiceIDName: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
storageServiceID := DatabaseStorage
|
storageServiceID := LocalStorage
|
||||||
if systemSettingStorageServiceID != nil {
|
if systemSettingStorageServiceID != nil {
|
||||||
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
|
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -672,15 +672,13 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc
|
|||||||
}
|
}
|
||||||
create.Blob = fileBytes
|
create.Blob = fileBytes
|
||||||
return nil
|
return nil
|
||||||
}
|
} else if storageServiceID == LocalStorage {
|
||||||
|
|
||||||
// `LocalStorage` means save blob into local disk
|
// `LocalStorage` means save blob into local disk
|
||||||
if storageServiceID == LocalStorage {
|
|
||||||
systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
|
systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to find SystemSettingLocalStoragePathName: %s", err)
|
return fmt.Errorf("Failed to find SystemSettingLocalStoragePathName: %s", err)
|
||||||
}
|
}
|
||||||
localStoragePath := "assets/{filename}"
|
localStoragePath := "assets/{timestamp}_{filename}"
|
||||||
if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
|
if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
|
||||||
err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
|
err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// LocalStorage means the storage service is local file system.
|
// LocalStorage means the storage service is local file system.
|
||||||
|
// Default storage service is local file system.
|
||||||
LocalStorage int32 = -1
|
LocalStorage int32 = -1
|
||||||
// DatabaseStorage means the storage service is database.
|
// DatabaseStorage means the storage service is database.
|
||||||
DatabaseStorage int32 = 0
|
DatabaseStorage int32 = 0
|
||||||
@ -214,7 +215,7 @@ func (s *APIV1Service) DeleteStorage(c echo.Context) error {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
}
|
}
|
||||||
if systemSetting != nil {
|
if systemSetting != nil {
|
||||||
storageServiceID := DatabaseStorage
|
storageServiceID := LocalStorage
|
||||||
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
|
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
||||||
|
@ -89,7 +89,7 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
|
|||||||
Appearance: "system",
|
Appearance: "system",
|
||||||
ExternalURL: "",
|
ExternalURL: "",
|
||||||
},
|
},
|
||||||
StorageServiceID: DatabaseStorage,
|
StorageServiceID: LocalStorage,
|
||||||
LocalStoragePath: "assets/{timestamp}_{filename}",
|
LocalStoragePath: "assets/{timestamp}_{filename}",
|
||||||
MemoDisplayWithUpdatedTs: false,
|
MemoDisplayWithUpdatedTs: false,
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ INSERT INTO
|
|||||||
memo (`id`, `content`, `creator_id`)
|
memo (`id`, `content`, `creator_id`)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
1001,
|
1,
|
||||||
"#Hello 👋 Welcome to memos.",
|
"#Hello 👋 Welcome to memos.",
|
||||||
101
|
101
|
||||||
);
|
);
|
||||||
@ -16,7 +16,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
1002,
|
2,
|
||||||
'#TODO
|
'#TODO
|
||||||
- [x] Take more photos about **🌄 sunset**;
|
- [x] Take more photos about **🌄 sunset**;
|
||||||
- [x] Clean the room;
|
- [x] Clean the room;
|
||||||
@ -35,7 +35,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
1003,
|
3,
|
||||||
"**[Slash](https://github.com/boojack/slash)**: A bookmarking and url shortener, save and share your links very easily.
|
"**[Slash](https://github.com/boojack/slash)**: A bookmarking and url shortener, save and share your links very easily.
|
||||||

|

|
||||||
|
|
||||||
@ -54,7 +54,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
1004,
|
4,
|
||||||
'#TODO
|
'#TODO
|
||||||
- [x] Take more photos about **🌄 sunset**;
|
- [x] Take more photos about **🌄 sunset**;
|
||||||
- [ ] Clean the classroom;
|
- [ ] Clean the classroom;
|
||||||
@ -74,7 +74,7 @@ INSERT INTO
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
1005,
|
5,
|
||||||
'三人行,必有我师焉!👨🏫',
|
'三人行,必有我师焉!👨🏫',
|
||||||
102,
|
102,
|
||||||
'PUBLIC'
|
'PUBLIC'
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
INSERT INTO
|
INSERT INTO
|
||||||
memo_organizer (`memo_id`, `user_id`, `pinned`)
|
memo_organizer (`memo_id`, `user_id`, `pinned`)
|
||||||
VALUES
|
VALUES
|
||||||
(1001, 101, 1);
|
(1, 101, 1);
|
||||||
|
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
memo_organizer (`memo_id`, `user_id`, `pinned`)
|
memo_organizer (`memo_id`, `user_id`, `pinned`)
|
||||||
VALUES
|
VALUES
|
||||||
(1003, 101, 1);
|
(3, 101, 1);
|
27
web/src/components/FloatingNavButton.tsx
Normal file
27
web/src/components/FloatingNavButton.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Dropdown, IconButton, Menu, MenuButton, MenuItem } from "@mui/joy";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
const FloatingNavButton = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown>
|
||||||
|
<div className="fixed bottom-6 right-6">
|
||||||
|
<MenuButton
|
||||||
|
slots={{ root: IconButton }}
|
||||||
|
slotProps={{ root: { className: "!bg-white dark:!bg-zinc-900 drop-shadow", variant: "outlined", color: "neutral" } }}
|
||||||
|
>
|
||||||
|
<Icon.MoreVertical className="w-5 h-auto" />
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
|
<Menu placement="top-end">
|
||||||
|
<MenuItem onClick={() => navigate("/")}>Back to home</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FloatingNavButton;
|
@ -1,5 +1,4 @@
|
|||||||
import { Divider } from "@mui/joy";
|
import { Divider } from "@mui/joy";
|
||||||
import { isEqual, uniqWith } from "lodash-es";
|
|
||||||
import { memo, useEffect, useRef, useState } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@ -7,7 +6,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { UNKNOWN_ID } from "@/helpers/consts";
|
import { UNKNOWN_ID } from "@/helpers/consts";
|
||||||
import { getRelativeTimeString } from "@/helpers/datetime";
|
import { getRelativeTimeString } from "@/helpers/datetime";
|
||||||
import { useFilterStore, useMemoStore, useUserStore } from "@/store/module";
|
import { useFilterStore, useMemoStore, useUserStore } from "@/store/module";
|
||||||
import { useMemoCacheStore, useUserV1Store } from "@/store/v1";
|
import { useUserV1Store } from "@/store/v1";
|
||||||
import { useTranslate } from "@/utils/i18n";
|
import { useTranslate } from "@/utils/i18n";
|
||||||
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
|
||||||
import { showCommonDialog } from "./Dialog/CommonDialog";
|
import { showCommonDialog } from "./Dialog/CommonDialog";
|
||||||
@ -23,24 +22,20 @@ import "@/less/memo.less";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
showCreator?: boolean;
|
|
||||||
showVisibility?: boolean;
|
showVisibility?: boolean;
|
||||||
showRelatedMemos?: boolean;
|
|
||||||
lazyRendering?: boolean;
|
lazyRendering?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Memo: React.FC<Props> = (props: Props) => {
|
const Memo: React.FC<Props> = (props: Props) => {
|
||||||
const { memo, showCreator, showRelatedMemos, lazyRendering } = props;
|
const { memo, lazyRendering } = props;
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const filterStore = useFilterStore();
|
const filterStore = useFilterStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const memoCacheStore = useMemoCacheStore();
|
|
||||||
const userV1Store = useUserV1Store();
|
const userV1Store = useUserV1Store();
|
||||||
const [shouldRender, setShouldRender] = useState<boolean>(lazyRendering ? false : true);
|
const [shouldRender, setShouldRender] = useState<boolean>(lazyRendering ? false : true);
|
||||||
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs));
|
const [displayTime, setDisplayTime] = useState<string>(getRelativeTimeString(memo.displayTs));
|
||||||
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
|
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const readonly = userStore.isVisitorMode() || userStore.getCurrentUsername() !== memo.creatorUsername;
|
const readonly = userStore.isVisitorMode() || userStore.getCurrentUsername() !== memo.creatorUsername;
|
||||||
const creator = userV1Store.getUserByUsername(memo.creatorUsername);
|
const creator = userV1Store.getUserByUsername(memo.creatorUsername);
|
||||||
@ -50,27 +45,12 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
userV1Store.getOrFetchUserByUsername(memo.creatorUsername);
|
userV1Store.getOrFetchUserByUsername(memo.creatorUsername);
|
||||||
}, [memo.creatorUsername]);
|
}, [memo.creatorUsername]);
|
||||||
|
|
||||||
// Prepare related memos.
|
|
||||||
useEffect(() => {
|
|
||||||
Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then(
|
|
||||||
(results) => {
|
|
||||||
const memoList = [];
|
|
||||||
for (const result of results) {
|
|
||||||
if (result.status === "fulfilled") {
|
|
||||||
memoList.push(result.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setRelatedMemoList(uniqWith(memoList, isEqual));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [memo.relationList]);
|
|
||||||
|
|
||||||
// Update display time string.
|
// Update display time string.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalFlag: any = -1;
|
let intervalFlag: any = -1;
|
||||||
if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) {
|
if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) {
|
||||||
intervalFlag = setInterval(() => {
|
intervalFlag = setInterval(() => {
|
||||||
setCreatedTimeStr(getRelativeTimeString(memo.displayTs));
|
setDisplayTime(getRelativeTimeString(memo.displayTs));
|
||||||
}, 1000 * 1);
|
}, 1000 * 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,26 +226,25 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
<>
|
<>
|
||||||
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && !readonly ? "pinned" : ""}`} ref={memoContainerRef}>
|
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && !readonly ? "pinned" : ""}`} ref={memoContainerRef}>
|
||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<div className="status-text-container">
|
<p className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
|
||||||
{showCreator && creator && (
|
{creator && (
|
||||||
<>
|
<>
|
||||||
<Link className="flex flex-row justify-start items-center" to={`/u/${memo.creatorUsername}`}>
|
<Link className="flex flex-row justify-start items-center" to={`/u/${memo.creatorUsername}`}>
|
||||||
<UserAvatar className="!w-5 !h-auto mr-1" avatarUrl={creator.avatarUrl} />
|
<UserAvatar className="!w-5 !h-auto mr-1" avatarUrl={creator.avatarUrl} />
|
||||||
<span className="text-sm text-gray-600 dark:text-zinc-300">{creator.nickname}</span>
|
<span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-zinc-300">{creator.nickname}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
|
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}>
|
<span className="text-sm text-gray-400" onClick={handleMemoCreatedTimeClick}>
|
||||||
{createdTimeStr}
|
{displayTime}
|
||||||
</Link>
|
</span>
|
||||||
</div>
|
</p>
|
||||||
<div className="btns-container space-x-2">
|
<div className="btns-container space-x-2">
|
||||||
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
|
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
<>
|
<>
|
||||||
<span className="btn more-action-btn">
|
<span className="btn more-action-btn">
|
||||||
<Icon.MoreHorizontal className="icon-img" />
|
<Icon.MoreVertical className="icon-img" />
|
||||||
</span>
|
</span>
|
||||||
<div className="more-action-btns-wrapper">
|
<div className="more-action-btns-wrapper">
|
||||||
<div className="more-action-btns-container min-w-[6em]">
|
<div className="more-action-btns-container min-w-[6em]">
|
||||||
@ -306,24 +285,8 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
onMemoContentDoubleClick={handleMemoContentDoubleClick}
|
||||||
/>
|
/>
|
||||||
<MemoResourceListView resourceList={memo.resourceList} />
|
<MemoResourceListView resourceList={memo.resourceList} />
|
||||||
{!showRelatedMemos && <MemoRelationListView relationList={memo.relationList} />}
|
<MemoRelationListView relationList={memo.relationList} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showRelatedMemos && relatedMemoList.length > 0 && (
|
|
||||||
<>
|
|
||||||
<p className="text-sm dark:text-gray-300 my-2 pl-4 opacity-50 flex flex-row items-center">
|
|
||||||
<Icon.Link className="w-4 h-auto mr-1" />
|
|
||||||
<span>Related memos</span>
|
|
||||||
</p>
|
|
||||||
{relatedMemoList.map((relatedMemo) => {
|
|
||||||
return (
|
|
||||||
<div key={relatedMemo.id} className="w-full">
|
|
||||||
<Memo memo={relatedMemo} showCreator />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,12 +9,7 @@ import Empty from "./Empty";
|
|||||||
import Memo from "./Memo";
|
import Memo from "./Memo";
|
||||||
import "@/less/memo-list.less";
|
import "@/less/memo-list.less";
|
||||||
|
|
||||||
interface Props {
|
const MemoList: React.FC = () => {
|
||||||
showCreator?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MemoList: React.FC<Props> = (props: Props) => {
|
|
||||||
const { showCreator } = props;
|
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@ -142,7 +137,7 @@ const MemoList: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="memo-list-container">
|
<div className="memo-list-container">
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility showCreator={showCreator} />
|
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility />
|
||||||
))}
|
))}
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
<div className="status-text-container fetching-tip">
|
<div className="status-text-container fetching-tip">
|
||||||
|
@ -156,7 +156,10 @@ const PreferencesSection = () => {
|
|||||||
{userList.map((user) => (
|
{userList.map((user) => (
|
||||||
<tr key={user.id}>
|
<tr key={user.id}>
|
||||||
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900">{user.id}</td>
|
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900">{user.id}</td>
|
||||||
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.username}</td>
|
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">
|
||||||
|
{user.username}
|
||||||
|
<span className="ml-1 italic">{user.rowStatus === "ARCHIVED" && "(Archived)"}</span>
|
||||||
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.nickname}</td>
|
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.nickname}</td>
|
||||||
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.email}</td>
|
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.email}</td>
|
||||||
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">
|
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">
|
||||||
|
@ -8,7 +8,7 @@ interface Props {
|
|||||||
const UserAvatar = (props: Props) => {
|
const UserAvatar = (props: Props) => {
|
||||||
const { avatarUrl, className } = props;
|
const { avatarUrl, className } = props;
|
||||||
return (
|
return (
|
||||||
<div className={classNames(`w-8 h-8 overflow-clip`, className)}>
|
<div className={classNames(`w-8 h-auto overflow-clip rounded-full`, className)}>
|
||||||
<img className="w-full h-auto rounded-full min-w-full min-h-full object-cover" src={avatarUrl || "/logo.webp"} alt="" />
|
<img className="w-full h-auto rounded-full min-w-full min-h-full object-cover" src={avatarUrl || "/logo.webp"} alt="" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -130,29 +130,22 @@ export const getRelativeTimeString = (time: number, locale = i18n.language, form
|
|||||||
|
|
||||||
// numeric: "auto" provides "yesterday" for 1 day ago, "always" provides "1 day ago"
|
// numeric: "auto" provides "yesterday" for 1 day ago, "always" provides "1 day ago"
|
||||||
const formatOpts = { style: formatStyle, numeric: "auto" } as Intl.RelativeTimeFormatOptions;
|
const formatOpts = { style: formatStyle, numeric: "auto" } as Intl.RelativeTimeFormatOptions;
|
||||||
|
|
||||||
const relTime = new Intl.RelativeTimeFormat(locale, formatOpts);
|
const relTime = new Intl.RelativeTimeFormat(locale, formatOpts);
|
||||||
|
|
||||||
if (pastTimeMillis < minMillis) {
|
if (pastTimeMillis < minMillis) {
|
||||||
return relTime.format(-Math.round(pastTimeMillis / secMillis), "second");
|
return relTime.format(-Math.round(pastTimeMillis / secMillis), "second");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pastTimeMillis < hourMillis) {
|
if (pastTimeMillis < hourMillis) {
|
||||||
return relTime.format(-Math.round(pastTimeMillis / minMillis), "minute");
|
return relTime.format(-Math.round(pastTimeMillis / minMillis), "minute");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pastTimeMillis < dayMillis) {
|
if (pastTimeMillis < dayMillis) {
|
||||||
return relTime.format(-Math.round(pastTimeMillis / hourMillis), "hour");
|
return relTime.format(-Math.round(pastTimeMillis / hourMillis), "hour");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pastTimeMillis < dayMillis * 7) {
|
if (pastTimeMillis < dayMillis * 7) {
|
||||||
return relTime.format(-Math.round(pastTimeMillis / dayMillis), "day");
|
return relTime.format(-Math.round(pastTimeMillis / dayMillis), "day");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pastTimeMillis < dayMillis * 30) {
|
if (pastTimeMillis < dayMillis * 30) {
|
||||||
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 7)), "week");
|
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 7)), "week");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pastTimeMillis < dayMillis * 365) {
|
if (pastTimeMillis < dayMillis * 365) {
|
||||||
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 30)), "month");
|
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 30)), "month");
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.memo-content-wrapper {
|
.memo-content-wrapper {
|
||||||
@apply w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-200;
|
@apply w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300;
|
||||||
|
|
||||||
> .memo-content-text {
|
> .memo-content-text {
|
||||||
@apply w-full max-w-full word-break text-base leading-6;
|
@apply w-full max-w-full word-break text-base leading-6;
|
||||||
|
@ -8,30 +8,6 @@
|
|||||||
> .memo-top-wrapper {
|
> .memo-top-wrapper {
|
||||||
@apply flex flex-row justify-between items-center w-full h-6 mb-1;
|
@apply flex flex-row justify-between items-center w-full h-6 mb-1;
|
||||||
|
|
||||||
> .status-text-container {
|
|
||||||
@apply flex flex-row justify-start items-center;
|
|
||||||
|
|
||||||
> .time-text {
|
|
||||||
@apply text-sm text-gray-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .name-text {
|
|
||||||
@apply ml-1 text-sm text-gray-400 cursor-pointer hover:opacity-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .status-text {
|
|
||||||
@apply text-xs cursor-pointer ml-2 rounded border px-1;
|
|
||||||
|
|
||||||
&.public {
|
|
||||||
@apply border-green-600 text-green-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.protected {
|
|
||||||
@apply border-gray-400 text-gray-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .btns-container {
|
> .btns-container {
|
||||||
@apply flex flex-row justify-end items-center relative shrink-0;
|
@apply flex flex-row justify-end items-center relative shrink-0;
|
||||||
|
|
||||||
@ -56,7 +32,7 @@
|
|||||||
@apply flex flex-row justify-center items-center leading-6 text-sm rounded hover:bg-gray-200 dark:hover:bg-zinc-600;
|
@apply flex flex-row justify-center items-center leading-6 text-sm rounded hover:bg-gray-200 dark:hover:bg-zinc-600;
|
||||||
|
|
||||||
&.more-action-btn {
|
&.more-action-btn {
|
||||||
@apply w-auto opacity-60 cursor-default hover:bg-transparent;
|
@apply w-auto opacity-50 cursor-default hover:bg-transparent;
|
||||||
|
|
||||||
> .icon-img {
|
> .icon-img {
|
||||||
@apply w-4 h-auto dark:text-gray-300;
|
@apply w-4 h-auto dark:text-gray-300;
|
||||||
|
@ -93,7 +93,7 @@ const Explore = () => {
|
|||||||
<main className="relative w-full h-auto flex flex-col justify-start items-start">
|
<main className="relative w-full h-auto flex flex-col justify-start items-start">
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{sortedMemos.map((memo) => {
|
{sortedMemos.map((memo) => {
|
||||||
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showCreator />;
|
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />;
|
||||||
})}
|
})}
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
memos.length === 0 && (
|
memos.length === 0 && (
|
||||||
|
@ -33,7 +33,7 @@ const MemoDetail = () => {
|
|||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative top-0 w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
|
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
|
||||||
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-6">
|
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-6">
|
||||||
<div className="max-w-2xl w-full flex flex-row justify-center items-center px-4 py-2 mt-2 bg-zinc-100 dark:bg-zinc-800">
|
<div className="max-w-2xl w-full flex flex-row justify-center items-center px-4 py-2 mt-2 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<div className="detail-header flex flex-row justify-start items-center">
|
<div className="detail-header flex flex-row justify-start items-center">
|
||||||
@ -45,7 +45,7 @@ const MemoDetail = () => {
|
|||||||
(memo ? (
|
(memo ? (
|
||||||
<>
|
<>
|
||||||
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
|
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
|
||||||
<Memo memo={memo} showCreator showRelatedMemos />
|
<Memo memo={memo} />
|
||||||
</main>
|
</main>
|
||||||
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
|
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
|
||||||
<Link
|
<Link
|
||||||
|
74
web/src/pages/UserProfile.tsx
Normal file
74
web/src/pages/UserProfile.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import FloatingNavButton from "@/components/FloatingNavButton";
|
||||||
|
import MemoFilter from "@/components/MemoFilter";
|
||||||
|
import MemoList from "@/components/MemoList";
|
||||||
|
import UserAvatar from "@/components/UserAvatar";
|
||||||
|
import useLoading from "@/hooks/useLoading";
|
||||||
|
import { useGlobalStore, useUserStore } from "@/store/module";
|
||||||
|
import { useTranslate } from "@/utils/i18n";
|
||||||
|
|
||||||
|
const UserProfile = () => {
|
||||||
|
const t = useTranslate();
|
||||||
|
const globalStore = useGlobalStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const loadingState = useLoading();
|
||||||
|
const user = userStore.state.user;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentUsername = userStore.getCurrentUsername();
|
||||||
|
userStore
|
||||||
|
.getUserByUsername(currentUsername)
|
||||||
|
.then(() => {
|
||||||
|
loadingState.setFinish();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(t("message.user-not-found"));
|
||||||
|
});
|
||||||
|
}, [userStore.getCurrentUsername()]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.setting.locale) {
|
||||||
|
globalStore.setLocale(user.setting.locale);
|
||||||
|
}
|
||||||
|
}, [user?.setting.locale]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center">
|
||||||
|
{!loadingState.isLoading &&
|
||||||
|
(user ? (
|
||||||
|
<>
|
||||||
|
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
|
||||||
|
<div className="w-full flex flex-row justify-start items-start">
|
||||||
|
<div className="flex-grow shrink w-full">
|
||||||
|
<div className="w-full flex flex-col justify-start items-center py-8">
|
||||||
|
<UserAvatar className="w-16 h-auto mb-4 drop-shadow" avatarUrl={user?.avatarUrl} />
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold text-gray-700 dark:text-gray-300">{user?.nickname}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
|
||||||
|
<MemoFilter />
|
||||||
|
</div>
|
||||||
|
<MemoList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>Not found</p>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<FloatingNavButton />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserProfile;
|
@ -13,6 +13,7 @@ const Auth = lazy(() => import("@/pages/Auth"));
|
|||||||
const AuthCallback = lazy(() => import("@/pages/AuthCallback"));
|
const AuthCallback = lazy(() => import("@/pages/AuthCallback"));
|
||||||
const Explore = lazy(() => import("@/pages/Explore"));
|
const Explore = lazy(() => import("@/pages/Explore"));
|
||||||
const Home = lazy(() => import("@/pages/Home"));
|
const Home = lazy(() => import("@/pages/Home"));
|
||||||
|
const UserProfile = lazy(() => import("@/pages/UserProfile"));
|
||||||
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
|
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
|
||||||
const EmbedMemo = lazy(() => import("@/pages/EmbedMemo"));
|
const EmbedMemo = lazy(() => import("@/pages/EmbedMemo"));
|
||||||
const NotFound = lazy(() => import("@/pages/NotFound"));
|
const NotFound = lazy(() => import("@/pages/NotFound"));
|
||||||
@ -78,28 +79,6 @@ const router = createBrowserRouter([
|
|||||||
return redirect("/explore");
|
return redirect("/explore");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "u/:username",
|
|
||||||
element: <Home />,
|
|
||||||
loader: async () => {
|
|
||||||
await initialGlobalStateLoader();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await initialUserState();
|
|
||||||
} catch (error) {
|
|
||||||
// do nth
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user } = store.getState().user;
|
|
||||||
const { systemStatus } = store.getState().global;
|
|
||||||
|
|
||||||
if (isNullorUndefined(user) && systemStatus.disablePublicMemos) {
|
|
||||||
return redirect("/auth");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "explore",
|
path: "explore",
|
||||||
element: <Explore />,
|
element: <Explore />,
|
||||||
@ -238,6 +217,20 @@ const router = createBrowserRouter([
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "u/:username",
|
||||||
|
element: <UserProfile />,
|
||||||
|
loader: async () => {
|
||||||
|
await initialGlobalStateLoader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initialUserState();
|
||||||
|
} catch (error) {
|
||||||
|
// do nth
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
element: <NotFound />,
|
element: <NotFound />,
|
||||||
|
Reference in New Issue
Block a user