diff --git a/api/v1/auth.go b/api/v1/auth.go index 16c62c31..981a6ba5 100644 --- a/api/v1/auth.go +++ b/api/v1/auth.go @@ -169,6 +169,24 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again") } 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{ Username: userInfo.Identifier, // The new signup user should be normal user by default. diff --git a/api/v1/resource.go b/api/v1/resource.go index 5b5a5f9d..d171cb31 100644 --- a/api/v1/resource.go +++ b/api/v1/resource.go @@ -656,7 +656,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc return fmt.Errorf("Failed to find SystemSettingStorageServiceIDName: %s", err) } - storageServiceID := DatabaseStorage + storageServiceID := LocalStorage if systemSettingStorageServiceID != nil { err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID) if err != nil { @@ -672,15 +672,13 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc } create.Blob = fileBytes return nil - } - - // `LocalStorage` means save blob into local disk - if storageServiceID == LocalStorage { + } else if storageServiceID == LocalStorage { + // `LocalStorage` means save blob into local disk systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()}) if err != nil { return fmt.Errorf("Failed to find SystemSettingLocalStoragePathName: %s", err) } - localStoragePath := "assets/{filename}" + localStoragePath := "assets/{timestamp}_{filename}" if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" { err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath) if err != nil { diff --git a/api/v1/storage.go b/api/v1/storage.go index c49df677..e00ea65a 100644 --- a/api/v1/storage.go +++ b/api/v1/storage.go @@ -13,6 +13,7 @@ import ( const ( // LocalStorage means the storage service is local file system. + // Default storage service is local file system. LocalStorage int32 = -1 // DatabaseStorage means the storage service is database. 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) } if systemSetting != nil { - storageServiceID := DatabaseStorage + storageServiceID := LocalStorage err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err) diff --git a/api/v1/system.go b/api/v1/system.go index 72e649f9..766c1210 100644 --- a/api/v1/system.go +++ b/api/v1/system.go @@ -89,7 +89,7 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { Appearance: "system", ExternalURL: "", }, - StorageServiceID: DatabaseStorage, + StorageServiceID: LocalStorage, LocalStoragePath: "assets/{timestamp}_{filename}", MemoDisplayWithUpdatedTs: false, } diff --git a/store/db/seed/10002__memo.sql b/store/db/seed/10002__memo.sql index 8fa7d313..3db52e5c 100644 --- a/store/db/seed/10002__memo.sql +++ b/store/db/seed/10002__memo.sql @@ -2,7 +2,7 @@ INSERT INTO memo (`id`, `content`, `creator_id`) VALUES ( - 1001, + 1, "#Hello πŸ‘‹ Welcome to memos.", 101 ); @@ -16,7 +16,7 @@ INSERT INTO ) VALUES ( - 1002, + 2, '#TODO - [x] Take more photos about **πŸŒ„ sunset**; - [x] Clean the room; @@ -35,7 +35,7 @@ INSERT INTO ) VALUES ( - 1003, + 3, "**[Slash](https://github.com/boojack/slash)**: A bookmarking and url shortener, save and share your links very easily. ![](https://github.com/boojack/slash/raw/main/resources/demo.gif) @@ -54,7 +54,7 @@ INSERT INTO ) VALUES ( - 1004, + 4, '#TODO - [x] Take more photos about **πŸŒ„ sunset**; - [ ] Clean the classroom; @@ -74,7 +74,7 @@ INSERT INTO ) VALUES ( - 1005, + 5, 'δΈ‰δΊΊθ‘ŒοΌŒεΏ…ζœ‰ζˆ‘εΈˆη„‰οΌπŸ‘¨β€πŸ«', 102, 'PUBLIC' diff --git a/store/db/seed/10003__memo_organizer.sql b/store/db/seed/10003__memo_organizer.sql index 324ea790..d41d9717 100644 --- a/store/db/seed/10003__memo_organizer.sql +++ b/store/db/seed/10003__memo_organizer.sql @@ -1,9 +1,9 @@ INSERT INTO memo_organizer (`memo_id`, `user_id`, `pinned`) VALUES - (1001, 101, 1); + (1, 101, 1); INSERT INTO memo_organizer (`memo_id`, `user_id`, `pinned`) VALUES - (1003, 101, 1); \ No newline at end of file + (3, 101, 1); \ No newline at end of file diff --git a/web/src/components/FloatingNavButton.tsx b/web/src/components/FloatingNavButton.tsx new file mode 100644 index 00000000..3ea05e3a --- /dev/null +++ b/web/src/components/FloatingNavButton.tsx @@ -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 ( + <> + +
+ + + +
+ + navigate("/")}>Back to home + +
+ + ); +}; + +export default FloatingNavButton; diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index efda89d9..4282d8ad 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -1,5 +1,4 @@ import { Divider } from "@mui/joy"; -import { isEqual, uniqWith } from "lodash-es"; import { memo, useEffect, useRef, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; @@ -7,7 +6,7 @@ import { Link } from "react-router-dom"; import { UNKNOWN_ID } from "@/helpers/consts"; import { getRelativeTimeString } from "@/helpers/datetime"; import { useFilterStore, useMemoStore, useUserStore } from "@/store/module"; -import { useMemoCacheStore, useUserV1Store } from "@/store/v1"; +import { useUserV1Store } from "@/store/v1"; import { useTranslate } from "@/utils/i18n"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; import { showCommonDialog } from "./Dialog/CommonDialog"; @@ -23,24 +22,20 @@ import "@/less/memo.less"; interface Props { memo: Memo; - showCreator?: boolean; showVisibility?: boolean; - showRelatedMemos?: boolean; lazyRendering?: boolean; } const Memo: React.FC = (props: Props) => { - const { memo, showCreator, showRelatedMemos, lazyRendering } = props; + const { memo, lazyRendering } = props; const { i18n } = useTranslation(); const t = useTranslate(); const filterStore = useFilterStore(); const userStore = useUserStore(); const memoStore = useMemoStore(); - const memoCacheStore = useMemoCacheStore(); const userV1Store = useUserV1Store(); const [shouldRender, setShouldRender] = useState(lazyRendering ? false : true); - const [createdTimeStr, setCreatedTimeStr] = useState(getRelativeTimeString(memo.displayTs)); - const [relatedMemoList, setRelatedMemoList] = useState([]); + const [displayTime, setDisplayTime] = useState(getRelativeTimeString(memo.displayTs)); const memoContainerRef = useRef(null); const readonly = userStore.isVisitorMode() || userStore.getCurrentUsername() !== memo.creatorUsername; const creator = userV1Store.getUserByUsername(memo.creatorUsername); @@ -50,27 +45,12 @@ const Memo: React.FC = (props: Props) => { userV1Store.getOrFetchUserByUsername(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. useEffect(() => { let intervalFlag: any = -1; if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) { intervalFlag = setInterval(() => { - setCreatedTimeStr(getRelativeTimeString(memo.displayTs)); + setDisplayTime(getRelativeTimeString(memo.displayTs)); }, 1000 * 1); } @@ -246,26 +226,25 @@ const Memo: React.FC = (props: Props) => { <>
-
- {showCreator && creator && ( +

+ {creator && ( <> - {creator.nickname} + {creator.nickname} )} - - {createdTimeStr} - -

+ + {displayTime} + +

- {memo.pinned && } {!readonly && ( <> - +
@@ -306,24 +285,8 @@ const Memo: React.FC = (props: Props) => { onMemoContentDoubleClick={handleMemoContentDoubleClick} /> - {!showRelatedMemos && } +
- - {showRelatedMemos && relatedMemoList.length > 0 && ( - <> -

- - Related memos -

- {relatedMemoList.map((relatedMemo) => { - return ( -
- -
- ); - })} - - )} ); }; diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 61cf9a61..b2823460 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -9,12 +9,7 @@ import Empty from "./Empty"; import Memo from "./Memo"; import "@/less/memo-list.less"; -interface Props { - showCreator?: boolean; -} - -const MemoList: React.FC = (props: Props) => { - const { showCreator } = props; +const MemoList: React.FC = () => { const t = useTranslate(); const memoStore = useMemoStore(); const userStore = useUserStore(); @@ -142,7 +137,7 @@ const MemoList: React.FC = (props: Props) => { return (
{sortedMemos.map((memo) => ( - + ))} {isFetching ? (
diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index 645010f4..7ac73a36 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -156,7 +156,10 @@ const PreferencesSection = () => { {userList.map((user) => ( {user.id} - {user.username} + + {user.username} + {user.rowStatus === "ARCHIVED" && "(Archived)"} + {user.nickname} {user.email} diff --git a/web/src/components/UserAvatar.tsx b/web/src/components/UserAvatar.tsx index 281a7dd3..4832d24e 100644 --- a/web/src/components/UserAvatar.tsx +++ b/web/src/components/UserAvatar.tsx @@ -8,7 +8,7 @@ interface Props { const UserAvatar = (props: Props) => { const { avatarUrl, className } = props; return ( -
+
); diff --git a/web/src/helpers/datetime.ts b/web/src/helpers/datetime.ts index 8904ea4e..8b06a2aa 100644 --- a/web/src/helpers/datetime.ts +++ b/web/src/helpers/datetime.ts @@ -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" const formatOpts = { style: formatStyle, numeric: "auto" } as Intl.RelativeTimeFormatOptions; - const relTime = new Intl.RelativeTimeFormat(locale, formatOpts); - if (pastTimeMillis < minMillis) { return relTime.format(-Math.round(pastTimeMillis / secMillis), "second"); } - if (pastTimeMillis < hourMillis) { return relTime.format(-Math.round(pastTimeMillis / minMillis), "minute"); } - if (pastTimeMillis < dayMillis) { return relTime.format(-Math.round(pastTimeMillis / hourMillis), "hour"); } - if (pastTimeMillis < dayMillis * 7) { return relTime.format(-Math.round(pastTimeMillis / dayMillis), "day"); } - if (pastTimeMillis < dayMillis * 30) { return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 7)), "week"); } - if (pastTimeMillis < dayMillis * 365) { return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 30)), "month"); } diff --git a/web/src/less/memo-content.less b/web/src/less/memo-content.less index e8253cfa..20cdbb30 100644 --- a/web/src/less/memo-content.less +++ b/web/src/less/memo-content.less @@ -1,5 +1,5 @@ .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 { @apply w-full max-w-full word-break text-base leading-6; diff --git a/web/src/less/memo.less b/web/src/less/memo.less index 6d855c67..1cd5185b 100644 --- a/web/src/less/memo.less +++ b/web/src/less/memo.less @@ -8,30 +8,6 @@ > .memo-top-wrapper { @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 { @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; &.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 { @apply w-4 h-auto dark:text-gray-300; diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index 79520495..c65842d5 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -93,7 +93,7 @@ const Explore = () => {
{sortedMemos.map((memo) => { - return ; + return ; })} {isComplete ? ( memos.length === 0 && ( diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 9ccd32b1..359b323a 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -33,7 +33,7 @@ const MemoDetail = () => { }, [location]); return ( -
+
@@ -45,7 +45,7 @@ const MemoDetail = () => { (memo ? ( <>
- +
{ + 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 ( + <> +
+
+ {!loadingState.isLoading && + (user ? ( + <> +
+
+
+
+ +
+

{user?.nickname}

+
+
+
+ +
+ +
+
+
+ + ) : ( + <> +

Not found

+ + ))} +
+
+ + + + ); +}; + +export default UserProfile; diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 55ce4695..bd04d464 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -13,6 +13,7 @@ const Auth = lazy(() => import("@/pages/Auth")); const AuthCallback = lazy(() => import("@/pages/AuthCallback")); const Explore = lazy(() => import("@/pages/Explore")); const Home = lazy(() => import("@/pages/Home")); +const UserProfile = lazy(() => import("@/pages/UserProfile")); const MemoDetail = lazy(() => import("@/pages/MemoDetail")); const EmbedMemo = lazy(() => import("@/pages/EmbedMemo")); const NotFound = lazy(() => import("@/pages/NotFound")); @@ -78,28 +79,6 @@ const router = createBrowserRouter([ return redirect("/explore"); }, }, - { - path: "u/:username", - element: , - 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", element: , @@ -238,6 +217,20 @@ const router = createBrowserRouter([ return null; }, }, + { + path: "u/:username", + element: , + loader: async () => { + await initialGlobalStateLoader(); + + try { + await initialUserState(); + } catch (error) { + // do nth + } + return null; + }, + }, { path: "*", element: ,