From c4176b4ef1c122d2aaa14dc899acfa9b37619fac Mon Sep 17 00:00:00 2001 From: Johnny Date: Sat, 7 Feb 2026 16:03:52 +0800 Subject: [PATCH 1/4] fix: videos attachment --- .../components/metadata/AttachmentList.tsx | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/web/src/components/MemoView/components/metadata/AttachmentList.tsx b/web/src/components/MemoView/components/metadata/AttachmentList.tsx index eb976182c..bb3e8a0f3 100644 --- a/web/src/components/MemoView/components/metadata/AttachmentList.tsx +++ b/web/src/components/MemoView/components/metadata/AttachmentList.tsx @@ -1,5 +1,5 @@ import { FileIcon, PaperclipIcon } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; import { formatFileSize, getFileTypeLabel } from "@/utils/format"; @@ -11,13 +11,18 @@ interface AttachmentListProps { attachments: Attachment[]; } +// Type guards for attachment types +const isImageAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "image/*"; +const isVideoAttachment = (attachment: Attachment): boolean => getAttachmentType(attachment) === "video/*"; +const isMediaAttachment = (attachment: Attachment): boolean => isImageAttachment(attachment) || isVideoAttachment(attachment); + +// Separate attachments into media (images/videos) and documents const separateMediaAndDocs = (attachments: Attachment[]): { media: Attachment[]; docs: Attachment[] } => { const media: Attachment[] = []; const docs: Attachment[] = []; for (const attachment of attachments) { - const attachmentType = getAttachmentType(attachment); - if (attachmentType === "image/*" || attachmentType === "video/*") { + if (isMediaAttachment(attachment)) { media.push(attachment); } else { docs.push(attachment); @@ -55,27 +60,39 @@ const DocumentItem = ({ attachment }: { attachment: Attachment }) => { ); }; -const MediaGrid = ({ attachments, onImageClick }: { attachments: Attachment[]; onImageClick: (url: string) => void }) => ( +interface MediaItemProps { + attachment: Attachment; + onImageClick: (url: string) => void; +} + +const MediaItem = ({ attachment, onImageClick }: MediaItemProps) => { + const isImage = isImageAttachment(attachment); + + const handleClick = () => { + if (isImage) { + onImageClick(getAttachmentUrl(attachment)); + } + }; + + return ( +
+ +
+ ); +}; + +interface MediaGridProps { + attachments: Attachment[]; + onImageClick: (url: string) => void; +} + +const MediaGrid = ({ attachments, onImageClick }: MediaGridProps) => (
{attachments.map((attachment) => ( -
onImageClick(getAttachmentUrl(attachment))} - > -
- - {getAttachmentType(attachment) === "video/*" && ( -
-
- - - -
-
- )} -
-
+ ))}
); @@ -98,18 +115,20 @@ const AttachmentList = ({ attachments }: AttachmentListProps) => { mimeType: undefined, }); - const { media: mediaItems, docs: docItems } = separateMediaAndDocs(attachments); + const { media: mediaItems, docs: docItems } = useMemo(() => separateMediaAndDocs(attachments), [attachments]); + + // Pre-compute image URLs for preview dialog to avoid filtering on every click + const imageAttachments = useMemo(() => mediaItems.filter(isImageAttachment), [mediaItems]); + const imageUrls = useMemo(() => imageAttachments.map(getAttachmentUrl), [imageAttachments]); if (attachments.length === 0) { return null; } const handleImageClick = (imgUrl: string) => { - const imageAttachments = mediaItems.filter((a) => getAttachmentType(a) === "image/*"); - const imgUrls = imageAttachments.map((a) => getAttachmentUrl(a)); - const index = imgUrls.findIndex((url) => url === imgUrl); + const index = imageUrls.findIndex((url) => url === imgUrl); const mimeType = imageAttachments[index]?.type; - setPreviewImage({ open: true, urls: imgUrls, index, mimeType }); + setPreviewImage({ open: true, urls: imageUrls, index, mimeType }); }; return ( From d9dc5be20085ccee2c14d29d6aa5030940974fcb Mon Sep 17 00:00:00 2001 From: Johnny Date: Sun, 8 Feb 2026 19:23:34 +0800 Subject: [PATCH 2/4] fix: replace echo.NewHTTPError with status.Errorf --- server/router/api/v1/memo_relation_service.go | 8 ++++---- server/router/api/v1/memo_service.go | 2 +- server/router/api/v1/shortcut_service.go | 6 +++--- server/router/api/v1/user_service.go | 6 ++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/server/router/api/v1/memo_relation_service.go b/server/router/api/v1/memo_relation_service.go index b70088b2a..25d97f24a 100644 --- a/server/router/api/v1/memo_relation_service.go +++ b/server/router/api/v1/memo_relation_service.go @@ -100,7 +100,7 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List MemoFilter: &memoFilter, }) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to list memo relations: %v", err) } for _, raw := range tempList { relation, err := s.convertMemoRelationFromStore(ctx, raw) @@ -114,7 +114,7 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List MemoFilter: &memoFilter, }) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to list related memo relations: %v", err) } for _, raw := range tempList { relation, err := s.convertMemoRelationFromStore(ctx, raw) @@ -133,7 +133,7 @@ func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.List func (s *APIV1Service) convertMemoRelationFromStore(ctx context.Context, memoRelation *store.MemoRelation) (*v1pb.MemoRelation, error) { memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.MemoID}) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err) } memoSnippet, err := s.getMemoContentSnippet(memo.Content) if err != nil { @@ -141,7 +141,7 @@ func (s *APIV1Service) convertMemoRelationFromStore(ctx context.Context, memoRel } relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.RelatedMemoID}) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to get related memo: %v", err) } relatedMemoSnippet, err := s.getMemoContentSnippet(relatedMemo.Content) if err != nil { diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index f5d250a16..954a665c5 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -345,7 +345,7 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err) } if memo == nil { return nil, status.Errorf(codes.NotFound, "memo not found") diff --git a/server/router/api/v1/shortcut_service.go b/server/router/api/v1/shortcut_service.go index b2057dbe8..aa2c4f6f7 100644 --- a/server/router/api/v1/shortcut_service.go +++ b/server/router/api/v1/shortcut_service.go @@ -62,7 +62,7 @@ func (s *APIV1Service) ListShortcuts(ctx context.Context, request *v1pb.ListShor Key: storepb.UserSetting_SHORTCUTS, }) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err) } if userSetting == nil { return &v1pb.ListShortcutsResponse{ @@ -186,7 +186,7 @@ func (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateS _, err = s.Store.UpsertUserSetting(ctx, userSetting) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) } return &v1pb.Shortcut{ @@ -313,7 +313,7 @@ func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteS } _, err = s.Store.UpsertUserSetting(ctx, userSetting) if err != nil { - return nil, err + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) } return &emptypb.Empty{}, nil diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go index 794949cdd..0956000e3 100644 --- a/server/router/api/v1/user_service.go +++ b/server/router/api/v1/user_service.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "encoding/hex" "fmt" - "net/http" "regexp" "strconv" "strings" @@ -13,7 +12,6 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/ast" - "github.com/labstack/echo/v4" "github.com/pkg/errors" "golang.org/x/crypto/bcrypt" "google.golang.org/grpc/codes" @@ -163,7 +161,7 @@ func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserR passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost) if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err) + return nil, status.Errorf(codes.Internal, "failed to generate password hash: %v", err) } user, err := s.Store.CreateUser(ctx, &store.User{ @@ -272,7 +270,7 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR case "password": passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost) if err != nil { - return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err) + return nil, status.Errorf(codes.Internal, "failed to generate password hash: %v", err) } passwordHashStr := string(passwordHash) update.PasswordHash = &passwordHashStr From f827296d6ba51e7ac40822f04c3fe0d1d935d885 Mon Sep 17 00:00:00 2001 From: Johnny Date: Sun, 8 Feb 2026 19:46:03 +0800 Subject: [PATCH 3/4] chore: fix broken links --- README.md | 2 +- web/src/components/CreateShortcutDialog.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f36e6c9f2..ac644dbbe 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Don't want to install yet? Try our [live demo](https://demo.usememos.com/) first - **Kubernetes** - Helm charts and manifests available - **Build from Source** - For development and customization -See our [installation guide](https://usememos.com/docs/installation) for detailed instructions. +See our [installation guide](https://usememos.com/docs/deploy) for detailed instructions. ## Contributing diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index 47d440499..b32d297e9 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -126,6 +126,21 @@ function CreateShortcutDialog({ open, onOpenChange, shortcut: initialShortcut, o onChange={onShortcutFilterChange} /> +
+

{t("common.learn-more")}:

+ +