mirror of
https://github.com/usememos/memos.git
synced 2024-12-20 01:31:29 +03:00
parent
0b34b142c8
commit
b68d6e2693
@ -37,6 +37,7 @@ type Memo struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Visibility Visibility `json:"visibility"`
|
Visibility Visibility `json:"visibility"`
|
||||||
Pinned bool `json:"pinned"`
|
Pinned bool `json:"pinned"`
|
||||||
|
DisplayTs int64 `json:"displayTs"`
|
||||||
|
|
||||||
// Related fields
|
// Related fields
|
||||||
Creator *User `json:"creator"`
|
Creator *User `json:"creator"`
|
||||||
@ -59,7 +60,8 @@ type MemoPatch struct {
|
|||||||
ID int
|
ID int
|
||||||
|
|
||||||
// Standard fields
|
// Standard fields
|
||||||
CreatedTs *int64 `json:"createdTs"`
|
CreatedTs *int64 `json:"createdTs"`
|
||||||
|
UpdatedTs *int64
|
||||||
RowStatus *RowStatus `json:"rowStatus"`
|
RowStatus *RowStatus `json:"rowStatus"`
|
||||||
|
|
||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
|
@ -16,7 +16,7 @@ const (
|
|||||||
UserSettingEditorFontStyleKey UserSettingKey = "editorFontStyle"
|
UserSettingEditorFontStyleKey UserSettingKey = "editorFontStyle"
|
||||||
// UserSettingEditorFontStyleKey is the key type for mobile editor style.
|
// UserSettingEditorFontStyleKey is the key type for mobile editor style.
|
||||||
UserSettingMobileEditorStyleKey UserSettingKey = "mobileEditorStyle"
|
UserSettingMobileEditorStyleKey UserSettingKey = "mobileEditorStyle"
|
||||||
// UserSettingMemoSortOptionKey is the key type for sort time option.
|
// UserSettingMemoSortOptionKey is the key type for memo sort option.
|
||||||
UserSettingMemoSortOptionKey UserSettingKey = "memoSortOption"
|
UserSettingMemoSortOptionKey UserSettingKey = "memoSortOption"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ var (
|
|||||||
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
|
UserSettingMemoVisibilityValue = []Visibility{Privite, Protected, Public}
|
||||||
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
|
UserSettingEditorFontStyleValue = []string{"normal", "mono"}
|
||||||
UserSettingMobileEditorStyleValue = []string{"normal", "float"}
|
UserSettingMobileEditorStyleValue = []string{"normal", "float"}
|
||||||
UserSettingSortTimeOptionKeyValue = []string{"created_ts", "updated_ts"}
|
UserSettingMemoSortOptionKeyValue = []string{"created_ts", "updated_ts"}
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserSetting struct {
|
type UserSetting struct {
|
||||||
@ -135,7 +135,7 @@ func (upsert UserSettingUpsert) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
invalid := true
|
invalid := true
|
||||||
for _, value := range UserSettingSortTimeOptionKeyValue {
|
for _, value := range UserSettingMemoSortOptionKeyValue {
|
||||||
if memoSortOption == value {
|
if memoSortOption == value {
|
||||||
invalid = false
|
invalid = false
|
||||||
break
|
break
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -101,8 +102,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentTs := time.Now().Unix()
|
||||||
memoPatch := &api.MemoPatch{
|
memoPatch := &api.MemoPatch{
|
||||||
ID: memoID,
|
ID: memoID,
|
||||||
|
UpdatedTs: ¤tTs,
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
|
if err := json.NewDecoder(c.Request().Body).Decode(memoPatch); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch memo request").SetInternal(err)
|
||||||
@ -175,6 +178,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
return list[i].DisplayTs > list[j].DisplayTs
|
||||||
|
})
|
||||||
|
|
||||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo list response").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo list response").SetInternal(err)
|
||||||
@ -227,6 +234,10 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
|
|||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(list, func(i, j int) bool {
|
||||||
|
return list[i].DisplayTs > list[j].DisplayTs
|
||||||
|
})
|
||||||
|
|
||||||
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(list)); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode all memo list response").SetInternal(err)
|
||||||
|
@ -3,6 +3,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ func (raw *memoRaw) toMemo() *api.Memo {
|
|||||||
// Domain specific fields
|
// Domain specific fields
|
||||||
Content: raw.Content,
|
Content: raw.Content,
|
||||||
Visibility: raw.Visibility,
|
Visibility: raw.Visibility,
|
||||||
|
DisplayTs: raw.CreatedTs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +64,25 @@ func (s *Store) ComposeMemo(ctx context.Context, memo *api.Memo) (*api.Memo, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memoSortOptionKey := api.UserSettingMemoSortOptionKey
|
||||||
|
memoSortOption, err := s.FindUserSetting(ctx, &api.UserSettingFind{
|
||||||
|
UserID: memo.CreatorID,
|
||||||
|
Key: &memoSortOptionKey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
memoSortOptionValue := "created_ts"
|
||||||
|
if memoSortOption != nil {
|
||||||
|
err = json.Unmarshal([]byte(memoSortOption.Value), &memoSortOptionValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal user setting memo sort option value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if memoSortOptionValue == "updated_ts" {
|
||||||
|
memo.DisplayTs = memo.UpdatedTs
|
||||||
|
}
|
||||||
|
|
||||||
return memo, nil
|
return memo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +265,9 @@ func patchMemoRaw(ctx context.Context, tx *sql.Tx, patch *api.MemoPatch) (*memoR
|
|||||||
if v := patch.CreatedTs; v != nil {
|
if v := patch.CreatedTs; v != nil {
|
||||||
set, args = append(set, "created_ts = ?"), append(args, *v)
|
set, args = append(set, "created_ts = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
|
if v := patch.UpdatedTs; v != nil {
|
||||||
|
set, args = append(set, "updated_ts = ?"), append(args, *v)
|
||||||
|
}
|
||||||
if v := patch.RowStatus; v != nil {
|
if v := patch.RowStatus; v != nil {
|
||||||
set, args = append(set, "row_status = ?"), append(args, *v)
|
set, args = append(set, "row_status = ?"), append(args, *v)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import "dayjs/locale/zh";
|
import "dayjs/locale/zh";
|
||||||
import { UNKNOWN_ID } from "../helpers/consts";
|
import { UNKNOWN_ID } from "../helpers/consts";
|
||||||
import { editorStateService, locationService, memoService, userService } from "../services";
|
import { editorStateService, locationService, memoService, userService } from "../services";
|
||||||
import { useAppSelector } from "../store";
|
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
import MemoContent from "./MemoContent";
|
import MemoContent from "./MemoContent";
|
||||||
@ -33,20 +32,17 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
|||||||
|
|
||||||
const Memo: React.FC<Props> = (props: Props) => {
|
const Memo: React.FC<Props> = (props: Props) => {
|
||||||
const memo = props.memo;
|
const memo = props.memo;
|
||||||
const user = useAppSelector((state) => state.user.user);
|
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
|
const [displayTimeStr, setDisplayTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
|
||||||
const [updatedAtStr, setUpdatedAtStr] = useState<string>(getFormatedMemoTimeStr(memo.updatedTs, i18n.language));
|
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isVisitorMode = userService.isVisitorMode();
|
const isVisitorMode = userService.isVisitorMode();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalFlag: any = -1;
|
let intervalFlag: any = -1;
|
||||||
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
|
if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) {
|
||||||
intervalFlag = setInterval(() => {
|
intervalFlag = setInterval(() => {
|
||||||
setCreatedAtStr(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
|
setDisplayTimeStr(getFormatedMemoTimeStr(memo.displayTs, i18n.language));
|
||||||
setUpdatedAtStr(getFormatedMemoTimeStr(memo.updatedTs, i18n.language));
|
|
||||||
}, 1000 * 1);
|
}, 1000 * 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,13 +182,12 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
editorStateService.setEditMemoWithId(memo.id);
|
editorStateService.setEditMemoWithId(memo.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeStr = user?.setting.memoSortOption === "created_ts" ? createdAtStr : `${t("common.update-on")} ${updatedAtStr}`;
|
|
||||||
return (
|
return (
|
||||||
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
|
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
|
||||||
<div className="memo-top-wrapper">
|
<div className="memo-top-wrapper">
|
||||||
<div className="status-text-container">
|
<div className="status-text-container">
|
||||||
<span className="time-text" onClick={handleShowMemoStoryDialog}>
|
<span className="time-text" onClick={handleShowMemoStoryDialog}>
|
||||||
{timeStr}
|
{displayTimeStr}
|
||||||
</span>
|
</span>
|
||||||
{memo.visibility !== "PRIVATE" && !isVisitorMode && (
|
{memo.visibility !== "PRIVATE" && !isVisitorMode && (
|
||||||
<span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span>
|
<span className={`status-text ${memo.visibility.toLocaleLowerCase()}`}>{memo.visibility}</span>
|
||||||
|
@ -174,7 +174,6 @@ const MemoEditor: React.FC = () => {
|
|||||||
});
|
});
|
||||||
locationService.clearQuery();
|
locationService.clearQuery();
|
||||||
}
|
}
|
||||||
locationService.setUpdatedFlag();
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
|
@ -12,8 +12,7 @@ import "../less/memo-list.less";
|
|||||||
const MemoList = () => {
|
const MemoList = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const query = useAppSelector((state) => state.location.query);
|
const query = useAppSelector((state) => state.location.query);
|
||||||
const updatedTime = useAppSelector((state) => state.location.updatedTime);
|
const memoSortOption = useAppSelector((state) => state.user.user?.setting.memoSortOption);
|
||||||
const user = useAppSelector((state) => state.user.user);
|
|
||||||
const { memos, isFetching } = useAppSelector((state) => state.memo);
|
const { memos, isFetching } = useAppSelector((state) => state.memo);
|
||||||
|
|
||||||
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {};
|
const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {};
|
||||||
@ -72,8 +71,8 @@ const MemoList = () => {
|
|||||||
|
|
||||||
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);
|
||||||
const memoSorting = (m1: Memo, m2: Memo) => {
|
const memoSorting = (mi: Memo, mj: Memo) => {
|
||||||
return user?.setting.memoSortOption === "created_ts" ? m2.createdTs - m1.createdTs : m2.updatedTs - m1.updatedTs;
|
return mj.displayTs - mi.displayTs;
|
||||||
};
|
};
|
||||||
pinnedMemos.sort(memoSorting);
|
pinnedMemos.sort(memoSorting);
|
||||||
unpinnedMemos.sort(memoSorting);
|
unpinnedMemos.sort(memoSorting);
|
||||||
@ -89,19 +88,19 @@ const MemoList = () => {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
toastHelper.error(error.response.data.message);
|
toastHelper.error(error.response.data.message);
|
||||||
});
|
});
|
||||||
}, [updatedTime]);
|
}, [memoSortOption]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pageWrapper = document.body.querySelector(".page-wrapper");
|
const pageWrapper = document.body.querySelector(".page-wrapper");
|
||||||
if (pageWrapper) {
|
if (pageWrapper) {
|
||||||
pageWrapper.scrollTo(0, 0);
|
pageWrapper.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
}, [query, updatedTime]);
|
}, [query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="memo-list-container">
|
<div className="memo-list-container">
|
||||||
{sortedMemos.map((memo) => (
|
{sortedMemos.map((memo) => (
|
||||||
<Memo key={`${memo.id}-${memo.createdTs}-${memo.updatedTs}`} memo={memo} />
|
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />
|
||||||
))}
|
))}
|
||||||
{isFetching ? (
|
{isFetching ? (
|
||||||
<div className="status-text-container fetching-tip">
|
<div className="status-text-container fetching-tip">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { stringify } from "qs";
|
import { stringify } from "qs";
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
import { setQuery, setPathname, setUpdatedTime, Query, updateStateWithLocation } from "../store/modules/location";
|
import { setQuery, setPathname, Query, updateStateWithLocation } from "../store/modules/location";
|
||||||
import { getTimeStampByDate } from "../helpers/utils";
|
|
||||||
|
|
||||||
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
|
const updateLocationUrl = (method: "replace" | "push" = "replace") => {
|
||||||
const { query, pathname, hash } = store.getState().location;
|
const { query, pathname, hash } = store.getState().location;
|
||||||
@ -113,10 +112,6 @@ const locationService = {
|
|||||||
);
|
);
|
||||||
updateLocationUrl();
|
updateLocationUrl();
|
||||||
},
|
},
|
||||||
|
|
||||||
setUpdatedFlag: () => {
|
|
||||||
store.dispatch(setUpdatedTime(getTimeStampByDate(new Date()).toString()));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default locationService;
|
export default locationService;
|
||||||
|
@ -8,6 +8,7 @@ const convertResponseModelMemo = (memo: Memo): Memo => {
|
|||||||
...memo,
|
...memo,
|
||||||
createdTs: memo.createdTs * 1000,
|
createdTs: memo.createdTs * 1000,
|
||||||
updatedTs: memo.updatedTs * 1000,
|
updatedTs: memo.updatedTs * 1000,
|
||||||
|
displayTs: memo.displayTs * 1000,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,7 +17,6 @@ interface State {
|
|||||||
pathname: string;
|
pathname: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
query: Query;
|
query: Query;
|
||||||
updatedTime: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getValidPathname = (pathname: string): string => {
|
const getValidPathname = (pathname: string): string => {
|
||||||
@ -36,7 +35,6 @@ const getStateFromLocation = () => {
|
|||||||
pathname: getValidPathname(pathname),
|
pathname: getValidPathname(pathname),
|
||||||
hash: hash,
|
hash: hash,
|
||||||
query: {},
|
query: {},
|
||||||
updatedTime: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (search !== "") {
|
if (search !== "") {
|
||||||
@ -88,15 +86,9 @@ const locationSlice = createSlice({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setUpdatedTime: (state, action: PayloadAction<string>) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
updatedTime: action.payload,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setPathname, setQuery, setUpdatedTime, updateStateWithLocation } = locationSlice.actions;
|
export const { setPathname, setQuery, updateStateWithLocation } = locationSlice.actions;
|
||||||
|
|
||||||
export default locationSlice.reducer;
|
export default locationSlice.reducer;
|
||||||
|
@ -18,13 +18,13 @@ const memoSlice = createSlice({
|
|||||||
setMemos: (state, action: PayloadAction<Memo[]>) => {
|
setMemos: (state, action: PayloadAction<Memo[]>) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
memos: action.payload.filter((m) => m.rowStatus === "NORMAL").sort((a, b) => b.createdTs - a.createdTs),
|
memos: action.payload.filter((m) => m.rowStatus === "NORMAL"),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
createMemo: (state, action: PayloadAction<Memo>) => {
|
createMemo: (state, action: PayloadAction<Memo>) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
memos: state.memos.concat(action.payload).sort((a, b) => b.createdTs - a.createdTs),
|
memos: state.memos.concat(action.payload),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
patchMemo: (state, action: PayloadAction<Partial<Memo>>) => {
|
patchMemo: (state, action: PayloadAction<Partial<Memo>>) => {
|
||||||
|
1
web/src/types/modules/memo.d.ts
vendored
1
web/src/types/modules/memo.d.ts
vendored
@ -13,6 +13,7 @@ interface Memo {
|
|||||||
content: string;
|
content: string;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
|
displayTs: TimeStamp;
|
||||||
|
|
||||||
creator: User;
|
creator: User;
|
||||||
resourceList: Resource[];
|
resourceList: Resource[];
|
||||||
|
Loading…
Reference in New Issue
Block a user