feat: use username instead of uid (#1977)

* #1916 replace userId to username

* resolve

---------

Co-authored-by: Александр Тумайкин <AATumaykin@tsum.ru>
This commit is contained in:
Alexandr Tumaykin
2023-07-20 14:48:39 +03:00
committed by GitHub
parent 336b32004d
commit f91f09adea
24 changed files with 189 additions and 109 deletions

View File

@ -53,9 +53,10 @@ type Memo struct {
Pinned bool `json:"pinned"` Pinned bool `json:"pinned"`
// Related fields // Related fields
CreatorName string `json:"creatorName"` CreatorName string `json:"creatorName"`
ResourceList []*Resource `json:"resourceList"` CreatorUsername string `json:"creatorUsername"`
RelationList []*MemoRelation `json:"relationList"` ResourceList []*Resource `json:"resourceList"`
RelationList []*MemoRelation `json:"relationList"`
} }
type CreateMemoRequest struct { type CreateMemoRequest struct {
@ -354,11 +355,18 @@ func (s *APIV1Service) registerMemoRoutes(g *echo.Group) {
findMemoMessage.CreatorID = &userID findMemoMessage.CreatorID = &userID
} }
if username := c.QueryParam("creatorUsername"); username != "" {
user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
if user != nil {
findMemoMessage.CreatorID = &user.ID
}
}
currentUserID, ok := c.Get(getUserIDContextKey()).(int) currentUserID, ok := c.Get(getUserIDContextKey()).(int)
if !ok { if !ok {
// Anonymous use should only fetch PUBLIC memos with specified user // Anonymous use should only fetch PUBLIC memos with specified user
if findMemoMessage.CreatorID == nil { if findMemoMessage.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo") return echo.NewHTTPError(http.StatusBadRequest, "Missing user to find memo")
} }
findMemoMessage.VisibilityList = []store.Visibility{store.Public} findMemoMessage.VisibilityList = []store.Visibility{store.Public}
} else { } else {
@ -467,6 +475,14 @@ func (s *APIV1Service) registerMemoRoutes(g *echo.Group) {
if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil { if creatorID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
findMemoMessage.CreatorID = &creatorID findMemoMessage.CreatorID = &creatorID
} }
if username := c.QueryParam("creatorUsername"); username != "" {
user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
if user != nil {
findMemoMessage.CreatorID = &user.ID
}
}
if findMemoMessage.CreatorID == nil { if findMemoMessage.CreatorID == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo") return echo.NewHTTPError(http.StatusBadRequest, "Missing user id to find memo")
} }
@ -526,6 +542,13 @@ func (s *APIV1Service) registerMemoRoutes(g *echo.Group) {
findMemoMessage.Pinned = &pinned findMemoMessage.Pinned = &pinned
} }
if username := c.QueryParam("creatorUsername"); username != "" {
user, _ := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
if user != nil {
findMemoMessage.CreatorID = &user.ID
}
}
contentSearch := []string{} contentSearch := []string{}
tag := c.QueryParam("tag") tag := c.QueryParam("tag")
if tag != "" { if tag != "" {
@ -650,6 +673,8 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
memoResponse.CreatorName = user.Username memoResponse.CreatorName = user.Username
} }
memoResponse.CreatorUsername = user.Username
// Compose display ts. // Compose display ts.
memoResponse.DisplayTs = memoResponse.CreatedTs memoResponse.DisplayTs = memoResponse.CreatedTs
// Find memo display with updated ts setting. // Find memo display with updated ts setting.

View File

