4 Commits

Author SHA1 Message Date
anian 12a953e960 修改安全策略 2026-07-05 16:59:17 +08:00
anian edd2867a47 新增详情菜单项 2026-07-04 18:04:51 +08:00
anian 70ab5ef059 优化归档页、图标 2026-07-04 16:56:31 +08:00
anian adb151dc53 修改commit基础链接 2026-07-04 02:06:33 +08:00
14 changed files with 117 additions and 67 deletions
+6 -3
View File
@@ -11,6 +11,9 @@ memos
.dockerignore .dockerignore
docs/ docs/
.DS_Store .DS_Store
memos_prod.db /memos_prod.db
memos_prod.db-shm /memos_prod.db-shm
memos_prod.db-wal /memos_prod.db-wal
/assets/
/.thumbnail_cache/
/.motion_cache/
+7 -4
View File
@@ -32,7 +32,10 @@ dist
# Frontend test coverage output (Vitest + @vitest/coverage-v8). # Frontend test coverage output (Vitest + @vitest/coverage-v8).
web/coverage/ web/coverage/
# Database # Dev
memos_prod.db /memos_prod.db
memos_prod.db-shm /memos_prod.db-shm
memos_prod.db-wal /memos_prod.db-wal
/assets/
/.thumbnail_cache/
/.motion_cache/
+9
View File
@@ -2,6 +2,15 @@
> 基于原作者v0.29.1代码修改 > 基于原作者v0.29.1代码修改
#### v0.29.1-4
1. 修复缓存图片无法删除
2. 修改安全策略避免视频无法播放
#### v0.29.1-3
1. 修改设置页面commit基础链接
2. 优化图标样式、备忘录菜单跳转
3. 归档页面允许设置表情
#### v0.29.1-2 #### v0.29.1-2
1. 置顶内容新增自动折叠;置顶效果及图标改为全局显示 1. 置顶内容新增自动折叠;置顶效果及图标改为全局显示
2. 归档页面新增(取消)置顶、编辑、复制按钮 2. 归档页面新增(取消)置顶、编辑、复制按钮
+2 -2
View File
@@ -451,7 +451,7 @@ func (s *FileServerService) getThumbnailPath(attachment *store.Attachment) (stri
if err := os.MkdirAll(cacheFolder, os.ModePerm); err != nil { if err := os.MkdirAll(cacheFolder, os.ModePerm); err != nil {
return "", errors.Wrap(err, "failed to create thumbnail cache folder") return "", errors.Wrap(err, "failed to create thumbnail cache folder")
} }
filename := fmt.Sprintf("%s.v2.jpeg", attachment.UID) filename := fmt.Sprintf("%s.jpeg", attachment.UID)
return filepath.Join(cacheFolder, filename), nil return filepath.Join(cacheFolder, filename), nil
} }
@@ -743,7 +743,7 @@ func setSecurityHeaders(c *echo.Context) {
h := c.Response().Header() h := c.Response().Header()
h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY") h.Set("X-Frame-Options", "DENY")
h.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline';") h.Set("Content-Security-Policy", "default-src 'self'; style-src 'unsafe-inline';")
} }
// setMediaHeaders sets headers for media file responses. // setMediaHeaders sets headers for media file responses.
+1 -1
View File
@@ -282,7 +282,7 @@ func TestServeAttachmentFile_SVGThumbnailServedAsImageWithSecurityHeaders(t *tes
require.Equal(t, "image/svg+xml", rec.Header().Get("Content-Type")) require.Equal(t, "image/svg+xml", rec.Header().Get("Content-Type"))
require.Empty(t, rec.Header().Get("Content-Disposition")) require.Empty(t, rec.Header().Get("Content-Disposition"))
require.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options")) require.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options"))
require.Equal(t, "default-src 'none'; style-src 'unsafe-inline';", rec.Header().Get("Content-Security-Policy")) require.Equal(t, "default-src 'self'; style-src 'unsafe-inline';", rec.Header().Get("Content-Security-Policy"))
require.Equal(t, svgContent, rec.Body.Bytes()) require.Equal(t, svgContent, rec.Body.Bytes())
} }
@@ -1,4 +1,5 @@
import { import {
ArrowLeftIcon,
ArchiveIcon, ArchiveIcon,
ArchiveRestoreIcon, ArchiveRestoreIcon,
BookmarkMinusIcon, BookmarkMinusIcon,
@@ -6,6 +7,7 @@ import {
CheckCheckIcon, CheckCheckIcon,
CopyIcon, CopyIcon,
Edit3Icon, Edit3Icon,
EyeIcon,
FileTextIcon, FileTextIcon,
LinkIcon, LinkIcon,
ListChecksIcon, ListChecksIcon,
@@ -32,7 +34,7 @@ import { useMemoActionHandlers } from "./hooks";
import type { MemoActionMenuProps } from "./types"; import type { MemoActionMenuProps } from "./types";
const MemoActionMenu = (props: MemoActionMenuProps) => { const MemoActionMenu = (props: MemoActionMenuProps) => {
const { memo, readonly } = props; const { memo, readonly, isInMemoDetailPage, onBack, onViewDetail } = props;
const t = useTranslate(); const t = useTranslate();
// Dialog state // Dialog state
@@ -45,6 +47,8 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
const canMutateTasks = !readonly && !isArchived && taskStats.total > 0; const canMutateTasks = !readonly && !isArchived && taskStats.total > 0;
const hasOpenTasks = taskStats.completed < taskStats.total; const hasOpenTasks = taskStats.completed < taskStats.total;
const hasCompletedTasks = taskStats.completed > 0; const hasCompletedTasks = taskStats.completed > 0;
const DetailActionIcon = isInMemoDetailPage ? ArrowLeftIcon : EyeIcon;
const detailActionLabel = isInMemoDetailPage ? t("common.collapse") : t("memo.view-detail");
// Action handlers // Action handlers
const { const {
@@ -71,6 +75,11 @@ const MemoActionMenu = (props: MemoActionMenuProps) => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2}> <DropdownMenuContent align="end" sideOffset={2}>
<DropdownMenuItem onClick={isInMemoDetailPage ? onBack : onViewDetail}>
<DetailActionIcon className="w-4 h-auto" />
{detailActionLabel}
</DropdownMenuItem>
{/* Edit actions (non-readonly) */} {/* Edit actions (non-readonly) */}
{!readonly && ( {!readonly && (
<> <>
@@ -4,5 +4,8 @@ export interface MemoActionMenuProps {
memo: Memo; memo: Memo;
readonly?: boolean; readonly?: boolean;
className?: string; className?: string;
isInMemoDetailPage: boolean;
onBack: () => void;
onViewDetail: () => void;
onEdit?: () => void; onEdit?: () => void;
} }
@@ -1,6 +1,5 @@
import { memo } from "react"; import { memo } from "react";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service_pb"; import type { Memo, Reaction } from "@/types/proto/api/v1/memo_service_pb";
import { useReactionGroups } from "./hooks"; import { useReactionGroups } from "./hooks";
import ReactionSelector from "./ReactionSelector"; import ReactionSelector from "./ReactionSelector";
@@ -15,7 +14,6 @@ const MemoReactionListView = (props: Props) => {
const { memo: memoData, reactions } = props; const { memo: memoData, reactions } = props;
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const reactionGroup = useReactionGroups(reactions); const reactionGroup = useReactionGroups(reactions);
const readonly = memoData.state === State.ARCHIVED;
if (reactions.length === 0) { if (reactions.length === 0) {
return null; return null;
@@ -26,7 +24,7 @@ const MemoReactionListView = (props: Props) => {
{Array.from(reactionGroup).map(([reactionType, users]) => ( {Array.from(reactionGroup).map(([reactionType, users]) => (
<ReactionView key={`${reactionType.toString()} ${users.length}`} memo={memoData} reactionType={reactionType} users={users} /> <ReactionView key={`${reactionType.toString()} ${users.length}`} memo={memoData} reactionType={reactionType} users={users} />
))} ))}
{!readonly && currentUser && <ReactionSelector memo={memoData} />} {currentUser && <ReactionSelector memo={memoData} />}
</div> </div>
); );
}; };
@@ -1,7 +1,6 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { State } from "@/types/proto/api/v1/common_pb";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb"; import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
import type { User } from "@/types/proto/api/v1/user_service_pb"; import type { User } from "@/types/proto/api/v1/user_service_pb";
import { formatReactionTooltip, useReactionActions } from "./hooks"; import { formatReactionTooltip, useReactionActions } from "./hooks";
@@ -16,16 +15,15 @@ const ReactionView = (props: Props) => {
const { memo, reactionType, users } = props; const { memo, reactionType, users } = props;
const currentUser = useCurrentUser(); const currentUser = useCurrentUser();
const hasReaction = users.some((user) => currentUser && user.username === currentUser.username); const hasReaction = users.some((user) => currentUser && user.username === currentUser.username);
const readonly = memo.state === State.ARCHIVED;
const { handleReactionClick } = useReactionActions({ memo }); const { handleReactionClick } = useReactionActions({ memo });
const handleClick = () => { const handleClick = () => {
if (!currentUser || readonly) return; if (!currentUser) return;
handleReactionClick(reactionType); handleReactionClick(reactionType);
}; };
const isClickable = currentUser && !readonly; const isClickable = Boolean(currentUser);
return ( return (
<TooltipProvider> <TooltipProvider>
+4 -3
View File
@@ -27,7 +27,10 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const creator = useUser(memoData.creator).data; const creator = useUser(memoData.creator).data;
const isArchived = memoData.state === State.ARCHIVED; const isArchived = memoData.state === State.ARCHIVED;
const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser); const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser);
const parentPage = parentPageProp || "/"; const location = useLocation();
const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`) || location.pathname.startsWith("/memos/shares/");
const currentPage = `${location.pathname}${location.search}${location.hash}`;
const parentPage = parentPageProp || (isInMemoDetailPage ? "/" : currentPage);
// Blur content when any tag has blur_content enabled in the instance tag settings. // Blur content when any tag has blur_content enabled in the instance tag settings.
const [showBlurredContent, setShowBlurredContent] = useState(false); const [showBlurredContent, setShowBlurredContent] = useState(false);
@@ -39,8 +42,6 @@ const MemoView: React.FC<MemoViewProps> = (props: MemoViewProps) => {
const openEditor = useCallback(() => setShowEditor(true), []); const openEditor = useCallback(() => setShowEditor(true), []);
const closeEditor = useCallback(() => setShowEditor(false), []); const closeEditor = useCallback(() => setShowEditor(false), []);
const location = useLocation();
const isInMemoDetailPage = location.pathname.startsWith(`/${memoData.name}`) || location.pathname.startsWith("/memos/shares/");
const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0; const showCommentPreview = !isInMemoDetailPage && computeCommentAmount(memoData) > 0;
useEffect(() => { useEffect(() => {
@@ -1,7 +1,7 @@
import { BookmarkIcon } from "lucide-react"; import { ArchiveIcon, BookmarkIcon } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
import i18n from "@/i18n"; import i18n from "@/i18n";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -12,7 +12,6 @@ import MemoActionMenu from "../../MemoActionMenu";
import { ReactionSelector } from "../../MemoReactionListView"; import { ReactionSelector } from "../../MemoReactionListView";
import UserAvatar from "../../UserAvatar"; import UserAvatar from "../../UserAvatar";
import VisibilityIcon from "../../VisibilityIcon"; import VisibilityIcon from "../../VisibilityIcon";
import { useMemoActions } from "../hooks";
import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext"; import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
import type { MemoHeaderProps } from "../types"; import type { MemoHeaderProps } from "../types";
@@ -21,14 +20,37 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false); const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext(); const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext();
const { createTime, updateTime, displayTime: memoDisplayTime, isDisplayingUpdatedTime, relativeTimeFormat } = useMemoViewDerived(); const {
createTime,
updateTime,
displayTime: memoDisplayTime,
isDisplayingUpdatedTime,
isInMemoDetailPage,
relativeTimeFormat,
} = useMemoViewDerived();
const navigateTo = useNavigateTo(); const navigateTo = useNavigateTo();
const navigate = useNavigate();
const handleGotoMemoDetailPage = useCallback(() => { const handleGotoMemoDetailPage = useCallback(() => {
navigateTo(`/${memo.name}`, { state: { from: parentPage } }); navigateTo(`/${memo.name}`, { state: { from: parentPage } });
}, [memo.name, parentPage, navigateTo]); }, [memo.name, parentPage, navigateTo]);
const handleBackToParentPage = useCallback(() => {
const historyState = window.history.state as { idx?: number } | null;
if (typeof historyState?.idx === "number" && historyState.idx > 0) {
navigate(-1);
return;
}
const { unpinMemo } = useMemoActions(memo); navigateTo(parentPage, { preventScrollReset: true });
}, [parentPage, navigate, navigateTo]);
const handleMemoTimeClick = useCallback(() => {
if (isInMemoDetailPage) {
handleBackToParentPage();
return;
}
handleGotoMemoDetailPage();
}, [isInMemoDetailPage, handleBackToParentPage, handleGotoMemoDetailPage]);
const timeValue = isArchived ? ( const timeValue = isArchived ? (
memoDisplayTime?.toLocaleString(i18n.language) memoDisplayTime?.toLocaleString(i18n.language)
@@ -51,14 +73,14 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
<div className="w-full flex flex-row justify-between items-center gap-2"> <div className="w-full flex flex-row justify-between items-center gap-2">
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center"> <div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
{showCreator && creator ? ( {showCreator && creator ? (
<CreatorDisplay creator={creator} displayTime={displayTime} timeTooltip={timeTooltip} onGotoDetail={handleGotoMemoDetailPage} /> <CreatorDisplay creator={creator} displayTime={displayTime} timeTooltip={timeTooltip} onTimeClick={handleMemoTimeClick} />
) : ( ) : (
<TimeDisplay displayTime={displayTime} timeTooltip={timeTooltip} onGotoDetail={handleGotoMemoDetailPage} /> <TimeDisplay displayTime={displayTime} timeTooltip={timeTooltip} onTimeClick={handleMemoTimeClick} />
)} )}
</div> </div>
<div className="flex flex-row justify-end items-center select-none shrink-0 gap-2"> <div className="flex flex-row justify-end items-center select-none shrink-0 gap-2">
{currentUser && !isArchived && ( {currentUser && (
<ReactionSelector <ReactionSelector
className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "block sm:hidden sm:group-hover:block")} className={cn("border-none w-auto h-auto", reactionSelectorOpen && "block!", "block sm:hidden sm:group-hover:block")}
memo={memo} memo={memo}
@@ -66,6 +88,32 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
/> />
)} )}
{showPinned && memo.pinned && (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center rounded-md">
<BookmarkIcon className="w-4 h-auto text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.pinned")}</p>
</TooltipContent>
</Tooltip>
)}
{isArchived && (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center rounded-md">
<ArchiveIcon className="w-4 h-auto text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t("common.archived")}</p>
</TooltipContent>
</Tooltip>
)}
{showVisibility && ( {showVisibility && (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
@@ -79,22 +127,14 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ showCreator, showVisibility, sh
</Tooltip> </Tooltip>
)} )}
{showPinned && memo.pinned && ( <MemoActionMenu
<TooltipProvider> memo={memo}
<Tooltip> readonly={readonly}
<TooltipTrigger asChild> isInMemoDetailPage={isInMemoDetailPage}
<span className="cursor-pointer"> onBack={handleBackToParentPage}
<BookmarkIcon className="w-4 h-auto text-primary" onClick={unpinMemo} /> onViewDetail={handleGotoMemoDetailPage}
</span> onEdit={openEditor}
</TooltipTrigger> />
<TooltipContent>
<p>{t("common.unpin")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<MemoActionMenu memo={memo} readonly={readonly} onEdit={openEditor} />
</div> </div>
</div> </div>
); );
@@ -104,10 +144,10 @@ interface CreatorDisplayProps {
creator: User; creator: User;
displayTime: React.ReactNode; displayTime: React.ReactNode;
timeTooltip: TimeTooltipContent; timeTooltip: TimeTooltipContent;
onGotoDetail: () => void; onTimeClick: () => void;
} }
const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, timeTooltip, onGotoDetail }) => ( const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, timeTooltip, onTimeClick }) => (
<div className="w-full flex flex-row justify-start items-center"> <div className="w-full flex flex-row justify-start items-center">
<Link className="w-auto hover:opacity-80 rounded-md transition-colors" to={`/u/${encodeURIComponent(creator.username)}`} viewTransition> <Link className="w-auto hover:opacity-80 rounded-md transition-colors" to={`/u/${encodeURIComponent(creator.username)}`} viewTransition>
<UserAvatar className="mr-2 shrink-0" avatarUrl={creator.avatarUrl} /> <UserAvatar className="mr-2 shrink-0" avatarUrl={creator.avatarUrl} />
@@ -124,7 +164,7 @@ const CreatorDisplay: React.FC<CreatorDisplayProps> = ({ creator, displayTime, t
<button <button
type="button" type="button"
className="w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer hover:opacity-80 transition-colors text-left" className="w-auto -mt-0.5 text-xs leading-tight text-muted-foreground select-none cursor-pointer hover:opacity-80 transition-colors text-left"
onClick={onGotoDetail} onClick={onTimeClick}
> >
{displayTime} {displayTime}
</button> </button>
@@ -151,15 +191,15 @@ const TimeTooltip = ({ children, content }: { children: React.ReactElement; cont
interface TimeDisplayProps { interface TimeDisplayProps {
displayTime: React.ReactNode; displayTime: React.ReactNode;
timeTooltip: TimeTooltipContent; timeTooltip: TimeTooltipContent;
onGotoDetail: () => void; onTimeClick: () => void;
} }
const TimeDisplay: React.FC<TimeDisplayProps> = ({ displayTime, timeTooltip, onGotoDetail }) => ( const TimeDisplay: React.FC<TimeDisplayProps> = ({ displayTime, timeTooltip, onTimeClick }) => (
<TimeTooltip content={timeTooltip}> <TimeTooltip content={timeTooltip}>
<button <button
type="button" type="button"
className="w-auto text-sm leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors text-left" className="w-auto text-sm leading-tight text-muted-foreground select-none cursor-pointer hover:text-foreground transition-colors text-left"
onClick={onGotoDetail} onClick={onTimeClick}
> >
{displayTime} {displayTime}
</button> </button>
@@ -1,3 +1,2 @@
export { useImagePreview } from "./useImagePreview"; export { useImagePreview } from "./useImagePreview";
export { useMemoActions } from "./useMemoActions";
export { useMemoHandlers } from "./useMemoHandlers"; export { useMemoHandlers } from "./useMemoHandlers";
@@ -1,13 +0,0 @@
import { useUpdateMemo } from "@/hooks/useMemoQueries";
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
export const useMemoActions = (memo: Memo) => {
const { mutateAsync: updateMemo } = useUpdateMemo();
const unpinMemo = async () => {
if (!memo.pinned) return;
await updateMemo({ update: { name: memo.name, pinned: false }, updateMask: ["pinned"] });
};
return { unpinMemo };
};
+1 -1
View File
@@ -16,7 +16,7 @@ import useMediaQuery from "@/hooks/useMediaQuery";
import { User_Role } from "@/types/proto/api/v1/user_service_pb"; import { User_Role } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
const GITHUB_COMMIT_URL_PREFIX = "https://github.com/usememos/memos/commit/"; const GITHUB_COMMIT_URL_PREFIX = "https://git.anian.cc/anian/memos/commit/";
const isCommitSha = (commit: string) => /^[0-9a-f]{7,40}$/i.test(commit); const isCommitSha = (commit: string) => /^[0-9a-f]{7,40}$/i.test(commit);