@ -46,6 +46,7 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
g.GET("/status", func(c echo.Context) error { g.GET("/status", func(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
systemStatus := SystemStatus{ systemStatus := SystemStatus{
Profile: *s.Profile, Profile: *s.Profile,
DBSize: 0, DBSize: 0,
@ -76,7 +77,7 @@ func (s *APIV1Service) registerSystemRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find host user").SetInternal(err)
} }
if hostUser != nil { if hostUser != nil {
systemStatus.Host = convertUserFromStore(hostUser) systemStatus.Host = &User{ID: hostUser.ID}
// data desensitize // data desensitize
systemStatus.Host.OpenID = "" systemStatus.Host.OpenID = ""
systemStatus.Host.Email = "" systemStatus.Host.Email = ""

View File

@ -258,6 +258,26 @@ func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
return c.JSON(http.StatusOK, userMessage) return c.JSON(http.StatusOK, userMessage)
}) })
// GET /user/:username - Get user by username.
g.GET("/user/:username", func(c echo.Context) error {
ctx := c.Request().Context()
username := c.Param("username")
user, err := s.Store.GetUser(ctx, &store.FindUser{Username: &username})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
userMessage := convertUserFromStore(user)
// data desensitize
userMessage.OpenID = ""
userMessage.Email = ""
return c.JSON(http.StatusOK, userMessage)
})
// PUT /user/:id - Update user by id. // PUT /user/:id - Update user by id.
g.PATCH("/user/:id", func(c echo.Context) error { g.PATCH("/user/:id", func(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()

View File

@ -32,7 +32,6 @@ func TestSystemServer(t *testing.T) {
status, err = s.getSystemStatus() status, err = s.getSystemStatus()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, user.ID, status.Host.ID) require.Equal(t, user.ID, status.Host.ID)
require.Equal(t, user.Username, status.Host.Username)
} }
func (s *TestingServer) getSystemStatus() (*apiv1.SystemStatus, error) { func (s *TestingServer) getSystemStatus() (*apiv1.SystemStatus, error) {

View File

@ -6,7 +6,6 @@ import { useTranslate } from "@/utils/i18n";
import { resolution } from "@/utils/layout"; import { resolution } from "@/utils/layout";
import Icon from "./Icon"; import Icon from "./Icon";
import UserBanner from "./UserBanner"; import UserBanner from "./UserBanner";
import showAboutSiteDialog from "./AboutSiteDialog";
import UpgradeVersionView from "./UpgradeVersionBanner"; import UpgradeVersionView from "./UpgradeVersionBanner";
const Header = () => { const Header = () => {
@ -95,7 +94,7 @@ const Header = () => {
</NavLink> </NavLink>
</> </>
)} )}
{!isVisitorMode && ( {!globalStore.getDisablePublicMemos() && (
<> <>
<NavLink <NavLink
to="/explore" to="/explore"
@ -179,13 +178,6 @@ const Header = () => {
<Icon.LogIn className="mr-3 w-6 h-auto opacity-70" /> {t("common.sign-in")} <Icon.LogIn className="mr-3 w-6 h-auto opacity-70" /> {t("common.sign-in")}
</> </>
</NavLink> </NavLink>
<button
id="header-about"
className="px-4 pr-5 py-2 rounded-full border border-transparent flex flex-row items-center text-lg text-gray-800 dark:text-gray-300 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-600 dark:hover:bg-zinc-700"
onClick={() => showAboutSiteDialog()}
>
<Icon.CupSoda className="mr-3 w-6 h-auto opacity-70" /> {t("common.about")}
</button>
</> </>
)} )}
</div> </div>

View File

@ -38,7 +38,7 @@ const Memo: React.FC<Props> = (props: Props) => {
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs)); const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs));
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]); const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
const memoContainerRef = useRef<HTMLDivElement>(null); const memoContainerRef = useRef<HTMLDivElement>(null);
const readonly = userStore.isVisitorMode() || userStore.getCurrentUserId() !== memo.creatorId; const readonly = userStore.isVisitorMode() || userStore.getCurrentUsername() !== memo.creatorUsername;
useEffect(() => { useEffect(() => {
Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then( Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then(
@ -220,7 +220,7 @@ const Memo: React.FC<Props> = (props: Props) => {
{createdTimeStr} {createdTimeStr}
</Link> </Link>
{showCreator && ( {showCreator && (
<Link className="name-text" to={`/u/${memo.creatorId}`}> <Link className="name-text" to={`/u/${memo.creatorUsername}`}>
@{memo.creatorName} @{memo.creatorName}
</Link> </Link>
)} )}

View File

@ -10,7 +10,12 @@ import Empty from "./Empty";
import Memo from "./Memo"; import Memo from "./Memo";
import "@/less/memo-list.less"; import "@/less/memo-list.less";
const MemoList = () => { interface Props {
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();
@ -20,7 +25,7 @@ const MemoList = () => {
const { memos, isFetching } = memoStore.state; const { memos, isFetching } = memoStore.state;
const [isComplete, setIsComplete] = useState<boolean>(false); const [isComplete, setIsComplete] = useState<boolean>(false);
const currentUserId = userStore.getCurrentUserId(); const currentUsername = userStore.getCurrentUsername();
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = filter; const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = filter;
const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null; const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null;
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility); const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility);
@ -76,7 +81,7 @@ const MemoList = () => {
return shouldShow; return shouldShow;
}) })
: memos : memos
).filter((memo) => memo.creatorId === currentUserId && memo.rowStatus === "NORMAL"); ).filter((memo) => memo.creatorUsername === currentUsername && memo.rowStatus === "NORMAL");
const pinnedMemos = shownMemos.filter((m) => m.pinned); const pinnedMemos = shownMemos.filter((m) => m.pinned);
const unpinnedMemos = shownMemos.filter((m) => !m.pinned); const unpinnedMemos = shownMemos.filter((m) => !m.pinned);
@ -103,7 +108,7 @@ const MemoList = () => {
console.error(error); console.error(error);
toast.error(error.response.data.message); toast.error(error.response.data.message);
}); });
}, [currentUserId]); }, [currentUsername]);
useEffect(() => { useEffect(() => {
const pageWrapper = document.body.querySelector(".page-wrapper"); const pageWrapper = document.body.querySelector(".page-wrapper");
@ -153,7 +158,7 @@ const MemoList = () => {
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} showVisibility /> <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showVisibility showCreator={showCreator} />
))} ))}
{isFetching ? ( {isFetching ? (
<div className="status-text-container fetching-tip"> <div className="status-text-container fetching-tip">

View File

@ -51,7 +51,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => {
const createdDays = Math.ceil((Date.now() - getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24); const createdDays = Math.ceil((Date.now() - getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24);
useEffect(() => { useEffect(() => {
getMemoStats(user.id) getMemoStats(user.username)
.then(({ data }) => { .then(({ data }) => {
setPartialState({ setPartialState({
memoAmount: data.length, memoAmount: data.length,

View File

@ -44,19 +44,19 @@ const UsageHeatMap = () => {
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp)); const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestamp));
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null); const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
const containerElRef = useRef<HTMLDivElement>(null); const containerElRef = useRef<HTMLDivElement>(null);
const currentUserId = userStore.getCurrentUserId(); const currentUsername = userStore.getCurrentUsername();
useEffect(() => { useEffect(() => {
userStore.getUserById(currentUserId).then((user) => { userStore.getUserByUsername(currentUsername).then((user) => {
if (!user) { if (!user) {
return; return;
} }
setCreatedDays(Math.ceil((Date.now() - getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24)); setCreatedDays(Math.ceil((Date.now() - getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
}); });
}, [currentUserId]); }, [currentUsername]);
useEffect(() => { useEffect(() => {
getMemoStats(currentUserId) getMemoStats(currentUsername)
.then(({ data }) => { .then(({ data }) => {
setMemoAmount(data.length); setMemoAmount(data.length);
const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp); const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp);
@ -75,7 +75,7 @@ const UsageHeatMap = () => {
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
}); });
}, [memos.length, currentUserId]); }, [memos.length, currentUsername]);
const handleUsageStatItemMouseEnter = useCallback((event: React.MouseEvent, item: DailyUsageStat) => { const handleUsageStatItemMouseEnter = useCallback((event: React.MouseEvent, item: DailyUsageStat) => {
const tempDiv = document.createElement("div"); const tempDiv = document.createElement("div");

View File

@ -43,7 +43,7 @@ const UserBanner = () => {
<div className="px-4 py-2 max-w-full flex flex-row justify-start items-center cursor-pointer rounded-lg hover:shadow hover:bg-white dark:hover:bg-zinc-700"> <div className="px-4 py-2 max-w-full flex flex-row justify-start items-center cursor-pointer rounded-lg hover:shadow hover:bg-white dark:hover:bg-zinc-700">
<UserAvatar avatarUrl={user?.avatarUrl} /> <UserAvatar avatarUrl={user?.avatarUrl} />
<span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate"> <span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate">
{userStore.isVisitorMode() ? systemStatus.customizedProfile.name : username} {user != undefined ? username : systemStatus.customizedProfile.name}
</span> </span>
{user?.role === "HOST" ? ( {user?.role === "HOST" ? (
<span className="text-xs px-1 bg-blue-600 dark:bg-blue-800 rounded text-white dark:text-gray-200 shadow">MOD</span> <span className="text-xs px-1 bg-blue-600 dark:bg-blue-800 rounded text-white dark:text-gray-200 shadow">MOD</span>
@ -54,7 +54,7 @@ const UserBanner = () => {
positionClassName="top-full mt-2" positionClassName="top-full mt-2"
actions={ actions={
<> <>
{!userStore.isVisitorMode() && ( {user != undefined && (
<> <>
<button <button
className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded flex flex-row justify-start items-center dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800" className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded flex flex-row justify-start items-center dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
@ -64,7 +64,7 @@ const UserBanner = () => {
</button> </button>
<a <a
className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded flex flex-row justify-start items-center dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800" className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded flex flex-row justify-start items-center dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
href={`/u/${user?.id}/rss.xml`} href={`/u/${user?.username}/rss.xml`}
target="_blank" target="_blank"
> >
<Icon.Rss className="w-5 h-auto mr-2 opacity-80" /> RSS <Icon.Rss className="w-5 h-auto mr-2 opacity-80" /> RSS
@ -77,7 +77,7 @@ const UserBanner = () => {
> >
<Icon.Info className="w-5 h-auto mr-2 opacity-80" /> {t("common.about")} <Icon.Info className="w-5 h-auto mr-2 opacity-80" /> {t("common.about")}
</button> </button>
{!userStore.isVisitorMode() && ( {user != undefined && (
<button <button
className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded flex flex-row justify-start items-center dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800" className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded flex flex-row justify-start items-center dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={handleSignOutBtnClick} onClick={handleSignOutBtnClick}

View File

@ -21,14 +21,14 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
const { className, isFutureDateDisabled, datestamp, handleDateStampChange } = props; const { className, isFutureDateDisabled, datestamp, handleDateStampChange } = props;
const [currentDateStamp, setCurrentDateStamp] = useState<DateStamp>(getMonthFirstDayDateStamp(datestamp)); const [currentDateStamp, setCurrentDateStamp] = useState<DateStamp>(getMonthFirstDayDateStamp(datestamp));
const [countByDate, setCountByDate] = useState(new Map()); const [countByDate, setCountByDate] = useState(new Map());
const currentUserId = useUserStore().getCurrentUserId(); const currentUsername = useUserStore().getCurrentUsername();
useEffect(() => { useEffect(() => {
setCurrentDateStamp(getMonthFirstDayDateStamp(datestamp)); setCurrentDateStamp(getMonthFirstDayDateStamp(datestamp));
}, [datestamp]); }, [datestamp]);
useEffect(() => { useEffect(() => {
getMemoStats(currentUserId).then(({ data }) => { getMemoStats(currentUsername).then(({ data }) => {
const m = new Map(); const m = new Map();
for (const record of data) { for (const record of data) {
const date = getDateStampByDate(record * 1000); const date = getDateStampByDate(record * 1000);
@ -36,7 +36,7 @@ const DatePicker: React.FC<DatePickerProps> = (props: DatePickerProps) => {
} }
setCountByDate(m); setCountByDate(m);
}); });
}, [currentUserId]); }, [currentUsername]);
const firstDate = new Date(currentDateStamp); const firstDate = new Date(currentDateStamp);
const firstDateDay = firstDate.getDay() === 0 ? 7 : firstDate.getDay(); const firstDateDay = firstDate.getDay() === 0 ? 7 : firstDate.getDay();

View File

@ -56,8 +56,8 @@ export function getUserList() {
return axios.get<User[]>("/api/v1/user"); return axios.get<User[]>("/api/v1/user");
} }
export function getUserById(id: number) { export function getUserByUsername(username: string) {
return axios.get<User>(`/api/v1/user/${id}`); return axios.get<User>(`/api/v1/user/${username}`);
} }
export function upsertUserSetting(upsert: UserSettingUpsert) { export function upsertUserSetting(upsert: UserSettingUpsert) {
@ -81,13 +81,17 @@ export function getAllMemos(memoFind?: MemoFind) {
queryList.push(`limit=${memoFind.limit}`); queryList.push(`limit=${memoFind.limit}`);
} }
if (memoFind?.creatorUsername) {
queryList.push(`creatorUsername=${memoFind.creatorUsername}`);
}
return axios.get<Memo[]>(`/api/v1/memo/all?${queryList.join("&")}`); return axios.get<Memo[]>(`/api/v1/memo/all?${queryList.join("&")}`);
} }
export function getMemoList(memoFind?: MemoFind) { export function getMemoList(memoFind?: MemoFind) {
const queryList = []; const queryList = [];
if (memoFind?.creatorId) { if (memoFind?.creatorUsername) {
queryList.push(`creatorId=${memoFind.creatorId}`); queryList.push(`creatorUsername=${memoFind.creatorUsername}`);
} }
if (memoFind?.rowStatus) { if (memoFind?.rowStatus) {
queryList.push(`rowStatus=${memoFind.rowStatus}`); queryList.push(`rowStatus=${memoFind.rowStatus}`);
@ -104,8 +108,8 @@ export function getMemoList(memoFind?: MemoFind) {
return axios.get<Memo[]>(`/api/v1/memo?${queryList.join("&")}`); return axios.get<Memo[]>(`/api/v1/memo?${queryList.join("&")}`);
} }
export function getMemoStats(userId: UserId) { export function getMemoStats(username: string) {
return axios.get<number[]>(`/api/v1/memo/stats?creatorId=${userId}`); return axios.get<number[]>(`/api/v1/memo/stats?creatorUsername=${username}`);
} }
export function getMemoById(id: MemoId) { export function getMemoById(id: MemoId) {
@ -163,8 +167,8 @@ export async function chatStreaming(messageList: Array<Message>, onmessage: any,
export function getShortcutList(shortcutFind?: ShortcutFind) { export function getShortcutList(shortcutFind?: ShortcutFind) {
const queryList = []; const queryList = [];
if (shortcutFind?.creatorId) { if (shortcutFind?.creatorUsername) {
queryList.push(`creatorId=${shortcutFind.creatorId}`); queryList.push(`creatorUsername=${shortcutFind.creatorUsername}`);
} }
return axios.get<Shortcut[]>(`/api/v1/shortcut?${queryList.join("&")}`); return axios.get<Shortcut[]>(`/api/v1/shortcut?${queryList.join("&")}`);
} }
@ -228,8 +232,8 @@ export function deleteMemoResource(memoId: MemoId, resourceId: ResourceId) {
export function getTagList(tagFind?: TagFind) { export function getTagList(tagFind?: TagFind) {
const queryList = []; const queryList = [];
if (tagFind?.creatorId) { if (tagFind?.creatorUsername) {
queryList.push(`creatorId=${tagFind.creatorId}`); queryList.push(`creatorUsername=${tagFind.creatorUsername}`);
} }
return axios.get<string[]>(`/api/v1/tag?${queryList.join("&")}`); return axios.get<string[]>(`/api/v1/tag?${queryList.join("&")}`);
} }

View File

@ -1,5 +1,6 @@
// UNKNOWN_ID is the symbol for unknown id // UNKNOWN_ID is the symbol for unknown id
export const UNKNOWN_ID = -1; export const UNKNOWN_ID = -1;
export const UNKNOWN_USERNAME = "";
// default animation duration // default animation duration
export const ANIMATION_DURATION = 200; export const ANIMATION_DURATION = 200;

View File

@ -46,7 +46,7 @@ const EmbedMemo = () => {
<div className="w-full flex flex-col justify-start items-start"> <div className="w-full flex flex-col justify-start items-start">
<div className="w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300"> <div className="w-full mb-2 flex flex-row justify-start items-center text-sm text-gray-400 dark:text-gray-300">
<span>{getDateTimeString(state.memo.displayTs)}</span> <span>{getDateTimeString(state.memo.displayTs)}</span>
<a className="ml-2 hover:underline hover:text-green-600" href={`/u/${state.memo.creatorId}`}> <a className="ml-2 hover:underline hover:text-green-600" href={`/u/${state.memo.creatorUsername}`}>
@{state.memo.creatorName} @{state.memo.creatorName}
</a> </a>
</div> </div>

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useFilterStore, useMemoStore } from "@/store/module"; import { useFilterStore, useMemoStore, useUserStore } from "@/store/module";
import { TAG_REG } from "@/labs/marked/parser"; import { TAG_REG } from "@/labs/marked/parser";
import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts"; import { DEFAULT_MEMO_LIMIT } from "@/helpers/consts";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
@ -16,24 +16,16 @@ const Explore = () => {
const location = useLocation(); const location = useLocation();
const filterStore = useFilterStore(); const filterStore = useFilterStore();
const memoStore = useMemoStore(); const memoStore = useMemoStore();
const userStore = useUserStore();
const filter = filterStore.state; const filter = filterStore.state;
const memos = memoStore.state.memos; const { memos } = memoStore.state;
const [isComplete, setIsComplete] = useState<boolean>(false); const [isComplete, setIsComplete] = useState<boolean>(false);
const loadingState = useLoading(); const loadingState = useLoading();
useEffect(() => {
memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, 0).then((memos) => {
if (memos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
}
loadingState.setFinish();
});
}, [location]);
const { tag: tagQuery, text: textQuery } = filter; const { tag: tagQuery, text: textQuery } = filter;
const showMemoFilter = Boolean(tagQuery || textQuery); const showMemoFilter = Boolean(tagQuery || textQuery);
const shownMemos = showMemoFilter const fetchedMemos = showMemoFilter
? memos.filter((memo) => { ? memos.filter((memo) => {
let shouldShow = true; let shouldShow = true;
@ -57,13 +49,35 @@ const Explore = () => {
}) })
: memos; : memos;
const sortedMemos = shownMemos const username = userStore.getUsernameFromPath();
let sortedMemos = fetchedMemos
.filter((m) => m.rowStatus === "NORMAL" && m.visibility !== "PRIVATE") .filter((m) => m.rowStatus === "NORMAL" && m.visibility !== "PRIVATE")
.sort((mi, mj) => mj.displayTs - mi.displayTs); .sort((mi, mj) => mj.displayTs - mi.displayTs);
if (username != undefined) {
sortedMemos = sortedMemos.filter((m) => m.creatorUsername === username);
}
useEffect(() => {
const username = userStore.getUsernameFromPath();
memoStore
.fetchAllMemos(DEFAULT_MEMO_LIMIT, 0, username)
.then((fetchedMemos) => {
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true);
}
loadingState.setFinish();
})
.catch((error) => {
console.error(error);
toast.error(error.response.data.message);
});
}, [location]);
const handleFetchMoreClick = async () => { const handleFetchMoreClick = async () => {
try { try {
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, memos.length); const username = userStore.getUsernameFromPath();
const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, memos.length, username);
if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) {
setIsComplete(true); setIsComplete(true);
} else { } else {

View File

@ -15,14 +15,14 @@ function Home() {
const user = userStore.state.user; const user = userStore.state.user;
useEffect(() => { useEffect(() => {
const currentUserId = userStore.getCurrentUserId(); const currentUsername = userStore.getCurrentUsername();
userStore.getUserById(currentUserId).then((user) => { userStore.getUserByUsername(currentUsername).then((user) => {
if (!user) { if (!user) {
toast.error(t("message.user-not-found")); toast.error(t("message.user-not-found"));
return; return;
} }
}); });
}, [userStore.getCurrentUserId()]); }, [userStore.getCurrentUsername()]);
useEffect(() => { useEffect(() => {
if (user?.setting.locale) { if (user?.setting.locale) {

View File

@ -63,22 +63,25 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global; const { systemStatus } = store.getState().global;
if (isNullorUndefined(host)) { // if user is authenticated, then show home
return redirect("/auth"); if (!isNullorUndefined(user)) {
} else if (isNullorUndefined(user) && !systemStatus.disablePublicMemos) { return null;
return redirect("/explore"); }
} else if (isNullorUndefined(user) && systemStatus.disablePublicMemos) {
// if user is anonymous, then redirect to auth if disabled public memos, else redirect to explore
if (systemStatus.disablePublicMemos) {
return redirect("/auth"); return redirect("/auth");
} }
return null;
return redirect("/explore");
}, },
}, },
{ {
path: "/u/:userId", path: "/u/:username",
element: <Home />, element: <Explore />,
loader: async () => { loader: async () => {
await initialGlobalStateLoader(); await initialGlobalStateLoader();
@ -88,12 +91,13 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global; const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user) && systemStatus.disablePublicMemos) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
}, },
}, },
@ -109,10 +113,10 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global; const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user) && systemStatus.disablePublicMemos) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
@ -130,10 +134,9 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user)) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
@ -151,10 +154,9 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user)) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
@ -172,10 +174,9 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user)) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
@ -194,10 +195,9 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user)) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
@ -215,10 +215,9 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user)) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;
@ -238,10 +237,10 @@ const router = createBrowserRouter([
// do nth // do nth
} }
const { host, user } = store.getState().user; const { user } = store.getState().user;
const { systemStatus } = store.getState().global; const { systemStatus } = store.getState().global;
if (isNullorUndefined(host) || (isNullorUndefined(user) && systemStatus.disablePublicMemos)) { if (isNullorUndefined(user) && systemStatus.disablePublicMemos) {
return redirect("/auth"); return redirect("/auth");
} }
return null; return null;

View File

@ -65,6 +65,9 @@ export const useGlobalStore = () => {
getState: () => { getState: () => {
return store.getState().global; return store.getState().global;
}, },
getDisablePublicMemos: () => {
return store.getState().global.systemStatus.disablePublicMemos;
},
isDev: () => { isDev: () => {
return state.systemStatus.profile.mode !== "prod"; return state.systemStatus.profile.mode !== "prod";
}, },

View File

@ -41,7 +41,7 @@ export const useMemoStore = () => {
offset, offset,
}; };
if (userStore.isVisitorMode()) { if (userStore.isVisitorMode()) {
memoFind.creatorId = userStore.getUserIdFromPath(); memoFind.creatorUsername = userStore.getUsernameFromPath();
} }
const { data } = await api.getMemoList(memoFind); const { data } = await api.getMemoList(memoFind);
const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
@ -54,16 +54,22 @@ export const useMemoStore = () => {
return fetchedMemos; return fetchedMemos;
}, },
fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => { fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number, username?: string) => {
store.dispatch(setIsFetching(true));
const memoFind: MemoFind = { const memoFind: MemoFind = {
rowStatus: "NORMAL", rowStatus: "NORMAL",
limit, limit,
offset, offset,
}; };
if (username != undefined) {
memoFind.creatorUsername = username;
}
const { data } = await api.getAllMemos(memoFind); const { data } = await api.getAllMemos(memoFind);
const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); const fetchedMemos = data.map((m) => convertResponseModelMemo(m));
store.dispatch(upsertMemos(fetchedMemos)); store.dispatch(upsertMemos(fetchedMemos));
store.dispatch(setIsFetching(false));
for (const m of fetchedMemos) { for (const m of fetchedMemos) {
memoCacheStore.setMemoCache(m); memoCacheStore.setMemoCache(m);
@ -76,7 +82,7 @@ export const useMemoStore = () => {
rowStatus: "ARCHIVED", rowStatus: "ARCHIVED",
}; };
if (userStore.isVisitorMode()) { if (userStore.isVisitorMode()) {
memoFind.creatorId = userStore.getUserIdFromPath(); memoFind.creatorUsername = userStore.getUsernameFromPath();
} }
const { data } = await api.getMemoList(memoFind); const { data } = await api.getMemoList(memoFind);
const archivedMemos = data.map((m) => { const archivedMemos = data.map((m) => {

View File

@ -14,7 +14,7 @@ export const useTagStore = () => {
fetchTags: async () => { fetchTags: async () => {
const tagFind: TagFind = {}; const tagFind: TagFind = {};
if (userStore.isVisitorMode()) { if (userStore.isVisitorMode()) {
tagFind.creatorId = userStore.getUserIdFromPath(); tagFind.creatorUsername = userStore.getUsernameFromPath();
} }
const { data } = await api.getTagList(tagFind); const { data } = await api.getTagList(tagFind);
store.dispatch(setTags(data)); store.dispatch(setTags(data));

View File

@ -1,7 +1,7 @@
import { camelCase } from "lodash-es"; import { camelCase } from "lodash-es";
import * as api from "@/helpers/api"; import * as api from "@/helpers/api";
import storage from "@/helpers/storage"; import storage from "@/helpers/storage";
import { UNKNOWN_ID } from "@/helpers/consts"; import { UNKNOWN_USERNAME } from "@/helpers/consts";
import { getSystemColorScheme } from "@/helpers/utils"; import { getSystemColorScheme } from "@/helpers/utils";
import store, { useAppSelector } from ".."; import store, { useAppSelector } from "..";
import { setAppearance, setLocale } from "../reducer/global"; import { setAppearance, setLocale } from "../reducer/global";
@ -82,6 +82,16 @@ const getUserIdFromPath = () => {
return undefined; return undefined;
}; };
const getUsernameFromPath = () => {
const pathname = window.location.pathname;
const usernameRegex = /^\/u\/(\w+).*/;
const result = pathname.match(usernameRegex);
if (result && result.length === 2) {
return String(result[1]);
}
return undefined;
};
const doSignIn = async () => { const doSignIn = async () => {
const { data: user } = await api.getMyselfUser(); const { data: user } = await api.getMyselfUser();
if (user) { if (user) {
@ -100,7 +110,7 @@ export const useUserStore = () => {
const state = useAppSelector((state) => state.user); const state = useAppSelector((state) => state.user);
const isVisitorMode = () => { const isVisitorMode = () => {
return state.user === undefined || (getUserIdFromPath() && state.user.id !== getUserIdFromPath()); return state.user === undefined || (getUsernameFromPath() && state.user.username !== getUsernameFromPath());
}; };
return { return {
@ -110,17 +120,18 @@ export const useUserStore = () => {
}, },
isVisitorMode, isVisitorMode,
getUserIdFromPath, getUserIdFromPath,
getUsernameFromPath,
doSignIn, doSignIn,
doSignOut, doSignOut,
getCurrentUserId: () => { getCurrentUsername: () => {
if (isVisitorMode()) { if (isVisitorMode()) {
return getUserIdFromPath() || UNKNOWN_ID; return getUsernameFromPath() || UNKNOWN_USERNAME;
} else { } else {
return state.user?.id || UNKNOWN_ID; return state.user?.username || UNKNOWN_USERNAME;
} }
}, },
getUserById: async (userId: UserId) => { getUserByUsername: async (username: string) => {
const { data } = await api.getUserById(userId); const { data } = await api.getUserByUsername(username);
if (data) { if (data) {
const user = convertResponseModelUser(data); const user = convertResponseModelUser(data);
store.dispatch(setUserById(user)); store.dispatch(setUserById(user));

View File

@ -5,7 +5,7 @@ type Visibility = "PUBLIC" | "PROTECTED" | "PRIVATE";
interface Memo { interface Memo {
id: MemoId; id: MemoId;
creatorId: UserId; creatorUsername: string;
createdTs: TimeStamp; createdTs: TimeStamp;
updatedTs: TimeStamp; updatedTs: TimeStamp;
rowStatus: RowStatus; rowStatus: RowStatus;
@ -38,7 +38,7 @@ interface MemoPatch {
} }
interface MemoFind { interface MemoFind {
creatorId?: UserId; creatorUsername?: string;
rowStatus?: RowStatus; rowStatus?: RowStatus;
pinned?: boolean; pinned?: boolean;
visibility?: Visibility; visibility?: Visibility;

View File

@ -3,7 +3,7 @@ type ShortcutId = number;
interface Shortcut { interface Shortcut {
id: ShortcutId; id: ShortcutId;
creatorId: UserId; creatorUsername: string;
rowStatus: RowStatus; rowStatus: RowStatus;
createdTs: TimeStamp; createdTs: TimeStamp;
updatedTs: TimeStamp; updatedTs: TimeStamp;
@ -25,5 +25,5 @@ interface ShortcutPatch {
} }
interface ShortcutFind { interface ShortcutFind {
creatorId?: UserId; creatorUsername?: string;
} }

View File

@ -1,3 +1,3 @@
interface TagFind { interface TagFind {
creatorId?: UserId; creatorUsername?: string;
} }